【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 :检测是成功的,只不过子进程还没退出,需要下一次进行重复等待。

相关推荐
vip45115 分钟前
Linux 经典面试八股文
linux
大霞上仙18 分钟前
Ubuntu系统电脑没有WiFi适配器
linux·运维·电脑
weixin_4426434236 分钟前
推荐FileLink数据跨网摆渡系统 — 安全、高效的数据传输解决方案
服务器·网络·安全·filelink数据摆渡系统
Karoku0661 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
为什么这亚子1 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
半桶水专家1 小时前
用go实现创建WebSocket服务器
服务器·websocket·golang
布值倒区什么name1 小时前
bug日常记录responded with a status of 413 (Request Entity Too Large)
运维·服务器·bug
孤客网络科技工作室1 小时前
VMware 虚拟机使用教程及 Kali Linux 安装指南
linux·虚拟机·kali linux
。puppy2 小时前
HCIP--3实验- 链路聚合,VLAN间通讯,Super VLAN,MSTP,VRRPip配置,OSPF(静态路由,环回,缺省,空接口),NAT
运维·服务器
颇有几分姿色2 小时前
深入理解 Linux 内存管理:free 命令详解
linux·运维·服务器