【Linux进程】进程控制

目录

一、进程创建

1.2 fork函数初识

1.2 fork函数返回值

1.3 写时拷贝

1.4 fork常规用法

1.5 fork调用失败的原因

二、进程终止

2.1 进程退出场景

2.2 进程退出码

2.2.1 用strerror打印错误信息

2.2.2 errno全局变量

2.3 进程常见退出方法

2.3.1 进程正常退出

2.3.1.1 从main返回

2.3.1.2 调用exit

2.3.1.3 _exit及exit与_exit的区别

2.3.2 进程异常退出

2.4 总结

三、进程等待

3.1 进程等待的概念

3.2 进程等待必要性

3.3 进程等待的方法

3.3.1 wait方法

3.3.2 waitpid方法

3.4 获取子进程status

 3.5 非阻塞轮询

四、进程程序替换

4.1 替换原理

4.2 替换函数

 4.2.1函数解释

4.2.2 命名理解

4.2.3 exec调用举例如下

五、实现一个简易的shell 


一、进程创建

1.2 fork函数初识

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

【Linux进程】进程控制_第1张图片

返回值:子进程中返回0,父进程返回子进程id,出错返回-1

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

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

【Linux进程】进程控制_第2张图片

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

  1 #include   
  2 #include   
  3 #include   
  4 
  5 int main( void )
  6 {
  7   pid_t pid;
  8 
  9   printf("Before: pid is %d\n", getpid());
 10 
 11   if ( (pid=fork()) == -1 )
 12   {
 13     perror("fork()");
 14     exit(1);
 15   }
 16   printf("After:pid is %d, fork return %d\n", getpid(), pid);
 17   sleep(1);                                                                                                                                                                            
 18   return 0;
 19 }

 运行结果:

 这里看到了三行输出,一行before,两行after。进25508先打印before消息,然后它有打印after。另一个after消息有25509打印的。注意到进程25509没有打印before,为什么呢?如下图所示

【Linux进程】进程控制_第3张图片所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。我们之前说过fork函数会创建子进程,给父进程返回子进程PID,给子进程返回0。因此我们通过返回值可以知道After第一次是由父进程打印的,第二次是由子进程打印的。

注意:fork之后,无论是父子进程还是兄弟进程,谁先执行完全由调度器决定。

1.2 fork函数返回值

为什么有两个返回值?

fork之后创建了子进程,父子两个执行流分别执行,父子进程代码共享(包括return),所以两个执行流有各自的返回值。

fork函数为什么要给子进程返回0,给父进程返回子进程的PID?

一个父进程是可以有多个子进程的,但是一个子进程只能有一个父进程。对于子进程来说,可以通过getppid得到父进程,因此我们并不需要去标识父进程。但是对于父进程而言,假如你有三个儿子,然后你叫了一声儿子,你的三个儿子并不知道你叫的是他们当中的哪一个,所以fork需要给父进程返回子进程的PID。

1.3 写时拷贝

当一个子进程刚被创建的时候,父子进程的代码和数据是共享的。也就是父子进程的代码和数据通过页表映射到同一块物理内存且页表上的权限默认是只读的。当任意一方试图写入,便会触发操作系统的权限问题,但是针对这种情况,操作系统并不会抛异常,而是以写时拷贝的方式各自一份副本。具体见下图:

【Linux进程】进程控制_第4张图片

写时拷贝本质 :上图中,子进程试图写入,操作系统就会将该进程的数据在物理内存重新开辟一块空间,然后将子进程的页表对应的虚拟地址映射到一块新的物理内存上(重新建立映射),然后再进行写入操作。这个过程是不会影响虚拟地址的,也就是说父子进程的虚拟地址还是一样的,但是映射的物理地址不一样。

1.4 fork常规用法

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

1.5 fork调用失败的原因

fork函数创建子进程也是有可能会失败的。fork调用失败主要有以下两种原因:

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

二、进程终止

2.1 进程退出场景

进程退出有以下三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

上面三种退出情况中,我们通常更关心后面两种。

举个例子:加入小李这次考试考了一百分,小李的爸爸大李会问小李为什么考了一百分吗?很显然不会。我们只会关心为什么拿不了一百分,为什么做错题。

2.2 进程退出码

我们先来谈进程退出码,注意这里的进程退出码只跟代码运行完毕,结果正确或者代码运行完毕,结果不正确这两种情况有关系,也就是说,我们这两种情况我们统一用进程退出码进行判定。跟代码异常终止没有关系。原因我们后面再讲。

我们在写C/C++程序的时候,通常会在main函数的最后加上一个return 0。

那么为什么main函数总是会返回return 0? 1? 2?, 这个东西给谁了?为什么要返回这个值?

main函数是间接被操作系统所调用的,当main函数被操作系统所调用后,这个程序就会变成进程。既然是进程,那是不是就应该给操作系统返回相应的退出信息,告诉操作系统自己是正常退出还是说异常退出呢?

所以,main函数的返回值,本质表示:进程运行完成时是否是正确的结果,它又称为进程的退出码!0代表运行正确,如果不是正常退出,可以用不同的数字,表示不同的出错原因!

下面我们来用这段代码演示一下:

  1 #include 
  2 #include 
  3 #include 
  4 
  5 int main()
  6 {
  7   printf("hello process!\n");
  8 
  9   return 0;                                                                                                                                                                            
 10 }

运行结果:

运行后,我们可以通过echo $?指令查看我们最近一次的进程退出码。我们发现最近的进程退出码是0。

2.2.1 用strerror打印错误信息

c语言中的strerror函数可以将返回值转化为错误信息

【Linux进程】进程控制_第5张图片

 下面我们将其他返回值的信息打印出来,来看下面这段代码:

  1 #include   
  2 #include   
  3 #include   
  4 #include   
  5                                                                                                                                                                                        
  6 int main()                                       
  7 {                                                
  8                                                  
  9             
 10     for(int i = 0 ; i < 200; i++)                
 11     {                                            
 12         printf("%d: %s\n", i, strerror(i));      
 13     }                                                                      
 14     return 0; // 进程的退出码,表征进程的运行结果是否正确. 0->success  
 15 }   

运行结果:

【Linux进程】进程控制_第6张图片

我们看到只有0是成功运行,其他数字都是代表各种程序的异常退出的原因。

其实Linux下ls、pwd等命令也是可执行程序,使用这些命令后我们也可以查看其对应的退出码。我们再来看下面的例子:

【Linux进程】进程控制_第7张图片

我们看到,我们目录中码有mytest.txt文件,运行之后报错,当我们打印退出码的时候是2,正是和我们上面的错误信息一致的。

所以,系统提供的进程退出码和退出码描述是有对应关系的。

2.2.2 errno全局变量

下面我们再来看一个变量errno,当我们系统调用或库函数执行失败时,会将特定的错误代码返回给 errno。

注意:这里返回的是最后一次库函数调用失败的错误码。

【Linux进程】进程控制_第8张图片

我们来看下面这个例子:

【Linux进程】进程控制_第9张图片

运行结果:
【Linux进程】进程控制_第10张图片

当我们调用malloc函数申请空间失败,会将失败的原因存到errno,可以看到这里错误代码是12。我们还能将错误码作为进程失败的退出码:我们再用strerror将errno存的错误代码信息打印出来,原因是不能够分配到这么多的内存。

2.3 进程常见退出方法

2.3.1 进程正常退出

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

2.3.1.1 从main返回

在main函数中return是进程常见的退出方法之一。

  1 #include 
  2 #include 
  3 #include 
  4 
  5 int main()
  6 {
  7   printf("hello process!\n");
  8 
  9   return 0;
 10 }

2.3.1.2 调用exit

我们来简单看一下exit的用法:

【Linux进程】进程控制_第11张图片

我们看到exit的功能是终止进程,参数其实就是进程退出码 

下面来看这段代码验证一下:

 23 int main()  
 24 {  
 25     printf("hello Linux\n");
 26  
 27     exit(12);
 28    // return 12;                                                                                                                                                                       
 29 } 

运行结果:

【Linux进程】进程控制_第12张图片

所以exit的参数其实就是进程退出码 

我们知道用return + 进程退出码也可以得到同样的效果,那么exit和return的区别是什么呢:

return在函数内表示函数的返回,exit在任何地方被调用都表示进程直接退出

我们来看下面这个例子进行验证:

  8 void show()  
  9 {  
 10     printf("hello show!\n");  
 11     printf("hello show!\n");  
 12     printf("hello show!\n");  
 13     printf("hello show!\n");  
 14     printf("hello show!\n");  
 15     printf("hello show!\n");  
 16     exit(13); // 在任意地方被调用,都表示调用进程直接退出 return 只表示当前函数返回
 17     printf("end show \n");
 18     printf("end show \n");
 19     printf("end show \n");
 20     printf("end show \n");
 21 }
 22 
 23 int main()
 24 {
 25     show();                                                                                                                                                                            
 26     printf("hello Linux\n");    
 27                                 
 28                     
 29     return 0;                  
 30 }  

运行结果:

【Linux进程】进程控制_第13张图片

我们发现只打印了exit之前的内容,exit之后的不再打印。说明exit函数在任意地方调用,都代表终止该进程。

exit函数在退出进程之前还会做以下工作:

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

【Linux进程】进程控制_第14张图片

_exit和exit的功能是一样的,都是终止一个进程,而且他们的参数都代表进程退出码。 

同样的,_exit函数在任意地方调用,都代表终止该进程。

那么exit和_exit的区别是什么呢?

我们来看下面这个例子:

【Linux进程】进程控制_第15张图片

运行结果:

【Linux进程】进程控制_第16张图片

我们再来看_exit的运行结果:

【Linux进程】进程控制_第17张图片

【Linux进程】进程控制_第18张图片

我们发现调用_exit的时候这里什么都没有输出。

所以,exit函数在退出进程之前还会做以下工作:

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

下面我们通过一张图来表示exit与 _exit的区别和联系:

【Linux进程】进程控制_第19张图片

所以,我们的printf函数一定是把数据写入缓冲区,合适的时候,再进行刷新。从上面这种图我们可以看到,这个缓冲区一定不在内核,否则_exit()也会进行刷新。

小结:在任意地方调用exit或者_exit函数,都能够起到终止进程的作用,但是exit在终止进程前会完成一系列的收尾工作,但是 _exit不会。

2.3.2 进程异常退出

上面我们留了一个问题,进程退出码代码异常终止没有关系,那么为什么跟进程退出码代码异常终止没有关系?

我们的代码异常终止,就相当于代码没有跑完,既然代码没有跑完,那么进程的退出码肯定是没有意义的。

我们来举个例子:假设小李这次考试考了九十分,但是他中途考试作弊了,我们还有给他颁发奖状吗?很明显不会。同样的,当我们的代码异常终止,那么进程的退出码也没有意义了,我们也就不关心退出码了。

因此异常退出我们应该关心的是为什么发生了异常?发什么什么异常?

其实进程出现异常,本质是我们的进程收到了对应的信号。

下面我们来看下面的例子:

int main()
{
  char *p = NULL;
  *p = 100;

  return 0;
}

运行结果:

我们知道一个空指针不能被解引用,也就是说不能对其指向的内存位置赋值。所以这里运行完提示发生了段错误。

我们再来看一个例子:

int main()
{
   int a = 10;
   a /= 0;

  return 0;
}

运行结果:

【Linux进程】进程控制_第20张图片

