Linux下 进程控制(一) —— 进程的创建、终止和等待

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】

文章目录

  • [1. 进程的创建](#1. 进程的创建)
    • [1.1 fork函数](#1.1 fork函数)
    • [1.2 写时拷⻉](#1.2 写时拷⻉)
    • [1.3 fork的常规用法和失败原因](#1.3 fork的常规用法和失败原因)
  • [2. 进程终⽌](#2. 进程终⽌)
    • [2.1 进程退出场景](#2.1 进程退出场景)
    • [2.2 进程常⻅退出⽅法的引出](#2.2 进程常⻅退出⽅法的引出)
    • [2.3 退出码](#2.3 退出码)
      • [2.3.1 strerror](#2.3.1 strerror)
      • [2.3.2 errno](#2.3.2 errno)
      • [2.3.3 退出码------库函数和进程](#2.3.3 退出码——库函数和进程)
    • [2.4 进程异常终止](#2.4 进程异常终止)
    • [2.5 进程终止本质的总结](#2.5 进程终止本质的总结)
    • [2.6 exit(函数)](#2.6 exit(函数))
    • [2.7 _exit(系统调用)](#2.7 _exit(系统调用))
    • [2.8 _exit 、 exit 关系和对比](#2.8 _exit 、 exit 关系和对比)
  • 3.进程等待
    • [3.1 进程等待必要性](#3.1 进程等待必要性)
    • [3.2 进程等待的⽅法](#3.2 进程等待的⽅法)
      • [3.2.1 wait](#3.2.1 wait)
      • [3.2.2 waitpid方法](#3.2.2 waitpid方法)
      • [3.2.3 获取⼦进程status](#3.2.3 获取⼦进程status)
      • [3.2.4 父进程阻塞与非阻塞等待(option)](#3.2.4 父进程阻塞与非阻塞等待(option))
      • [3.3.5 多进程创建](#3.3.5 多进程创建)

1. 进程的创建

1.1 fork函数

在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

c 复制代码
#include <unistd.h>
pid_t fork(void);

返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用 fork,当控制转移到内核中的 fork 代码后,内核执行以下操作:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表(就是过期队列中)当中
  • fork 返回,开始调度器调度

当⼀个进程调⽤fork之后,就有两个⼆进制代码相同的进程。⽽且它们都运⾏到相同的地⽅。但每个进程都将可以开始它们⾃⼰的旅程

所以,fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完全由调度器决定。

1.2 写时拷⻉

通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅

式各⾃⼀份副本。具体⻅下图:

  1. fork后如果父进程的数据段的页表中的读写权限为rw,则会直接改成r(如果本来就是r则不变)。
  2. 然后子进程会拷贝父进程的PCB、虚拟内存和页表内容,并且父子进程在物理内存中页表映射在同一块代码和数据区
  3. 当其中一方(假设是子进程),试图修改数据,由于在页表中的权限是r,此时系统就会反应出你想修改子进程数据同时会做出相应的反应(这也就是为什么创建进程的时候要把权限改成r的原因,就是让系统反应到你想修改数据),页表中读取权限改为rw,然后重新在一块新的物理内存中开辟一块地址,将数据拷贝进去,随后与子进程页表创建新的映射关系,并修改对应值。

因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!

写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率。

1.3 fork的常规用法和失败原因

常规用法:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如bash和一些指令(如ls)。

fork调⽤失败的原因:

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

2. 进程终⽌

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1 进程退出场景

  • 代码运行完毕(运行期间没有收到任何信号),结果正确,退出码为0

比如冒泡排序,排序结果正确(类比考完试,考过了)

  • 代码运行完毕(运行期间没有收到任何信号),结果不正确,退出码非0

比如冒泡排序,排序结果不正确(类比考完试,不及格)

  • 代码没跑完,异常终止,信号编号不为0,退出码无意义

代码没跑完就挂了(类比考试作弊被抓)

问题来了,你怎么知道你执行完成没成功呢?

其实这个就是为什么 在main函数要return 0的原因,这里的0就是该进程的退出码,子进程会把自己的退出码交给父进程,因为只有父进程会关心子进程执行的怎么样。

2.2 进程常⻅退出⽅法的引出

程序退出方式

  1. main 函数返回
  2. 调用 exit 函数
  3. 调用 _exit 函数 异常退出
  • ctrl + c:通过信号终止程序

⚠️:上面进程退出方法在后面 2.6 2.7 会详细讲解 2.2部分为保证知识完整提及一下

我们写下以下程序:

cpp 复制代码
#include<stdio.h>

int main()
{       
        return 0;
}       

我们可以通过 echo $? 查看上一个bash子进程的退出码数字 发现就是return后的0数字

关于什么是 退出码 是什么 请看 2.3

2.3 退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。

我们可以自定义退出码的含义(一般以宏的形式进行自定义,这是比较推荐的),同时也可以使用系统自带的退出码的含义表:

  • 退出码 0 表示命令执行无误,这是完成命令的理想状态。
  • 退出码 1 我们也可以将其解释为 "不被允许的操作"。例如在没有 sudo 权限的情况下使用 yum;再例如除以 0 等操作也会返回错误码 1,对应的命令为 let a=1/0
  • 130(SIGINT 或 ^C)和 143(SIGTERM)等终止信号是非常典型的,它们属于 128+n 信号,其中 n 代表终止码。
  • 可以使用 strerror 函数来获取退出码对应的描述。

2.3.1 strerror

strerror 是 C 标准库( 或 <string.h>)中的一个函数,用于将错误码(errno)转换为可读的错误描述字符串。

cpp 复制代码
#include<stdio.h>
#include<string.h>

int main()
{
  for(int i=0;i<200;i++)
  printf("%d->%s\n",i,strerror(i));
  return 0
}  

打印出了对应的退出码:

超出部分也显示出来了:

2.3.2 errno

errno (Error Number) 是 C/C++ 标准库中用于报告系统调用或库函数错误的全局变量。

当某个函数(如 fopen, malloc, sqrt 等)执行失败时,它们通常会返回一个特殊值(如 NULL 或 -1),并将具体的错误原因代码以错误码形式存入 errno 中。

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
    //errno是C语言的全局变量 成功:errno=0 ;失败: errno=!0
   FILE *fp = fopen("./logtxt","r");//注定失败
   if(fp==NULL)
   {
           printf("%d:%s\n",errno,strerror(errno));
   }
}   

2.3.3 退出码------库函数和进程

我们此时得到两个概念:

  1. 当我们库函数调用失败,会产生对应的错误码。
  2. 我们进程退出的时候也会产生对应的进程退出码。

这两者概念并不是对立的!!

当然 当我们打开文件失败,我们可以直接return errno然后再去查对应的退出码具体含义!

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
    //errno是C语言的全局变量 成功:errno=0 ;失败: errno=!0
   FILE *fp = fopen("./logtxt","r");//注定失败
   if(fp==NULL)
   {
   return errno;
   }
   return 0;
}   

2.4 进程异常终止

⚠️:只有代码跑完了,那么你的退出码才会有意义,那么如果代码异常(越界,野指针,除0等)终止,那么你的退出码将没有任何意义,那么事什么样子的原因导致进程的终止的呢?

答案是 被信号终止了。

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
int main()
{
while(1)
printf("我就是一个进程: pid: %d, ppid: %d\n", getpid(), getppid());  
}

上面的代码显然会一直跑,但是我们可以通过信号杀死该进程,而 这种被信号杀死进程的方式,实际上就是进程异常终止的原因!

2.5 进程终止本质的总结

综上,可以得出一个结论:进程执行的结果状态,可以用两个数字表示:int sig(退出信号)、int exit_code(退出码)。并且这些用户是不需要维护的,因为当一个进程退出的时候,OS会把进程退出的详细信息写入到进程的 task_struct 结构体中!!这也就是为什么进程在退出的时候需要僵尸进程维护自己退出状态的原因!!

2.6 exit(函数)

exit 是 C/C++ 标准库函数,用于立即终止当前程序的运行。

当你调用 exit() 时,程序会停止执行后续代码,清理资源(如关闭文件、刷新缓冲区),并将一个状态码返回给操作系统(或父进程)。

函数原型: void exit(int status);

  • status (退出码):
    • 0 或 EXIT_SUCCESS: 表示程序正常结束。
    • 非零值 (通常 1-255) 或 EXIT_FAILURE: 表示程序异常结束或出错。
cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("我就是一个进程: pid: %d, ppid: %d\n", getpid(), getppid());
exit(123);
}

此时退出码如下:


但是这并不是 exit 真正用法 不然这么做和return有什么区别

我们知道 在函数内部return 此时进程不会终止,因为返回的是函数的返回值仅仅表示函数结束,只有在main函数中,return返回的才是退出码,表示进程结束;而如果我们在非main函数中 写了exit 那么会直接终止进程 返回退出码!

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>

void Print()
{
        printf("hello\n");
       exit(99);
}


int main()
{
        Print();
   printf("我就是一个进程: pid: %d, ppid: %d\n", getpid(),getppid());    
        exit(123);
}

2.7 _exit(系统调用)

_exit (以及 POSIX 标准的 _Exit) 是比 exit 更底层、更粗暴的程序终止函数,属于系统层面的封装,即 系统调用


注意:_exitexit在功能上没什么区别都是终止进程,返回退出码,但是_exitexit还是有一些区别的

2.8 _exit 、 exit 关系和对比

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
printf("hello bit");
sleep(3);


exit(11);
}

上面代码因为 printf 的缓冲区机制(此时内容在标准输出缓冲区的内存中) 和 sleep 的阻塞机制 共同作用,导致前 3 秒屏幕上一片空白,3 秒后因为exit终止进程后强制刷新缓冲区 瞬间显示 hello bit
但是当换成_exit后,我们发现_exit不会刷新缓存区:

由此可以的得出:缓存区刷新操作,一定不在内核中!!!


其实exit本质就是:
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexiton_exit定义的清理函数。(了解)
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit

参考,看不懂的地方不用管暂时:

特性 exit(int status) _exit(int status)
头文件 <cstdlib> (C++) / <stdlib.h> © <unistd.h> (POSIX/Linux/Unix)
标准 C/C++ 标准库 POSIX 标准 (系统调用封装)
刷新缓冲区 ✅ 会 (清空 stdout, stderr 等) ❌ 不会 (缓冲区数据直接丢失)
关闭文件流 ✅ 会 (调用 fclose) ❌ 不会 (文件描述符可能保持打开)
调用析构函数 ✅ 会 (C++ 全局/静态对象) ❌ 不会 (直接跳过)
执行 atexit ✅ 会 (执行注册的清理函数) ❌ 不会
速度 较慢 (因为要做清理) 极快 (立即终止)
适用场景 正常程序结束、普通错误处理 子进程退出、严重崩溃、避免重复清理

3.进程等待

进程等待就是通过系统调用接口wait/waitpid,来对子进程进行状态检查和回收的功能!(回收的就是僵尸状态的子进程)

3.1 进程等待必要性

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

总结:

  1. 防止内存泄漏(必须)
  2. 父进程可能需要获取子进程的退出信息(可选)

3.2 进程等待的⽅法

3.2.1 wait

wait 是 Linux/Unix 系统编程中用于进程同步的核心函数。它的主要作用是:让父进程暂停执行,直到它的某个子进程结束。

  • 这就好比家长(父进程)送孩子(子进程)去考试,家长在考场外等待 (wait),直到孩子考完出来,家长才能继续做下一件事(比如回家做饭)。
cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

我们可以用该函数 解决僵尸进程!
cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>

int main()
{
 pid_t id = fork();
 if(id==0)
 {//子进程
 int cnt = 5;
 while(cnt--)
  { 
   printf("我是子进程:pid:%d\n",getpid());
   sleep(1);
  }
}
 else if(id >0)
 {
  //回收子进程,等待僵尸
  sleep(10);
  pid_t rid =wait(NULL); 

 if(rid==id)
  {
 printf("wait success!");
  }
 }
 
}
 
  1. 子进程会每隔 1 秒打印一次"我是子进程...",总共打印 5 次,然后退出。
  2. 父进程会直接跳过 if 块,执行 sleep(10),睡大觉 10 秒。
  3. 关键点:在子进程退出后(大约第 5 秒),到父进程醒来调用 wait() 之前(第 10 秒),这中间的 5 秒钟里,子进程虽然已经死了,但它的进程号(PID)依然存在于系统进程表中,状态为 Z(Zombie)。

具体如下 我们用ps指令进行查看

结论:

  1. 如果父进程wait子进程,但是子进程就是没有退出,则父进程会阻塞在wait函数中。

3.2.2 waitpid方法

cpp 复制代码
pid_ t waitpid(pid_t pid, int *status, int options);
- 返回值:

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

- 参数:

- pid:
1. Pid=-1,等待任⼀个⼦进程。与wait等效。(这是因为在父进程的PCB中有一个子进程表,通过遍历该表,查看是否有对应子进程)
2. Pid>0.等待其进程ID与pid相等的⼦进程。

status: 输出型参数(关于status具体解释 请移步3.2.3)
⚠️:下面两个参数都是以宏的形式定义的!
1. WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程
是否是正常退出,说人话信号为0即为真,有信号则为假)
2. WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程
的退出码,相当于:(status>>8)&0xFF)

options:默认为0,表⽰阻塞等待(具体参考3.2.4)
1. WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该⼦进程的ID。
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

cpp 复制代码
waitpid(-1,NULL,0) ;

waitpid按照如上填写对应参数的时候,其与wait(NULL)功能相同。

3.2.3 获取⼦进程status

  • waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
  1. 如果进程是被正常终结,比特位的第9---16位表示进程退出时候的退出码,1-8位都为0
  2. 如果进程是被信号杀死,比特位的第9-16位为0,第8位位core dump标志(暂不了解),第1-7位为终止的信号!

我们可以利用位图操作获取退出码和信号:

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
 pid_t id = fork();
 if(id==0)
 {//子进程
   int cnt = 5;
 while(cnt--)
 {
   printf("我是子进程:pid:%d\n",getpid());
   sleep(1);
  }
  
  exit(1);
}
 else if(id >0)
 {
  //回收子进程,等待僵尸
  sleep(10);
  pid_t rid =waitpid(id,&status,0);

 if(rid==id)
  {
  //执行位图操作获取对应的退出码或者信号
// 1. 获取退出码:右移8位,保留低8位
// 对应宏:WEXITSTATUS(status)
  int exit_code=(status>>8)&0XFF;

// 2. 获取信号:保留低7位 (第8位是核心转储标志,这里先忽略)
// 对应宏:WTERMSIG(status)
  int exit_sig=(status)&0X7F;
 printf("pid:%d,wait success!,status:%d,exit_code:%d,exit_sig:%d\n",id,rid,exit_code,exit_sig);
  }
 }

}

效果如下:


关于WIFEXITEDWEXITSTATUS这两个宏(3.2.2有详细定义)如何使用

cpp 复制代码
int stauts;
pid rid()=waitpid(id+1,&status,0);
if(rid>0)
{
   if(WIFEXITED(status))//进程正常结束则为真
   {
      printf("wait success,exit_code:%d",WIFEXITSTATUS(status));//直接输出退出码!
   }

3.2.4 父进程阻塞与非阻塞等待(option)

特性 阻塞等待 (Blocking) 非阻塞等待 (Non-blocking)
行为描述 父进程调用后立即挂起(进入可中断睡眠状态),直到目标子进程结束。 父进程调用后立即返回,不管子进程是否结束。
CPU 占用 等待期间不占用 CPU 时间片(让出给其他进程)。 需要父进程不断轮询,若频繁调用会浪费 CPU。
返回值 只有当子进程结束时才返回子进程 PID。 若子进程已结束:返回 PID。 若未结束:立即返回 0
典型场景 脚本、简单程序、必须等子进程结果才能继续的逻辑。 服务器、守护进程、GUI 程序、需要同时处理多任务的场景。
关键参数 options = 0 options = WNOHANG (这是一个宏)
  1. 阻塞等待 (默认行为)
  • 父进程说:"孩子没回来,我哪儿也不去,就在这儿死等。"

下面代码可以很好模拟阻塞等待,很简单,就不模拟展示和解析了

cpp 复制代码
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid == 0)
     {
        // 子进程:睡 5 秒后退出
        sleep(5);
        printf("子进程退出\n");
        return 0;
    } 
    else
     {
        printf("父进程:开始阻塞等待...\n");
        
        // ⚠️ 第三个参数为 0,表示阻塞
        // 这一行代码会卡住,直到 5 秒后子进程退出
        pid_t ret = waitpid(pid, NULL, 0); 
        
        printf("父进程:等待结束!子进程 %d 已回收。\n", ret);
        // 注意:在这 5 秒内,父进程无法执行任何打印或其他逻辑
    }
    return 0;
}
  1. 非阻塞等待 (WNOHANG)【非阻塞轮询】
  • 父进程说:"我去看看孩子回来了没?没回?那我先去刷会b站,过会儿再来瞅瞅。"
cpp 复制代码
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) 
    {
        // 子进程:睡 3 秒后退出
        sleep(3);
        printf("子进程退出\n");
        return 0;
    } 
    else 
    {
        int seconds = 0;
        while (1) 
        {
            printf("父进程:正在处理其他任务... (第 %d 秒)\n", ++seconds);
            
            // ⚠️ 关键:第三个参数为 WNOHANG,表示非阻塞
            pid_t ret = waitpid(pid, NULL, WNOHANG);

            if (ret > 0) 
            {
                // 子进程已结束,被成功回收
                printf("父进程:发现子进程 %d 已结束,回收成功!\n", ret);
                break; // 退出循环
            } 
            else if (ret == 0) 
            {
                // 子进程还在运行,没结束
                // 父进程不等待,继续下一次循环(干别的事)
            }
             else
              {
                // ret < 0,出错了
                perror("waitpid error");
                break;
            }

            sleep(1); // 模拟父进程忙里偷闲,每秒检查一次
        }
    }
    return 0;
}

输出顺序:

父进程:正在处理其他任务... (第 1 秒)

父进程:正在处理其他任务... (第 2 秒)

父进程:正在处理其他任务... (第 3 秒)

子进程退出

父进程:正在处理其他任务... (第 4 秒) -> 此时检测到子进程已死

父进程:发现子进程 ... 回收成功!


阻塞等待vs非阻塞等待:

并不是说非阻塞等待阻塞等待效率高,只是非阻塞等待在等待期间做了更多事情,而效率看的是子进程退出时间,跟是否阻塞没关系!!

3.3.5 多进程创建

cpp 复制代码
// 引入 C 标准库头文件,用于进程控制、等待、标准输入输出等
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>

// 引入 C++ 标准库,用于流式输出和容器管理
#include <iostream>
#include <vector>

/**
 *  定义函数指针类型别名
 * 
 * 含义:callback_t 是一个指向函数的指针类型,该函数:
 * 1. 没有返回值 (void)
 * 2. 没有参数 ()
 * 
 * 作用:让代码更简洁,便于在函数间传递"任务逻辑"。
 */
typedef void (*callback_t)();

// 定义程序退出状态码枚举
enum
{
    OK,       // 成功退出 (0)
    USAGE_ERR // 用法错误退出 (1)
};

/**
 * 任务函数 1: Task
 * 
 * 模拟一个耗时任务,循环打印 5 次,每次间隔 1 秒。
 * 这个函数将被子进程执行。
 */
void Task()
{
    int cnt = 5;
    while(cnt--)
    {
        // 打印当前进程信息:
        // getpid(): 获取当前进程 ID
        // getppid(): 获取父进程 ID
        // cnt: 剩余循环次数
        printf("我是一个子进程, 我在完成 Task 任务,pid: %d, ppid:%d, cnt: %d\n", 
               getpid(), getppid(), cnt);
        sleep(1); // 休眠 1 秒,模拟工作负载
    }
}

/**
 * 任务函数 2: Hello
 * 
 * 另一个模拟任务,逻辑同上,只是打印内容不同。
 * 用于演示可以通过回调函数切换不同的任务逻辑。
 */
void Hello()
{
    int cnt = 5;
    while(cnt--)
    {
        printf("我是一个子进程, 我在完成 Hello 任务,pid: %d, ppid:%d, cnt: %d\n", 
               getpid(), getppid(), cnt);
        sleep(1);
    }
}

///////////////////////////////
// C++ 参数传递最佳实践注释:
// - 输入参数 (只读): 使用 const & (避免拷贝,保证安全)
// - 输出参数 (修改): 使用 * (指针) 或 & (引用),此处用指针明确表示"我要修改你指向的内容"
// - 输入输出参数: 使用 & (引用)

/**
 * @brief 批量创建子进程的工厂函数
 * 
 * @param num   [输入] 需要创建的子进程数量
 * @param subs  [输出] 指向 vector 的指针,用于存储所有子进程的 PID
 * @param cb    [输入] 函数指针,指定子进程要执行的具体任务逻辑
 */
void CreateChildProcess(int num, std::vector<pid_t> *subs, callback_t cb)
{
    for(int i = 0; i < num; i++)
    {
        pid_t id = fork(); // 创建新进程
        
        if(id == 0)
        {
            // ================= 子进程区域 =================
            // fork() 返回 0,表示当前是子进程
            
            // 1. 执行传入的回调函数 (任务逻辑)
            // 注意:此时子进程拥有父进程代码段的副本,可以直接调用 cb()
            cb();
            
            // 2. 任务完成后必须退出!
            // 如果不调用 exit(),子进程会继续执行下面的父进程代码 (push_back),
            exit(0); 
        }
        else if (id > 0)
        {
            // ================= 父进程区域 =================
            // fork() 返回子进程的 PID (>0)
            
            // 将新创建的子进程 PID 存入容器
            // 因为 subs 是指针,所以用 -> 操作符
            subs->push_back(id);
            
            // 父进程继续循环,创建下一个子进程
        }
        else
        {
            // fork 失败处理 (通常是因为系统进程数达到上限)
            perror("fork failed");
            exit(USAGE_ERR);
        }
    }
}

/**
 *  等待并回收所有子进程
 * 
 * @param subs [输入] 包含所有子进程 PID 的 vector (使用 const 引用,只读不修改)
 * 
 * 逻辑:遍历 PID 列表,逐个调用 waitpid 进行阻塞式回收。
 * 注意:这是一种串行回收方式。如果想提高效率,可以在信号处理函数中非阻塞回收。
 */
void WaitAllChild(const std::vector<pid_t> &subs)
{
    // 使用 C++11 范围 for 循环遍历 vector
    for(const auto &pid : subs)
    {
        int status = 0;
        
        // waitpid 参数详解:
        // 1. pid: 等待指定的子进程
        // 2. &status: 用于保存子进程退出状态的内核位图
        // 3. 0: 选项为 0,表示【阻塞等待】,直到该子进程结束才返回
        pid_t rid = waitpid(pid, &status, 0);
        
        if(rid > 0)
        {
            // 回收成功
            // WEXITSTATUS(status) 是宏,等价于 (status >> 8) & 0xFF,用于提取正常退出码
            printf("子进程:%d Exit, exit code: %d\n", rid, WEXITSTATUS(status));
        }
        else
        {
            // 理论上不会走到这里,除非 waitpid 出错
            perror("waitpid error");
        }
    }
}

/**
 *  主函数:程序入口
 * 
 * 启停多进程的方案演示
 * 命令行用法:./myproc 5  (表示创建 5 个子进程)
 */
int main(int argc, char *argv[])
{
    // 1. 参数校验
    if(argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " process_num" << std::endl;
        exit(USAGE_ERR);
    }

    // 2. 解析命令行参数
    // std::stoi 将字符串转换为整数 (C++11 特性)
    int num = std::stoi(argv[1]);

    // 3. 准备容器,用于存储子进程 PID
    std::vector<pid_t> subs;
    
    // (可选) 如果需要动态切换不同任务,可以定义一个回调函数数组
    // std::vector<callback_t> cbs; 

    // 4. 创建多进程
    // 传入:数量,PID 容器地址,任务函数指针 (这里固定传 Hello,也可以传 Task)
    CreateChildProcess(num, &subs, Hello);

    // 5. 父进程等待所有子进程结束
    // 此时父进程会阻塞,直到最后一个子进程退出
    WaitAllChild(subs);

    // 6. 所有子进程回收完毕,主程序退出
    return OK;
}
相关推荐
万象.7 小时前
Linux传输层TCP,UDP相关内容
linux·tcp/ip·udp
耀耀_很无聊7 小时前
09_Jenkins安装JDK环境
java·运维·jenkins
MaximusCoder7 小时前
等保测评命令——Centos Linux
linux·运维·经验分享·python·安全·centos
万象.7 小时前
Linux数据链路层通信原理及报文格式
linux·网络·网络协议
卷Java8 小时前
Linux服务器Docker部署OpenClaw:腾讯云/阿里云/VPS安装避坑指南
linux·运维·服务器
原来是猿10 小时前
Linux-【动静态库】
linux·运维·服务器
深圳市恒讯科技10 小时前
云服务器怎么选?从CPU、内存到IOPS的零基础选型手册
运维·服务器
艾莉丝努力练剑11 小时前
【脉脉】AI创作者崛起:掌握核心工具,在AMA互动中共同成长
运维·服务器·c++·人工智能·安全·企业·脉脉
catchadmin11 小时前
保姆级 OpenClaw (原 Clawdbot)飞书对接教程 手把手教你搭建 AI 助手
人工智能·elasticsearch·飞书