从零开始学Linux进程控制:fork、wait、exec 详解

1:进程创建

1.1:fork函数初识

1.2:fork函数返回值

1.3:fork函数常规用法

1.4:fork函数调用失败原因

2:进程终止

2.1:退出码

代码1

代码2

2.2:异常

代码1

代码2

2.3:进程常见退出方法

2.3.1:main函数return

2.3.2:exit函数

2.3.3:_exit函数

2.3.4:return、exit与_exit之间的区别与联系

3:进程等待

3.1:进程等待的方法

3.1.1:wait方法

代码1

代码2

3.1.2:waitpid方法

代码1

代码2(等待指定进程退出)

代码3

3.2:获取进程status

3.3:阻塞等待与非阻塞等待

代码1(子进程正常退出)

代码2(子进程非正常退出)

3.3.1:阻塞等待

3.3.2:非阻塞等待

3.3.3:基于非阻塞接口的轮询检测方案

4:进程程序替换

代码1

替换原理

代码2

4.1:替换函数

函数解释

命名理解

4.1.1:execl函数

4.1.2:execlp函数

4.1.3:execv函数

4.1.4:execvp函数

4.1.5:execvpe函数

4.2:小结


1:进程创建

1:进程 = 内核相关的数据结构(task_struct + mm_struct + 页表) + 代码 + 数据.

2:创建子进程会经过以下步骤.

  1. 分配新的内存块和内核数据结构给子进程.
  2. 将父进程部分数据结构内容拷贝给子进程 (子进程要继承于父进程).
  3. 添加子进程到系统的进程列表中
  4. 代码:子进程与父进程共享代码
  5. 数据:则通过写时拷贝的方式

如果理解进程具有独立性

  • 根本原因在于:进程 = 内核的相关管理数据结构(task_struct + mm_struct(地址空间) + 页表) + 代码 + 数据.子进程有自己的task_struct + mm_struct(地址空间) + 页表.
  • 代码虽然与父进程共享,但是与父进程之间互不影响.
  • 而数据是通过写时拷贝的方式进行复用.
  • 因此无论从内核的相关管理数据结构还是从代码以及数据,它都是独立的.

1.1:fork函数初识

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

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

  1. 分配新的内存块和内核数据结构给子进程.
  2. 将父进程部分数据结构内容拷贝至子进程.
  3. 添加子进程到系统进程列表中.
  4. fork返回,开始调度器调度.
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    printf("Before: pid is %d\n", getpid());
    if((pid = fork()) == -1)
    {
        perror("fork() fail");
        exit(-1);      
    }
    printf("After:pid is %d, fork return %d\n", getpid(), pid);
    return 0;
}

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

  • fork之前父进程独立执行,而fork之后父子两个执行流分别执行.

PS:fork之后,父进程和子进程谁先执行完全由调度器决定.

1.2:fork函数返回值

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

一个父进程可以创建多个子进程,而一个子进程只能有一个父进程.因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其完成任务的,父进程只有知道了子进程的pid才能更加有针对性地去给子进程指定任务.

为什么fork函数有两个返回值

父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的地址空间、创建子进程对应的页表等等.子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表中,此时子进程便创建完毕了

那么也就是说,在fork函数内部执行return语句以前,子进程就已经创建完毕了,那么之后的return语句不仅需要父进程执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因.

1.3:写时拷贝
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝.

1:为什么数据要进行写时拷贝?

进程具有独立性.多个进程在运行时,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的改变影响到父进程.

2:为什么不在创建子进程的时候进行写时拷贝.

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,那么应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间,避免空间的浪费.

3:代码会不会进行写时拷贝?

90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝.

1.3:fork函数常规用法

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

1.4:fork函数调用失败原因

fork函数创建子进程也可能会失败,有以下两种情况:

  1. 系统中有太多的进程,内存空间不足,子进程创建失败。
  2. 实际用户的进程数超过了限制,子进程创建失败

2:进程终止

进程 = 内核相关的数据结构(task_struct + mm_struct + 页表) + 代码 + 数据.

创建进程时,会先创建进程的内核的相关管理数据结构.

那么终止是在做什么呢

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

那么进程终止存在以下三种情况

  1. 代码跑完,结果正确.
  2. 代码跑完,结果不正确.
  3. 代码执行的时候,出现了异常,提前退出了.
  • 第一种情况和第二种情况可以通过进程的退出码来决定.
  • 第三种情况,得通过OS发给进程的信号来确定.

