十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Linux系统】十二节—进程终止 + 进程等待 + 进程程序替换

【Linux系统】十二节—进程终止 + 进程等待 + 进程程序替换 hello好久不见我是云边有个稻草人后续我会继续更新本篇文章所属专栏《Linux系统》目录一、进程创建—回顾二、进程终止2.1 进程退出场景2.2 退出码2.3 进程的退出方法三种三、进程等待3.1 为什么要进程等待3.2 进程等待是什么3.3 获取⼦进程status3.4 阻塞与⾮阻塞等待四、进程程序替换4.1 替换原理4.2 替换函数一、进程创建—回顾初始父进程代码段页表权限只读数据段可读可写。fork 创建子进程后父子进程共享物理页系统将双方数据段页表权限统一改为只读代码段依旧保持只读。任意进程尝试写数据段内存触发缺页异常内核执行写时拷贝分配新物理页、拷贝数据修改对应页表权限为可写后续各自独立访问。页面被标记只读进程执行写指令时CPU 硬件判定权限非法触发缺页异常陷入内核态处理内核识别是 fork 后的写时拷贝场景新建物理页、复制数据更新当前进程页表权限为可写程序正常继续执行为什么要有写时拷贝减少创建时间fork 时只复制页表不拷贝实际数据子进程创建秒级完成。节约物理内存父子共享同一份物理页面避免重复占用空间。按需延迟拷贝仅数据被修改时才复制页面减少无效拷贝开销。二、进程终止进程终⽌的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码。2.1进程退出场景代码运⾏完毕结果正确代码运⾏完毕结果不正确例如fopen一个文件但是打开失败了走的是失败的逻辑路线代码异常终⽌野指针子进程也是进程子进程由父进程创建子进程的执行结果无非就是上面的三种之一。函数里的局部变量到底怎么返回给外面为什么要用寄存器局部变量存在栈stack里函数结束栈会销毁变量就没了。CPU 规定用固定的寄存器存放返回值寄存器不在栈里不会被销毁。函数返回时把局部变量的值 → 复制到寄存器函数退出、栈销毁调用方从寄存器里取出值使用这就是局部变量能安全返回的根本原因。2.2退出码打印最近一个程序进程退出时的退出码也就是进程退出码 echo $?子进程的退出码要写到子进程的task_struct内部如果父进程要获取子进程的退出码父进程往子进程的PCB里读就能读到子进程的退出结果。怎么知道错误码对应的错误信息呢可以使⽤strerror函数来获取退出码对应的描述可以直接return errno所以一个进程程序的运行结果由进程的退出码决定。当一个程序/进程异常终止的时候退出码无意义。进程一旦出现异常一般是进程收到了信号TODO2.3 进程的退出方法三种1mainreturn函数结束表示进程结束其他函数只表示自己函数调用完成返回执⾏return n等同于执⾏exit(n)因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数。2exit(status)status表示进程的退出码#include unistd.h void exit(int status);看下面的现象所以任何地方调用exit表示进程结束并返回给父进程bash子进程的退出码。exit最后也会调⽤_exit, 但在调⽤_exit之前还做了其他⼯作执⾏⽤⼾通过 atexit或on_exit定义的清理函数。关闭所有打开的流所有的缓存数据均被写⼊调⽤_exit3_exit()status表示进程的退出码谁调我我把谁干死退出#include unistd.h void _exit(int status); 参数status 定义了进程的终⽌状态⽗进程通过wait来获取该值那exit() vs _exit()下面进行解答看下面的现象将exit()换成_exit()so\n在终端的行缓冲模式下是强制刷新缓冲区的关键。exit()会在退出前主动刷新所有 I/O 缓冲区而_exit()完全不会。所以exit()和_exit()的区别是什么1、exit()C 库函数_exit()Linux 系统调用2、库函数是对系统调用的封装exit()的底层封装了_exit()三、进程等待3.1为什么要进程等待1、⼦进程退出⽗进程如果不管不顾就可能造成‘僵⼫进程’的问题进⽽造成内存泄漏。2、另外进程⼀旦变成僵⼫状态那就⼑枪不⼊“杀⼈不眨眼”的kill -9 也⽆能为⼒因为谁也没有办法杀死⼀个已经死去的进程。3、最后⽗进程派给⼦进程的任务完成的如何我们需要知道。如⼦进程运⾏完成结果对还是不对或者是否正常退出。4、所以⽗进程通过进程等待的⽅式回收⼦进程资源获取⼦进程退出信息。真正产生僵尸进程的唯一条件 必须同时满足 3 点 子进程先退出父进程还活着父进程没有调用 wait () /waitpid () 去回收如果父进程先死子进程还活着此时的子进程就是孤儿进程子进程会被init 进程PID1收养子进程以后结束时init 会自动帮它收尸不会变成僵尸3.2 进程等待是什么1wait等待任意一个退出的子进程#includesys/types.h #includesys/wait.h pid_t wait(int* status); 返回值 成功返回被等待进程pid失败返回-1。 参数 输出型参数获取⼦进程退出状态,不关⼼则可以设置成为NULL上面的代码中如果等待子进程子进程没有退出父进程会阻塞在wait调用处例如scanf。2waitpidpid_ t waitpid(pid_t pid, int *status, int options); 返回值当正常返回的时候waitpid返回收集到的⼦进程的进程ID失败则返回-1 pid选择等待指定子进程 status: 输出型参数让父进程获取子进程的退出信息下面我们来创建一个等待失败的场景不存在指定的子进程3.3获取⼦进程status基本认识有个问题为什么那如果进程异常终止呢?进程异常终止分两种情况一种是代码异常终止代码崩了一种是被外界使用信号终止讨论一下三个问题1父进程是如何拿到子进程的退出码退出信号的呢属于子进程的信息进程之间具有隔离性怎么办到的父子进程正常运行用户空间互相隔离不能直接访问对方内存。子进程退出 / 被信号杀死操作系统内核处理退出释放子进程的代码、数据、页表、虚拟地址空间把退出码、终止信号存入子进程的 PCB子进程变成僵尸进程只保留 PCB父进程调用系统调用waitpid ()这是父进程主动向操作系统要子进程信息父进程进入内核态操作系统内核工作根据 PID 找到子进程 PCB从 PCB 中读取退出码、退出信号把这些信息拷贝给父进程的 status 变量内核删除子进程 PCB僵尸进程彻底消失父进程回到用户空间解析 status拿到子进程的退出结果2WEXITSTATUS(status)WIFEXITED(status)3.4阻塞与⾮阻塞等待下面演示下非阻塞轮询如何让父进程做自己的事情呢#include stdio.h #include stdlib.h #include sys/wait.h #include unistd.h #include vector typedef void (*handler_t)(); // 函数指针类型 std::vectorhandler_t handlers; // 函数指针数组 void fun_one() { printf(这是一个临时任务1\n); } void fun_two() { printf(这是一个临时任务2\n); } void Load() { handlers.push_back(fun_one); handlers.push_back(fun_two); } void handler() { if (handlers.empty()) Load(); for (auto iter : handlers) iter(); } int main() { pid_t pid; pid fork(); if (pid 0) { printf(%s fork error\n, __FUNCTION__); return 1; } else if (pid 0) { // child printf(child is run, pid is : %d\n, getpid()); sleep(5); exit(1); } else { int status 0; pid_t ret 0; do { ret waitpid(-1, status, WNOHANG); // 非阻塞式等待 if (ret 0) { printf(child is running\n); } handler(); } while (ret 0); if (WIFEXITED(status) ret pid) { printf(wait child 5s success, child return code is :%d.\n, WEXITSTATUS(status)); } else { printf(wait child failed, return.\n); return 1; } } return 0; }四、进程程序替换fork()之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢进程的程序替换来完成这个功能 程序替换是通过特定的接⼝加载磁盘上的⼀个全新的程序(代码和数据)加载到调⽤进程的地址空间中4.1替换原理在程序替换的过程中并没有创建新的进程只是把当前进程的代码和数据用新的程序的代码和数据覆盖式的进行替换。1、一旦程序替换成功就去执行新代码了原始代码的后半部分已经不存在了2、exec*函数只有失败返回值没有成功返回值所以exec*系列的函数不用做返回值判断只要返回就是失败execl() 之前 进程 PID 1000 ├── file1 的代码 ├── file1 的全局数据 ├── file1 的堆 └── file1 的栈 execl() 成功之后 进程 PID 1000 ├── ls 的代码 ├── ls 的全局数据 ├── ls 的堆 └── ls 的新栈4.2替换函数其实有六种以exec开头的函数统称exec函数。#includeunistd.h【execl】上面的程序替换影响到了父进程那如何才能不影响呢创建一个子进程让子进程去进程程序替换能替换我们自己写的程序吗一切能转化为进程的程序全都能替换现在尝试使用C语言去调用C程序成功调用。同样的也可以去调shell脚本python这里的“加载器”指的是当子进程调用execl()后系统负责把/usr/bin/ls装入子进程地址空间并启动它的机制。加载器是系统内部真正负责加载新程序的机制或组件。父进程 | | fork() v 父进程 子进程 | | execl(/usr/bin/ls, ...) v 子进程中的原程序被替换为 ls【execlp】【execv】【execvp】【execvpe】putenv给当前进程新增或修改环境变量。它只影响当前进程以及之后由它创建并继承环境的子进程不会反向修改父进程。putenv(MYVAL123);【execve】int execve(const char *path, char *const argv[], char *const envp[]);这是真正的系统调用execl / execlp / execv / execvp / execvpe都是基于 execve 的库封装。至此本篇结束——我是云边有个稻草人期待与你的下一次相遇凡是让你难受的东西只要它是正向的就一定能帮助你成长告诉自己不要害怕
返回列表