程序替换
简单来看: 就是替换一个进程正在运行的程序
替换一个pcb映射在内存中的代码和数据(加载另一个程序到内存中,然后更新页表信息,初始化虚拟地址空间),这个进程pcb将从头开始调度新的进程开始运行( 说白了就是让这个进程运行另一个程序)(pcb不变)
注意:
程序替换之后,当前进程运行完替换后的程序就会退出,并不会又回去运行原先的程序
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数
其实有六种以exec开头的函数,统称exec函数族:
#include <unistd.h>
int execl(const char *path, const char *arg, ...); // 对当前调用进程进行程序替换
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
/* 以上的五个函数都是库函数 */
// 系统调用接口
int execve(const char *path, char *const argv[], char *const envp[]);
-
将新程序的运行参数,通过不定参的形式传递进入新的程序函数,以NULL结尾
-
path:带有路径的新程序名称,就是使用这个程序替换进程正在调度运行的程序
-
const char * arg:是程序的运行参数
-
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它函数在 man手册第3节。这些函数之间的关系如下图所示。
exec函数区别
- l 和 v 的区别:
程序运行参数的赋予方式不同
- l通过不定参完成
- v通过字符串指针数组进行赋予
- 带p和不带p的区别
第一个参数加载的新程序名称是否需要带路径,
- 带p则不需要,只要文件名即可,默认回去在PATH环境变量指定的路径下查找文件
- 不带p则需要加路径
- 带e和不带e的区别
这个进程的环境变量是否需要重新初始化
- 有e则表示初始化
- 若没有e则表示使用默认的环境变量
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
- 为什么调用成功没有返回值?
因为调用exec成功,则当前进程的代码和数据已经被替换,以前的堆和栈已经被销毁,返回值也保存在栈中,栈已经被销毁,则返回值不存在。
- 为什么调用成功没有返回值?
- 调用exec并不创建新进程,所以调用exec前后,进程的pid不变。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
exec调用举例如下:
#include <unistd.h>
int main() {
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
实现一个minishell
下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。 所以要写一个shell(shell就是一个命令行解释器),需要循环以下过程:
- 等待用户的标准输入 [ls -l -a]
- 对用户数据进行解析,得到程序名称 [ls] 以及参数信息[-l] 和[-a]
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
实现代码
#include <stdio.h> #include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
// 命令行
char command[1024];
int do_face() {
// 初始化命令行缓冲区 --- 全局变量默认初始化为 NULL
memset(command, 0x00, 1024);
printf("[user@localhost]$ ");
// 刷新缓冲区 --- 将缓冲区中的内容输出到屏幕上
fflush(stdout);
// %[^\n]: 接收到 '\n' 就停止接收
// %*c 接收最后一个字符('\n')但是不赋予 command
if (scanf("%[^\n]%*c", command) == 0) {
// 如果只输入 '\n' 则将 '\n' 吃掉
getchar();
return -1;
}
return 0;
}
// 将命令行中的字符串切割成各个命令
char** do_parse(char* cmd) {
// 保存各个字符串命令
static char* argv[32];
// 下标
int argc = 0;
char* cur = cmd;
while (*cur != '\0') {
if (*cur != ' ') {
argv[argc++] = cur;
while (*cur != ' ' && *cur != '\0') {
++cur;
}
if (*cur != '\0') {
*cur = '\0';
++cur;
}
}
while (*cur != '\0' && *cur == ' ') {
++cur;
}
}
// 在最后一个命令后面加 NULL
argv[argc] = NULL;
return argv;
}
// 进程替换, 让子进程来完成shell调用各个命令的任务
void do_exec() {
pid_t pid = fork();
char** argv = {NULL};
if (pid < 0) {
perror("fork error");
exit(-1);
}
else if (pid == 0) {
argv = do_parse(command);
// 如果命令行不为空, 则进程替换
if (argv[0] != NULL) {
execvp(argv[0], argv);
}
else {
exit(0);
}
}
else {
// 进程等待
waitpid(pid, NULL, 0);
}
}
int main() {
while (1) {
if (do_face() < 0) {
continue;
}
do_exec();
}
return 0;
}
如有不同见解,欢迎留言讨论~~