🎉Linux:进程控制
博主主页:桑榆非晚ᴷ
博主能力有限,如果有出错的地方希望大家不吝赐教
给自己打气:每一点滴的进展,都是缓慢而艰苦的,祝我们都能在往后的生活里找到属于自己有意义的快乐🥰🎉✨
一、⚽进程创建
1.1 🎧fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd> pid_t fork(void); //return value解读 //如果fork成功,给父进程返回子进程的pid,给子进程返回0 //如果fork失败,给父进程返回-1 //参数,不可传参,传参报错,因为里面设置为void
进程调用fork,fork之后OS会做些啥?
在fork之前,只有父进程一个独立执行代码,而在fork之后,父进程就会创建一个子进程,父子进程分流执行代码。那么有人就会问,子进程的代码和数据是哪里来的?其实子进程执行的是父进程的代码和数据。那么子进程是怎么获得到父进程的代码和数据的呢?其实这都要归功于OS(Operator System)。之前在进程概念中,我们就提到了task_struct,每一个进程都会有自己的task_struct,在task_struct当中,会有描述进程的许多数据结构,在这里我们重点谈论mm_struct和页表。而在fork之后,OS也要为子进程创建task_struct,而子进程的task_struct的大部分内核数据结构都要拷贝父进程的task_struct中的内核数据结构,包括mm_struct和页表。这样一来,子进程mm_struct可以通过页表映射到物理内存上,由于父子进程的mm_struct和页表一样,所以映射的物理内存也一样,最终父子进程就可以执行同一份代码和数据了。但是由于代码是只读的,而数据是可读可写的,那么如果父子进程任意一方对数据进行了修改,那么不会影响不修改那方的数据,这是为什么呢?这是由于OS采用了一种写时拷贝的方法来处理父子进程对数据修改的问题。假设子进程要修改一个数据,那么OS就会重写在物理内存上开辟一块相同大小的内存,把这块空间重新与虚拟内存进行映射,填入页表当中,所以父子进程是相互独立的,互不影响的。总结:fork之后,父子进程代码共享,数据发生写时拷贝。
1.2 🎧fork常规用法
fork的用途:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec系列函数
fork失败的原因:
- 系统中有太多的进程
- 实际用户的进程数超过了限制
二、⚽进程终止
2.1 🎧进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(除零错误、野指针、栈溢出…)
2.2 🎧进程常见退出方法
正常终止:(可以通过 echo $? 查看在bash中最近一次进程执行完毕时,对应进程的退出码):
从main函数中return X;
调用exit(X);
_exit(X);
X就是进程退出码!
if(x == 0) 表示进程执行结果正确
if(x != 0) 表示进程执行结果错误
exit与_exit的区别:
exit()c语言中的库函数,而_exit()是系统接口函数。exit()对 _exit()做了封装,也就是说exit()底层就是 _exit()。它两最重要的区别是exit()退出后会刷新(fflush)缓冲区,而 _exit()不会刷新缓存区。
异常退出:
ctrl + c
信号终止(重点)
使用 kill -singal pid可以给相应的进程发送信号,终止进程。
三、⚽进程等待
3.1 🎧进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成**“僵尸进程”的问题,进而造成内存泄漏**。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
所以进程等待的意义就在于解决上面涉及到的三个问题。
3.2 🎧进程等待的方法
3.2.1 🖋wait方法
//wait()函数的头文件 #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); //return value解读,假设返回值为 ret //if (ret > 0) 父进程等待子进程成功success,ret就是子进程的pid //if (ret < 0) 父进程等待子进程失败fail, ret为-1 //status是一个输入输出型参数,这里主要是用的输出型的性质 //?输出型参数,要输出什么呢? //这里解决的就是进程等待必要性的第三个问题,输出的是子进程的退出信息(退出码exit code和退出信号exit singal) //下面对status进行解读
status解读
有的人就会有疑问,status只是一个32位的整数,一个整数怎么会返回两个信息呢?其实它是用位来返回信息的。返回exit code 和 exit singal主要是用status的低16位比特位。
- 正常退出时,status的低16位的状态:
- 异常退出时,status的低16位的状态:
其中的code dump标志现在不需要了解。
简单理解一下图片中的英文:调用wait()函数的进程(父进程)会在wait()函数停下来,不会继续向下执行代码,直到它的一个子进程终止。
也就是说只要子进程不终止,父进程会阻塞等待子进程,只要它的任意的一个子进程终止,父进程就会把该子进程进行回收,父进程等待子进程成功,父进程才会继续向下执行代码。
代码演示wait()等待子进程
#include <stdio.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { //子进程 int cnt = 5; while(1) { printf("我是子进程,我正在运行...我的pid : %d\n", getpid()); sleep(1); if(cnt == 0) { break; } cnt--; } exit(1); } else if(id > 0) { //父进程 int status = 0; printf("我是父进程,我准备等待子进程啦...我的pid : %d\n", getpid()); sleep(10); pid_t ret = wait(&status); if(ret > 0) { //wait child success if((status & 0x7f) == 0) { printf("子进程是正常退出的,退出码 : %d\n", (status>>8) & 0xff); } else { printf("子进程是异常退出的, 退出信号 : %d, 退出码 : %d\n", status & 0x7f, (status>>8) & 0xff); } } else//ret < 0 { //wait child fail printf("等待子进程失败\n"); } } else { //创建子进程失败 printf("fork failed :: %s\n", strerror(errno)); } }
3.2.2 🖋waitpid方法
//wait()函数的头文件 #include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options); //return value解读,假设返回值为 ret //if (ret > 0) 父进程等待子进程成功success,ret就是子进程的pid //if (ret < 0) 父进程等待子进程失败fail, ret为-1 //if (ret == 0) 且 options为WNOHANG,则说明子进程还没有终止 //参数解读 //1. pid //pid为要等待进程的pid,你想等待哪个进程,就输入那个进程的pid;也可以输入-1,输入-1表示等待任意子进程 //2. status //是一个输入输出型参数,这里主要是用的输出型的性质(可以参考wait中的status解读) //3. options // 0 : 阻塞等待子进程,和wait阻塞等待子进程一样 // WNOHANG : 非阻塞等待子进程,就是如果子进程没有终止,父进程不会暂停执行,还会继续向下执行代码 //当pid = -1, 且 options = 0及waitpid(-1, &status, 0) 等价于 wait(&status)
代码演示waitpid等待子进程
#include <stdio.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { //子进程 int cnt = 2; while(1) { printf("我是子进程,我正在运行...我的pid : %d\n", getpid()); sleep(1); if(cnt == 0) { break; } cnt--; } exit(1); } else if(id > 0) { //父进程 int status = 0; printf("我是父进程,我准备等待子进程啦...我的pid : %d\n", getpid()); sleep(4); pid_t ret = waitpid(id, &status, 0); if(ret > 0) { //wait child success if((status & 0x7f) == 0) { printf("子进程是正常退出的,退出码 : %d\n", (status>>8) & 0xff); } else { printf("子进程是异常退出的, 退出信号 : %d, 退出码 : %d\n", status & 0x7f, (status>>8) & 0xff); } } else//ret < 0 { //wait child fail printf("等待子进程失败\n"); } } else { //创建子进程失败 printf("fork failed :: %s\n", strerror(errno)); } }
非阻塞等待代码
#include <iostream> #include <vector> #include <string.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> typedef void (*handler_t)(); std::vector<handler_t> handlers; void Fun1() { std::cout << "hello, 我是Fun1" << std::endl; } void Fun2() { std::cout << "hello, 我是Fun2" << std::endl; } void Load() { handlers.push_back(Fun1); handlers.push_back(Fun2); } int main() { pid_t id = fork(); if (id == 0) { //子进程 int cnt = 5; while (1) { printf("我是子进程,我正在运行...我的pid : %d\n", getpid()); sleep(1); if (cnt == 0) { break; } cnt--; } exit(1); } else if (id > 0) { //父进程 int status = 0; printf("我是父进程,我准备等待子进程啦...我的pid : %d\n", getpid()); while (1) { // sleep(4); pid_t ret = waitpid(id, &status, WNOHANG); if (ret > 0) { // wait child success if ((status & 0x7f) == 0) { printf("子进程是正常退出的,退出码 : %d\n", (status >> 8) & 0xff); break; } else { printf("子进程是异常退出的, 退出信号 : %d, 退出码 : %d\n", status & 0x7f, (status >> 8) & 0xff); break; } } else if (ret == 0) { printf("子进程还没有退出,我继续执行我的代码啦!\n"); if(handlers.empty()) Load(); for(auto f : handlers) { f(); } sleep(1); } else // ret < 0 { // wait child fail printf("等待子进程失败\n"); } } } else { //创建子进程失败 printf("fork failed :: %s\n", strerror(errno)); } }
四、 ⚽进程程序替换
4.1 🎧进程程序替换的概念,原理(进程程序替换是什么东西,它是如何进行进程替换的)
1.如果不对子进程进行进程程序替换的话,子进程是执行的父进程的代码片段,如果我们想要子进程执行全新的程序呢?我们就会用到进程的程序替换。
那么说回来,进程程序替换就是让一个进程执行一个全新的程序(新的代码片段)。
2.操作系统通过系统调用,调用一些接口,将磁盘中的程序加载到内存结构当中,并且重新建立页表映射(谁执行程序替换,就重新建立谁的映射)。最终的效果是让父子进程彻底分离,让发生进程程序替换的进程执行一个全新的程序!!!
图一
根据父进程创建子进程的知识,父进程fork一个子进程,父子进程代码数据共享,数据会发生写时拷贝(父子进程之间相互独立)。而在发生进程程序替换的时候,我们可以根据上图发现,数据和代码都发生了写时拷贝(可以这样理解),子进程的页表映射到了新的程序的地址,和父进程彻底分离,子进程执行了全新的程序。在这个过程当中,并没有创建新的进程,只是把子进程要执行的程序替换为全新的程序。
4.2 🎧为什么要有进程程序替换
1.我们一般在服务器开发(Linux编程)的时候,往往需要子进程干两类事情
- 让子进程执行父进程的代码片段
- 让子进程执行磁盘中的一个全新的程序(比如在开发过程中,我们写两个不同功能模块用到了不同的语言,这个时候我们就可以通过我们的进程执行其他人写的进程代码等等)
4.3 🎧如何进行进程程序替换
4.3.1、首先跑一段程序替换的代码看看(单进程程序)
#include<stdio.h> #include<unistd.h> int main() { printf("我是一个进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a execl("/usr/bin/ls", "ls", "-l", "-a", NULL); printf("我执行完毕了,我的pid是%d\n",getpid()); return 0; }
下面为这段代码执行的结果:
我们可以很清楚的看到,在execl函数下面的的printf(“我执行完毕了,我的pid是%d\n”,getpid())语句没有被执行,我想此时你一定会好奇,为什么这一句代码不会被执行呢?因为execl函数是基于系统调用函数接口之上封装的一个用于进程的程序替换的函数。它会把新的程序加载到内存中,让该进程的页表映射到新的物理内存中的代码和数据,让它执行新的程序代码,原来的程序不在会有映射关系,不会在被执行(可以参考图一)。
4.3.2 🖋进程的程序替换接口学习(exec系列)
上面程序是一个单进程程序,而在实际中我们往往不会让主进程实现进程程序替换的,通常我们会让父进程创建一个子进程,让子进程进行进程程序替换
🌞execl
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> int main() { printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a execl("/usr/bin/ls", "ls", "-l", "-a", NULL); exit(1);//当子进程执行到这里的时候,说明程序替换失败了 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
下面为这一段代码执行的结果:
观察上图的执行结果,我们又发现,在execl函数之后的代码执行了后面的代码了呀,不是程序被替换了吗???页表与之前的程序没有映射关系了呀!为啥还可以执行后面的代码???
我们在把图一拿过来:
我们不要忘记了进程创建子进程的知识,父进程fork一个子进程,父子进程代码数据共享,数据会发生写时拷贝(父子进程之间相互独立)。而在发生进程程序替换的时候,我们可以根据上图发现,数据和代码都发生了写时拷贝(可以这样理解),子进程的页表映射到了新的程序的地址,和父进程彻底分离,子进程执行了全新的程序。在这个过程当中,并没有创建新的进程,只是把子进程要执行的程序替换为全新的程序,而父进程的页表还是映射的原来的代码和数据,所以父进程还是会执行原来的代码的。
3.进程的程序替换的函数接口
像上面来个进程进行进程的程序替换的时候,会用到进程的程序替换的函数接口,像execl这样基于系统调用函数接口之上封装的进程的程序替换的函数,还有很多,下面我们来一一介绍:
🌞execv
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> int main() { printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a char* const _arg[] = {(char*)"ls", (char*)"-l", (char*)"-a", NULL}; execv("/usr/bin/ls", _arg); exit(1);//当子进程执行到这里的时候,说明进程程序替换失败 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
下面为上面代码的执行结果:
对比这两种接口,我们会发现,它们本质还是一样的,只是一个是以可变参数进行传参,另一个是以指针数组进行传参,两个接口的第一个参数都是要执行程序的路径path。(execl 中的l可以理解为list,以可变参数支持的传参的,execv中的v可以理解为vector,以数组支持传参的)
🌞execlp
l还是按之前说的链表传参,p 是值得环境变量PATH,当我们执行的命令可以根据环境变量找的路径的时候,我们可以直接传入要执行的指令或者程序给execlp函数的第一个参数,让它根据环境变量去找到相应的路径。
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> int main() { printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a execlp("ls", "ls", "-l", "-a", NULL);//这里出现了两个"ls",可以省略一个吗??? exit(1);//当子进程执行到这里的时候,说明进程程序替换失败 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
上面代码的执行结果:
execlp("ls", "ls", "-l", "-a", NULL);//这里出现了两个"ls",可以省略一个吗???
上面说到execlp函数中会出现两个"ls",可以省略一个吗???
首先我们要明确这两个"ls"的意义,第一个"ls"的意义是要让类似与“ls”这样的系统指令根据环境变量去找到相应指令的路径,第二个"ls"的意思是告诉操作系统要以什么方式去执行这个指令,所以两个"ls"是都不可以被省略的。
🌞execvp
execvp函数相比与我们上面谈到execlp函数,只有一个字母v不相同,而我们说过l是以链表形式进行传参的,而v是以指针数组的形式进行传参的,下面为具体实现的代码:
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> int main() { printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a char* const _arg[] = {(char*)"ls", (char*)"-l", (char*)"-a", NULL}; execvp("ls", _arg); exit(1);//当子进程执行到这里的时候,说明进程程序替换失败 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
🌞execle
execle中的e 是environ,也就是需要我们传入环境我们所需要的环境量。
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> int main() { printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a char* const _env[] = {(char*)"MYPATH=YouCanSeeMe!!", NULL}; execle("./mycmd", "mycmd", NULL, _env); exit(1);//当子进程执行到这里的时候,说明进程程序替换失败 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
#include<iostream> #include<stdlib.h> using namespace std; int main() { cout << "-------------------------" << endl; cout << "PATH:" << getenv("PATH") << endl; cout << "-------------------------" << endl; cout << "MYPATH:" << getenv("MYPATH") << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; return 0; }
以下是上面代码的执行结果:
我们发现PATH和MYPATH都没有打印,我们单独运行一下mycmd,运行结果如下:
我们发现PATH可以被打印出来。我们继续把mycmd.cpp 的改一改:
#include<iostream> #include<stdlib.h> using namespace std; int main() { cout << "-------------------------" << endl; //cout << "PATH:" << getenv("PATH") << endl; //修改处 cout << "-------------------------" << endl; cout << "MYPATH:" << getenv("MYPATH") << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; cout << "hello c++" << endl; return 0; }
用我们的程序去调用mycmd,执行结果如下:
我们可以观察到,把PATH注释掉后,MYPAYH是可以被打印的,通过上面三个运行情况,我们不难得出,通过execle函数传进去的环境变量是覆盖式的,而不是添加式的。
如果我只是想单纯的添加环境变量可以这样:
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> int main() { //环境变量的指针声明 extern char** environ; printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); //用我们的程序去执行系统指令ls,选项是-l -a execle("./mycmd", "mycmd", NULL, environ); exit(1);//当子进程执行到这里的时候,说明进程程序替换失败 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
让后通过命令行添加MYPATH,export MYPAYH = “MYPATH=YouCanSeeMe!!”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mlMAf0II-1667620781081)(C:\Users\13916\AppData\Roaming\Typora\typora-user-images\image-20221003130146645.png)]
🌞execvpe
通过上面的学习,这个应该已经难不倒你们了吧!!!
v通过指针数组进行传参;p把要执行的指令传参过去,让他根据环境变量去找到相应的路径;e 覆盖式的传环境变量,也是以指针数组的方式进行传参。
🌞execve(真正的系统调用接口)
v通过指针数组进行传参;e 覆盖式的传环境变量,也是以指针数组的方式进行传参。
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> int main() { extern char** environ; printf("我是父进程,我的pid是%d\n",getpid()); pid_t id = fork(); if(0 == id) { printf("我是子进程,我的pid是%d\n",getpid()); char* const _arg[] = {(char*)"mycmd", NULL}; execve("./mycmd", _arg, environ); exit(1);//当子进程执行到这里的时候,说明进程程序替换失败 } int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { sleep(2); printf("父进程等待成功!\n"); } return 0; }
执行结果: