【Linux】开始掌握进程控制吧!

简介: 通过对进程的学习,我们对虚拟地址,页表,物理地址有了大概认识。我们平时使用的地址都是虚拟地址,通过页表可以访问物理地址(统一的视角进行控制,保证数据安全)。也认识到写时拷贝。也认识O(1)调度算法,通过两个队列(活跃队列,过期队列)完成进程的分时控制,通过优先级来放入不同位置,以时间复杂度O(1)快速寻找进程。

送给大家一句话:

我并不期待人生可以一直过得很顺利,但我希望碰到人生难关的时候,自己可以是它的对手。—— 加缪

开始学习进程控制

1 前言

通过对进程的学习,我们对虚拟地址,页表,物理地址有了大概认识。我们平时使用的地址都是虚拟地址,通过页表可以访问物理地址(统一的视角进行控制,保证数据安全)。也认识到写时拷贝。

也认识O(1)调度算法,通过两个队列(活跃队列,过期队列)完成进程的分时控制,通过优先级来放入不同位置,以时间复杂度O(1)快速寻找进程。

2 进程创建

2.1 fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。fork函数具有两个返回值,通过对返回值的判断(if else )可以进行父进程和子进程的不同书写。

注意:进程调用fork,当控制转移到内核中的fork代码后,内核做以下工作:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程(进程:内核的相关数据管理的数据结构(task_struct + mm_struct + 页表)+ 代码与数据)
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

这里是为了保证父进程和子进程的独立性。

2.2 fork函数返回值

  • 子进程返回0
  • 父进程返回的是子进程的pid

那为什么父进程返回子进程PID ,给子进程返回0呢???

很好理解:就像现实生活中,父母有了孩子,会给他或她起一个名字,父母知道了名字,就可以很好管理孩子。父进程与子进程同理,父进程为了便于管理子进程,所以fork函数会返回对应子进程的pid。

2.3 写时拷贝

通过图解可以很好理解写时拷贝。

在创建子进程的时候,子进程的页表映射与父进程一致(默认继承的),一旦子进程要进行修改数据,为了保证进程的独立性(保证父进程安全运行),不得不开辟一个新空间,并修改子进程页表的映射(虚拟地址不变!)。

2.4 fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

一般使用if else 分开书写,也可以通过系统调用打开新的进程。

2.5 fork调用失败的原因

  1. 系统中有太多的进程(数据空间是有限的)
  2. 实际用户的进程数超过了限制(必须是有限的)

2 进程终止

2.1 终止是在做什么

进程终止会进行:

  1. 释放曾经的代码和数据所占据的空间
  2. 释放内核数据结构

但是task_struct会延期处理,因为终止的进程处于Z状态(僵尸进程)

2.2 进程终止的情况

我们的main函数常常会有一个返回值 0 ,那为什么要返回零呢???

  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 #include<string.h>  
  4   
  5 
  6 int main()
  7 {
  8    
  9   printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
 10   sleep(2);
 11    
 12   return 100;                                                                                                                                                                 
 13 }  

来看我们返回100(为了效果明显)时

echo 打印的是bash 的环境变量,这个100 就是刚才进程返回到父进程(bash)的退出码(环境变量 表示最近一个进程的退出码),一般0表示正常运行,非零表示有问题。

父进程关心子进程的信息,想要知道子进程是否正常运行。不同的退出码表示不一样的失败原因,我们来获取一下:

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<string.h>
    4 
    5 int g_val = 10000;
    6 
    7 int main()
    8 {
    9   
   10   for(int errcode = 0 ;errcode <=255; errcode++)
   11   {
   12     printf("%d: %s\n",errcode,strerror(errcode));
   13                                                                                                                                                                             
   14   }                                                                                                                                                
   15   printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());                                                                               
   16   sleep(1);                                                                                                                                        
   17                                                                                                                                                    
   18   return 0;                                                                                                                                        
   19 }     

这样就可以获取所有的退出码和对应的退出信息了:

通过退出码就能获取对应退出信息,来告知用户为什么退出了。其底层实现也好理解,通过数字与字符串的一一对应来做到。

常见进程退出场景:

  1. 代码运行完毕,结果正确(正常结束进程)
  2. 代码运行完毕,结果不正确
  3. 代码异常终止,出现异常提前退出

就像:VS编程运行的时候,如果崩溃了 — 操作系统发现你的进程做了不应该做的事情,OS就杀死进程!!!

