【Linux】进程控制

🌈个人主页: 秦jh__https://blog.csdn.net/qinjh_?spm=1010.2135.3001.5343
🔥 系列专栏: https://blog.csdn.net/qinjh_/category_12625432.html

目录

fork函数初识

[fork 函数返回值](#fork 函数返回值)

写时拷贝

fork调用失败的原因

进程终止

进程退出场景

进程常见退出方法

进程等待

进程等待必要性

进程等待的方法

wait方法

waitpid方法

获取子进程status

非阻塞等待


前言

💬 hello! 各位铁子们大家好哇。

今日更新了Linux的进程控制的内容

🎉 欢迎大家关注🔍点赞👍收藏⭐️留言📝

fork函数初识

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

cpp 复制代码
#include <unistd.h>
pid_t fork(void);
//返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中 fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程。

fork 函数返回值

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

为什么父进程返回的是子进程的pid?

为了让父进程方便对子进程进行标识,进而进行管理。

写时拷贝

通常,父子代码共享,父子不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。 (进程的独立性)

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

进程终止

进程终止做的事:

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

内核数据结构中,PCB会被延期处理,因为有一种状态是僵尸状态。

cpp 复制代码
 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6   printf("I am process, pid:%d, ppid:%d\n",getpid(),getppid());
  7   sleep(2);
  8   return 100;                                                                                                                                            
  9 }

任何命令行启动的进程,它的父进程都是bash,所以ppid都一样。

echo是内建命令,打印的都是bash内部的变量数据。**?**是一个变量名。

**echo $?**表示的是父进程获取到的,最近一个子进程退出的退出码。

main函数的返回值叫做进程的退出码。

退出码:

  • 为0,标识成功
  • 不为0,表示失败

第一个echo ?返回./myprocess 的退出码,第二个echo ?返回上一个echo $?的退出码

虽然echo $?没有创建子进程,但它是由父进程执行的,所以他也会影响?的值。

cpp 复制代码
 #include<stdio.h>  
  2 #include<unistd.h>  
  3 #include<string.h>  
  4   
  5 int main()  
  6 {  
  7   for(int errcode=0;errcode<=255;errcode++)  
  8   {  
  9     printf("%d:%s\n",errcode,strerror(errcode));                                                                                                         
 10   }                                                                                                                       
 11   printf("I am process, pid:%d, ppid:%d\n",getpid(),getppid());                                                           
 12   sleep(2);                                                                                                               
 13   return 0;                                                                                                               
 14 }   

退出码不为0表示失败。不同的非0值,一方面表示失败,另一方面表示失败的原因。

strerror函数会将错误码转成对应的错误描述,如下图;

父进程为什么要得到子进程的退出码呢?

因为要知道子进程的退出情况。(成功还是失败,失败的原因是什么),然后展现给用户看。

退出码可以使用系统默认的,也可以自定义。

进程退出场景

进程终止的3中情况:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码异常终止

代码跑完,结果不正确的原因可以通过退出码确定。

一旦出现异常,退出码就没有意义了。

进程出异常,本质是因为进程收到了OS发给进程的信号。

cpp 复制代码
 int main()  
  6 {  
  7   int *p=NULL;  
  8   while(1)
  9   {
 10     printf("I am a process,pid:%d\n",getpid());
 11     sleep(1);
 12     *p=100;//野指针                                                                                                                                      
 13   }                                                                                      
 19   sleep(2);                                                                                                                    
 20   return 0;                                                                                                      
 21 }         

当外面运行上面代码后,会报段错误。 OS就会提前终止进程。

我们把代码里的野指针注释掉,此时代码正常运行,一直循环。此时我们给该进程发11号信号,该进程即使没有错误,收到信号后,也会进行对应的报错。 所以说进程出异常,本质是因为进程收到了OS发给进程的信号。
所以如果进程异常了,我们可以通过退出信号,就可以判断进程为什么异常了,此时的退出码是无意义的。

在用户层面上,要确定进程是什么情况:

  1. 先确认是否异常
  2. 如果不是异常,就一定是代码跑完了,看退出码即可。

衡量一个进程退出,只需要两个数字:退出码和退出信号。

进程的PCB里面有退出信号和退出码,当进程退出时,会释放代码和数据,但是PCB会保存一段时间,该进程变成Z(僵尸)状态。父进程就可以从子进程的PCB中拿到退出信息。

进程常见退出方法

正常终止:

  1. main函数return,表示进程终止(非main函数的return,都只是表示函数结束)
  2. 调用exit函数 注意:在代码的任意位置调用exit,都表示进程退出
  3. _exit (系统调用)

下面是exit的使用举例:


_exit和exit在使用上没什么区别,只有一个细微的差别,如下例子:

上图是带\n的。结果打印并且换行了。

上面是不带\n的。结果打印了但没换行。

上面是不带\n的_exit的使用。结果什么也没打印。

结论:exit会在进程退出的时候,冲刷缓冲区,_exit不会。

exit在底层是调用_exit的。 由上面的结论可得,缓冲区在_exit之上,不然_exit也会冲刷缓冲区。

进程等待

进程等待必要性

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

进程等待的方法

wait方法

cpp 复制代码
  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 process, pid: %d, ppid:%d, cnt: %d\n", getpid(), get    ppid(), cnt);
 14         sleep(1);
 15         cnt--;
 16     }
 17 }
 18 
 19 int main()
 20 {
 21     printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
 22 
 23     pid_t id = fork();
 24     if(id == 0)
 25     {
 26         // child
 27         ChildRun();
 28         printf("child quit ...\n");
 29         exit(0);
 30     }
 31     // fahter
 32     pid_t rid = wait(NULL);
 33     if(rid > 0)
 34     {
 35         printf("wait success, rid: %d\n", rid);
 36     }
 37 }