2.1:退出码

  • 我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的.
  • 既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因.

代码1

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  printf("I am process and My pid =%d,ppid == %d\n",getpid(),getppid());
  sleep(1);
  return 99;
}

当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用**echo $?**命令查看最近一次进程退出的退出码信息.

  • 退出码存在的意义就是在于:告诉关心方,我将任务完成得怎么样了.
  • 退出码为0则表示成功,为非0表示失败.(博主使用99是为了方便uu们查看).

代码2

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>

int main()
{
  for (int errorcode = 0; errorcode <= 255; errorcode++)
  {
    printf("errorcode == %d:%s\n",errorcode,strerror(errorcode));
  }
  return 0;
}
  • 1,2,3,4,5,6.....不同的非0值,一方面表示失败,另一方面表示失败的原因.

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

  • 要知道子进程的退出情况(成功,失败,以及失败的原因是什么)------>核心宗旨是为用户负责.

2.2:异常

异常这里后面会有专门的部分讲解,这里首先uu们简单理解下就好

  • 可以想象一个场景,编程运行的时候,突然崩溃了,这个时候的根本原因是:发生了异常,操作系统系统发现进程做了不该做的事情,从而杀死了进程.
  • 那么为什么会出现异常,原因在于:进程出现了异常,本质是因为进程收到了OS发给进程的信号.
  • 一旦出现了异常,退出码就没有任何意义了.

代码1

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  while (1)
  {
    printf("I am process and My pid =%d,ppid == %d\n",getpid(),getppid());
    sleep(1);
  }
  return 0;
}

代码2

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  int * p = NULL;
  while (1)
  {
    printf("I am a Process And My pid == %d\n",getpid());
    *p = 100;
    sleep(2);
  }
  
  return 0;
}

怎么判断异常.

在进程退出的时候,查看进程的退出信号是多少,就可以判断进程为什么异常了.

如何判断进程终止.

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

总结:衡量一个进程退出,我们只需要两个数字:退出码与退出信号

2.3:进程常见退出方法

进程正常终止一般有三种

  1. 从main函数return**(非main函数return表示函数结束)**
  2. 调用exit.
  3. _exit.

2.3.1:main函数return

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{

  printf("I am process and My pid =%d,ppid == %d\n",getpid(),getppid());
  sleep(1);
  return 0;
}

2.3.2:exit函数

使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:

  1. 执行用户通过atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit函数终止进程。
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void Test()
{
  printf("hello world");
  exit(1);  
}

int main()
{
    Test();
    return 0;
}

上述代码使用exit终止进程前会将缓冲区当中的数据输出.

2.3.3:_exit函数

使用_exit函数退出进程的方法我们并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作.

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void Test()
{
  printf("hello world");
  _exit(1);  
}

int main()
{
    Test();
    return 0;
}

使用_exit终止进程,则缓冲区当中的数据将不会被输出。

2.3.4:return、exit与_exit之间的区别与联系

  • 只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程.
  • 而exit函数和_exit函数在代码中的任何地方使用都可起到退出进程的作用.
  • 使用exit函数退出进程前,exit函数会执行用户定义的清理函数,冲刷缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作.

return、exit、_exit之间的联系

  • 执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。
  • 使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。

3:进程等待

结论:任何子进程,在退出的前提下,一般必须要被父进程进行等待

进程在退出的时候,如果父进程不管不顾,那么此退出进程, 就会进入Z状态(僵尸进程)
为什么要有进程等待.

  1. 父进程通过等待,解决子进程退出的僵尸问题,回收系统资源**(一定要考虑的因素).**
  2. 获取子进程的退出信息,得知子进程是因为什么原因而退出.

进程等待的必要性.

  1. 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进程造成了内存泄漏.
  2. 进程一旦变成僵尸进程,那么就算是kill - 9命令也无法将其杀死,因为谁也无法杀死一个死去的进程.
  3. 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己安排给子进程的任务完成得如何.
  4. 父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息.

3.1:进程等待的方法

3.1.1:wait方法

  • 作用:等待任意子进程.
  • 返回值:等待成功返回被等待进程的pid,等待失败返回-1.
  • 参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL.
代码1
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(2);
      count--;
    }  
}

