1. 程式人生 > >2018.12.10 把shelllab撿起來做了

2018.12.10 把shelllab撿起來做了

好久沒寫了啊,總算把這個lab撿起來了。

具體內容不寫了,把網上另一個人寫的部落格轉載一下算作mark。

以下內容轉載自https://zhuanlan.zhihu.com/p/28695244

實驗思路

本次實驗就是自己實現一個shell,不過不是從頭自己寫,是完成幾個函式的實現。

  • eval函式包含了shell的主要操作,讀取命令列,fork子程序,執行
  • builtin_cmd函式包含了處理內建命令列函式的操作,包括quit, fg, bg, jobs
  • do_bgfg函式用來處理fg和bg操作,主要是對程序變換狀態以及傳送SIGCONT訊號
  • waitfg函式用來等待前臺程式結束,因為回收子程序交給了sigchld_handler來做,所以waitfg只要用sleep寫一個忙等待來等到前臺程序結束。
  • 三個訊號的操作函式也是要重點實現的內容。

這次實驗比較難,有很多需要注意的細節,我開頭沒有理解透整個shell的操作,外加要求沒有完全看清,導致前期做的非常困難。

需要注意的細節有:

  • 子程序的回收在sigchld_handler中來做,waitfg只要忙等待前臺程序結束就行。
  • 後臺程序執行之後放著不用管就行,父程序不用等到後臺子程序執行結束,直接可以繼續後續操作(開頭寫錯了,導致語句輸出順序出現錯誤,以及一些後臺程序被執行完後才開始執行後續操作)
  • 熟練使用waitpid中的引數,WNOHANG|WUNTRACED組合在一起用是立即返回的意思。
  • 用WIFEXITED(status),WIFSIGNALED(status),WIFSTOPPED(status)等來補獲終止或者被停止的子程序的退出狀態(主要用於sigchld_handler中)
  • 對於每個fork的子程序,執行setgpid(0, 0),這樣就會以子程序號單獨開一個程序組,也可以方便的使用kill(-pid, SIGNAL)來把訊號發到pid所在的整個程序組。
  • 對於呼叫sigchld_handler回收子程序時,必須用掩碼阻塞SIGCHLD訊號,防止子程序還沒執行,就回收了。
  • 對於每個子程序,加入joblist之後,在結束時在sigchld_handler中有三種操作,一種是正常結束,deletejob,一種是被訊號終止了,也要deletejob,還有一種是被訊號停止了,不用delete,只要修改job的狀態。
  • 最重要的是,不要死磕一個trace,其實有的trace,需要你實現很多內容,可能實現了前一個,後面幾個都完成了,所以需要仔細分析所有的需求。
  • 看一下給的其他程式,myspin,主要是一個延遲函式,所以在前臺執行,就要等它延遲結束,如果在後臺執行,就可以在exevce之後不管。mystop是用來發送停止訊號,myint是用來發送終止訊號。

eval

