进程从生到死有很多个过程和不同种情况,下面一一介绍
1.进程创建
1.步骤(父进程默认已存在且用fork创建):
(1)给子进程创建PCB及其数据结构内容和分配内存块
(2)将父进程部分数据结构内容拷贝给子进程
(3)将子进程添加到系统的进程列表中
(4)fork返回,开始调度器的调度
2.内核级虚拟机(题外话)
OS本身也是一个软件,但我们在一个进程中运行一个OS的代码时就叫内核级虚拟机。
3.发生写时拷贝的过程
当父进程的数据拷贝给子进程时,二者共享的数据在页表中会被设置为只读,当有进程想对数据进行修改时,OS查页表会发现权限矛盾,然后去查发现想要修改的数据在数据段时才会该进程发生写时拷贝,此时如果之前共享的数据此时只有一个进程指向时就会将只读权限去除。
4.写时拷贝的意义
(1)减少创建子进程的时间
(2)减少内存浪费
5.fork的常规操作
(1)子父进程对同一个程序进行不同的操作
(2)父进程希望子进程执行其他程序
调度失败的原因:
(1)OS中的进程太多
(2)每个用户所能调用的进程有限
2.进程退出
进程退出时的情况有三种:
(1)代码运行完且结果正确
(2)代码运行完但结果不正确
(3)代码异常中止
首先介绍与复习一些乱七八糟的知识
(1)子进程运行完的结果会返回给父进程
(2)main函数的返回值在代码非异常中止时能代表该程序的执行情况
(3)有时一个main函数是可以不写返回值的,此时的默认返回值为0
(4)return值都是由寄存器管理的,且一般返回0表成功,非0为不成功
(5)echo $? 打印最近一个进程退出时的退出码(目前来讲就是mian函数的返回值且该退出码是存在task_struct中的)
(6)strrerror(int i);打印i对应的错误信息.errno 储存错误码的全局变量
(7)exit(int i);直接是一个进程中止,退出码就是i
(8)进程中止的方式:
1.exit(c语言库)和_exit(OS提供)。无论在哪个函数中调用都会立即停止该进程,二者的区别就只在于exit会最后刷新一次缓冲区但_exit不会,因此exit的内部是封装了一个_exit的。
2.在main函数中的return。
(9)缓冲区其实有很多个,上面提及的缓冲区是c语言库的缓冲区而非OS内部的缓冲区。
3.进程等待
(1)僵尸进程没有任何方法杀死除了回收,因为其已经死了。
(2)进程等待的目的就是回收僵尸进程来获取子进程返回的数据与防止内存泄露的。
(3)进程等待是通过wait和waitpid这两个函数实现的(在<sys/type.h>与<sys/wait.h>中)且这两个进程都是写在父进程的代码区域中的。
首先介绍wait:
(1)pid_t wait(int status);
只要父进程中的子进程有一个进入了僵尸进程就对其进行回收且返回该进程的pid,如果到了wait这行代码却没有子进程进入僵尸进程时,父进程就会进入阻塞状态直到有子进程退出为止。回收失败返回-1.
例(wait的使用位置):
pid_t id = fork();
if(id == 0){//...子进程的代码}
//..后面是父进程的代码
//NULL表示不接收子进程的返回数据
pid_t wait(NULL);
(2)pid_t waitpid(pid_t pid,int* status,int options)
(1)pid的意义
1.pid > 0 :代表只等待该pid进程的僵尸状态
2.pid = -1 : 代表等待任意进程的僵尸状态
(2)status
status是一个输出型参数,用于接收子进程返回给父进程的值从而给父进程说明子进程的进行情况。
status的值说明:
status的后16位bit位是没有意义的,在8~15这里的bit位代表退出码,第7位代表core dump,0~6位代表终止信号。
1.在子进程正常中止时,其会将退出码存在status的8~15这里的bit位上,其余全是0。
2.被信号杀死时,退出码无意义,此时0~6位返回中止信号。
(补充说明wait和waitpid都是系统中的接口,由于进程之间具有独立性因此父进程是无法直接拿到子进程中的数据的,而只有OS能拿到所有进程的信息,因此父进程才需要调用OS的接口来得到子进程的信息)
3.中止信号可以先代表kill指令中的命令符:
[root@VM-0-2-centos ~]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2
这里面的所有信号本质只是一个宏定义,左边的数字代表对应的信号值,没有0信号的原因是,这里的所有信号基本代表的是各种异常退出的情况,而0就代表了正常运行。
总结一下有status的0~6位总值位0就代表该进程正常退出,此时8_15位就代表退出码,否则就是异常退出同时此时的退出码无意义。
4.waitpid的原理
task_struct中有两个成员exit_code(退出码)和exit_signed(终止信号),当父进程调用waitpid时,OS就会从子进程的这两个成员变量中提取数据并存入父进程的status变量中。说白了僵尸状态的目的就是让父进程得到子进程的这两个数据罢了。
5.两个处理status的宏定义(父进程中使用)
WEXITSTAUS(int status),返回status中退出码部分的值
WIFEXITED(int status),返回一个bool,中止信号值为0时返回true否则返回false
(3)options
(题外话,一台计算机完全卡死也就是全部父进程处于阻塞状态时就叫夯住)
首先介绍一下阻塞调用和非阻塞轮询
非阻塞轮询:
父进程每个一段时间就调用waitpid函数查看子进程状态,其为z状态时就返回并回收,不为z状态也返回但后续会继续调用waitpid(使用循环实现)直到其为z状态。
阻塞调用:
父进程调用该函数且直到子进程处于z状态时才返回并回收。
总结也有waitpid的返回值意义:
1.>0,等待结束且正常返回
2.=0,调用结束但此时子进程并没有结束
3.<0,调用失败
options值
1:0默认值,表示为采用阻塞调用
2:WNOHANG,宏定义,表示采用非阻塞轮询
使用方法:
pid_t id = fork();
if(id == 0){//..子进程代码}
//父进程,死循环来一直调用waitpid
while(1)
{
int status = 0;
pid_t rid = waitpid(id,&status,WNOHANG);
if(rid > 0)
{//..; break;}
else if(rid == 0)
{
//这里可以写一个函数指针数组的回调来让每一次waitpid==0时父进程也可以运行一些代码 }
}
else
{//调用失败 break;}
}