int main()
{
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(0);
   }  
   //父进程在等待
   pid_t rid = wait(NULL);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
   }
}
代码2
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(0);
   }  
   sleep(10);
   //父进程在等待
   pid_t rid = wait(NULL);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
   }
   sleep(3);
   printf("Father Process Quit\n");
}

3.1.2:waitpid方法

函数原型:pid_t waitpid(pid_t pid, int* status, int options);
返回值:

  1. 正常返回的时候waitpid返回收集到的子进程的进程pid
  2. 如果设置了选项WNOHANG,则调用中waitpid发现没有已退出的子进程可以收集,则返回0
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在.
    参数:

pid:

Pid = -1,等待任何一个子进程,与wait等效.

Pid > 0,等待其进程id与pid相等的子进程.

status(输出型参数,获取子进程的退出状态):

WIFEXITED(status):若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)

WEXITSTATUS(status):若WIFEXITED为非零,提取子进程退出码.(查看进程的退出码)

不关心则可设置为NULL.

options:

WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待.若正常结束,则返回该子进程的id.

代码1
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(0);
   }  
   //Pid = -1,等待任何一个子进程,与wait等效.
   pid_t rid = waitpid(-1,NULL,0);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
   }
}
代码2(等待指定进程退出)
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(0);
   }  
   //等待指定进程退出
   pid_t rid = waitpid(id,NULL,0);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
   }
   sleep(3);
   return 0;
}
代码3
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   int status = 0;
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(1);
   }  
   sleep(10);
   //等待指定进程退出,并且获取进程退出状态码
   pid_t rid = waitpid(id,&status,0);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
   }
   sleep(3);
   printf("Father Process Quit and status == %d",status);
   return 0;
}
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息.
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞.
  • 如果不存在该子进程,则立即出错返回~

3.2:获取进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充.
  • 如果传递NULL,表示不关心子进程的退出状态信息.
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程.
  • statu是一个整型变量,但status不能简单地当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下.

在status的低16比特位中,高8位表示进程的退出状态,即退出码.进程若是被信号所杀掉,则低7位表示终止信号,而第8位比特位是core dump标志.

我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。

cpp 复制代码
exitCode = (status >> 8) & 0xFF;//退出码
exitSignal = status & 0x7f;//退出信号

对于此,系统当中提供了两个宏来获取退出码和退出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
cpp 复制代码
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码

需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了.

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   int status = 0;
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(1);
   }  
   sleep(10);
   //等待指定进程退出,并且获取进程退出状态码
   pid_t rid = waitpid(id,&status,0);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
   }
   sleep(3);
   printf("Father Process Quit and status == %d,child quit code:%d,child quit signal:%d\n",status,(status >> 8) & 0xFF,status & 0x7F);
   return 0;
}

3.3:阻塞等待与非阻塞等待

在讲解阻塞等待与非阻塞等待前,我们先来看看子进程正常退出与非正常退出的情况

代码1(子进程正常退出)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   int status = 0;
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit....\n");
      _exit(123);
   }
   //父进程休眠7s
   sleep(7);
   //等待指定进程退出,并且获取进程退出状态码
   pid_t rid = waitpid(id,&status,0);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
      //wifexited判断子进程是否正常退出
      if(WIFEXITED(status))
      {
         //wexitstatus获取进程的退出码
         printf("Child Quit Success,Child exit Code == %d\n",WEXITSTATUS(status));
      }
      else
      {
         printf("Child Quit Unnormal\n");
      }
   }
   else
   {
      printf("Wait Faild\n");
   }
   sleep(3);
   return 0;
}

代码2(子进程非正常退出)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

void ChildRun()
{
   int * p = NULL;
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
      *p = 150;
    }  
}

int main()
{
   int status = 0;
   printf("I am Father process and my pid == %d,ppid == %d\n",getpid(),getppid());
   pid_t id = fork();
   if(id == 0)
   {
      ChildRun();
      printf("Child Quit....\n");
      _exit(123);
   }
   //父进程休眠7s
   sleep(7);
   //等待指定进程退出,并且获取进程退出状态码
   pid_t rid = waitpid(id,&status,0);
   if(rid > 0)
   {
      printf("Wait Success,rid == %d\n",rid);
      //wifexited判断子进程是否正常退出
      if(WIFEXITED(status))
      {
         //wexitstatus获取进程的退出码
         printf("Child Quit Success,Child exit Code == %d\n",WEXITSTATUS(status));
      }
      else
      {
         printf("Child Quit Unnormal\n");
      }
   }
   else
   {
      printf("Wait Faild\n");
   }
   sleep(3);
   return 0;
}

