进程控制一、进程创建1. 通过fork()函数创建新进程在linux中fork函数是非常重要的函数它从已存在进程中创建⼀个新进程。新进程为子进程而原进程为父进程。调用fork函数后系统做的分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中fork返回开始调度器调度2.fork的常见用法⼀个父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程等待客户端请求生成子进程来处理请求。⼀个进程要执行⼀个不同的程序。例如子进程从fork返回后调用exec函数。3.fork调用失败系统中有太多的进程实际用户的进程数超多了限制少见二、写实拷贝Copy‑On‑WriteCOWfork()时发生什么内核创建子进程新 task_struct进程控制块新 mm_struct虚拟地址空间复制父进程页表但不复制物理内存所有共享页的权限设为只读父子进程虚拟地址完全一样物理地址完全共享都只能读不能写什么时候真正拷贝写触发只要任何一方父 / 子对共享页执行写操作CPU 检测到页表标志位只读 →触发缺页异常Page Fault写不进去内核判断是 COW 场景分配新物理页把旧页内容复制到新页修改当前进程页表指向新页 → 设为可写另一进程继续共享原页结果只复制 “被写的那一页”不是全量拷贝三、进程终止三种结果进程终止的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码。进程终止有三种结果代码运行完毕结果正确代码运行完毕结果不正确代码异常终止进程main函数返回的相当于一个进程退出码在Linux中可以通过命令echo $?将最近一次进程运行的退出码打印在屏幕上。一个进程正常退出就是main函数返回0除了返回0和1其他返回值都代表着不成功。1一般是程序员自己定义的通用错误。错误码与退出码的区别1. 退出码进程退出状态码全称进程退出码 exit code作用一个进程结束运行后留给父进程看的运行结果来源程序里exit(数值)main函数return 数值被信号杀死由内核赋值范围0~255只占低 8 位2. 错误码系统调用错误码 errno作用调用 Linux 内核函数失败时标记失败原因来源open/read/write/fork/pipe等系统调用执行失败本质全局整型变量int errno范围几十上百个宏值EPERM、ENOENT、EINTR…头文件errno.h特点只有调用失败才赋值成功不清空3.核心五大区别所属主体不同退出码整个进程跑完后的最终结果错误码某一次系统调用失败的原因使用时机不同退出码进程彻底结束之后使用错误码程序运行中途调用函数出错立刻查看取值范围不同退出码0~255错误码多枚举宏数量极多获取方式不同退出码父进程wait读取子进程状态错误码程序内部直接读全局变量errno4.直观例子例子 1退出码intmain(){return0;// 退出码 0 成功}Shell 执行echo $?查看上一条命令退出码例子 2错误码intfdopen(不存在的文件.txt,O_RDONLY);if(fd0){printf(%d\n,errno);// 打印错误码代表文件不存在}5.最容易混淆的点退出码可以自己随便设errno 是内核固定规定一个进程只有一个最终退出码但运行中能产生无数次 errno 错误码$?拿到的是退出码不是errno程序调用系统调用出错 → 产生errno程序结束运行 → 给出退出码三种结果的表示Linux中进程终止有不同信号1代码跑完结果正确运行期间没有收到信号0 return 0 - signumber:0 退出码:02代码跑完结果错误signumber:0 退出码 !0;3代码没跑完进程异常。signumber:!0此时退出码已经没有意义了此时关注的就是什么原因导致的异常Linux中就是被信号终止了所以一个进程执行的结果状态可以用两个数字表示int sig、int exit_code。用户不需要维护这些当一个进程结束时OS会把进程退出的详细信息写入到进程的task_struct结构体中那么进程退出需要僵尸维护自己的退出状态不考虑进程异常如何退出进程main函数return在任意地方exit()函数exit()与_exit()的区别一、本质区别exit()属于C 标准库函数封装了系统调用作用正常、优雅地终止进程会做大量清理工作_exit()属于Linux 系统调用作用立即、暴力终止进程不做任何清理工作#includestdio.h#includestdlib.h#includeunistd.hintmain(){printf(Hello);// 没有 \n数据在缓冲区里// _exit(0); // 用这个 → 什么都不输出exit(0);// 用这个 → 会输出 Hello}二、最关键的区别3点是否刷新 I/O 缓冲区exit ()会刷新_exit ()不刷新层级不同exit () 库函数上层_exit () 系统调用底层使用场景exit ()正常程序退出_exit ()子进程在 fork 后 exec 前使用防止缓冲区混乱四、进程等待进程等待的必要性回收子进程资源子进程退出后若父进程不等待会变成僵尸进程占用进程号等系统资源造成资源泄漏。获取子进程退出状态父进程可通过等待拿到子进程退出码判断子进程是正常结束、异常终止还是被信号终止。控制父子进程执行顺序让父进程阻塞等待子进程完成任务后再继续执行实现业务逻辑先后次序。避免孤儿进程防止父进程先退出子进程被 init 进程接管导致进程管理混乱。保证数据交互完整性确保子进程读写、运算等任务执行完毕父进程再读取其运行结果。等待方法当父进程还在进行而子进程结束时#includestdio.h#includeunistd.h#includestring.h#includeerror.h#includestdlib.hintmain(){pid_tidfork();if(id0){intcnt5;while(cnt--){printf(我是子进程, pid: %d\n,getpid());sleep(1);}exit(0);}elseif(id0){while(1){printf(我是父进程, pid : %d\n,getpid());sleep(1);}}return0;}1.wait()函数#includesys/types.h#includesys/wait.hpid_twait(int*status);// 返回值成功返回被等待进程pid失败返回-1。// 参数输出型参数获取子进程退出状态,不关⼼则可以设置成为NULL通过在父进程中添加代码// 等待子进程pid_tridwait(NULL);if(ridid){// 等待成功printf(pid: %d, wait success!\n,getpid());}也就是说当父进程wait子进程但是子进程就是没有退出则父进程会阻塞在wait函数中再利用sleep来直观的查看对僵尸进程的改善2.waitpid函数更推荐#includesys/types.h#includesys/wait.hpid_ twaitpid(pid_tpid,int*wstatus,intoptions);// 当pid-1;options0时函数等同于wait()参数pidpid 0等待指定 pid子进程pid 0等待同组任意子进程pid -1等待任意子进程等价 waitpid -1等待指定进程组任意子进程wstatus传出参数存子进程退出信息传NULL表示不关心。常用宏WIFEXITED(w)正常退出为真WEXITSTATUS(w)获取退出码options0阻塞等待默认WNOHANG非阻塞不等待无子进程退出立即返回 0返回值0成功返回退出子进程 pid0非阻塞模式子进程还没退出-1出错将wait函数换成waitpid后3.waitpid的返回参数wstatus当我们将子进程的退出码设置为1if(id0){intcnt5;while(cnt--){printf(我是子进程, pid: %d\n,getpid());sleep(1);}exit(1);// 修改}我们知道wstatus是获取子进程退出信息的子进程退出有三种情况三种情况与两个数字有关所以wstatus本质是得到进程退出的两个数字那一个wstatus怎么得到两个数字呢其实wstatus是有32个比特位所以当我们需要拿到具体的退出码或者错误码时底层用的是位移加按位与平常使用时就用定义好的宏intexit_code((wstatus8)0xff);// 1111 1111intexit_sigwstatus0x7f;// 0111 1111// 将子进程退出码改为123exit(123);那我们再试一试将父子都设置成死循环再通过kill -9杀死子进程会发生什么此时父进程立马回收并显示子进程的退出信号9。4.阻塞与非阻塞第三个参数options决定阻塞 / 非阻塞1阻塞模式默认waitpid(pid,status,0);options0阻塞等待逻辑父进程卡死不动一直等到指定子进程退出函数才返回特点父进程暂停执行专一等子进程结束2非阻塞模式waitpid(pid,status,WNOHANG);optionsWNOHANG非阻塞逻辑不等立刻返回子进程已退出返回子进程 PID子进程还在运行立刻返回 0父进程继续往下跑代码非阻塞的用法如果只用一次非阻塞子进程没结束就直接跳过回收容易产生僵尸进程正确用法循环轮询while(1){// 非阻塞查看pid_tretwaitpid(-1,NULL,WNOHANG);if(ret0)printf(回收子进程\n);elseif(ret0){// 子进程还在跑父进程做别的事printf(子进程运行中父进程忙别的\n);sleep(1);}elsebreak;// 没有子进程了}完整的测试代码#includestdio.h#includeunistd.h#includestring.h#includeerror.h#includestdlib.h#includesys/types.h#includesys/wait.hintmain(){pid_tidfork();if(id0){intcnt5;while(cnt--){printf(我是子进程, pid: %d\n,getpid());sleep(1);}exit(10);}else{while(1){intwstatus0;pid_tridwaitpid(id,wstatus,WNOHANG);if(rid0){printf(wait success,退出的子进程是: %d, exit_code: %d\n,rid,WEXITSTATUS(wstatus));break;}elseif(rid0){printf(子进程还在运行父进程还得等\n);sleep(2);}else{perror(waitpid\n);break;}}}return0;}五、进程程序替换1.现象C语言头文件unistd.h中有一系列程序替换的相关函数exec*intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);试试execl函数#includestdio.h#includeunistd.hintmain(){printf(我变成了一个进程%d\n,getpid());// 执行另一个程序execl(/usr/bin/ls,-a,-l,NULL);// 程序替换函数printf(我的代码运行中...);printf(我的代码运行中...);printf(我的代码运行中...);printf(我的代码运行中...);return0;}我们发现此时进程没有执行之后的代码而是执行了ls -a -l2.原理其中我们发现在替换的过程中有一个文件的IO过程它是由OS来完成的。补充在运行代码程序时其实最开始运行的程序是一个加载器加载器通过找到需要运行的目标程序进行程序替换来运行那个目标程序。而且一般我们用程序替换都是在子进程中替换而且因为子进程需要替换那么肯定就不能还和父进程共享数据了此时就会发生写实拷贝3.系列函数说明execl、execlp、execle、execv、execvp、execvel(list): 表示参数采用列表那么实际传参里就要有NULLv(vector): 参数用数组实际传参里可以没有NULLp(path): 有 p 自动搜索环境变量 PATHe(env): 表示自己维护环境变量传入时用的就是自己的新的系统的不参与返回值规则相同成功时程序直接被新程序覆盖原来代码全部没了所以不会回到exec*后面代码无返回值失败时会return -1用来告诉程序启动新程序失败了继续往下跑原来代码。传参规则1execl// 格式execl(全路径, 程序名, 参数1, 参数2, ..., NULL);execl(/bin/ps,ps,-ef,NULL);必须写绝对路径挨个写参数末尾补NULL2execlp// 格式execlp(程序名, 程序名, 参数..., NULL);execlp(ps,ps,-ef,NULL);不用全路径自动搜 PATH其余同 execl逐个传参3execlechar*constenvp[]{PATH/bin:/usr/bin,TERMconsole,NULL};// 格式execle(全路径, 程序名, 参数..., NULL, 自定义环境数组);execle(/bin/ps,ps,-ef,NULL,envp);绝对路径 逐个传参最后多传一层环境变量数组4execvchar*constargv[]{ps,-ef,NULL};// 格式execv(全路径, 参数字符串数组);execv(/bin/ps,argv);绝对路径所有参数提前放进char * 数组数组尾存 NULL5execvpchar*constargv[]{ps,-ef,NULL};// 格式execvp(程序名, 参数字符串数组);execvp(ps,argv);搜 PATH不用全路径参数放数组传入6execve原生系统调用char*constargv[]{ps,-ef,NULL};char*constenvp[]{PATH/bin:/usr/bin,TERMconsole,NULL};// 格式execve(全路径, 参数数组, 环境变量数组);execve(/bin/ps,argv,envp);三个参数路径、参数数组、环境数组无可变参纯数组传参7传入环境变量注意如果传入的是自己整的一个数组比如像char *const envp[] {PATH/bin:/usr/bin, TERMconsole, NULL};函数用的时候就只会用到数组里面仅有的这些进程不会用原有的系统环境变量当我们需要用到进程原有的系统环境变量并且还要追加用自己的时我们可以取到首先要将环境变量定义出来再用函数putenv()追加自己的char*constargv[]{myexe,-a,NULL};externchar**environ;putenv((char*)myenvabcd);execve(./myexe,argv,environ);补充所以之前main函数的环境变量的参数也是父进程通过程序替换时传入的参数。f, NULL};char *const envp[] {“PATH/bin:/usr/bin”, “TERMconsole”, NULL};