操作系统——进程的控制


前言:进程的控制,我们需要学会创建进程,知晓进程等待的原理,进程替换的原理,以及进程的终止。


1.进程的创建

fork()函数,是非常重要的函数。它可以创建一个新的进程,新创建的进程为子进程,原进程为父进程。可以man fork来查看此函数。
在这里插入图片描述
返回的参数是pid_t,可以看着一个整型,如果返回值等于0,那么说明此进程是子进程;返回值大于0,说明此进程是父进程;返回值小于0,说明此进程创建失败。
返回值>0时,返回的是子进程的PID。


以上内容是好理解的,我们都知道子进程会继承父进程的代码和数据,但如果子进程需要修改数据,那么会发生写时拷贝。既然子进程继承了父进程的代码和数据,那么我们可以看一下,fork()之后,子进程会不会做和父进程一样的事情。

验证:

 1 #include<unistd.h>
  2 #include<stdio.h>
  3 int main()
  4 {
  5   printf("father do this,can child do?\n");
  6   //创建子进程
  7   fork();
  8 
  9   printf("father do and child do\n");
 10                                                                                                                                                                                        
 11    sleep(1);
 12   return 0; 
 13 }          

运行一下结果:
在这里插入图片描述
很明显,fork()之后的代码被运行了两次,但是fork()之前的代码只运行了一次。运行俩次,我们可以理解因为有父子两个进程来运行。运行一次的什么情况?子进程不是继承了父进程的代码和数据吗?

注意: fork()之前的代码和数据,子进程也是继承了的,但是默认情况下,子进程执行的代码是从fork()之后的。子进程也是可以执行fork()之前的代码,不过需要人为控制一下。


fork()创建进程失败的原因:

  • 系统中进程过载
  • 实际用户的进程超出了限制

2.进程的退出

2.1 进程退出的情况
  • 代码运行完毕,程序结果正确
  • 代码运行完毕,程序结果错误
  • 代码运行失败,异常崩溃
2.2 进程退出码的概念

每个进程退出时,都会返回一个退出码;据此来判断进程执行的情况。
查看进程的退出码:

echo $?

指令也是一个可执行文件(进程),来查看一下其退出码:
在这里插入图片描述
可以看到,进程的退出码是0;0就是代表进程运行成功且正确。
有很多的退出码,我可以打印一部分出来,看一下:

  1 #include<unistd.h>
  2 #include<string.h>
  3 #include<stdio.h>
  4 int main()
  5 {
  6   for(int i=0;i<140;i++)
  7   {
  8     printf("退出码:%d %s\n",i,strerror(i));                                                                                                                                            
  9   }                                                                                                                                                
 10   return 0;                                                                                                                                        
 11 }                                

退出码总共有133个,只展示一部分。
在这里插入图片描述

2.3 进程常见退出的方式
  • main函数中的return
  • 调用exit
  • 调用_exit

(1) return
我们经常写return 0,意思就是此进程正确执行的退出码是0。很少有人写return 100。当然也能这么搞,可以验证一下。

  1 int main()
  2 {
  3   return 100;                                                                                                                                                                          
  4 }               

在这里插入图片描述
所以这个进程的退出码是100,正常人都是return 0.


(2) exit()

#include <unistd.h>
void exit(int status);

exit()的参数是退出码,它可以在任意位置调用,都代表结束进程。它其实相当于return,所以它会调用用户的清理资源函数,以及刷新缓冲区,关闭流等操作。

  1 #include<stdlib.h>
  2 #include<unistd.h>
  3 #include<stdio.h>
  4 int main()
  5 {
  6   printf("hollow");
      exit(0);
  7   //return 0;                                                                                                                                                                            
  8 }

以上程序中printf()里没有\n,所以缓冲区不会被刷新,return 时,会刷新缓冲区同样exit(),也会刷新缓存区。
在这里插入图片描述
(3)_exit()
此函数也可用于进程退出,参数同样是进程退出码。但是它就比较直接粗暴了,是直接退出,并不会做任何的操作。

 1 #include<stdlib.h>
  2 #include<unistd.h>
  3 #include<stdio.h>
  4 int main()
  5 {
  6   printf("hollow");
      _exit(0);                                                                                                                                                                         
  8 }

在这里插入图片描述
很明显,并没有刷新缓冲区。

总结:exit()功能和return基本一致,在进程退出前,会做一些善后工作;但是_exit()比较直接,不会去做善后工作。


2.4 进程退出,操作系统做了什么

操作系统会释放掉进程的进程控制块,代码数据的虚拟地址空间,页表,映射关系以及物理内存。


3. 进程的等待

进程退出时,需要其父进程回收它的退出信息。如果进程一直没有被回收就会形成僵尸进程,这是不太好的。所以父进程要等待子进程。

3.1 父进程等待的原因
  • 通过获取子进程的退出信息,得知子进程的执行结果
  • 可以保证:子进程先退出,父进程后退出
  • 进程退出时先进入僵尸状态,通过父进程的等待回收,可以避免僵尸进程造成的内存泄漏
3.2 如何进行进程的等待

在这里插入图片描述

(1)wait()

  • wait()的参数就一个整型指针status,这是一个输出性参数,也就是说如果我们不关心子进程的退出信息那么直接传参null即可;如果想要拿到子进程的退出信息那么就需要传一个整型指针。
  • wait()的返回值:如果等待成功则返回子进程的PID;失败则返回-1。

(2)waitpid()

  • waitpid()有三个参数:pid传的是-1,那么和wait()一样等待任意的一个子进程;传的是某个子进程的PID,那么会等待此PID进程。status和wait()里的参数一样。options提供了其它的方式来控制这个等待进程的过程,常见的options有两个:WNOHANG ->如果子进程还没结束,那么直接waitpid()返回,不会再继续等待,如果子进程结束了,那么就回收子进程退出信息。WUNTRACED ->如果子进程处于暂停状态,那么waitpid()立即返回不再等待。平常的话,options直接设为0就OK了。
  • waitpid()的返回值和wait()情况一样。

所以说进程的等待,status是关键,通过status就可以拿到进程的退出信息。返回值的话,只能判断子进程是否退出成功。
详解status
int总共有32位,只使用其低位的16位。
在这里插入图片描述
非常清楚的表示了上图,那么我们来试着拿出status。

首先子进程退出就这三种情况:

  • 代码运行完毕,程序结果正确
  • 代码运行完毕,程序结果错误
  • 代码运行失败,异常崩溃

前两种情况,说明终止状态是没毛病的,它没有异常,无非是运行结束后查看退出码。
最后一种情况,说明终止状态异常,也就是程序运行崩溃,运行都崩溃了,退出码已经不用看了没意义。

终止状态为0,说明代码运行成功,所以只需要和 0000 0000 0111 1111按位与即可,如果按位与的结果为0,那就终止状态没毛病,这拿到的是子进程退出的信号值。

在终止状态没问题的情况下,我们考虑取出退出码,也就是8~15位。只需要将status右移上8位,在和1按位与,就可以拿出退出码了。

那个code_dump标志:表示子进程终止状态是否异常,异常的话为1,正常为0;

代码实现:

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
 pid_t pid; 
 //创建子进程失败,直接退出
 if ( (pid=fork()) == -1 )
 {
 perror("fork"),exit(1);
 }
 // 子进程创建成功
 if ( pid == 0 )
 {
 //让子进程多跑会
 sleep(20);
 //设置退出码为10
 exit(10);
 } 
 //走进父进程
 else 
 {
   //st就是我们要取得status
   int st;
   //ret为wait返回值
   int ret = wait(&st);
   //ret>0说明等待成功,0X7F就是0X……0111 1111,按位与等于0,说明终止状态正常
   if ( ret > 0 && ( st & 0X7F ) == 0 )
   { // 正常退出
     //st右移动8位和0X……1111 1111,按位与,拿出子进程退出码
   printf("child exit code:%d\n", (st>>8)&0XFF);
   }
   //这种情况,终止状态有问题,取出终止状态,便于查看
   else if( ret > 0 ) 
   { // 异常退出
  printf("sig code : %d\n", st&0X7F );
   }
 }
}

代码运行:
退出码为10,这是我设置的。验证成功。
在这里插入图片描述
我用kill命令杀死子进程,我们来看看终止状态异常的情况:

12000是子进程的PID,我打印了一下,这没毛病、
在这里插入图片描述
可以看到终止状态是9,我就是用kill -9命令杀死的子进程。
在这里插入图片描述


3.3 阻塞等待和非阻塞等待

父进程有两种方式去等待子进程:

  • 阻塞等待:就干等着,什么也不做,父进程进入S状态
  • 非阻塞等待:不能干等,一直在询问子进程,你完事没?完事了我就回收你

