在Linux系統中,⽤用fork創建⼦子進程後執⾏行的是和⽗父進程相同的程序(但有可能執⾏行不同的代碼分⽀支),⼦進程往往 要調⽤用⼀一種exec函數以執⾏行另⼀一個程序。當進程調⽤用⼀一種exec函數時,該進程的⽤用戶空間代碼和數 據完全被新程序替換,從新程序的啓動例程開始執⾏行。調⽤用exec並不創建新進程,所以調⽤用exec前後該進程的id並未改變。
Linux操作系統中的shell就是運用這個原理處理客戶請求的,不是每個請求都是shell親力親爲的,所以shell會創建子程序替換他,在實現shell的過程中我們會用到exec函數,所以我們先了解一下exec函數族並對其每個的用法用代碼實現一遍。
其實有六種以exec開頭的函數,統稱exec函數:
#include <unistd.h>
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<stdio.h>
#include<stdlib.h>
#include<unistd.h>
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<stdio.h>
#include<unistd.h>
#include<stdlib.h>
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<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
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;
}
運行結果: