进程控制详解
Posted 燕麦冲冲冲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程控制详解相关的知识,希望对你有一定的参考价值。
进程控制
进程创建
1⃣️命令行启动命令(程序、指令等)。
2⃣️通过自身程序fork出子进程。
进程调用fork,内核分配新的内存块和内核数据结构给子进程,将父进程部分数据结构拷贝至子进程,添加子进程到系统进程列表中,fork返回,调度器开始调度。
创建子进程,本质是系统多了一个进程,本质是多了一套进程相关的数据结构。
(1)为什么fork有两个返回值?返回两次?
(2)一个变量里面,怎么会有两个不同的值?从而让父子进入不同的业务逻辑
fork执行完业务逻辑时返回时,已经创建好子进程了,又需要一个变量来接收返回值,把返回值写入变量一定发生写时拷贝。一个变量名内容不同,本质是父子页表映射数据到了不同的内存区域。
为什么要有写时拷贝?
1、保证父子进程的独立性。
2、所有的数据父子都不一定会写入,那么再拷贝一份就是浪费内存和系统资源。
3、fork时,创建数据结构,如果还拷贝数据,则会降低内存。
4、fork向申请的资源越少,失败的概率越低。(失败的原因:进程太多或超出用户进程限制)
进程终止
1⃣️为何main结束总是return 0?
main函数返回值代表进程退出,结果是否运行正确。0代表成功。
给系统看进程的退出码,确认进程执行结果是否正确。
给工程师看
echo $? #查看最近一次执行程序的退出码
2⃣️进程退出的情况分类
(1)代码跑完,结果正确。退出码 0
(2)代码跑完,结果不正确。逻辑出错但未使程序崩溃。退出码!0
(3)代码未跑完,程序崩溃。退出码无意义。
退出码可以人为的定义,也可以使用系统的错误码list
当程序运行失败的时候,最关心:为什么失败?
1 #include<stdio.h>
2 #include<string.h>
3
4 int main()
5
6 for(int i = 0; i < 100; i++)
7
8 printf("%s\\n", strerror(i)); //c提供的错误码列表
9
10 return 0;
11
3⃣️操作
(1)main函数return,非main函数的return不是终止进程,而是结束函数。
(2)任何函数exit,都表示直接终止进程。在退出的时候,会进行后续资源处理,包括刷新缓冲区。而_exit()恰恰不会。
4⃣️站在操作系统的角度,如何理解进程终止?
核心思想:归还资源
(1)释放为管理进程所维护的数据结构对象。
(2)释放程序代码和数据所占用的空间。
(3)取消曾经该进程的链接关系。(如父子、兄弟进程间的关系)
关于“释放“:不是真的把数据结构对象销毁,而是设置为不用的状态,然后保存起来,如果不用的对象多了,就有一个”数据结构的池“。“释放”的本质是将结构体对象链入操作系统的“数据结构池”。申请的时候就从池中取,不够才重新开辟。
类似于内存池:
进程等待
1⃣️等待的必要性⌛️(为什么要等待?)
(1)回收僵尸♻️,解决内存泄漏
(2)需要获取子进程的运行结束状态(并非结果,且非必需)
(3)父进程尽量晚于子进程退出,可以规范化进行资源回收♻️
2⃣️wait()
pid_t wait(int* status); //输出型参数,获取子进程退出状态,不关心就传NULL //返回值为子进程pid,失败-1
等待任意一个子进程,当子进程退出,wait就可以返回了。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8
9 pid_t id = fork();
10 if(id < 0)
11
12 perror("fork");
13 return 1;
14
15 else if(id == 0)
16
17 //child
18 int count = 5;
19 while(count)
20
21 printf("%d: child is running, ppid: %d, pid: %d\\n", count--, getppid(), getpid());
22 sleep(2);
23
24 printf("child quit!\\n");
25 exit(0);
26
27 else
28 //father
29 printf("father is waiting\\n");
30 pid_t ret = wait(NULL);
31 printf("father is wait done, ret: %d\\n", ret);
32
33 return 0;
34
让父进程一开始就休眠一段时间,且子进程结束时父进程仍在休眠。之后父进程再调用wait接口让OS回收子进程,再休眠一段时间,最后结束父进程。
while :; do echo “________”; ps ajx | head -1 && ps ajx | grep proc | grep -v grep; echo “________”; sleep 1; done #监视脚本
一般而言,我们需要fork之后,让父进程进行等待⌛️。
3⃣️waitpid()
pid_t waitpid(pid_t pid, int* status, int options);
参数:
(1)pid
pid=-1,等待任意进程,与wait()等价。
pid>0,等待其进程ID与pid相等的子进程,即等待指定的进程。
父进程等待子进程,要知道子进程的pid,故可利用fork返回的子进程pid。
(2)status
输出型参数
要自己定义一个整型变量,传入其地址,从而获取退出码。
32bit位,但只看低16位。
事实:一般进程提前终止,本质是该进程收到了OS发送的信号。
正常终止:9~16位代表子进程的退出码。信号为0.
被信号所杀:1~7位代表终止信号📶。第8位是core dump标志。
(3)options
设置为0,则为阻塞式等待。(干等)
设置为WNOHANG,则为非阻塞等待。(边等边做别的事情,并多次检测状态,即为非阻塞轮询方案)
利用waitpid()替代wait()
pid_t id = fork();
//子进程完成业务后退出
waitpid(id, NULL, 0);
失败:(1)子进程状态未达到预期(2)真的失败了
为什么不能定义一个全局变量来替代退出码?写时拷贝。
子进程已经结束了,waitpid拿到的status值是从哪里拿到的?子进程Z,有资源未释放,如task_struct。
阻塞式使用示例:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8
9 pid_t id = fork();
10 if(id < 0)
11
12 perror("fork");
13
14 else if(id == 0)
15
16 //child
17 int count = 5;
18 while(count)
19
20 printf("%d: child is running! pid: %d ppid: %d\\n", count--, getpid(), getppid());
21 sleep(1);
22
23 printf("child quit!\\n");
24 exit(123);
25
26 //father
27 int status = 0;
28 pid_t ret = waitpid(id, &status, 0);
29 if(ret > 0)
30
31 printf("wait success!\\n");
32 if((status & 0x7F) == 0)
33
34 printf("process quit normal!\\n");
35 printf("exit code: %d\\n", (status>>8)&0xFF);
36
37 else
38 printf("process quit error!\\n");
39 printf("sig: %d\\n", status & 0x7F);
40
41
42 return 0;
43
系统提供了宏来判断退出码和退出状态。
WIFEXITED(status) 计算信号,进程退出正常返回true,反之false
WEXITSTATUS(status) 返回进程的退出码
非阻塞式使用示例:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6
7 int main()
8
9 pid_t id = fork();
10 if(id == 0)
11
12 //child
13 int count = 5;
14 while(count)
15
16 printf("%d: child is running\\n", count--);
17 sleep(1);
18
19 printf("child exit\\n");
20 exit(0);
21
22 //parent
23 sleep(3);
24 printf("father is runing\\n");
25 while(1)
26
27 pid_t ret = waitpid(id, NULL, WNOHANG);
28 if(ret < 0)
29
30 printf("wait failed\\n");
31 break;
32
33 else if(ret == 0)
34
35 printf("wait next and father do other thing\\n");
36
37 else
38 printf("wait success\\n");
39 break;
40
41 sleep(1);
42
43 printf("father exit\\n");
44 return 0;
45
如何理解阻塞等待?
父进程在等,子进程在运行,子进程在运行队列,父进程在等待队列,直至子进程结束后,唤醒进程,由等待队列切换到运行队列,并修改状态。由于wait等函数是系统接口,所以OS知道该将谁唤醒。
进程程序替换
1⃣️为什么要进程替换?
创建子进程的目的:1、执行父进程的部分代码。2、执行其他程序的代码。
要实现第二点必须要程序替换。
2⃣️什么是进程替换?
子进程要执行其他程序的代码,而代码区是只读的,因此需要为子进程额外开辟一块空间用于存放代码。
在进行程序替换的时候,没有创建新的进程。
与内核相关的数据结构不变,且子进程的pid不变。
3⃣️如何进程替换?
(1)execl
int execl(const char* path, const char* arg, ···);
path为执行程序的路径(包括程序名),arg为执行的程序名,···为可变参数列表(命令行怎么执行,传入什么选项,就可以在这按顺序填写,以NULL结束)
使用示例:
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5
6 printf("my process begin\\n");
7 execl("/usr/bin/ls", "ls", "-i", "-a", "-l", NULL);
8 printf("my process end\\n");
9 return 0;
10
最后一行代码并不会执行,因为程序已经被替换成ls的代码了。
并不用考虑函数的返回值,要是返回那么调用就失败了。
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[]);
execl使用示例
execl(“/user/bin/ls”, “ls”, “-a”, “-l”, NULL);
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/wait.h>
5 #include<sys/types.h>
6
7 int main()
8
9 pid_t id = fork();
10 if(id == 0)
11
12 //child
13 printf("I am child. pid: %d ppid: %d\\n", getpid(), getppid());
14 execl("/usr/bin/ls", "ls", "-l", NULL);
15 exit(1);
16
17 //father
18 int count = 3;
19 while(count--)
20
21 printf("I am father. pid: %d\\n", getpid());
22 sleep(1);
23
24 int status = 0;
25 pid_t ret = waitpid(id, &status, 0);
26 if(ret > 0)
27
28 printf("wait success! sig: %d code: %d\\n", status&0x7F, (status>>8)&0xFF);
29
30 else
31 printf("wait error\\n");
32
33 return 0;
34
子进程进行代码替换时,是不会影响父进程的,通过写时拷贝,让父子进程指向不同的代码区。
运行结果:
execv使用示例
execlp使用示例
有p自动搜索环境变量PATH
execlp(“top”, “top”, NULL);
价值在于不必知道命令在哪。
只有系统的命令才可以找到,或者将自己的命令导入PATH。
两个top并不冲突,前者表示执行谁,后者代表命令行上的执行方式。
调用自己写的程序
可应用于跨语言的程序耦合
示例中采用的是:编写两个程序,在一号程序exec_cmd的子进程中利用系统接口execl替换二号程序mycmd。
首先需要更改Makefile
1 .PHONY:all
2 all: exec_cmd mycmd
3
4 exec_cmd:exec_cmd.c
5 gcc -o $@ $^ -std=c99
6 mycmd:mycmd.c
7 gcc -o $@ $^ -std=c99
8
9 .PHONY:clean
10 clean:
11 rm -f exec_cmd mycmd
execl(“./mycmd”, “mycmd”, “NULL”);
execle的使用示例
利用一号程序创建本地环境变量,再::利用execle传入默认的或者自定义的环境变量::给二号程序读取。
⚠️一号程序重点是在14~18行。
7 int main()
8
9 pid_t id = fork();
10 if(id == 0)
11
12 //child
13 printf("I am child. pid: %d ppid: %d\\n", getpid(), getppid());
14 char* const my_env[] =
W> 15 "MYENV=helloworld!",
16 NULL
17 ;
18 execle("./mycmd", "mycmd", NULL, my_env);
19 exit(1);
20
21 //father
22 int status = 0;
23 pid_t ret = waitpid(id, &status, 0);
24 if(ret > 0)
25
26 printf("wait success! sig: %d code: %d\\n", status&0x7F, (status>>8)&0xFF);
27
28 else
29 printf("wait error\\n");
30
31 return 0;
32
4 int main()
5
6 printf("I am mycmd!\\n");
7 printf("getenv->MYENV: %s\\n", getenv("MYENV"));
8 return 0;
9
execve的使用方式类似。
还可以通过main函数的第三个参数env导入默认全局环境变量。
所有的接口的底层实现都是调用了execve
编写一个简单的shell
1 #include<stdio.h>
2 #include<string.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8 #define NUM 128
9 #define SIZE 32
10
11 char command_line[NUM];
12 char* command_parse[SIZE];
13
14 int main()
15
16 wh以上是关于进程控制详解的主要内容,如果未能解决你的问题,请参考以下文章