我们知道除数不能为0的,这里提示浮点异常。 

通常我们的进程运行出现问题,我们会用kill -9信号来终止我们的进程。这和我们上面的俩个例子接受异常信号的原理是一样的。我们可以通过kill -l来查看进程的退出信号,找到11和8分别对应我们上面的退出信号。

【Linux进程】进程控制_第21张图片

下面我们来模拟一下通过这俩个进程结束信号来终止进程。

int main()
{
  while(1)
  {
      printf("hello Linux!: pid:%d\n", getpid());
      sleep(1);
  }

  return 0;
}

运行实例:

模拟浮点数异常:

【Linux进程】进程控制_第22张图片

模拟段错误:

【Linux进程】进程控制_第23张图片 我们可以看到,这俩个进程都是收到对应的退出信号才退出的。这也印证了我们什么说的进程出现异常,本质是我们的进程收到了对应的信号。

2.4 总结

  • main函数的返回值,本质表示:进程运行完成时是否是正确的结果,它又称为进程的退出码!0代表运行正确,如果不是正常退出,可以用不同的数字,表示不同的出错原因! 
  • 进程退出码只跟代码运行完毕,结果正确或者代码运行完毕,结果不正确这两种情况有关系,进程异常退出我们不再关心他的进程退出码,而是关心为什么会异常退出。
  • 只有在main函数中return才能起到退出进程的作用,在非main函数中return表示函数返回。
  • 任意地方调用exit或者_exit函数,都能够起到终止进程的作用,但是exit在终止进程前会完成一系列的收尾工作,但是 _exit不会。
  • 进程出现异常,本质是我们的进程收到了对应的信号。

三、进程等待

3.1 进程等待的概念

进程等待是父进程等待子进程退出时的一个过程。当子进程退出,如果父进程不管不顾,就可能使子进程成为僵尸进程,进而造成内存泄漏进程等待是为了避免产生僵尸进程,回收子进程资源,获取子进程退出信息。进程等待可以通过系统调用wait或waitpid两种方法来实现。

3.2 进程等待必要性

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

3.3 进程等待的方法

3.3.1 wait方法

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

 下面我们来通过一段程序来验证wait可以回收子进程的资源。

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
  6 
  7 int main()
  8 {
  9   pid_t id = fork();
 10   if(id < 0)
 11   {
 12     perror("fork");
 13     return 1;
 14   }
 15   else if(id == 0)
 16   {
 17     //child
 18     int cnt = 3;
 19     while(cnt)
 20     {
 21       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid    (),cnt);
 22       cnt--;                                
 23       sleep(1);                      
 24     }                      
 25                                         
 26     exit(0);           
 27   }
 28   else                                                             
 29   {                                                              
 30     // parent                                             
 31     int cnt = 8;                      
 32     while(cnt)                                                                 
 33     {                                         
 34       printf("I am a father process, pid:%d, ppid:%d,cnt%d\n",getpid(),getppid(    ),cnt);                         
 35       cnt--;          
 36       sleep(1);
 37     }
 38 
 39     pid_t ret = wait(NULL);
 40     if(ret == id)
 41     {
 42       printf("wait process, ret: %d\n",ret);
 43     }
 44 
 45     sleep(3);
 46   }
 47   return 0;
 48 }

我们通过一段监控脚本对进程进行实时监控

while :; do ps ajx | head -1 && ps ajx | grep testWait | grep -v grep; sleep 1;echo "---------------------------"; done

 运行结果:

【Linux进程】进程控制_第24张图片

通过运行结果我们可以看到,当子进程退出之后,父进程通过wait读取了子进程的退出信息,子进程资源被回收,不会变成僵尸进程。 

上面的情况是只有一个子进程,如果我们有多个子进程如何使用wait读取子进程退出信息呢?

我们来看下面这段代码:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
  6 
  7 #define N 10 
  8 
  9 void RunChild()
 10 {
 11   int cnt = 5;
 12   while(cnt)
 13   {
 14     printf("I am Child Process, pid:%d, ppid:%d\n",getpid(),getppid());
 15     sleep(1);
 16     cnt--;
 17   }
 18 }
 19 
 20 int main()
 21 {
 22   for(int i = 0;i < N; i++)
 23   {
 24     pid_t id = fork();
 25     if(id == 0)
 26     {
 27       RunChild();
 28       exit(0);
 29     }
 30     printf("create child process: %d success\n",id);//这句话只有父进程能够执行
 31   }
 32 
 33   sleep(10);
 34   //等待                                                                                        
 35   for(int i = 0; i < N;i++)
 36   {
 37     pid_t id = wait(NULL);
 38     if(id > 0)
 39     {
 40       printf("wait %d success\n", id);
 41     }
 42   }
 43   
 44   sleep(5);
 45 
 46   return 0;
 47 }

上面这段代码我们利用for循环创建了N个子进程,每个子进程执行RunChild函数,最后利用for循环进行循环等待,通过wait读取了所有子进程的退出信息,将所有子进程的资源回收。

运行结果:

【Linux进程】进程控制_第25张图片

 下面我们有一个疑问,如果上面这个过程子进程不退出,会出现上面情况呢?

我们将上面代码RunChild改成死循环。

【Linux进程】进程控制_第26张图片

运行发现如果子进程不退出,父进程也不退出,wait一直没有返回值。

也就是说,子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态! 

小结:

  • wait调用可以回收子进程的资源,防止内存泄漏的问题。
  • 如果子进程不退出,wait调用不进行返回,直到进程退出。——阻塞状态

3.3.2 waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果第三个参数options设置了选项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。
0:表示阻塞等待,父进程等待子进程期间不执行任何操作
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

【Linux进程】进程控制_第27张图片

下面我们先来用waitpid方法来回收子进程的资源:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
  6 
  7 int main()
  8 {
  9   pid_t id = fork();
 10   if(id < 0)
 11   {
 12     perror("fork");
 13     return 1;
 14   }
 15   else if(id == 0)
 16   {
 17     //child
 18     int cnt = 3;
 19     while(cnt)
 20     {
 21       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid    (),cnt);
 22       cnt--;                                
 23       sleep(1);                      
 24     }                      
 25                                         
 26     exit(0);           
 27   }
 28   else                                                             
 29   {                                                              
 30     // parent                                             
 31     int cnt = 8;                      
 32     while(cnt)                                                                 
 33     {                                         
 34       printf("I am a father process, pid:%d, ppid:%d,cnt%d\n",getpid(),getppid(    ),cnt);                         
 35       cnt--;          
 36       sleep(1);
 37     }
 38 
 39     pid_t ret = waitpid(id,NULL,0); 
 40     if(ret == id)
 41     {
 42       printf("wait process, ret: %d\n",ret);
 43     }
 44 
 45     sleep(3);
 46   }
 47   return 0;
 48 }

同样的对进程进行监控,运行结果: 

【Linux进程】进程控制_第28张图片

同样的,当子进程退出之后,父进程通过wait读取了子进程的退出信息,子进程资源被回收,不会变成僵尸进程。 

3.4 获取子进程status

【Linux进程】进程控制_第29张图片

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

我们前面讲过进程退出有三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止。

相应的,父进程等待的时候,期望获得子进程的哪些信息呢?

  1. 子进程代码是否异常?
  2. 没有异常,结果正确吗?这个通过进程退出码,可以知道是否正确,并且不同的退出码,表示不同的出错原因。

【Linux进程】进程控制_第30张图片

所以在status的低16bit位中,低7位表示终止信号,第8位表示core dump标志,高8位是进程退出码(只有进程正常退出时这个退出码才有意义)

进程若是正常终止的话,终止信号为0,我们需要获取高8位的内容,即退出码。进程若是非正常终止,我们便不需要再去获取高8位的内容了,因为获取了也没有意义。

方法一:我们可以通过位运算操作根据status来获得子进程的退出码与退出信号

exitsignal = status&0x7f; //退出信号 7F:0111 1111
exitcode = (status>>8)&0xff; //退出码 ff:1111 1111

下面我们用一段程序来用这种方法:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
 
 50 int main()
 51 {
 52   pid_t id = fork();
 53   if(id < 0)
 54   {
 55     perror("fork");
 56     return 1;
 57   }
 58   else if(id == 0)
 59   {
 60     //child
 61     int cnt = 3;
 62     while(cnt)                                                                                                                                                                         
 63     {
 64       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
 65       cnt--;
 66       sleep(1);
 67     }
 68 
 69     exit(1);//设置子进程的退出码为1
 70   }
 71   else
 72   {
 73     // parent
 74     int cnt = 8;
 75     while(cnt)
 76     {
 77       printf("I am a father process, pid:%d, ppid:%d,cnt%d\n",getpid(),getppid(),cnt);
 78       cnt--;
 79       sleep(1);
 80     }
 81     
 82     int status = 0;
 83     //pid_t ret = wait(NULL);
 84     pid_t ret = waitpid(id,&status,0);
 85     if(ret == id)
 86     {
 87       printf("wait process, ret: %d, exit sig: %d, exit code: %d\n",ret, status&0x7f, (status>>8)&0xff);
 88     }
 89 
 90     sleep(3);
 91   }
 92   return 0;
 93 }

运行结果:

 【Linux进程】进程控制_第31张图片

下面我们再来看异常退出的例子:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
 
 50 int main()
 51 {
 52   pid_t id = fork();
 53   if(id < 0)
 54   {
 55     perror("fork");
 56     return 1;
 57   }
 58   else if(id == 0)
 59   {
 60     //child
 61     int cnt = 3;
 62     while(cnt)                                                                                                                                                                         
 63     {
 64       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
 65       cnt--;
 66       sleep(1);
 67       int a = 10;
W> 68       a /= 0;
 69     }
 70            
 71     exit(11);
 72   }
 73   else
 74   {        
 75     // parent                                                                         
 76     int cnt = 8;
 77     while(cnt)
 78     {                                                                               
 79       printf("I am a father process, pid:%d, ppid:%d,cnt%d\n",getpid(),getppid(),cnt);
 80       cnt--; 
 81       sleep(1);
 82     }
 83     
 84     int status = 0;
 85     //pid_t ret = wait(NULL);
 86     pid_t ret = waitpid(id,&status,0);
 87     if(ret == id)
 88     {
 89       printf("wait process, ret: %d, exit sig: %d, exit code: %d\n",ret, status&0x7f,       (status>>8)&0xff);
 90     }
 91 
 92     sleep(3);
 93   }
 94   return 0;
 95 }

运行结果:

【Linux进程】进程控制_第32张图片

通过kill -l我们看到8号退出信号就是浮点数错误。 【Linux进程】进程控制_第33张图片

方法二:除了上面的方法外,我们还可以通过系统提供的两个宏来获取子进程的退出码和退出信号

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

