Linux 进程控制

发布于:2025-02-10 ⋅ 阅读:(62) ⋅ 点赞:(0)

目录

一、进程创建

1.fork函数初识 

二、进程终止

1.main函数的返回值

 2.进程退出的三种情况

3.进程退出码

4.return、exit、_exit的区别

5、站在 OS 角度:理解进程终止 

 三、进程等待

1.我们先讲第二个问题,为什么要进程等待

2、如何进程等待

wait函数

 waitpid函数

获取子进程的退出码 

获取子进程的终止信号 

判断进程是否被正常终止

 options 参数

补充:内核源码中的退出码和终止信号

 四、进程替换

1.什么是进程替换

2.单进程版的程序替换

3.程序替换的原理

4.多进程版的程序替换

 5.各种exec的接口

(1)execl 函数 

(2)execv 函数 

(3)execlp 函数

(4)execle和execvp 函数

(5)新增环境变量

(6) 彻底替换环境变量


一、进程创建

目前学习到的进程创建的两种方式:

  1. 命令行启动命令(程序、指令等) 。
  2. 通过程序自身,调用 fork 函数创建出子进程。

1.fork函数初 

linux fork 函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

前段时间已经为大家介绍过fork以及fork的核心问题:

  •  两个返回值问题
  • 一个变量为什么会存在两个值呢? 
  • 为什么在父进程中返回子进程的 pid,在子进程中返回的是 0 呢?

如下面这个链接!

fork初识

二、进程终止

1.main函数的返回值

我们平时在main函数写代码的时候,写到最后return 0,return 0是什么东西呢?

其实在main函数里,return是退出这个进程,0就是这个进程的退出码。

 #include<stdio.h>
  2 int main()
  3 {
  4   return 0                                                                                                                                   
  5 }

退出码是给操作系统看的,看这个进程的退出状态是怎样的。

用户可以输入echo $命令查看最近一次可执行程序的退出码。

 2.进程退出的三种情况

  • 运行完毕,结果正确。
  • 运行完毕,结果不正确。
  • 运行异常,提前结束。

3.进程退出码

我们平常写的可执行程序本质上都是bash的子进程,所有有退出码,告诉父进程,子进程完成的任务怎么样了。

在C语言中存在一个描述错误码的接口。

 #include<stdio.h>
  2 #include <string.h>
  3 int main()
  4 {
  5   int i=0;
  6   for(i=0;i<200;i++)
  7   {
  8     printf("%d--%s",i,strerror(i));                                                                                                          
  9   }
 10   return 0;
 11 }
~

 我们能看到 平时写的return 0 是指退出成功,无异常。

4.return、exit、_exit的区别

  • 只有 main 函数中的 return 表示的是终止进程,非 main 函数中的 return 不是终止进程,而是结束函数。
  • 在任何函数中调用 exit 函数,都表示直接终止该进程。

exit库函数

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

_exit库函数

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值

我们先来看一段代码。

#include<stdio.h>  
 24 #include <string.h>  
 25 #include <errno.h>  
 26 #include<stdlib.h>
 27 #include<unistd.h>
 28 int main()
 29 {
 30   pid_t id=fork();
 31   if(id==0)
 32   {
 33     printf("我是子进程,我的程序退出了");
 34     sleep(1);
 35     _exit(0);
 36   }
 37   else if(id>0)
 38   {
 39     printf("我是父进程,我的程序退出了");
 40     sleep(1);
 41     exit(0);
 42   }
 43   else{
 44     printf("fork失败");                                                                                                                      
 45   }
 46   return 0;   
 47 } 

运行结果如下:

 它只打印了父进程的输出内容,子进程的并没有打印。

  • exit:在进程退出的时候,会进行后续资源处理(比如刷新缓冲区)。
  • _exit:在进程退出的时候,不会进行后续资源处理,直接终止进程。

补充:

  1. 其实,库函数 exit 最后也会调用系统接口 _exit,但在调用 _exit 之前,还做了其他工作:
  2. 执行用户通过 atexit 或 on_exit 定义的清理函数。
  3. 关闭所有打开的流,所有的缓存数据均被写入。
  4. 调用 _exit。

 

5、站在 OS 角度:理解进程终止 

站在操作系统角度,如何理解进程终止?

(1)“释放” 曾经为了管理该进程,在内核中维护的所有数据结构对象。

注意:这里的 “释放” 不是真的把这些数据结构对象销毁,即占用的内核空间还给 OS;而是设置成不用状态,把相同类型的对象归为一类(如进程控制块就是一类),保存到一个 “数据结构池” 中,凡是有不用的对象,就链入该池子中。

我们知道在内核空间中维护一个内存池,减少了用户频繁申请和释放空间的操作,提高了用户使用内存的效率,但每次从内存池中申请和使用一块空间时,还需要先对这块空间进行类型强转,再初始化。

现在有了这些 “数据结构池” ,比如:当创建新进程时,需要创建新的 PCB,不需要再从内存池中申请一块空间,进行类型强转并初始化,而是从 “ 数据结构池 ” 中直接获取一块不用的 PCB 覆盖初始化即可,减少了频繁申请和释放空间的过程,提高了使用内存的效率。

这种内存分配机制在 Linux 中叫做 slab 分配器。

(2)释放程序代码和数据占用的内存空间。

注意:这里的释放不是把代码和数据清空,而是把占用的那部分内存设置成「未使用」就可以了。

(3)取消曾经该进程的链接关系。

 三、进程等待

进程等待,联想到三个问题

  1. 进程等待什么呢?
  2. 为什么要进程等待呢?
  3. 进程等待怎么办?

1.我们先讲第二个问题,为什么要进程等待

  • 当存在父子进程时,子进程处于僵尸状态,僵尸进程连kill -9都无法杀死,因为无法杀死一个死进程,需要通过进程等待,解决内存泄漏问题。退出状态本身要用数据维护,也属于进程的基本信息,所以保存在 task_struct(PCB) 中,换句话说,僵尸进程一直不退出,它对应的 PCB 就要一直维护。
  • 通过等待获取子进程的退出情况。父进程需要知道派给子进程的任务完成的如何

2、如何进程等待

我们有两个系统调用函数wait()和waitpid(),等待任意一个子进程改变状态,子进程终止时,函数才会返回。(其实就是等待进程由 R/S(运行/睡眠) 状态变成 Z(僵尸) 状态,然后父进程读取子进程的状态,操作系统回收子进程)

wait函数

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

接下来看一段代码

#include <stdio.h>
#include <stdlib.h>    // exit
#include <sys/types.h> // getpid, getppid
#include <sys/wait.h>  // wait
#include <unistd.h>    // fork, sleep, getpid, getppid
 
int main()
{
    pid_t cpid = fork();
 
    if (cpid == 0)
    {         
        // child process
        int count = 5;
        while (count)
        {
            // 子进程运行5s
            printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
            sleep(1);
        }
        
        printf("child quit...!\n");
        exit(1); // 终止子进程
    }
    else if (cpid > 0)
    {     // father process
        printf("father is waiting...\n");
        
        pid_t ret = wait(NULL); // 等待子进程终止,不关心子进程退出状态
        
        printf("father waits for success, cpid: %d\n", ret); // 输出终止子进程的pid
    }
    else
    {
        // fork failure
        perror("fork");
        return 1; // 退出码设为1,表示fork失败
    }
 
    return 0;
}

运行结果如下:

等待多个进程 

#include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <stdio.h>
  5 #include <stdlib.h>    // exit
  6 #include <sys/types.h> // getpid, getppid
  7 #include <sys/wait.h>  // wait
  8 #include <unistd.h>    // fork, sleep, getpid, getppid
  9 int main()        
 10 {  
 11   int i=0;          
 12   for(i=0;i<5;i++)
 13   {  
 14     pid_t id=fork();
 15     if(id==0)   
 16     {  
 17       int cnt=5;                                                                18       while(cnt)
 19       {           
 20          printf("child is running: %ds, pid: %d, ppid: %d\n", cnt--, getpid(),     getppid());   
 21          sleep(1);
 22       }
 23       exit(1);   
 24      
 25     }         
 26     else if(id<0)                                                              
 27     {
 28       exit(1);
 29     }      
 30     sleep(7);
 31   }         

没有等待的时候五个进程全都僵尸了

更改一下代码

#include <stdio.h>
#include <stdlib.h>    // exit
#include <sys/types.h> // getpid, getppid
#include <sys/wait.h>  // wait
#include <unistd.h>    // fork, sleep, getpid, getppid
 
int main()
{
    for (int i = 0; i < 5; i++) // 创建5个子进程
    {
        pid_t cpid = fork();
 
        if (cpid == 0)
        {
            // child process
            int count = 5;
            while (count)
            {
                // 子进程运行5s
                printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
                sleep(1);
            }
            
            printf("child quit!\n");
            exit(0); // 终止子进程
        }
        else if (cpid < 0)
        {
            // fork failure
            perror("fork");
            return 1;
        }
    }
 
    sleep(7); // 休眠7s
 
    // 父进程进行进程等待
    for (int i = 0; i < 5; i++)
    {
        printf("father is waiting...\n");
 
        pid_t ret = wait(NULL);  // 等待任意一个子进程终止,不关心子进程退出状态
 
        printf("father waits for success, ret: %d\n", ret); // 输出终止子进程的id
        sleep(2);
    }
 
    printf("father quit!\n");  // 父进程退出
 
    return 0;
}

运行结果如下:

 

总结:一般而言,我们在 fork 之后,是需要让父进程进行进程等待的。

上述例子,父进程只是等待子进程终止,并没有关心子进程的退出状态。

 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

 ① status 参数
wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。

如果传递 NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

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

status 变量:

注:一般进程提前(异常)终止,本质是该进程收到了操作系统发送的信号。

所以:

  • 我们通过检测 status 参数的次低 8 位,可以得到该进程的退出码。
  • 我们通过检测 status 参数的低 7 位,可以知道该进程是否被信号所杀,以及被哪个信号所杀。

信号是从 1 号开始的,没有 0 号。如果低 7 位全为 0,说明该进程一定是正常终止的,没有收到任何退出信号;如果 status 参数的低 7 位不为 0,说明该进程是被信号终止的。

获取子进程的退出码 

通过对父进程中 waitpid 函数的第二个参数 status 进行操作,得到 status 次低 8 位的值,即子进程退出码:

  • (status >> 8) & 0xFF

比如下面代码:

1 #include <stdio.h>
  2 #include <stdlib.h>    // exit
  3 #include <sys/types.h> // getpid, getppid
  4 #include <sys/wait.h>  // wait
  5 #include <unistd.h>    // fork, sleep, getpid, getppid
  6 int main()
  7 {
  8   pid_t id=fork();
  9   if(id==0)
 10   {
 11     int cnt=5;
 12     while(cnt)
 13   {
 14      printf("child is running: %ds, pid: %d, ppid: %d\n", cnt--, getpid(), getppid());
 15      sleep(1);
 16     }
 17     printf("child quit\n");
 18     exit(123);
 19   }
 20   else if(id<0)
 21   {
 22     exit(0);
 23   }
 24   else{
 25     int status=0;
 26     pid_t ret=waitpid(-1,&status,0);
 27     printf("父进程等待成功:%d,退出码为:%d\n",ret,(status>>8)&0xff);                                                                          
 28   }
 29 
 30 
 31   return 0;
 32 }

运行结果如下:

 

为什么操作系统要通过 waitpid 函数的 status 参数把子进程的退出码反馈给父进程,而不是定义一个全局变量作为子进程的退出码,然后反馈给父进程呢?

因为用户数据被父子进程各自私有。

子进程的退出码是如何被填充到 waitpid 函数的 status 参数中的呢?

子进程的 task_struct 中保存的有子进程的退出信息,所以 wait / waitpid 函数通过子进程的 PCB 拿到退出码和终止信号,填充到 status 参数中。

获取子进程的终止信号 

通过对父进程中 waitpid 函数的第二个参数 status 进行操作,得到 status 低 7 位的值,即子进程终止信号:

  • status & 0x7F
#include <stdio.h>
  2 #include <stdlib.h>    // exit
  3 #include <sys/types.h> // getpid, getppid
  4 #include <sys/wait.h>  // wait
  5 #include <unistd.h>    // fork, sleep, getpid, getppid
  6 int main()
  7 {
  8   pid_t id=fork();
  9   if(id==0)
 10   {
 11     int cnt=5;
 12     while(cnt)
 13   {
 14      printf("child is running: %ds, pid: %d, ppid: %d\n", cnt--, ge    tpid(), getppid());
 15      sleep(1);
 16     }
 17     printf("child quit\n");
 18     exit(123);
 19   }
 20   else if(id<0)
 21   {
 22     exit(0);
 23   }
 24   else{
 25     int status=0;
 26     pid_t ret=waitpid(-1,&status,0);                               
 27     printf("父进程等待成功:%d,退出码为:%d,终止码为:%d\n",ret,(statu    s>>8)&0xff,status&0x7f);
 28   }
       return 0;
 32 }

运行结果如下:

 为什么终止码为9呢?

因为我们输入了kill指令。

判断进程是否被正常终止
#include<stdio.h>
#include<stdlib.h>    // exit
#include<sys/types.h> // wait, getpid
#include<sys/wait.h>  // wait
#include<unistd.h>    // fork, sleep, getpid
 
int main()
{
    pid_t cpid = fork();
    if (cpid == 0) // child process
    {      
        int count = 5;
        while (count) // 子进程运行5s
        {
            printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
            sleep(1);
        }
        printf("child quit...\n");
        exit(123); // 终止子进程
    }
    else if (cpid > 0) // father process
    {  
        int status = 0; // 进程退出状态
 
        pid_t ret = waitpid(-1, &status, 0); // 等待子进程终止
 
        // 判断父进程是否等待成功
        if (ret > 0) // waitpid返回值大于0,父进程等待成功
        {
            printf("father waits for success, ret: %d\n", ret); // 输出子进程id
            
            // 判断子进程是否正常终止
            if ((status & 0x7f) == 0) // 子进程正常终止(终止信号为0)
            {  
                // 输出退出码
                printf("child process exits normally, exit_code: %d\n", (status >> 8) & 0xff);
            }
            else // 子进程异常终止(终止信号不为0)
            {                       
                // 输出终止信号
                printf("child process exits abnormally, sign: %d\n", status & 0x7f);
            }
        }
        else
        {
            // wait failure
        }
    }
    else
    {
        // fork failure
    }
    return 0;
}

运行结果如下:

 

每次都要这样判断子进程是否正常终止((status & 0x7f) == 0),以及计算退出码((status >> 8) & 0xff),太麻烦了,有没有什么更便捷的方法呢?
系统中定义了一堆的宏(函数),可以用来判断退出码、退出状态。

父进程中 waitpid 函数调用结束后,把它的第二个参数 status 传递给宏函数:

  • 宏函数 WIFEXITED(status):如果子进程正常终止,则返回 true。(查看进程是否是正常退出)w if exited
  • 宏函数 WEXITSTATUS(status):若 WIFEXITED 非零,说明子进程正常终止,返回子进程的退出码。(查看进程的退出码)w exit status

实际中,一般都是使用宏函数来检测子进程的退出状态和获取子进程的退出码。

改进后的一个完整的进程等待:

#include<stdio.h>
  2 #include<stdlib.h>    // exit
  3 #include<sys/types.h> // wait, getpid
  4 #include<sys/wait.h>  // wait
  5 #include<unistd.h>    // fork, sleep, getpid
  6  
  7 int main()
  8 {
  9     pid_t cpid = fork();
 10     if (cpid == 0) // child process
 11     {      
 12         int count = 5;
 13         while (count) // 子进程运行5s
 14         {
 15             printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
 16             sleep(1);
 17         }
 18         printf("child quit...\n");
 19         exit(123); // 终止子进程
 20     }
 21     else if (cpid > 0) // father process
 22     {
 23         int status = 0; // 进程退出状态
 24 
 25         pid_t ret = waitpid(-1, &status, 0); // 等待子进程终止
 26 
 27         // 判断父进程是否等待成功                                                                                                            
 28         if (ret > 0) // waitpid返回值大于0,父进程等待成功
 29         {
 30             printf("father waits for success, ret: %d\n", ret); // 输出子进程id
 31 
 32             // 判断子进程是否正常终止
 if (WIFEXITED(status)) // 子进程正常终止(终止信号为0)                                                                          
 34             {  
 35                 // 输出退出码
 36                 printf("child process exits normally, exit_code: %d\n", (status >> 8) & 0xff);
 37             }
 38             else // 子进程异常终止(终止信号不为0)
 39             {                       
 40                 // 输出终止信号
 41                 printf("child process exits abnormally, sign: %d\n", status & 0x7f);
 42             }
 43         }
 44         else
 45         {
 46             // wait failure
 47         }
 48     }
 49     else
 50     {
 51         // fork failure
 52     }
 53     return 0;
 54 }

运行结果与上面一致。

 options 参数

options:

  • 如果设为 0,默认是阻塞式等待,与 wait 等效。
  • 如果设为 WNOHANG:是非阻塞等待。

  • 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。(说明这一次等待失败了,需要再次等待,此时父进程可以去干别的事情)
  • 若正常结束,则返回该子进程的 ID。(说明等待成功了)

waitpid 的两种等待方式:阻塞 & 非阻塞

  • 阻塞等待(给 options 参数传 0)
  • 非阻塞等待(给 options 参数传 WNOHANG)

例子1:

张三做菜缺酱油,叫李四去买,相当于张三调了一个买酱油的函数,若李四还没回来,则函数就没结束,而李四在买酱油期间,张三一直被卡住,不继续做菜。这就是「阻塞等待」。

注意:我们目前的大多数接口,都是阻塞函数(调用 --> 执行 --> 返回 --> 结束),因为都是单执行流,同时实现起来也比较简单。

阻塞等待:调用方需要一直等着,不能做其他事情,直到函数返回。

例子2:

张三打电话问李四作业写完没,李四说没有,过了一会儿,张三又打电话问李四作业写完没,李四说没有……,张三多次打电话问李四作业写完没,直到李四作业写完,张三才会停止打电话。

上述例子的本质是,张三打电话不会把张三一直卡住,张三可以忙忙自己的事情,通过间隔多次打电话,检测李四的状态。张三每一次打电话,称之为「非阻塞等待」。多次打电话的过程,称之为「非阻塞轮询检测方案」。

为什么自然界一般选择非阻塞呢 —— 因为更加高效一些,不会一直卡在那里不做事。

非阻塞等待:调用方不需要一直等着,可以边轮询检测边做自己的事情。

  • 进程的阻塞等待:

父进程中的 wait 和 waitpid 函数默认是阻塞调用,调用该函数后,只要子进程没有退出,父进程就得一直等,什么事情都做不了,直到子进程退出,函数才返回。


  • 进程的非阻塞等待:

想让父进程中的 waitpid 函数是非阻塞调用(即父进程边运行边调用),需要将函数的第三个参数设为 WNOHANG。

 

 

这里的失败,有两种情况:

  1. 并非真的等待失败,而是子进程此时的状态没有达到预期。
  2. 真的等待失败了。

父进程中 waitpid 函数如果是非阻塞调用,返回值有三种情况:

  1. 等待失败:此次等待失败,需要再次检测。
  2. 等待失败:真的失败。
  3. 等待成功:已经返回。

我们写一段代码看看:

#include<stdio.h>
  2 #include<stdlib.h>    // exit
  3 #include<sys/types.h> // wait, getpid
  4 #include<sys/wait.h>  // wait
  5 #include<unistd.h>    // fork, sleep, getpid
  6 int main()
  7 {
  8   pid_t id =fork();
  9   if(id==0)
 10   {
 11     int cnt=3;
 12     while(cnt)
 13     {
 14       printf("chile is running:%d,pid:%d,ppid:%d\n",cnt--,getpid(),getppid());
 15       sleep(1);
 16     }
 17     printf("child quit\n");
 18     exit(123);
 19   }
 20   else if(id>0)
 21   {
 22     int status=0;
 23     while(1)
 24     {
 25       pid_t ret=waitpid(id,&status,WNOHANG);
 26       if(ret==0)
 27       {                                                                                                                                      
 28         sleep(1);
 29         printf("wait next\n");
 30 
 31       }
 32       else if(ret>0)
         {                                                                                                                                      
 34         printf("wait success!,ret:%d,exit_code:%d\n",ret,WEXITSTATUS(status));
 35         break;
 36       }
 37       else{
 38 
 39       }
 40 
 41   }
 42 }
 43 else{
 44 
 45 }
 46 return 0;
 47 }

运行结果如下:

 

如何理解阻塞 / 等待?

  • 如何理解进程等待:即父进程在等待子进程终止,而子进程在跑自己的代码。
  • 如何理解进程在 “ 阻塞 / 等待 ”:阻塞的本质就是进程被卡住了,没有被 CPU 执行。

操作系统将当前进程放入等待队列,并把进程状态设置为非 R(运行) 状态,暂时不会被 CPU 执行,当需要的时候,会唤醒等待(即把进程从等待队列移出,放回运行队列,并把进程状态设置为 R(运行) 状态,让 CPU 去调度)。

比如:我们电脑上运行的软件太多,发现某个软件卡起了,其实是当前运行队列中的进程太多,系统资源不足,把一些进程放入等待队列中了。

补充:内核源码中的退出码和终止信号

上面说到,父进程中的 wait/waitpid 函数通过子进程的 PCB 拿到退出码和终止信号,填充到 status 参数中。

我们来看 Linux 内核 2.6 的源码,进程控制块(PCB)中保存的退出码和终止信号:

struct task_struct
{
    ...
    /* task state */
    int exit_state;
    int exit_code, exit_signal;   // 退出码和终止信号
    int pdeath_signal; /* The signal sent when the parent dies */
    ...
}

总结:

  • 如果子进程已经退出,调用 wait / waitpid 时,wait / waitpid 会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用 wait / waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

 四、进程替换

1.什么是进程替换

通过 exec* 函数,把磁盘中的其它程序(代码+数据)加载到内存中,替换当前进程的代码和数据,让页表重新构建映射关系,这期间不会创建新的进程。

2.单进程版的程序替换

先看一段代码

1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 int main()
  5 {
  6   printf("befor,pid:%d,ppid:%d",getpid(),getppid());
  7   execl("/usr/bin/ls","ls","-a","-l",NULL);                                                                                                  
  8 
  9   printf("after,pid:%d,ppid:%d",getpid(),getppid());
 10   return 0;
 11 }

运行结果如下:

运行结果显示,只打印了before和ls命令,after不执行。

这就是程序替换。 

3.程序替换的原理

思考:为什么要进程替换?

因为创建子进程的目的一般是这两个:

执行父进程的部分代码,完成特定功能。
执行其它新的程序。——> 需要进行「进程替换」,用新程序的代码和数据替换父进程的代码和数据,让子进程执行。

代码逐步运行时,当运行到execl时,新程序的数据和代码覆盖老的数据和代码。

这个过程并没有改变进程和创建新进程。

4.多进程版的程序替换

接下来看一段代码

#include <stdio.h>
  2 #include <stdlib.h>    // exit
  3 #include <sys/types.h> // getpid, getppid, waitpid
  4 #include <sys/wait.h>  // waitpid
  5 #include <unistd.h>    // exec, fork, getpid, getppid
  6 
  7 int main()
  8 {
  9     pid_t cpid = fork();
 10 
 11     if (cpid == 0)
 12     {
 13         // child
 14         printf("I'm child process, pid: %d\n", getpid());
 15         sleep(3);
 16         execl("/usr/bin/ls", "ls","-a","-l", NULL);               // 进程替换                                                                
 17         exit(1);
 18     }
 19     else if (cpid > 0)
 20     {
 21         // father
 22         printf("I'm father process, pid: %d\n", getpid());
 23 
 24         int status = 0; // 进程退出信息
 25         pid_t ret = waitpid(cpid, &status, 0); // 进程等待
 26         if (ret > 0)
 27         {
 28             // 等待成功,打印子进程的ID、退出码、终止信号
 29             printf("father waits for success, ret: %d, code: %d, sig: %d\n", ret, (status >> 8) & 0xff, status & 0x7f);
 30         }
 31         else
 32         {
                  // wait failure
 34         }
 35     }
 36     else
 37     {
 38        // fork failure
 39     }
 40     return 0;
 41 }

运行结果如下:

 

结论

  • 子进程进行程序替换并不影响父进程
  • 父进程的代码和数据发生了写时拷贝。
  • 程序替换,不创建新进程,只进行进程的程序代码和数据的替换工作。

 5.各种exec的接口

#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 execvpe(const char *file, char *const argv[], char *const envp[]);

系统调用 execve 函数,功能:执行文件名 filename 指向的程序,文件名必须是一个二进制的 exe 可执行文件。

#include <unistd.h>
 
int execve(const char *filename, char *const argv[], char *const envp[]);

其实,只有 execve 是真正的系统调用,其它 6 个函数都是库函数,最终都是调用的 execve,所以 execve 在 man 手册的第 2 节,其它函数在 man 手册第 3 节。 

 

exec 函数命名理解,这些函数原型看起来很容易混,但只要掌握了规律就很好记:

  • l (list):表示参数采用列表(可变参数列表)
  • v (vector):参数采用数组
  • p (path):自动在环境变量 PATH 中搜索可执行程序(不需要带可执行程序的路径)
  • e (env):可以传入默认的或者自定义的环境变量给目标可执行程序

 

(1)execl 函数 

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

函数介绍:

 代码如下:

 #include <stdio.h>
  2 #include <stdlib.h>    // exit
  3 #include <sys/types.h> // getpid, getppid, waitpid
  4 #include <sys/wait.h>  // waitpid
  5 #include <unistd.h>    // exec, fork, getpid, getppid                                                                                        
  6 int main()
  7 {
  8     printf("my process begin...\n");
  9     execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 进程的程序替换
 10     printf("my process end...\n");
 11     return 0;
 12 }

运行结果如下:

 

多进程下的代码上面写了,我们能看到exit(1),并没有被执行。

(2)execv 函数 

在功能上和 execl 没有任何区别,只在传参的方式上有区别。

#include <stdio.h>
    2 #include <stdlib.h>    // exit
    3 #include <sys/types.h> // getpid, getppid, waitpid
    4 #include <sys/wait.h>  // waitpid
    5 #include <unistd.h>    // exec, fork, getpid, getppid
W>  6 char* const arr[]={"ls","-a","-l",NULL};
    7 int main()
    8 {
    9     printf("my process begin...\n");
   10     execv("/usr/bin/ls", arr); // 进程的程序替换                                                                                           
   11     printf("my process end...\n");
   12     return 0;
   13 }

 

(3)execlp 函数

在功能上和 execl 没有任何区别,唯一区别是,只需要给出要执行程序的名称即可,自动去 PATH 中搜索,不需要给出绝对路径。

但是:只有系统的命令,或者自己的命令(前提是已经导入到 PATH 中了),才能够找到。

 

 #include <stdio.h>
  2 #include <stdlib.h>    // exit
  3 #include <sys/types.h> // getpid, getppid, waitpid
  4 #include <sys/wait.h>  // waitpid
  5 #include <unistd.h>    // exec, fork, getpid, getppid
  6 int main()
  7 {
  8   printf("begin....\n");
  9   execlp("ls","ls","-a","-l",NULL);                                                                                                          
 10   printf("aaaaaaaaaaaaaaaaaaaaaa\n");
 11   return 0;
 12 }

运行代码如下:

这里两个"ls"是不一样的概念。

 

(4)execle和execvp 函数

/*
* 调用 execle 或 execve 函数进行进程替换(执行 xxx 程序)时,可以把在当前程序中定义的环境变量传递给要替换的程序 xxx,此时在 xxx 程序中通过 getenv 就可以获取到这些环境变量
*/
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);

了解这两个函数之前,我们先学习一下怎么用一个函数调用另一个函数

我们另外创建一个mytest.c

 #include <stdio.h>
  2 int main()
  3 {
  4   printf("i am a other process\n");                                                                                                          
  5   return 0;                                                                                                               
  6 }  

并进行编译形成可执行程序。

 我们想用test调用mytest程序

test.c

 #include <stdio.h>
  2 #include <stdlib.h>    // exit
  3 #include <sys/types.h> // getpid, getppid, waitpid
  4 #include <sys/wait.h>  // waitpid
  5 #include <unistd.h>    // exec, fork, getpid, getppid
  6 int main()
  7 {
  8   printf("begin....\n");
  9   execl("./mytest","mytest",NULL);                                                                                                           
 10   printf("aaaaaaaaaaaaaaaaaaaaaa\n");
 11   return 0;
 12 }
 13 //ch

运行结果如下:

 我们不仅输入了test.c中的begin 还输出了mytest.c的内容。

我们来看看怎么同时形成两个可执行程序

最初的makefile代码

1 mytest:mytest.c
  2     gcc -o mytest mytest.c -std=c99
  3 test:test.c
  4     gcc -o test test.c -std=c99
  5 clean:
  6   rm -rf mytest test  

我们能看到只形成了一个可执行文件,为什么呢?