作用:等待任何一个子进程退出

返回值:等待成功时,返回子进程的pid。失败返回-1。

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

运行上面的代码,结果如下图:

上面代码if后面不需要else就表示是父进程的代码了。因为if里面即子进程里面用exit退出了,所以后面的都是父进程的。

下面做出修改:

运行结果:

修改后的代码先让父进程休眠十秒。子进程运行五秒后退出,此时由于父进程还在休眠无法回收,所以子进程就变成Z状态,再过五秒后,子进程就被父进程回收了。

如果我们把sleep(10)注释掉,此时父进程开始就马上进入等待,等待期间父进程不会被调度。如果子进程没有退出,父进程其实一直在阻塞等待。

waitpid方法

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。

waitpid有三个参数,当pid,即第一个参数为-1时,等待任意一个子进程,与wait等效。

当第一个参数pid>0时,就会等待其进程ID与pid相等的子进程 。

如下图,此时等待上方父进程的子进程。

等待失败例子:

当我们把pid给一个错误的,此时进程就是等待失败。

获取子进程status

第二个参数status代表的是子进程的退出信息。

退出信息=退出码+退出信号

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待(只研究status低16比特 位):

最低7位表示终止信号,9到16位表示退出码。所以退出码的范围是0~255。退出信号的范围是0~125。这两个范围足以表示退出码的退出信号的情况了。

cpp 复制代码
 19 int main()                  
 20 {                           
 21     printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
 22                             
 23     pid_t id = fork();      
 24     if(id == 0)             
 25     {                       
 26         // child            
 27         ChildRun();         
 28         printf("child quit ...\n");
 29         exit(1);            
 30     }                       
 31     sleep(7);               
 32     // fahter               
 33     // pid_t rid = wait(NULL);
 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     else                    
 41     {                       
 42       printf("wait failed !\n");
 43     }                       
 44     sleep(3);               
 45     printf("father quit, status:%d, child quit code : %d,child quit signal: %    d\n",status,(status>>8)&0xFF,status&0x7F);                                   
 46 } 

上面是通过status退出信息来获取退出码和退出信号的代码。

status右移8位,然后与0xFF即二进制的8个1进行按位与,获取退出码。

status按位与0x7F即二进制的7个1来获取退出信号。

结果表明,前面所说都是正确的。

实际上我们不使用位操作符处理status,而是使用两个宏,WIFEXITED和WEXITSTATUS。

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

非阻塞等待

我们用的大部分接口都是阻塞等待接口,在阻塞等待时,父进程干不了别的事,一直在等待子进程退出。下面介绍非阻塞等待。

这里也需要用到一个宏:WNOHANG

cpp 复制代码
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 process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid    (), cnt);
 14         sleep(1);
 15         cnt--;
 16     }
 17 }
 18 
 19 int main()
 20 {
 21     printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
 22 
 23     pid_t id = fork();
 24     if(id == 0)
 25     {
 26         // child
 27         ChildRun();
 28         printf("child quit ...\n");
 29         exit(123);
 30     }
 31   
 32     //father
 33     while(1)
 34     {                                                                              
 35         int status=0;
 36         pid_t rid=waitpid(id,&status,WNOHANG); //非阻塞等待
 37         if(rid==0)
 38         {
 39           sleep(1);
 40           printf("child is running, father check next time!\n");
 41           //DoOtherThing();
 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     }

使用WNOHANG的时候,需要使用循环结构。因为WNOHANG只会查看一次子进程是否结束,使用循环结构就可以到最后判断子进程是什么情况了。即非阻塞等待的时候+循环=非阻塞轮询。

在非阻塞等待时,父进程可以在每次查看子进程的间隙做其他事情。

pid_ t waitpid(pid_t pid, int *status, int options);

pid_t >0 :等待成功,子进程退出了,并且父进程回收成功

pid_t <0 :等待失败了

pid_t =0 :检测是成功的,只不过子进程还没退出,需要下一次进行重复等待。

相关推荐
sduwcgg19 小时前
IQ-Learn 在 RTX 3090 服务器上的环境配置与踩坑记录
运维·服务器
呱呱巨基20 小时前
Linux 基础IO
linux·c++·笔记·学习
QFIUNE20 小时前
CD-HIT 详解:序列去冗余、安装使用与聚类结果解析
linux·服务器·机器学习·数据挖掘·conda·聚类
vortex520 小时前
XFCE 桌面环境组件详解:从面板到剪贴板管理
linux·xfce·桌面环境
marsh020620 小时前
43 openclaw熔断与降级:保障系统在异常情况下的可用性
java·运维·网络·ai·编程·技术
摇滚侠20 小时前
Docker 如何查询挂载的目录
运维·docker·容器
勇闯逆流河21 小时前
【Linux】linux进程控制(进程池的详解与实现)
linux·运维·服务器
zhangfeng11331 天前
部署到服务器上 宝塔系统 使用宝塔在线编辑器 FTP 批量上传 Git 部署 打包上传 codebudyy 编程程序开发
服务器·git·编辑器
WJ.Polar1 天前
Scapy基本应用
linux·运维·网络·python
lljss20201 天前
1. NameServer 域名服务器---NS
linux·服务器·前端