阻塞等待:意味着父进程被放入等待队列
非阻塞等待:意味这父进程一直在运行队列

阻塞等待很好实现,上面我写的就是阻塞等待;非阻塞等待,可以写一个循环来实现,平且用到了options这个参数,我们设置成WNOHANG。


阻塞等待的实现:

#include <sys/wait.h>
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <errno.h>
  6 int main()
  7 {
  8  pid_t pid;
  9  pid = fork();
 10  if(pid < 0)
 11  {
 12  printf("%s fork error\n",__FUNCTION__);
 13  return 1;
 14  } 
 15  else if( pid == 0 )
 16  { //child
 17  printf("child is run, pid is : %d\n",getpid());
 18  sleep(5);
 19  exit(0);
 20  }
 21  else
 22  {
 23  int status = 0;
     //关键点:options 设为0
 24  pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
 25  printf("this is test for wait\n");
     
 26  if( WIFEXITED(status) && ret == pid )
 27  {
 28  printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
 29  }
 30  else
     {
 31  printf("wait child failed, return.\n");
 32  return 1;
 33  }                                                                                                                                                                                     
 35  }        
 36  return 0;
 37 }

在这里插入图片描述
注意上面用了俩个函数 WIFEXITED(),WEXITSTATUS()。第一个是用来获取子进程的终止状态,第二个适用于获取子进程的退出码。上面讲的时候不用这两个函数的原因是为了帮助大家理解status。


非阻塞等待的实现:

  1 #include <stdio.h> 
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>
  5 int main()
  6 {
  7  pid_t pid;
  8  
  9  pid = fork();
 10  
 11  if(pid < 0)
 12  {
 13  printf("%s fork error\n",__FUNCTION__);
 14  return 1;
 15  }
     
 16  else if( pid == 0 )
 17  { //child
 18  printf("child is run, pid is : %d\n",getpid());
 19  sleep(5);
 20  exit(1);
 21  }
     
 22  else{
 23  int status = 0;
 24  pid_t ret = 0;
     //走循环
 25  do
 26  { 
     //这就是非阻塞等待,WNOHANG选项:子进程还在运行,直接返回0
 27  ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
     //子进程还在跑
 28  if( ret == 0 )
     {
 29  printf("child is running\n");
 30  }
 31  sleep(1);
 32  }while(ret == 0);
 33  //ret !=0,说明子进程运行完了
     
 34  if( WIFEXITED(status) && ret == pid )
    { 
      //说明子进程运行没问题                                                                                                                                                
 35  printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
 36  }
     else
     {
     //说明子进程运行有问题
 37  printf("wait child failed, return.\n");
 38  return 1;
 39  }
 return 0;
 }                                      

运行结果:
在这里插入图片描述

这就是非阻塞等待,父进程通过一个循环,不断的问子进程你跑完没?跑完了,咱们就去回收一下。


4. 进程的程序替换

通过对fork()的学习,我们知道,可以根据fork的返回值来判断是子进程or父进程;但是如果想让子进程去完全的执行一个不同的程序(和父进程不挂钩),该怎么呢?那就是用进程替换。


4.1 进程替换的原理

进程替换需要创建新的进程吗?不需要!!!只需要替换子进程的代码和数据就好了。子进程会继承父进程的代码和数据,默认情况下,代码和数据子进程和父进程指向的是同一物理地址,如果子进程要进行程序替换,那么必然发生写时拷贝。页表+虚拟地址+物理地址重新构建映射关系。


4.2 进程替换的操作

调用exec系列函数,开头为exec的函数。
在这里插入图片描述
在这里插入图片描述
我们先学习一个execl(),我们要进行程序替换,需要调用execl()函数。
详解execl()

  • 参数:可变参数列表,也就说参数列表是可变的,path传的是路径,arg传的是在命令行上运行时的字符串。
  • 返回值:成功则不返回值, 失败返回-1, 失败原因存于errno中,可通过perror()打印。

(1)使用execl():

  1 #include <unistd.h>
  2 #include<stdio.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>
  5 int main()
  6 {
  7   pid_t i=fork();
  8   if(i==0)
  9   {
 10   int ret=  execl("/usr/bin/ls","ls","-l","-a","-n",NULL);                                                                                                                                  
 11   printf("%d\n",ret);
 12     exit(1);
 13   }
 14   wait(NULL);
 15   printf("father wait \n");
 16   return 0;
 17 }

可以看到,程序替换的内容是ls -l -a -n;
e
(2)使用 execv()
对比着execl(),execl()传路径+命令行上的字符串;execv()传的是路径+一个字符串数组,字符串数组中存着命令行上的字符串,注意也需要以null为结尾。

#include <unistd.h>
#include<stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{()
  pid_t i=fork();
  if(i==0)
  {

    char *arv[]={"ls","-a","-l",NULL};
    int ret= execv("/usr/bin/ls",arv);
  printf("%d\n",ret);
    exit(1);
  }
  wait(NULL);
  printf("father wait \n");
  return 0;
}

