目录
1.替换原理:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行,调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承下去了
2.替换函数
int execve(const char *path, char *const argv[], char *const envp[]);
execve系统调用接口:上图中的函数其实最终都是调用execve接口exec
函数解释:
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。因为当调用exec系列函数时,操作系统会完全替换当前进程的代码段和数据段,【但保留进程控制块(PCB)中的部分元数据(如PID、文件描述符表等)】所以不再返回。
- 如果调用出错则返回-1
总之,exec函数只有出错的返回值而没有成功的返回值
命名理解:
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path):有p自动搜索环境变量PATH
- e(env):表示自己维护环境变量
exec调用举例如下:
#include<unistd.h>
int main()
{
char *const argv[] = {"ps","aux",NULL};
char *const envp[] = {"PATH=/bin:/usr/bin","TERM=console",NULL};
execl("/bin/ps","ps","aux",NULL);
//带p的,可以使用环境变量PATH,无需写全路径
execlp("ps",// 查找PATH中的ps
"ps", // argv[0]
"aux",// argv[1]
NULL);// 必须的终止符
//带e的,需要自己组装环境变量
execle("ps","ps","aux",NULL,envp);
execv("/bin/ps",argv);
//带p的,可以使用环境变量PATH,无需写全路径
execvp("ps",argv);
//带e的,需要自己组装环境变量
execve("/bin/ps",argv,envp);
return 0;
}
只有execve是真正的系统调用,其他五个函数最终都调用execve,所以execve在man手册第二节,其他函数在man手册第三节。这些函数之间的关系如下图:
3.综合前面的知识,做一个简易的shell
shell从用户读入字符串“ls”。shell建立一个新的进程,然后在那个进程中运行ls程序并等待哪个进程结束。然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序,并等待这个进程结束。所以写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
//自定义环境变量表
char myenv[LINE_SIZE];
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void interact(char *cline, int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(), gethostname(),pwd);
char *s = fgets(cline,size,stdin);
assert(s);
(void)s;
//"abcd\n\0"
cline[strlen(cline)-1] = '\0';
}
//ls -a -l | wc -l | head
int splitstring(char cline[],char *_argv[])
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
//让子进程执行命令
//execvpe(_argv[0],_argv,environ);
execvp(_argv[0],_argv);
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0)
{
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"), "%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export")==0){
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";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit){
//交互问题,获取命令行,ls -a -l > myfile / ls -a -l >> myfile /cat < file.txt
interact(commandline,sizeof(commandline));
//commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
//子串分割的问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0)continue;
//指令的判断
//debug
//for(int i = 0;argv[i];i++)printf("[%d]: %s\n", i , argv[i]);
//内键命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
//普通命令执行
if(!n) NormalExcute(argv);
//
}
}
4.本章知识补充:
- 程序替换成功之后,exec*后续的代码不会被执行
- Linux中形成的可执行程序,是由格式的,ELF,可执行程序的表头,可执行程序的入口地址就在表中
- 子进程独立:进程替换会在后台启动子进程来执行指定的命令。子进程有自己独立的环境,这意味着在子进程中对环境变量的修改不会影响到主进程