下面我们同样来用一段程序来运行一下:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
 
 50 int main()
 51 {
 52   pid_t id = fork();
 53   if(id < 0)
 54   {
 55     perror("fork");
 56     return 1;
 57   }
 58   else if(id == 0)
 59   {
 60     //child
 61     int cnt = 3;
 62     while(cnt)
 63     {
 64       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
 65       cnt--;
 66       sleep(1);
 67      // int a = 10;
 68      // a /= 0;
 69     }
 70                                                                                                                                       
 71     exit(11);
 72   }
 73   else
 74   {
 75     // parent
 76     int cnt = 8;
 77     while(cnt)
 78     {
 79       printf("I am a father process, pid:%d, ppid:%d,cnt%d\n",getpid(),getppid(),cnt);
 80       cnt--;
 81       sleep(1);
 82     }
 83     
 84     int status = 0;
 85     //pid_t ret = wait(NULL);
 86     pid_t ret = waitpid(id,&status,0);
 87     if(ret == id)
 88     {
 89      // printf("wait process, ret: %d, exit sig: %d, exit code: %d\n",ret, status&0x7f, (status>>8)&0xff);
 90       if(WIFEXITED(status))
 91       {
 92         printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
 93       }
 94       else
 95       {
 96         printf("进程异常退出了\n");
 97       }
 98     }
 99     //走到这说明waitpid调用失败
100     else
101     {
102       printf("wait fail\n");
103     }
104 
105     sleep(3);
106   }
107   return 0;
108 }

运行结果:

【Linux进程】进程控制_第34张图片

再来看一段异常的例子,将上面代码进行如下修改:

【Linux进程】进程控制_第35张图片

运行结果:

 【Linux进程】进程控制_第36张图片

 3.5 非阻塞轮询

阻塞的本质: 其实是父进程的PCB被放入到了等待队列,并将父进程的状态改为S状态,这段时间内父进程不可被CPU调度。

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

在waitpid调用中,我们还有第三个参数options:

  • 0:表示阻塞等待:父进程等待子进程期间不执行任何操作 
  • WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

WNOHANG的模式就是非阻塞轮询:父进程会不断检测子进程的退出状态,子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息。这种轮询方式可以提高程序的效率和响应速度。

阻塞等待示例:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
 
 50 int main()
 51 {
 52   pid_t id = fork();
 53   if(id < 0)
 54   {
 55     perror("fork");
 56     return 1;
 57   }
 58   else if(id == 0)
 59   {
 60     //child
 61     int cnt = 3;
 62     while(cnt)
 63     {
 64       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
 65       cnt--;
 66       sleep(1);
 67      // int a = 10;
 68      // a /= 0;
 69     }
 70                                                                                                                                       
 71     exit(11);
 72   }
 73   else
 74   {
 75     // parent
 76     printf("father wait begin\n");
 84     int status = 0;
 85     //pid_t ret = wait(NULL);
 86     pid_t ret = waitpid(id,&status,0);
 87     if(ret == id)
 88     {
 89      // printf("wait process, ret: %d, exit sig: %d, exit code: %d\n",ret, status&0x7f, (status>>8)&0xff);
 90       if(WIFEXITED(status))
 91       {
 92         printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
 93       }
 94       else
 95       {
 96         printf("进程异常退出了\n");
 97       }
 98     }
 99     //走到这说明waitpid调用失败
100     else
101     {
102       printf("wait fail\n");
103     }
104 
105     sleep(3);
106   }
107   return 0;
108 }

运行结果:

【Linux进程】进程控制_第37张图片

父进程一直在等待子进程的退出,并且在等待期间不执行任何操作。 

非阻塞轮询示例:

  1 #include
  2 #include
  3 #include
  4 #include
  5 #include 

 50 int main()
 51 {
 52   pid_t id = fork();
 53   if(id < 0)
 54   {
 55     perror("fork");
 56     return 1;
 57   }
 58   else if(id == 0)
 59   {
 60     //child
 61     int cnt = 3;
 62     while(cnt)
 63     {
 64       printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
 65       cnt--;
 66       sleep(1);
 67      // int a = 10;
 68      // a /= 0;
 69     }
 70 
 71     exit(11);
 72   }
 73   else                                                                                                                                
 74   {
 83     printf("father wait begin\n");
 84     int status = 0;
 87     while(1)
 88     {
 89       pid_t ret = waitpid(id,&status,WNOHANG);
 90       if(ret > 0)                                                                                                                     
 91       {
 92        // printf("wait process, ret: %d, exit sig: %d, exit code: %d\n",ret, status&0x7f, (status>>8)&0xff);
 93         if(WIFEXITED(status))
 94         {
 95           printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
 96           break;
 97         }
 98         else
 99         {
100           printf("进程异常退出了\n");
101           break;
102         }
103       }
104       //走到这说明waitpid调用失败
105       else if(ret < 0)
106       {
107         printf("wait fail\n");
108       }
109       else
110       {
111         //ret == 0
112         printf("你好了没?子进程还没有退出,我再做点自己的事情...\n");
113         sleep(1);
114       }
115 
116      }  
117    }
118   return 0;
119 }

运行结果:

【Linux进程】进程控制_第38张图片

父进程会不断检测子进程的退出状态,子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息。

下面我们再来模拟一段非阻塞轮询示例:

#include
#include
#include
#include
#include

#define N 10 

#define TASK_NUM 10

typedef void(*task_t)();//这一行定义了一个新的类型task_t。task_t是一个函数指针类型,它指向一个没有参数并且返回void的函数。
task_t tasks[TASK_NUM];

void task1()
{
    printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}

void task2()
{
    printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
}
                                                                                                                          
void task3()
{
    printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}

int AddTask(task_t t);

// 任务的管理代码
void InitTask()
{
    for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
    AddTask(task1);
    AddTask(task2);
    AddTask(task3);
}

int AddTask(task_t t)
{
    int pos = 0;
    for(; pos < TASK_NUM; pos++) {
        if(!tasks[pos]) break;
    }
    if(pos == TASK_NUM) return -1;
    tasks[pos] = t;
    return 0;
}

void DelTask()
{}

void CheckTask()
{}

void UpdateTask()
{}

void ExecuteTask()
{
    for(int i = 0; i < TASK_NUM; i++)
    {
        if(!tasks[i]) continue;
        tasks[i]();
    }
}

