前言:欢迎 各位光临本博客,这里小编带你直接手撕,文章并不复杂,愿诸君耐其心性,忘却杂尘,道有所长!!!!

IF'Maxue :个人主页
🔥 个人专栏 :
《C语言》
《C++深度学习》
《Linux》
《数据结构》
《数学建模》
⛺️生活是默默的坚持,毅力是永久的享受。不破不立!
文章目录
-
- 一、为啥sleep命令会"卡住"?------bash的阻塞等待
- 二、进程替换到底是啥?------不建新进程,只换"代码和数据"
- 三、exec系列接口怎么用?------5个函数,记住"字母含义"就够了
-
- [1. execl:传全路径+参数列表](#1. execl:传全路径+参数列表)
- [2. execlp:不用写路径,靠PATH找](#2. execlp:不用写路径,靠PATH找)
- [3. execv:用数组传参数](#3. execv:用数组传参数)
- [4. execvp:结合v和p的优点](#4. execvp:结合v和p的优点)
- [5. execvpe:自定义环境变量](#5. execvpe:自定义环境变量)
- 四、为啥子进程替换,父进程没事?------写时拷贝的"保护"
- 五、进程替换能跨语言吗?------当然能!不管啥语言,能执行就行
- 六、怎么证明没建新进程?------看PID就知道
- 七、自己写个简单Shell?------4步搞定:循环、获取命令、解析、执行
-
- [1. 第一步:显示命令提示符(用户名@主机名:路径)](#1. 第一步:显示命令提示符(用户名@主机名:路径))
- [2. 第二步:获取用户输入(处理回车)](#2. 第二步:获取用户输入(处理回车))
- [3. 第三步:解析命令(按空格切割)](#3. 第三步:解析命令(按空格切割))
- [4. 第四步:执行命令(子进程替换,父进程等待)](#4. 第四步:执行命令(子进程替换,父进程等待))
- 问题:cd命令为啥没用?------内建命令的必要性
- 总结
一、为啥sleep命令会"卡住"?------bash的阻塞等待
先从一个小问题切入:为啥用execl
调用sleep
时,程序像"卡住"了?
其实这不是卡住,是bash在等它执行完 。
比如你写了个程序,里面用execl
调用系统的sleep
命令(如图1、图2所示):
c
// 类似图中代码的逻辑
#include <unistd.h>
int main() {
// 执行sleep 1秒:路径/bin/sleep,参数sleep、1,末尾NULL
execl("/bin/sleep", "sleep", "1", NULL);
return 0;
}
当你运行这个程序时,bash(你的命令行父进程)会"阻塞等待"这个子进程(你写的程序)执行完。而你的程序又被sleep
替换了,所以bash会等sleep
跑完1秒才恢复,看起来就像"卡住"------这其实是正常的等待逻辑,如图1、图2里的执行效果所示。

(图1:程序执行execl调用sleep的效果)

(图2:sleep执行时bash阻塞等待的状态)
二、进程替换到底是啥?------不建新进程,只换"代码和数据"
很多人以为"执行新程序"就是"新建进程",其实不是!进程替换的核心是:不创建新进程,只把当前进程的"代码和数据"换成新程序的。
就像一个人(进程)换衣服(代码和数据),身份证(PID、PCB进程控制块)没变,还是同一个人,只是穿的衣服不一样了。如图3、图4所示:进程的"结构"(PID、PCB)早就建好,替换时只把里面的"代码段、数据段"覆盖掉。

(图3:进程替换前的结构,PID等信息已存在)

(图4:替换时只覆盖代码和数据,结构不变)
关键问题:为啥exec系列函数"只有失败返回值"?
你查man execl
会发现:exec*
函数成功时没有返回值 ,只有失败时返回-1
(如图5所示)。为啥?
因为一旦替换成功,当前进程的代码已经被新程序覆盖了------原来的代码(包括exec
之后的返回语句)全没了,根本没法返回!只有替换失败时,原来的代码还在,才能返回-1
告诉你"没换成"。

(图5:man execl说明"只有错误时有返回值")
比如图6的代码:execl
之后写了printf("替换后执行");
,但如果execl
成功,这段代码会被覆盖,永远不会执行;只有execl
失败(比如路径写错),才会打印"替换失败"。

(图6:exec成功后,后续代码不执行的验证)
三、exec系列接口怎么用?------5个函数,记住"字母含义"就够了
exec
有5个常用函数:execl
、execlp
、execv
、execvp
、execvpe
。不用死记,记住字母代表的意思:
- l(list) :参数用"列表"一个个传(比如
sleep 1
传成sleep
,1
, NULL); - v(vector):参数用"数组"传(把参数放进字符数组,最后放NULL);
- p(path) :不用写全路径,系统会在
PATH
环境变量里找程序(比如直接写sleep
,不用/bin/sleep
); - e(environment):可以自定义环境变量(默认继承父进程环境变量)。
1. execl:传全路径+参数列表
格式:execl(全路径, 程序名, 参数1, 参数2, ..., NULL)
注意:
- 第一个参数是"程序全路径"(比如
/bin/sleep
); - 第二个参数是"程序名"(和执行时的名字一致,比如
sleep
); - 后面跟参数,最后必须用NULL结尾(告诉系统参数传完了)。
如图7的代码,调用sleep 1
:
c
execl("/bin/sleep", "sleep", "1", NULL); // 正确,最后有NULL
如果少传NULL或路径错,会替换失败(如图8所示)。

(图7:execl的正确用法,全路径+NULL结尾)

(图8:少传NULL或路径错误导致替换失败)
2. execlp:不用写路径,靠PATH找
比execl
多了个p
,意思是"用PATH找程序"。比如调用sleep
,不用写/bin/sleep
,直接写sleep
:
c
execlp("sleep", "sleep", "1", NULL); // 正确,系统会在PATH里找sleep
如图9的例子,执行ls
时,execlp("ls", "ls", "-l", NULL)
能成功,因为系统在PATH
(比如/bin
)里找到了ls
。

(图9:execlp用PATH找ls,不用写全路径)
3. execv:用数组传参数
比execl
多了个v
,参数放进字符数组,最后放NULL。比如调用sleep 1
:
c
char* arg[] = {"sleep", "1", NULL}; // 参数数组,最后NULL
execv("/bin/sleep", arg); // 传数组,不用一个个写参数
如图10所示,数组必须以NULL结尾,否则系统会读错参数。

(图10:execv的参数数组必须NULL结尾)
4. execvp:结合v和p的优点
既可以用数组传参数,又不用写全路径(靠PATH找)。比如:
c
char* arg[] = {"sleep", "1", NULL};
execvp("sleep", arg); // 不用写路径,数组传参
如图11,执行ls -l
时,execvp("ls", arg)
能成功,不用写/bin/ls
。

(图11:execvp结合数组和PATH的用法)
5. execvpe:自定义环境变量
比execvp
多了个e
,可以传自定义环境变量。默认情况下,子进程继承父进程的环境变量(比如PATH
),但execvpe
可以指定新的环境变量数组。
如图12,自定义env
数组,传给execvpe
,新程序会用这个环境变量:
c
char* arg[] = {"echo", "$MY_ENV", NULL};
char* env[] = {"MY_ENV=hello", NULL}; // 自定义环境变量
execvpe("echo", arg, env); // 传自定义env

(图12:execvpe传自定义环境变量)
四、为啥子进程替换,父进程没事?------写时拷贝的"保护"
如果直接在父进程里做替换,父进程会被改成新程序,这显然不行(比如bash自己不能被替换成sleep)。所以通常的做法是:fork一个子进程,让子进程做替换,父进程继续运行。
为啥子进程替换不影响父进程?因为Linux有"写时拷贝"机制:
- 父进程和子进程一开始共享代码和数据;
- 当子进程要修改代码(比如替换程序)时,系统会"拷贝一份代码"给子进程,让子进程改自己的拷贝,父进程的代码不变。
如图13的代码:fork
后,子进程执行execl
替换,父进程执行wait
等待,父进程的代码没被改,所以能继续运行。

(图13:子进程做替换,父进程不受影响)
五、进程替换能跨语言吗?------当然能!不管啥语言,能执行就行
exec
不管程序是用啥语言写的,只要是"可执行文件"(或脚本加了shebang),就能替换执行。比如:
- C程序替换执行C++编译的程序;
- C程序替换执行Python脚本。
例子1:C程序执行C++程序
-
先写个C++程序
cpp_prog.cpp
,编译成cpp_prog
:cpp#include <iostream> int main() { std::cout << "我是C++程序" << std::endl; return 0; }
-
写C程序
c_prog.c
,用execl
执行cpp_prog
:c#include <unistd.h> int main() { execl("./cpp_prog", "cpp_prog", NULL); // 执行C++编译的程序 return 0; }
如图14所示,C程序能成功执行C++程序,输出"我是C++程序"。

(图14:C程序替换执行C++程序的效果)
例子2:C程序执行Python脚本
-
写Python脚本
py_prog.py
,开头加shebang(告诉系统用Python解释器):python#!/usr/bin/python3 print("我是Python程序")
-
给脚本加执行权限:
chmod +x py_prog.py
; -
C程序用
execl
执行脚本:cexecl("./py_prog.py", "py_prog.py", NULL);
如图15所示,C程序能成功执行Python脚本,输出"我是Python程序"。

(图15:C程序替换执行Python脚本的效果)
六、怎么证明没建新进程?------看PID就知道
前面说"替换不建新进程",怎么证明?打印PID,替换前后PID不变,就说明没建新进程。
如图16的代码:
- 先打印当前进程PID(
getpid()
); - 然后用
execl
替换成sleep
,并让sleep
执行echo $$
($$
是当前进程PID); - 替换后,
echo $$
打印的PID和之前一致,说明还是同一个进程。

(图16:打印PID证明替换不建新进程)
如图17的输出,替换前打印的PID是1234,替换后sleep
执行echo $$
也打印1234,说明没建新进程。

(图17:PID不变的输出结果)
七、自己写个简单Shell?------4步搞定:循环、获取命令、解析、执行
Shell的核心是"死循环":不断获取用户命令→解析命令→执行命令。我们一步步实现:
1. 第一步:显示命令提示符(用户名@主机名:路径$)
要显示类似user@ubuntu:~/test$
的提示符,需要获取3个信息:
- 用户名:用
getenv("USER")
(环境变量里有); - 主机名:用
gethostname()
函数; - 当前路径:用
getcwd()
函数。
如图18的代码:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
char host[256];
char cwd[1024];
gethostname(host, sizeof(host)); // 获取主机名
getcwd(cwd, sizeof(cwd)); // 获取当前路径
// 打印提示符:用户名@主机名:路径$
printf("%s@%s:%s$ ", getenv("USER"), host, cwd);
return 0;
}

(图18:获取用户名、主机名、路径的代码)
2. 第二步:获取用户输入(处理回车)
用fgets
获取用户输入(scanf
会被空格截断,不适合命令行),但fgets
会把末尾的回车(\n
)也读进来,需要去掉。
如图19的代码:
c
char buf[1024];
fgets(buf, sizeof(buf), stdin); // 获取输入,比如输入"ls -l\n"
// 去掉末尾的\n:把\n换成\0
buf[strcspn(buf, "\n")] = '\0'; // strcspn找\n的位置,换成结束符

(图19:用strcspn去掉输入中的回车)
3. 第三步:解析命令(按空格切割)
用strtok
函数按空格切割命令,比如把"ls -l"切成"ls"和"-l",放进参数数组(最后放NULL)。
如图20的代码:
c
char* arg[64] = {NULL}; // 参数数组,最多64个参数
int i = 0;
// 第一次调用strtok:传buf和分隔符" "
arg[i] = strtok(buf, " ");
while (arg[i] != NULL) {
i++;
// 后续调用:第一个参数传NULL,用上次的位置继续切
arg[i] = strtok(NULL, " ");
}
// 循环结束后,arg = {"ls", "-l", NULL}

(图20:用strtok解析命令的代码)
4. 第四步:执行命令(子进程替换,父进程等待)
fork
子进程,子进程用execvp
执行命令(不用写路径,数组传参),父进程用wait
等待子进程结束。
如图21的完整代码:
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
while (1) { // 死循环,Shell不退出
// 1. 显示提示符
char host[256], cwd[1024];
gethostname(host, sizeof(host));
getcwd(cwd, sizeof(cwd));
printf("%s@%s:%s$ ", getenv("USER"), host, cwd);
// 2. 获取输入
char buf[1024] = {0};
fgets(buf, sizeof(buf), stdin);
buf[strcspn(buf, "\n")] = '\0'; // 去掉\n
// 3. 解析命令
char* arg[64] = {NULL};
int i = 0;
arg[i] = strtok(buf, " ");
while (arg[i] != NULL) {
i++;
arg[i] = strtok(NULL, " ");
}
if (arg[0] == NULL) continue; // 输入为空,跳过
// 4. 执行命令
pid_t pid = fork();
if (pid == 0) { // 子进程
execvp(arg[0], arg); // 执行命令
exit(1); // 替换失败才会到这步
} else if (pid > 0) { // 父进程
wait(NULL); // 等待子进程结束
}
}
return 0;
}

(图21:简单Shell的完整代码)
问题:cd命令为啥没用?------内建命令的必要性
上面的Shell能执行ls
、sleep
,但执行cd ~
会发现路径没变化。为啥?
因为cd
是"内建命令"------它需要修改当前Shell进程(父进程)的路径 ,而不是子进程的路径。如果让子进程执行cd
,子进程的路径改了,但子进程退出后,父进程的路径没变,等于白改。
解决办法:父进程自己执行chdir
函数(不用子进程),比如判断如果命令是cd
,就调用chdir
:
c
if (strcmp(arg[0], "cd") == 0) {
if (arg[1] != NULL) chdir(arg[1]); // 父进程自己改路径
continue;
}
这种"父进程亲自执行"的命令,就是内建命令(比如cd
、exit
)。
总结
- sleep卡住:不是卡住,是bash作为父进程在等待子进程(sleep)执行完;
- 进程替换:不建新进程,只替换当前进程的代码和数据,PID不变;
- exec函数 :记
l/v/p/e
的含义,execlp
和execvp
最常用; - 子进程替换:靠写时拷贝保护父进程,父进程不受影响;
- 自定义Shell:死循环+获取命令+解析命令+执行命令,内建命令需父进程亲自执行。