(3)使用 execlp().
execlp()中l 表示传的是命令行上的字符串,但是p表示可以使用环境变量去查找可执行文件,也就是说不需要传具体的路径,而是传文件名即可,它会自动的去环境变量PATH中查找具体路径。

#include <unistd.h>
#include<stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  pid_t i=fork();
  if(i==0)
  {
    int ret= execlp("ls","ls","-a","-l",NULL);
    printf("%d\n",ret);
    exit(1);
  }
  wait(NULL);
  printf("father wait \n");
  return 0;
}

(4)使用 execvp()
这个就可以比照execlp(),学习它俩差的也是一点:execlp()传的命令行上的字符串;execvp传的是字符串数组。

#include <unistd.h>
#include<stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  pid_t i=fork();
  if(i==0)
  {

    char *arv[]={"ls","-a","-l",NULL};
    int ret= execvp("ls",arv);
    printf("%d\n",ret);
    exit(1);
  }
  wait(NULL);
  printf("father wait \n");
  return 0;
}

(5)使用 execle()
比execl多了一个e,意思是不能使用当前的环境变量,必须使用自己配置环境变量。所以它最后还得传入自己配置的环境变量。

为了验证execle(),我可以在当下目录中写一个可以打印环境变量的程序,程序名为PATH。

#include<stdio.h>
int main()
{
  extern char ** environ;
  for(int i=0;environ[i];i++)
  {
    printf("%s\n",environ[i]);
  }
 return 0;
}

当然我可以进行程序替换,来执行PATH程序,先用execl()来验证一下。

#include <unistd.h>
#include<stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  pid_t i=fork();
  if(i==0)
  {

    //char *arv[]={"ls","-a","-l",NULL};
    int ret= execl("./PATH","PATH","NULL");
    printf("%d\n",ret);
    exit(1);
  }
  wait(NULL);
  printf("father wait \n");
  return 0;
}

可以看到,打印出了默认的环境变量。
在这里插入图片描述
现在,我们用execle()来进行程序替换,并且配置自己的环境变量。

#include <unistd.h>
#include<stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  pid_t i=fork();
  if(i==0)
  {
    char *env[] = {
            "MYENV=hahahahahahahahehehe",
            "MYENV1=hahahahahahahahehehe",
            "MYENV2=hahahahahahahahehehe",
            "MYENV3=hahahahahahahahehehe",
            NULL
        };
        execle("./myexe", "myexe", NULL, env);
    exit(1);
  }
  wait(NULL);
  printf("father wait \n");
  return 0;
}

我们配置的环境变量就是env,我们用execle进行程序替换,就必须使用自己配置的环境变量,那么PATH程序会打印出默认的环境变量,还是我们自己配置的环境变量。

毫无疑问,是我们自己配置的环境变量
在这里插入图片描述
(6) execve() 的使用
有了execle(),那么学习execve(),就简单多了。我们只需要改变一点:传字符串数组即可。

#include <unistd.h>
#include<stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
  pid_t i=fork();
  if(i==0)
  {

    //char *arv[]={"ls","-a","-l",NULL};
    char *env[] = {
            "MYENV=hahahahahahahahehehe",
            "MYENV1=hahahahahahahahehehe",
            "MYENV2=hahahahahahahahehehe",
            "MYENV3=hahahahahahahahehehe",
            NULL
        };
        char *argv[] = {
            "PATH",
            NULL
        };
        execve("./PATH", argv, env);
    exit(1);
  }
  wait(NULL);
  printf("father wait \n");
  return 0;
}


4.3 进程替换函数的总结

以上的6个函数,都能进行进程替换,为了适应不同的场景,所以才有了这么多的接口函数。但是这六个函数,其实都是封装一个函数,来实现的不同应用场景;这个函数就是execve()。只有execve()是系统调用的,其它的都是对它的封装。
在这里插入图片描述


结尾语: 以上就是本期内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

动名词

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值