3.3.1:阻塞等待

当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情------>这种等待叫做阻塞等待.

3.3.2:非阻塞等待

父进程没有一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息------>非阻塞等待.

因此waitpid如果是以阻塞等待的方式的调用的话,本质是在检测子进程的状态.

3.3.3:基于非阻塞接口的轮询检测方案

向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没有结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回该子进程的pid。

例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以先去做一些其他事,过一段时间再调用waitpid函数读取子进程的退出信息。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
typedef void(*Fun_T)();
#define N 3
Fun_T task[N] = {NULL};
void PrintLog()
{
   printf("PrintLog\n");
}

void DownLoad()
{
   printf("DownLoad\n");
}

void MySQLDataSync()
{
   printf("MySQLDataSync\n");
}

//加载任务
void LoadTask()
{
   //函数名表示地址
   task[0] = PrintLog;
   task[1] = DownLoad;
   task[2] = MySQLDataSync;
}

void HandleTask()
{
   for (size_t i = 0; i < N; i++)
   {
      //拿到函数对应的地址,然后进行调用
      task[i]();
   }
   
}
//非阻塞轮询等待父进程做其他事情
void DoOtherThing()
{
   HandleTask();
}


void ChildRun()
{
    int count = 5;
    while(count)
    {
      printf("I am Child process and my pid == %d,ppid == %d\n",getpid(),getppid());
      sleep(1);
      count--;
    }  
}

int main()
{
   printf("I am Father process and my pid == %d,ppid ==%d\n",getpid(),getppid());
   pid_t id = fork();
   if(0 == id)
   {
      ChildRun();
      printf("Child Quit\n");
      exit(123);
   }

   LoadTask();

   //非阻塞轮询
   while (1)
   {
      int status = 0;
      //WNOHANG表示非阻塞等待
      pid_t Return_id = waitpid(id,&status,WNOHANG);
      //若pid指定的子进程没有结束,则waitpid函数返回0,不予以等待
      if (Return_id == 0)
      {
         sleep(1);
         printf("Child is Running,Please Check Next Time\n");
         DoOtherThing();         
      }
      //大于0表示等待成功
      else if(Return_id > 0)
      {
         printf("Wait Success,rid == %d\n",Return_id);
         //wifexited判断子进程是否正常退出
         if(WIFEXITED(status))
         {
            //wexitstatus获取进程的退出码
            printf("Child Quit Success,Child exit Code == %d\n",WEXITSTATUS(status));
         }
         else
         {
            printf("Child Quit Unnormal\n");
         }
         break;
      }
      else
      {
         printf("Wait Failed\n");
         break;
      }
   }
   
   return 0;
}

运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。

4:进程程序替换

在讲进程程序替换前,我们先看一段代码

代码1

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
   printf("Test Execl Begin\n");
   execl("/usr/bin/ls","ls","-l","-a",NULL);
     printf("Test Execl End\n");
   return 0;
}

我们可以清晰地看到,上述代码并未执行 printf("Test Execl End\n");这段代码,那么这是为什么呢?这就牵扯到exec*系列函数的原理了

替换原理

  • 进程 = 内核数据结构 + 代码 + 数据,而进程替换的本质是将进程的代码与数据进行了替换,站在被替换进程的角度:本质就是这个程序(在上述例子是ls指令)被加载到内存中.

那么程序为什么要加载到内存当中

  • 因为冯诺依曼体系结构规定代码和数据只能够被CPU访问,CPU只能够就近访问内存,不能够访问外设,因此必须将磁盘里的代码和数据加载到内存当中.

所以exec*系列的函数,执行完毕以后,后续的代码就不见了,因为旧进程代码和数据被exec*系列函数替换掉了,但是并没有替换旧进程的内核数据结构**(因此在执行exec*系列函数的过程中,没有创建新进程).**

代码2

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
   printf("Test Execl Begin\n");
   pid_t id = fork();
   if(id == 0)
   {
      sleep(3);
      execl("/usr/bin/ls","ls","-l","-a",NULL);
      exit(1);
   }

   int status = 0;
   //为0表示阻塞等待
   pid_t Rid = waitpid(id,&status,0);
   if (Rid > 0)
   {
      printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
   }
   
   printf("Test Execl End\n");
   return 0;
}

