进程控制:从创建到终结的完整指南

进程控制:从创建到终结的完整指南

文章目录

一、进程创建:从`fork`开始的 "分身术"1.1 `fork`函数:一次调用,两次返回1.2 写时拷贝:高效的内存共享策略1.3 `fork`的常见用法与失败原因

二、进程终止:优雅结束与资源释放2.1 进程退出的三种场景2.2 正常终止的三种方式2.3 退出码:进程的 "遗言"

三、进程等待:避免僵尸进程,获取退出状态3.1 为什么需要进程等待?3.2 两种等待函数:`wait`与`waitpid`3.2.1 `wait`函数:简单的阻塞等待3.2.2 `waitpid`函数:更灵活的等待

3.3 解析`status`参数:子进程的 "最后消息"3.4 阻塞与非阻塞等待

四、进程程序替换:让子进程执行全新程序4.1 替换原理:不换进程换 "内容"4.2 `exec`函数簇:6 个函数的命名密码4.3 替换后的注意事项

总结:进程控制的核心逻辑

进程是操作系统的基本执行单位,而进程控制则是管理进程生命周期的核心技术。无论是创建新进程、终止运行中的进程,还是回收子进程资源、替换进程执行的程序,都离不开关键的系统调用。本文将以通俗易懂的方式,带你梳理进程控制的四大核心环节:

进程创建、

进程终止、

进程等待和

程序替换,让你轻松掌握

fork、

wait、

exec等核心函数的工作原理与实践技巧。

一、进程创建:从fork开始的 “分身术”

当你需要一个进程 “分身” 执行不同任务时,fork函数就是实现这一功能的关键。它能从已存在的进程(父进程)中创建一个全新的进程(子进程),二者如同 “双胞胎” 却又各自独立。

1.1 fork函数:一次调用,两次返回

fork函数的神奇之处在于:一次调用会产生两个返回值。父进程会得到子进程的 PID(进程 ID),而子进程则返回 0。如果调用失败,返回 - 1。

#include

pid_t fork(void); // 父进程返回子进程PID,子进程返回0,失败返回-1

来看一个简单例子:

int main() {

printf("Before: pid is %d\n", getpid()); // 父进程ID

pid_t pid = fork();

if (pid == -1) { perror("fork failed"); return 1; }

printf("After: pid is %d, fork return %d\n", getpid(), pid);

return 0;

}

运行结果会出现一行 “Before” 和两行 “After”:

Before: pid is 1234

After: pid is 1234, fork return 1235 // 父进程输出

After: pid is 1235, fork return 0 // 子进程输出

为什么子进程不打印 “Before”?

因为fork是在 “Before” 之后调用的,子进程从fork返回后才开始执行,相当于复制了父进程在fork时刻的执行状态。所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

1.2 写时拷贝:高效的内存共享策略

你可能会疑惑:子进程是完全复制父进程的内存吗?如果是这样,内存消耗也太大了。实际上,操作系统采用了写时拷贝(Copy-On-Write) 技术优化这一过程:

未写入时:父子进程共享同一份代码和数据(只读),不占用额外内存。需要写入时:操作系统才会为修改的部分创建副本,仅复制被修改的数据,避免不必要的内存浪费。

修改前:父子进程的页表(记录虚拟内存与物理内存映射),都指向同一块物理内存(粉色、蓝色块 ),且标记为“只读”,防止直接修改共享内容。修改后:假设父进程要改“页表项100”对应的数据,系统会拷贝一份物理内存(新蓝色块 ) 给父进程,父进程页表指向新拷贝,子进程仍指向原物理内存。这样父子数据分离,互不干扰。

这种 “延时复制” 策略大幅提高了内存利用率,也是进程独立性的技术保障(父子进程修改数据互不影响)。

1.3 fork的常见用法与失败原因

fork的典型应用场景有两个:

父子分工:父进程等待客户端请求,子进程处理具体请求(如 Web 服务器)。执行新程序:子进程通过fork创建后,调用exec系列函数执行全新程序(后续会详细讲解)。

fork失败的情况较少见,主要原因包括:

系统进程数量超过上限(如 Linux 的pid_max限制)。普通用户创建的进程数超过系统对该用户的限制。

二、进程终止:优雅结束与资源释放

进程终止的本质是释放系统资源(包括内核数据结构、内存、文件描述符等)。无论是正常结束还是异常崩溃,进程最终都会走向终止。

2.1 进程退出的三种场景

正常结束,结果正确:如程序完成计算并输出正确结果。正常结束,结果错误:如逻辑错误导致计算结果错误,但程序未崩溃。异常终止:如被信号杀死(如ctrl+c触发的SIGINT信号)、访问非法内存等。

2.2 正常终止的三种方式

方式特点适用场景return n(从main函数)等同于exit(n),由运行时库处理主程序退出exit(int status)先执行清理函数(atexit或on_exit定义),刷新缓冲区,再调用_exit需要释放资源的正常退出_exit(int status)直接终止进程,不刷新缓冲区,不执行清理函数紧急退出(如内核态)

关键区别:exit会确保缓冲区数据写入文件,而_exit直接丢弃缓冲区数据。例如:

// 使用exit:会打印"hello"(缓冲区被刷新)

printf("hello");

exit(0);

// 使用_exit:不会打印"hello"(缓冲区数据被丢弃)

printf("hello");

_exit(0);

2.3 退出码:进程的 “遗言”

进程终止时会返回一个退出码(status),用于告知父进程执行结果。Linux 中:

退出码为0表示执行成功。非 0 表示执行失败(具体含义由程序定义)。

可以通过echo $?在 Shell 中查看上一个进程的退出码,常见退出码及含义如下:

退出码含义示例0成功执行ls命令正常列出文件1通用错误无权限执行命令、除以 0127命令未找到执行不存在的程序130被SIGINT(ctrl+c)终止手动中断运行中的程序143被SIGTERM终止kill命令默认信号255/*退出码超过了零到255的范围,因此重新计算void _exit(int status),虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现 返回值是255。

通过strerror(int errnum)函数可获取退出码对应的描述文本,例如strerror(1)返回 “Operation not permitted”。

三、进程等待:避免僵尸进程,获取退出状态

如果父进程不处理子进程的终止,子进程会变成僵尸进程(Zombie)—— 保留 PID 和退出状态,占用系统资源且无法被杀死。进程等待就是父进程回收子进程资源、获取退出状态的关键机制。

3.1 为什么需要进程等待?

之前的文章提到,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,我们需要知道,父进程派给子进程的任务完成的如何。如,子进程运行完成,结果对还是 不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

3.2 两种等待函数:wait与waitpid

3.2.1 wait函数:简单的阻塞等待

#include

#include

pid_t wait(int *status);

// 返回值:成功返回子进程PID,失败返回-1

// 参数:输出型参数,获取子进程退出状态,不关心则可设为NULL

wait会阻塞等待任意子进程终止,并回收其资源。例如:

pid_t pid = fork();

if (pid == 0) { exit(10); } // 子进程退出码10

else {

int status;

wait(&status); // 等待子进程

if (WIFEXITED(status)) { // 判断是否正常退出

printf("子进程退出码:%d\n", WEXITSTATUS(status)); // 输出10

}

}

3.2.2 waitpid函数:更灵活的等待

waitpid是wait的增强版,支持指定等待的子进程、非阻塞等待等:

pid_t waitpid(pid_t pid, int *status, int options);

参数含义pidpid=-1:等待任意子进程(同wait);pid>0:等待 PID 为pid的子进程status同wait,通过宏解析退出状态(如WIFEXITED、WEXITSTATUS)options0:阻塞等待;WNOHANG:非阻塞等待(若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。)

返回值:

当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

“如果子进程已经退出,调用 wait / waitpid 时,会立即返回,释放资源,拿到子进程退出信息”

比如:子进程早就跑完、退出了,父进程这时候调用 wait ,会 立刻得到结果(不用等),还能拿到子进程的退出状态(比如 exit(n) 里的 n ),同时系统会回收子进程占的资源。

“如果任意时刻调用 wait / waitpid ,子进程还在正常运行,则父进程可能阻塞”

比如:子进程还在忙活着(没执行 exit ),父进程调用 wait ,就会 卡住(阻塞),直到子进程退出,父进程才会“被唤醒”,继续往下执行。

“如果不存在该子进程,则立即出错返回”

比如:父进程要等的子进程压根没创建,或者已经被回收了,调用 wait 就会直接报错(返回错误码),告诉你“没这个子进程可以等”。

左边:父进程阻塞在 wait

父进程执行到 parent_code() 里的 wait(&s) 时,子进程还在 child_code() 里跑(没 exit ),所以父进程会卡在 wait 这里不动(阻塞)。

右边:子进程退出后,父进程继续

子进程执行 exit(n) 退出后,父进程的 wait(&s) 被“唤醒”,拿到子进程的退出状态(存在 s 里 ),然后父进程继续往下执行 parent_code() 后续代码(比如图里的 exit(&s) ,实际更常见的是处理退出状态后干别的活 )。

3.3 解析status参数:子进程的 “最后消息”

status是一个 32 位整数,其低 16 位存储退出信息,结构如下:

高 8 位:正常退出时存储退出码(仅当低 7 位为 0 时有效)。低 7 位:存储终止信号(若进程被信号杀死,则非 0)。

1.wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

2.如果传递NULL,表示不关心子进程的退出状态信息。

3.否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

4.status不能简单的当作整形来看待,要按 二进制位(比特位)拆分 理解,低 16 位藏着关键信息,分两种场景:

(1)子进程“正常退出”(比如自己调用 exit(n) 或 return n )

低 16 位里,高 8 位(第 8~15 位) 存的是 exit(n) 的 n (退出状态码 );低 8 位(第 0~7 位) 是 0 (标记“正常退出” )。

(2)子进程“被信号杀死”(比如被 kill 命令、段错误信号终止 )

低 16 位里,高 8 位(第 8~15 位) 没用(标记为“未用” );低 7 位(第 0~6 位) 存的是“终止信号编号”(比如 9 是 SIGKILL ,强制杀死 );第 7 位 是 core dump 标志( 1 表示程序崩溃时生成了 core 文件,可用来调试;0表示未生成 )。

测试代码:

#include

#include

#include

#include

#include

int main( void )

{

pid_t pid;

if ( (pid=fork()) == -1 )

perror("fork"),exit(1);

if ( pid == 0 )

{

sleep(20);

exit(10);

}

else

{

int st;

int ret = wait(&st);

if ( ret > 0 && ( st & 0X7F ) == 0 )

{ // 正常退出

printf("child exit code:%d\n", (st>>8)&0XFF);//把st右移8位,再取低8位,就得到子进程的退出码

}

else if( ret > 0 )

{ // 异常退出

printf("sig code : %d\n", st&0X7F );// st的低7位,这里存的是信号编号,(比如9表示SIGKILL,强制杀死)

}

}

}

测试结果:

# ./a.out #等20秒退出

child exit code:10

# ./a.out #在其他终端kill掉

sig code : 9

通过以下宏可解析status:

WIFEXITED(status):若为真,表示子进程正常退出。

组成及含义: W 是 “wait” 的缩写,表明这是与等待子进程状态相关的操作; IF 表示 “是否”,用来进行条件判断; EXITED 意为 “已退出”。

功能对应:这个宏用于判断子进程是否正常退出,从命名上就很清晰地表达出,是在等待子进程状态的过程中,判断子进程是否处于已正常退出的状态 。 WEXITSTATUS(status):若WIFEXITED为真,返回退出码。

组成及含义:同样开头的 W 代表 “wait” ; EXIT 表示 “退出” , STATUS 表示 “状态、码值” 。功能对应:该宏的作用是在确认子进程正常退出(即 WIFEXITED 为真时 ),获取子进程的退出码,命名直观地体现了它是获取与子进程退出相关状态码的功能 。 WIFSIGNALED(status):若为真,表示子进程被信号杀死。

组成及含义: W 还是 “wait” 的意思 ; IF 依旧是 “是否” 的判断含义 ; SIGNALED 表示 “被信号终止” 。功能对应:它用于判断子进程是否是被信号杀死的,命名直接反映出是在等待子进程状态时,判断子进程是否处于被信号终止的状态 。 WTERMSIG(status):若WIFSIGNALED为真,返回终止信号编号。

组成及含义: W 为 “wait” ; TERM 是 “terminate(终止 )” 的缩写 , SIG 是 “signal(信号 )” 的缩写。功能对应:当确认子进程是被信号杀死(即 WIFSIGNALED 为真时 ),这个宏用于获取导致子进程终止的信号编号 ,从名字能看出其与获取子进程终止信号相关 。

WEXITSTATUS解析status时,只关注低8位(二进制最后8位),超出部分会被截断; WTERMSIG是取低7位。

3.4 阻塞与非阻塞等待

阻塞等待:父进程暂停执行,直到子进程终止(如waitpid(-1, &status, 0))。

进程的阻塞等待方式:

int main()

{

pid_t pid;

pid = fork();

if(pid < 0)

{

printf("%s fork error\n",__FUNCTION__);//__FUNCTION__是当前函数名的快捷方式,这里是"main"

return 1;

}

else if( pid == 0 )

{ //child

printf("child is run, pid is : %d\n",getpid());

sleep(5);

exit(257);

}

else

{

int status = 0;

pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S

printf("this is test for wait\n");

if( WIFEXITED(status) && ret == pid )

{

printf("wait child 5s success, child return code is

:%d.\n",WEXITSTATUS(status));

}

else

{

printf("wait child failed, return.\n");

return 1;

}

}

return 0;

}

运行结果:

[root@localhost linux]# ./a.out

child is run, pid is : 45110

this is test for wait

wait child 5s success, child return code is :1.//257的二进制是1 0000 0001,但WEXITSTATUS解析status时只关注低8位,即0000 0001 所以是1

非阻塞等待:父进程不暂停,若子进程未退出则立即返回 0,可用于 “轮询” 等待(如waitpid(-1, &status, WNOHANG))。

进程的非阻塞等待方式:

#include

#include

#include // 提供waitpid等进程等待函数

#include // 提供fork、sleep、sleep等函数

#include // 提供C++的vector容器

// 定义函数指针类型,用于指向指向无参数无返回值的函数

typedef void (*handler_t)();

// 存储函数指针的vector容器,用于管理需要执行的任务

std::vector handlers;

// 任务函数1

void fun_one()

{

printf("这是一个临时任务1\n");

}

// 任务函数2

void fun_two()

{

printf("这是一个临时任务2\n");

}

// 加载任务函数到vector容器中

void Load()

{

handlers.push_back(fun_one); // 添加任务1

handlers.push_back(fun_two); // 添加任务2

}

// 执行所有已加载的任务

void handler()

{

// 如果任务容器为空,则先加载任务

if (handlers.empty())

Load();

// 遍历容器,执行每个任务函数

for (auto iter : handlers)

iter(); // 调用函数指针指向的任务

}

int main()

{

pid_t pid; // 用于存储fork的返回值(进程ID)

// 创建子进程

pid = fork();

// fork失败的情况

if (pid < 0)

{

// __FUNCTION__宏表示当前函数名(此处为"main")

printf("%s fork error\n", __FUNCTION__);

return 1; // 异常退出

}

// 子进程执行的代码

else if (pid == 0)

{

printf("child is run, pid is : %d\n", getpid()); // 输出子进程ID

sleep(5); // 子进程休眠5秒,模拟执行任务

exit(1); // 子进程退出,退出码为1

}

// 父进程执行的代码

else

{

int status = 0; // 用于存储子进程的退出状态

pid_t ret = 0; // 用于存储waitpid的返回值

// 循环进行非阻塞等待

do {

// 非阻塞等待任意子进程:

// -1表示等待任意子进程

// status用于接收退出状态

// WNOHANG表示非阻塞模式(子进程未退出则立即返回0)

ret = waitpid(-1, &status, WNOHANG);

// 如果ret为0,说明子进程还在运行

if (ret == 0)

{

printf("child is running\n"); // 提示子进程正在运行

}

// 执行预设的任务(无论子进程是否退出,每次循环都执行)

handler();

} while (ret == 0); // 当子进程还在运行时,继续循环等待

// 子进程退出后,判断退出状态

// WIFEXITED(status):检查子进程是否正常退出

// ret == pid:确保回收的是目标子进程

if (WIFEXITED(status) && ret == pid)

{

// WEXITSTATUS(status):提取子进程的退出码

printf("wait child 5s success, child return code is :%d.\n",

WEXITSTATUS(status));

}

// 等待失败的情况(如子进程被信号终止等)

else

{

printf("wait child failed, return.\n");

return 1; // 异常退出

}

}

return 0; // 正常退出

}

四、进程程序替换:让子进程执行全新程序

fork创建的子进程默认执行与父进程相同的代码,若想让子进程执行全新程序(如从磁盘加载新的可执行文件),需通过程序替换实现。

4.1 替换原理:不换进程换 “内容”

程序替换通过exec系列函数实现,其核心是:

替换用户空间的代码和数据:丢弃原进程的代码和数据,加载新程序的代码、数据和堆栈。不创建新进程:进程 PID 保持不变,仅替换进程的执行内容。

例如:子进程通过exec加载/bin/ls,之后就会执行ls命令,而非原父进程的代码。

4.2 exec函数簇:6 个函数的命名密码

exec系列有 6 个函数,均以exec开头,区别在于参数格式和是否自动搜索路径:

函数特点示例execl(path, arg0, arg1, ..., NULL)参数列表形式(l=list),需指定程序全路径execl("/bin/ls", "ls", "-l", NULL)execlp(file, arg0, ..., NULL)自动搜索PATH环境变量(p=path),无需全路径execlp("ls", "ls", "-l", NULL)execle(path, arg0, ..., NULL, envp)自定义环境变量(e=env)char* env[] = {"PATH=/bin", NULL}; execle("/bin/ls", "ls", NULL, env);execv(path, argv)参数数组形式(v=vector)char* argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);execvp(file, argv)结合v和p,参数数组 + 自动搜路径execvp("ls", argv);execve(path, argv, envp)系统调用原函数(其他函数均调用它),自定义环境变量内核级实现,用户态少直接使用

命名解释:

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1所以exec函数只有出错的返回值而没有成功的返回值。

命名规律:

l(list):参数以列表形式传递(如arg0, arg1, NULL)。v(vector):参数以数组形式传递(如argv[])。p(path):自动从PATH环境变量搜索程序路径。e(env):允许自定义环境变量(默认继承父进程环境)。

exec调用举例如下:

#include

int main()

{

char *const argv[] = {"ps", "-ef", NULL};

char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

execl("/bin/ps", "ps", "-ef", NULL);

// 带p的,可以使⽤环境变量PATH,⽆需写全路径

execlp("ps", "ps", "-ef", NULL);

// 带e的,需要⾃⼰组装环境变量

execle("ps", "ps", "-ef", NULL, envp);

execv("/bin/ps", argv);

// 带p的,可以使⽤环境变量PATH,⽆需写全路径

execvp("ps", argv);

// 带e的,需要⾃⼰组装环境变量

execve("/bin/ps", argv, envp);

exit(0);

}

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节, 其它函数在man手册第3节。

execve 是 “真·系统调用”:直接和操作系统内核交互,负责加载新程序替换当前进程,在 man 手册第 2 节(系统调用分类 )。其他 5 个( execlp / execl / execle / execvp / execv )是 “库函数封装”:它们最终会调用 execve ,只是帮你简化了参数准备(比如自动找路径、传环境变量 ),在 man 手册第 3 节(库函数分类 )。

这些函数之间的关系如下图所示。 下图exec函数簇一个完整的例子:

4.3 替换后的注意事项

替换成功后,原进程的代码和数据被完全丢弃,不会返回(若返回则表示替换失败)。替换不改变进程的 PID、文件描述符(已打开的文件仍可访问)等内核属性。通常与fork配合使用:父进程fork后,子进程调用exec执行新程序,父进程继续等待或执行其他任务。

总结:进程控制的核心逻辑

进程控制围绕生命周期管理展开,四大环节环环相扣:

创建:fork通过写时拷贝高效创建子进程,父子进程 PID 不同是关键标识。终止:exit与_exit实现正常退出,退出码传递执行结果。等待:wait/waitpid解决僵尸进程问题,通过status解析子进程状态。替换:exec系列函数让子进程执行全新程序,实现进程功能的灵活扩展。

掌握这些机制,你就能轻松应对进程管理的各种场景,从简单的多进程程序到复杂的服务器架构,都能游刃有余。记住:进程控制的核心是资源管理与生命周期把控,理解这一点,技术细节便迎刃而解。

相关推荐

烟雨江湖开服时间表 最新开服时间一览
det365娱乐场

烟雨江湖开服时间表 最新开服时间一览

📅 10-13 👁️ 9714
钿的笔画顺序分布演示:
det365娱乐场

钿的笔画顺序分布演示:

📅 10-18 👁️ 4520
黄金胶囊惊喜回归 金秋回归/新晋快速毕业玩法指南