您的位置:首页 > 汽车 > 新车 > 【Linux】进程控制

【Linux】进程控制

2024/9/20 10:21:21 来源:https://blog.csdn.net/2301_77578940/article/details/142069645  浏览:    关键词:【Linux】进程控制

进程控制

    • 进程创建
      • 进程的创建方式
      • 进程间的独立性
    • 进程终止
      • 进程终止是在干什么
      • 进程终止的3种情况
        • 退出码
        • 退出信号
        • 如何终止
          • exit
          • exit 和 _exit 的区别
    • 进程等待
      • 等待是什么
      • 等待的必要性
      • 等待的方法
        • wait
        • waitpid
          • 转换退出码
          • 转换退出信号
        • WIFEXITED 和 WEXITSTATUS
      • 非阻塞等待
    • 进程替换
      • 代码 && 现象
      • 原理
      • 多进程代码
      • 参数含义
        • execl
        • execv
        • execvp 和 execlp
        • execvpe 和 execle

进程创建


进程的创建方式

  1. 由 bash 创建子进程
  2. 在程序中,通过 fork 创建子进程

以上两种进程创建方式详情见进程的概念,这里不再赘述

fork 函数的返回值:

  1. 子进程返回 0
  2. 父进程返回子进程的 pid

为什么这样返回呢?

子进程创建成功,对于子进程来说就没事了,返回 0 表示创建成功。对于父进程来说,要对子进程进行管理,例如回收僵尸状态的子进程,所以需要返回子进程的 pid 给父进程

进程间的独立性

创建一个进程,本质就是创建进程对应的 内核的相关管理数据结构(task_struct、地址空间、页表等)+ 代码和数据

父进程创建子进程时,子进程的内核数据结构大部分都是直接拷贝父进程的,少部分特殊属性(pid、ppid)除外。代码是只读的,所以是父子进程共享;对于数据,父子进程暂时共享,当任一方要对数据进行修改时,就会触发写时拷贝,这样就会出现虚拟地址相同,但是物理地址不同的数据。这样就保证了进程的独立性,细节见地址空间

在这里插入图片描述

进程终止


进程终止是在干什么

我们知道,创建进程就是先创建内核数据结构,再加载代码与数据。而终止进程就是释放进程代码和数据占据的空间,再释放内核结构,留下进程PCB,此时进程进入僵尸状态,等待父进程的等待。僵尸子进程被父进程等待后,将释放最后的 PCB 占据的空间,彻底结束

进程终止的3种情况

进程终止时,有以下 3 种情况:

  1. 代码运行完,运行结果正确
  2. 代码运行完,运行结果不正确
  3. 代码没有运行完,出现了异常,提前退出

以上的前两种情况相对第三种情况来说,应该是同一种。

退出码

在代码运行完毕后,我们怎么知道运行结果正不正确呢?我们平时写 C/C++ 程序时,一般都会在 main 函数的结尾写上一句 return 0,main 函数返回的数字叫退出码。

我们在命令行运行的程序会变成进程,并且是 bash 的子进程。当程序运行完毕后,就会返回退出码,我们可以使用echo $?查看退出码。其中?是一个存放在 bash 中的变量,代表父进程bash获得的,最后一次运行的程序的退出码,我们可以使用$来读取其中的信息,就和环境变量一样。

例如有如下程序,我们运行一次程序,使用命令查看它的退出码

#include <stdio.h>
#include <unistd.h>int main()
{printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());return 0;
}

[图片]

为了更明显地查看退出码,我们可以将 main函数 中的返回值修改为 123,再查看一次

在这里插入图片描述

那么退出码究竟有什么用呢?它可以用来表示进程的运行情况:退出码为零表示成功,非零表示失败。一个进程运行起来,最后的运行结果总是要被其父进程知道的,就好像我们托人办事,无论事情办得怎么样,都得有一个反馈。将来父进程等待子进程,就可以通过退出码知道子进程的运行结果,成功了就不关心了,要是失败了就得知道失败的原因

退出码非零不仅可以表示失败,还可以用不同数字表示不同的失败原因。非零数字有很多,我们如何知道每个数字代表的意义呢?在 C/C++ 中,有一个函数可以将退出码转换为字符串,有了它我们就可以知道退出码所对应的错误描述了。这个函数就是strerror,相关资料如下:

在这里插入图片描述

我们可以使用 strerror 将退出码对应的错误描述都打印出来看看

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

在这里插入图片描述

退出码太多了截不完,这里只是一部分。可以看到不同退出码都对应了不同的错误描述,这样父进程 bash 就可以知道子进程失败的原因,然后就可以把这些信息输送给用户了。即使用户不懂退出码,也可以看懂错误描述。

其中 2 号退出码的描述是不是有些眼熟呢?当我们使用 ll命令查看不存在的文件时就会出现这个错误描述,这时候用户就知道查看的文件是不存在的

在这里插入图片描述

自定义退出码

对于这些退出码,我们可以使用默认的,也可以自定义。例如我们写一个简单的除法函数,设置一个全局变量 int exit_code,默认值为0,表示程序运行正确。当除数为零时,是不合法的,返回 -1 作为函数调用结果,并将 exit_code 设置为 1 表示程序运行错误。当整个 main 函数运行结束时,就将 exit_code 作为退出码返回

为了更直观地看出 exit_code 变量的状态,我们可以定义枚举常量来表示 exit_code

#include <stdio.h>
#include <unistd.h>
#include <string.h>// 枚举常量
enum
{Sucess = 0,Div_Zero,
};// 退出码
int exit_code = Sucess;// 除法函数
int Div(int x, int y)
{if (y == 0){exit_code = Div_Zero;return -1;}return x/y;
}int main()
{int result = Div(10, 0);printf("result: %d\n", result);return exit_code;
}

程序输出 -1,我们可以查看退出码来确认程序是否正确运行

在这里插入图片描述

退出码为 1,和我们定义的枚举常量 Div_Zero 的值是一样的,说明程序并没有正确运行,错误原因是除数为零

可是这样查看退出码的方式不是很直观,我们可以写一个类似 strerror 的函数,将我们的自定义退出码打印出来。如下

#include <stdio.h>
#include <unistd.h>
#include <string.h>// 枚举常量
enum
{Sucess = 0,Div_Zero,
};
// 退出码
int exit_code = Sucess;
// 除法函数
int Div(int x, int y)
{if (y == 0){exit_code = Div_Zero;return -1;}return x/y;
}// 退出码转换
const char* CodeToString(int exit_code)
{switch(exit_code){case Sucess:return "Sucess";case Div_Zero:return "Div zero!";default:return "Unknown error!";}
}int main()
{int result = Div(10, 0);printf("result: %d[%s]\n", result, CodeToString(exit_code));return exit_code;
}

在这里插入图片描述

这样我们就可以直观地看出程序是否是正确运行了

退出信号

所以进程的代码跑完后,我们可以查看退出码,获得进程的退出信息。但是有时候我们的代码并不一定会跑完,当遇到异常时,代码就会被中断,提前退出,这时候退出码就没有意义了

为什么程序异常会提前退出呢?这是因为操作系统检测到我们的代码做了一些非法行为,例如越界等问题,就会杀掉进程。这时候进程还没有运行完,想要查看退出原因不是要看退出码,而是要看退出信号

这个退出信号我们之前也使用过,例如 kill -9可以用来杀掉一个进程

在这里插入图片描述

这时用户主动使用信号杀死进程,所以我们知道信号代表什么,也知道进程退出的原因

在这里插入图片描述

下面我们写一个代码,来做一些非法操作,看看进程是如何被操作系统杀掉的

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{int* p = NULL;while(1){printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);     *p = 10; // 空指针错误                                                       }return 0;
}

在这里插入图片描述

程序在进行了一个循环后就被操作系统杀掉了,给出的描述是 Segmentation fault,程序发生了段错误,通常都是程序非法访问地址导致。我们使用 kill -l 可以查看到 Segmentation fault 对应的信号

