这里写目录标题
- [<font color="FF00FF">1. 进程创建](#1. 进程创建)
-
- [<font color="FF00FF">1.1 fork函数](#1.1 fork函数)
- [<font color="FF00FF">1.2 写时拷贝](#1.2 写时拷贝)
- [<font color="FF00FF">2. 进程终止](#2. 进程终止)
-
- [<font color="FF00FF">2.1 进程退出场景](#2.1 进程退出场景)
- [<font color="FF00FF">2.2 exit](#2.2 exit)
- [<font color="FF00FF">2.3 _exit](#2.3 _exit)
- [<font color="FF00FF">exit vs _exit](#exit vs _exit)
- [<font color="FF00FF">3. 进程等待](#3. 进程等待)
-
- [<font color="FF00FF">3.1 为什么要进程等待](#3.1 为什么要进程等待)
- [<font color="FF00FF">3.2 进程等待的方法](#3.2 进程等待的方法)
-
- [<font color="FF00FF">3.2.1 wait方法](#3.2.1 wait方法)
- [<font color="FF00FF">3.2.2 waitpid方法](#3.2.2 waitpid方法)
- [<font color="FF00FF">4. 获取子进程status](#4. 获取子进程status)
- [<font color="FF00FF">5. 三个问题](#5. 三个问题)
1. 进程创建
1.1 fork函数
-
fork函数可以从已存在进程中创建一个新进程
新进程为子进程,而原进程为父进程。 -
fork之前父进程独立执行,fork之后,父子两个执行流分别执行
注意,fork之后,谁先执行,完全由调度器决定。
1.2 写时拷贝
-
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本

页表里面存在权限,代码段是只读的,但是当我们没有创建子进程之前其实父进程页表里存的数据段是读写的,但当我们创建子进程后,操作系统就会把数据段的权限也改成只读的,之后父子进程的任何一方尝试对数据段写入时,操作系统会发现你的数据是合法的,因为虚拟地址和物理地址都有,并且操作系统知道访问的数据是数据段,操作系统检测到用户要对一个只读的区域写入,操作系统检查到该区域是数据段,而且当前是子进程,此时发生写时拷贝 -
操作系统为什么知道你访问的数据在那个区域?
因为每个vm_area_struct里面有各个区域的起始地址和结束地址,所以访问某个区域时,虚拟地址一定是在这个区域的_start和_end这个区间里,所以操作系统知道你访问的区域是哪里
- 为什么要写时拷贝?不能在创建子进程的时候,先把父进程的数据拷贝给子进程,然后再创建呢?此时父子进程不也独立吗?
2.1. 如果在创建子进程时,先把父进程的数据拷贝给子进程,之后再创建的话,创建子进程就会变慢
2.2. 如果父进程的数据段有100个变量,而子进程只改40个,此时如果你把父进程的变量全部拷贝给子进程就会导致物理内存有大量的重复数据,此时空间浪费
- 所以存在写时拷贝的原因
3.1.减少创建子进程的时间
3.2. 减少内存浪费
2. 进程终止
2.1 进程退出场景
-
代码运行完毕,结果正确:进程退出码为0
-
代码运行完毕,结果不正确:进程退出码是不同的值,表面不同的出错原因
-
代码异常终止:退出码没意义


-
这个文件在当前目录下不存在,所以退出码是-1,而这个退出码一般父进程要拿到,因为父进程创建子进程一定是要完成某种任务的,也就是说子进程要执行部分父进程的代码,所以子进程完成任务的结果要被父进程知道,子进程是完成任务后正常退出,还是完成任务后结果不正确,出错原因是什么
-
echo ?:打印最近一次程序(进程)退出时的退出码,所以如果你写两次echo ?,第二次的退出码就是0,因为第二次拿到的退出码是echo这个进程退出时的退出码
这个代码里main函数退出就是该进程退出,所以main函数程序退出时的返回值也叫该进程的退出码
-
当进程退出时时,要把进程的退出码写到该进程的task_struct内部,所以父进程bash就可以读取子进程的task_struct,就可以知道子进程的退出结果
-
可以使用 strerror 函数来获取退出码对应的描述,linux上一共有134个错误码




打开文件失败会创建errno表明错误信息


打开文件失败就会创建errno,所以可以直接返回errno,退出码是2,表面没有该文件或目录


-
此时浮点数异常,但返回值却不是0,或者我们可以随便返回一个值,但返回值都是136,因为进程异常终止退出码没意义,进程一旦出现异常,一般是进程收到了信号
-
main函数结束,表示进程结束
-
其它函数,只表示自己函数调用完成,返回


2.2 exit
return是⼀种常见的退出进程方法。
执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。
区别:
- 任何地方调用exit,表示进程结束,并返回给父进程bash子进程的退出码
- return只有在main函数里返回才表示进程结束
2.3 _exit
用_exit替换掉exit效果一样,那它们两个的区别是什么呢?
exit vs _exit
-
exit是c语言提供的接口,而_exit是系统调用
-
exit会刷新缓冲区,但_exit不会刷新缓冲区
-
进程只能被操作系统杀掉或终止,但是exit这个库函数也可以终止进程,说明exit在底层封装了_exit,它会调用_exit这个系统调用来终止进程,也只能通过系统调用终止进程
-
所以prinrf,然后\n刷新缓冲区,它一定不是操作系统内部的缓冲区,因为_exit不会刷新缓冲区,这个缓冲区在库里面,也叫库缓冲区,是c语言提供的缓冲区,因为exit可以刷新缓冲区


printf会把写入的数据放到缓冲区里,但因为库缓冲区是行缓冲,所以\n直接刷新缓冲区,把内容打印到显示器上,所以现象是先打印出消息,再停止2s后,进程退出


而不加\n,此时现象就是等2s后,打印出消息,进程退出
换成_exit后,此时如果有\n,_exit和exit的效果一样


如果没有\n,不能向显示器打印消息
3. 进程等待
3.1 为什么要进程等待
-
子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而导致内存泄漏。
-
另外,进程一旦变成僵尸状态,kill-9也无法杀掉进程,因为谁也没有办法杀死一个已经死去的进程
-
最后,父进程派给子进程的任务完成的如何,我们需要知道。
如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待的方法
3.2.1 wait方法

-
如果等待子进程,子进程没有退出,父进程会阻塞在wait调用处,直到子进程退出
-
等待的是任意一个退出的子进程,等待成功返回的是目标僵尸进程的pid



这个代码就是先让子进程运行5秒,然后进入僵尸,10秒后,父进程等待子进程成功,子进程僵尸状态被回收,打印子进程pid,再过5秒,父进程退出
3.2.2 waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
-
返回值:
-
当正常返回的时候waitpid返回收集到的子进程的进程ID
-
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
-
参数
pid:
- Pid = -1,等待任意一个子进程,与wait等效
- Pid>0,等待其进程ID与pid相等的子进程
options:默认为0,表示阻塞等待
阻塞等待:
-
父进程等待子进程的时候,如果子进程一直不退,父进程就一直等待子进程直到子进程退出,等待成功,返回等待成功后的子进程的pid
-
等待子进程的时候父进程不可以做自己的事,只能成功等待子进程后,才可以做自己的事情
非阻塞等待:
- 返回值大于0,等待结束
- 返回值等于0:waitpid方法调用结束,但子进程没有退出
- 返回值小于0:等待失败
- 等待子进程的时候,父进程可以做自己的事,所以非阻塞等待效率高,因为可以父子进程并发执行
- WNOHANG:设置非阻塞等待,若pid指定的子进程没有退出,则waitpid()函数返回0,本轮不予以等待,父进程可以做自己的事情,等到下轮继续看子进程是否退出,若子进程正常退出,则返回该子进程的ID
c
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
typedef void(*fun_t)();
#define Num 5
fun_t handlers[Num+1];
void Downloads()
{
printf("这是一个下载的任务\n");
}
void Flush()
{
printf("这是一个刷新的任务\n");
}
void Log()
{
printf("这是一个日志任务\n");
}
void registerhandlers(fun_t h[],fun_t f)
{
int i=0;
for( i=0;i<Num;i++)
{
if(h[i]==NULL)
{
break;
}
}
if(i==Num)
{
return;
}
h[i]=f;
h[i+1]=NULL;
}
int main()
{
registerhandlers(handlers,Downloads);
registerhandlers(handlers,Flush);
registerhandlers(handlers,Log);
pid_t id = fork();
if (id == 0)
{
while (1)
{
printf("我是子进程,pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
while(1)
{
int status = 0;
pid_t tid = waitpid(id, &status, WNOHANG);
if (tid > 0)
{
// printf("wait success ,pid: %d, status: %d , exit signal: %d\n", tid, (status >> 8)&0xFF, status & 0x7F);
printf("wait success ,pid: %d, status: %d , exit signal: %d\n", tid, WEXITSTATUS(status), status & 0x7F);
break;
}
else if(tid==0)
{
int i=0;
for(i=0;handlers[i];i++)
{
handlers[i]();
}
printf("本轮调用结束,子进程没有退出\n");
sleep(1);
}
else
{
printf("wait fail!\n");
break;
}
}
return 0;

这里可以看到,父进程在等待子进程的时候,父进程可以执行其它任务,也就是可以做自己的事,如果是阻塞等待父进程就会一直等待子进程退出,自己干不了任何事
- 非阻塞等待一般采用非阻塞轮询,由循环完成,就是一直循环直到子进程退出



kill -9 pid 杀掉子进程,此时子进程退出,等待成功
4. 获取子进程status


status是获取子进程退出状态的,但是这里的子进程退出码是1,为什么打印的是256呢?
wait和waitpid,都有一个status参数,该参数是⼀个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
- status不能简单的当作整形来看待,可以当作位图来看待,如下图:

其实status是个位图,进程的退出码只存在在status低16比特位中,而这16个比特位,的低7位是终止信号,第8位是core dump标志,而高8位是进程的退出状态,所以exit(1),是0000000100000000,所以status是2的8次幂,结果是256
那怎么获取进程退出状态呢?
把status右移8位,此时后8位就被移走了,高位补0,而获取进程的退出状态只保留低8位,所以(status>>8)&0xFF,此时拿到的就是低8位,因为0xFF是前24位是0,后8位是1,所以此时就可以获取进程的退出状态,也就是后8位
代码异常终止:低7位保存异常时对应的信号编号
- 所以没有异常就代表低7个比特位是0
- 一旦低7个比特位!0,异常退出,退出码无意义
我们同样可以获取信号,只要把status&0x7F就可以获取信号,因为0x7F是前25位是0,后7位为1,此时获取后7位



此时signal是0,代表代码运行完毕,status(退出码)为10,代表结果不正确,这就是进程退出场景
如果kill -9 pid杀掉该进程此时,退出码无意义,因为代码都没有成功运行,没有正常退出,退出码也就没意义了


这个代码有除0错误,会引发异常,所以操作系统会发信号中断该进程,此时的信号是8,就是浮点数错误,也就是除0异常

为什么不能定义全局变量,然后子进程修改该变量,此时父进程可以拿到子进程的退出信息吗?
拿不到,因为子进程修改全局变量发生写时拷贝,父进程拿不到子进程的退出信息,父进程只能通过系统调用的方式来拿子进程的退出信息
5. 三个问题
- 父进程怎么能拿到子进程的退出信息?

- 僵尸状态怎么解决的?
就是父进程通过系统调用获取子进程的pcb里面的信息,从而解决僵尸状态,下面是内核代码,有进程状态,所以一旦发现僵尸状态。才会把子进程的资源回收,我们用了waitpid这个系统调用后,底层操作系统就自己回收了
status: 输出型参数
WIFEXITED(status):若进程正常退出,则为真。
(查看进程是否是正常退出)
WEXITSTATUS(status):提取子进程退出码。
(查看进程的退出码)
这里的WEXITSTATUS(status):等同于(status >> 8)&0xFF,而WIFEXITED(status)一般用来判断子进程是否正常退出


子进程退出异常就不获取子进程的消息了,没有意义