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[]);
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