在Linux系统中,⽤用fork创建⼦子进程后执⾏行的是和⽗父进程相同的程序(但有可能执⾏行不同的代码分⽀支),⼦进程往往 要调⽤用⼀一种exec函数以执⾏行另⼀一个程序。当进程调⽤用⼀一种exec函数时,该进程的⽤用户空间代码和数 据完全被新程序替换,从新程序的启动例程开始执⾏行。调⽤用exec并不创建新进程,所以调⽤用exec前后该进程的id并未改变。
Linux操作系统中的shell就是运用这个原理处理客户请求的,不是每个请求都是shell亲力亲为的,所以shell会创建子程序替换他,在实现shell的过程中我们会用到exec函数,所以我们先了解一下exec函数族并对其每个的用法用代码实现一遍。
其实有六种以exec开头的函数,统称exec函数:
#include
int execl(const char *path, const char *arg, ...);
参数:路径,操作,以NULL结尾
int execlp(const char *file, const char *arg, ...);
参数:文件名,操作,NULL
int execle(const char *path, const char *arg, ..., char *const envp[]);
参数:路径,操作,环境变量
int execv(const char *path, char *const argv[]);
参数:路径,argv
int execvp(const char *file, char *const argv[]);
参数:文件名,argv[]
int execve(const char *path, char *const argv[], char *const envp[]);
最标准的系统调用,参数:路径,argv,环境变量
这些函数如果调⽤用成功则加载新的程序从启动代码开始执⾏行,不再返回,如果调⽤用出错则返回-1, 所以exec函数只有出错的返回值⽽而没有成功的返回值。exec函数族的特点是谁调用他他就替换谁,只要exec函数调用成功,后续代码全部失效。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
不带字母p (表⽰示path)的exec函数 第⼀一个参数必须是程序的相对路径或绝对路径,例如
"/bin/ls"或"./a.out",⽽而不能 是"ls"或"a.out"。对于带字母p的函数: 如果参数中包含/,则
将其视为路径名。 否则视为不带路径的程序名,在PATH环境变量的⽬目录列表中搜索这
个程序。
带有字母l( 表⽰示list)的exec函数要求将新程序的每个命令⾏行参数都当作⼀一个参数传给
它,命令⾏行 参数的个数是可变的,因此函数原型中有...,...中的最后⼀一个可变参数应该是
NULL, 起sentinel的作⽤用。
带有字母v( 表⽰示vector)的函数,则应该先构造⼀一个指向各参数的指针数 组,然后将该数
组的⾸首地址当作参数传给它,数组中的最后⼀一个指针也应该是NULL,就像main函数 的
argv参数或者环境变量表⼀一样。
对于以e (表⽰示environment)结尾的exec函数,可以把⼀一份新的环境变量表传给它,其他
exec函数 仍使⽤用当前的环境变量表执⾏行新程序。
下面我们用代码验证exec函数族:
源代码:
Makefile的编写:
.PHONY:all
all:other myexec
other:other.c
gcc -o other other.c
myexec:myexec.c
gcc -o myexec myexec.c
.PHONY:clean
clean:
rm -f myexec other
代码实现:
Myexec.c:
#include
#include
#include
int main()
{
//创建子进程
pid_t id=fork();
if(id<0)
{
printf("new process is faild\n");
return 1;
}
else if(id==0)
{
//child
printf("I am a process\n");
sleep(1);
char* const myenv[]={"MYPATH=aa/bb/cc/dd/hello/world",NULL};
//char* const myargv[]={"ls","-l","-a",NULL};
//execl("/bin/ls","ls","-l","-i","-n","-a",NULL);
//execv("/bin/ls",myargv);
//execlp("ls","ls","-l","-i","-n","-a",NULL);
//execvp("ls",myargv);
execle("./other","other",NULL,myenv);
//替换失败,退出码为2
exit(2);
}
else
{
//father
pid_t ret=waitpid(id,NULL,0);
int status=0;
if(ret>0)
{
//打印退出码,获取时尽量用宏,不要用移位
printf("wait success,exet code:%d\n",WEXITSTATUS(status));
}
else{
printf("wait failed\n");
return 3;
}
}
return 0;
}
Other.c:
#include
#include
#include
int main()
{
printf("I am another proc,I am running,MYPATH : %s\n",getenv("MYPATH"));
return 0;
}
对exec函数族熟悉了之后,为了实现shell,我们还必须了解一个函数就是read,对read函数的返回值一定要理解:
Read函数一共有三种返回值
当read的返回值大于0时,表示读取成功了并且读取成功的数值小于等于sizeof(buf)-1。
当read的返回值等于0时,表示读取的文件已经读到了文件尾。
当read的返回值小于0时,表示读取出错。
对以上知识点掌握之后,我们就来实现自己的shell:
源码:
Makefile的实现:
.PHONY:myshell
myshell:myshell.c
gcc -o myshell myshell.c
.PHONY:clean
clean:
rm -f myshell
Shell.c:
#include
#include
#include
#include
#include
int main()
{
char cmd[128];
while(1)
{
//打印命令行的提示符(包括用户名,主机名等)
printf("[test@my-host-name myshell]#");
fflush(stdout);
ssize_t _s=read(0,cmd,sizeof(cmd)-1);
if(_s>0)
{
//把最后一个赋为\0
cmd[_s-1]='\0';
}
else
{
perror("read");
return 1;
}
char * _argv[32];
_argv[0]=cmd;
int i=1;
char *start=cmd;
while(*start)
{
//把用户输入的命令中的空格用\0代替
if(isspace(*start))
{
*start='\0';
start++;
_argv[i]=start;
i++;
}
else
start++;
}
_argv[i]=NULL;
pid_t id=fork();
if(id<0)
{
perror("fork");
}
else if(id==0)
{
//child
//程序替换
execvp(_argv[0],_argv);
exit(1);
}
else
{
//father
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0&&WIFEXITED(status))
{}
else
{
perror("wait");
}
}
}
return 0;
}
运行结果: