在Linux环境下,进程之间相互影响、相互依赖,就像一个大家庭一样。作为程序员,我们不仅需要创建子进程,更要熟练掌握监控和管理子进程的技能,确保整个进程族能稳健高效地运行。本文将为你揭开进程创建、监控子进程、处理SIGCHLD信号等核心知识,并通过丰富的C++示例代码,让你融会贯通。
一、创建子进程:fork()孕育新生命
我们首先来看一下如何创建子进程。
在Linux系统中,通过fork()系统调用可以让一个进程创建出一个新的进程,也就是所谓的子进程。
子进程会获得父进程的数据段、代码段等资源的拷贝,然后父子进程分道扬镳,各自独立运行。
#include <unistd.h>
#include <iostream>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程代码
std::cout << "Child process, pid=" << getpid() << std::endl;
} else if (pid > 0) {
// 父进程代码
std::cout << "Parent process, pid=" << getpid() << std::endl;
} else {
// fork失败
}
return 0;
}
值得注意的是,fork()实际上并不会立即拷贝所有内存资源,而是采用写时拷贝的策略,只有在父子进程中的某一方首次试图修改资源时,内核才会进行实际拷贝。这种做法避免了不必要的浪费。
注意:
- 内存拷贝并不一开始就发生
- 从理论上来说,fork() 创建出来的子进程中的堆、栈、数据段应该是父进程的完整拷贝。
- 但是,在大部分情况下,fork() 在调用后会直接调用 exec() 函数族来执行其它的程序或者是命令,重新的初始化堆、栈以及数据段等内容。
- 因此,如果 fork() 在一开始就进行内存拷贝,很多情况下会被视为“无用之举”。
- 因此,内核会使用写时复制的方式进行 lazy copy。也就是说当子进程确实需要这一份内存时,内核才会进行拷贝动作。
- 在一般的业务需求下,通常都是由当前进程创建出多个子进程以供使用
int main(){
pid_t pid;
for(int i=0;i<4;i++){
pid=fork();
if(pid<=0)// 子进程不允许进入 for 循环内,保证进程只由父进程创建
break;
}
if(pid==0)// 子进程跳出循环以后,我们可以在这里判断当前进程
}
二、子进程监控:等待子进程的归来
作为父进程,我们通常需要监控子进程的运行状态,并在子进程结束时作出响应。
Linux提供了两个系统调用wait()和waitpid()来实现这个功能。
相比wait(),waitpid()功能更强大,可以更精确地指定等待哪个子进程,并提供了非阻塞等待的选项。
因此实际使用中我们一般选择waitpid():
#include <sys/wait.h>
#include <iostream>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
std::cout << "Child process, pid=" << getpid() << std::endl;
sleep(5); // 模拟子进程运行
} else if (pid > 0) {
// 父进程
int status;
pid_t child = waitpid(pid, &status, 0);
if (child > 0) {
std::cout << "Child process " << child << " exited" << std::endl;
// 解析子进程退出状态
if (WIFEXITED(status)) {
std::cout << "Exit status: " << WEXITSTATUS(status) << std::endl;
} else if (WIFSIGNALED(status)) {
std::cout << "Killed by signal: " << WTERMSIG(status) << std::endl;
}
} else {
// waitpid出错
}
}
return 0;
}
1、waitpid()的参数详解
- 第一个参数 pid: 可以是具体的子进程PID,或者是0(等待同组任意子进程),-1(等待任意子进程)。
- 第二个参数 status:用于获取子进程的退出状态。子进程的终止状态将写入至该指针变量中,用来表示子进程是以哪种方式结束的。因为 status 虽然定义成一个整型,但是实际上只用到了其最低的两个字节,所以我们需要使用一些宏来对其进行断言。
- 第三个参数 options :是附加选项 位掩码,可以按位或多个选项, 比如WNOHANG可以使waitpid()不阻塞。
- WUNTRACED 除了返回终止子进程的信息外,还返回因信号而停止的子进程信息。
- WCONTINUED 返回那些因收到 SIGCONT 信号而恢复执行的已停止子进程的状态信息。
- WNOHANG 非阻塞的等待,若子进程的状态并未发生改变,那么 waitpid() 则返回 0。
2、调用 waitpid() 的必要性
- 如果父进程创建了某一子进程,但并未执行 waitpid(),那么在内核的进程表中将为该子进程永久保留一条记录。并且, 即使是 SIGKILL 也无法“杀死”这个子进程,使其从进程记录表中移除,因此,这样的子进程我们通常称之为僵尸进程。
- 如果存在大量此类僵尸进程,它们势必将填满内核进程表,从而阻碍新进程的创建。
- 既然无法用信号杀死僵尸进程,那么从系统中将其移除的唯一方法就是杀掉它们的父进程 (或等待其父进程终止),此时 init 进程将接管和等待这些僵尸进程,从而从系统中将它们清理掉。
三、SIGCHLD信号:及时收割僵尸进程
如果父进程创建了子进程却未正确等待,那么当子进程结束时就会变成僵尸进程,浪费系统资源。虽然可以通过轮询的方式调用waitpid()来避免,但效率低下。
幸运的是,内核为我们提供了SIGCHLD信号,当子进程结束时会给父进程发送这个信号。我们可以捕获并处理这个信号,在信号处理函数中调用waitpid(),从而及时清理僵尸进程。
#include <signal.h>
#include <sys/wait.h>
#include <iostream>
#include <unistd.h>
void sigchld_handler(int sig) {
int status;
while (waitpid(-1, &status, WNOHANG) > 0) {
// 处理已结束的子进程
}
}
int main() {
// 为SIGCHLD注册信号处理函数
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa, NULL);
pid_t pid = fork();
if (pid == 0) {
// 子进程代码
sleep(2);
std::cout << "Child process exiting" << std::endl;
exit(0);
} else if (pid > 0) {
// 父进程代码
while (true) {
// 父进程继续运行
}
}
return 0;
}
需要注意的是,SIGCHLD信号不保证按子进程结束的顺序到达,也不保证能为每个子进程接收一次。因此我们在信号处理函数中要循环调用waitpid(),直到没有已结束的子进程为止。
通过掌握进程创建、监控子进程以及SIGCHLD信号处理等技术,相信你已经能够娴熟地在Linux环境下管理并维护一个"进程大家庭"了。
不过,这仅仅是进程管理的冰山一角,在多进程通信、进程同步等更高阶主题中,还有许多精彩的内容值得去探索。有兴趣一同前行吗?敬请期待我们的下一篇分享!