嵌入式全栈开发学习笔记---Linux系统编程(进程控制)
目录
进程概念
程序和进程
进程的生命周期
为什么有了操作系统,就能同时处理多个任务?
进程的三种状态
进程互斥
临界区和临界资源
进程同步和事件
死锁
进程调度
获取进程号getpid()/getppid()
创建子进程fork()
fork笔试题
fork原理
编程训练
创建子进程vfork()
exec函数族
execl()
execv()
系统调用system()
僵尸进程和孤儿进程
wait()
waitpid()
wait和waitpid有什么区别?
进程的退出exit和_exit
上节我们学习了文件操作,本节开始学习进程控制!
进程概念
进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元;
类似于在我们的windows系统上面有任务管理器
打开后有一栏就叫做进程,显示了我们电脑目前正在运行的所有应用,一个.exe就是一个进程
进程是动态的,在Linux系统下如果查看进程?
补充命令20:ps -elf或者ps -ef
看到的是操作系统所有的进程。
表头解释:
UID:user ID(启动这个进程是哪个用户启动的)
PID:进程号
PPID: PPID父进程,第一个P是parent父母的意思,第二个P和PID进程号的P一样是process进程的意思,一个进程可能不是凭空产生的,它一定有父进程,比如27503这个进程的父进程是26255这个进程
任意一个进程如果往后追溯的话父进程一定是1号进程,1号进程我们也称为Init进程,祖先进程
可想而知init的这个进程是不能“死掉”的,否则系统就重启了。
CMD:进程的名字
TIME:运行的时间
以上我们主要关注的是PID和PPID
程序和进程
程序是放到磁盘的可执行文件,程序是静态的
进程是指程序执行的实例,进程是动态的,输入“./程序文件”,运行起来的那一个瞬间就是进程
进程是动态的,程序是静态的:程序是有序代码的集合;进程是程序的执行。通常进程不可在计算机之间迁移;而程序通常对应着文件、静态和可以复制
进程是暂时的,程序使长久的:进程是一个状态变化的过程,程序可长久保存
进程与程序组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)
进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。
现在我们写一个简单的死循环程序test1.c感受一下进程
如果我们输入“./程序文件”让程序运行起来后,这个进程是在前台运行的,如果这个进程没有结束,则输入的其他命令是无效的,
想要结束进程可以按CTRL+C。
想让进程在后台运行可以按CTRL+Z(在后台运行的进程并不是结束,切换到后台之后可以又在前台启动另一个进程,后台可以运行多个进程)
现在有两个./test1正在后台运行
切换到后台的进程怎么又切换到前台结束掉?
补充命令21:fg
这个可以将在后台运行的进程切换到前台,然后就可以按CTRL+C结束掉这个进程了
结束掉一个./test1进程后,现在后天只剩下一个./test1进程了
进程太多了,如果想要搜索进程怎么办?
补充命令22:ps -ef|grep test
这个命令的意思是在ps -ef命令的执行的结果中搜索test这个关键字,回车后可以看到搜索的进程
注意,ps -efh和ps -ef是一个命令也是一个进程,当我们输入命令回车的那一瞬间这些命令就是一个进程,但是运行之后这些进程就死掉了,所以这个进程已经不存在了
进程的生命周期
进程是有生命周期的,分为:
创建(输入“./程序文件”回车就是创建了一个进程): 每个进程都是由其父进程创建,进程可以创建子进程,子进程又可以创建子进程的子进程
运行(创建进程回车后就在运行了):多个进程可以同时存在,进程间可以通信
撤销(CTRL+C可以结束进程): 进程可以被撤销,从而结束一个进程的运行
为什么有了操作系统,就能同时处理多个任务?
比如单片机一次只能下载一个main函数进去,因为一个处理器同一时刻只能处理一个事情(一个main函数)。而我们的电脑可以上QQ,同时也能使用浏览器,但是为什么我们的电脑可以同时处理多件事情,单片机就不行呢?是因为我们的单片机是裸机,没有操作系统,而我们的电脑是有操作系统的。
比如我们的电脑现在正在运行QQ,音乐,浏览器三个进程
操作系统让QQ在CPU里面进行5ms,然后让它出来,又让音乐进去5ms,然后让它出来,再让浏览器经常去5ms,让它出来,再它QQ进去5ms......这个过程中三个进程各自运行的时间是5ms,停的时间是10ms
停留10ms的这个时间太短了,一般人是感知不到的(而且这里的10ms只是一个假设),所以我们人是察觉不到这个停顿的,只感觉这三个软件是同时进行的。
因此有了操作系统后的一个好处就是可以随着进程调度,选择谁来运行,谁在就绪这个过程是由操作系统决定的。
如果我们的电脑CPU运行速度很慢的话,我们就能感受到明显的卡顿,体验感极差。
所以能上操作系统的芯片必须要速度足够快,效率足够高才行,才能让每个进程流畅切换。
这也是51单片机不能上操作系统的原因,因为它的速度太慢了。
进程的三种状态
运行的时候进程在三种状态之间来回切换
执行状态:进程正在占用CPU
就绪状态:进程已具备一切条件,正在等待分配CPU的处理时间片
等待状态:进程不能使用CPU,若等待事件发生则可将其唤醒(比如我们写代码的时候经常用到scanf这个函数,这个函数让程序卡在这里等待用户从键盘上输入东西,此时如果让这个进程在占领CPU也没什么意思,于是操作系统把它列为等待状态,让其他进程先轮番切换,等到用户输入信息后,这个进程再重新进入就绪状态,等待调度)
由于存在状态切换,这个操作系统不可避免会出现一个问题:就是一个进程还没有完成任务,执行的时间就结束了,操作系统就让下一个进程执行了,这种现象不可避免一定会出现。
解决办法就是要我们写代码来进行锁机,就是即使一个进程的执行时间结束,下一个进程也进不来CPU执行。
在多进程的操作系统中,一定会出现进程同步的问题,进程同步就是如何让多个进程有序地执行。
Linux系统是一个多进程的系统,它的进程之间具有并行性(伪并发)、互不干扰等特点。
也就是说,每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间(比如说int在内存占4个字节,这个都是虚拟内存),因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。
进程互斥
进程互斥是指当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止;(也就是我们上面提到的锁机操作)
临界区和临界资源
操作系统中将一次只允许一个进程访问的资源称为临界资源;
进程中访问临界资源的那段程序代码称为临界区,为实现对临界资源的互斥访问,应保证诸进程互斥地进入各自的临界区;
进程同步和事件
一组并发进程按一定的顺序执行的过程称为进程间的同步;具有同步关系一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。
死锁
多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能再向前推进。
比方说当一个进程进去访问临界资源时,我们进行了锁机操作,等该进程执行结束后,按理应该是解锁后退出,别的进程才能进去执行,但是如果该程序只退出不解锁的话,就造成了死锁,别的进程被卡在门外进不去,这个时候就需要外力作用,打破僵局,当然这种情况一般不会发生。
一般会出现的情况是我们写代码的时候会写成两个进程都各有一把锁,每个进程都不愿意释放,这个时候就需要外力作用,打破僵局,这就是常见的一种死锁,就是由于我们写的代码逻辑有问题造成的。
进程调度
按一定算法,从一组待运行的进程中选出一个来占有CPU运行。
调度方式:
1、抢占式(即使一个进程还没有完成,另一个进程也会抢占运行,现在的操作系统一般都是抢占式的)
2、非抢占式(以前的操作系统一般都是这种)
调度算法:
先来先服务调度算法(谁先运行就先调度谁)
短进程优先调度算法(谁的运行时间更短就先调度谁)
高优先级优先调度算法(每个进程其实都有优先级)
时间片轮转法
现在的操作系统一般都是后两种。在轮转的时候,谁的优先级高就先调度谁。
以上四种调度算法详解推荐:详解操作系统四大常用的作业调度算法(FCFS丨SJF丨HRRN丨RR)-阿里云开发者社区 (aliyun.com)
获取进程号getpid()/getppid()
我们写代码的时候如何获取进程的ID号?
系统给我们提供了获取ID号函数
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void) 获取本进程ID。
pid_t getppid(void) 获取父进程ID
注:一般xxx_t 类型都是整数,有一些特别之后遇到再提。
创建2.process文件夹
然后在2.process文件夹下创建1.getpid.c文件开始写代码
getpid的原型和头文件
返回值就是进程号
运行结果:
这样得出来的结果我们无法验证,因为./1.getpid这个进程敲下回车的瞬间已经执行完了,我们可以加一个死循环,让它不要结束
现在这个进程还在运行中
这样我们去查看进程的时候看到它的PID和PPID的确跟我们获取的一样
创建子进程fork()
Fork这个函数非常重要
#include <unistd.h>
pid_t fork(void)
功能:创建子进程。
fork的奇妙之处在于它被调用一次,却返回两次,它可能有三种不同的返回值:
1、失败返回-1;
2、父进程返回子进程的id;
3、子进程返回0。
也就是说调用一次 fork就有两个进程在执行,相当于这个程序有两个分支。
创建一个文件2.fork.c
fork的原型和头文件
代码演示:
以上这段代码从C语言的角度来看只能执行一个if分支语句,但是有了fork函数之后,程序就像可以分身了一样,可以执行两个if分支语句中的while循环,并且有两个返回值
运行结果:
很明显这是两个死循环同时进去了
那父子进程运行的顺序有什么讲究吗?
在ubuntu中运行的话一般都是父进程先运行,而如果是在Red Hat里面运行的话父子进程是随机的,不过子进程先运行的概率比较大。
因此fork创建子进程后,运行时父子进程运行的顺序是随机的。
我们打印出他们的进程号研究一下
运行结果
注:fork()函数对于父进程来说返回的是子进程的ID号,对于子进程来说返回的是0
如果我们在底下再加这样一行代码,看看哪个进程会打印它
这个语句被打印了两次,因此结论就是:如果这个语句写在if分支之外,父子进程都会执行。
下面解释一下为什么fork能有两个返回值
我们最开始说过进程是资源分配的最小单元,每次创建一个进程,系统会分配4G的虚拟空间,比如现在创建了一个进程
执行到程序的第8行的时候调用fork函数产生了一个子进程
然后父进程从第9行继续往下执行,子进程会复制父进程的代码开始执行(比如我们刚刚写的2.fork.c中有36行代码,第8行的开始调用了fork函数,子进程就复制父进程的36行代码,并且从fork函数下面的第9行开始运行),这样它就有两个分支。
也就是说父进程和子进程的代码是一样的,区别就是PID不一样
这两个进程经过下面if的几个分支语句时,对于父进程来说,它的返回值不是-1,也不是0,所以进入else分支,对于子进程来说,它的返回值死活0,所以进入了else if(0==pid)分支,因此结论就是父进程执行else分支,子进程执行else if(0==pid)分支。
两个进程都经过了printf("helloworld\n");这行代码,所以打印了两次helloworld。
综上所述,C语言的语法规则并没有被打破,if语句的分支在同一时刻仍是只能被执行一个,这个程序之所以能执行两个分支,是因为调用了fork函数复制了一份代码,执行了两份一摸一样的代码而已。
fork笔试题
问:以下这段代码输入的结果是怎样的?
解析:
大部分人会这样分析这段代码:
一开始i=0的时候,父进程调用了fork,产生了一个子进程,父子进程各打印了一个横杠
然后程序进程到第一次i++前,父子进程的i都是0,经过i++后,父子进程的i都是1
父进程再次调用fork,再产生了一个子进程,之后父子进程都打印了一个横杠,父进程累计打印了两个了。
然后这对父子进程经过i++后都等于2,2<2不成立,所以退出了for循环。
与此同时,之前的子进程在i=1的时候也调用了一次fork,产生了一个孙进程),然后子孙进程都打印了一个横杠
然后这对子孙进程经过i++后都等于2,2<2不成立,所以退出了for循环。
分析到这里,我们知道按理来说它应该输出6个横杠,但是实际上它输出了8个横杠,
如果我们加上换行符,它就输出了6个横杠
为什么呢?
因为之前我们讲过行缓冲的时候说过换行符有刷新缓冲区的功效。
有了这个背景之后我们再重新分析一下题目中原先的代码
第一次i=0的时候父子都打印了横杠
但是由于没有换行符,缓冲区没有刷新,所以这两个横杠留在了缓冲区,没有输出到屏幕。
然后经过i++后,父子进程的i都是1,父进程再次调用fork,再产生了一个子进程,子进程复制了父进程的代码和标准输出缓冲区,而由于没有换行符,缓冲区没有刷新,父进程打印的第一个横杠还留在里面,所以这个子进程刚产生的时候就自带了一个横杠,之后子进程和父进程又各打印了一个横杠
与此同时,之前的第一个子进程产生的孙进程也同样把它的标准输出缓冲区也复制过来了,所以孙进程刚产生的时候也自带了一个横杠,之后子孙进程又各打印了一个横杠
所以这道题就打印了8个横杠
结论:子进程除了复制父进程的代码外还复制父进程输出缓冲区里面的数据
fork原理
我们分析一道笔试题:
问这段代码输出的结果是怎样?
结果是两个输出的num都是1
原因是父子进程操作的num不是同一个num
为了验证这一点我们可以把它们的地址打印出来看看
结果他们的地址一样的
这就和我们刚才的结论矛盾了,难道他们操作的是同一个num?
我们再分析一下,进程资源分配的最小单元,每次创建一个进程,系统会分配4G的虚拟空间,这个空间又分为数据段,代码段,堆,栈
刚开始父进程里定义了一个num局部变量,编译器就会在栈空间中找一块空闲的内存来表示这个num,并且存放的是0,假设这个num所在的地址是0x100
然后父进程中调用了fork产生了一个子进程,子进程直接把这4个G的内存直接复制过去了
父子进程是独立的,虽然地址是一样的,但是不是同一块内存,是两块空间里面的内存
这就像学校里的两栋宿舍楼,1号楼里面有个111的宿舍,2号楼也有个111的宿舍,但是显然他们不是同一间宿舍。
父进程里面的数据都会复制到子进程里面去,我们只是进程号不一样。
注意:并不是说一调用fork函数,子进程就完全剥离了父进程有了自己的地址空间,而是当内存里有东西被修改的时候,子进程才真正剥离出去有了自己的地址空间,也就是题目这段代码中当进程执行到num++这句的时候,寄存器从内存中把数据读过去,进行加1操作后再把它写回内存中去,这个时候父子进程才真正分开。
这个过程涉及到一个概念叫“写时拷贝”,即涉及到写内存的时候才拷贝。
编程训练
接下来我们用一道编程题来验证“子进程也继承了父进程的文件描述符”,规定父进程负责写文件,子进程负责读文件
代码演示
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void child_write(int fd)
{
char buf[128]={0};//存放要写的字符串
while(1)
{
scanf("%s",buf);
if(-1==write(fd,buf,strlen(buf)))//写到哪,写什么,写多少
{
perror("write");
break;//退出循环
}
//把文件指针挪回来
lseek(fd,-1*strlen(buf),SEEK_CUR);//相对当前往前移动strlen(buf)个字节
if(!strcmp(buf,"bye"))//如果scanf获取的是bye就退出循环,这句一定放在指针挪回来之后,这样父进程才能读到bye,才能结束进程
break;//退出循环
memset(buf,0,128);//用完数组,最好清空
}
}
void parent_read(int fd)
{
char buf[128]={0};
while(1)
{
int ret=read(fd,buf,sizeof(buf));
if(-1==ret)
{
perror("read");
break;
}
else if(0==ret)//如果没有读到数据
{
//因为父子进程的执行顺序是随机的,所以父进程先执行时有可能读到的是0
continue;//进入下一次循环
}
if(!strcmp(buf,"bye"))//如果读到的是bye就退出循环
break;//退出循环
//打印读到的数据
printf("child get:%s\n",buf);//加了换行符就会不断刷新缓冲区
memset(buf,0,128);//清除数组,清空为0,清空128个字节
}
}
int main()
{
//打开文件
int fd=open("hello.txt",O_CREAT|O_RDWR,00400|00200);
if(-1==fd)
{
perror("open");
exit(1);
}
if(fork()==0) //子进程负责写文件
{
child_write(fd);//执行fork后系统会自动把这个文件描述符拷贝一份到子进程
}
else //父进程负责读文件
{
parent_read(fd);
}
close(fd);
return 0;
}
运行结果:
输入helloworld回车之后并没有看到有东西打印出来,手动结束进程后,查看我们输入的字符串已经写入到了hello.txt了
为什么子进程没有输出?原因是父子进程用的是同一个文件描述符,所以它们用的也是同一个文件指针,当子进程往这个文件中写入数据的时候,此时文件指针指向的是字符串后面一个位置,所以父进程是读不到数据的,因此我们每次写完数据之后都要不停地将文件指针挪回来
再运行
这样我们就能看到我们输入的Helloworld回车键后被读出到屏幕了
我们以后可以不用手动输入CTRL+C结束进程,可以在父子进程中都加入这句,之后只要我们在终端输入bye就结束了这个子进程
运行
当子进程获取到bye后,写入了hello.txt,退出进程,之后父进程读取到bye直接退出进程,没有打印输出到屏幕,到此父子进程都退出了进程,于是整个./5.fork读写这个进程就全部结束。
补充:多进程能不能加快文件读写的速度?
答案是不能,因为磁盘和内存之间的IO操作的通道只有一个,并且磁盘不停地转,只有转到对应的地方才可以将数据输出到磁盘上,同一时刻只能有一个进程输出的结果经过IO通道到达磁盘对应的地方。因此并不能加快文件读写的速度。这就好比一个饭店里面只有一个厨师,为了加快上菜的速度,请了两个服务员,这样还是不能加快上菜的速度,根本原因是厨师还是只有一个,炒菜的速度并没有改变。所以关于多进程能不能加快文件读写的速度的问题,关键还是在于硬盘的特性。
创建子进程vfork()
vfork这个函数也可以创建子进程,和fork类似
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void)
功能:创建子进程
vfork()会产生一个新的子进程,其子进程会共享父进程的数据与堆栈空间,并继承父进程的用户代码、组代码、环境变量、已打开的文件代码、工作目录和资源限制等;
子进程不会继承父进程的文件锁定和未处理的信号;
注意,vfork产生的子进程,一定是子进程先执行、父进程后执行。
它的返回值和fork也是一样的,父进程返回子进程的id,子进程返回0
如果从代码的角度来看的话,fork和vfork几乎一模一样,但是实现的原理是有区别的
代码演示
这个代码在编译的时候没有问题,但是运行的时候出现了段错误
因为调用vfork创建的子进程,必须在它的子进程中指明退出的方式
加上退出的方式后再编译运行就可以了
之前说用fork创建的子进程,父子进程运行的顺序是随机的,但是vfork就有所不同,用vfork创建子进程后,子进程不结束,父进程就不运行。
我们在子进程中加上睡眠5s验证一下
一开始是子进程运行
5s后子进程退出,父进程才运行
再加上这样一段代码,验证一下num输出的值
可以看到父进程操作的num在子进程的基础上加了1,这和之前fork产生的子进程那个程序运行的结果不同,同样的代码,fork的子进程和父进程操作的num都等于1
因此我们又可以得出一个结论,也是vfork和fork最大的一个区别就是:vfork产生的子进程和父进程共享地址空间(同一个空间)。而fork产生的子进程是复制了父进程的地址空间(两个空间是独立的)
exec函数族
虽然vfork产生的子进程和父进程共享地址空间,但是父子进程的进程号是不一样的。一般来说子进程都是在父进程的基础上加1(有特殊情况)。
vfork产生的子进程和父进程共享地址空间有什么意思?
我们一般用vfork启动另外一个毫不相干的进程。比如我们现在在Linux里面写代码,正在跑一个进程,然后想在这个进程中启动一个毫不相干的进程,用于图像处理或者播放音乐等等,我们就会用vfork产生一个子进程,然后子进程中使用exec(注意exec并不是一个函数,而是一系列由名称前缀为exec组成的一个函数族,比如execl, execv, execp)。
exec用于启动一个不相关的新的进程,被执行的程序替换调用它的程序。
exec和fork区别:
fork创建一个新的进程(子进程),产生一个新的PID。
exec启动一个新程序,替换原有的进程,因此进程的PID不会改变。
execl()
#include<unistd.h>
int execl(const char * path,const char * arg1, ...)//可变参数,后面可以跟很多参数
参数:
path:被执行程序名(含完整路径),
arg1至argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束,
示例:
意思是启动cp命令的完整路径/usr/bin/cp,需要输入cp -r /usr/local/bin/ . 将bin这个目录拷贝到当前目录下,最后以NULL结束。
补充命令23:which 命令名
这行命令可以查某个命令的路径
然后我们写一个进程,在该进程中将调用拷贝bin到当前目录下的命令,就相当于我们在一个进程里面又启动了一个进程(命令运行的瞬间也是一个进程)
代码演示:
结果真的并没有输出helloworld,并且已经把bin这个目录拷贝到当前目录下了
图片解释
此时cp这个进程号和子进程号是一样的。
execv()
exec中还有一个execv
它只有两个参数,第一个参数是一个指针数组,也就是把execl的可变参数放在这个数组里面,功能和execl是一样的。
#include<unistd.h>
int execv (const char * path, char * const argv[ ])
参数:
path:被执行程序名(含完整路径)。
argv[]:被执行程序所需的命令行参数数组。
系统调用system()
system用来启动进程
#include <stdlib.h>
int system( const char* string )
功能:
调用fork产生子进程,由子进程来调用/bin/sh -c string来执行参数string所代表的命令
示例:system(“clear”);//启动清屏进程
僵尸进程和孤儿进程
孤儿进程:父进程创建了子进程,但是父进程提前运行结束;此时子进程变成孤儿进程。操作系统一般会将孤儿进程“过继”给init进程,即1号进程。
僵尸进程:僵尸进程指的是那些虽然已经终止的进程,但仍然保留一些信息,等待其父进程为其收尸。
下面用代码测试一下
代码演示:
因为我们让子进程睡眠了1s,所以子进程后结束,结果就出现了这种现象,子进程打印出来的东西一部分出现了#后面,而光标出现在了子进程打印的结果后面
这个时候我们敲一下回车才又恢复正常
还有一个问题,按理来说子进程的ppid应该是父进程的pid是一样的,但是很明显运行的结果是错误的,子进程现在父进程变成了祖先进程
为什么会出现这种问题?
在Linux中父进程存在的意义不仅是产生子进程,而且最后还要回收子进程,这才是父进程完整的流程,只有流程结束,父进程才算正在结束。
而我们上面测试的这段代码中子进程还在睡眠的时候,父进程就结束了,导致子进程变成了孤儿进程,所以孤儿进程被过继给了祖先进程。子进程运行结束后(死了),但是没有父进程来回收子进程(收尸),子进程的“尸体”还在占着资源,变成了僵尸进程。
我们现在把代码改成这样,看看僵尸进程的状态是怎样的
运行结果
此时子进程已经运行结束了,父进程还在睡眠,我们可以查看一下进程的状态
此时子进程是僵尸进程,在进程表格中可以看到的现象是它被方括号”[ ]”括起来了,可以把它形象为僵尸的棺材。
如果我们用”ps -elf”这条命令在完整的进程表格中查看的话,就可以看到它的状态被标成“Z”
“S”状态表示睡眠的意思,“Z”状态表示Zombies僵尸的意思
僵尸进程是“杀不死”的,可以测试一下
补充命令24:kill -9 进程号
这行命令可以杀死一个进程,-9就最高级别的杀死命令,kill发送信号-9给某个进程。
重新运行,把僵尸进程的进程号复制一下
输入命令kill -9 15785 ,之后再查看进程表,这个僵尸进程还在
所以僵尸进程是杀不死的,我们只能等待它的父进程结束后再回收子进程的尸体
如果某天系统中出现了僵尸进程,又找不到它的父进程,我们只能重启系统。我们写代码的时候要避免孤儿进程和僵尸进程的存在。
wait()
对于孤儿进程,我们可以在子进程睡眠的时候,在父进程中加一个wait()等待并回收子进程
这个函数一定是在父进程中使用的,因为它要回收子进程的资源。
结果就不会像之前那样异常了
所以我们以后在父进程中尽量加上这个wait
status用来保存子进程的状态,那如果打印子进程的状态呢?
这个status是int四个字节,每个字节表示不同的意思,我们可以用系统提供的几个宏函数来解析这个status。
其中这个WIFEXITED(status)是用来检测子进程是否正常退出
运行后子进程正常结束后
我们把子进程的睡眠时间改长了一点,带回运行程序之后,我们在子进程还在睡眠的时候用kill这个命令将它杀死,然后父进程中的wait函数中的宏函数WIFEXITED(status)会解析到子进程是异常退出的
正常退出的话,退出状态如何获取?
我们在这加一个exit(100),等会儿这个100就是子进程退出的状态
我们可以哟wait里面的这个宏函数WEXITSTATUS(status)来获取这个状态
综上,以后我们用fork,建议就在父进程中加上这个wait.
waitpid()
还有一个函数叫waitpid()
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid (pid_t pid, int *status, int options)
功能:
该函数默认为阻塞,直到有信号来到或指定的子进程结束。
参数:如果不在意结束状态值,则参数status可以设成NULL。
参数pid为欲等待的子进程识别码:
pid<-1 等待进程组识别码为pid绝对值的任何子进程。
pid=-1 等待任何子进程,相当于wait()。
pid=0 等待进程组识别码与目前进程相同的任何子进程。
pid>0 等待任何子进程识别码为pid的子进程。
参数option可以为 0 或下面的宏组合
WNOHANG :如果没有任何已经结束的子进程则马上返回,不予以等待。
WUNTRACED :如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
返回值:
如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno中。
wait和waitpid有什么区别?
比如一个进程产生了10个子进程,用wait就会在那里等待,只要有一个子进程结束了就回收掉,然后父进程也结束了,那其他9个子进程就会变成孤儿进程。而这个waitpid的作用就是它可以指定等待哪个进程,比如我们要等待第三个子进程,我们就只要提供第三个子进程的进程号给它就行了。
进程的退出exit和_exit
exit和_exit用于终止进程
区别:
_exit: 直接使进程停止,清除其使用的内存,并清除缓冲区中内容
exit与 _exit的区别:在停止进程之前,要检查文件的打开情况,并把文件缓冲区中的内容写回文件才停止进程。
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