当前位置: 首页 > news >正文

Linux进程控制

今年的中秋又要到啦,诚邀亲爱的博主参与投稿,分享“程序员”视角下的中秋夜之美!

内容可以是:

  • 程序员过中秋的正确方式:团圆、赏月、还是惨兮兮地加班?
  • 互联网大厂的中秋仪式感:壕无人性!
  • 炫出我的中秋节礼盒:红包、奖金、还是空气月饼?
  • 我的中秋节祝福程序源代码分享:过什么节,代码走起!
  • 如何用代码制作水墨风格的中秋佳节网页?
  • 如何用代码绘制月饼?
  • 用代码分析月饼节的那些事儿:什么月饼口味卖的最好?
  • 打造我的专属中秋节小程序:我的浪漫不是梦~~
  • ······

主题不限哦,仅供参考 ~~你有什么想要分享的呢,快开启你的创作吧!

写在前面

我们前面的几个博客都弱化进程控制的知识,主要关注原理了。这里我们正式的谈谈进程的控制,包括进程的创建和退出等等,这里面最难的是进程的等待,不过也不要担心,我尽量把涉及到的知识谈透。

进程创建

我们先来看一下进程的创建.这个我们在之前已经使用了,就是使用fork.不过在这里之前,我们还是需要谈一下写时拷贝的.

写时拷贝

我们这里要谈两个问题,一个就是拷贝什么,而是为何要使用写时拷贝.这也是我们今天的一个比较重要的点.

拷贝什么

我们先来解决第一个问题,拷贝的是什么.我们在前面已经和大家谈过.刚开始子进程和父进程共享同一片空间.也就是代码和数据都是共享的.这里我就疑惑了,所谓的代码共享是指从fork开始代码共享部分代码还是共享父进程所有的代码?

我们可能会这么想,我们子进程是从fork后面开始执行的,那么有极大的可能是共享部分代码.这样想也有一定的道理.不过大家看一下下面的代码.举得例子可能不太恰当,不过也能证明一部分.

int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    // parent    
    printf("我是父进程 %d\n",g_val);      }    
  else     
  {    
    // child    
    printf("我是子进程 %d\n",g_val);               
  }                  return 0;        
} 

image-20220828214119499

这个例子在一定程度上证明了我们最起码不是从fork开始复用的.至于是不是复用全部代码,这里还体现不出来.这里我直接给结论,子进程是复用父进程的全部代码.至于验证方法这里就不演示了,有兴趣的可以去看一看vfork这个函数,这里可以解决你的疑惑.

那么这里我们就疑惑了,既然我们是复用全局的代码,那么子进程又是如何可以准确的找到fork函数这一行对应的地址的.这里面就要看一下子进程在创建的时候编译器是如何做的.首先第一点,我们肯定是先创建一个task_struct+mm_struct + 页表.那么这里就会出现问题的答案.我们在函数栈帧那个博客提过,在CPU中存在一个寄存器eip,这个寄存器记录着下一条语句的地址,也叫PC指针.我们子进程拷贝的时候也会把这个地址给拷贝下来,这就是我们为何子进程在fork这个函数语句执行.我们修改eip里面的PC指针,也可以修改子进程的执行的初识地址.

为何是写时拷贝

这个问题我们在前面的一篇博客稍微提过一下.首先不是我们非要使用写时拷贝,而是写时拷贝相对于其他的方法比较优良.想一下,我们计算机的内存空间是有一定的限度的.如果我们每一个子进程被创建出来,都要开辟一款空间.如果我们不修改该里面的数据,那么这篇空间就被浪费了.我们可能回想,是不是我们可以把要修改的数据的空间给开辟出来,这也是一个解决方法啊.是的,但是你有没有想过,编译器知道哪些是要被修改的数据吗,它只有跑完程序才会知道,所以这个方法不可以被实现.人们就像到,既然我们不知道要修改那个数据,我们就像不开辟空间,等到修改的时候在开辟,这就是写是]时拷贝,也叫延迟拷贝.当然,写实拷贝需要一定的效率,但是相比较其他的方法总体而言是非常优秀的,所以Linux采用写时拷贝.

进程创建

这里我们就谈fork这个函数就可以了,也没有什么可以多说的.

image-20220810195503149

fork失败

我们知道进程的创建是需要空间的,内存就这么大,一般内存中进程过多的话,就会出现进程的创建失败.

#include <unistd.h>
int count = 0;
int main()
{
  while(1)
  {
    pid_t id = fork();
    if(id < 0)
    {
      printf("进程创建失败 %d\n",count);
      break;
    }
    else if(id == 0) 
    {
      printf("我是子进程 %d\n",getpid());
      sleep(20);                            
      exit(0);
    }
    ++count;
  }        
  return 0;
}

image-20220829145434277

大家可以看到,我们创建了4k个进程,不过实际进程要比这小的多,主要是我们创建子进程的时候什么是都没有干,空间要求的比较少.

进程终止

现在我们就要好好的谈论一一下进程的终止了,所谓的进程终止就是进程进入X状态.这个涉及到的内容可就多了.不过在这里之前我们还是需要解决几个问题,.进程的退出只可能是下面的三种情况.

  • 进程跑完了,结果正确
  • 进程跑完了,结果错误
  • 进程没跑完,遇到异常终止了

这里我们先来解释前面的两种情况,至于第三种等到以后再说.

进程退出码

大家可能对这个进程退出码感到疑惑.我举个例子大家就可以明白了.看一下下面的代码.

这里我想提问一下,为何会出现一个return,假设我们理解出现return,那么为何事0,不能事其他数吗?

#include <stdio.h>    
int main()    
{    
    int a = 1;                                     
    return 0;    
} 

上面的问题是我们从未想过的,之前我们都是无脑的在main函数中return 0.今天我们要稍微的揭开一下它神秘的面纱.

首先我先解释一下,return语句可以结束相应的函数,main函数也是一个函数,所以这里我们使用return.再来说一下,我们为何返回0.首先,不是我们必须返回0,而是0可以代表一种情况,前面我们说过,进程会出现三大种情况,其中0就代表我们进程跑完了,而且结果是正确的.我们把return X中的X 称为进程退出码.

我带大家看一一下进程退出码所对应的信息,我们这这里先来了解一下.

#include <string.h>  
  
int main()  
{  
  int n = 50;  
  for(int i=0;i<n;i++)  
  {  
    printf("%d %s\n",i,strerror(i));               
  }  
  return 1;  
}

image-20220829115417124

echo $?

如果我们想要查看最近一i次进程的退出码就可以用这个指令.

[bit@Qkj 08_29]$ echo $?

image-20220829114632312

这里我们也验证一下,我们已经知道的Linux里面的指令本质上也是函数,或者是一个进程,我们查看一下不存在的文件,然后看一下进程退出码.

[bit@Qkj 08_29]$ ls m

image-20220829115553212

这里我还要和大家说一下,以后我们写网络编程,这个进程退出码就非常重要了,不能再像以前无脑return0了.而且我们也可以自己定义不同的进程退出码代表的不同的信息,没必要一定按照上面的来.

进程终止方法

现在我们就可以正式谈一下进程的终止了.进程终止也是一个大章节.我们正常进程终止有俩种方法.关于我们之前的ctrl + c,是信号终止 异常退出,这俩先放一下,等到后面的几个博客在和大家谈.我们先来简单的看一下.

  • 在main函数中遇到 return 语句
  • 在任意地方遇到exit函数

这里我先简单的解释一下第一个,记住我们是在main函数遇到return这个进程才算是终止的,其他函数是不行的.

exit

我们在C语言的时候好象见过这个函数,这里我们不多废话,先来看用法,后面再说其他的.

#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
                                           
void func()                             
{                                       
  exit(111);                            
}                                       
int main()                              
{                                       
  func();                               
  return 0;                             
} 

image-20220829135523832

我们发现退出码是exit的,这里就可以得到一个结论,只要一碰到exit,这个进程就会终止.

_exit

有的同学可能会感到疑问,好象还是存在一个_exit这个函数吧,他们有什么区别?这里和大家提一下功能上的区别,exit的底层是调用了_exit,当然也做了一些另外的功能,比如说是缓冲区的问题.

  • _exit 不会涮洗缓冲区
  • exit 退出前会刷新缓冲区
#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
                                           
void func()                              
{                                        
  printf("hello word");                  
  exit(111);                            
}                                        
int main()                               
{                                        
  func();                                
  return 0;                              
} 

image-20220829140152940

#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
#include <unistd.h>  
                                           
void func()                              
{                                        
  printf("hello word");                  
  _exit(111);                            
}                                        
int main()                               
{                                        
  func();                                
  return 0;                              
} 

image-20220829140350868

注意,这两个函数的作用和main函数里面的return 0一样,记住是main函数.

当只有一个进程时,我们把退出码给bash进程,bash进程会释放相应的资源,但是对于多个进程,它的作用只是提供退出码,子进程的资源没有被释放,这是父进程的事,你需要手动释放拿,要和后面的进程控制配套使用.

在一个进程调用了exit()之后,该进程并不会立刻完全消失,而是留下一个称为僵尸进程(Zombie)的数据结构 。
僵尸进程是一种非常特殊的进程,它已经释放了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间.留下一个称为僵尸进程(Zombie)的数据结构 这句话有些不对,不是留下一个僵尸进程的数据结构,而是僵尸进程退出程序的运行了,但是pcb资源没有被完全释放也正是因为pcb没有被释放,所以ps查看进程信息的时候才能依然查看到进程信息,但是pcb内部的大部分资源都被释放掉了

操作系统做了什么

这里我们就开是考虑一件事了,关于进程的终止,我们计算机内核做了什么.这才是我们进程终止的重点.

首先我们要明白,每一个进程程度都是存在一个父进程的,我们对于子进程的退出码会给到父进程,这一点可以稍微放一下.我们再看,对于进程的销毁,肯定是存在下面两个方面的销毁.

  1. 内核结构 task_struct 和 mm_struct
  2. 代码+数据

首先要明确一点,代码和数据一定是会被销毁的,至于数据结构存在一定考虑.大家想我们进程运行的时候首先就是开辟空间+初始化,这两个都要耗费时间,操作系统就想我们能不能把这个要销毁的数据结构保存下来,我放在一个地方,如果有新进程,就不开辟空间,直接初始化就可以了,这是一些内核的做法.其中这个特殊存放这些数据结构的地区叫做内核的数据缓冲池,也叫做slab分派器.

进程控制

我们前面见过僵尸进程,进程已经进入的Z状态,我们是杀不死它的,僵尸进程的出现会造成内存的泄漏,这里就出现可以进程等待.所谓的进程等待就类似于你的老板让你去出差,等你工作结束了,你不能只闷头回到公司工作,肯定要先领导汇报情况啊.领导正在等会着你的结果呢.这就是进程等待的原因,子进程的工作已经做完了,但是里面的资源还没有释放,这里父进程等着释放资源.

我们进程等待主要是考两个函数,不过我们需要理解一下他们背后的一些深层次的功能.

  • wait
  • waitpid

其中我们重点关注第二个函数.

wait

我们先先来认识一下这个函数,这个函数还是比较简单的,里面传入的是一个指针参数,这里我们先不管它,后面也存在一个函数,其中传入参数的类型和作用和这个一样,到后面再谈.

image-20220829160238701

我们在父进程中使用wait,等到子进程功能运行结束后,父进程释放掉子进程对应的资源,继续向后执行.

#include <sys/wait.h>    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    //child    
    while(1)    
    {    
      printf("我是子进程 pid %d\n",getpid());    
      sleep(1);    
    }    
  }    
  else    
  {    
    printf("我是父进程,正在等待子进程 pid %d\n",getpid());    
    sleep(20);                                                                                                   // 进程等待                                             
    pid_t ret = wait(NULL);    
      
    sleep(20);    
    if(ret == -1)    
    {    
      printf("等待错误\n");    
    }    
    else    
    {    
      printf("等待成功\n");    
    }    
  }    
  return 0; 
}

image-20220829163421007

这里我们又可以得到一个结论,我们观察到父进程处于S状态,也就是阻塞态,这里的阻塞态可不是为了等待硬件,而是软件部分没有到位,这里算是前面博客的一个补充,放在这里比较好理解一点.

waitpid

这个才是我们经常使用的进程等待函数,这个函数完全包含了上一个wait.我们这里就要看一下他们的区别了.我们这里先说一下,由于前面我们都是一个子进程,所以来说wait就可以等待,wait这个函数的本质就是等待任意一个子进程,如果是多个的话就需要我们现在的waitpid这个函数了.

image-20220829165053503