在这里插入图片描述

我们也可以使用 kill -11 来杀掉一个进程

在这里插入图片描述

所以当一个进程异常退出时,只需要查看它的退出信号就可以判断进程为什么异常了

通过以上,我们知道进程退出后会有退出码与退出信号,要想了解一个进程退出的原因

  1. 先判断进程是否异常,异常就查看退出信号
  2. 不是异常,就一定是代码跑完了,就查看退出码

所以说,衡量一个进程退出的原因,只需要两个数字:退出码和退出信号

当一个进程退出时,就会将退出码与退出信号写入 task_struct 中,父进程对其进行等待后就可以获得退出信息了

如何终止

main 函数的 return 表示进程的终止;非 main 函数的 return 表示函数结束。

如果在非 main 函数中发生了不该出现的情况,例如除法函数中的除数为零的情况,这时想要终止进程一般都是 return 到 main 函数,再由 main 函数 return 来终止进程。这样貌似有点麻烦,可不可以在非 main 函数终止进程呢?答案是可以的

exit

C/C++ 为我们提供了一个函数:exit 在程序的任意位置调用此函数,都可以让进程正常退出,它的参数就是退出码。我们输入的参数会被设置为进程的退出码

在这里插入图片描述

下面我们就在非 main 函数中调用 exit 测试一下,当除数为零时,调用 exit,并将退出码设为 1

#include <stdio.h>
#include <stdlib.h>int Div(int x, int y)
{if (y == 0){printf("Div zero!\n");exit(1);}return x/y;
}int main()
{int result = Div(10, 0);printf("result: %d, Sucess\n", result);return 0;
}

在这里插入图片描述

_exit

除了语言层面的 exit,在系统层面还存在一个系统调用_exit,用法和上面相同

exti(1);
_exit(1);
exit 和 _exit 的区别

在退出进程时,exit 会冲刷缓冲区,_exit 则不会,可以使用如下代码验证一下

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{printf("I am a process, pid: %d, ppid: %d", getpid(), getppid()); // 不加 \n 刷新缓冲区exit(1); // _exit(1)
}

在这里插入图片描述

在这里插入图片描述

而我们上面也说了,exit 是C库函数,exit 是系统调用,而系统调用层之下就是操作系统。实际上,exit 在实现时就是调用的 _exit。但是 exit 可以冲刷缓冲区,exit 不可以,这就说明:我们目前说的缓冲区不在系统调用层,更不可能在更下面的操作系统。

在这里插入图片描述

进程等待


等待是什么

任何子进程,在退出的情况下,一般必须被父进程进行等待。如果父进程退出时对子进程不管不顾,那么处于僵尸状态的子进程就会一直存在,造成内存泄漏问题。

等待的必要性

父进程为什么必须等待,有以下两个原因:

  1. 必须要做的:通过等待,释放僵尸子进程,避免内存泄漏
  2. 不是必须做的:通过等待,获取子进程的退出信息,明白子进程为什么退出

等待的方法

wait

在这里插入图片描述

wait 的功能就是等待父进程的任一子进程退出。等待成功就返回子进程的 pid,等待失败则返回 -1。而参数 status 则是一个输出型参数,可以存放子进程的退出信息,如果不用就可以填 NULL

我们可以通过以下代码和脚本,观察子进程退出后成为僵尸进程,进而被父进程等待回收资源的过程