因为当生成该目标文件就直接退出了,不会执行test.c 

更改makefile代码 

 1 .PHONY:all
  2 all:mytest test
  3 mytest:mytest.c                                                                                                                              
  4     gcc -o $@ $^ -std=c99
  5 test:test.c
  6     gcc -o $@ $^ -std=c99
  7 clean:
  8   rm -rf mytest test

运行结果如下: 

 

 我们回到最开始讲解的函数

mytest.c

 1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>                                                                                                                          
  4 int main()
  5 {
  6   printf("mytest->getenv:%s\n",getenv("MYENV"));
  7   return 0;
  8 }
~

如果单独执行 mytest.c 程序,运行结果为空,系统中没有这个环境变量:

如果在 test.c 程序中,调用 execle 函数进行进程替换(执行 mytest.c 程序)时,可以把在 test 程序中定义的环境变量通过传递给要替换的 mytest 程序,如下: 

#include <stdio.h>
    2 #include <stdlib.h>    // exit
    3 #include <sys/types.h> // getpid, getppid, waitpid
    4 #include <sys/wait.h>  // waitpid
    5 #include <unistd.h>    // exec, fork, getpid, getppid
    6 int main()
    7 {
    8   printf("begin....\n");
W>  9   char* const arr[]={"MYENV=hello world!",NULL};
   10   execle("./mytest","mytest",NULL,arr);                                                                                                    
   11   printf("aaaaaaaaaaaaaaaaaaaaaa\n");
   12   return 0;
   13 }

运行结果如下:

 

结论:

  • 环境变量也是数据,创建子进程,环境变量被传输到子进程的main函数里,也就是被子进程继承下去。
  • 程序替换中,环境变量的信息不会被替换。

(5)新增环境变量

我们在命令行输入export HELLO=123456,这是在bash中新增

我们能不能给mytest.c这个文件增加一个环境变量呢?

mytest.c文件打印该main函数的环境变量。

mytest.c

1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
W>  4 int main(int argc,char* agrv[],char*env[])
    5 {
    6   for(int i=0;env[i];i++)
    7   {
    8     printf("i:%d,env[i]->%s\n",i,env[i]);                                                                                                  
    9   }
   10   return 0;
   11 }
  ~

test.c文件为给mytest.c增加环境变量的相关代码 

test.c
 #include <stdio.h>
    2 #include <stdlib.h>    // exit
    3 #include <sys/types.h> // getpid, getppid, waitpid
    4 #include <sys/wait.h>  // waitpid
    5 #include <unistd.h>    // exec, fork, getpid, getppid
    6 int main()
    7 {
    8   extern char**environ;
W>  9   putenv("PRIVATE_ENV=666");
   10   execle("./mytest","mytest",NULL,environ);
   11   return 0;                                                                                                                                
   12 }

我们能看到确实是在原有的环境变量中新增了一个

 

(6) 彻底替换环境变量

原来的mytest.c代码不变,还是打印该main函数的环境变量表。

test.c代码如下:

test.c

#include <stdio.h>
    2 #include <stdlib.h>    // exit
    3 #include <sys/types.h> // getpid, getppid, waitpid
    4 #include <sys/wait.h>  // waitpid
    5 #include <unistd.h>    // exec, fork, getpid, getppid
    6 int main()
    7 {
W>  8   char* const myenvarr[]={"PRIVATE_VAL=1111","PRIVATE_CAL=2222",NULL};
    9   execle("./mytest","mytest",NULL,myenvarr);                                                                                               
   10   return 0;
   11 }

我们执行./test可执行程序时,结果如下

我们能看到只有myenvarr内我自己定义的环境变量,没有原来继承bash的环境变量。

这采用的是覆盖,而不是叠加! 


网站公告

今日签到

点亮在社区的每一天
去签到