进程控制
在一个进程生命周期中一共有四个,前三很好理解,分别为进程创建、进程终止、进程等待,最后一个叫进程替换。
进程创建
fork函数初识
它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
fork之后多了个子进程,本质是系统多了个进程,操作系统就要给新的进程分配内存块和内核数据结构。同时将父进程的部分数据拷贝给子进程,还有添加子进程到系统进程列表当中,最后fork返回然后开始调度。
fork这个东西在我们创建子进程的时候会和父进程的数据是写时拷贝各自私有一份。
关于fork返回值的理解
1.如何理解fork函数有两个返回值的问题?
有两个空间,一个是用户空间,写我们自己的代码,另一个是内核空间,里面是操作系统的代码。用户空间的进程调用了一个fork函数,fork函数的实现操作系统内部。fork函数底层特别复杂,但是最后都会return pid,我们调fork时是进入操作系统里,由操作系统帮我们把创建子进程的工作做完,然后再返回,让我们拿到返回值。
返回两次是因为父进程和子进程各自执行return的
。
2.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?
必须得有名字来标识某一个进程,所以要给父进程返回子进程pid
。
3.如何理解同一个id值,怎么可能会保存两个不同的值,让if 、else if同时执行?
fork在创建子进程的时候要return返回,返回的本质就是写入。所以谁先返回谁就先写入id,因为进程具有独立性,要发生写时拷贝,
同一个id地址是一样的,但是内容不一样
,这就是写时拷贝。
写时拷贝
fork常见用法
1.父进程希望子进程复制自己,希望和子进程执行不同的代码区域。
2.一个进程要执行不同的程序,即我们创建一个子进程,想让这个子进程帮我们执行不同的程序。比如子进程从fork返回,调用exec函数。
fork调用失败的原因
系统中有太多的进程。
实际用户的进程数超过了限制。
可用下面代码实验:
进程终止
退出码
平时在写c/cpp代码时,main函数总会写return 0,这里的return 0,在系统当中这个数字是
进程退出的时候,对应的退出码
。这个退出码能够
帮我们标定进程执行的结构是否正确
。
我们写了一个错误的从1加到100的代码
echo $?
我们再输入
echo $?
命令的时候:
为什么第一次是1,第二次成0,其实就是echo本身也是一个进程,它打出的是上次一次进程的退出码。
我们如何设定main函数返回值呢?
如果不关心进程退出码,return 0,就行,如果未来我们要
关心进程退出码的时候,要返回特定的数据表明特定的错误
。
退出码的意义:
0:success
!0:表示失败
!0具体是几,表示不同的错误,数字对人不友好,对计算机友好,一般而言,
退出码都必须有对应的退出码的文字描述
,1.可以自定义。2.可以使用系统的映射关系(不太频繁)。
运行:
每一个错误码都有它对应的错误的原因。 下面看看描述和错误码都有的情况:
进程退出的情况
代码运行完毕,结果正确 —– return 0;
代码运行完毕,结果不正确 —– return !0 (退出码在这个时候起效果)
代码没跑完,程序异常终止。
进程如何退出?
1.从main函数返回。
2.任意地方调用exit(code)。
exit
是c语言给我们提供终止一个进程的函数,可以让正常进程终止。
编译运行:
下面代码:
运行:
假如说我们在addToTarget中不调用return sum,我调exit(21),
此时运行程序我们发现:
3._exit () ——了解即可
库函数是在系统调用之上的。
两种程序运行完我们发现下面的hello bit并没有被执行,
exit 终止进程,进程终止之后会主动刷新缓冲区。_exit 终止进程,不会刷新缓冲区。
exit是通过_exit来终止进程的。
上面我们所说的缓冲区在哪?
如果缓冲区在操作系统里,那么exit和 _exit都是会刷新的,因为操作系统比这两个更底层,上面缓冲区不在OS里及往下,所以缓冲区是在用户级的缓冲区。
进程等待
我们通过进程等待的方式来进行解决僵尸进程的问题。
进程等待的必要性
1.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
2.进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
3.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的方法
wait接口,成功了就返回子进程的id失败了就返回-1。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
实验现象:
我们使用创建子进程的方式调用fork,创建子进程,子进程跑10s,10s后退出,然后父进程会等15s,在前10s,子进程一定会退,退了之后,没有人等他没有人wait他,所以子进程处于Z状态,大概再等5s钟,父进程休眠完了,醒来调用wait的时候,此时wait会去回收子进程,回收子进程就会将子进程的状态从Z释放了,我们就能看到,开始是两个进程,后来变成一个进程是S或者R,另一个是Z,再过上5s钟,发现只有一个父进程了。
获取子进程的退出信息
等的是那个id,返回的就是哪个id。
ret为什么是2560呢,status不是被整体使用的,它是有自己的位图结构,它会根据自己的位图结构来设置不同的值。
拿到退出信号和退出码:
编译运行
信号编号为0,子进程的退出码为10,信号为0证明没有出问题,退出码为10代码跑完了,结果不对,自己定义10代表什么错误。
假如我们让代码没跑完,崩溃了,让子进程终止,父进程就拿到了子进程退出时的退出信息,那么会收到对应的信号,
得到了sig number是8,说明进程运行出错了,kill-l发现对应的是浮点数错误。
再修改代码:
再运行:
进程退出了,退出的信号编号是11,SIGSEGV—-段错误。
child exit code代表的是一定正常运行完了,但结果对不对用是否是0来表示。
进程僵尸了之后,它的代码和数据可以被操作系统释放掉,它的PCB必须被保留起来,PCB在保留的同时它的,也要把它的退出信息保留起来以供父进程读取。
子进程和父进程各自有自己的PCB信息,父进程代码会调用waitpid,父进程中的int status就是一个整数,子进程在退出的时候,子进程的代码和数据被释放掉了。但是它在退出的时候一定要把自己的退出码和退出时的信号保存到PCB中,此时状态我们称为僵尸状态,在僵尸期间,如果父进程此时调用waitpid(系统调用,执行操作系统的代码),操作系统就会通过id找到想要等待的子进程,找到后把子进程中对应的退出码和信号的值设置进status里面,然后就把值填到status里面,父进程也就看到了。
等待本质就是:
检测子进程退出的信息(在子进程的PCB里面也就是test_struct里),将子进程的退出信息通过status拿到父进程的上下文里
。