while :; do ps ajx | head -1 && ps ajx | grep 进程名 | grep -v grep; sleep 1; done
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>void ChildRun()
{int cnt = 5;while(cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}int main()
{printf("I am father process, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork(); // 创建子进程if (id == 0){// childChildRun(); // 子进程运行 5 秒printf("Child quit!\n");exit(0); // 子进程退出,设置退出码}// fathersleep(7); // 睡眠七秒pid_t rid = wait(NULL);if (rid > 0){// 等待成功printf("Wait sucess, rid:%d\n", rid);}else{// 等待失败printf("Wait failed!\n");}sleep(2);printf("Father quit!\n");// 父进程退出return 0;
}

以上代码含义:父进程创建子进程,之后父进程睡眠七秒,子进程运行五秒退出。这样等子进程退出之后,父进程还有两秒才会醒来,这两秒叫窗口期,在窗口期我们可以看到处于僵尸状态的子进程。两秒后父进程醒来,对子进程进行 wait,之后子进程彻底退出,父进程睡眠两秒后退出。运行结果如下:

在这里插入图片描述

假如父进程创建子进程后不进行睡眠,直接执行到 wait,此时还需等待子进程运行五秒。那在这五秒内,父进程一直都会等待子进程吗?是的,如果子进程没有退出,父进程就会一直等待,处于阻塞等待态

阻塞就是在等待某种资源就绪,而这里的子进程属于软件。也就是说父进程的阻塞等待本质就是在等待某软件条件就绪。进程在等待外设资源就绪时,PCB 会被链入到外设的等待队列中;同样等待子进程就绪时,也会被链入到子进程的队列中,这时父进程就处于阻塞态,不可被调度。当子进程退出,父进程醒来,回收子进程的资源

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

waitpid 也是等待子进程退出,只不过是可以等待特定 pid 的子进程。

当参数 pid 为 -1 时,就和上面的 wait 功能相同了,等待任意子进程退出。如果其他两个参数暂时不用,分别可以填 NULL 和 0

这里就不演示用法了,下面我们来看一下参数 status。这是一个输出型参数,可以存放子进程的退出信息,所以我们需要预先创建好变量 status,然后取地址传给 waitpid,使用起来和 scanf 一样

int status;
pid_t rid = waitpid(111, &status, 0);

然后我们实践一下,将子进程退出码设为1,取出子进程的退出信息,打印出来看看

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>void ChildRun()
{int cnt = 5;while(cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}int main()
{printf("I am father process, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork(); // 创建子进程if (id == 0){// childChildRun(); // 子进程运行 5 秒printf("Child quit!\n");exit(1); // 子进程退出,设置退出码为 1}// fathersleep(7); // 睡眠七秒int status; // 获得子进程退出信息pid_t rid = waitpid(id, &status, 0);if (rid > 0){// 等待成功printf("Wait sucess, rid:%d\n", rid);}else{// 等待失败printf("Wait failed!\n");}sleep(2);printf("Father quit, status: %d\n", status);// 父进程退出return 0;
}

运行结果:

在这里插入图片描述

为什么退出信息 status是 256 呢?它和我们设置的退出码 1 又有什么关系呢?

退出信息是 退出码和退出信号,只用一个 status 似乎无法直接表示两个数字,所以这里就需要位图了。

一个整型是 4 字节,也就是 8 * 4 = 32 比特位,其中我们只会用到低 16 位

在这里插入图片描述

在这 16 位当中,高 8 位用来表示退出码,低 7 位用来表示退出信号。中间剩下的 1 位表示 core dump 标志,这个先不管。那么退出码最多可以有2的八次方,也就是 256 个,范围是[0, 255];同理,退出信号最多有2的七次方128个,范围是[0, 127]

在这里插入图片描述

那么该怎么把上面的 status = 256 转换为退出码和退出信号呢?首先将 256 转换为二进制,0000 0001 0000 0000(这里只写低 16位)

转换退出码

对于退出码,只关心高 8 位,所以将status右移 8 位,将干扰数据(退出信号)去掉

在这里插入图片描述

然后呢,把 status>>81 都取下来,只需要与一个低 8 位都是 1,其余位置都是 0 的数进行按位与操作就可以了,这个数就是 0xFF

在这里插入图片描述

转换退出信号

如果退出信号不为零,就代表进程是被信号所杀,所以退出码就是未用的,都是零

所以不用进行移位操作,只需要把低 7 位的 1 都取下来就可以了,同样是与一个低 7 位都是 1,其余位置都是 0 的数进行按位与操作,这个数是 0x7F

在这里插入图片描述

这样就可以把 status 转换为退出码和退出信号了

printf("Father quit, status: %d, exit_code: %d, exit_signal: %d\n", status, (status>>8) & 0xFF, status & 0x7F);

在这里插入图片描述

程序正常退出,所以有退出码,无退出信号,并且退出码与我们设置的相同

我们可以故意让子进程访问空指针,再来看看父进程获得的退出信号

void ChildRun()
{int* p = NULL;int cnt = 5;while(cnt){*p = 10; // 尝试修改空指针printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}

在这里插入图片描述

之前也说过,信号11是 SIGSEGV,段错误。以上两个测试都说明我们的转换是正确的

可是这样查看也太麻烦了,如果我们用户不懂位操作怎么办呢? 这时就有两个帮我们实现位操作

WIFEXITED 和 WEXITSTATUS

WIFEXITED(status) 可以帮我们检测进程是否正常退出。如果 WIFEXITED(status) 值为非0就表示进程正常退出,否则异常退出

在进程正常退出的情况下,我们可以使用 WEXITSTATUS(status) 来获得进程的退出码。也就是说,WEXITSTATUS 一般是在 WIFEXITED 检测进程为正常退出后使用的。至于获取退出信号这件事,还是要我们手动写代码来查看

下面写代码运用一下

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>void ChildRun()
{int cnt = 5;while(cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}int main()
{printf("I am father process, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork(); // 创建子进程if (id == 0){// childChildRun(); // 子进程运行 5 秒printf("Child quit!\n");exit(1); // 子进程退出,设置退出码为 1}// fathersleep(7); // 睡眠七秒int status; // 获得子进程退出信息pid_t rid = waitpid(id, &status, 0);if (rid > 0){if (WIFEXITED(status) != 0){// 子进程正常退出,打印退出信息printf("Child quit normal, exit_code: %d\n", WEXITSTATUS(status));}else{// 子进程异常退出printf("Child quit unnormal!\n");}// 等待成功printf("Wait sucess, rid:%d\n", rid); }else{// 等待失败printf("Wait failed!\n");}sleep(2);printf("Father quit\n");// 父进程退出return 0;
}

运行结果:

在这里插入图片描述

非阻塞等待

上面提到,父进程运行到 waitpid 时,会一直等待子进程退出,父进程此时不会被调度,处于阻塞等待状态。也就是说,在子进程退出之前,父进程是什么事情都不做的。如果我们想要父进程在这段时间里做一些事情,就要让父进程进行非阻塞等待。由于非阻塞等待较为复杂,所以一般用的都是阻塞等待

下面举个例子来帮我们更好地理解非阻塞等待:

张三打电话约李四出去玩,而李四在忙事情要张三等一会,于是张三就开始等待李四。在等待期间呢,张三做了一些其他事情,例如刷网课,写博客等。过了一会儿呢,张三又打电话问李四,李四说还没好,于是张三继续等待,顺便做一些其他事情。如此循环,直到李四忙完事情

在上面的例子中,张三代表父进程,李四代表子进程;打电话类似函数调用 waitpid,本质就是检测子进程的状态,这就是非阻塞等待。非阻塞等待通常都要结合循环使用,这就叫做非阻塞轮询

如何实现呢?将 waitpid 的参数options设置为WNOHANG

pid_t waitpid(pid_t pid, int *stat_loc, int options);

这时,waitpid 的返回值就不是两个了,变为了三个

  1. 返回值 > 0,等待成功,返回等待的子进程的pid
  2. 返回值 == 0,检测成功,只不过子进程还未退出,需要进行下一次等待
  3. 返回值 == -1,函数调用失败,也就是等待失败

下面我们就使用 waitpid + 循环,来实现非阻塞轮询

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>                                                                                                                                       
#include <sys/wait.h>
#include <stdlib.h>void ChildRun()
{int cnt = 5;while(cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}
}int main()
{printf("I am father process, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork(); // 创建子进程if (id == 0){// childChildRun(); // 子进程运行 5 秒printf("Child quit!\n");exit(1); // 子进程退出,设置退出码为 1}// father// 非阻塞 + 循环 = 非阻塞轮询while(1){int status; // 获得子进程退出信息pid_t rid = waitpid(id, &status, WNOHANG); // 非阻塞等待if (rid > 0){// 等待成功if (WIFEXITED(status) != 0){// 子进程正常退出,打印退出信息                                                                                                              printf("Child quit normal, exit_code: %d\n", WEXITSTATUS(status));}else{// 子进程异常退出printf("Child quit unnormal!\n");}printf("Wait sucess, rid:%d\n", rid);break;}else if (rid == 0){// 检测成功,子进程还未退出usleep(1000);printf("Child is running, father check next time!\n");// DoOtherThing();}else{// 等待失败printf("Wait failed!\n");break;                                                                                                                                           }} sleep(2);printf("Father quit\n");// 父进程退出return 0;
} 

运行结果:

在这里插入图片描述

非阻塞轮询,父进程会一直查询子进程的状态,如果子进程未退出,父进程就可以做一些其他事情,之后继续查询子进程状态,直到等待成功或者失败

接下来完善一下 DoOtherThing 函数,让父进程真的做一些事情。以下内容非必看

首先重定义函数指针,方便之后函数回调

typedef void(* func_t)();

然后定义一个函数指针数组,用来当作任务列表

#define N 3
func_t tasks[3];

task.c 和 task.h分别用来存放将来要处理的任务的定义与声明

task.h

#pragma once
#include <stdio.h>void PrintLog(); // 打印日志
void Download(); // 下载
void MysqlDataSync(); // 数据同步

task.c

void PrintLog()
{printf("start to PrintLog...\n");
}
void Download()
{printf("start to Download...\n");
}
void MysqlDataSync()
{printf("start to MysqlDataSync...\n");
}

然后再回到我们的主文件,有了任务后,就需要加载任务

void LoadTask()
{tasks[0] = PrintLog;tasks[1] = Download;tasks[2] = MysqlDataSync;
}

加载好任务后就可以处理任务了

void HandlerTask()
{for (int i = 0; i < N; i++){tasks[i]();}
}

最后,父进程要做的事情 DoOtherThing

void DoOtherThing()
{HandleTask();
}

在父进程创建子进程之后,就可以调用加载任务函数了,之后就进入非阻塞轮询

int main()
{printf("I am father process, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork(); // 创建子进程if (id == 0){// childChildRun(); // 子进程运行 5 秒printf("Child quit!\n");exit(1); // 子进程退出,设置退出码为 1}LoadTask(); // 加载任务// father// 非阻塞 + 循环 = 非阻塞轮询// ......

运行结果:

在这里插入图片描述

进程替换


代码 && 现象

直接来看进程替换需要使用的函数:exec*函数族

在这里插入图片描述

先使用最简单的 execl,看看现象是怎样的

int execl(const char *path, const char *arg, ...);

参数解释:

  1. path:我们要执行的程序的路径,例如 “/usr/bin/ls”
  2. arg:怎样执行程序,在命令行中怎么输入,在这里就怎么输入,不同的是要这样写:“ls -a -l” -> “ls”, “-a”, “-l”, NULL。注意一定要加 NULL
  3. ...可变参数,参数数量不固定,例如 printf 函数的参数就是这样的

下面写一个程序,并在中途替换为 ls 命令

#include <stdio.h>
#include <unistd.h>int main()
{printf("testexec start...\n");execl("/usr/bin/ls", "ls", "-a", "-l", "--color", NULL);printf("testexec end...\n");
}

运行结果:

在这里插入图片描述

可以看到,程序在打印"testexec start…“后,调用 execl 函数执行了 “ls -a -l --color”,但是并没有打印"testexec end…”,这是为什么呢?

下面说一下原理

原理

程序运行时会变为进程,进程 = 内核数据结构 + 代码和数据

在这里插入图片描述

假设上图就是我们正在运行的 testexec 程序的进程,当执行 execl 函数时,要替换的程序(例如上面的 ls)的代码和数据就会被加载到内存,覆盖掉被替换进程(testexec)的代码和数据,而内核数据结构基本不变,只有个别属性会变

在这里插入图片描述

那么这种情况下创建了新的进程吗?并没有,只是用旧进程的外壳执行新进程的代码和数据而已

因为原进程的代码和数据被覆盖了,所以 exec* 函数后面的代码都不见了,自然不会被执行了。这就是我们的 testexec 程序只打印了 start,没有打印 end 的原因

在这里插入图片描述

因为这种特性,我们也就不用关心 exec* 函数的返回值了

  1. 一旦进程替换成功,那么 exec* 函数后面的代码就没用了,有返回值也没有意义
  2. 如果 exec* 后面的代码还可以执行,那么就一定是进程替换失败

综上, exec* 函数就是相当于把要替换程序的代码和数据加载到内存中*。而将磁盘的数据加载到内存,这种级别的操作显然是操作系统才可以做的,所以 exec* 函数要么是系统调用,要么是内部调用了系统调用

多进程代码

我们之前的父子进程都是:子进程执行父进程的部分代码。了解 execl 的原理之后,子进程不再只可以执行父进程部分代码,而是可以执行一个全新程序,可以实现父进程和子进程各自执行不同的功能

由于进程之间的独立性,子进程在替换为新程序后,由于要覆盖代码和数据,自然是要发生写时拷贝的

在这里插入图片描述

下面的代码中,父进程创建子进程并等待子进程,而子进程的任务就是将自己替换为其他进程,这里还是用 “ls -a -l --color”

#include <stdio.h>
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    int main()    
{    printf("testexec start...\n");    pid_t id = fork();    if (id == 0)    {    // child    execl("/usr/bin/lss", "-a", "-l", "--color", NULL);    exit(1); // 代码运行到这里,说明替换失败}    // father    int status = 0;    pid_t rid = waitpid(id, &status, 0);    if (rid > 0)    {    // 等待成功    printf("father wait sucess, child exit_code: %d\n", WEXITSTATUS(status));    }    printf("testexec end...\n");    
}   

在这里插入图片描述

如果将 execl 中的参数给成错误的,进程就会替换失败,运行 exit(1) 将子进程退出码设为 1

在这里插入图片描述

参数含义

在这里插入图片描述

对于这么多的 exec* 函数,看起来很容易混,但只要掌握各个字母代表什么意思就很好记了

  • l:list,表示参数采用列表形式
  • v:vector,表示参数采用数组形式
  • p:PATH,表示自动搜索环境变量PATH
  • e:environment,表示用户自己维护环境变量
execl
int execl(const char *path, const char *arg, ...);
  • 参数中的 path 表示要到哪里寻找要执行的文件
  • arg 表示如何执行文件,以**列表(list)**的形式传参。例如"ls -a -l",传参形式为 “ls”, “-a”, “-l”, NULL

之前已经演示如何使用,这里不再赘述

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

v 表示以数组的形式传参,如下:

char *const argv[] = {"ls", "-a", "-l", NULL};

代码演示:

#include <stdio.h>
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    int main()    
{    printf("testexec start...\n");    pid_t id = fork();    if (id == 0)    {    // child    char *const argv[] = {"ls", "-a", "-l", NULL};execv("/usr/bin/ls", argv);// execl("/usr/bin/lss", "-a", "-l", "--color", NULL);    exit(1); // 代码运行到这里,说明替换失败}    // father    int status = 0;    pid_t rid = waitpid(id, &status, 0);    if (rid > 0)    {    // 等待成功    printf("father wait sucess, child exit_code: %d\n", WEXITSTATUS(status));    }    printf("testexec end...\n");    
}   

在这里插入图片描述

execvp 和 execlp
int execvp(const char *file, char *const argv[]);
int execlp(const char *file, const char *arg, ...);

p 表示传参时不需要写文件的路径,只需写文件名即可,系统会自动到环境变量 PATH 中查找。

代码演示:

char *const argv[] = {"ls", "-a", "-l", NULL};
execvp("ls", argv);

在这里插入图片描述

execvpe 和 execle
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execle(const char *path, const char *arg, ..., char * const envp[]);

e 表示自己维护环境变量,在演示之前,我们先来看一下如何启动自己写的程序

先写一个 C++ 程序

#include <iostream>using namespace std;int main()
{cout << "I am a C++ process" << endl;cout << "I am a C++ process" << endl;cout << "I am a C++ process" << endl;
}

然后编译形成 myprocess 可执行文件

在这里插入图片描述

然后在子进程中启动我们刚写的 C++ 程序,环境变量参数 envp 暂时填 NULL

#include <stdio.h>
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    int main()    
{    printf("testexec start...\n");    pid_t id = fork();    if (id == 0)    {    // child    char *const argv[] = {"myprocess", NULL};execvpe("./myprocess", argv, NULL);// execv("/usr/bin/ls", argv);// execl("/usr/bin/lss", "-a", "-l", "--color", NULL);    exit(1); // 代码运行到这里,说明替换失败}    // father    int status = 0;    pid_t rid = waitpid(id, &status, 0);    if (rid > 0)    {    // 等待成功    printf("father wait sucess, child exit_code: %d\n", WEXITSTATUS(status));    }    printf("testexec end...\n");    
}   

运行结果:

在这里插入图片描述

这就说明启动我们自己写的程序也是可以的,这时候我们也可以修改一下命令行参数,并且自定义一个环境变量表,再传给我们的 C++ 程序

char *const argv[] = {"myprocess", "-a", "-b", NULL}; // 命令行参数
char *const envp[] = {"HAHA=123", "HEHE=456", NULL}; // 自定义环境变量表
execvpe("./myprocess", argv, envp);

并让 C++ 程序将这些命令行参数打印出来

#include <iostream>2 3 using namespace std;4 5 int main(int argc, char* argv[], char* env[])6 {7     // 命令行参数8     for (int i = 0; i < argc; i++)9     {10       printf("argv[%d]: %s\n", i, argv[i]);11     }12 13     printf("-----------------------------\n");                                                                                                           14 15     // 环境变量16     for (int i = 0; env[i]; i++)17     {18       printf("env[%d]: %s\n", i, env[i]);19     }20 21     cout << "I am a C++ process" << endl;22     cout << "I am a C++ process" << endl;23     cout << "I am a C++ process" << endl;24 }

然后运行我们的 testexec 程序:

在这里插入图片描述

这时我们子进程替换的 C++ 程序成功地将传给 execvpe 函数的参数打印了出来。

我们不但可以给 execvpe 传自定义的环境变量,还可以把 bash 维护的 env 表传给它。因为环境变量具有全局性,我们在环境变量说过。这里再解释一遍:当前的子进程的父进程是 main 函数,而 main 函数的父进程是 bash,因为子进程可以继承父进程的数据,所以当前子进程自然可以得到 env 表

char *const argv[] = {"myprocess", "-a", "-b", NULL}; 
extern char** environ;
execvpe("./myprocess", argv, envp, environ);

运行结果:

在这里插入图片描述

到现在我们可以给 execvpe 传的环境变量为:

  1. 自己定义的全新环境变量
  2. bash 维护的 env 表

此外,我们还可以对当前进程持有的 env 表修改后传给 execvpe,需要用到一个函数:putenv

将要添加的环境变量传给 putenv 后,即可在 env 表新增环境变量

在这里插入图片描述

char *const argv[] = {"myprocess", "-a", "-b", NULL};
putenv("HELLO=1111111111111111"); // 向 env 表添加环境变量
extern char** environ;
execvpe("./myprocess", argv, envp, environ);

运行结果:

在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com