目录
一、进程等待
理解进程等待
父进程等创建子进程是为了完成某种任务,当子进程完成任务后父进程需要回收子进程,所以父进程等待子进程退出的过程就叫做进程等待!
所以进程等待可能等待的不止是硬件资源,也有可能是在等待软件资源(进程)!
进程等待的必要性1. 解决子进程处于僵尸状态带来的内存泄漏问题(博客: 进程概念和进程状态)2. 父进程需要知道子进程执行任务的结果(退出码与终止信号)
演示进程等待
获取进程的status
下面要介绍的两个系统调用 wait 和 waitpid 都有1个参数,参数名字就叫做 status, status是一个输出型参数,也就是由用户定义,调用完wait/waitpid之后,status会被设置成等待的子进程的退出状态!如果不关心子进程的退出状态信息,传空指针即可!
但是status不能看成一个整体去分析,而是要如下分析:
status是一个整形变量,我们只研究较低的16个比特位,16个比特位被划分成3部分,最低的7表示终止信号, 较高的8位表示退出码,中间的1个比特位是core dump(后续博客讲解)
ps: 当进程异常退出时,也就是终止信号为非0时,进程的退出码也就没有了任何意义!
获取进程的退出码和终止信号有两种方式:
1. 位操作
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //终止信号
2. 系统定义的宏常量
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //获取退出码
进程等待接口讲解
wait用法![](https://img-blog.csdnimg.cn/direct/2a80c067746a45948b0bbc2a3c713e78.png)
wait等待的是任意一个进程, 等待成功返回等待进程pid, 等待失败,返回-1
父进程睡眠了10秒,而子进程运行了5秒就退出了,所以当子进程退出后,父进程还在运行,没有回收子进程,所以子进程先处于僵尸状态,5秒之后,父进程调用wait接口,回收子进程,子进程从僵尸状态变为死亡状态(瞬时状态), 然后就查不到子进程了!
如果子进程还没有退出,那么父进程就要进行阻塞式等待(停留在调用wait接口那行代码),直到子进程退出,回收资源!
waitpid用法
waitpid等待的是特定的进程! 如果pid传-1,等待的就是任意一个进程!
位操作获取退出码与终止信号
正常退出
异常退出
程序运行时出问题
![](https://img-blog.csdnimg.cn/direct/9d6717d2dec9440390143afb7ee27eb4.png)
![](https://img-blog.csdnimg.cn/direct/09d79aed04d44ffcaa838784ac01e078.png)
![](https://img-blog.csdnimg.cn/direct/bd5ad96b71dc4ce782b99853f50884a4.png)
![](https://img-blog.csdnimg.cn/direct/b442ba99e9d74de9b32412cdb9669068.png)
![](https://img-blog.csdnimg.cn/direct/0d9f7e05701f497c999a1ae1b09bfe05.png)
等待多进程
进程是按照0,1,2···的编号创建的,但是调度顺序完全是由调度器决定的!
下图演示了用户是如何通过系统调用接口获得子进程退出的状态!
ps: 为啥不用全局变量获取子进程的退出信息呢??
进程之间具有独立性,子进程退出时写入全局变量会发生写时拷贝,因此父进程是无法读到子进程的值的,因此只能通过系统调用让操作系统帮我们获取!
基于非阻塞调用的轮询式检测
接下来就要介绍第三个参数了,第三个参数是让我们选择等待的方式, 进程等待分两种:
1. 阻塞等待, 传0
2. 非阻塞等待,传 WNOHANG, WNOHANG 意思是 wait no hang, 意思是等待时不要夯住了!
阻塞等待: 在父进程调用 wait/waitpid接口时,子进程如果不退出,wait/waipid就不返回,这就注定了父进程在等待子进程时候什么事情都做不了,只能等!
非阻塞等待: 在父进程等待子进程时如果子进程没有退出,wait/waitpid 不阻塞,也就是wait/waitpid立即返回,这就是非阻塞等待,而非阻塞注定了 wait/waitpid要重复调用(轮询的过程), 不断的查看子进程的状态!
非阻塞等待的优势就是父进程在等待子进程的过程中可以做一做自己的事情!
非阻塞等待waipid的返回值:
1. >0 等待成功
2. ==0 子进程还没有退出, waitpid返回了
3. <0 等待失败(等待失败一般是因为把要等待的进程pid传错了)
以下代码是演示基于非阻塞的的轮巡式检测以及父进程在等待期间做其他事情!
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 #define TASK_NUM 5
8
9 typedef void (*task_t)(); //函数指针类型
10
11
12 void download()
13 {
14 printf("A download task is running!\n");
15 }
16 void printlog()
17 {
18 printf("A prinlog task is running!\n");
19 }
20 void show()
21 {
22 printf("A show task is running!\n");
23 }
24 ///
25
26 void InitTask(task_t tasks[], int num)
27 {
28 for(int i = 0; i < num; i++) tasks[i] = NULL;
29 }
30
31 int AddTask(task_t tasks[], task_t t)
32 {
33 for(int i = 0; i < TASK_NUM; i++)
34 {
35 if(tasks[i] == NULL)
36 {
37 tasks[i] = t;
38 return 1;
39 }
40 }
41 return 0;
42 }
43
44 void ExecuteTask(task_t tasks[], int num)
45 {
46 for(int i = 0; i < num; i++)
47 {
48 if(tasks[i]) tasks[i]();
49 }
50 }
51
52 void Worker(int cnt)
53 {
54 printf("I am child, pid: %d, cnt: %d\n", getpid(), cnt);
55 }
56
57 int main()
58 {
59 task_t tasks[TASK_NUM];
60 InitTask(tasks, TASK_NUM);
61 AddTask(tasks, download);
62 AddTask(tasks, printlog);
63 AddTask(tasks, show);
64
65 pid_t id = fork();
66 if(0 == id)
67 {
68 //child
69 int cnt = 5;
70 while(cnt)
71 {
72 Worker(cnt);
73 sleep(2);
74 cnt--;
75 }
76 exit(0);
77 }
78
79 //father
80 int status = 0;
81 while(1) //非阻塞轮询检测
82 {
83 pid_t rid = waitpid(id, &status, WNOHANG);
84 if(rid > 0)
85 {
86 //wait sucess, child quit already
87 printf("child quit sucess, exit code: %d, exit signal:%d\n", (status>>8)&0xFF, status&0x7F);
88 break;
89 }
90 else if(0 == rid)
91 {
92 //wait sucess, but child not quit
93 //printf("child is alive, wait again, father do other things...\n");
94 ExecuteTask(tasks, TASK_NUM); //也可以在内部进行移除或者新增对应的任务!
95 }
96 else
97 {
98 //wait failed, child unknown
99 printf("wait failed!\n");
100 break;
101 }
102 sleep(1);
103 }
104 return 0;
105 }
二、进程程序替换
我们目前所创建的所有子进程执行的代码都是父进程代码的一部分!如果想让子进程执行新的程序,执行新的代码和访问全新的数据,不再和父进程有关系,这就要依靠程序替换了!
excel接口
1.execl是一个用于程序替换的接口,可执行程序本质就是磁盘上的二进制文件
2.所有的程序替换接口都是以exec开头的,exec是execute的简写,execl中的 l 代表传参方式是list列表方式
3. 第一个参数传的是文件的路径+文件名,保证找到文件!
后面的参数含义是如何执行这个可执行程序,命令行下如何运行可执行程序,就如何传参!
最后一个参数必须是NULL, 表示传参完成
程序替换演示
单进程程序替换
但是我们发现excel后续的printf语句没有执行,在下面讲解程序替换原理我们再详谈!
多进程程序替换
程序替换原理
单进程程序替换
哪一个进程调用execl等程序替换的系统调用,系统就会将要替换的磁盘上文件的代码和数据直接覆盖到子进程的代码段和数据段!cpu再调度运行时,就执行的是新程序的代码和数据了!
程序替换过程并没有创建新进程,进程的pid不会改变!
多进程程序替换
多进程程序替换相比单进程程序替换无非就是多了写时拷贝,和之前写时拷贝不同的是,此处不只要写时拷贝数据,还要写时拷贝代码,因为子进程发生程序替换,代码和数据都会被覆盖掉,所以也要写时拷贝代码,否则父子进程独立性就无法保证了!
Q:程序替换之后,子进程如何知道从新的程序的最开始执行,如何知道最开始在哪?
A: 编译形成可执行程序,可执行程序的代码和数据是有一定的格式的(ELF格式), 在可执行程序的开始有一个字段entry, 里面保存了可执行程序的入口地址!发生程序替换时,系统调用接口会读取entry, 并且将entry内容填充到当前进程的eip寄存器中(之前博客已经讲过进程切换,eip寄存器只有1个,但是eip的内容可以有多份,详见博客) 进程优先级与环境变量-CSDN博客
excel之后的printf没有执行的原因
在调用execl的时候发生了程序替换,所以最后运行我们的程序把系统指令调了起来,但是会发现,并没有输出execl后续的printf代码! 是因为发生了程序替换,调用完execl接口后,代码和数据都被覆盖了,所以后续的printf代码都被覆盖了, 同时程序计数器内容也被改变了!所以程序替换一旦成功,后续代码就没有机会再执行了,只有当程序替换失败了,才会有返回值,才会执行后续代码!
小细节:
excel第一个参数也就是路径+文件名是对的,但是程序运行方法传递错了,但是程序替换还是成功的,这是因为系统接口代码的健壮性(鲁棒性)比较强,他根据第一个参数已经能够识别是ls了,所以最终也能程序替换成功,但是建议我们在写的时候按标准来!!!
程序替换函数总结
这些都是程序替换的函数,都封装了execve这个系统调用!! 之所以设计这么多接口是为了满足各种调用的场景!
任何程序替换函数都要解决两个问题:
a.必须先站到这个可执行程序
b.必须告诉exec*,怎么执行
file:文件名,p: PATH, 表示execlp会自动的去环境变量PATH中根据file去寻找可执行程序!
v表示vector, 是数组的意思,除了可变参数接收选项之外,argv[ ]指针数组也可以存放选项!
file:文件名,p: PATH, 表示execlp会自动的去环境变量PATH中根据file去寻找可执行程序!
传递系统环境变量
传递自定义环境变量(默认覆盖式传递)
上述演示的都是程序替换系统指令,下面演示一下程序替换自己的可执行程序
替换C++程序
替换脚本语言
替换python
指令运行起来是进程,我们自己写的可执行程序运行起来也是进程,因为exec*是进程程序替换的接口,所以都能替换,系统大于一切!
程序如何加载到内存中? 也是靠程序替换接口来完成的!
两个小结论
1.程序替换时,子进程对应的环境变量是可以直接从父进程来的
在bash中导入自定义的环境变量,最后发现子进程(fork创建孙子进程)和孙子进程(发生程序替换)都有自定义的环境变量
而在myprocess.c中导入自定义环境变量,父进程bash看不到环境变量,但是fork创建的子进程可以继承!
2.环境变量被子进程继承下去是一种默认行为,不受程序替换的影响, 因为程序替换,只替换新进程的代码和数据,环境变量不会被替换!
三、实现命令行解释器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define NUM 1024 //命令行提示符个数限制
#define SIZE 64 //命令行提示符个数限制
#define SEP " " //strtok分隔符
//#define Debug 1 //对代码实现动态裁剪
char cwd[1024];
char enval[1024]; //for test
int lastcode = 0; //最近一个进程的退出码
const char* getUsername() //获取用户名
{
const char* name = getenv("USER");
if(name) return name;
else return "none";
}
const char* getHostname() //获取主机名
{
const char* hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char* getCwd() //获取当前路径
{
const char* cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
char* homepath() //获取家目录
{
char* home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
int getUserCommand(char* command, int num)
{
//输出命令行提示符
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd()); //获取环境变量
//char *fgets(char *s, int size, FILE *stream);
char* r = fgets(command, num, stdin); //从键盘获取用户指令, 用户最终一定会输入回车符(\n)
if(r == NULL) return -1;
//这里不会越界,因为当用户至少都要输入\n
command[strlen(command)-1] = '\0'; //去掉用户最后输入的\n
return strlen(command);
}
void commandSplit(char* in, char* out[])
{
int argc = 0;
//char *strtok(char *str, const char *delim);
out[argc++] = strtok(in, SEP);
while(out[argc++] = strtok(NULL, SEP));
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
void cd(const char* path)
{
//int chdir(const char *path); 哪个进程调用chdir, 哪个进程的当前路径就会被修改!
chdir(path);
//char cwd[1024]; //不能写在此处,因为cd调用完后空间就释放了,环境变量就不是永久有效的了!
char tmp[1024];
//char *getcwd(char *buf, size_t size);
getcwd(tmp, sizeof(tmp)); //获取当前进程的绝对路径!
//int sprintf(char *str, const char *format, ...); 将本来应该打印到屏幕上的字符串格式化写入到str指向的空间中
sprintf(cwd, "PWD=%s", tmp); //将tmp以"PWD=%s"的格式写入到cwd中
putenv(cwd); //将cwd环境变量导入到当前进程的环境变量表中
}
//内建命令就是bash自己执行的类似于自己内部的一个函数!
//1->yes, 0->no,-1->err
int doBulidin(char* argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char* path = NULL;
if(argv[1] == NULL) path = homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval, argv[1]);//必须定义一个全局enval数组,否则导入环境变量之后,执行其他命令之后,usercommand就会重新覆盖写入,导入的环境变量就没了
putenv(enval);
return 1;
}
else if(strcmp(argv[0], "echo") == 0)
{
if(argv[1] == NULL)
{
printf("\n");
return 1;
}
if((*argv[1]) == '$' && strlen(argv[1]) > 1)
{
char* val= argv[1] + 1; //echo $PATH argv[1]是$, argv[1]+1是PATH的首地址
if(strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else
{
const char* enval = getenv(val);
if(enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else
{
printf("%s\n", argv[1]);
return 1;
}
}
//还有其他内建命令往后加分支语句即可!!
return 0;
}
int execute(char* argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0)
{
//child
//exec command
//int execvp(const char *file, char *const argv[]);
execvp(argv[0], argv);
exit(1);
}
else
{
//father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) {
lastcode = WEXITSTATUS(status);
};
}
return 0;
}
int main()
{
while(1)
{
//1. 打印提示符&&获取命令行输入
char usercommand[NUM];
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue;
//printf("%s", usercommand);
//2.对字符串做切割
char* argv[SIZE];
commandSplit(usercommand, argv);
//3.检查命令是否内建命令
n = doBulidin(argv);
if(n) continue; //内建命令由父进程直接执行,不用创建子进程
//4.创建子进程执行对应的命令
execute(argv);
}
return 0;
}