🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:fork进程创建,理解fork返回值和常规用法,进程终止(退出码、退出场景、退出方法、exit),进程等待(wait、waitpid),阻塞等待和非阻塞等待
🚢🚢作者简介:本科在读,计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
目录
一、进程创建
1.fork函数初识
2.fork返回值
(1)如何理解 fork函数有两个返回值
(2)如何理解 fork返回后,给父进程返回子进程的 pid,给子进程返回 0?
(3)如何理解同一个 id值,会返回两个不同的值,让 if 和 else if 同时执行
(4)理解写时拷贝
3.fork常规用法
4.fork调用失败的原因
二、进程终止
1.进程退出码
2.进程退出场景
3.进程常见退出方法
4.exit函数
5._exit函数
6.return
三.进程等待
1.wait方法
2.waitpid方法
3.获取子进程status
4.进程等待总结
四、再谈进程退出
五、阻塞等待 vs 非阻塞等待
1.阻塞等待应用示例
2.非阻塞等待应用示例
一、进程创建
1.fork函数初识
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
如下图所示,调用fork后,将生成一个新的进程
代码示例如下
int main(void)
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ((pid = fork()) == -1)perror("fork()"), exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
[root@localhost linux]# . / a.out
Before : pid is 43676
After : pid is 43676, fork return 43677
After : pid is 43677, fork return 0
这里看到了三行运行结果,一行before,两行after。进程43676先打印before消息,然后它又打印after。另一个after消息是43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示
我们可以看到上述代码中并没有对父子进程进行条件限制,那么在程序运行起来时,在 fork之后,会先执行父进程还是子进程呢?实际上,fork之后,谁先执行完全由调度器决定。
2.fork返回值
(1)如何理解 fork函数有两个返回值
首先,我们必须要知道 fork函数是操作系统为我们提供的,也就是说,fork操作是在操作系统内实现的。
接下来,我们一起来看下 fork函数内部的结构,然后思考一个问题:在代码在 return之前,内部的核心代码实现完了吗?
答案是,实现完了!也就是说,子进程早在 return前就创建好了,并且可能已经在 OS的运行队列中,准备被调度了。对应的,当代码运行到 return时,会有父进程、子进程两个进程各自执行return。
(2)如何理解 fork返回后,给父进程返回子进程的 pid,给子进程返回 0?
因为一个父亲的孩子可以有很多个,可是每个孩子都只有一个父亲。也就是说,孩子找父亲是具有唯一性的。以此类推,子进程 fork之后,不需要父进程的 id值,因为父进程具有唯一性。而父进程 fork之后需要对应子进程的 id,因为该父进程可能不止一个子进程,它需要对应的子进程 id做标识。
(3)如何理解同一个 id值,会返回两个不同的值,让 if 和 else if 同时执行
返回的本质:就是写入。我们不知道父子进程谁先返回,谁先返回,谁就先写入 id值。由于进程具有独立性,进程在执行 fork相应代码时,会在操作系统内部进行写时拷贝,使 fork对应的进程可以返回两个不同的值,再让对应的父子进程根据自己返回的 id,去执行 if 或 else if 中的代码内容。
(4)理解写时拷贝
通常情况下,父子代码共享,父子进程在不写入(不修改共享部分的数据)时,对应的数据也是共享的。当任意一方试图写入,操作系统便会以写时拷贝的方式,给需要修改的一方在物理内存中开辟一块新空间,将原来的数据拷贝到新空间中,再对新空间中的数据做修改。具体见下图:
———— 我是一条知识分割线 ————
3.fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
4.fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制(一个用户可以创建的进程是有限制的)
二、进程终止
1.进程退出码
这里给大家讲解一下进程退出码,我们知道写代码是为了完成某件事情,那我们如何知道这件事完成的怎么样呢?我们可以通过进程退出码来了解。
进程在退出时,会有对应的退出码,标定进程执行的结果是否正确。
退出码的意义:0:success,!0:标识失败。 !0具体是几,标识不同的错误 -- 数字对人不友好,对计算机友好。
可以通过 echo $? 查看进程退出码
./mytest #运行可执行程序mytest
echo $? #永远记录了最近一个进程在命令行中执行完毕时对应的退出码(main->return ?;),这里的?为一个变量
———— 我是一条知识分割线 ————
一般而言,退出码,都必须有对应的文字描述,
- 可以自定义;
- 可以使用系统的映射关系(不太频繁)。
补充:在循环内定义变量,如果Linux系统版本比较低,需要在 makefile文件的依赖生成关系的行末加 -std=c99。
strerror函数展现所有退出码 - 查看系统不同数字的映射关系
#include<string.h>
int main()
{
for (int i = 0; i < 200; i++)
{
printf("%d: %s\n", i, strerror(i));
}
return 0;
}
2.进程退出场景
- 代码运行完毕,结果正确,return 0;
- 代码运行完毕,结果不正确,return !0;
- 代码异常终止,退出码无意义。
3.进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
- 1. 从main函数return返回;
- 2. 任意地方调用exit(code);
- 3. _exit。
异常退出:
- ctrl + c,信号终止
exit - 库函数的一种,作用为终止一个进程的函数(头文件 #include<stdlib.h>),在调用 exit时,进程就终止退出了,代码不会继续向下执行。
_exit - 系统调用的一种,作用为终止一个进程的函数(头文件 #include<unistd.h>),在调用 _exit时,进程就终止退出了,代码不会继续向下执行。
我们尝试在Linux中编译下面的代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello");
exit(0);
}
运行结果:
[root@localhost linux] # . / a.out
hello[root@localhost linux]#
int main()
{
printf("hello");
_exit(0);
}
运行结果:
[root@localhost linux] # . / a.out
[root@localhost linux]#
运行后我们会发现一个现象,exit(0)代码运行需要几秒后才能把 hello打印出来,这是因为数据在缓冲区中,进程退出后才能在缓冲区刷到我们对的屏幕上。但是当我们使用 _exit(0)时,频幕上不会有任何信息打印,也就是说,_exit()退出后不会刷新缓冲区。
总结:exit 终止进程,主动刷新缓冲区;_exit 终止进程,不会刷新缓冲区。缓冲区存在于用户层。
4.exit函数
#include <unistd.h>
void exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值,后面第三节会讲
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
exit最后也会调用 _exit, 但在调用 _exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数;
- 关闭所有打开的流,所有的缓存数据均被写入;
- 调用_exit。
5._exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
6.return
在我们编写 C/C++代码时,通常我们的 main函数结尾都会带有一段 return 0 的代码,不知道朋友们有没有想过这里的 return 0 的作用是什么呢?
结合我们上面的讲解,我们不难理解其实这里的 0就是进程退出码,如果我们不关心进程退出码,return 0就可以了。如果未来我们是要关心进程退出码的时候,要返回特定的数据表明不同的错误。
三.进程等待
我们之前就学习过僵尸进程的相关知识,我们知道在一个在进程退出的时候,需要退出的相关信息返回给父进程。那么假如子进程处于僵尸状态无法退出返回相关信息怎么办呢?这里就需要用到我们进程等待的知识了。我们可以通过进程等待的方式解决僵尸进程。
进程等待的必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
1.wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回 -1。
参数:
输出型参数,获取子进程退出状态, 不关心则可以设置成为NULL
常见使用方法
pid_t ret = wait(NULL);
代码示例
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
int cnt = 10;
while (cnt)
{
printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(0); //进程退出
}
// 父进程
sleep(15);
pid_t ret = wait(NULL);
if (id > 0)
{
printf("wait success: %d", ret);
}
sleep(5);
}
注意:这里我们可以把 wait理解为一个函数调用,目的是返回一个值(子进程的返回信息),给父进程。
2.waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
常见使用方法
int status = 0; // 不是被整体使用的,有自己的位图结构
pid_t ret = waitpid(id, &status, 0); //wait子进程
补充:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
3.获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
退出状态中保存的是进程的退出码(判断运行结果是否正确),终止信号可以知道进程是否正常退出/结束。终止信号为0代表正常结束,只有终止信号为0才能去看退出状态,退出状态为0则代表运行结果正确,其他数字则代表其他不同的运行情况。
- 代码示例1(代码正常结束、运行正确,返回退出/终止状态信号0、exit的退出码10,这里的10是我自己自定义的)
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(10); //进程退出
}
// 父进程
int status = 0; // 不是被整体使用的,有自己的位图结构
pid_t ret = waitpid(id, &status, 0);
if (id > 0)
{
printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);
}
sleep(5);
}
信号编号(终止信号)为低七位,我们可以通过 status & 0x7F获得;
退出码(在退出状态中)在次低八位,我们可以通过 (status>>8)&0xFF获得;
在程序正常运行的情况下,使用 exit返回,终止信号为0,退出码为 exit(?) 的 ?值。
- 代码示例2(野指针,返回退出/终止状态信号11、exit的退出码0,这里的0是系统自动返回的)
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
int* p = NULL;//野指针
*p = 100;
}
exit(10); //进程退出
}
// 父进程
int status = 0; // 不是被整体使用的,有自己的位图结构
pid_t ret = waitpid(id, &status, 0);
if (id > 0)
{
printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);
}
sleep(5);
}
在程序非正常运行时,使用 exit返回,终止信号为对应的信号值,退出码为0。终止信号不为0,代表非正常退出。
11)SIGSEGV - 段错误,出现野指针后会有此类错误
- 代码示例3(借助status值,检验子进程是否正常退出)
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
int main()
{
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
//child
int cnt = 10;
while (cnt)
{
printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
// int *p = 0;
// *p = 100; //野指针问题
}
exit(10);
}
int status = 0;
// 1. 让OS释放子进程的僵尸状态
// 2. 获取子进程的退出结果
// 在等待期间,子进程没有退出的时候,父进程只能阻塞等待
int ret = waitpid(id, &status, 0);
if (ret > 0)
{
// 是否正常退出
if (WIFEXITED(status))
{
// 判断子进程运行结果是否ok
printf("exit code: %d\n", WEXITSTATUS(status));
}
else {
//TODO
printf("child exit not normal!\n");
}
//printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
}
return 0;
}
4.进程等待总结
wait和waitpid中的status参数,可以在子进程处于阻塞状态(运行完但是运行信息还没有被父进程回收)时,检测子进程的退出信息,将子进程存储于 PCB中的退出信息拿回到父进程中。这个操作由造作系统完成。
四、再谈进程退出
- 进程退出会变成僵尸,同时该进程也会把自己对应的退出码写入到自己的 task_struct中;
- wait/waitpid 是一个系统调用,也就是说,它们是由 OS完成的,OS有能力去读取子进程的task_struct;
- 所以,父进程获取到的子进程退出信息,是从退出子进程的 task_struct中获取到的。
下面是 Linux源码 task_struct中的部分内容
五、阻塞等待 vs 非阻塞等待
- 阻塞等待:持续检测子进程状态,不进行其他工作;
- 非阻塞等待:间隔性检测子进程状态,如果没有就绪,直接返回。在子进程没有退出的前提下,每一次的退出都是以此非阻塞等待;
- 非阻塞等待下,获取子进程退出状态成功和不成功,对应的返回值不同。成功返回一个大于0的值,不成功则返回0。
- 多次非阻塞等待称为轮询。
非阻塞等待有什么好处?
不会让等待占用父进程所有的精力,可以在轮询期间,干干别的!具体是什么意思呢?简单来说,就是父进程每隔一段时间去查看等待是否成功,在此期间,父进程可以进行别的工作。
1.阻塞等待应用示例
int main()
{
pid_t pid;
pid = fork();
if (pid < 0) {
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0)
{
//child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if (WIFEXITED(status) && ret == pid) {
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else {
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
运行结果:
[root@localhost linux] # . / a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is : 1.
可以观察到,进程在打印 child is run, pid is : 45110 5s之后才打印后续的代码
2.非阻塞等待应用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#define NUM 10
typedef void (*func_t)(); //函数指针
func_t handlerTask[NUM]; //函数指针数组
//样例任务
void task1()
{
printf("handler task1\n");
}
void task2()
{
printf("handler task1\n");
}
void task3()
{
printf("handler task1\n");
}
void loadTask()
{
memset(handlerTask, 0, sizeof(handlerTask));//将数组handlerTask初始化为0,需要#include <string.h>
handlerTask[0] = task1;//将3个函数指针对应3个任务
handlerTask[1] = task1;
handlerTask[2] = task1;
}
void addtask()
{}
int main()
{
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
//child
int cnt = 10;
while (cnt)
{
printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
exit(10);
}
loadTask(); //给父进程装载一批任务
// parent
int status = 0;
while (1)
{
pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
if (ret == 0)
{
// waitpid调用成功 && 子进程没退出
//子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.
printf("wait done, but child is running...., parent running other things\n");
for (int i = 0; handlerTask[i] != NULL; i++)
{
handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情
}
}
else if (ret > 0)
{
// 1.waitpid调用成功 && 子进程退出了
printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
break;
}
else
{
// waitpid调用失败
printf("waitpid call failed\n");
// break;
}
sleep(1);
}
return 0;
}
可以观察到,在子进程还没有退出的时候,父进程完成了其他的工作
🌹🌹 Linux下的fork进程创建 & 进程终止 & 进程等待 的知识大概就讲到这里啦,博主后续会继续更新更多Linux操作系统的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