int main()
{
  pid_t id = fork();
  if(id < 0)                                                                           
  {
    perror("fork");
    return 1;
  }
  else if(id == 0)
  {
    //child
    int cnt = 5;
    while(cnt)
    {
      printf("I am a child process, pid:%d, ppid:%d, cnt:%d\n",getpid(),getppid(),cnt);
      cnt--;
      sleep(1);
    }

    exit(11);
  }
  else
  {

    printf("father wait begin\n");
    int status = 0;
    InitTask();//初始化

    while(1)
    {
      pid_t ret = waitpid(id,&status,WNOHANG);
      if(ret > 0)
      {

        if(WIFEXITED(status))
        {
          printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
          break;
        }
        else
        {
          printf("进程异常退出了\n");
          break;
        }
      }
      //走到这说明waitpid调用失败
      else if(ret < 0)
      {
        printf("wait fail\n");
      }
      else//父进程非阻塞轮询期间要做的自己事情都放在这一段代码块
      {
         //ret == 0
         // printf("你好了没?子进程还没有退出,我再做点自己的事情...\n");
         // sleep(1);
         ExecuteTask();
         usleep(500000);
       }
 
     }  
    }
   return 0;
 }
}

运行结果:

【Linux进程】进程控制_第39张图片

注意:非阻塞轮询的时候我们的主要任务还是等待子进程退出,所以我们必须要确保轻量级任务:在非阻塞轮询的情况下,父进程应该只执行轻量级的事情,避免执行耗时较长的任务。这样可以确保轮询的效率和响应速度,避免阻塞轮询。 

四、进程程序替换

4.1 替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec替换函数以执行另一个程序

 我们先来看一段简单的程序替换示例:

#include   
#include 
#include 
#include 
#include                                                        
                                                                            
int main()                                                                  
{                                                                           
  printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  // 这类方法的标准写法                                                    
  execl("/usr/bin/ls", "ls", "-a", "-l", NULL);                            
  printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
           
  return 0;
}

运行结果: 

【Linux进程】进程控制_第40张图片

我们发现我们的可执行程序竟然将系统的指令封装起来,在运行的时候调用了ls-al的指令。我们还发现一个问题,在我们调用完execl进行程序替换后之后,我们程序的后续代码并没有被执行。这也说明了该进程的代码和数据替换了。

所以只有替换失败才可能后续代码。exec*函数,只有失败返回值,没有成功返回值。

我们再来看一段程序,用子进程进行执行execl替换函数:

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);
    // 这类方法的标准写法
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    exit(1);
   }

  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
                                                                                   
  return 0;
}

运行结果:

【Linux进程】进程控制_第41张图片

我们发现子进程进行程序替换后,子进程exec后续的代码不再运行,父进程的代码和数据不会受影响。并且exec前后进程的id并未改变。

原因是在子进程创建时,在子进程刚被创建的时候父子进程代码和数据都是共享的,由于程序替换的本质就是把程序的代码和数据加载到特定进程的上下文中。因此当子进程进行程序替换时,就意味着子进程对代码和数据要进行写入操作,又因为进程之间是具有独立性的,所以这个时候会发生写时拷贝,通过页表将子进程的代码和数据映射到新的物理内存上。所以子进程进行程序替换后,并不会影响父进程的代码和数据。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换, 从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

【Linux进程】进程控制_第42张图片

小结两个问题: 

1.当进行进程程序替换时,有没有创建任何新的进程呢?

没有创建新的进程。进程程序替换后,该进程的PCB、PID、进程地址空间以及页表等数据结构都没有发生改变,只是替换了当前进程的代码和数据,因此进行进程程序替换时并没有创建新的进程。

2.子进程进行程序替换后,会影响父进程的代码和数据吗?

不会,在子进程刚被创建的时候父子进程代码和数据都是共享的,当子进程进行程序替换时,就意味着子进程对代码和数据要进行写入操作,又因为进程之间是具有独立性的,所以会发生写时拷贝,通过页表将子进程的代码和数据映射到新的物理内存上。所以子进程进行程序替换后,并不会影响父进程的代码和数据。

4.2 替换函数

其实有六种以exec开头的函数,统称exec函数:

 【Linux进程】进程控制_第43张图片

 4.2.1函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

4.2.2 命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

【Linux进程】进程控制_第44张图片

4.2.3 exec调用举例如下

  • int execl(const char *path, const char *arg, …);

第一个参数表示要执行的目标程序的全路径,第二个参数表示你要如何执行这个程序(要执行的目标程序在命令上怎么执行,这里的参数就怎么一个一个的传递进去),最后必须以NULL作为参数传递的结束!!!

  我们先来看一段简单的程序替换示例:

#include   
#include 
#include 
#include 
#include                                                        
                                                                            
int main()                                                                  
{                                                                           
  printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  // 这类方法的标准写法                                                    
  execl("/usr/bin/ls", "ls", "-a", "-l", NULL);                            
  printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
           
  return 0;
}

运行结果: 

【Linux进程】进程控制_第45张图片

可以看到exexl后面的printf语句并没有输出。

  • int execlp(const char *file, const char *arg, …);

前面说到这里的p代表PATH,exexlp会自己在环境变量中查找,因此我们只需要传系统命令的名称(系统命令就是可执行程序),第二个参数表示你要如何执行这个程序(要执行的目标程序在命令上怎么执行,这里的参数就怎么一个一个的传递进去),最后必须以NULL作为参数传递的结束!

#include   
#include 
#include 
#include 
#include                                                        
                                                                            
int main()                                                                  
{                                                                           
  printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  // 这类方法的标准写法                                                    
  execl("ls", "ls", "-a", "-l", NULL);                            
  printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
           
  return 0;
}

 运行结果:

 【Linux进程】进程控制_第46张图片

  • int execv(const char *path, char *const argv[]);

