fork()函数模型

进程概念:

一个可执行程序,执行起来就是一个进程,再执行一次又是一个进程(多个进程可以共享同一个可执行文件)

文雅说法:进程定义为程序执行的一个实例

image-20220125100002758

在一个进程(程序)中,可以用fork()创建一个子进程,当该子进程创建时,从fork()指令的的下一条(或者说fork()函数的返回处)开始执行与父进程相同的代码

说白了:fork()函数产生了一个和当前进程完全一样的新进程,并和当前进程一样从fork()函数里返回;原来一条执行通路(父进程),现在变成两条(父进程+子进程)

fork() 一分为二

对于以下代码,两个fork();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>

int main(int argc, char *const *argv)
{

fork(); //一般fork都会成功所以不判断返回值了,我们假定成功
fork();

//((fork() && fork()) || (fork() && fork()));
//printf("每个实际用户ID的最大进程数=%ld\n",sysconf(_SC_CHILD_MAX));


for(;;)
{
sleep(1); //休息1秒
printf("休息1秒,进程id=%d!\n",getpid());
}
printf("再见了!\n");
return 0;
}

运行结果如下,有四个进程

image-20220125122045469

image-20220125121935958

再来看一个复杂例子,有7个进程。

image-20220125123932763

fork()函数简单范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>

//信号处理函数
void sig_usr(int signo)
{
printf("收到了SIGUSR1信号,进程id=%d!\n",getpid());
}

int main(int argc, char *const *argv)
{
pid_t pid;

printf("进程开始执行!\n");

//先简单处理一个信号
if(signal(SIGUSR1,sig_usr) == SIG_ERR) //系统函数,参数1:是个信号,参数2:是个函数指针,代表一个针对该信号的捕捉处理函数
{
printf("无法捕捉SIGUSR1信号!\n");
exit(1);
}

//---------------------------------
pid = fork(); //创建一个子进程

//要判断子进程是否创建成功
if(pid < 0)
{
printf("子进程创建失败,很遗憾!\n");
exit(1);
}

//现在,父进程和子进程同时开始 运行了
for(;;)
{
sleep(1); //休息1秒
printf("休息1秒,进程id=%d!\n",getpid());
}
printf("再见了!\n");
return 0;
}

image-20220125102431916

子进程12477号一个SIGKILL信号之后,父进程12476收到了一个SIGCHLD信号,如图所示

image-20220125102649414

SIGCHLD:一个进程被终止或者停止的时候,这个信号会被发送给父进程。

继续查看进程状态发现子进程12477变为了僵尸进程并且仍然挂载在前台,如下图所示:

image-20220125102806346

僵尸进程

僵尸进程的产生:在Unix系统中,一个子进程结束了,但是他的父进程还活着,但该父进程没有调用(wait/waitpid)函数对子进程进行额外处理,那么该子进程会变成一个僵尸进程。

僵尸进程:已经被终止,不干活了,但是依旧没有被内核丢弃掉,因为内核认为父亲进程可能还需要子进程的一些信息

如何杀死僵尸进程:

  1. 重启电脑
  2. 手工的把僵尸进程的父进程kill掉,僵尸进程会自动消失

waitpid()函数

头文件:#include <sys/types.h> #include <sys/wait.h>

定义函数:pid_t waitpid(pid_t pid, int * status, int options);

函数说明:waitpid()会暂时停止目前进程的执行, 直到有信号来到或子进程结束。 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status 返回, 而子进程的进程识别码也会一快返回. 如果不在意结束状态值, 则参数status 可以设成NULL. 参数pid 为欲等待的子进程识别码, 其他数值意义如下:

1、pid<-1 等待进程组识别码为pid 绝对值的任何子进程.
2、pid=-1 等待任何子进程, 相当于wait().
3、pid=0 等待进程组识别码与目前进程相同的任何子进程.
4、pid>0 等待任何子进程识别码为pid 的子进程.

参数option 可以为0 或下面的OR 组合:

WNOHANG:如果没有任何已经结束的子进程则马上返回, 不予以等待.
WUNTRACED:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会. 子进程的结束状态返回后存于status, 底下有几个宏可判别结束情况
WIFEXITED(status):如果子进程正常结束则为非0 值.
WEXITSTATUS(status):取得子进程exit()返回的结束代码, 一般会先用WIFEXITED 来判断是否正常结束才能使用此宏.
WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真
WTERMSIG(status):取得子进程因信号而中止的信号代码, 一般会先用WIFSIGNALED 来判断后才使用此宏.
WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真. 一般只有使用WUNTRACED时才会有此情况.
WSTOPSIG(status):取得引发子进程暂停的信号代码, 一般会先用WIFSTOPPED 来判断后才使用此宏.

返回值

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

当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;

函数加入SIGCHLD处理

在信号处理函数中添加对SIGCHLD信号的处理并且在main函数中加入signal(SIGCHLD,sig_usr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void sig_usr(int signo)
{
int status;

switch(signo)
{
case SIGUSR1:
printf("收到了SIGUSR1信号,进程id=%d!\n",getpid());
break;

case SIGCHLD:
printf("收到了SIGCHLD信号,进程id=%d!\n",getpid());
//这里大家学了一个新函数waitpid,有人也用wait,但老师要求大家掌握和使用waitpid即可;
//这个waitpid说白了获取子进程的终止状态,这样,子进程就不会成为僵尸进程了;
pid_t pid = waitpid(-1,&status,WNOHANG); //第一个参数为-1,表示等待任何子进程,
//第二个参数:保存子进程的状态信息(大家如果想详细了解,可以百度一下)。
//第三个参数:提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回
if(pid == 0) //子进程没结束,会立即返回这个数字,但这里应该不是这个数字
return;
if(pid == -1) //这表示这个waitpid调用有错误,有错误也理解返回出去,我们管不了这么多
return;
//走到这里,表示 成功,那也return吧
return;
break;
} //end switch
}

再次编译运行之后,如下图所示,杀死子进程后没有再出现僵尸进程。

image-20220125105736469

写时复制,读时共享

原进程(父进程)和子进程一起共享一个内存空间,但这个内存空间的特性是“写时复制”,也就是说:原来的进程和fork()出来的子进程可以同时、自由的读取内存,但如果子进程(父进程)对内存进行修改的话,那么这个内存就会复制一份给该进程单独使用,以免影响到共享这个内存空间的其他进程使用;

完善fork()代码

fork()会返回两次,子进程返回一次,父进程返回一次,而且fork在父进程中返回的值和子进程中返回的值不同,正好可以区分父子进程。

父进程的fork()返回值会 > 0(实际返回的是子进id程)而子进程的fork()返回值为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>

int g_mygbltest = 0;
int main(int argc, char *const *argv)
{
pid_t pid;
printf("进程开始执行!\n");
//---------------------------------
pid = fork(); //创建一个子进程

//要判断子进程是否创建成功
if(pid < 0)
{
printf("子进程创建失败,很遗憾!\n");
exit(1);
}

//走到这里,fork()成功,执行后续代码的可能是父进程,也可能是子进程
if(pid == 0)
{
//子进程,因为子进程的fork()返回值会是0;
//这里专门针对子进程的处理代码
while(1)
{
g_mygbltest++;
sleep(1); //休息1秒
printf("真是太高兴了,我是子进程的,我的进程id=%d,g_mygbltest=%d!\n",getpid(),g_mygbltest);
}
}
else
{
//这里就是父进程,因为父进程的fork()返回值会 > 0(实际返回的是子进id程)
//这是专门针对父进程的处理代码
while(1)
{
g_mygbltest++;
sleep(5); //休息5秒
printf("......。。,我是父进程的,我的进程id=%d,g_mygbltest=%d!\n",getpid(),g_mygbltest);
}
}

return 0;
}

image-20220125123516570

fork()失败原因

  1. 系统中进程太多了,缺省情况,最大的pid:32767
  2. 每个用户有个允许开启的进程总数最大值:7788