【Linux进程】三、进程控制——fork()系统调用深度刨析
前言: 本文介绍了进程控制相关的命令与函数,并深入讨论了fork函数的实现机制以及fork函数的一次调用两次返回,最后通过实例演示了如何使用fork函数控制进程的创建。
🥇1. fork()、getpid()、getppid()函数介绍
🥈1.1 fork()函数介绍
fork()用于创建一个子进程,我们在shell下执行一个命令其实也是通过fork()实现的,fork()是Linux下最基本的一个系统调用。fork()最大的特点就是一次调用,两次返回,两次返回主要是区分父子进程,因为fork()之后将出现两个进程,所以有两个返回值,父进程返回子进程ID,子进程返回0。
- 包含头文件
#include <unistd.h>
- 函数原型
pid_t fork(void);
-
函数功能
fork() creates a new process by duplicating the calling process. The new process, referred to as the child, the calling process, referred to as the parent. 通过复制的方式创建一个进程,被创建的进程称为子进程,调用进程称为父进程,复制的子进程是从父进程fork()调用后面的语句开始执行的。
-
函数参数
- void
-
函数返回值
-
On success, the PID of the child process is returned in the parent, and 0 is returned in the child.
- 父进程返回子进程ID
- 子进程返回0
-
On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately. 失败返回-1并设置errno。
-
🥈1.2 getpid()函数与getppid()函数介绍
- 包含头文件
#include <sys/types.h>
#include <unistd.h>
- 函数原型
pid_t getpid(void);
pid_t getppid(void);
-
函数功能
- getpid() returns the process ID of the calling process. 获得当前进程的ID。
- getppid() returns the process ID of the parent of the calling process. 获得当前进程的父进程的ID。
-
函数参数
void
-
函数返回值
- getpid()返回当前进程ID
- getppid()返回当前进程的父进程ID
🥇2. fork()工作机制
🥈2.1 fork()的实现机制——一次调用两次返回与进程复制
下面通过一个案例来分析fork()是如何创建进程,又是如何返回的。
/************************************************************
>File Name : fork_test.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月18日 星期三 15时59分29秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char* argv[])
{
printf("=== process begin ===\n");
pid_t pid = fork();
if(pid == -1)
{
perror("fork err");
return -1;
}
if(pid == 0) /*子进程*/
{
printf("i am child: %d, may parent: %d\n", getpid(), getppid());
/* test2
while(1)
{
printf("fork process\n");
sleep(1);
}
*/
}
if(pid > 0)
{
printf("i am call: %d, child: %d, parent: %d\n", getpid(), pid, getppid());
/* test1
sleep(1);
*/
/* test2
while(1)
{
sleep(1);
}
*/
}
printf("=== process end ===\n");
return 0;
}
编译运行该程序,我们会发现一个很有意思的现象
首先反常的第一点,我们在程序中的打印顺序是先进入子进程(pid == 0)分支,再进入父进程(pid > 0)分支,但实际的打印顺序是先执行了父进程分支的printf()函数,后执行的子进程分支到的printf()函数;第二点是,在执行子进程的printf()函数时,竟然已经回到了shell下,可以看图中高亮标出的位置。下面对着两点详细分析;第三点,子进程打印的父进程ID和父进程自己打印的ID不同。
我们已经知道,fork()系统调用的特点是一次调用两次返回,并且子进程的创建是对父进程的复制,那么是从哪复制开始复制的呢,我们根据程序运行结果分析,程序只打印了一次begin语句,说明不是从头开始复制的,实际上它是从fork()的下一句开始复制的,从fork()开始,后面就成了两个分支。
我们看到的运行结果中红色标记的①,实际上是由父进程打印的,②是由子进程打印的,既然不是一个进程打印的,那也就没有先后顺序的问题了。而子进程打印的父进程ID是1,父进程打印的自己的ID是5270,这是因为在子进程结束前,父进程就已经结束了,新建的子进程变成了孤儿进程,所以它会被1号进程收养,所以新建子进程的父进程ID是1,这也是为什么第二个printf()语句是在shell下执行的原因,因为原来的父进程结束了,所以回到了shell进程下,此时子进程还没有结束,它被1号进程接管,继续执行后面的语句,直到结束。
(实际上,这里的3397进程就是我们的shell进程,shell进程是我们自己启动的进程的父进程;而1号进程则是init进程,init进程是Linux下最原始的进程,是所有进程最终的父进程。)
我们可以在父进程中加一个sleep()函数(放开上面代码中test1注释掉的代码即可),让父进程等一下子进程,并看一下效果,这次就好了。
🥈2.2 shell进程控制命令
下面我们通过shell下的进程控制命令进一步分析上面所讲的fork()实现机制,首先介绍几个命令:
-
ps 查看进程信息,主要用到下面两个参数
-
ps aux
-
ps ajx:可以查看父进程ID,追溯进程之间的关系
-
-
kill 给进程发送信号,通过这个命令可以杀死进程,常用的两个参数
- kill -l:查看所有信号;
- kill -9 pid:给进程号为pid的进程发送9号信号,杀死进程,实际上相当于 kill -SIGKILL pid,也可以直接通过 kill pid 来杀死pid进程;
我们再做一个测试,将上面代码中的test2处的注释放开,编译并运行程序,让两个进程一直在while中执行
开始循环后,我们另起一个shell来查看进程信息,可以通过管道和grep过滤我们需要的进程信息
通过ajx追溯进程血缘关系
可以看到fork()的调用进程5721,它的父进程是3397也就是 bash shell 进程
通过kill杀死父进程,可以看到子进程被1号进程接管
1号进程就是init进程
🥇3. 进程创建的控制
🥈3.1 控制进程创建个数
我们通过一个for循环来创建进程
/************************************************************
>File Name : mutifork.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月18日 星期三 19时33分50秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char* argv[])
{
int i = 0;
pid_t pid = 0;
for(i = 0; i < 5; i++)
{
pid = fork();
if(pid == 0)
{
printf("i am chiled: %d, ppid: %d\n", getpid(), getppid());
/*
break;
*/
}
if(pid > 0)
{
printf("i am call: %d, child:%d, ppid: %d\n", getpid(), pid, getppid());
}
}
while(1)
{
sleep(1);
}
return 0;
}
编译执行,在程序我们期望的是创建5个进程,但是实际运行后出现了一大堆进程,我们可以用wc命令统计一下
shell命令统计创建的进程个数
ps aux | grep mutifork | grep -v grep | wc -l
总共有32个进程,我们在程序中只循环了5次,为什么有32个进程呢,下面看一张图
每次fork的时候,进程都会一分为二,所以5次循环相当于创建了2的5次方,也就是32个进程。要想避免这种情况,只需要根据返回值判断当前为子进程的时候就退出循环即可,也就是把上面代码中注释掉的break放开即可。
🥈3.2 进程顺序控制
使用fork()创建的进程都是一样的,在操作系统看来没有区别,先后顺序也是不确定的,我们要想控制进程的退出顺序,需要自己去实现这个逻辑。比如说我们可以依据for循环中i的值来判断哪个进程先创建的,哪个进程后创建的,按照逻辑i小的应该是先创建的,因为C语言就是顺序执行的。因为子进程创建出来就break退出for循环了,所以五个子进程对应的i是0-4,而只有最开始的父进程可以执行到i=5。
sleep(i); /*不同进程睡眠时间不同,第一个创建的进程
i的值为0,睡眠最短,最先退出,后面的进
程对应的i逐渐增大,睡眠时间增加,退出越晚*/
if(i < 5)
{
printf("child: %d, parent: %d\n", getpid(), getppid());
}
else
{
printf("parent: %d\n", getpid());
}