文章目录
- Linux下进程替换
- 1. c库exec函数族
- 一、exec函数族简介
- 二、exec函数族函数原型及参数说明
- 三、exec函数族的工作机制
- 四、注意事项
- 五、示例代码
- 2. 系统调用execve接口
- 一、execve接口与C库exec函数族的关系
- 二、函数原型
- 三、参数说明
- 四、工作原理
- 五、返回值
- 六、注意事项
- 七、 图示
- 八、示例代码
- 3. 基于进程相关接口实现的自定义简单的shell
Linux下进程替换
在Linux系统中,exec程序替换接口是一组函数,允许当前进程被一个新的程序映像替换。这些函数不会创建新的进程,而是会替换当前进程的地址空间、代码、数据、堆栈等,使新的程序成为当前进程的继续。以下是Linux下exec程序替换接口的详解:
1. c库exec函数族
一、exec函数族简介
exec函数族包括多个函数,如execl、execlp、execle、execv、execve、execvp等。这些函数在功能上是相似的,但在参数传递方式和环境变量处理上有所不同。
二、exec函数族函数原型及参数说明
- execl
int execl(const char *path, const char *arg, ...);
- path:要执行的程序文件的路径。
- arg:传递给新程序的参数列表,必须以NULL结尾。
- execlp
int execlp(const char *file, const char *arg, ...);
- file:要执行的程序文件的名称(不带路径),函数会在PATH环境变量中查找该文件。
- arg:与execl相同。
- execle
int execle(const char *path, const char *arg, ..., char *const envp[]);
- path:与execl相同。
- arg:与execl相同。
- envp:传递给新程序的环境变量数组,以NULL结尾。
- execv
int execv(const char *path, char *const argv[]);
- path:与execl相同。
- argv:传递给新程序的参数数组,数组的第一个元素通常是程序名,数组以NULL结尾。
- execve
int execve(const char *filename, char *const argv[], char *const envp[]);
- filename:要执行的程序文件的路径。
- argv:与execv相同。
- envp:与execle相同。
- execvp
int execvp(const char *file, char *const argv[]);
- file:与execlp相同。
- argv:与execv相同。
三、exec函数族的工作机制
- 查找程序:根据提供的路径或文件名(对于execlp和execvp),找到要执行的程序文件。
- 加载程序:将程序文件加载到当前进程的地址空间中,替换掉原有的代码、数据等。
- 初始化新程序:为新程序设置参数、环境变量等。
- 开始执行:从新程序的入口点开始执行,通常是main函数。
四、注意事项
- exec函数不返回:如果exec函数调用成功,它不会返回给调用者。如果调用失败,它会返回-1,并设置errno以指示错误类型。
- 进程ID不变:尽管进程的内容被替换了,但进程ID(PID)保持不变。
- 环境变量:对于execl和execv等不直接处理环境变量的函数,新的程序将继承调用exec函数之前的进程的环境变量。对于execle和execve等可以指定环境变量的函数,新的程序将使用提供的环境变量。
五、示例代码
以下是一个使用execl函数的示例代码:
#include <stdio.h>
#include <unistd.h>int main() {printf("Before exec: PID = %d\n", getpid());execl("/bin/ls", "ls", "-l", NULL);// 如果execl调用成功,下面的代码将不会被执行。printf("After exec: This line will not be printed.\n");return 0;
}
在这个例子中,当程序执行到execl函数时,它会替换当前进程的映像为/bin/ls
程序,并传递-l
参数。因此,输出将是当前目录下的文件和目录列表,而不是"After exec"这条消息。
2. 系统调用execve接口
execve接口与C库中的exec函数族有着密切的关系。以下是对它们之间关系的详细解释:
一、execve接口与C库exec函数族的关系
- 基础:
execve是Linux内核提供的一个底层系统调用
,它允许一个进程加载并执行一个新的程序。而C库中的exec函数族则是对这个系统调用的封装和扩展,提供了更易于使用的接口。 - 功能:C库中的exec函数族(如
execl、execv、execle、execlp、execvp
等)都基于execve系统调用实现。它们之间的主要区别在于参数传递的方式和是否使用环境变量等方面。例如,execl和execv直接接受参数列表和环境变量(或继承当前环境变量),但参数传递方式不同
;execlp和execvp则接受程序名而不是路径,并使用PATH环境变量来查找程序
;execle则类似于execve,但允许指定文件描述符的关闭和重定向操作。 - 调用过程:
当C库中的exec函数被调用时,它们会构建适当的参数和环境变量数组,然后调用execve系统调用来执行新的程序
。如果execve调用成功,新的程序将替换当前进程的映像,并且不会返回到调用exec函数的代码。如果execve调用失败,则exec函数会返回一个错误码,并设置errno以指示错误类型。
二、函数原型
int execve(const char *filename, char *const argv[], char *const envp[]);
三、参数说明
- filename:指定要执行的程序的路径。这个路径可以是绝对路径,也可以是相对于当前工作目录的相对路径。
- argv:传递给新程序的命令行参数数组。数组的第一个元素通常是程序名(即filename中的basename部分,但也可以不遵循这个约定),后续元素是传递给程序的参数,数组以NULL结尾。
- envp:传递给新程序的环境变量数组。每个元素都是一个形如
"name=valu"
的字符串,数组以NULL结尾。如果不需要传递特定的环境变量,可以传递NULL,此时新程序将继承调用execve之前的进程的环境变量。
四、工作原理
- 查找可执行文件:内核根据提供的filename参数查找可执行文件。这个过程涉及到路径搜索、文件访问权限检查等。
- 加载可执行文件:找到可执行文件后,内核会将其内容映射到进程的虚拟地址空间。这个过程包括读取可执行文件头部信息、分配内存、设置内存保护等。
- 设置参数和环境变量:
内核将argv和envp参数传递给新进程
,并在新进程的内存中复制这些信息。 - 设置程序入口点:内核设置新程序的入口点,并跳转到新程序的执行地址。此时,新进程开始执行,原来进程的程序代码和数据都被替换掉。
五、返回值
- 如果execve调用成功,它不会返回给调用者。新程序将作为当前进程的继续开始执行。
- 如果execve调用失败,
它会返回-1
,并设置errno以指示错误类型。常见的错误包括文件不存在、没有执行权限、内存不足等。
六、注意事项
- execve函数不返回:由于execve调用成功后会替换当前进程的映像,因此它不会返回给调用者。如果需要在execve之后执行代码,应该将其放在另一个进程中,或者使用其他机制(如信号处理)来确保代码的执行。
- 进程ID不变:尽管进程的内容被替换了,但进程ID(PID)保持不变。这意味着新的程序将继承原来进程的PID和其他相关属性。
- 安全性:execve系统调用涉及到程序加载和执行,因此其安全性至关重要。内核需要确保只有授权的程序才能被执行,以防止恶意代码的攻击。
七、 图示
八、示例代码
以下是一个使用execve函数的示例代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {char *args[] = {"/bin/ls", "-l", NULL};char *envp[] = {NULL}; // 可以传递NULL以继承当前环境变量,也可以传递自定义的环境变量数组printf("Before execve: PID = %d\n", getpid());if (execve("/bin/ls", args, envp) == -1) {perror("execve failed");exit(EXIT_FAILURE);}// 如果execve调用成功,下面的代码将不会被执行printf("After execve: This line will not be printed.\n");return 0;
}
在这个例子中,当程序执行到execve函数时,它会替换当前进程的映像为/bin/ls
程序,并传递-l
参数。因此,输出将是当前目录下的文件和目录列表的详细信息,而不是"After execve"这条消息。如果execve调用失败,则会打印错误信息并退出程序。
3. 基于进程相关接口实现的自定义简单的shell
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>
#include <assert.h>#define LEFT "[" // 左边界符号
#define RIGHT "]" // 右边界符号
#define DELIM " \t" // 分隔符:空格和制表符
#define LINE_SIZE 1024 // 命令行输入的最大长度
#define ARGC_SIZE 32 // 命令行参数的最大数量char commandline[LINE_SIZE]; // 存储命令行输入
char pwd[LINE_SIZE]; // 存储当前工作目录
char* argv[ARGC_SIZE]; // 存储命令行参数
int lastcode = 0; // 上一个命令的退出码
int quit = 0; // 是否退出Shell的标志
char myenv[LINE_SIZE]; // 存储export命令设置的环境变量enum EXIT {GETHOSTNAME_FAILED = 1,FORK_FAILED = 2,EXIT_CODE = 3,
};// 获取当前工作目录
void GetPwd() {getcwd(pwd, sizeof(pwd));
}// 获取当前用户名
const char* GetUserName() {return getenv("USER");
}// 获取主机名
const char* GetHostName() {static int initialized = 0;if (!initialized) {// 设置HOSTNAME为"remote"if (putenv("HOSTNAME=remote") != 0) {perror("HOSTNAME获取失败");exit(GETHOSTNAME_FAILED);}initialized = 1;}return getenv("HOSTNAME");
}// 交互函数,获取用户输入的命令行
void interact(char* cline, int size) {GetPwd();// 打印Shell提示符:[用户名@主机名当前工作目录]printf(LEFT "%s@%s%s" RIGHT "\n", GetUserName(), GetHostName(), pwd);// 从标准输入读取用户输入的命令行char* s = fgets(cline, size, stdin);if (s == NULL) {perror("fgets失败");exit(EXIT_CODE);}// 去掉末尾的换行符size_t len = strlen(cline);if (cline[len - 1] == '\n') {cline[len - 1] = '\0';}
}// 字符串分割函数,将命令行字符串分割为参数数组
int SplitString(char* cline, char* _argv[]) {int i = 0;_argv[i++] = strtok(cline, DELIM); // 使用strtok第一次分割// 使用strtok继续分割,直到达到最大参数数量或分割结束while (i < ARGC_SIZE && (_argv[i++] = strtok(NULL, DELIM)));return i - 1;
}// 内建命令处理函数
int BuildCommand(int _argc, char* _argv[]) {if (!strcmp(_argv[0], "quit")) {quit = 1; // 设置退出标志} else if (_argc == 2 && strcmp(_argv[0], "cd") == 0) {chdir(_argv[1]); // 改变工作目录GetPwd();setenv("PWD", pwd, 1); // 更新PWD环境变量return 1;} else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {//重要的是,putenv只是修改环境变量的指针,并不复制字符串内容。//这意味着如果原始字符串(这里是_argv[1]指向的内容)被修改或释放//环境变量的值也会受到影响。strcpy(myenv, _argv[1]); // 复制设置的环境变量putenv(myenv); // 设置环境变量return 1;} else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) {if (strcmp(_argv[1], "$?") == 0) {printf("%d\n", lastcode); // 输出上一个命令的退出码lastcode = 0;} else if (*_argv[1] == '$') {char* val = getenv(_argv[1] + 1);if (val) printf("%s\n", val); // 输出环境变量的值} else {printf("%s\n", _argv[1]); // 输出指定的字符串}return 1;}// 特殊处理下的ls命令if (strcmp(_argv[0], "ls") == 0) {_argv[_argc++] = "--color"; // 增加参数--color_argv[_argc] = NULL;}return 0; // 返回0表示不是内建命令
}// 外部命令执行函数
void NormalExecute(char* _argv[]) {pid_t pid = fork();if (pid < 0) {perror("fork失败");exit(FORK_FAILED);} else if (pid == 0) {execvp(_argv[0], _argv); // 在子进程中执行命令exit(EXIT_CODE);} else {int status = 0;pid_t rtpid = waitpid(pid, &status, 0); // 等待子进程退出if (rtpid == pid) {lastcode = WEXITSTATUS(status); // 获取子进程的退出状态}}
}int main() {while (!quit) {interact(commandline, sizeof(commandline)); // 获取用户输入的命令行if (strlen(commandline) == 0) continue; // 如果是空行则继续下一轮循环int argc = SplitString(commandline, argv); // 分割命令行字符串为参数数组if (argc == 0) continue; // 如果没有参数则继续下一轮循环int is_builtin = BuildCommand(argc, argv); // 执行内建命令if (!is_builtin) NormalExecute(argv); // 执行外部命令}return 0;
}