一、常见的进程替换函数

Linux下常见的6种进程替换函数如上,此类函数统一以exec形式开头所以也能被称为exec函数,头文件都为 unistd.h 。对于其后面的字符也具备相对应的意义:
l(list) :表示参数采⽤列表
v(vector) :参数用数组
p(path) :有p自动搜索环境变量 PATH
e(env) :表示自己维护环境变量
execl
使用模板:int execl (const char *path, const char *arg, ...);
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/wait.h>
5
6 int main()
7 {
8 int status;
9 pid_t id=fork();
10 if(id==0)
11 {
12 printf("我是子进程\n");
13 execl("/bin/ls","ls","-al",NULL);
14
15 printf("Hello ");
16 }
17
18 if(id>0)
19 {
20 printf("我是父进程\n");
21 waitpid(id,&status,0);
22
23 if (WIFEXITED(status))
24 {
25 printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
26 }
27 else if (WIFSIGNALED(status))
28 {
29 printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
30 }
31 printf("HeiHei!\n");
32 }
33
34 else
35 {
36 exit(1);
37 }
38 return 0;
39 }
运行结果:
可以看到execl函数在替换子进程后子进程第二个打印函数并没有执行,这其实是因为execl函数后面的程序都被替换掉转而执行ls指令。
注意事项:
execl参数:int execl (const char *path, const char *arg, ...);
-
第一个参数(const char* path): 表示要执行程序的绝对或者相对路径。 建议使用绝对路径,不仅能减少意外出错的可能,还能降低后续记忆和调试成本。
-
第二个参数(const char* arg,...): 从该参数开始是可变参数列表,示例中的 "ls","-al" 都传入这个列表,必须以 NULL 作为结尾标识。 可变参数列表存储的是要执行程序的具体名称(argv[0])及命令行参数,惯例上第一个可变参数(arg)要写程序名本身。
-
可变参数列表的核心特性: - 逻辑上等价于数组:示例中 argv[0] 存储 "ls",argv[1] 存储 "-al"; - 禁止拼接参数:若写成 execl("/bin/ls","ls -la",NULL),会导致 argv[0] = "ls -la",ls 无法解析参数,最终仅执行默认 ls 命令(无 -la 效果);
4.- 执行特性:execl 成功则不返回(进程被替换),失败返回 -1,错误处理代码写在调用后即可。
execlp
execl与execlp的核心区别仅在第一个参数:execlp带p后缀,支持仅写程序名并自动搜索 PATH 环境变量,而 execl 必须写全路径。虽然execl与execlp的环境变量都来源于父进程,但execlp支持自动搜索PATH
execle
int execle(const char *path, const char *arg, ..., char * const envp[]);
execlp包含l和e,也就是list和env。execlp跟execl相比多了最后一个环境变量数组参数
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/wait.h>
5
6 int main()
7 {
8 //注意这里要加const否则就是权限放大!
9 const char* envp[]=
10 {
11 "PATH=/home/hzh/ExeChang"
12 "HOME=/home/hzh",
13 NULL
14 };
15
16 int status;
17 pid_t id=fork();
18 if(id==0)
19 {
20 printf("我是子进程\n");
21 //execl("/bin/ls","ls","-al",NULL);
22 execle("hello","hello",NULL,envp);
23
24 printf("Hello ");
25 }
26
27 if(id>0)
28 {
29 printf("我是父进程\n");
30 waitpid(id,&status,0);
31
32 if (WIFEXITED(status))
33 {
34 printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
35 }
36 else if (WIFSIGNALED(status))
37 {
38 printf("子进程被信号终止,信号编号:%d\n", WTERMSIG(status));
39 }
40 printf("HeiHei!\n");
41 }
42
43 else
44 {
45 exit(1);
46 }
47 return 0;
48 }
运行结果:
如果不改变自定义的环境变量,将进程替换为ls指令的话:

可以看到程序并没有替换成功,要想成功替换接得把ls的路径添加到我们自定义的envp环境变量中。
execv
execv函数的模板为:int execv (const char *path, char *const argv[]);
下面给出例子:

运行结果:
execv函数中包含v(vector),v与l不同,v使用用户自己的字符串数组进行传参而不是可变参数列表。不过两者都是以NULL结尾!
由于execv的第二个参数为char*const类型,而在 C++ 中,直接将字符串常量赋值给 char* 是被明确禁止的(仅在旧版 C 或宽松编译模式下允许),这时候旧就需要将execv的第二个参数强转为char*const* 类型。
对于char*const* 类型,我们从右往左看。最右边的 * 表明这是一个指针类型,const 又表示这个指针本身不能修改是常量。而char* 则说明这是一个char* 类型的指针,进而可推出是char*const
execvp execve
execvp的模板为:int execvp (const char *file, char *const argv[]);
execve的模板为:int execve (const char *path, char *const argv[], char *const envp[]);
execvp与execv函数相比有自动搜索的环境变量,execve与execv相比则是使用用户自己的环境变量配置。
这些差别跟execl与execlp、execle函数之间的关系十分相似。
二、exec类型函数的返回值
exec类型的函数返回值类型都是int 类型,但是如果exec函数替换进程成功就无法接收返回值也就不会有返回值。但是如果exec函数未正常执行进程未替换成功就会有返回值产生,返回-1。
三、进程替换本质

在执行程序替换的时候是不会创建新进程的,程序的执行是需要从磁盘通过IO读取文件到内存中。当exec函数进行替换时本质是将磁盘上对应文件IO加载到内存之中,并不需要再次建立新进程。进程的内核标识(如 PID)、进程控制块(PCB)、已打开的文件描述符、当前工作目录、用户 / 组权限等核心资源会完全保留,仅替换进程内存空间中的程序内容。
四、自主Shell
获取用户、主机、路径信息
头文件 stdlib.h 中包含了 getenv 函数可用于获取当前的用户名、主机名、路径等信息。

获取用户的输入指令并解析
在官方Shell中命令,参数之间通常以空格为分隔符,但是用户具体摁了多少个空格以及什么位置加了空格无法预测。这时候就可以用 strtok 函数进行空格切割。
strtok 函数的第一个参数设置为实际的数组地址的时候会开始切割,直到完成第一个切割为止。如果后续strtok函数第一个参数传入NULL的话就表明在上一次切割的基础上以第二个参数为切割符号继续切割直到无法切割为止。
int GetCommand(char* commandline,int size)
56 {
57 //fgets从指定中读取,第2个参数表示最多读取多少个字符,最后自动加\0
58 if(fgets(commandline,size,stdin)==NULL)
59 return 0;
60 //把数组最后一个字符(有可能是空格)换为终止符
61 commandline[strlen(commandline)-1]='\0';
62 return strlen(commandline);
63
64 }
65
66 #define prase " "
67
68 //分割命令行参数表,提取具体指令
69 int ParseCommand(char commandline[])
70 {
71 //提前清理命令行参数表
72 gargc=0;
73 memset(gargv,0,sizeof(gargv));
74
75 gargv[gargc++]= strtok(commandline,prase);
76
W> 77 while(gargv[gargc++]=strtok(NULL,prase));
78 gargc--;}
使用exec函数替换子进程为指定程序
切割好后的命令行参数会放到gargv数组中,供后续子进程程序替换作为参数使用。
int ExecuteCommand()
87 {
88 int status=0;
89
90 pid_t id=fork();
91 if(id<0)
92 return -1;
93 if(id>0)
94 {
95 //父进程
96 pid_t wa=waitpid(id,&status,0);
97 if(wa>0)
98 {
99 //等待成功
100
101 }
102 }
103
104 else
105 {
106 //子进程
107 execvp(gargv[0],gargv);
108 //替换失败,退出码为1
109 exit(1);
110 }
111 return 0;
112 }
内建命令
上面内容就足够搭建一个很简单的Shell,说到底其关键就是将子进程替换为要执行的程序。但是由于是将子进程替换,就会有一个隐形问题:我们进程替换后如果要改变环境参数之类的变量最多只能影响到子进程而无法影响到父进程的环境变量。
例如,我们假设替换的进程会改变系统的环境变量,由于子进程与父进程的分流导致父进程无法被修改环境变量,这时候就需要额外判别这一类的程序,让父进程直接进行相关联的更改。
这一类需要突破进程隔离限制需要更改Shell环境的指令我们统一称为内建命令,典型的如cd指令。

另外我们还需要注意到操作系统是不会主动更改环境变量的,这又会导致虽然我们使用cd指令已经改变了当前目录但打印出来的路径却保持不变的情况,这时候就需要使用 getcwd 函数保证工作路径的及时更新。