炸裂!Linux 程序员必看的 “进程换装秀”:exec 换程序如换衣服,PID、父子关系、env 一个都不丢,绝了!
文 章 目 录
- 一、进 程 程 序 替 换
-
- 1、定 义
- 2、execl 系 列
- 3、现 象
- 4、原 理
- 5、父 子 进 程
- 6、结 论
- 二、程 序 替 换 的 接 口
-
- 1、7 个 接 口 函 数
- 2、函 数 解 释
- 3、命 名 理 解
- 4、execl
- 5、execlp
- 6、execv
- 7、execvp
- 8、execle
- 9、脚 本
- 10、putenv
- 11、execle
-
- (1)系 统 环 境 变 量
- (2)自 定 义 环 境 变 量
- 12、总 结
- 三、总 结
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 Linux。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:Linux 探 索 之 旅:从 命 令 行 到 系 统 内 核
✨代 码 趣 语:进 程 的 “换 装 游 戏” 里,exec 是 化 妆 师,PID 是 身 份 证 - - - 旧 数 据 成 了 褪 色 的 残 影,新 代 码 踩 着 调 度 的 节 拍,在 CPU 舞 台 上 重 启 了 舞 步。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee
Linux 中,fork 创 建 的 子 进 程 常 需 执 行 新 程 序(而 非 重 复 父 进 程 代 码),这 就 依 赖 进 程 程 序 替 换:通 过 exec 系 列 函 数,用 新 程 序 覆 盖 当 前 进 程 的用 户 空 间 代 码、数 据,从 新 程 序 启 动 例 程 执 行,且 PID 不 变。本 文 将 解 析 其 定 义、原 理,详 解 7 个 exec 接 口 差 异,结 合 父 子 进 程、环 境 变 量 等 场 景 说 明。
一、进 程 程 序 替 换
1、定 义
fork 创 建 子 进 程 后 执 行 的 是 和 父 进 程 相 同 的 程 序(但 有 可 能 执 行 不 同 的 代 码 分 支),子 进 程 往 往 要 调 用 一 种 exec 函 数 以 执 行 另 一 个 程 序。当 进 程 调 用 一 种 exec 函 数 时,该 进 程 的 用 户 空 间 代 码 和 数 据 完 全 被 新 程 序 替 换,从 新 程 序 的 启 动 例 程 开 始 执 行。调 用 exec 并 不 创 建 新 进 程,所 以 调 用 exec 前 后 该 进 程 的 id 并 未 改 变。
2、execl 系 列
执 行 对 应 的 文 件。execl 是 一 个 可 变 参 数,可 以 传 不 同 的 选 项。
3、现 象
#include<stdio.h>#include<unistd.h>#include<stdlib.h>int main(){ printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); execl(\"/usr/bin/ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); return 0;}
excel 执 行 了 ls -al 跳 过 了 after 这 条 语 句。这 种 现 象 叫 做 程 序 替 换。程 序 替 换 成 功 之 后 的 代 码 不 会 被 执 行。exec 系 列 的 函 数 只 有 失 败 返 回 值 没 有 成 功 返 回 值。
4、原 理
程 序 替 换 指 的 是 通 过 系 统 调 用,用 一 个 新 的 程 序 替 换 当 前 进 程 的 代 码、数 据、堆 栈 等 内 容,而 进 程 的 PID(进 程 ID)保 持 不 变 的 操 作。
5、父 子 进 程
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); sleep(5); execl(\"/usr/bin/ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
6、结 论
-
子 进 程 的 程 序 替 换 并 不 会 影 响 父 进 程 的 代 码,是 因 为 写 时 拷 贝 以 及 进 程 之 间 的 独 立 性。替 换 数 据 和 代 码 是 写 时 拷 贝,如 果 要 写 入 的 区 域 是 只 读 的 并 且 代 码 区 是 父 子 进 程 共 享 的,此 时 不 能 直 接 替 换,应 进 行 写 时 拷 贝。
-
程 序 替 换 没 有 创 建 新 进 程,只 进 行 进 程 的 程 序 代 码 和 数 据 的 替 换 工 作。
-
替 换 之 后 父 子 关 系 不 发 生 改 变,父 进 程 等 待 的 是 子 进 程 的PCB,
cpu 知 道 程 序 的 入 口 地 址 是 因 为 linux 中 形 成 可 执 行 程 序 是 有 格 式 的,为 ELF,有 可 执 行 程 序 的 表 头,可 执 行 程 序 的 入 口 地 址 就 在 表 中。
二、程 序 替 换 的 接 口
1、7 个 接 口 函 数
linux 总 共 有 7 个 接 口 函 数,在 3 号 手 册 中 有 6 个,2 号 手 册 中 有 1 个。
3 号 手 册
这 6 个 函 数 的 区 别 是 传 参 的 不 同,都 是 C 语 言 的 库 函 数。
2 号 手 册
这 个 是 操 作 系 统 提 供 的 接 口,为 系 统 调 用,
2、函 数 解 释
这 六 种 以 exec 开 头 的 函 数 统 称 exec 函 数,这 些 函 数 如 果 调 用 成 功 则 加 载 新 的 程 序 从 启 动 代 码 开 始 执 行 不 再 返 回。如 果 调 用 出 错 则 返 回 -1。所 以 exec 函 数 只 有 出 错 的 返 回 值 而 没 有 成 功 的 返 回 值。
3、命 名 理 解
这 些 函 数 原 型 看 起 来 很 容 易 混,但 只 要 掌 握 了 规 律 就 很 好 记。
l(list):表 示 参 数 采 用 列 表
v(vector):参 数 用 数 组
p(path):有 p 自 动 搜 索 环 境 变 量 PATH
e(env):表 示 自 己 维 护 环 境 变 量
4、execl
exec 系 列 的 所 有 函 数 名 开 头 都 是 exec,execl 中 的 l 可 以 理 解 为 list
,execl 中 可 以 使 用 可 变 参 数,即 一 个 一 个 传 参。命 令 行 中 如 何 写 的,传 参 时 就 怎 样 传,只 是 将 空 格 换 成 逗 号,然 后 最 后 添 加 NULL
即 可。
5、execlp
execlp 中 p 指 的 是 PATH,即 execlp 自 己 会 在 默 认 的 PATH 环 境 变 量 中 查 找,使 用 时 可 以 添 加 路 径 也 可 以 不 添 加 路 径。
#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); sleep(5); //execl(\"/usr/bin/ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 //execlp(\"/usr/bin/ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 execlp(\"ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
6、execv
v 指 的 是 vector 即 数 组 或 者 顺 序 表。第 1 个 参 数 是 路 径。第 2 个 参 数 的 类 型 是 字 符 串 指 针 数 组,将 选 项 和 指 令 当 做 字 符 串 存 储。const 的 意 思 是 指 针 本 身 不 能 被 修 改。
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); sleep(5); //execl(\"/usr/bin/ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 //execlp(\"/usr/bin/ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 //execlp(\"ls\",\"ls\",\"-a\",\"-l\",NULL);//必须以NULL结尾 char *const myargv[] = {\"ls\",\"-a\",\"-l\",NULL}; execv(\"/usr/bin/ls\",myargv); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
linux 当 中 所 有 的 进 程 一 定 是 别 人 的 子 进 程,在 命 令 行 当 中,所 有 的 进 程 都 是 bash 的 子 进 程。
exec 系 列 的 函 数 承 载 着 加 载 器 的 效 果,是 代 码 级 别 的 加 载 器,将 可 执 行 程 序 导 入 内 存 中。
7、execvp
execvp
则 直 接 使 用 可 执 行 程 序 的 名 字 即 可。
实 际 是 将 myargv 的 参 数 传 递 给 ls。
8、execle
execle 中 的 e 表 示 环 境 变 量,第 1 个 参 数 为 文 件 的 路 径。
exec* 能 够 执 行 系 统 命 令,可 以 执 行 自 己 的 命 令。
mytest.c
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); execl(\"./mytestcpp\",\"mytestcpp\",NULL); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
mytest.cpp
#include<iostream>using namespace std;int main(){ cout << \"hello world\" << endl; return 0;}
makefile
.PHONY:allall:mytestc mytestcppmytestc:mytest.cgcc mytest.c -o mytestc -std=c99mytestcpp:mytest.cppg++ mytest.cpp -o mytestcpp -std=c++11.PHONY:cleanclean:rm -rf mytestc mytestcpp
9、脚 本
以 .sh
结 尾 的 文 件 为 shell 脚 本。脚 本 语 言 都 以 #!
作 为 开 头。后 面 是 脚 本 语 言 的 解 释 器。脚 本 语 言 就 是 文 本 文 件,bash 可 以 对 文 本 文 件 边 读 取 边 执 行。
test.sh
#!/usr/bin/bash echo \"hello world\"echo \"hello world\"echo \"hello world\"echo \"hello world\"echo \"hello world\"echo \"hello world\"ls -al
解 释 器 + 脚 本 文 件 名 执 行
execl 调 用 脚 本 文 件
mytest.c
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); execl(\"/usr/bin/bash\",\"bash\",\"test.sh\",NULL); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
test.sh
#!/usr/bin/bash function myfun(){ cnt=1 while [ $cnt -le 10 ] do echo \"hello $cnt\" let cnt++ done}echo \"hello world\"echo \"hello world\"echo \"hello world\"echo \"hello world\"echo \"hello world\"echo \"hello world\"ls -al
程 序 替 换 可 以 跨 语 言 调 用 是 因 为 所 有 语 言 运 行 起 来 都 是 进 程,只 要 是 进 程 就 可 以 被 调 用。
使 用 命 令 行 参 数
mytest.c
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); char* const argv[] = {\"mytestcpp\",\"-a\",\"-b\",\"-c\",NULL}; execv(\"./mytestcpp\",argv); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
mytest.cpp
#include<iostream>using namespace std;int main(int argc,char *argv[],char* env[]){ cout << argv[0] << \"begin running\" << endl; cout << \"这是命令行参数\" << endl; for(int i = 0;argv[i];i++) { cout << i << \":\" << argv[i] << endl; } cout << \"这是环境变量信息\" << endl; for(int i = 0;env[i];i++) { cout << i << \":\" << env[i] << endl; } cout << argv[0] << \"stop running\" << endl; return 0;}
使 用 命 令 行 参 数 和 环 境 变 量 也 可 以 进 行 程 序 替 换。环 境 变 量在 execv 中 没 有 传 参,但 是 却 能 输 出,这 是 因 为 环 境 变 量 也 是 数 据,创 建 子 进 程 的 时 候,环 境 变 量 已 经 被 子 进 程 继 承 下 去。
在 程 序 地 址 空 间 里 有 环 境 变 量 的 存 储 空 间。
程 序 替 换 中,环 境 变 量 的 信 息 不 会 被 替 换。
给 子 进 程 传 递 环 境 变 量
新 建 环 境 变 量
- 新 建 环 境 变 量
- 编 译 并 运 行 上 面 验 证 中 的 程 序 可 以 查 看 新 建 的 环 境 变 量,因 为 生 成 的 可 执 行 程 序 本 质 是 bash 的 子 进 程,同 样 的,mytestcpp 也 是 mytestc 的 子 进 程。
环 境 变 量 并 不 会 随 着 程 序 替 换 而 被 替 换,它 会 随 着 系 统 一 路 的 被 所 有 子 进 程 获 取。
10、putenv
改 变 或 添 加 环 境 变 量,添 加 到 调 用 进 程 的 上 下 文。
代 码 示 例
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ putenv(\"PRIVATE_ENV=666666\"); pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); char* const argv[] = {\"mytestcpp\",\"-a\",\"-b\",\"-c\",NULL}; execv(\"./mytestcpp\",argv); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
putenv 添 加 的 环 境 变 量 和 父 进 程 没 有 关 系 说 明 了 每 一 个 子 进 程 可 以 给 自 己 定 义 只 属 于 自 己 的 环 境 变 量。
11、execle
(1)系 统 环 境 变 量
mytest.cpp
#include<iostream>using namespace std;int main(int argc,char *argv[],char* env[]){ cout << argv[0] << \"begin running\" << endl; cout << \"这是命令行参数\" << endl; for(int i = 0;argv[i];i++) { cout << i << \":\" << argv[i] << endl; } cout << \"这是环境变量信息\" << endl; for(int i = 0;env[i];i++) { cout << i << \":\" << env[i] << endl; } cout << argv[0] << \"stop running\" << endl; return 0;}
mytest.c
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ extern char** environ; putenv(\"PRIVATE_ENV=666666\"); pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); char* const argv[] = {\"mytestcpp\",\"-a\",\"-b\",\"-c\",NULL}; execle(\"./mytestcpp\",\"mytestcpp\",\"-a\",\"-w\",\"-v\",NULL,environ); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
(2)自 定 义 环 境 变 量
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>int main(){ extern char** environ; putenv(\"PRIVATE_ENV=666666\"); pid_t id = fork(); if(id == 0) { //child printf(\"before:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); char* const argv[] = {\"mytestcpp\",\"-a\",\"-b\",\"-c\",NULL}; char* const myenv[] = {\"MYVAL=1111\",\"MYPATH=/usr/bin/linux\",NULL}; execle(\"./mytestcpp\",\"mytestcpp\",\"-a\",\"-w\",\"-v\",NULL,myenv); printf(\"after:I am a process,pid:%d,ppid:%d\\n\",getpid(),getppid()); exit(0); } pid_t ret = waitpid(id,NULL,0); if(ret>0) { printf(\"wait success,father pid:%d,ret pid:%d\\n\",getpid(),ret); } sleep(5); return 0;}
mytest.cpp
#include<iostream>using namespace std;int main(int argc,char *argv[],char* env[]){ cout << argv[0] << \"begin running\" << endl; cout << \"这是命令行参数\" << endl; for(int i = 0;argv[i];i++) { cout << i << \":\" << argv[i] << endl; } cout << \"这是环境变量信息\" << endl; for(int i = 0;env[i];i++) { cout << i << \":\" << env[i] << endl; } cout << argv[0] << \"stop running\" << endl; return 0;}
传 递 自 定 义 的 环 境 变 量 时,采 用 的 策 略 是 覆 盖,而 不 是 追 加。
12、总 结
三、总 结
进 程 程 序 替 换 核 心:三 “不 变”(PID、父 子 关 系、环 境 变 量 继 承),三 “关 键 特 性”(exec 仅 失 败 返 回、接 口 差 异 依 命 名 规 律、跨 语 言 调 用)。它 是 Linux 命 令 行(bash 的 fork+exec)和 多 进 程 模 块 化 的 基 础,建 议 通 过 多 接 口 调 用 实 践 加 深 理 解。