Linux进程控制核心:创建·等待·终止·替换
目录
- Linux进程控制
- 1. 进程创建
- 创建方式
- 创建流程
- 内核行为
- 函数介绍
- 2. 进程等待
- 等待原因
- 函数介绍
- 等待的两种方式
- 阻塞等待
- 非阻塞等待
- 3. 进程终止
- 终止原因
- 终止方式和函数介绍
- 4. 进程替换
- 概念
- 原理
- 多进程程序替换
- 函数介绍
Linux进程控制
1. 进程创建
创建方式
1.命令行直接运行程序(本质也是bash函数帮我们创建)
2.fork函数创建子进程(我们自己的代码创建)
创建流程
1.分配新的内存块和内核数据结构给子进程
2.将父进程部分数据结构内容拷至子进程
3.添加子进程到系统进程列表当中
4.fork返回,开始调度器调度
内核行为
创建进程时,先创子进程的内核相关数据结构(task_struct + mm_struct + 页表) 在加载代码和数据。创建时,相关数据结构拷贝一份,修改部分信息(pid,ppid等信息独有信息)。代码和数据刚开始只有一份,后续进行写时拷贝产生父子数据分离
当一个进程调用fork之后,就有两个二进制代码相同的进程。进而他们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程
代码等只读属性的数据共享,读写属性的数据写时拷贝
因为创建的进程各自有各自独立的地址空间,物理内存依靠页表和写时拷贝也不会发生冲突,所以一个进程挂掉不会影响其他进程,也就是进程独立性
函数介绍
#include <unistd.h>
pid_t fork(void); // 返回值:// >0: 父进程,返回值为子进程PID// =0: 子进程// <0: 创建失败
为了让父进程方便对子进程进行标识,进而进行管理,所以给父进程子进程的pid,给子进程返回0只是为了确认是否创建成功,出错返回-1
2. 进程等待
任何子进程,在退出的情况下,一般必须要被父进程进行等待。进程在退出的时候如果父进程不管不顾,子进程会一直维持状态Z。
等待原因
1.父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑)
2.获取子进程的退出信息(退出码,退出信号),知道子进程因为什么原因退出的(可选的功能)
函数介绍
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);//等待最近一个子进程退出
pid_t waitpid(pid_t pid,int * status,int options);
//等待pid为pid进程的退出,若pid参数值为-1,则等待任意一个子进程,与wait等效
//这两个函数返回值正常返回时为收集到的子进程id,如果调用出错则返回-1
调用这两个其中某一个函数时,父进程会进入阻塞等待状态,等待子进程状态发生改变。
status参数是输入型参数,记录子进程退出信息,
其中高16位不用,剩余16位的高8位记录退出码,低8位的最高一位记录core dump 状态,剩余7位记录退出信号。
如何获取到退出码和退出信号的值?
pid_t waitpid(pid_t pid,int * status,int options);
//自己使用位操作来提取
printf("quit code : %d , quit signal : %d", (status>>8)&0xFF, status & 0x7F);
//库里的宏
WIFEXITED(status);//若为正常终止子进程的返回状态,则为真,(查看status低7位是否为0)
WEXITSTATUS(status);//若WIFEXITED非零,提取子进程退出码
等待的两种方式
阻塞等待
等待的时候,进程状态为S,D,此时,进程的pcb从运行队列被移动到某个等待队列,不被调度,等待子进程退出。
pid_t waitpid(pid_t pid,int * status,int options);
waitpid函数中,options参数默认为0,此时该函数将进行阻塞等待
非阻塞等待
调用函数后,子进程会很快给出返回值,不会再等待子进程退出后再给返回值
waitpid函数中,options参数为WNOHANG(宏定义),此时该函数将进行非阻塞等待。
此时,返回值有3中情况
1. pid_t > 0:等待成功,子进程退出并且父进程回收成功
2. pit_t < 0:等待失败。
3. pit_t == 0:检测是成功的,只不过子进程还没退出,需要下次等待
非阻塞等待再检测子进程状态结束后,可以执行其他代码功能,不会一直等待,但是要进程下次检测,所以使用非阻塞轮询等待(非阻塞等待 + 循环),相比阻塞等待,非阻塞轮询等待可以允许父进程做其他事情。
3. 进程终止
终止原因
进程终止的必要性
1.释放曾经的代码和数据所占的空间
2.释放内核数据结构
释放数据结构时,task_struct延迟释放(此时为Z状态,僵尸状态)
为什么父进程bash要的到子进程的退出码呢?
要知道子进程退出的情况(成功,失败,失败的原因是什么?),本质也是想告诉用户错误原因
进程终止3种情况:
1.代码跑完,结果正确
2.代码跑完,结果不正确
3.代码执行时,出现了异常,提前退出了(操作系统发现进程做了不该做的事情,操作系统杀了进程,一旦出现异常,退出码就没有意义了)。提前退出是因为操作系统像进程发送了信号(比如 kill -11,段错误)
如何判断是那种情况?
1.先确认是否异常,是异常后查看是那种异常信号
2.不是异常,看退出码
进程退出时,他会释放掉代码,将退出信息(退出码exit_code,退出信号exit_signal)写入进程pcb(task_struct)中等待父进程读取,此时进程状态为僵尸状态(Z状态,代码释放,pcb不释放)
终止方式和函数介绍
1.mian函数return,表示进程终止(非main函数,return,函数结束)
2.代码调用exit函数(代码任意位置调用exit,都表示进程终止)
#include<stdlib.h>
void exit(int status);
3._exit(),系统调用终止进程,exit和 _exit的区别就是前者会刷新缓冲区,后者不会,前者是C语言库函数,后者是系统调用,exit内部会调用 _exit。
补充
bash内部维护着一个变量"?",这个变量会获取最近一个子进程的退出码,比如我们自己写的程序里面,最有一句return 0;0就会被?获取,我们在命令行也可以通过echo $?打印查看
退出码一般0表示正确,非零表示错误,错误有对应的错误信息,可以通过函数来查看
#include<string.h>
strerror(errorcode);
4. 进程替换
概念
一个进程在执行过程中调用exec*系列函数,将进程的代码和数据替换,可以执行起来新的程序。
原理
一个进程在执行时,会有自己的进程内核数据结构(pcb)+代码和数据,当进行程序替换时(调用exec*函数),会将新程序的代码和数据覆盖在旧程序的物理内存上,虚拟内存和页表以及pcb几乎不变,只会改变一些属性,这样就可以用旧的进程“外壳”执行新的进程,叫做进程替换
站在被替换进程的角度,本质就是这个进程被加载到内存,所以说exec类似于Linux系统上的加载函数
exec系列函数执行完毕后,旧进程的后续代码不被执行(旧进程的代码和数据已经被替换了),因此这些 函数的返回值大部分情况可以忽略,替换失败的时候返回值才有数据在内存中
多进程程序替换
由于进行程序替换后,父进程的后续代码不会执行,因此我们可以创建一个子进程,让子进程去执行新的程序,因为进程具有独立性,子进程执行完新的程序后不会影响父进程的后续执行,这样就可以实现在进程中启动另外一个进程,并且不影响本身的进程执行
内存变化:
创建子进程时,会创建新的内核数据结构,但是代码和数据刚开始共享,子进程进行程序替换,操作系统会将磁盘上新程序的代码和数据加载到内存,由于父子进程页表相同,因此加载的位置就是父进程的代码和数据的地址,进程具有独立性,子进程不可以影响父进程,会有写时拷贝发生,将代码和数据放在不同于父进程代码和数据的物理内存的位置,在修改页表映射,此时父子进程完全解耦,完全分离
函数介绍
#include <unistd.h>extern char **environ; //外部声明环境变量int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg,..., char * const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[],char *const envp[]);//path参数表示要进行替换的程序的位置,即相对路径或者绝对路径//file表示要进行替换的程序名称,替换时,操作系统会去PATH环境变量里面的路径下寻找该程序//arg是可变参数,表示替换程序时所带的命令行参数,以NULL结尾//argv[]是可变参数表,表内元素为命令行参数,最后一个元素为NULL,//envp[]是环境变量表,替换后的进程内部的环境变量表就是由该参数传递,并且是整体传递//替换前的子进程的环境变量表(由父进程拷贝的)不会默认传递给替换后的程序,若要进行替换,则需要进行外部声明改进程的环境变量表后,直接进行传递即可
#include <stdlib.h>char *getenv(const char *name);//若想增加自定义的环境变量给子进程的环境变量表,则可以使用函数getenv,该函数那个进程调用,则把环境变量加入到(name的内容)该进程的环境变量表中
这6个函数都是用来进行进程替换的函数,但是他们不是系统调用函数,是GNU C语言标准中的函数,是C语言封装出来的函数,真正的系统进程替换系统调用函数只有一个,上面6个函数底层都是调用execve系统调用函数
#include <unistd.h>int execve(const char *filename, char *const argv[],char *const envp[]);