一、创建进程
(1)fork函数初识
- 在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程, 而原进程为父进程
- 进程调用fork ,当控制转移到内核中的 fork 代码后,内核做:
- 分配新的内存块和内核数据结构给⼦进程
- 将⽗进程部分数据结构内容拷贝至⼦进程
- 添加⼦进程到系统进程列表当中
- fork 返回,开始调度器调度
- 样例代码
cppint main( void ) { pid_t pid; printf("Before: pid is %d\n", getpid()); if ( (pid=fork()) == -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
- _exit
- ctrl+c,信号终⽌
Ⅰ.退出码
- 退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态;在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0时表示执行成功,没有问题;代码1或0以外的任何代码都被视为不成功
- Linux Shell中的主要退出码:
- 退出码0表示命令执行无误,这是完成命令的理想状态
- 退出码1我们也可以将其解释为"不被允许的操作"。例如在没有sudo权限的情况下使⽤ yum;再例如除以0等操作也会返回错误码1 ,对应的命令为let a=1/0
- 130 (SIGINT或^C )和143(SIGTERM)等终⽌信号是⾮常典型的,它们属于128+n 信号,其中n代表终⽌码
- 可以使用strerror函数来获取退出码对应的描述
Ⅱ._exit函数(系统调用)
cpp#include <unistd.h> void _exit(int status); 参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
- 说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现 返回值是255
Ⅲ.exit函数(函数调用)
cpp#include <unistd.h> void exit(int status);exit最后也会调用_exit,但在调用_exit之前,还做了其他⼯作:
- 执行用户通过atexit或on_exit定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写⼊
- 调用_exit
通过本图我们可知,exit的底层封装了_exit,只是在_exit的基础上添加了刷新缓冲等功能
cppint main() { printf("hello"); exit(0); } 运⾏结果: [root@localhost linux]# ./a.out hello[root@localhost linux]# int main() { printf("hello"); _exit(0); } 运⾏结果: [root@localhost linux]# ./a.out [root@localhost linux]#由此可知,缓冲区和刷新缓冲区的操作,一定不是在内核中进行
Ⅳ.return退出
return是⼀种更常见的退出进程方法。执行return(n)等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数
三、进程等待
(1)进程等待的必要性
- 之前讲过,子进程退出,⽗进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏(因为资源不会被释放,进程也是有种资源)
- 另外,进程⼀旦变成僵尸状态,那就刀枪不⼊,"杀⼈不眨眼"的kill-9也⽆能为力,因为谁也没有办法杀死⼀个已经死去的进程
- 最后,父进程派给⼦进程的任务完成的如何,我们需要知道;如,子进程运行完成,结果对还是不对,或者是否正常退出
- ⽗进程通过进程等待的方式,回收子进程资源,获取⼦进程退出信息
(2)进程等待的方法
- wait方法
cpp#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
- waitpid方法
cpppid_ t waitpid(pid_t pid, int *status, int options); 返回值: 当正常返回的时候waitpid返回收集到的⼦进程的进程ID; 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0; 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在; 参数: pid: Pid=-1,等待任⼀个⼦进程。与wait等效。 Pid>0.等待其进程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比特位)
测试代码:
cpp#include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int main( void ) { pid_t pid; if ( (pid=fork()) == -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", (st>>8)&0XFF); } else if( ret > 0 ) { // 异常退出 printf("sig code : %d\n", st&0X7F ); } } } 测试结果: # ./a.out #等20秒退出 child exit code:10 # ./a.out #在其他终端kill掉 sig code : 9阻塞等待:
cppint 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; } 运⾏结果: [root@localhost linux]# ./a.out child is run, pid is : 45110 this is test for wait wait child 5s success, child return code is :1.非阻塞等待
cpp#include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> #include <vector> typedef void (*handler_t)(); // 函数指针类型 std::vector<handler_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函数:
cpp#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调用如下
cpp#include <unistd.h> int main() { char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 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*函数的关系










