# fork()函数
系统从当前进程生成一个新的进程, 原来的进程叫做父进程, 新生成的进程叫做子进程.
子进程获得父进程数据空间、堆和栈的复制.
fork()执行一次, 返回两次, 一次返回到新进程(0), 一次返回到父进程(子进程的pid).
子进程从fork()返回后的位置开始执行, 但是拥有父进程前面定义的变量名和变量值.
fork()执行失败返回负值.
一般来说, 在fork之后是父进程先执行还是子进程先执行是不确定的, 这取决于内核所使用的调度算法.
# gcc命令
# 将zombie.c预处理、汇编、编译并链接形成可执行文件zombie, -o选项用来指定输出文件的文件名
gcc zombie.c -o zombie
子进程是通过父进程创建的, 当一个进程结束之后, 它的父进程需要调用wait()或者waitpid()取得子进程的结束状态.
unix提供了一种机制, 这种机制保证了父进程只要想知道子进程结束时的状态信息就可以得到, 这种机制是:
在每个进程退出的时候, 内核释放该进程所有的资源, 包括打开的文件、占用的内存等, 但仍为该进程保存一定的信息(进程号(the process ID)、退出状态(the termination of the process)、运行时间(the amount of CPU time taken by the process)等), 直到父进程通过wait()或者waitpid()来取时才释放
孤儿进程:
父进程已经退出, 它的一个或多个子进程还在运行, 这些子进程将成为孤儿进程.
孤儿进程将被init进程(进程号为1)收养, 并由init进程完成对它们的状态收集工作.
init进程就像民政局, 专门负责孤儿进程的善后工作, 每当出现一个孤儿进程的时候,
内核会把孤儿进程的父进程设置为init, 而init进程会循环地wait()它已经退出的子进程,
这样, 当孤儿进程结束之后, init进程会处理它的善后工作, 因此孤儿进程没有什么危害.
僵尸进程(Zombie, 进程状态为Z):
一个进程通过fork()创建了子进程, 如果子进程退出, 而父进程没有调用wait()或waitpid()获取子进程的状态信息,
那么子进程保存的信息(进程号、退出状态、运行时间等)就不会释放, 进程号就会一直被占用, 系统能够使用的进程号是有限的,
如果产生大量的僵尸进程, 可能会导致系统不能产生新的进程.
严格来说, 僵尸进程并不是问题的根源, 根源是产生大量僵尸进程的父进程, 是父进程没有在子进程exit()之后进行处理.
如果通过kill发送SIGTERM信号(kill -15 pid)或者SIGKILL信号(kill -9 pid)杀死父进程,
则该父进程产生的僵尸进程就会变成孤儿进程, 这些孤儿进程会被init进程接管, init进程释放这些孤儿进程占用的系统资源.
代码:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pid_t pid;
// 创建进程
pid = fork();
// 创建失败
if (pid < 0) {
perror("fork error");
exit(1);
}
// 子进程
if (pid == 0) {
printf("我是子进程\n");
printf("进程ID: %d\t父进程ID: %d\n", getpid(), getppid());
printf("我将睡眠5s\n");
// 子进程睡眠5s, 保证父进程先退出
sleep(5);
printf("进程ID: %d\t父进程ID: %d\n", getpid(), getppid());
printf("子进程已经退出\n");
}
// 父进程
else {
printf("我是父进程\n");
// 父进程睡眠1s, 保证子进程第一次能够输出父进程ID
sleep(1);
printf("父进程已经退出\n");
}
return 0;
}
运行结果:
[root@localhost c]# gcc orphan.c -o orphan
[root@localhost c]# ./orphan
我是父进程
我是子进程
进程ID: 13762 父进程ID: 13761
我将睡眠5s
父进程已经退出
[root@localhost c]# 进程ID: 13762 父进程ID: 1
子进程已经退出
分析:
子进程第二次输出了父进程ID为1, 说明子进程成为了孤儿进程
代码:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
}
// 子进程
else if (pid == 0) {
printf("我是子进程\n");
printf("进程ID: %d\t父进程ID: %d\n", getpid(), getppid());
printf("子进程已经退出\n");
exit(0);
}
printf("我是父进程, 我将睡眠2s\n");
// 父进程睡眠2s, 保证子进程先退出
sleep(2);
// 输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("父进程已经退出\n");
return 0;
}
运行结果:
[root@localhost c]# gcc zombie.c -o zombie
[root@localhost c]# ./zombie
我是父进程, 我将睡眠2s
我是子进程
进程ID: 13794 父进程ID: 13793
子进程已经退出
PID PPID S TT COMMAND
13719 13717 S pts/0 -bash
13793 13719 S pts/0 ./zombie
13794 13793 Z pts/0 [zombie] <defunct>
13795 13793 R pts/0 ps -o pid,ppid,state,tty,command
父进程已经退出
分析:
子进程ID是13794, 可以看到PID为13794的一行的状态是Z(Z表示僵尸进程)
# 通过信号机制
子进程退出时会向父进程发送一个SIGCHLD信号, 告诉父进程在信号处理函数waitpid()或wait()来回收子进程的退出状态
代码:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
static void sig_child_handler(int signo);
int main()
{
pid_t pid;
// 处理子进程SIGCHLD信号的函数
signal(SIGCHLD, sig_child_handler);
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
}
// 子进程
else if (pid == 0) {
printf("我是子进程\n");
printf("进程ID: %d\t父进程ID: %d\n", getpid(), getppid());
printf("子进程已经退出\n");
exit(0);
}
printf("我是父进程, 我将睡眠2s\n");
// 父进程睡眠2s, 保证子进程先退出
sleep(2);
// 输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("父进程已经退出\n");
return 0;
}
static void sig_child_handler(int signo)
{
pid_t pid;
int stat;
// 使用waitpid()处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0) {
printf("子进程 %d 结束\n", pid);
}
}
运行结果:
[root@localhost c]# gcc signal.c -o signal
[root@localhost c]# ./signal
我是父进程, 我将睡眠2s
我是子进程
进程ID: 13896 父进程ID: 13895
子进程已经退出
子进程 13896 结束
PID PPID S TT COMMAND
13719 13717 S pts/0 -bash
13895 13719 S pts/0 ./signal
13897 13895 R pts/0 ps -o pid,ppid,state,tty,command
父进程已经退出
分析:
没有僵尸进程