一旦出现异常,退出码就没有意义了!!! 为什么出异常才是最重要的!!!

那为什么会出现异常呢??? 原因是:进程出现异常的本质是进程收到来自OS发给进程的信号!(kill -9 就是一个信号)

注意:

  • 先确认是否异常
  • 不是异常就是代码正常跑完,看退出码即可。
  • 可以通过退出信号来判断出现了什么异常

2.3 如何终止

正常终止(可以通过 echo $? 查看进程退出码)

  1. 从main函数return,表示进程终止
  2. 调用exit
  3. _exit
    异常退出
    ctrl + c,信号终止

来看手册中如何描述的:

调用exit 函数试试:

  1 #include<stdio.h>  
  2 #include<unistd.h>  
  3 #include<string.h>  
  4 #include<stdlib.h>
  5 int g_val = 10000;  
  6   
  7 int main()  
  8 {  
  9   
 10   printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
 11   sleep(1);
 12   exit(123);                                                                                                                                                                  
 13                                                                                                                                     
 14 }                                                                                                                                                
 15 

运行后是这样的效果:

exit比return 直接,调用一次就可以完全退出!

_exit 是一个系统调用(system call),参数与exit一致,使用与exit几乎一模一样。

**但是exit会冲刷缓冲区,而_exit 不会,**因为缓冲区在系统调用之上 ,而exit 是一个C语言库函数。图解:

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

3 进程等待

3.1 进程等待必要性

  • 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

也就是说,任何进程在退出时都要被父进程进行等待,不然子进程处于僵尸进程就会造成内存泄漏!!!

3.2 进程等待的方法

  1. wait方法
  2. waitpid方法
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回

wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

