【Linux】非阻塞等待 | 轮询监测机制 | 进程程序替换
创始人
2024-06-02 13:17:23
0

  🤣 爆笑教程 👉 《看表情包学Linux》👈 猛戳订阅  🔥

💭 写在前面:大家好,我是柠檬叶子C,在上一章中我们讲了 waitpid 的 status 参数,本章我们讲解它的 options 参数。在讲解之前我们需要理解进程阻塞,介绍非阻塞的轮询检测机制,学会 waitpid。然后我们重点讲解二进程程序替换,这是本章的重点,如何让子进程执行一个新的程序?本章之前,我们都是让子进程执行父进程代码的,本章我们让子进程能够开启属于自己的 "全新旅程",这听上去很 cool,通过讲解进程替换的替换原理,然后先介绍一个进程替换函数 execl,通过介绍这个函数来打开突破口,引入进程创建的知识点。最后,我们在学习进程创建的 exec 函数簇,这里面有很多形似 execl 的,各种关于替换的函数,因为它们的功能大同小异,所以我们称之为函数簇。话就说到这里,让我们速速开始今天的学习!


Ⅰ. 进程阻塞(Process Blocking)

0x00 回顾:继续讲解 waitpid

 我们先来简单回顾一下上一章的内容:

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

上一章介绍了 status 参数,知道了如何通过位操作来截 status 获取进程错误码与错误信号:

status&0x7F        // 获取错误信号
(status>>8)&0xFF   // 获取错误码

 使用这种方式前提是你必须得 status 位图的构成,操作系统设计成接口的初衷是让你用起来更简单,所以准备了宏。当然,如果你就用位操作来获取也是可以的。但是我们还是不建议这么做,因为直接用操作系统提供的宏就好了,我们可以通过 WIFEXITED 宏来检测子进程是否正常退出(检测进程退出时信号是否为 0),在用 WEXITSTAUS 宏还获取进程的退出码:

if (WIFEEXITED(status)) 
{printf("等待成功: exit code: %d\n", WIFEEXITED(status));
}

这些都是上一章讲解 status 参数的内容了,我们下面要讲的是 waitpid 另一个参数 options。 

options 为 0,则标识为 阻塞等待 (关于阻塞等待的概念我们之前将进程状态时说过)

比如:如果子进程不退出,父进程在等,等的时候子进程是卡在那等的,在用户的角度就是:

" 沃日怎么卡了"

 这种用户级的现象就叫作阻塞等待!

0x01 如何理解进程阻塞?

❓ 思考:如何理解父进程进程阻塞?

首先,进程状态我们说过:如果一个进程在系统层面上要等待某件事情发生,

但这件事情还没发生,那么当前进程的代码还没法向后运行,只能让该进程处于阻塞状态。

就是让父进程的 task_struct 状态由 R\rightarrow S,从运行队列投入到等待队列,等待子进程退出。

子进程退出的本质是条件就绪,如果子进程退出条件一旦就绪,操作系统会逆向地做上述工作。

将父进程的 \textrm{pcb} 从等待队列再搬回运行队列,并将状态 S\rightarrow R,此时父进程就会继续运行。

0x02 非阻塞的轮询检测机制

所谓的阻塞,其实就是挂起。在上层表现来看,就是进程卡住了(比如 scanf,cin 等)。

阻塞式等待就是 "傻等",就是在等的时侯父进程什么也不做,就坐那傻等。

  

而非阻塞式等待是 "巧等",会做些自己的事,而不是一屁股做那傻等!

多次调用非阻塞接口,这个过程我们称之为 轮询检测 (Polling)。

"轮询检测检测期间,你不退,我就直接返回做我自己的事情去啦"

我们上一章中讲解 waitpid 时,举的例子都是 阻塞式 的等待。

如果我们想 非阻塞式 的等,我们可以设置 options 选项为 WNOHANG (With No Hang)。

