一.进程创建
1.1 fork函数
我们创建进程的方式有./xxx和fork()两种
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
1.分配新的内存块和内核数据结构给子进程
2.将父进程部分数据结构内容拷贝至子进程
3.添加子进程到系统进程列表当中
4.fork返回,开始调度器调度
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2.fork返回值
1.子进程返回0。
2.父进程返回的是子进程的pid。
1.3.写时拷贝
当子进程被创建出来,子进程中的数据和代码都是和父进程共用一份,也就是说父子进程的页表中的代码和数据都是只读权限,当子进程想要修改数据的时候,就会发生缺页中断,也就是子进程修改数据的操作被暂停,然后操作系统开辟新的空间,将数据的数值拷贝进该空间中,修改父子进程对于页表中该数据的权限为可读可写,将页表的数据地址指向该新开辟的空间,操作系统完成这些操作之后,子进程修改数据的操作继续进行,通过页表完成数据修改。
1.4 fork常规用法
1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.5 fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
二.进程终止
2.1 进程退出场景
1. 代码运行完毕,结果正确
2. 代码运行完毕,结果不正确
3. 代码异常终止
main函数的return值是进程的退出码
通过echo $?可以输出最近一次进程退出时的退出码,可以看到main函数的return值是进程的退出码
代码执行完毕,结果正确,只有一种可能,所以返回0,但是代码运行完毕,结果不正确,却有很多种可能性,在Linux中,我们查看到有134种错误的返回
当进程异常终止的时候就相当于程序运行崩溃,它的退出码是没有意义的,程序正常运行结束之后的退出码才有意义
我们除以0值为例子
它的程序运行会崩溃,退出码没有意义
2.2 进程常用退出方法
正常终止(可以通过echo $? 查看进程退出码):
1. 从main返回:
main函数return,代表进程退出!!其他的非main函数呢??代表的是函数返回
2. 调用exit:
exit在任意地方调用,都代表终止进程,参数是退出码!它跟main返回一样,都会刷新输出缓冲区
在4s之后,才刷新输出缓冲区,打印输出hello world
3. _exit:
终止进程,但强制终止进程,没有进行进程的后续收尾工作,比如刷新缓冲区(用户级缓冲区)!!
进程退出,在操作系统层面做了什么呢?
在系统层面,少了一个进程:释放进程控制块,释放进程地址空间,释放页表和各种映射关系,还有代码和数据也都会被释放掉!
三.进程等待
子进程被创建出来,是为了完成父进程的某些任务的,但是子进程和父进程何时结束,这是未知的,所以父进程fork之后,需要通过wait()/waitpid()等待子进程退出。
为什么要让父进程等待呢?
1.通过获取子进程退出的信息,能够得知子进程执行结果
2.可以保证:时序问题,子进程先退出,父进程后退出
3.进程退出的时候会先进入僵尸状态,会造成内存泄漏问题,需要通过父进程wait,释放该子进程占用的资源
两个等待函数:wait()和waitpid()
wait(int* status):
查看Linux进程的相关信息的指令:
while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "#####################################";done;
子进程被创建:
5秒后,子进程终止退出,进入僵尸状态:
10秒后,父进程回收子进程,父进程继续执行程序:
再过5秒父进程结束
上面这段代码是为了实现在子进程退出之后,成为僵尸进程,然后父进程继续执行程序,回收子进程的信息
waitpid(pid_t pid, int* status, int options):
执行结果跟wait()一样
在上面的代码中waitpid的第一个参数是要等待的那一个进程的pid,如果值为-1,表示等待任意一个子进程
第二个参数就是
我们通过上面的代码知道,当子进程退出码是0的时候,父进程从waitpid获取到的状态码是0,而子进程退出码是10的时候,父进程从waitpid获取到的状态码是2556;也就是说,父进程拿到什么status结果,一定和子进程如何退出是强相关的,而我们刚刚知道进程退出时,有三种结果,那么不难得出最终父进程一定会通过status得到子进程执行的结果,
如果程序代码运行完毕,结果与否,进程就会返回退出码给父进程(通过return / exit),如果代码异常终止,本质上时这个进程因为异常问题,导致自己收到某种信号
这个status是int类型,有32位,只使用低16位
父进程首先识别status的低7位终止信号是否为0,如果为0,表示正常退出,否则异常退出,若为0,继续识别高8位是否为0,为0结果正确
验证:
tips:退出码=(status>>8)&0xFF,退出信号=status&0x7F
代码运行完成 ,且结果正确的情况:
代码运行完成 ,结果不正确的情况:
代码运行过程中,收到信号异常退出的情况:
判断status的另外一种操作:
理解一下waitpid():
waitpid()处在用户层和操作系统之间,是操作系统提供给用户层的接口,在操作系统中,僵尸子进程的PCB中保存着进程退出时的退出数据,里面包括退出码和退出信号,父进程回收该子进程的时候,就会将status与退出码和退出信号进行位与运算,获取到带有子进程退出时的退出码和退出信号信息的退出状态status,然后将status返回给用户
在WaitPid()的第三个参数中,0表示阻塞等待,WNOHANG表示非阻塞等待
阻塞等待:父进程等待子进程退出,子进程不退出,父进程就会一直等待,直到子进程退出。
阻塞了是不是意味着父进程不被调度执行了呢?
不会,父进程会被链入到等待队列中,从R状态变为S状态,不会被CPU调度执行,直到子进程退出,父进程从等待队列中取出,然后S状态变为R状态,被插入运行队列中,被CPU继续调度执行
阻寒的本质: 其实是进程的PCB被放入了等待队列,并将进程的状态改为S状态,返回的本质: 进程的PCB从等待队列拿到R队列,从而被CPU调度
非阻塞等待:父进程轮询检测子进程是否退出,如果子进程退出,查看waitpid检测成功与否,如果子进程没有退出,就继续轮询检测子进程是否退出
测试代码:
执行结果:
子进程在执行代码得时候,父进程轮询等待,期间每轮询一次,父进程在做自己的事情。
四.程序替换
4.1为什么要进行程序替换??
如果我们想让一个子进程执行一个全新的程序的时候,我们就需要程序替换
4.2 什么是程序替换?原理是什么?
进程不变,仅仅替换当前进程得代码和数据得技术叫做进程得程序替换
程序本质上就是存放在磁盘中的文件,这些文件=程序代码+程序数据,操作系统将该文件中的代码和数据替换到已存在的进程中的代码和数据,这一过程我们称之为程序替换!!他并没有创建新的进程,只是老进程的壳子(进程地址空间,PCB,页表)不变,把新程序的代码和数据替换进物理内存就可以了
在上面的代码中,程序替换之后的的代码是不会执行的
程序替换的本质就是把程序的进程代码+数据,加载进特定进程的上下文中!!C/C++程序要运行,必须得先加载到内存中!那么怎么加载呢?通过exce*程序替换函数。
在上面的代码中,由于进程具有独立性,以及写时拷贝的存在,虽然父子代码是共享的,但是进程程序替换会更改代码区的代码,然后发生写时拷贝
执行结果:
进程的程序替换的使用?
1.现象
2.fork()
3.exec*返回值
4.3 各个程序替换函数的基本使用
替换函数:
其实有六种以exec开头的函数,统称exec函数:
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[]);
函数解释:
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
命令理解:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
1.execl(const char* path, const char* arg, ...);
在execl中,第一个参数是你的要执行的目标程序的全路径(所在路径/文件名)
第二个参数往后就是要执行的目标程序,在命令行上怎么写的这里的参数就怎么一个一个的传递进去,最后以NULL结尾
等价于
2.int execlp(const char *file, const char *arg, ...);
在execlp中,第一个参数是你的要执行的目标程序的文件名(不带路径,会自动去环境变量PATH找该文件)
第二个参数往后就是要执行的目标程序,在命令行上怎么写的这里的参数就怎么一个一个的传递进去,最后以NULL结尾
3.int execv(const char *path, char *const argv[]);
在execv中,第一个参数是你的要执行的目标程序的全路径(所在路径/文件名)
第二个参数是一个命令行参数数组,把命令行上写的那些参数一个一个写进这个数组里面
4.int execvp(const char *file, char *const argv[]);
在execv中,第一个参数是你的要执行的目标程序的文件名(不带路径,会自动去环境变量PATH找该文件)
第二个参数是一个命令行参数数组,把命令行上写的那些参数一个一个写进这个数组里面
5.int execle(const char *path, const char *arg, ...,char *const envp[]);
在execlp中,第一个参数是你的要执行的目标程序的文件名(不带路径,会自动去环境变量PATH找该文件)
第二个参数往后就是要执行的目标程序,在命令行上怎么写的这里的参数就怎么一个一个的传递进去,最后以NULL结尾
第三个参数是给该程序赋予环境变量
让myproc的子进程执行程序myexe,在myproc的子进程中使用execle函数将指定的环境变量给程序myexe,然后程序myexe打印出来
6.int execve(const char *path, char *const argv[], char *const envp[]);
在execlp中,第一个参数是你的要执行的目标程序的文件名(不带路径,会自动去环境变量PATH找该文件)
第二个参数是一个命令行参数数组,把命令行上写的那些参数一个一个写进这个数组里面
第三个参数是给该程序赋予环境变量
有了这些函数,我们就可以是在C/C++中调用其他编程语言的程序了!!
所有的接口看起来是没有太大差别的,只有一个参数的不同!!
为什么会有这么多接口,是为了满足不同的应用场景的
六个函数接口的关系:
execve是系统调用函数其他函数都是在该函数的基础上进行的封装
4.4 利用程序替换实现shell命令解释器
在Linux中shell命令行解释器本质上就是一个进程,它的名字叫bash,我们在命令行中执行的程序的父进程就是它,它通过解析我们在命令行上输入的命令字符串,然后去环境变量PATH中找到相应的命令程序,创建子进程来执行该命令程序,最后将结果打印出来。
上面说的是执行第三方命令的时候就会这样操作,但是执行内建命令的时候,就不需要创建子进程去执行命令,而是直接shell进程调用命令函数来执行内建命令
tips:什么是内建命令,什么是第三方命令?
第三方命令:
外部命令,有时候也被称为文件系统命令,是存在于bash shell之外的程序。外部命令程序通常位于/bin、/usr/bin、/sbin或/usr/sbin中。外部命令需要使用子进程来执行。受到环境变量的影响
内建命令:
它是shell的一部分,执行内建命令等于调用bash shell程序的一个程序,它不会受到环境变量的影响,内建命令比外部命令,效率更高,执行更快,执行内建命令相当于调用当前 Shell 进程的一个函数。比如cd、exit 这些是内部命令,本质是函数调用,可以直接使用,内建命令并不是某个外部程序,而是bash shell该程序的组成部分,只要在 bash shell 中就可以运行这个命令。
代码:
1 #include<stdlib.h>
2 #include<iostream>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 #include<sys/types.h>
6 #include<string.h>
7 #define NUM 128
8 #define CMD_NUM 64
9
10 int main()
11 {
12 char command[NUM];//命令字符串
13 while(1)
14 {
15 char* argv[CMD_NUM] = {NULL};
16 //1.打印提示符
17 command[0] = 0;//清空字符串;
18 std::cout << "[who@myhostname mysir]# ";
19 fflush(stdout);
20 //sleep(10);
21
22 //2.获取命令字符串
23 fgets(command, NUM, stdin);//从命令行获取命令字符串
24 command[strlen(command) - 1] = 0;
25 //std::cout << "command is: " << command << std::endl;
26
27 //3.解析命令字符串,char* argv[];
28 int cnt = 0;
29 argv[0] = strtok(command, " ");
30 cnt++;
31 while(argv[cnt] = strtok(NULL, " "))
32 {
33 cnt++;
34 }
35
36 //4.执行内建命令,相当于调用一个函数
37 if(strcmp(argv[0], "cd"))
38 {
39 if(argv[1] != NULL) chdir(argv[1]);
40 continue;
41 }
42
43 //执行第三方命令
44 if(fork() == 0)//创建子进程执行第三方命令
45 {
46 execvp(argv[0], argv);
47 //td::cout << "执行命令失败" << std::endl;
48 exit(1);
49 }
50 waitpid(-1, NULL, 0);
51 }
52 return 0;
53 }