Linux系统编程Day14 -- 进程控制(了解)
本文深入探讨Linux进程管理中的fork()系统调用,包括其工作原理、执行流程和应用场景。文章详细解析了fork()如何创建子进程,包括虚拟地址空间复制、写时复制机制以及父子进程的区别。同时介绍了进程终止的多种方式(正常/异常终止)及其资源回收机制,并重点讲解了进程等待的概念与方法,包括wait/waitpid等系统调用及其参数配置。此外,文章还提供了避免僵尸进程的实用策略,如信号处理、非阻塞轮
往期内容回顾
前言
在操作系统的世界里,进程是资源分配和调度的最小单位。无论是服务器的多任务处理,还是桌面应用的多窗口运行,都是通过进程的创建、调度、终止来完成的。
而在 Linux 中,fork() 是创建新进程最核心的系统调用之一。掌握 fork,不仅可以让我们更深入理解父子进程、地址空间、进程管理,还为多进程编程打下坚实基础。
主要内容介绍
本文将围绕以下几个方面展开:
-
进程控制的核心概念(父进程、子进程、进程状态)
-
fork() 的原理与执行流程
-
父子进程的地址空间关系
-
通过 fork 创建进程的实战示例
-
了解进程终止的几种方式
-
进程等待的概念理解
-
获取进程等待信息和进程回收的策略
一. 进程控制的核心概念
在 Linux 中,进程控制包括 创建进程、终止进程、等待进程、进程间通信(IPC) 等内容。
|
核心概念 |
说明 |
|---|---|
|
进程控制块 (PCB) |
操作系统为每个进程维护的核心数据结构,包含进程状态、程序计数器、寄存器内容、内存映射、打开的文件等信息。 |
|
进程状态切换 |
创建、新建、就绪、运行、等待、终止等状态的转换过程。 |
|
进程调度 |
决定哪个进程获得 CPU 执行权,包括调度策略(先来先服务、时间片轮转、优先级等)。 |
|
进程通信 (IPC) |
进程间交换数据的机制,如管道、共享内存、消息队列、信号量、套接字等。 |
|
同步与互斥 |
控制进程在共享资源上的访问顺序,防止竞争条件和死锁。 |
|
资源分配与回收 |
记录和管理进程占用的内存、I/O 设备、文件句柄等资源。 |
例如使用fork()来创建进程:
-
父进程(Parent Process):调用 fork 创建新进程的进程。
-
子进程(Child Process):由 fork 复制父进程创建的新进程,拥有独立的 PID。
-
进程状态:就绪(Ready)、运行(Running)、阻塞(Blocked),由内核调度器控制切换。
二. fork() 的原理与执行流程
fork() 是 Linux 提供的系统调用,用于创建一个新进程。
执行特点:
分配新的内存块和内核数据(PCB)给子进程
-
fork 会复制当前进程的整个虚拟地址空间(代码段、数据段、堆、栈、文件描述符等)给子进程。
-
父子进程的虚拟地址空间相同,但物理内存采用写时复制(Copy-On-Write, COW),只有在写入时才分配新的物理页。
-
fork 会返回两次:在父进程中返回子进程 PID,在子进程中返回 0,开始调度器调度。
回到我们之前的关于fork的使用代码:
#include <stdatomic.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> int globalvalue = 100; int main(){ pid_t id = fork(); int cnt = 0; if(id < 0){ printf("fork error!/n"); } else if (id == 0) { while (1) { printf("Child process: pid = %d, ppid = %d, global value = %d, &globalvalue = %p\n",getpid(),getppid(),globalvalue,&globalvalue); sleep(1); cnt++; if(cnt == 10) { globalvalue = 300; printf("The Child process has change the globalvalue!\n"); } } } else { while(1){ printf("Father process: pid = %d,ppid = %d, global value = %d, &globalvalue = %p\n",getpid(),getppid(),globalvalue,&globalvalue); sleep(2); } } }
1、如何理解fork()的两个返回值呢 ?
fork 会复制当前进程的整个虚拟地址空间(代码段、数据段、堆、栈、文件描述符等)给子进程,
复制 PCB(进程控制块),并做必要修改(如 PID、PPID, 然后让父、子从同一行代码继续执行 —— 也就是 fork() 这一行。
但系统必须让父子进程能区分身份,于是 fork() 的返回值:
-
父进程中:返回 子进程的 PID(> 0)
-
子进程中:返回 0
-
失败时(没创建成功):返回 -1
if (id == 0) { /* 子进程执行 */ }
else if (id > 0) { /* 父进程执行 */ }
2、 如何理解fork返回之后,给父进程返回子进程的pid,给子进程返回0呢?
这是内核在 fork() 系统调用内部主动赋值的,不是说同一个变量 magically 变成两个值。
具体过程:
-
父进程发起 fork() 系统调用 → 内核分配新 PCB 给子进程
-
在父进程返回前,内核在父进程的寄存器/栈空间写入子进程 PID
-
在子进程返回前,内核在子进程的寄存器/栈空间写入 0
-
然后分别调度两个进程继续执行 fork() 之后的代码
也就是说:
fork() 后父子进程共享 代码段,但数据段、堆、栈独立(写时复制)
-
id 在父、子进程里是不同内存里的两个变量副本(COW 之后它们看似同一份)
-
内核在 fork() 结束时,分别给这两份变量写了不同值
3、如何理解同一个id值,怎么可能会保存两个不同的值呢?并且让if,else if 同时执行?
关键点:id 在父进程和子进程里不是同一份内存(写时拷贝)
fork() 时复制了整个地址空间(包括局部变量 id 的栈空间)
-
所以父进程有它的 id,子进程有它的 id,只是初始值在内核写入前是一样的
-
内核在 fork() 返回时修改了这两份变量的值
-
父进程看到的 id 是子 PID → 走 else 分支
-
子进程看到的 id 是 0 → 走 if 分支结果就是两段几乎相同的代码在两个进程里分别执行,看起来就像同一个变量能存两份值,其实是两份不同的内存。
3、fork的常规用法,应用场景
简单创建子进程
只做简单任务分离,比如父进程打印信息,子进程执行其他操作:
父子分工
常用模式:父进程做管理,子进程做计算或处理任务:
创建守护进程 / 后台进程
通过 fork() + setsid() 可以让子进程脱离终端,常用于服务程序或后台守护进程:
多进程并行处理
当任务可以并行时,可以用 fork() 创建多个子进程同时工作
4、应用场景
-
任务并行化
Web 服务器:每个请求 fork 一个子进程处理
-
数据处理:大批量计算可拆分成多个子进程
-
-
后台守护进程
-
日志记录服务
-
系统监控服务
-
-
父子进程分工
-
父进程监控、管理
-
子进程执行具体任务
-
-
创建进程池
-
预创建一定数量的子进程,提高任务处理效率
-
注意事项
-
fork() 会复制父进程的全部内存,频繁调用可能浪费资源
-
需要处理 僵尸进程:父进程要调用 wait() 或 waitpid() 回收子进程
-
文件描述符会被子进程继承,需要注意打开/关闭
三、进程终止的概念
进程终止是指一个进程完成了它的任务或因为异常需要退出,操作系统释放它占用的资源(内存、文件描述符、PID 等)。
进程终止通常分为两种方式:
-
正常终止(正常退出)
-
异常终止(被信号或错误终止)
二、C/C++程序中的进程终止方式
1. 正常终止
-
return 从 main() 返回
int main() { printf("Program running\n"); return 0; // 程序正常退出 }底层行为:
内核回收进程资源
返回值 0 表示成功,非 0 表示异常退出
内核向父进程发送 SIGCHLD 信号(父进程可调用 wait() 收集子进程状态)
exit() 系列函数
#include <stdlib.h> int main() { printf("Program running\n"); exit(0); // 等价于 return 0 }特点:
exit() 会调用 清理函数(atexit() 注册的函数)
刷新 缓冲区,关闭打开的文件描述符
返回状态给父进程
_exit() / _Exit()
#include <unistd.h> int main() { _exit(0); // 立即终止,不调用清理函数 }区别:
不调用 atexit 注册的函数
不刷新标准 I/O 缓冲
通常在 fork() 后的子进程中使用
如何设定c语言main函数的返回值呢?如果不关心进程退出,return0就行
如果我们是要关心进程退出码的时候,要返回特定的数据表明特定的错误,
进程退出的时候,返回特定的退出码用于标定进程执行的结果是否正确?错误在哪?
如何返回特定的错误码呢?利用string.h的strerror函数查看错误码(代码跑完了)
#include <stdio.h> #include <string.h> #include <unistd.h> int main(){ for(int i = 0;i<200;i++){ printf("%d:%s\n",i,strerror(errnum: i)); } }
0:Undefined error: 0 1:Operation not permitted 2:No such file or directory 3:No such process 4:Interrupted system call 5:Input/output error 6:Device not configured 7:Argument list too long 8:Exec format error 9:Bad file descriptor 10:No child processes 11:Resource deadlock avoided 12:Cannot allocate memory 13:Permission denied 14:Bad address 15:Block device required 16:Resource busy 17:File exists 18:Cross-device link 19:Operation not supported by device 20:Not a directory 21:Is a directory 22:Invalid argument 23:Too many open files in system 24:Too many open files 25:Inappropriate ioctl for device 26:Text file busy 27:File too large 28:No space left on device 29:Illegal seek 30:Read-only file system 31:Too many links 32:Broken pipe 33:Numerical argument out of domain 34:Result too large 35:Resource temporarily unavailable 36:Operation now in progress 37:Operation already in progress 38:Socket operation on non-socket 39:Destination address required 40:Message too long 41:Protocol wrong type for socket 42:Protocol not available 43:Protocol not supported 44:Socket type not supported 45:Operation not supported 46:Protocol family not supported 47:Address family not supported by protocol family 48:Address already in use 49:Can't assign requested address 50:Network is down 51:Network is unreachable 52:Network dropped connection on reset 53:Software caused connection abort 54:Connection reset by peer 55:No buffer space available 56:Socket is already connected 57:Socket is not connected 58:Can't send after socket shutdown 59:Too many references: can't splice 60:Operation timed out 61:Connection refused 62:Too many levels of symbolic links 63:File name too long 64:Host is down 65:No route to host 66:Directory not empty 67:Too many processes 68:Too many users 69:Disc quota exceeded 70:Stale NFS file handle 71:Too many levels of remote in path 72:RPC struct is bad 73:RPC version wrong 74:RPC prog. not avail 75:Program version wrong 76:Bad procedure for program 77:No locks available 78:Function not implemented 79:Inappropriate file type or format 80:Authentication error 81:Need authenticator 82:Device power is off 83:Device error 84:Value too large to be stored in data type 85:Bad executable (or shared library) 86:Bad CPU type in executable 87:Shared library version mismatch 88:Malformed Mach-o file 89:Operation canceled 90:Identifier removed 91:No message of desired type 92:Illegal byte sequence 93:Attribute not found 94:Bad message 95:EMULTIHOP (Reserved) 96:No message available on STREAM 97:ENOLINK (Reserved) 98:No STREAM resources 99:Not a STREAM 100:Protocol error 101:STREAM ioctl timeout 102:Operation not supported on socket 103:Policy not found 104:State not recoverable 105:Previous owner died 106:Interface output queue is full 107:Capabilities insufficient
举个例子:
ls 21312412414 ls: 21312412414: No such file or directory echo $?
2. 异常终止(代码没跑完)
调用信号导致终止:如 SIGKILL、SIGTERM
#include <signal.h> kill(getpid(), SIGTERM); // 给自己发送终止信号未处理的异常:如除零、段错误
int main() { int *p = NULL; *p = 10; // 段错误,进程被内核终止 }异常终止行为:
内核释放进程资源
向父进程发送 SIGCHLD 信号
可以在父进程通过 wait() 或 waitpid() 获取异常终止信息
3、进程如何退出的?
一、应用层触发退出
在 C/C++ 程序里,进程可以通过几种方式退出:
1、return 从 main() 返回
编译器会把 return 转换为对 exit() 的调用(隐式)。
2、显式调用退出函数
exit(status):正常退出,执行清理动作(刷新缓冲区、调用 atexit() 注册的函数、关闭文件等)。
int sum(int num){ int ret = 0; for(int i = 0;i<num;i++){ ret+=i; } exit(1); printf("Sum(100) = %d\n",ret); } int main(){ int num = 100; sum(num: 100); return 0; }不会打印最后的求和值,因为exit已经退出了
_exit(status) / _Exit(status):立即退出,不做清理,直接进入内核(多用于 fork() 后的子进程)。
_exit和exit的区别?
-
exit()
-
属于 C 标准库函数(在 stdlib.h 中声明),由 libc 提供。
-
它不是直接的系统调用,而是库函数中封装了更多的清理逻辑,最终会调用 _exit()(或 _Exit())。
-
-
_exit()
-
属于 系统调用接口(Linux 对应 sys_exit,macOS 对应 exit Mach trap)。
-
在 <unistd.h> 中声明,直接请求内核终止进程,不经过 libc 的收尾清理逻辑。
-
exit终止进程,主动刷新缓冲区
_exit终止进程,不会刷新缓冲区
缓冲区:用户级的缓冲区,不在操作系统内。
以 exit() 为例,它会做以下工作(用户态):
调用已注册的 atexit() 回调函数。
刷新所有打开的标准 I/O 缓冲区(stdout、stderr 等)。
关闭所有用 fopen() 打开的文件(底层会调用 close())。
调用系统调用 _exit() 进入内核,正式结束进程。
如果直接调用 _exit(),会跳过 1-3,直接到 4。
测试代码:
int main(){ printf("Hello World!\n"); sleep(6); _exit(32); //_exit(32); }
3、被信号终止
- 例如 SIGKILL、SIGTERM、SIGSEGV(段错误)等。
4、内核态的进程终止流程
当进程执行 _exit(status) 系统调用后,内核会:
-
释放进程占用的用户态资源
-
释放用户空间内存(代码段、堆、栈、数据段)。
-
释放文件描述符、socket、共享内存等。
-
-
更新进程控制块(PCB)状态
-
把进程状态改为 ZOMBIE(僵尸状态),保留退出码和少量信息,供父进程读取。
-
-
向父进程发送 SIGCHLD 信号
-
父进程收到信号后,可以用 wait() 或 waitpid() 获取退出状态。
-
-
等待父进程回收
-
父进程调用 wait() → 内核释放 PCB,进程完全消失。
-
如果父进程先退出,孤儿进程会被 init 进程(PID 1)收养,并由它回收。
-
三、进程等待
-
1、进程状态Z: 僵尸状态:
通过进程等待的方式来回收僵尸进程
一、进程为什么要“等待”?
-
本质:等待是一种同步。父进程需要在合适的时机获取子进程的退出结果,或等一个外部事件(I/O、信号、定时器)发生,回收子进程资源。
-
目的:
资源回收:子进程退出时释放大部分资源,但其退出状态需父进程读取,否则会变成僵尸进程(zombie)。
顺序控制:需要在子进程结束后再继续下一步。 - 并发正确性:避免竞态条件(先后次序不确定导致错误)。
测试代码:
int main(){ pid_t id = fork(); if(id == 0){ int cnt = 10; while (cnt > 0) { printf("Child process, ID = %d, PID = %d, PPID = %d\n",id,getpid(),getppid()); cnt --; sleep(1); } } sleep(15); pid_t ret = wait(NULL); if(id >0){ printf("wait success: %d\n",ret); } sleep(5); }
二、与进程状态的关系(调度视角)
典型用户态可感知的三态:就绪 → 运行 → 阻塞(等待)。
-
等待/阻塞(Sleeping/Waiting):进程调用诸如 wait()、read()、accept()、futex() 等阻塞式系统调用后,内核把它放到相应的等待队列,直到条件满足被唤醒。
-
可中断/不可中断睡眠:wait() 等一般是可被信号打断的睡眠;磁盘 I/O 等可能进入不可中断睡眠(D 状态)。
三、获取进程等待信息
常见“等待子进程”系统调用族:
1) 阻塞等待一个子进程(最简单)
|
接口 |
说明 |
|---|---|
|
pid_t wait(int *wstatus) |
阻塞等待任一子进程退出 |
|
pid_t waitpid(pid_t pid, int *wstatus, int options) |
等待特定子进程(或任一,取决于 pid),支持非阻塞等选项精确控制:指定 PID、同组、任意 |
|
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options) |
更细粒度,直接给出 siginfo_t,可等待退出/停止/继续 |
|
兼容接口 wait3/wait4 |
非标准或历史接口,附带资源使用统计 |
waitpid 的 pid 语义:
-
> 0:等待这个 确切 PID
-
-1:等待 任意 子进程(等价于 wait,但可选非阻塞/更多选项)
-
0:等待 同进程组 的任意子进程
-
< -1:等待 进程组 ID = -pid 的任意子进程
返回值与状态宏(最常用的几组)
-
返回值:
-
> 0 :被回收的子进程 PID
-
0 :仅在 waitpid(..., WNOHANG) 且当前无状态变化时返回
-
-1:出错(errno 判因,如 ECHILD 没有子进程、EINTR 被信号中断)
-
pid_t pid = fork(); if (pid == 0) { // child _exit(42); } else if (pid > 0) { // parent int status; pid_t r = waitpid(pid, &status, 0); // 阻塞直到该子进程退出 if (r == pid && WIFEXITED(status)) { printf("child exit code=%d\n", WEXITSTATUS(status)); } }
2) 非阻塞轮询(WNOHANG)
int status; pid_t r = waitpid(-1, &status, WNOHANG); // -1 表示任意子进程 if (r == 0) { // 目前还没有已退出的子进程,做点别的事再来查 } else if (r > 0) { // 处理退出结果 }适合需要持续做其他工作的父进程。常见搭配:事件循环或定时器重复检查。
3) 信号驱动(处理 SIGCHLD)
static void on_sigchld(int sig) { int saved = errno; while (1) { // 可能一次信号对应多个子进程退出 int status; pid_t r = waitpid(-1, &status, WNOHANG); if (r <= 0) break; // 处理 r 的退出状态... } errno = saved; } int main() { struct sigaction sa = {0}; sa.sa_handler = on_sigchld; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // 可选:不关心 STOP/CONT sigaction(SIGCHLD, &sa, NULL); // 正常业务逻辑... }优点:无需主动轮询; 要点:handler 里用 循环 把所有已退出的子进程都 waitpid(-1, …, WNOHANG) 掉,避免遗漏。
4) 等待所有子进程
int status; pid_t r; while ((r = wait(&status)) > 0) { // 每迭代回收一个 } if (r == -1 && errno == ECHILD) { // 没有子进程了 }
四、避免僵尸进程的几种策略
-
父进程始终调用 wait/waitpid 回收
-
安装 SIGCHLD 处理器,在其中循环 waitpid(-1, …, WNOHANG)
-
对于短命后台子进程,双重 fork(double-fork) 把最终子进程交给 init/systemd 收养
-
在某些场景可用 SIGCHLD 的 SA_NOCLDWAIT 标志(或 prctl(PR_SET_CHILD_SUBREAPER) 进程子收割器),不过要明白副作用并确保可移植性
更多推荐



所有评论(0)