CSAPP : Shell Lab

實驗介紹

完成一個簡單的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]

難點主要在于對信號的處理,需要我們捕獲信號位谋,改變其對應的處理方式山析。
其他需要注意的地方:

  1. 系統(tǒng)函數(shù)的返回值檢查,一定要多注意有可能出錯的地方掏父;
  2. 競爭條件,fork子進程之后秆剪,如果子進程很快就結束了赊淑,而此時主進程還沒addjob就會有問題,總之就是不能假設進程之間以安全的順序執(zhí)行仅讽,這里利用互斥量的思路陶缺,主進程會阻塞子進程的信號,直到addjob之后洁灵;
  3. SIGCHLD信號處理函數(shù)饱岸,考慮多個子進程結束,以及非正常結束時waitpid的返回值徽千,后面結合課本里詳細說苫费。

有關Shell

我們要實現(xiàn)的shell有兩種執(zhí)行模式

  1. 如果用戶輸入的命令是內(nèi)置命令,那么 shell 會直接在當前進程執(zhí)行(例如 jobs)
  2. 如果用戶輸入的是一個可執(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一個新的子進程并且將該任務在子進程的上下文中運行未蝌。如果該任務是前臺任務那么需要等到它運行結束才返回驮吱。

  1. 注意每個子進程必須用戶自己獨一無二的進程組id,要不然就沒有前臺后臺區(qū)分啦
  2. 在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的可能選擇:

  1. pid大于零時,pid是信號欲送往的進程的標識狰腌。
  2. pid等于零時毕匀,信號將送往所有與調用kill()的那個進程屬同一個使用組的進程。
  3. pid等于-1時癌别,信號將送往所有調用進程有權給其發(fā)送信號的進程,除了進程1(init)蹋笼。
  4. 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的收獲有以下幾點

  1. 對Shell有了更加深刻的理解落蝙,借助實驗代碼實現(xiàn)了不帶重定向的簡單shell
  2. 掌握信號的正確接收處理,阻塞和解除阻塞機制作煌,寫出避免競爭的代碼
  3. 父進程和子進程的fork掘殴,回收,信號傳遞等
  4. linux下編程規(guī)范粟誓,以及進程相關函數(shù)的使用奏寨,學到了學到了
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鹰服,隨后出現(xiàn)的幾起案子病瞳,更是在濱河造成了極大的恐慌,老刑警劉巖悲酷,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件套菜,死亡現(xiàn)場離奇詭異,居然都是意外死亡设易,警方通過查閱死者的電腦和手機逗柴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來顿肺,“玉大人戏溺,你說我怎么就攤上這事⊥雷穑” “怎么了旷祸?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長讼昆。 經(jīng)常有香客問我托享,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任闰围,我火速辦了婚禮赃绊,結果婚禮上,老公的妹妹穿的比我還像新娘辫诅。我一直安慰自己凭戴,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布炕矮。 她就那樣靜靜地躺著么夫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肤视。 梳的紋絲不亂的頭發(fā)上档痪,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音邢滑,去河邊找鬼腐螟。 笑死,一個胖子當著我的面吹牛困后,可吹牛的內(nèi)容都是我干的乐纸。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼摇予,長吁一口氣:“原來是場噩夢啊……” “哼汽绢!你這毒婦竟也來了?” 一聲冷哼從身側響起侧戴,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤宁昭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酗宋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體积仗,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年蜕猫,在試婚紗的時候發(fā)現(xiàn)自己被綠了寂曹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡回右,死狀恐怖稀颁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情楣黍,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布棱烂,位于F島的核電站租漂,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜哩治,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一秃踩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧业筏,春花似錦憔杨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至台谢,卻和暖如春寻狂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背朋沮。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工蛇券, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人樊拓。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓纠亚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筋夏。 傳聞我的和親對象是個殘疾皇子蒂胞,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容

  • 又來到了一個老生常談的問題,應用層軟件開發(fā)的程序員要不要了解和深入學習操作系統(tǒng)呢叁丧? 今天就這個問題開始啤誊,來談談操...
    tangsl閱讀 4,104評論 0 23
  • 轉自:http://www.dbafree.net/?p=870 我們可以使用kill -l查看所有的信號量解釋,...
    AndreaArlex閱讀 6,088評論 0 4
  • 一直以來拥娄,我知道情緒是由自己的蚊锹,生氣是自己決定的,不是因為他人稚瘾。 總是想著怎么去控制自己的情緒牡昆,不做情...
    西想事成閱讀 228評論 9 7
  • 從前有個男孩丢烘,是個內(nèi)向的人,但他把一切看的很清楚些椒!他狠討厭這樣的自己播瞳,他曾想用一切去愛一個人!他發(fā)現(xiàn)女孩...
    一念_傾心閱讀 211評論 0 0
  • 你坐在窗邊 享受陽光 聽著手機里的樂曲 會不會忽然心一緊 想起了某人 你在街頭悠悠漫步 聽到熟悉的音調 會不會不自...
    小林子的春天閱讀 501評論 0 1