我们可以看到,退出码是0而不是0,这是因为我们创建了子进程,子进程执行了execl函数,进行了程序替换,以至于没有执行execl函数后面的代码.

  • 创建子进程,目的是为了让子进程完成任务.但完成任务的方式有两种.
  1. 让子进程执行父进程代码的一部分.
  2. 让子进程执行一个全新的程序(程序替换).
  • 父子进程之间要保证各自进程的独立性,默认不修改的情况下,父子进程共享代码和数据.
  • 一旦要让子进程执行一个全新的程序,不仅仅是将数据进行了写时拷贝,然后将新程序的数据对其进行覆盖,同样也将代码进行了写时拷贝,并且将新程序的代码对其进行进行了覆盖,并且通过页表重新建立映射关系.

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec*系列函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec*系列函数并不创建新进程,所以调用exec前后该进程的id并未改变.
当进行进程程序替换时,是否创建新的进程?

进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有发生改变.

子进程进行进程程序替换后,是否影响父进程的代码和数据?

子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

4.1:替换函数

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

cpp 复制代码
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
//其中...表示可变参数

函数解释

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

命名理解

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

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

4.1.1:execl函数

cpp 复制代码
#include <unistd.h>
#include <stdio.h>

int main()
{
    printf("Test_Execl Begin\n");
    execl("/usr/bin/ls","ls","-l","-a",NULL);
    printf("Test_Execl End\n");
    return 0;
}

4.1.2:execlp函数

4.1.3:execv函数

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
   printf("Test Execv Begin\n");
   pid_t id = fork();
   if(id == 0)
   {
      sleep(3);
      //与execl函数一样,以NULL结尾
      char * const argv[] = {"ls","-l","-a",NULL};
      execv("/usr/bin/ls",argv);
      exit(1);
   }

   int status = 0;
   //为0表示阻塞等待
   pid_t Rid = waitpid(id,&status,0);
   if (Rid > 0)
   {
      printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
   }
   
   printf("Test Execv End\n");
   return 0;
}

4.1.4:execvp函数

cpp 复制代码
​
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
   printf("Test Execvp Begin\n");
   pid_t id = fork();
   if(id == 0)
   {
      sleep(3);
      char * const argv[] = {"ls","-l","-a",NULL};
      execvp("ls",argv);
      exit(1);
   }

   int status = 0;
   //为0表示阻塞等待
   pid_t Rid = waitpid(id,&status,0);
   if (Rid > 0)
   {
      printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
   }
   
   printf("Test Execvp End\n");
   return 0;
}

​

4.1.5:execvpe函数

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
   printf("Test Execvpe Begin\n");
   pid_t id = fork();
   if(id == 0)
   {
      sleep(3);
      //与execl函数一样,以NULL结尾
      char * const argv[] = {"Cpp_Program.exe",NULL};
      char * const envp[] = {"LiuJunhao = No.1","LuoAiTao = No.2",NULL};
      execvpe("./Cpp_Program.exe",argv,envp);
      exit(1);
   }

   int status = 0;
   //为0表示阻塞等待
   pid_t Rid = waitpid(id,&status,0);
   if (Rid > 0)
   {
      printf("Father Wait Success,Child Exit Code:%d\n",WEXITSTATUS(status));
   }
   printf("Test Execvpe End\n");
   return 0;
}
cpp 复制代码
#include <iostream>
#include <unistd.h>
using namespace std;

int main(int argc,char * argv[],char* env[])
{
    int i = 0;
    //命令行参数表默认以NULL结尾
    for(i = 0; argv[i]; i++)
    {
        printf("argv[%d]:>%s\n",i,argv[i]);
    }
    printf("-----------------------------\n");
    //环境变量参数表也默认以NULL结尾
    for(i = 0; env[i]; i++)
    {
        printf("env[%d]:>%s\n",i,env[i]);
    }
    printf("-----------------------------\n");
    cout << "hello I am C++ Program and my pid == "<<getpid()<<endl;
    cout << "hello I am C++ Program and my pid == "<<getpid()<<endl;
    return 0;
}

4.2:小结

  • 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
相关推荐
陈希12330 分钟前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡1 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
@兽血沸腾2 小时前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
且从容.5 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道5 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
讯捷蓝达7 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.8 小时前
ubuntu安装flink
linux·ubuntu·flink
数字新视界9 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落9 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨9 小时前
WSL2的安装与使用
linux·wsl