替换原理
用fork创建子进程之后执行的是和父进程相同的程序,接下来的代码由哪个进程去执行就看调度器具体调度了。
但是往往父子进程需要执行不同的代码分支,这时候子进程就需要调用一种exec函数以执行另一个程序分支。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动开始执行。
调用exec函数并不创建新进程,所以调用exec前后该进程的id并未改变。
替换过程如下图:
小结:
- 程序替换不会创建进程,也不会销毁代码。
- 替换代码和数据从一个可执行文件中来。
- 原有的堆、栈中数据全部舍弃,根据新代码执行过程构建新的堆、栈内容。
替换函数
exec 函数(exec函数是个统称,它有6种以exec开头的函数)
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
//*file为可执行文件名
int execle(const char *path, const char *arg, ..., char *const envy[]);
//最后一个参数envp[],必须为NULL
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 envy[]);
//最后一个参数envp[],必须为NULL
可以分为两种,一种是execl开头,另一种是execv开头。
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1.
- 所以exec函数只有出错返回值,没有成功返回值。
函数命名
这些函数原型看起来很相似,很容易让人混淆,但是掌握规律就不难记忆了。
- l(list):表示参数采用变长参数列表
- v(vector):参数用数组
- p(path):有p则自动搜索环境变量PATH
- e(env):表示自己维护环境变量
函数使用
#include <unistd.h>
#include <stdlib.h>
int main()
{
char *const argv[] = {"ps","-ef",NULL};
char *const envp[] = {"PATH=/bin:user/bin","TERM=console",NULL};
execl("/bin/ps","ps","-ef",NULL);
//带p的,可以使用环境变量PATH,无需写全路径
execlp("ps","ps","-ef",NULL);
//带e的,需要自己组装环境变量
execle("ps","ps","-ef",NULL,envp);
execv("/bin/ps",argv);
//带p的,可以使用环境变量PATH,无需写全路径
execvp("ps",argv);
//带e的,需要自己组装环境变量
execve("/bin/ps",argv,envp);
exit(0);
}
⚠️:事实上,只有execve是真正的系统调用,其他五个函数都是最终调用execve,所以execve在man手册第二页,其他函数在man手册第三页。(man手册第二页为系统调用,第三页为库函数)
这些函数之间的关系如下图所示:
简易版本的shell
了解了进程程序替换之后,综合之前的知识,就可以实现一个简易版本的shell。
shell实现原理
用下图来描述shell原理:
解释:
用时间轴来表示事件的发生次序。(时间从左向右)
shell从用户读入字符串“ls”,建立一个新的进程,在子进程中处理ls程序,同时父进程等待子进程结束。
然后shell读取新的一行输入,再建立一个新的进程,在新的子进程中处理ps程序,同时父进程等待新的子进程结束。
所以要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出
shell实现代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{
memset(command,0X00,MAX_CMD);
printf("minishell$");
fflush(stdout);
if(scanf("%[^\n]*c",command) == 0)
{
getchar();
return -1;
}
return 0;
}
char **do_parse(char *buff)
{
int argc = 0;
static char *argv[32];
char *ptr = buff;
while(*ptr != '\0')
{
if(!isspace(*ptr))
{
argv[argc++] = ptr;
while((!isspace(*ptr)) && (*ptr) != '\0')
{
ptr++;
}
}
else
{
while(isspace(*ptr))
{
*ptr = '\0';
ptr++;
}
}
}
argv[argc] = NULL;
return argv;
}
int do_exec(char *buff)
{
char **argv = {NULL};
int pid = fork();
if(pid == 0)
{
argv == do_parse(buff);
if(argv[0] == NULL)
{
exit(-1);
}
execvp(argv[0],argv);
}
else
{
waitpid(pid,NULL,0);
}
return 0;
}
int main(int argc,char *argv[])
{
while(1)
{
if(do_face() < 0)
continue;
do_exec(command);
}
return 0;
}
函数和进程之间的相似性
exec/exit就相当于call/return。
一个C程序由很多函数组成。一个函数可以调用另一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数有自己的局部变量,不同的函数通过call/return系统进行通信。
这种通过参数和返回值在拥有私有数据的函数之间通信的模式是结构化程序设计的基础。
Linux将这种应用于程序之间的模式扩展到程序之间。如图:
一个C程序可以fork/exec另一个程序,并传给它一个参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。
调用它的进程可以通过wait(&ret)来获取exit的返回值。