實驗介紹
完成一個簡單的shell程序,總體的框架和輔助代碼都已經(jīng)提供好了假哎,我們需要完成的函數(shù)主要以下幾個:
- eval: 主要功能是解析cmdline瞬捕,并且運行. [70 lines]
- builtin cmd: 辨識和解析出bulidin命令: quit, fg, bg, and jobs. [25lines]
- do bgfg: 實現(xiàn)bg和fg命令. [50 lines]
- waitfg: 實現(xiàn)等待前臺程序運行結束. [20 lines]
- sigchld handler: 響應SIGCHLD. 80 lines]
- sigint handler: 響應 SIGINT (ctrl-c) 信號. [15 lines]
- sigtstp handler: 響應 SIGTSTP (ctrl-z) 信號. [15 lines]
難點主要在于對信號的處理,需要我們捕獲信號位谋,改變其對應的處理方式山析。
其他需要注意的地方:
- 系統(tǒng)函數(shù)的返回值檢查,一定要多注意有可能出錯的地方掏父;
- 競爭條件,fork子進程之后秆剪,如果子進程很快就結束了赊淑,而此時主進程還沒addjob就會有問題,總之就是不能假設進程之間以安全的順序執(zhí)行仅讽,這里利用互斥量的思路陶缺,主進程會阻塞子進程的信號,直到addjob之后洁灵;
- SIGCHLD信號處理函數(shù)饱岸,考慮多個子進程結束,以及非正常結束時waitpid的返回值徽千,后面結合課本里詳細說苫费。
有關Shell
我們要實現(xiàn)的shell有兩種執(zhí)行模式
- 如果用戶輸入的命令是內(nèi)置命令,那么 shell 會直接在當前進程執(zhí)行(例如 jobs)
- 如果用戶輸入的是一個可執(zhí)行程序的路徑双抽,那么 shell 會 fork 出一個新進程百框,并且在這個子進程中執(zhí)行該程序(例如 /bin/ls -l -d)
第二種情況中,如果命令以& 結束牍汹,那么這個job在后臺執(zhí)行
需要支持的功能:
- job control:允許用戶更改進程的前臺/后臺狀態(tài)以及京城的狀態(tài)(running, stopped, or terminated)
- ctrl-c 會觸發(fā) SIGINT 信號并發(fā)送給每個前臺進程铐维,默認的動作是終止該進程
- ctrl-z 會觸發(fā) SIGTSTP 信號并發(fā)送給每個前臺進程柬泽,默認的動作是掛起該進程,直到再收到 SIGCONT 信號才繼續(xù)
- jobs 命令會列出正在執(zhí)行和被掛起的后臺任務
- bg job 命令可以讓一個被掛起的后臺任務繼續(xù)執(zhí)行 嫁蛇,fg job 命令同理
參考課本代碼
先來看看課本上我們可以參考的代碼有哪些
P525 eval()函數(shù)原型
void eval(char *cmdline)
{
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE]; /* Holds modified command line */
int bg; /* Should the job run in bg or fg? */
pid_t pid; /* Process id */
strcpy(buf, cmdline);
bg = parseline(buf, argv); //解析命令行函數(shù)都提供好了
if (argv[0] == NULL)
return; /* Ignore empty lines */
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) { /* 子進程來執(zhí)行job */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* 如果是前臺作業(yè)锨并,主進程需要等待子進程運行完畢 */
if (!bg) {
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
}
else
printf("%d %s", pid, cmdline);
}
return;
}
基本功能都完成了,唯一的不足就是由于joblist的存在睬棚,需要考慮競爭條件第煮,也就是主進程一定要先addjob,然后才能deletejob
然后看一下信號處理函數(shù)闸拿,實驗代碼里已經(jīng)要求了空盼,需要對以下三個信號進行處理
Signal(SIGINT, sigint_handler); /* ctrl-c /
Signal(SIGTSTP, sigtstp_handler); / ctrl-z /
Signal(SIGCHLD, sigchld_handler); / Terminated or stopped child */
那么就來看看書上P532的示例:
#include "csapp.h"
void sigint_handler(int sig) /* SIGINT handler */ //line:ecf:sigint:beginhandler
{
printf("Caught SIGINT!\n"); //line:ecf:sigint:printhandler
exit(0); //line:ecf:sigint:exithandler
} //line:ecf:sigint:endhandler
int main()
{
/* Install the SIGINT handler */
if (signal(SIGINT, sigint_handler) == SIG_ERR) //line:ecf:sigint:begininstall
unix_error("signal error"); //line:ecf:sigint:endinstall
pause(); /* Wait for the receipt of a signal */ //line:ecf:sigint:pause
return 0;
}
嗯,SIGINT就是我們想自己處理的信號新荤,然后通過sigint_handler來進行自定義的處理揽趾。(當然這示例也忒簡單了)
還有一個重要的信號不排隊問題,涉及到父進程回收子進程:
- 子進程結束的時候向父進程發(fā)生信號苛骨,但是內(nèi)核的規(guī)矩是這樣的:
在任何時刻篱瞎,一種類型至多只會有一個待處理信號(內(nèi)核中負責維護待處理信號的pending位向量對應的特定信號類型只有一位),也就是說信號是不會排隊的痒芝,如果處理信號A的過程中又來了信號B俐筋,信號B是會被阻塞的,此時又來信號C严衬,那么信號C就被丟棄了澄者,處理辦法也很簡單,每次處理信號的時候请琳,用while循環(huán)盡可能多接收幾個信號粱挡。
書上的對應代碼P539
void handler2(int sig)
{
int olderrno = errno;
while (waitpid(-1, NULL, 0) > 0) {
Sio_puts("Handler reaped child\n");
}
if (errno != ECHILD)
Sio_error("waitpid error");
Sleep(1);
errno = olderrno;
}
接下來再看信號阻塞,也就是要避免父進程和子進程多job列表操作的競爭
linux提供阻塞信號的隱式機制和顯式機制
- 隱式:默認阻塞當前處理程序正在處理的信號類型
- 顯式:使用sigprocmask函數(shù)
具體怎么使用可以參考課本P543的promask2.c
#include "csapp.h"
void handler(int sig)
{
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;
Sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap a zombie child */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); //阻塞所有信號
deletejob(pid); /* 對joblist安全刪除 */
Sigprocmask(SIG_SETMASK, &prev_all, NULL); //恢復所有信號
}
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all, mask_one, prev_one;
Sigfillset(&mask_all);
Sigemptyset(&mask_one);
Sigaddset(&mask_one, SIGCHLD);
Signal(SIGCHLD, handler);
initjobs(); /* Initialize the job list */
while (1) {
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* 阻塞 SIGCHLD */
if ((pid = Fork()) == 0) { /* Child process */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* 子進程解除阻塞 SIGCHLD */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* 阻塞所有信號*/
addjob(pid); /* 對job列表的操作安全了 */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* 父進程解除阻塞 SIGCHLD */
}
exit(0);
}
看起來挺麻煩俄精,實際我們可以簡化的询筏,實際寫的時候只阻塞SIGCHLD即可。
課本里面還提到一點竖慧,主程序顯式等待某個信號處理程序進行嫌套,尤其是shell創(chuàng)建前臺作業(yè)的時候,基本思路是用循環(huán)圾旨,當接收到信號時踱讨,在信號處理程序中把while條件更改來跳出循環(huán),這樣做存在資源浪費以及無法精準喚醒的問題碳胳,比較好的解決方法是sigsuspend勇蝙,相當于一個原子操作,可以暫時取消阻塞SIGCHLD,然后pause接收信號味混,緊接著恢復阻塞产雹。
否則的話,本來的pause()是掛起進程直到收到信號翁锡,那么下面的代碼有可能永遠休眠
while(!pid)
pause(); //如果在while判斷之后和pause之前收到信號蔓挖,那就錯過啦,所以需要原子操作
實驗代碼
經(jīng)過上面對課本代碼的復習馆衔,實際上好多功能都已經(jīng)給出了實現(xiàn)方法瘟判,實驗代碼自然也就不難給出啦。
1. eval()
主要功能是對用戶輸入的參數(shù)進行解析并運行計算角溃。如果用戶輸入內(nèi)建的命令行(quit拷获,bg溪窒,fg配猫,jobs)那么立即執(zhí)行。 否則蛮寂,fork一個新的子進程并且將該任務在子進程的上下文中運行未蝌。如果該任務是前臺任務那么需要等到它運行結束才返回驮吱。
- 注意每個子進程必須用戶自己獨一無二的進程組id,要不然就沒有前臺后臺區(qū)分啦
- 在fork()新進程前后要阻塞SIGCHLD信號萧吠,防止出現(xiàn)競爭(race)這種經(jīng)典的同步錯誤
void eval(char *cmdline)
{
char* argv[MAXARGS];
pid_t pid;
sigset_t mask;
int fgorbg = parseline(cmdline,argv);
if(argv[0] == NULL)
return;
sigemptyset(&mask);
sigaddset(&mask,SIGCHLD);
sigprocmask(SIG_BLOCK,&mask,NULL);
if(!builtin_cmd(argv)){
if((pid = fork()) == 0){
sigprocmask(SIG_UNBLOCK,&mask,NULL); //子進程也是要解除阻塞的
if(setpgid(0,0)<0)
unix_error("eval: setgpid failed.\n");
if(execve(argv[0],argv,environ)<0){
printf("%s: Command not found.\n",argv[0]);
exit(0);
}
}
if(!fgorbg)
addjob(jobs,pid,FG,cmdline);
else
addjob(jobs,pid,BG,cmdline);
sigprocmask(SIG_UNBLOCK,&mask,NULL); //這里一定要addjob之后再解除阻塞
if(!fgorbg) // FG job
waitfg(pid);
else
printf("[%d] (%d) %s\n",pid2jid(pid),pid,cmdline);
}
return;
}
2. builtin_cmd()
判斷命令是否是內(nèi)置指令左冬,是的話立即執(zhí)行,不是則返回纸型,對單獨的‘&’無視
int builtin_cmd(char **argv)
{
if(strcmp(argv[0],"quit")==0)
{printf("exit\n");exit(0);}
if(strcmp(argv[0],"jobs")==0)
{
listjobs(jobs);
return 1;
}
if(strcmp(argv[0],"bg")==0 || strcmp(argv[0],"fg")==0)
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
3. do_bgfg()
執(zhí)行bg和fg指令功能
void do_bgfg(char **argv)
{
char *id = argv[1];
struct job_t *job;
int jobid;
if(id == NULL){
printf("%s command requires PID of jobid argument.\n",argv[0]);
return;
}
if(id[0] == '%')
jobid = atoi(id+1);
if((job = getjobjid(jobs,jobid))==NULL){
printf("Job does not exist.\n");
return;
}
if(strcmp(argv[0],"bg")==0){
job->state = BG;
kill(-(job->pid),SIGCONT);
}
if(strcmp(argv[0],"fg")==0){
job->state = FG;
kill(-(job->pid),SIGCONT);
waitfg(job->pid);
}
return;
}
kill函數(shù)的用法拇砰,向任何進程組或進程發(fā)送信號
int kill(pid_t pid, int sig);
參數(shù)pid的可能選擇:
- pid大于零時,pid是信號欲送往的進程的標識狰腌。
- pid等于零時毕匀,信號將送往所有與調用kill()的那個進程屬同一個使用組的進程。
- pid等于-1時癌别,信號將送往所有調用進程有權給其發(fā)送信號的進程,除了進程1(init)蹋笼。
- pid小于-1時展姐,信號將送往以-pid為組標識的進程。
4. waitfg()
等待前臺進程完成剖毯,這里偷懶了圾笨,沒用sigsuspend,還是用了比較消耗資源的方法哈哈哈逊谋。擂达。。
void waitfg(pid_t pid)
{
while(pid == fgpid(jobs));
return;
}
5. 幾個信號處理函數(shù)
SIGINT處理比較簡單胶滋,就是截獲CTRL+C然后發(fā)給前臺程序嘛
void sigint_handler(int sig)
{
pid_t pid = fgpid(jobs);
int jid = pid2jid(pid);
if(pid!=0){
printf("Job [%d] terminated by SIGINT.\n",jid);
deletejob(jobs,pid);
kill(-pid,sig);
}
return;
}
SIGTSTP就是把CTRL+Z發(fā)給前臺
void sigtstp_handler(int sig)
{
pid_t pid = fgpid(jobs);
int jid = pid2jid(pid);
if(pid!=0){
printf("Job [%d] stopped by SIGINT.\n",jid);
(*getjobpid(jobs,pid)).state = ST;;
kill(-pid,sig);
}
return;
}
SIGCHLD是最麻煩的了板鬓,參考網(wǎng)上大牛的方法悲敷,需要考慮子進程返回的原因
運用waitpid()函數(shù)并且用WNOHANG|WUNTRACED參數(shù),該參數(shù)的作用是判斷當前進程中是否存在已經(jīng)停止或者終止的進程俭令,如果存在則返回pid后德,不存在則立即返回
通過另外一個&status參數(shù),我們可以判斷返回的進程是由于什么原因停止或暫停的抄腔。
- WIFEXITED(status):
如果進程是正常返回即為true瓢湃,什么是正常返回呢?就是通過調用exit()或者return返回的 - WIFSIGNALED(status):
如果進程因為捕獲一個信號而終止的赫蛇,則返回true - WTERMSIG(status):
當WIFSIGNALED(status)為真時绵患,設置該值,返回導致當前狀態(tài)的信號編號 - WIFSTOPPED(status):
如果返回的進程當前是被停止悟耘,則為true - WSTOPSIG(status):
返回引起進程停止的信號
void sigchld_handler(int sig)
{
pid_t pid;
int status,child_sig;
while((pid = waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0 ){
printf("Handling chlid proess %d\n", (int)pid);
/*handle SIGTSTP*/
if( WIFSTOPPED(status) )
sigtstp_handler( WSTOPSIG(status) );
/*handle child process interrupt by uncatched signal*/
else if( WIFSIGNALED(status) ) {
child_sig = WTERMSIG(status);
if(child_sig == SIGINT)
sigint_handler(child_sig);
}
else
deletejob(jobs, pid);
}
return;
}
總結
本次Shell Lab的收獲有以下幾點
- 對Shell有了更加深刻的理解落蝙,借助實驗代碼實現(xiàn)了不帶重定向的簡單shell
- 掌握信號的正確接收處理,阻塞和解除阻塞機制作煌,寫出避免競爭的代碼
- 父進程和子進程的fork掘殴,回收,信號傳遞等
- linux下編程規(guī)范粟誓,以及進程相關函數(shù)的使用奏寨,學到了學到了