这个选项通过字面很好理解,就是等待的时候不要给我挂 (Hang) 住,其实就是非阻塞!

现在我们正式介绍一下 waitpid 的返回值:

  • 如果此时等待成功,返回值是子进程的退出码。
  • 如果你是非阻塞等待 (WNOHANG),等待的子进程没有退出,返回值为 0。

如果我们想把我们上一章节,演示 waitpid 使用方式的代码,改为非阻塞等待。

 我们只需要将 waitpid 的 options 参数加上。

💬 代码演示:基于非阻塞的轮询等待

#include 
#include 
#include 
#include 
#include 
#include int main(void)
{pid_t id = fork();if (id == 0) {// 子进程while (1) {printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());sleep(5);  // 先睡眠 5s,5s后退出break;}exit(233);}else if (id > 0) {// 父进程/* 基于非阻塞的轮询等待方案 */int status = 0;while (1) {pid_t ret = waitpid(-1, &status, WNOHANG);if (ret > 0) {          // 等待成功printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);}else if (ret == 0) {    // 等待成功,但是子进程没有退出printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n");sleep(1);}else {// 出错了,暂时不作处理}}}else {// 什么也不做}
}

💡 说明:我们只需要将 waitpid 中的 options 参数带上 WHOHANG 就可以了。返回值 ret>0 就是等待成功,我们这里新增一个等于 0 的判断,作为 "等待成功但是子进程还没有退出" 的情况,因为等待的子进程没有退出,返回值为 0 。运行后,就会问子进程好没好,如果没有好父进程就可以做自己的事情了,而不是在那傻等子进程!这,就是非阻塞式轮询等待。

 🚩 运行结果如下:

我们可以让父进程在非阻塞等待时真正做点事,别让父进程在等子进程闲着。

💬 代码演示:因为要使用 vector,我们创建 .cpp 文件

#include 
#include 
#include 
#include 
#include 
#include 
#include typedef void (* handler_t)();  // 函数指针类型// 方法集
std::vector handlers;void func1() {printf("Hello,我是方法1\n");
}
void func2() {printf("Hello,我是方法2\n");
}void Load() {// 加载方法handlers.push_back(func1);handlers.push_back(func2);
}int main(void)
{pid_t id = fork();if (id == 0) {// 子进程while (1) {printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());sleep(3);}exit(233);}else if (id > 0) {// 父进程/* 基于非阻塞的轮训等待方案 */int status = 0;while (1) {pid_t ret = waitpid(-1, &status, WNOHANG);if (ret > 0) {          // 等待成功printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);}else if (ret == 0) {    // 等待成功,但是子进程没有退出printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n");if (handlers.empty()) {Load();}for (auto f : handlers) {f();  // 回调处理对应的任务}sleep(1);}else {// 出错了,暂时不作处理}}}else {// 什么也不做}
}

如果你想要你的程序直接父进程做更多的事情,把方法加到 Load 里就可以了。

 写下 Makefile:

mytest:mytest.cppg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f mytest

🚩 运行结果如下:

Ⅱ. 进程程序替换(Process Substitution)

0x00 引入:让子进程执行一个新的程序

我们之前做的所有代码演示,子进程执行的都是父进程的代码片段。

 如果我们想让创建出来的子进程,执行全新的程序呢?

回顾:我们曾经创建的子进程和父进程是代码共享的,通过 if-else  同时执行(写时拷贝),经过同一个变量通过虚拟地址转化为物理地址,让父子进程得到不同的值,从而判断出来让父子进程执行不同的代码片段。这是我们之前的操作。

之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换

a238ee82fbb64fa0a1898b7c1b4e6552.jpeg为什么要进行程序替换?因为我们想让我们的子进程执行一个全新的程序。

那为什么要让子进程执行新的程序呢?

我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:

  • 让子进程执行父进程的代码片段(服务器代码…)
  • 想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)

 然而,这些操作都是真的,这不是梦 ~

0x01 替换原理

📃 程序替换的原理:

  • 将磁盘中的内存,加载入内存结构。
  • 重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。
  • 效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序!

这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!

因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。

 内核数据结构没有发生任何变化! 包括子进程的 \textrm{pid}\textrm{pid} 都不变,说明压根没有创建新进程。

0x02 以可变参数列表的接收参数的 execl 接口

我们要调用接口,让操作系统去完成这个工作 —— 系统调用。

如何进行程序替换?我们先见见猪跑 —— 从 execl 这个接口讲,看看它怎么跑的。

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

如果我们想执行一个全新的程序,我们需要做几件事情:

(要执行一个全新的程序,以我们目前的认识,程序的本质就是磁盘上的文件)

  • 第一件事情:先找到这个程序在哪里。
  • 第二件事情:程序可能携带选项进行执行(也可以不携带)。

明确告诉 OS,我想怎么执行这个程序?要不要带选项。

\Rightarrow 简单来说就是:① 程序在哪?  ② 怎么执行?

 所以,execl 这个接口就必须得把这两个功能都体现出来!

  • 它的第一个参数是 path,属于路径。
  • 参数  const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。

💬 代码演示:exec()

#include 
#include int main(void)
{printf("我是一个进程,我的PID是:%d\n", getpid());// ls -a -lexecl("/usr/bin/ls", "ls", "-l", "-a", NULL);  // 带选项printf("我执行完毕了,我的PID是:%d\n", getpid());return 0;
}

🚩 运行结果如下:

 刚才是带选项的,现在我们再来演示一下不带选项的: 

#include 
#include int main(void)
{printf("我是一个进程,我的PID是:%d\n", getpid());// topexecl("/usr/bin/top", "top", NULL);  // 不带选项printf("我执行完毕了,我的PID是:%d\n", getpid());return 0;
}

 🚩 运行结果如下:

这样我们的程序就直接能执行 top 命令了,除此之外,我们曾经学的大部分命令其实都可以通过 execl 执行起来。这就叫做 程序替换

不知道大家有没有发现问题?代码和输出结果有什么不对劲的地方?

最后一句代码 ——  "我执行完毕了,我的PID是" 似乎没有打印出来啊?

 为什么我们最后的代码并没有被打印出来?

因为 一旦替换成功,是会将当前进程的代码和数据全部替换的!

所以自然后面的 printf 代码早就被替换了,这意味着该代码不复存在了,荡然无存!

因为在程序替换的时候,就已经把对应进程的代码和数据替换掉了!

而第一个 printf 执行了的原因自然是因为程序还没有执行替换,

调了 execl 后的所有代码就会被:

" 全 部 曹 飞 "

所以,这里的程序替换函数用不用判断返回值?为什么?

int ret = execl(...);

一旦替换成功,还会执行返回语句吗?返回值有意义吗? 没有意义的!

 程序替换不用判断返回值!因为只要成功了,就不会有返回值。 而失败的时候,必然会继续向后执行。通过返回值最多能得到是什么原因导致替换失败的。只要执行了后面的代码,看都不用看,一定是替换失败了;只要有返回值,就一定是替换失败了。

我们来模拟一下失败的情况,我们来执行一个不存在的指令 giveMeMoney:

#include 
#include int main(void)
{printf("我是一个进程,我的PID是:%d\n", getpid());// 我们用 ret 接收函数返回值结果int ret = execl("/usr/bin/giveMeMoney", "ls", "-l", "-a", NULL);printf("我执行完毕了,我的PID是:%d,ret: %d\n", getpid(), ret);return 0;
}

🚩 运行结果如下:

💡 说明:execl 替换失败,就会继续向后执行。但是,一旦 execl 成功后就会跟着新程序的逻辑走,就不会再 return 了,再也不回来了,所以返回值加不加无所谓了。

* 先打个预防针:下面我们会讲解一大坨 exec 函数簇,特性和这个 execl 大同小异!

0x03 引入进程创建

"子进程:以前我没得选,现在我免费了!我要脱离父进程的怀抱了!(叛逆期)"

以前我们的示例都是让子进程执行父进程的代码,我们今天想让子进程执行自己的程序。

#include 
#include 
#include 
#include int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());execl("/usr/bin/ls", "ls", "-a", "-l", NULL);  /* 让子进程执行替换 */exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

🚩 运行结果如下:

💡 说明:成功执行代码,父进程也等待成功了。这里的子进程没有执行父进程的代码,而是执行了自己的程序,它开启了自己的旅程,执行了 execl 替换后直接高高兴兴地扬长而去了。

 诶……子进程执行程序替换,会不会影响父进程呢?不会!因为进程具有独立性。

为什么?如何做到的?子进程是如何做到代码和数据做分离的呢?

让子进程与父进程做相似的代码片段,子进程改了,父进程也不受影响。

我们在前几章,讲过数据层面发生写时拷贝的概念。

我们说过:fork 之后父子是共享的,如果要替换新的程序我能理解把新的程序的代码加载到内存里,我的子进程新的代码程序出来之后发生数据的写时拷贝,生成新的数据段。

不是说代码是共享的吗?我们该如何去理解呢?

当程序替换的时候,我们可以理解成 —— 代码和数据都发生了写时拷贝,完成了父子分离。

Ⅲ. exec 函数簇(Sheaf of functions exec)

0x00 以指针数组接收参数的 execv 接口

刚才我们学会了 execl 接口,我们下面开始学习更多的 exec 接口!它们都是用来替换的。

下面我们先来讲解一下和 execl 很近似的 execv:

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

path 参数和 execl 一样,关注的都是 "如何找到" 

argv[] 参数关注的是 "如何执行",是个指针数组,放 char* 类型,指向一个个字符串。

大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。

所以 execv 和 execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。

值得注意的是,在构建 argv[] 的时,结尾仍然是要加上 NULL!

💬 代码演示:execv()

#include 
#include 
#include 
#include int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());char* const argv_[] = {(char*)"-ls",    /* 这里强转消warning*/(char*)"-l",(char*)"-a",NULL};execv("usr/bin/ls", argv_);   /* 只是变为数组传参了而已 */exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

🚩 运行结果如下:

关于 execl 和 execv,我们可以这么记:

乱入的小贴士

批量化注释:ctrl+v  ->  hjkl 选中区域  -> 切换大写,输入 I    //, esc

去注释:小写,ctrl+v -> hjkl 选中区域(注释区域), d

0x01 无需带路径就能直接执行的 execlp 接口(可变参数列表)

int execlp(const char* file, const char* arg, ...);

execlp,它的作用和 execv、execl 是一样的,它的作用也是执行一个新的程序。

 仍然是需要两步:① 找到这个程序   ② 告诉我怎么执行

第一个参数 file 也是 "你想执行什么程序",第二个参数 arg 是 "如何去执行它"。

所以这一块的参数传递,和 execl 是一样的,唯一的区别是比 execl 多了一个 p

我们执行指令的时候,默认的搜索路径在环境变量 \textrm{PATH} 中,所以这个 p 的意思是环境变量。

这意味着:执行 execlp 时,会直接在环境变量中找,不用去输路径了,只要程序名即可。

我们上面例子中的代码,无论是 execl 还是 execv,执行程序都得带上路径。

而 execlp 可以不带路径,只说出你要执行哪一个程序即可,美滋滋:

execlp("ls", "ls", "-a", "-l", "NULL");   // 路径都不用,直接扔

 值得一提的是:这里出现的两个 ls 含义是不一样的,是不可以省略滴。

"老老实实敲,切勿投机取巧!"

  • 第一个参数是 "供系统去找你是谁的",后面的一坨代表的是 "你想怎么去执行它" 。

💬 代码演示:execlp()

#include 
#include 
#include 
#include int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());execlp("ls", "ls", "-a", "-l", NULL);exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

🚩 运行结果如下:

0x02 无需带路径的 execvp 接口(指针数组)

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

看到这里,想必大家光看到这个接口的名字,就能猜到它是什么意思了。

execvp 也是带 p 的,执行 execvp 时,会直接在环境变量中找,只要程序名即可。

简单来说就是 execv 的带 p 版本罢了,将命令行参数字符串,统一放入数组中即可完成调用。

想必大家早已轻车熟路了,我们直接演示就完事了!

💬 代码演示:execvp()

#include 
#include 
#include 
#include int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());char* const __argv[] = {(char*)"top",NULL};execvp("top", __argv);  // 只需要给个名就行exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

🚩 运行结果如下:

0x03 利用 exec 调各种程序(可耦合各种语言)

目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的 C/C++ 程序呢?

如果我们要执行其他语言写的程序呢?在回答这个问题前,我们先补充一下 Makefile 的知识。

Makefile 知识点补充

我们前几章写的 Makefile 文件只能形成一个可执行程序,现在我们学习如何形成多个。

比如,如果我们想一口气形成 2 个 可执行程序:

假设有两个可执行程序:mycmd.cpp & mytest.c,我们期望用 mytest.c 调用 mycmp.cpp:

/* mycmd.cpp */
#include int main(void)
{std::cout << "hello c++" << std::endl;return 0;
}

也就是 C 语言的可执行程序调用 C++ 的可执行程序,我们先来设计一下 Makefile。

我们需要在前面添加 .PHONY:all ,让伪目标 all 依赖 mytest 和 mycmd。

如果不这样做,直接写,默认生成的是 mycmd,轮不到后面的 mytest,属于 "先到先得"。

且 Makefile 默认也只能形成一个可执行程序,想要形成多个就需要用到 all 了。

.PHONY:all
all:mytest mycmdmytest:mytest,cgcc -o $@ $^
mycmd:mycmd.cppg++ -o $@ $^
.PHONY:clean
clean:rm -f mytest mycmd

Makefile 搞定了,现在我想用我的 mytest.c 去调用 mycmd.cpp 这个程序,如何做呢?

我们先 pwd 获取得到 mycmd.cpp  的绝对路径:

 下面,我们隆重有请我们的 execl 再次登场!!!

💬 代码演示:通过使用 execl() 来调用自己写的程序

#include 
#include 
#include 
#include int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());execl("/home/foxny/lesson14/mycmd", "mycmd", NULL);exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

🚩 运行结果如下:

💡 说明:通过绝对路径已经可以找到 mycmd 了,所以第二个参数也就不需要带 ./ 了,直接 mytest 即可。对于第三个参数,因为我们的 mycmd 没有设置任何选项,所以设置成 NULL。

"这个 NULL 真的是贯穿整个 exec 函数簇,一定别忘了这玩意!"

上面的代码演示我们使用的是绝对路径,用相对路径也是可以的,只要能找到对应目标就行:

execl("./mycmd", "mycmd", NULL);

这说明程序替换可以用我们自己写的程序,将 C 语言程序调用我们的 C++ 代码。

我们再试试用 C 调用 Python 看看,我们先创建一个 test.py 文件:

# test.py
print("Hello, Python");

然后我们输入 python test.py 来运行:

现在,我想我的用 C 语言程序调用 Python 程序,我们可以:

execl("/usr/bin/python3", "python3", "test.py", NULL);

此时我们要执行的是 /usr/bin 目录下的 python3,直接带入刚才的演示代码:

🚩 运行结果如下:

exec 的系统级函数,可以把任何语言耦合到一起!!!

0x04 添加环境变量给目标进程的 execle 接口

int execle(const char* path, const char* arg, ..., char* const envp[]);

我们可以使用 execle 接口传递环境变量,相当于自己把环境变量导进去。

打开 mycmd 文件,我们加上几句环境变量:

#include 
#include int main(void)
{std::cout << "PATH:" << getenv("PATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "hello c++" << std::endl;return 0;
}

 其中,\textrm{PATH} 是自带的环境变量,\textrm{MYPATH} 是我们自己的环境变量:

#include 
#include 
#include 
#include int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());char* const __env[] = {(char*)"MYPATH=YouCanSeeMe!",NULL};execle("./mycmd", "mycmd", NULL, __env);  // 手动传递环境变量exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

🚩 运行结果如下:

我们自己定义的 \textrm{MYPATH} 环境变量也没出来,我们先把 mycmd 的 \textrm{PATH} 注释掉,然后再运行:

#include 
#include int main(void)
{// std::cout << "PATH:" << getenv("PATH") << std::endl;// std::cout << "--------------------------------------\n";std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "hello c++" << std::endl;return 0;
}

 所以,为什么会出现这种情况?

我自己不定义自己的环境变量时,\textrm{PATH} 可以获取得到,一旦我传入了一个自己定义的环境变量时,\textrm{PATH} 就打不出来了?!

execle 接口,这个 e 表示的是 添加环境变量给目标进程,如果是自己的变量,那么是覆盖式的。

所以,如果你想把系统的环境变量 \textrm{PATH} 传给它,我们需要 extern 环境变量的指针申明:

extern char** environ;     // 环境变量的指针声明
execle("./mycmd", "mycmd", NULL, environ);

💬 代码演示:execle() 

#include 
#include 
#include 
#include int main(void) 
{extern char** environ;     // 环境变量的指针声明printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());char* const __env[] = {(char*)"MYPATH=YouCanSeeMe!",NULL};// execle("./mycmd", "mycmd", NULL, __env);  // 手动传递环境变量execle("./mycmd", "mycmd", NULL, environ);exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

 我们现在再把 mycmd.cpp 注释恢复:

#include 
#include int main(void)
{// std::cout << "PATH:" << getenv("PATH") << std::endl;// std::cout << "--------------------------------------\n";std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "hello c++" << std::endl;return 0;
}

🚩 运行结果如下:

因为系统本来就没有 \textrm{MYPATH},我们手动添加:

$ export MAPATH="哈哈哈哈哈"

如果你想把环境变量信息传给子进程,甚至子子进程,

都可以手动地用 execle 把环境变量传递过去,如果你想自定义,自己创建 env 传递就可以了:

        char* const __env[] = {(char*)"MYPATH=YouCanSeeMe!",NULL};

0x05 也是环境变量,但是拿数组传的 execve 接口

环境变量的概念和上面的 execle 一模一样,唯一区别只是:

execle 参数传递是参数列表,execve 参数传递是数组,仅此而已:

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

0x06 超级缝合怪 execvpe 接口

缝合怪罢了,v - 数组,p - 文件名,e - 可自定义环境变量:

int execvpe(const char* file, char* const argv[], char* const envp[]);

这也没什么好说的,execle、execve、execvpe 都是 "环境变量" 一伙的。

0x07 为什么会有这么多 exec 接口?

 为什么搞这么多接口,烦死了,这些接口好像没有太大的差别啊。

唯一的差别就是传参的方式不一样,有的带路径,有的不带路径,有的是列表传参,有的是数组传参,有的可带环境变量,有的不带环境变量。

因为要适配各种各样的应用场景,使用的场景不一样,有些人就喜欢列表传参,有些人喜欢数组传参。所以就配备了这么多接口,这就好比我们 C++ 函数重载的思想。 

那为什么 execve 是单独的呢?

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

它处于 man 2 号手册,execve 才属于是真正意义上的系统调用接口。

而刚才介绍的那些,实际上就是为了适配各种环境而封装的接口:

 总结一下它们的命名规律,通过这个来记忆对应接口的功能会好很多:

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

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2023.3.10
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. Linux[EB/OL]. 2021[2021.8.31 xi

相关内容

热门资讯

开晨会需要说什么内容 开晨会需要说什么内容晨会说的内容可以很多呀,可以说接下来的工作安排,也可以说,前期工作中的一些总结,...
有谁知道在鬼吹灯有声小说中前面... 有谁知道在鬼吹灯有声小说中前面有一段小女孩的声音念:人点烛 鬼吹灯... 这个版本的故事是谁讲的?周...
小蒂皮出自哪本书 小蒂皮出自哪本书小蒂皮出自哪本书我的野生动物朋友(蒂皮·本杰明·奥康迪·德格雷著图书)《我的野生动物...
盘古开天辟地的故事 盘古开天辟地的故事很久很久以前,天和地还没有分开,宇宙混沌一片。有个叫盘古的巨人一直睡在这混沌之中。...
英雄联盟中 赏金猎人怎么样? ... 英雄联盟中 赏金猎人怎么样? 多少钱 金币3150的ADC。入手的话不会吃亏,基本不会压箱底,很热门...
寻找一部累死何以笙箫默之类的小... 寻找一部累死何以笙箫默之类的小说也是辛夷坞的,叫山月不知心底事那就它的作者顾漫的其他小说呗 微微一...
大家谈谈对洪荒小说之中圣人的看... 大家谈谈对洪荒小说之中圣人的看法圣人不死,大盗不止都是人 人有的情绪他们都有 就是拳头大些对...
单位体检,自己一个人去害怕撞到... 单位体检,自己一个人去害怕撞到同事怎么办,不想别人看到观察我,漏出我不成熟样子,显出我的丑态。这让单...
鱼在天上飞? 鱼在天上飞?“鱼在天上飞,鸟在水里游”这句话的意思是鸟儿在水中的倒影就像是在水里游,天空在水中的倒影...
我想复婚,前夫说以后再,我改变... 我想复婚,前夫说以后再,我改变好了,他有可能追我,现在不想复婚,他有喜欢的女人了,我们就不可能了吗感...
原神草神什么时候复刻 原神草神什么时候复刻原神草神复刻是在3.6版本以后。草神纳西妲首次登场是逗腊3.2版本,新角色想要等...
网络拽姐语录小学生? 网络拽姐语录小学生?无论谁离开了你,请你别忘了,他没来之前,你本就是一个人生活。
有一部小说,女主小时候被男主收... 有一部小说,女主小时候被男主收养了,女主叫男主哥哥,男主对女主的我也在找这个小说我之前也看了,好像叫...
高层建筑立面大面积粉刷需要设伸... 高层建筑立面大面积粉刷需要设伸缩缝吗?高层建筑立面大面积粉刷需要设伸缩缝需要的,无正举论地面或立念清...
低头思故乡,为什么把杯思在长,... 低头思故乡,为什么把杯思在长,情愿何处在方歌曲叫什么名字“李白的歌”离开了家乡 背着沉沉的行囊开始了...
李白的诗有多少 李白的诗有多少典故中大多说是三千至四千首,现存不足一千首很多,总之不少~李白一生留下了九百六十多首诗...
怎么才可以学会拿得起放得下……... 怎么才可以学会拿得起放得下…… 有一段感情,正整3年了,可是我还是觉得分手那天就好像昨天发生的一样替...
为什么官方直播间买手机送耳机 为什么官方直播间买手机送耳机吸引消费者,增加销售量。官方直播间作为官方的直播平台,买手机送耳机是为了...
自考通还是一考通好?该选哪个? 自考通还是一考通好?该选哪个?我现在自考行政管理专科。专业课程我应该选自考通好还是一考通好呢?这个很...
淘宝实际付款价格为什么与订单价... 淘宝实际付款价格为什么与订单价格不符是这样的,我买了一双鞋,当时限价160我拍了下来,当时没付款,后...