进程控制详解

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

以上是关于进程控制详解的主要内容,如果未能解决你的问题,请参考以下文章

Linux——进程控制(创建终止等待程序替换)

Linux——进程控制(创建终止等待程序替换)

Linux——进程控制(创建终止等待程序替换)

Python并发编程—进程间通信

Linux进程控制--进程的等待与替换

Linux进程控制--进程的等待与替换