void eval(char *cmdline) { char *argv[MAXARGS]; char buf[MAXLINE]; int bg; pid_t pid; strcpy(buf, cmdline); bg = parseline(buf, argv); if(argv[0] == NULL) return; if(!builtin_cmd(argv)){ sigprocmask(SIG_BLOCK, &mask_one, &prev_one);//block SIGCHLD if((pid = fork()) == 0){ sigprocmask(SIG_SETMASK, &prev_one, NULL);//unblock SIGCHLD if(setpgid(0, 0) < 0){ printf("setpgid error"); exit(0); } if(execve(argv[0], argv, environ) < 0){ printf("%s: Command not found.\n", argv[0]); exit(0); } } else{ if(bg) addjob(jobs, pid, BG, cmdline); else addjob(jobs, pid, FG, cmdline); sigprocmask(SIG_SETMASK, &prev_one, NULL);//unblock SIGCHLD if(bg){ printf("[%d] (%d) %s",pid2jid(pid), pid, cmdline); } else{ waitfg(pid); } } } return; } 

eval函式,沒啥多說的,需要注意的上面都講過了,主要就是對每個操作是bg還是fg要分清楚操作,加掩碼阻塞SIGCHLD訊號的程式碼書本上有說明,然後就是父程序,對於前後臺程序,加入joblist要注意狀態BG和FG。然後後臺程序輸出一句話,前臺程序需要等待子程序執行結束。

 

builtin_cmd

int builtin_cmd(char **argv) { if(!strcmp(argv[0], "quit")) exit(0); if(!strcmp(argv[0], "jobs")){ listjobs(jobs); return 1; } if(!strcmp(argv[0], "bg")){ do_bgfg(argv); return 1; } if(!strcmp(argv[0], "fg")){ do_bgfg(argv); return 1; } return 0; /* not a builtin command */ } 

判斷是否是內建命令列函式,如果是,就執行相應操作,如果不是,就返回到eval中,建立子程序執行操作。(listjobs我開頭沒有弄好子程序回收,導致前臺程式還留在 joblist中,然後輸出時會輸出前臺程序。要考慮到前臺程序執行結束之後,才能執行jobs命令,所以絕對不會出現狀態是前臺的程序)

 

do_bgfg

void do_bgfg(char **argv) { int id; struct job_t *job; if(argv[1] == NULL){ printf("%s command requires PID or %%jobid argument\n",argv[0]); return; } if(argv[1][0]=='%'){ if(argv[1][1] >='0'&& argv[1][1] <='9'){ id=atoi(argv[1]+1); job = getjobjid(jobs, id); if(job==NULL){ printf("%s: No such job\n",argv[1]); return; } } else { printf("%s: argument must be a PID or %%jobid\n",argv[0]); return; } } else{ if(argv[1][0] >='0'&& argv[1][0] <='9'){ id = atoi(argv[1]); job = getjobpid(jobs, id); if(job == NULL){ printf("(%s): No such process\n",argv[1]); return; } } else { printf("%s: argument must be a PID or %%jobid\n",argv[0]); return; } } kill(-(job->pid), SIGCONT); if(!strcmp(argv[0], "bg")){ job->state = BG; printf("[%d] (%d) %s",job->jid, job->pid, job->cmdline); } else { job->state = FG; waitfg(job->pid); } return; } 

處理bg和fg命令的函式,主要是解析引數,判斷是否會出現命令錯誤。找到需要操作的程序,然後用kill函式對它發出SIGCONT訊號,因為前面建立程序的時候都是用setgpid,每個程序在單獨 一個程序組,所以這邊kill用的非常舒服。

程序繼續執行之後,只需要給後臺程序改一下state,前臺程序的話,也要改state,還要waitfg

 

waitfg

void waitfg(pid_t pid) { while(pid==fgpid(jobs)){ sleep(0); } return; } 

等到前臺程序執行結束的函式。呼叫自帶的fgpid函式就能知道當前前臺程序號,因為不需要負責子程序回收,只要判斷當前程序號是否是前臺程序,如果前臺程序號變化,就說明結束了,就跳出迴圈,否則忙等待。

 

sigchld_handler

void sigchld_handler(int sig) { pid_t pid; int status; while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){ if(WIFEXITED(status)){ deletejob(jobs, pid); } if(WIFSTOPPED(status)){ struct job_t *job = getjobpid(jobs, pid); int jid = pid2jid(pid); printf("Job [%d] (%d) stopped by signal %d\n",jid, pid, WSTOPSIG(status)); job->state = ST; } if(WIFSIGNALED(status)){ int jid = pid2jid(pid); printf("Job [%d] (%d) terminated by signal %d\n",jid, pid, WTERMSIG(status)); deletejob(jobs, pid); } } return; } 

處理一個子程序結束時發出SIGCHLD訊號的函式,對於一個子程序結束,主要有3種原因,正常執行結束,收到SIGINT終止,收到SIGTSTP停止。

對於這三種情況應該要有不同的處理,首先正常結束的肯定要delete,不然jobs的時候,很多前臺程式多會在其中。

對於終止的程序,肯定是不會再啟動了,也要delete,並且輸出一些資訊 。

對於停止的,可能會再啟動,所以只要修改其state就行了。

 

sigint_handler

void sigint_handler(int sig) { pid_t pid = fgpid(jobs); if(pid!=0){ kill(-pid,sig); } return; } 

給當前前臺程序傳送SIGINT訊號,用kill傳送比較好

 

sigtstp_handler

void sigtstp_handler(int sig) { pid_t pid = fgpid(jobs); if(pid!=0){ kill(-pid,sig); } return; } 

和上面類似。

 

總結

shell lab總體來說難度較大,因為開頭沒有理解shell的流程,對很多命令的執行情況不熟悉,所以對出來的結果很懵逼。不過後來慢慢搞懂了執行順序和情況,訊號的作用,程序的執行情況和週期,以及執行過程,就知道應該注意的點在哪裡了,然後一步步的實現就可以了,這個實驗讓我對shell和程序的理解加深了很多。