文章目录
进程创建
./ 执行程序
或者调用fork函数 本质都是创建进程
首先我们先新建工程对应的makefile文件
但是如果我们想使用C99的标准我们就需要写成:
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程
#include <unistd.h>
pid_t fork(void);
//fork函数有两个返回值:子进程中返回0,父进程返回子进程id
//如果fork函数创建子进程失败, 返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核会做的事情:
- 分配新的内存块加载代码和数据, 内核数据结构(包括进程控制块PCB,地址空间,页表,构建映射关系)给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表(运行队列)当中
- fork返回之后,开始调度器调度
fork执行之前父进程独立执行,fork之后,父子两个执行流分别执行
注意,fork之后,谁先执行完全由调度器决定
fork函数返回值
- 子进程返回0
- 父进程返回的是子进程的pid
问:fork函数为什么要给子进程返回0,给父进程返回子进程的PID
答:一个父进程可以创建多个子进程,而一个子进程只能有一个父进程.因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务
问2:为什么fork函数有两个返回值
答:父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块.创建子进程的进程地址空间.创建子进程对应的页表等等.子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了
也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因
例子:
执行结果:
写时拷贝
默认情况下,子进程会继承父进程的代码和数据父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝
的方式各自一份副本
在父/子修改数据时,会发生缺页中断:OS再开辟一段空间,把数据拷贝过来(写时拷贝),重新建立映射关系;
父子分开,更改读写权限.这时候再进行写操作 这样保证了父子进程的独立性
1.为什么数据要进行写时拷贝?
进程具有独立性.多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程
2.为什么不在创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间
3.代码会不会进行写时拷贝?
90%是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝
fork常规用法
- 一个进程希望复制自己,使父子进程执行同一个代码的不同的代码段
例如,父进程等待客户端请求,生成子进程来处理请求.
- 一个进程要执行一个不同的程序
例如子进程从fork返回后,调用exec*函数.
fork调用失败的原因
- 系统中有太多的进程导致资源的不足 -> 因为创建进程的成本很大 (空间+时间)
- 实际用户的进程数超过了限制 -> 防止某些用户创建多个无用进程
进程终止
进程退出场景
问:进程退出在操作系统层面上发生了什么?
意味着少了一个进程 -> 释放其对应的PCB,释放其的mm_strucu , 释放其页表即各种映射关系
注意:进程卡住了,不算进程退出!
- 代码运行完毕,结果正确 -> 错误码为0
例子:
- 代码运行完毕,结果不正确 -> 错误码非0
例子:
- 代码异常终止( 进程崩溃) ->这是运行时错误,即程序崩溃,或者浮点数错误(/0)等…
- 异常终止的本质是这个进程因为异常问题,导致自己收到了某种信号
例子:
运行结果:
程序崩溃时,退出码没有意义,因为没有执行return ,又上述也可看出,没有执行打印语句后面的内容
进程常见退出方法
问:为什么我们写main函数的时候,退出的时候总是return 0,这个0有什么意义?
- 我们平常写代码,函数return的值就是进程的退出码
- 用退出码衡量程序的结果是否正确 / 程序是否有错误,0表示成功
此时可以通过 echo $?
查看最近一次进程的退出码
-
1.如果代码运行完毕,执行结果正确 -> 退出码为0 代表success
-
2.代码运行完毕,执行结果不正确 -> 退出码非0, 代表failed
为什么会不正确呢?有很多种可能,我们可以根据错误码(strerror)对应的字符串进行判断
-
3.代码异常终止 -> 程序崩溃 ->此时退出码没有意义
查找错误码对应的内容:
strerror函数功能:查看错误码种类的函数,返回的是一个字符串
执行结果:
问:为什么echo $?可以查看进程的退出码?
原因:bash是所有在命令行启动的进程的父进程,bash一定是通过wait方式得到子进程的退出结果,所有echo $?可以查看子进程的退出码
问2:为什么以0表示代码执行成功,以非0表示代码执行错误
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足.非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因
正常终止
- 从main返回 return退出
main函数,return代表进程退出,return的值就是进程的退出码!
注意:非main函数return的值,代表的是函数返回,并不是退出码
return是一种更常见的退出进程方法.执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数
例子
在Linux下,指令本质也是可执行程序->也是进程,所以我们可以查看指令对应的退出码,这些命令成功执行,退出码为0 ,但是如果命令执行出错,退出码就是非0的数字,该数字代表具体的某一种错误信息
注意:退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同
2.exit退出
exit可以在任意地方调用
,都代表进程终止 -> 即exit后序的代码不执行,exit的参数代表的就是退出码
status:退出码, 可以为:exit(EXIT_SUCCESS) 或者为:exit(EXIT_FAILURE)
#include <unistd.h>
void exit(int status);
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数.
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
注意:exit终止进程前会先刷新缓冲区,将缓冲区当中的数据写入
例子:
3._exit退出
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用.所以**_exit(-1)时,在终端执行$?发现返回值是255**
首先我们知道:显示器的刷新策略是行刷新, \n可以进行刷新
但是exit函数退出和return 在进程退出时,还会要求系统进行刷新缓冲区
_exit函数也可以在代码中的任何地方退出进程 但是_exit函数会强制终止进程,不会进行进程的首尾工作,比如刷新缓冲区
return ,exit和_exit的区别:
只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用
使用exit函数退出进程前,exit函数会执行用户定义的清理函数.刷新缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作
return ,exit和_exit的关系:
执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数
使用exit函数退出进程前,exit函数会先执行用户定义的清理函数.冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程
异常退出
- 向进程发送信号导致进程异常退出
- 如:在进程运行的时候,给进程发送9号信号杀死进程 或者 ctrl + c终止进程
- 代码错误导致进程运行时异常退出
- 如:出现浮点数错误(整数/0的情况) 或者存在野指针的情况,使得进程在运行时异常退出
进程等待
进程等待必要性
- 1.子进程退出,父进程如果不读取子进程的退出信息, 就可能造成子进程变成 ‘僵尸进程’的问题,进而造成内存泄漏. 需要通过父进程wait释放该子进程占用的资源
- 2.进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 命令也无能为力,因为谁也没有办法杀死一个已经死去的进程
- 3.通过fork创建子进程的目的就是帮助父进程完成某种任务 父进程需要知道派给子进程的任务完成的如何,所以让父进程fork之后,需要通过wait/waitpid等待子进程退出
- 子进程:为了帮助父进程完成某种任务,父进程需要知道子进程运行完成,结果对还是不对,或者是否正常退出
- 4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
- 5.保证时序问题,保证子进程先退出,父进程后退出
进程等待的方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
-
作用: 等待任意子进程结束
-
返回值: 等待成功:返回被等待进程的pid,等待失败返回-1.
-
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
例子1:子进程变成了僵尸进程,父进程通过等待回收
验证:fork之后,我们让父进程休眠6s,然后使用wait函数等待子进程结束 在前3s的时候,子进程正常执行,后3s,子进程执行结束,exit退出,子进程进入僵尸状态, 此时父进程刚好结束休眠,开始等待,然后父进程会去读取子进程的退出信息,回收子进程
写一个监控脚本:
while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep;sleep 1; echo "------------------------------------------------------"; done
运行结果:我们可以看到wait确实可以回收僵尸进程:
例子2:wait防止子进程变成僵尸进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 10;
while(count)
{
printf("Child[%d] running,%d seconds \n",getpid(),count);
count--;
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(NULL);//等待任意子进程
if(ret>0)
{
//等待成功
printf("father wait child[%d] success\n",ret);
}
else
{
printf("father wait failed\n");
}
sleep(3);
return 0;
}
waitpid方法
#include<sys/types.h>
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);
作用:可以等待任意子进程 也可以等待指定子进程
返回值:
1.等待成功,返回被等待进程的的pid
2.如果第三个参数为WNOHANG, 而waitpid发现没有已退出的子进程可收集,则返回0;
3.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
简单来说:等待成功,返回被等待进程的PID,等待失败,返回-1
参数解析:
pid:
Pid=-1,表示等待任意一个子进程.与wait等效
Pid>0. 等待其进程ID与此时第一个参数pid值相等的子进程, 即等待特定的一个进程
status:
获取子进程退出状态,不关心则可以设置成为NULL
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码.(查看进程的退出码)
options:
若设置为WNOHANG: 若等待的子进程没有结束,则waitpid()函数返回0,不予以等待
若正常结束,则返回该子进程的PID
对应该参数我们通常置为0
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息.
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
- 如果等待不存在的子进程,则立即出错返回
关于返回值进一步说明:
- 如果等待成功了,返回收集到的子进程的ID,如果等待出错,则返回-1, 这时候
errno
会被设置成相应的值用来标识特定的错误 - 如果设置了
WNOHANG
选项,而调用中watpid发现没有进程可以收集,则返回0
例子:和上面wait一样,只不过是等待方法换了
pid_t ret = wait(NULL);//等待任意一个进程
pid_t ret = waitpid(id,NULL,0);//等待指定PID为id的进程
pid_t ret = waitpid(-1,NULL,0);//等待任意的一个进程,等价于wait
获取子进程status
- 上述的wait和waitpid函数都有一个status参数,该参数是一个输出型参数,由操作系统进行填充
例子:
- 如果status参数传入NULL,表示等待的时候,不关心子进程的退出状态信息, 否则操作系统会根据该参数,将子进程的退出信息反馈给父进程
- status虽然是一个整形变量,但是我们不能简单的把 status当作整形来看待,可以当作位图来看待,status的不同比特位所代表的信息不同,具体细节看下图:(只研究status低16比特位)
正常终止情况:代码跑完,运行正确 / 不正确 -> 退出码 代码异常终止:本质是进程因为异常问题导致自己收到了某种信号
在status的低16个比特位中,高8位表示进程的退出状态即退出码. 进出如果被信号所杀,那么低7位表示进程的终止信号,而第8位比特位是core dump
标志
所以我们可以通过位运算操作,就可以根据status得到进程的退出码和终止信号
如果status的低7比特位为0,就没有收到信号,证明程序是正常运行结束的,运行结果是否正确就通过status的次低8位获得! 如果收到了信号,退出码没有意义!
退出码:exitCode = (status>>8)&0xFF //只要高8位的内容 0xFF==1111 1111
退出信号:exitSignal = status & 0x7F //只要低8位的内容 0x7F==0111 1111
例子1:代码跑完,运行正确
例子2-代码运行结束, 结果不正确
情况3:代码异常终止
- 给子进程发送终止信号,提前把进程干掉,此时退出码没有意义, 退出信号就是我们发送的信号
- 浮点数错误 (整数/0)
执行结果:
系统当中也提供了两个宏来获取 退出码 和 退出信号,这样我们就不需要自己使用位运算了
WIFEXITED(status)
:用于查看进程是否是正常退出,本质是检查是否收到终止信号WEXITSTATUS(status)
:用于获取进程的退出码
exitCode = WEXITSTATUS(status);//获取进程的退出码
exitSignal = WIFEXITED(status);//是否收到终止信号
注意:当一个进程非正常退出(由于异常终止)的时候,说明是收到了终止信号,此时进程的退出码没有意义
例子1:代码运行结束,结果不正确
例子2:代码异常结束:
从操作系统层面理解waitpid
waitpid是系统调用接口,由用户调用! 子进程陷入僵尸状态时,PCB保存进程退出时的退出数据,即exit_code和signal
这两个值, 当用户层调用waitpid接口的时候,传过去的是一个&status,操作系统就拿到status的地址,然后让父进程拿到子进程的退出数据,把退出数据弄到status上
阻塞状态和非阻塞状态
pid_ t waitpid(pid_t pid, int *status, int options);
waitpid的第三个参数options,用来设置等待方式
- 0 :默认阻塞等待
- WNOHANG :设置为非阻塞等待
一个小例子说明什么是阻塞等待和非阻塞等待:
阻塞等待:就是我有事情找张三, 我给张三打电话叫他下来.他说要30分钟才下来,没问题 我等你,但是别挂电话,当你好的时候你在电话跟我说一声.此时我就知道你要下来了.这种不挂电话的方式就是阻塞等待,对方不完成 你也不返回 即:子进程不退出 父进程也不返回,直到子进程执行完再返回,这就叫阻塞
非阻塞等待:就是我给张三打电话叫他下来.他说要30分钟才下来,然后你说行吧,然后电话一挂, 然后我每隔一段时间就问他下来没有, 通过不断打电话挂电话的方式 即:不断检测张三的运行状态
阻塞等待和非阻塞等待都是一种等待方式,如果对应到操作系统中呢?谁在等待? 等待谁?等什么
父进程在等待,等待子进程,进一步来说是:等待子进程退出
阻塞等待
阻塞的时候,没有跑父进程的代码,父进程如果是在阻塞状态去等子进程,父进程就是处于等待状态,并不会被CPU运行,让父进程等待实际是把父进程的PCB加入到等待队列当中,将父进程的状态从R状态变为S状态,当操作系统识别到子进程执行结束时,发现父进程在等待队列当中, 把父进程的PCB重新加入到运行队列中,然后执行后序的过程,获取子进程的退出结果
阻塞的本质:意味着进程的PCB被放入等待队列中,并将进程状态由R改为S状态
子进程返回的本质:子进程退出时,父进程的PCB从等待队列拿回R队列,从而被CPU调度
例子:多进程创建和等待的模型
例子的意思:我们通过for循环同时创建10个子进程,同时将子进程的pid对应的放到ids数组中,然后将这10个子进程的退出码设置为该子进程在ids中的下标,然后父进程使用waitpid函数指定的等待这10个子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t ids[10];
for(int i = 0;i<10;i++)
{
pid_t id = fork(); //创建10个子进程
if(id == 0)
{
//child
printf("Child process created Success pid:%d\n",getpid());
sleep(3);
exit(i);//将子进程的退出码设置为该子进程在数组ids的下标
}
//father
ids[i] = id;//将子进程的pid对应在数组ids的下标位置
}
for(int i = 0;i<10;i++)
{
int status = 0;
pid_t ret = waitpid(ids[i],&status,0);//等待ids[i]这个进程
if(ret>=0)
{
//wait child success
//ids[i]就是子进程的pid
printf("father wait children success pid:%d\n",ids[i]);
if(WIFEXITED(status))
{
//正常退出 WEXITSTATUS(status):得到退出码
printf("exit code:%d\n",WEXITSTATUS(status));
}
else
{
//收到了信号 status&0x7F:收到了几号信号
printf("kill by signal %d \n",status&0x7F);
}
}
}
return 0;
}
执行结果:
我们之前写的都是阻塞等待,当子进程没有退出的时候,父进程一直等待子进程退出,等待的时候,父进程不能做自己的事情,这种等待叫做阻塞等待,所以如果我们想让父进程等待的时候做自己的事情,就要使用非阻塞等待
非阻塞等待
子进程没有退出的时候父进程做自己的事情,当子进程退出的时候, 再去读取子进程的退出信息
我们看到某些应用或者OS本身,卡住或长时间不动,叫做应用或者程序HANG住了 那么,WNOHANG
表示设置等待方式为非阻塞
例子:waitpid函数的第三个参数设置为
WNOHANG
:如果我们等待的子进程没有结束,那么waitpid直接返回0,不予以等待, 如果等待的子进程是正常结束,则返回该子进程的pid, 因为不知道要等到什么时候,所以父进程死循环等待子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();//创建子进程
if(id == 0)
{
//child
int count = 2;
while(count)
{
printf("child running pid:%d\n",getpid());
count-- ;
sleep(3);
}
exit(0);
}
//parent
//死循环等待子进程结束
while(1)
{
int status = 0;
pid_t ret = waitpid(id,&status,WNOHANG);//指定等待pid为id这个子进程结束 第三个参数设置为WNOHANG
if(ret > 0)
{
//子进程退出了
printf("father wait child success\n");
printf("exit code:%d\n",WEXITSTATUS(status));
break;//不需要再等待了
}
else if(ret == 0)
{
//子进程没有结束
printf("father do father things\n"); //父进程做自己的事情
sleep(1);
}
else
{
//等待失败
printf("father wait child fail\n");
break;//等待失败直接退出
}
}
return 0;
}
父进程在非阻塞等待子进程时,返回值有以下几种情况 ——
- 子进程根本就没退出
- 子进程退出,waitpid成功或失败(等待是有可能失败的,比如你等错了进程)
上述也成称为基于非阻塞等待的轮询方案
int status = 0;
while(1)
{
pid_t ret = waitpid(id,&status,WNOHANG);
if(ret == 0){
//子进程没有退出,但是waitpid等待成功,需要父进程重复进行等待
}
else if(ret > 0){
//子进程退出了,waitpid也成功了,获取到了对应的结果
}
else{ //ret<0
//等待失败
}
}