第一个参数表示要执行的目标程序的全路径,第二个参数是一个指针数组,数组的内容表示你要如何执行这个程序,数组以NULL结尾

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    char *const myargv[] = {
     "ls",
     "-l",
     "-a",
     NULL
    };
    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);
    //这类方法的标准写法
    execv("/usr/bin/ls", myargv);
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }                                                                               

  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);

  return 0;
}

 运行结果:

【Linux进程】进程控制_第47张图片

  • int execvp(const char *file, char *const argv[]);

前面说到这里的p代表PATH,execvp会自己在环境变量中查找,第一个参数我们只需要传要执行程序的名称,第二个参数是一个指针数组,数组的内容表示你要如何执行这个程序,数组以NULL结尾。

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    char *const myargv[] = {
     "ls",
     "-l",
     "-a",
     NULL
    };
    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);
    //这类方法的标准写法
    execvp("ls", myargv);                                                         
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }

  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);

  return 0;
}

运行结果:

【Linux进程】进程控制_第48张图片

前面列举了execl、execlp、execv、execvp的示例,剩下execle、execve两个还没列举。

讲后面这两个之前,我先来问一个问题,如果我们的exec*能够执行系统命令,能否执行我们自己的命令呢?

答案是可以的,下面我们来用execl实现直接我们自己实现的c++可执行程序、shell脚本以及python脚本

示例一:调用c++可执行程序

程序:

otherExe.cpp

#include 
using namespace std;

int main()
{
  cout << "hello exec*!" << endl;
  cout << "hello exec*!" << endl;
  cout << "hello exec*!" << endl;
  cout << "hello exec*!" << endl;
  cout << "hello exec*!" << endl;
  cout << "hello exec*!" << endl;
  cout << "hello exec*!" << endl;
  return 0;
}

 mycommand.c

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t id = fork();

  if(id == 0)
  {
    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);
    execl("./otherExe", "otherExe", NULL);//这里第二个参数不需要./ ,第一个参数已经告诉操作系统这个可执行程序的位置了。
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }

  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);

  return 0;
}

makefile:

【Linux进程】进程控制_第49张图片

注意makefile如果我们之间写生成两个可执行文件,运行自顶向下扫描会只生成第一个扫描到的可执行程序。因此我们这里要加上.PHONY:all;all:otherExe mycommand:这一行指定了all伪目标依赖于otherExe和mycommand目标。这个时候,当你运行make时,它会构建otherExe和mycommand。 空行表示没有依赖方法。这样make的时候我们才能同时生成两个可执行程序。

运行结果

【Linux进程】进程控制_第50张图片

我们看到./mycommmand调用了我们自己创建的otherExe可执行程序。

示例二:调用shell脚本

test.sh

#!/usr/bin/bash

function myfun()
{
    cnt=1
    while [ $cnt -le 10 ]
    do
        echo "hello $cnt"
        let cnt++
    done
}

echo "hello 1"
echo "hello 1"
echo "hello 1"
echo "hello 1"
echo "hello 1"
echo "hello 1"
echo "hello 1"
ls -a -l
myfun

第一行表示告诉系统使用/usr/bin/bash解释器来执行这个脚本。

 mycommand.c

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t id = fork();

  if(id == 0)
  {

    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);
    execl("/usr/bin/bash", "bash", "test.sh", NULL);                              
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }

  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);

  return 0;
}

运行结果:

【Linux进程】进程控制_第51张图片

我们看到我们将shell脚本的内容全部输出来了。

示例三:调用python脚本

test.py:

#!/usr/bin/python3     
                       
print("hello Python!") 

mycommand.c

#include 
#include 
#include 
#include 
#include 

int main()
{
  pid_t id = fork();

  if(id == 0)
  {

    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);
    execl("/usr/bin/python3", "python3", "test.py", NULL);                           
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }

  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);

  return 0;
}

运行结果:

小结:无论是我们的可执行程序,还是脚本,为什么能跨语言调用呢??所有语言运行起来,本质都是进程!!!只要是进程,就可以被exec*调用。

那我们如果将一个程序生成的环境变量,导给另一个程序?下面我们来介绍给子进程传递环境变量的两种方法。

  • int execvp(const char *file, char *const argv[]);

第一个参数表示你要执行程序的名称,第二个参数是一个指针数组,数组的内容表示你要如何执行这个程序,数组以NULL结尾。 

方法一:新增环境变量

  • 先看看父进程的地址空间直接putenv
#include 
#include 
#include 
#include 
#include 

int main()
{
  putenv("PRIVATE_ENV=666666");
  pid_t id = fork();
  if(id == 0)
  {
     char*const myargv[] = {
        "otherExe",
        "-a",
        "-b",
        "-c",
        NULL
     };

     printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
     sleep(3);
     execv("./otherExe", myargv);
     printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
   }
 
   pid_t ret = waitpid(id, NULL, 0);
   if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
   
   return 0;
 }

运行结果:

【Linux进程】进程控制_第52张图片

我们发现这段代码中,我们并没有将父进程的环境变量传递给子进程,但是我们却可以将父进程的环境变量打印出来。这是因为环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承下去了!!!(extern char** environ)所以,程序替换中,环境变量信息,不会被替换。

  • 也可以调用execle将父进程的环境变量environ传递给子进程
#include 
#include 
#include 
#include 
#include 

int main()
{
  putenv("PRIVATE_ENV=666666");
  pid_t id = fork();
  if(id == 0)
  {
     printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
     sleep(3);
     execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, environ);
     printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
   }
 
   pid_t ret = waitpid(id, NULL, 0);
   if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
   
   return 0;
 }

运行结果同上。 

注意execl这种的方式环境变量是自定义的,只不过我们自定义的环境变量是从父进程来的。我们再看下面的彻底替换的方法就可以感受到。

方法二:覆盖父进程的环境变量

#include 
#include 
#include 
#include 
#include 

int main()
{
  extern char **environ;
  putenv("PRIVATE_ENV=666666");
  pid_t id = fork();
  if(id == 0)
  {
    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);

    char *const myenv[] = {
      "MYVAL=1111",
      "MYPATH=/usr/bin/XXX",
      NULL
    };

    execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, myenv);
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }
    
  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
  
  return 0;
}

运行结果:

【Linux进程】进程控制_第53张图片

从运行结果我们看到,这里的环境变量只有子进程的环境变量,说明execle最后传递的环境变量参数,采用的策略是覆盖,而不是追加! 也就是父进程的环境变量不要了,用我们自己的环境变量。

  • int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数表示要执行的目标程序的全路径,第二个参数是一个指针数组,数组的内容表示你要如何执行这个程序,数组以NULL结尾。第三个参数是你自己设置的环境变量。

示例:

#include 
#include 
#include 
#include 
#include 

int main()
{
  extern char **environ;
  putenv("PRIVATE_ENV=666666");
  pid_t id = fork();
  if(id == 0)
  {
    printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
    sleep(3);

    char *const myargv[] = {                                                      
     "otherExe",
     "-a",
     "-b",
     "-c",
     NULL
    };
    char *const myenv[] = {
      "MYVAL=1111",
      "MYPATH=/usr/bin/XXX",
      NULL
    };

    execlev("./otherExe", myargv, myenv);
    printf("after: I am a process, pid: %d, ppid:%d\n", getpid(), getppid());
  }
    
  pid_t ret = waitpid(id, NULL, 0);
  if(ret > 0) printf("wait success, father pid: %d, ret id: %d\n", getpid(), ret);
  
  return 0;
}

运行结果:

【Linux进程】进程控制_第54张图片

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。我们之前说过库函数和系统调用是上下层关系,这也就是说其他五个函数实际上都是操作系统对系统调用execve进行了封装,从而满足不同用户的不同调用场景。这些函数之间的关系如下图所示。 

【Linux进程】进程控制_第55张图片

五、实现一个简易的shell 

考虑下面这个与shell典型的互动:

[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

【Linux进程】进程控制_第56张图片

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 内建命令的处理
  4. 正常命令的处理—建立一个子进程(fork)
  5. 替换子进程(execvp) 
  6. 父进程等待子进程退出(wait)

根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。

实现代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

int lastcode = 0;
int quit = 0;
char commandline[LINE_SIZE];//存放命令行参数
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];

// 自定义环境变量表
char myenv[LINE_SIZE];

const char *getusername()//获取用户名
{
  return getenv("USER");
}

const char *gethostname()//获取主机名           
{
  return getenv("HOSTNAME");
}

void getpwd()//获取当前路径
{
  getcwd(pwd, sizeof(pwd));
}                                                                                            
//获取命令行字符
void Interact(char *cline,int size)
{ 
  getpwd();
  printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
  char *s = fgets(cline, size, stdin);//用fgets从标准输入流获取命令行并存放到commanline数组中
  assert(s);
  (void)s;

  cline[strlen(cline)-1] = '\0';//把输入字符串后的回车(\n)去掉,改成\0
}

//解析命令行字符
int splitstring(char cline[], char *_argv[])
{
  int i = 0;
  _argv[i++] = strtok(cline, DELIM);
  while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
  return i - 1;
}

void NormalExcute(char* _argv[])
{

    pid_t id = fork();
    if(id < 0)
    {
      perror("fork");
      return;
    }
    else if(id == 0)
    {
      //子进程执行命令
      execvp(_argv[0], _argv);
      exit(EXIT_CODE);
    }                                                                     
    else{
      int status = 0;
      pid_t rid = waitpid(id, &status, 0);
      if(rid == id)
      {
        lastcode = WEXITSTATUS(status);//获取进程退出码
      }
    }
}

//内建命令处理
int buildCommand(char *_argv[], int _argc)
{
    //cd目录
    if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
      chdir(argv[1]);
      getpwd();
      sprintf(getenv("PWD"), "%s", pwd);
      return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0){//新增环境变量
      strcpy(myenv, _argv[1]);
      putenv(myenv);
      return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
        if(strcmp(_argv[1], "$?") == 0)//打印进程退出码
        {
            printf("%d\n", lastcode);
            lastcode=0;
        }
        else if(*_argv[1] == '$'){//打印环境变量
            char *val = getenv(_argv[1]+1);
            if(val) printf("%s\n", val);
        }
        else{//正常打印字符串
            printf("%s\n", _argv[1]);      
        }
                                                                 
      return 1;
    }

    // 特殊处理一下ls
    if(strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }

    return 0;
}

int main()
{
  while(!quit)//bash不退就可以一直输入命令
  {
    //1.
    //2.交互问题,获取命令行
    Interact(commandline,sizeof(commandline));
    //commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l"
    //3.子串分割问题,解析命令行
    int argc = splitstring(commandline,argv);
    if(argc == 0) continue;
    
    //4.指令的判断
    //debug
    //for(int i = 0;argv[i];++i) printf("[%d]: %s\n",i,argv[i]);
    //内键命令,本质就是一个shell内部的一个函数
    int n = buildCommand(argv, argc);

    //5.普通命令的执行
    if(!n) NormalExcute(argv);//n为0,说明不是内建命令,执行普通命令
  }
   
  return 0;
}

代码演示:

【Linux进程】进程控制_第57张图片

【Linux进程】进程控制_第58张图片

所以,当我们登陆的时候,系统就是启动一个shell进程。

我们shell本身的环境变量是从哪里来的?

当用户登陆的时候,shell会读取用户目录下的.bash_profile文件,里面保存了导入环境变量的方式。

你可能感兴趣的:(Linux,linux,开发语言,c语言)