我们来解释一下返回值,我们重点关注成功和不成功,另外的一种情况暂时不考虑.至于函数的三个参数,这就是我们需要注意的了.

  • pid_t pid 要等等待 子进程测 pid,是几就表示等待那个进程,如果是-1表示等待任意进程
  • int *status 一个变量的指针,这个是我们今天要重点分享的
  • int options 这个先不说,今天先设为0,0叫阻塞等待,阻塞的概念谈过,但是这个还没说,留下来后面谈

再析进程退出码

由于我们这里只学了进程退出码,这里先来看一下进程退出码在进程等待中的作用.在task_struct中是存在一个变量保存进程的退出码的.

image-20220829171035812

在子进程结束后,子进程的退出码就会保存在exit_code里面,父进程会拿到子进程里面的退出码,这就像领导会向你询问工作完成测状况.

int *status

这里面我们就可以讨论这个情况了,我们把进程的退出码放在一个变量里面,这就是我们要谈的.但是一个int类型的变量是32的比特位,这里面我们就用次16位.而且退出码还保存在高8位,这一点要记住.

image-20220829171617129

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    int count = 5;                                                 
    while(count--)
    {
      printf("我是子进程\n");
      sleep(1);
    }
    exit(13);
  }
  else 
  {
    int statu = 0;
    printf("我是父进程,在等待子进程结束\n");
    pid_t ret = waitpid(id,&statu,0);
    if(ret > 0)
    {
      printf("等待成功 退出码 %d\n",(statu&0xff00)>>8);
    }
    else
    {
      printf("等待失败\n");
    }
  }
  return 0;
}

image-20220829172522303

那么这里面我们就存在一个问题,我们是不是可以设计一个全局变量,一旦我们们退出子进程,我们就修改这个全局变量的值,这样还用这么麻烦的吗?但是你忘记了一个问题,一旦我们子进程修改变量,那么操作系统就会重现开辟空间,那么对于父进程来说,原本的变量的值可是没有变化.

这里我们还存在一个问题,既然高8位是进程退出码,那么我想问下低8位代表的什么?

我来解释一下后面的8位,准确来说是7位,因为有一位这里还无法谈,先放一下.

image-20220829190936932

大家可能对终止信号出现疑惑,那么我们前面用kill指令,用的是9好命令.我们看一下kill指令,这里我们提一下,具体的等到信号那个博客在谈.

我们后面会学习前31个信号代表的啥,注意看,这里是没有0号信号的.

[bit@Qkj 08_29]$ kill -l

image-20220829191309905

这里我们就可以测试出来低7位是做什么的了.

#include <stdio.h>    
#include <string.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/wait.h>    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    while(1)    
    {    
      printf("我是子进程 pid %d\n",getpid());    
      sleep(1);    
    }    
  }    
  else    
  {    
    int statu = 0;    
    printf("我是父进程,在等待子进程结束\n");    
    pid_t ret = waitpid(id,&statu,0);    
    if(ret > 0)    
    {    
      printf("子进程退出码 %d,子进程的退出信号 %d\n",(statu&0xff00)>>8,statu&0x7f);          
    }    
    else    
    {    
      printf("等待失败\n");    
    }    
  }    
  return 0;    
}  

image-20220829192258337

WIFEXITED & WEXITSTATUS 函数

既然我们已经知道了退出信号和退出码,那么这两个优先看哪一个?首先我们要确保进程可以运行,只有这样进程的退出码才有意义.那么我们是如何确定的进程正常运行呢?这里提供了两个函数.

  • WIFEXITED(status) 进程正常退出为真
  • WEXITSTASTUS(status) 若进程正常退出,可以直接查看退出码
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    while(1)
    {
      printf("我是子进程 pid %d\n",getpid());
      sleep(1);
    }
  }
  else
  {
    int statu = 0;
    printf("我是父进程,在等待子进程结束\n");
    pid_t ret = waitpid(id,&statu,0);
    if(ret > 0)
    {
      if(WIFEXITED(statu))
      {
        printf("子进程退出码 %d\n",WEXITSTATUS(statu));                                                                                                 
      }
      else
      {
        printf("进程非正常退出\n");
      }
    }
    else
    {
      printf("等待失败\n");
    }
  }
  return 0;
}

image-20220829193521583

这里就可以解释我们前面进程的第三种情况了,进程没有跑完,就异常了,这里也是.我们用野指针测试一下.

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    int count = 1;
    while(1)
    {
      printf("我是子进程 pid %d\n",getpid());
      sleep(1);
      count++;
      if(count == 15)
      {
       break;
      }
    }
    //  这里我们使用 也指针
    printf("注意,我们要使用 野指针了\n");
    int* p = NULL;
    *p = 10;
  }
  else
  {                                                                                                                                                     
    int statu = 0;
    printf("我是父进程,在等待子进程结束\n");
    pid_t ret = waitpid(id,&statu,0);
    if(ret > 0)
    {
      if(WIFEXITED(statu))
      {
        printf("子进程退出码 %d\n",WEXITSTATUS(statu));
      }
      else
      {
        printf("进程非正常退出\n");
      }
    }
    else
    {
      printf("等待失败\n");
    }
  }
  return 0;
}

image-20220829194212008

这应该就是我们在之前使用野指针报错的原因,要知道父进程也是bash的子进程,编译器多这个异常退出的进程进行报错处理.

阻塞等待

这里我们在前面谈到过,进程的等待分为阻塞和非阻塞,这里我们先来谈阻塞.前面我们在谈进程状态的时候遇到过阻塞的概念,由于是硬件的效率远远低于CPU的效率,所以要的条件就绪后,才会进入执行态.那么这里对于父进程面我们可是没有等待硬件资源,这是在等待软件.这里把阻塞态延伸一下,阻塞态只有当条件就绪后才会进入运行态,这个条件包括软硬件资源.

相关文章:

  • 猿创征文 | 一个大四学长分享自己的web前端学习路线--vue篇(1/3)
  • ELK日志分析系统简
  • 详细SpringBoot框架教程——SpringBoot配置SSL(https)
  • 性能测试你需要懂这些
  • 【付费推广】常见问题合集,焦点展台与任务管理
  • Android毕业论文选题基于Uniapp+Springboot实现的校园论坛
  • 佛山复星禅诚医院黄汉森:云边协同,打造线上线下一体化智慧医疗
  • Connor学JVM - 执行引擎
  • 【软考学习6】计算机存储结构——局部性原理、Cache、主存地址单元、磁盘存取、总线和可靠性
  • Python 基于OpenCV+face_recognition实现人脸捕捉与人脸识别
  • TensorFlow?PyTorch?Paddle?AI工具库生态之争:ONNX一统天下 ⛵
  • 关于 Java Long 类型传给前端损失精度
  • 30分钟熟练使用最常用的ES6,还不学是等着被卷死?
  • 【面试题】面试必备我跟面试官聊了一个小时线程池!
  • 设置服务器上MySQL允许外网访问
  • 时间复杂度分析经典问题——最大子序列和
  • (ckeditor+ckfinder用法)Jquery,js获取ckeditor值
  • 【comparator, comparable】小总结
  • 【译】React性能工程(下) -- 深入研究React性能调试
  • C++类的相互关联
  • CentOS 7 防火墙操作
  • egg(89)--egg之redis的发布和订阅
  • Just for fun——迅速写完快速排序
  • vue2.0项目引入element-ui
  • vue-loader 源码解析系列之 selector
  • 理清楚Vue的结构
  • 批量截取pdf文件
  • ​Java并发新构件之Exchanger
  • ​七周四次课(5月9日)iptables filter表案例、iptables nat表应用
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • #pragma预处理命令
  • (4)Elastix图像配准:3D图像
  • (ctrl.obj) : error LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MDd_DynamicDebug”不匹配值“
  • (c语言)strcpy函数用法
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (第27天)Oracle 数据泵转换分区表
  • (论文阅读26/100)Weakly-supervised learning with convolutional neural networks
  • (免费领源码)python#django#mysql校园校园宿舍管理系统84831-计算机毕业设计项目选题推荐
  • (五)网络优化与超参数选择--九五小庞
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • (转)创业家杂志:UCWEB天使第一步
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .apk 成为历史!
  • .gitignore文件—git忽略文件
  • .NET / MSBuild 扩展编译时什么时候用 BeforeTargets / AfterTargets 什么时候用 DependsOnTargets?
  • .NET Core/Framework 创建委托以大幅度提高反射调用的性能
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .net 调用php,php 调用.net com组件 --
  • .net 获取url的方法
  • .NET 命令行参数包含应用程序路径吗?
  • .NET分布式缓存Memcached从入门到实战
  • .sh 的运行
  • .vue文件怎么使用_vue调试工具vue-devtools的安装
  • /proc/vmstat 详解