进程的控制

进程的控制 一、创建进程(1)fork函数初识在 linux 中 fork 函数是非常重要的函数它从已存在进程中创建⼀个新进程。新进程为子进程 而原进程为父进程进程调用fork 当控制转移到内核中的 fork 代码后内核做分配新的内存块和内核数据结构给⼦进程将⽗进程部分数据结构内容拷贝至⼦进程添加⼦进程到系统进程列表当中fork 返回开始调度器调度样例代码int main( void ) { pid_t pid; printf(Before: pid is %d\n, getpid()); if ( (pidfork()) -1 )perror(fork()),exit(1); printf(After:pid is %d, fork return %d\n, getpid(), pid); sleep(1); return 0; }代码运行结果⼦进程中返回0⽗进程返回⼦进程id出错返回-1​​第一条after是父进程打印的第二条after是子进程打印的(2)写时拷贝通常父子代码共享父子再不写入时数据也是共享的当任意一方试图写入便以写时拷贝的方式各自⼀份副本(程序替换的原理)因为有写时拷贝技术的存在,所以父子进程得以彻底分离离完成了进程独⽴性的技术保证!写时拷贝,是⼀种延时申请技术,可以提高整机内存的使用率补充父进程页表项100中正常是r和w但是fork之前会被修改成r因为当子进程想要修改数据时因为权限是只读当越权时OS就会知晓(中断)就会来进行出来(3)fork的常规用法⼀个⽗进程希望复制自己使父子进程同时执行不同的代码段例如⽗进程等待客户端请求 生成子进程来处理请求⼀个进程要执行⼀个不同的程序例如子进程从fork返回后调用exec函数(4)fork调用失败的原因系统中有太多的进程(因为进程也是一种资源从内存中申请来的如果拿了太多不归还的话内存就不足以再次给用户一份进程资源)实际用户的进程数超过了限制二、进程终止进程终止的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码(1)进程三种退出场景代码运行完毕结果正确代码运行完毕结果不正确代码异常终⽌补充当一个进程退出时OS会把进程退出的详细信息写入到进程的task_struct结构体中所以进程退出需要僵尸进程维持自己的状态(2)进程的常见退出方式正常终⽌(可以通过 echo $? 查看进程退出码)从main返回调用exit_exitctrlc信号终⽌Ⅰ.退出码退出码退出状态可以告诉我们最后⼀次执行的命令的状态在命令结束以后我们可以知道命令是成功完成的还是以错误结束的。其基本思想是程序返回退出代码0时表示执行成功没有问题代码1或0以外的任何代码都被视为不成功Linux Shell中的主要退出码退出码0表示命令执行无误这是完成命令的理想状态退出码1我们也可以将其解释为“不被允许的操作”。例如在没有sudo权限的情况下使⽤ yum再例如除以0等操作也会返回错误码1对应的命令为let a1/0130 (SIGINT或^C )和143(SIGTERM)等终⽌信号是⾮常典型的它们属于128n 信号其中n代表终⽌码可以使用strerror函数来获取退出码对应的描述Ⅱ._exit函数(系统调用)#include unistd.h void _exit(int status); 参数status 定义了进程的终⽌状态⽗进程通过wait来获取该值说明虽然status是int但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时在终端执⾏$?发现 返回值是255Ⅲ.exit函数(函数调用)#include unistd.h void exit(int status);exit最后也会调用_exit,但在调用_exit之前还做了其他⼯作执行用户通过atexit或on_exit定义的清理函数关闭所有打开的流所有的缓存数据均被写⼊调用_exit通过本图我们可知exit的底层封装了_exit只是在_exit的基础上添加了刷新缓冲等功能int main() { printf(hello); exit(0); } 运⾏结果: [rootlocalhost linux]# ./a.out hello[rootlocalhost linux]# int main() { printf(hello); _exit(0); } 运⾏结果: [rootlocalhost linux]# ./a.out [rootlocalhost linux]#由此可知缓冲区和刷新缓冲区的操作一定不是在内核中进行Ⅳ.return退出return是⼀种更常见的退出进程方法。执行return(n)等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数三、进程等待(1)进程等待的必要性之前讲过子进程退出⽗进程如果不管不顾就可能造成‘僵尸进程’的问题进而造成内存泄漏(因为资源不会被释放,进程也是有种资源)另外进程⼀旦变成僵尸状态那就刀枪不⼊“杀⼈不眨眼”的kill-9也⽆能为力因为谁也没有办法杀死⼀个已经死去的进程最后父进程派给⼦进程的任务完成的如何我们需要知道如子进程运行完成结果对还是不对或者是否正常退出⽗进程通过进程等待的方式回收子进程资源获取⼦进程退出信息(2)进程等待的方法wait方法#includesys/types.h #includesys/wait.h pid_t wait(int* status); 返回值 成功返回被等待进程pid失败返回-1。 参数 输出型参数获取⼦进程退出状态,不关⼼则可以设置成为NULLwaitpid方法pid_ t waitpid(pid_t pid, int *status, int options); 返回值 当正常返回的时候waitpid返回收集到的⼦进程的进程ID 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在 参数 pid Pid-1,等待任⼀个⼦进程。与wait等效。 Pid0.等待其进程ID与pid相等的⼦进程。 status: 输出型参数 WIFEXITED(status): 若为正常终⽌⼦进程返回的状态则为真。查看进程 是否是正常退出 WEXITSTATUS(status): 若WIFEXITED⾮零提取⼦进程退出码。查看进程 的退出码 options:默认为0表⽰阻塞等待 WNOHANG: 若pid指定的⼦进程没有结束则waitpid()函数返回0不予以等 待。若正常结束则返回该⼦进程的ID。如果⼦进程已经退出调用wait/waitpid时wait/waitpid会立即返回并且释放资源获得⼦进程退出信息如果在任意时刻调用wait/waitpid⼦进程存在且正常运⾏则进程可能阻塞如果不存在该子进程则⽴即出错返回(3)获取子进程的状态wait和waitpid都有⼀个status参数该参数是⼀个输出型参数由操作系统填充如果传递NULL表示不关心进程的退出状态信息否则操作系统会根据该参数将⼦进程的退出信息反馈给⽗进程status不能简单的当作整形来看待可以当作位图来看待具体细节如下图(只研究status低16比特位测试代码#include sys/wait.h #include stdio.h #include stdlib.h #include string.h #include errno.h int main( void ) { pid_t pid; if ( (pidfork()) -1 ) perror(fork),exit(1); if ( pid 0 ){ sleep(20); exit(10); } else { int st; int ret wait(st); if ( ret 0 ( st 0X7F ) 0 ){ // 正常退出 printf(child exit code:%d\n, (st8)0XFF); } else if( ret 0 ) { // 异常退出 printf(sig code : %d\n, st0X7F ); } } } 测试结果 # ./a.out #等20秒退出 child exit code:10 # ./a.out #在其他终端kill掉 sig code : 9阻塞等待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(257); } else { int status 0; pid_t ret waitpid(-1, status, 0);//阻塞式等待等待5S printf(this is test for wait\n); 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; } 运⾏结果: [rootlocalhost linux]# ./a.out child is run, pid is : 45110 this is test for wait wait child 5s success, child return code is :1.非阻塞等待#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() 之后,父子各自执行父进程代码的⼀部分如果⼦进程就想执行⼀个全新的程序呢进程的程序替换来完成这个功能程序替换是通过特定的接口加载磁盘上的⼀个全新的程序(代码和数据)加载到调用进程的地址空间中(1)替换原理用fork创建子进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调用⼀种exec函数以执行另⼀个程序。当进程调用⼀种 exec 函数时,该进程的用户空间代码和数据完全被 新程序替换,从新程序的启动例程开始执行调用exec并不创建新进程,所以调用exec前后该进程的id并未改变(一直都是子进程)(2)替换程序其实有六种以exec开头的函数,统称exec函数:#include unistd.h 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 execve(const char *path, char *const argv[], char *const envp[]);补充前五个exec函数都是库函数而execve是系统调用函数后带p不用告诉路径只需告诉执行命令它自己会去环境变量中找函数后带l指的是列表你想要怎么执行这个函数函数后带v指的是vector就是原本是一个一个传参数现在只需要把信息写入到一个vector数组中传入这个数组对于execve系统调用是唯一一个后面带e的作用是可以传入自己的环境变量覆盖给子进程传新的环境变量表对于所有的exec*函数替换成功后都没有返回值替换失败仍然往下继续执行程序替换的是二进制文件与语言无关举例你用中文、英文、法语写的菜谱对应源代码最终都会被厨师对应编译器 / 解释器转换成 “放多少克盐、炒几分钟” 这种厨房能直接执行的操作指令对应二进制文件厨房对应操作系统 / CPU只认这些具体的操作指令根本不管菜谱原本是用哪种语言写的 —— 这就是 “与语言无关” 的核心exec调用如下#include unistd.h int main() { char *const argv[] {ps, -ef, NULL}; char *const envp[] {PATH/bin:/usr/bin, TERMconsole, NULL}; execl(/bin/ps, ps, -ef, NULL); // 带p的可以使⽤环境变量PATH⽆需写全路径 execlp(ps, ps, -ef, NULL); // 带e的需要⾃⼰组装环境变量 execle(ps, ps, -ef, NULL, envp); execv(/bin/ps, argv); // 带p的可以使⽤环境变量PATH⽆需写全路径 execvp(ps, argv); // 带e的需要⾃⼰组装环境变量 execve(/bin/ps, argv, envp); exit(0); }五个exec*函数的关系