我们测试一下:

  1 #include<stdio.h>                                          
  2 #include<unistd.h>                                         
  3 #include<string.h>                                         
  4 #include<stdlib.h>                                         
  5 #include<sys/types.h>                                      
  6 #include<sys/wait.h>                                       
  7                                                            
  8 void Childrun()                                            
  9 {                                                          
 10   int cnt = 5;                                             
 11   while(cnt)                                               
 12   {                                                        
 13     printf("I am child ,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
 14     sleep(1);                                              
 15     cnt--;                                                 
 16   }                                                        
 17 }                                                          
 18                                                            
 19 int main()                                                 
 20 {                                                          
 21                                                            
 22   printf("I am father ,pid:%d,ppid:%d\n",getpid(),getppid());
 23                                                            
 24   pid_t id = fork();                                       
 25   if(id == 0)                                              
 26   {                                                        
 27     //child                                                
 28     Childrun();                                            
 29     printf("child quit...\n");                             
 30     exit(0);                                               
 31   }                                                        
 32   //father                                                 
 33   sleep(10);                                               
 34   pid_t rid = wait(NULL);                                  
 35   if(rid > 0)                                              
 36   {                                                        
 37     printf("wait success,rid:%d\n",rid);                   
 38   }                                                        
 39   sleep(3);                                                                        
 40   return 0;                                                  
 41 }   

这个程序会在子进程运行结束前等待子进程,并且会存在一段时间的窗口期,此时子进程处于僵尸进程:

在这个父进程等待的过程中,父进程一直在等待子进程的退出,处于阻塞等待状态。父进程本质是等待某种软件条件就绪,那么如何理解阻塞等待子进程呢???

就是把自己列入等待队列,把状态列入不运行状态,等待子进程(类似scanf 的阻塞)。

waitpid方法

#include<sys/types.h>
#include<sys/wait.h>
pid_ 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:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

pid_t rid = waitpid(-1,NULL,0); 与刚才的wait等价!!!如果id不为-1,是一个对应的id,那么就会等待指定进程!!!,如果id错误(不存在该进程),就会发生等待错误!!!

status 是一个输出型参数,需要我们传入一个指针来获取。来测试一下(子进程退出码设置为1 )

  1 #include<stdio.h>                    
  2 #include<unistd.h>                   
  3 #include<string.h>                   
  4 #include<stdlib.h>                   
  5 #include<sys/types.h>                
  6 #include<sys/wait.h>                 
  7                                      
  8 void Childrun()                      
  9 {                                    
 10   int cnt = 5;                       
 11   while(cnt)                         
 12   {                                  
 13     printf("I am child ,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
 14     sleep(1);                        
 15     cnt--;                           
 16   }                                  
 17 }                                    
 18                                      
 19 int main()                           
 20 {                                    
 21                                      
 22   printf("I am father ,pid:%d,ppid:%d\n",getpid(),getppid());
 23                                      
 24   pid_t id = fork();                 
 25   if(id == 0)                        
 26   {                                  
 27     //child                          
 28     Childrun();                      
 29     printf("child quit..\n");       
 30     exit(1); //退出码设置为1  
 31   }                                  
 32   //father                           
 33   sleep(10);                         
 34   int status = 0 ;                   
 35   pid_t rid = waitpid(id , &status , 0);
 36   if(rid > 0)                        
 37   {                                  
 38     printf("wait success,rid:%d\n",rid);
 39   }                                  
 40   printf("father quit...,status:%d\n",status);                                     
 41   sleep(3);                                                                  
 42   return 0;                                                                  
 43 } 

这就成功获取了status!

我们需要的是 退出码 和 退出信号,那么我们如何通过status获取这两个数据呢???

也就通过位运算就可以成功获取了。

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/direct/2650b18879684c50981074b8e6bbdf7d.png)这样就可以了:

通过两个信息就可以判断进程是否正常运行,如果异常,也能知道异常原因了。

当然,如果使用位运算就有点那啥了,我们可以使用宏:

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

对于第三个参数,就可以让父进程在等待的刚才中区做其他事情。也就是进行非阻塞等待:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

  1. 阻塞等待就类似张三给李四打电话帮忙,李四正在忙,告诉张三等一会,然后张三这个电话就不挂了,等着李四完成工作,张三也不做其他事情。
  2. 非阻塞等待类似张三给李四打电话帮忙,李四正在忙,告诉张三等一会,然后张三说李四忙完打回来,张三就先去做其他事情。

下面写入了一段非阻塞轮询等待的代码,这样就能保证父进程在等待的过程中,可以去做其他事情!

 34   while(1)                                                                                                                                 
 35   {                                                                                                                                        
 36     int status = 0 ;                                                                                                                       
 37     pid_t rid = waitpid(id , &status , 0);                                                                                                 
 38     if(rid == 0)                                                                                                                           
 39     {                                                                                                                                      
 40       sleep(1);                                                                                                                            
 41       printf("child is running ,father check next time!\n");                                                                               
 42     }                                                                                                                                      
 43     else if(rid > 0)                                                                                                                        
 44     {                                                                                                                                      
 45       if(WIFEXITED(status))                                                                                                                
 46       {                                                                                                                                    
 47         printf("child quit success,child exit code:%d\n",WEXITSTATUS(status));                                                             
 48       }                                                                                                                                    
 49       else                                                                                                                                 
 50       {                                                                                                                                    
 51         printf("child quit unnormal!\n");                                                                                                  
 52       }                                                                                                                                    
 53       break;                                                                                                                               
 54     }                                                                                                                                      
 55     else                                                                                                                                   
 56     {                                                                                                                                      
 57       printf("waitpid failed!\n");                                                                                                         
 58       break;                                                                                                                                                                  
 59     }                                                            
 60   }

来看运行效果(父进程一直在查):

这样就完成了。

4 总结

  1. 等待很容易理解,等待是必须进行的,回收子进程资源,获取子进程退出信息
  2. 进程等待常用waitpid,并常用非阻塞等待

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

相关文章
|
5天前
|
消息中间件 算法 Linux
【Linux】详解如何利用共享内存实现进程间通信
【Linux】详解如何利用共享内存实现进程间通信
|
5天前
|
Linux
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
【Linux】命名管道的创建方法&&基于命名管道的两个进程通信的实现
|
5天前
|
Linux
【Linux】匿名管道实现简单进程池
【Linux】匿名管道实现简单进程池
|
5天前
|
Linux
【Linux】进程通信之匿名管道通信
【Linux】进程通信之匿名管道通信
|
5天前
|
存储 Linux Shell
Linux:进程等待 & 进程替换
Linux:进程等待 & 进程替换
30 9
|
5天前
|
存储 Linux C语言
Linux:进程创建 & 进程终止
Linux:进程创建 & 进程终止
28 6
|
5天前
|
Linux 数据库
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
linux守护进程介绍 | Linux的热拔插UDEV机制
|
5天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
5天前
|
Linux 调度 C语言