實(shí)驗(yàn)之前
這個實(shí)驗(yàn)難度比較適中椅邓,當(dāng)然前提是你第八章認(rèn)真研究過了幾遍匆光,在做這個實(shí)驗(yàn)之前畔裕,請必須閱讀以便官網(wǎng)的writeup文檔衣撬,否則你可能不明白這個實(shí)驗(yàn)要實(shí)現(xiàn)干點(diǎn)什么,然后下載官網(wǎng)的實(shí)驗(yàn)材料到你的電腦上:
Write文檔:
Writeup幫助文檔鏈接
實(shí)驗(yàn)材料:
實(shí)驗(yàn)材料下載鏈接
實(shí)驗(yàn)說明
你只能修改tsh.c文件來完成其中的7個函數(shù):
?eval:解析和解釋命令行的主例程扮饶。 [大約70行]
?builtin_cmd:識別并解釋內(nèi)置命令:quit具练,fg,bg和job甜无。 [大約25
行]
?do_bgfg:實(shí)現(xiàn)bg和fg內(nèi)置命令扛点。 [大約50行]
?waitfg:等待前臺作業(yè)完成。 [大約20行]
?sigchld處理程序:處理SIGCHILD信號岂丘。 [大約80行]
?sigint處理程序:處理SIGINT(ctrl-c)信號陵究。 [大約15行]
?sigtstp處理程序:處理SIGTSTP(ctrl-z)信號。 [大約15行]每次修改tsh.c文件后元潘,你都需要鍵入單獨(dú)的一個
make
命令來完成一系列準(zhǔn)備工作畔乙,官方提供了16個測試來檢驗(yàn)?zāi)愕拇鸢妇停阈枰I入make testXX
來輸出自己的答案翩概,然后輸入make rtestXX
來比對標(biāo)準(zhǔn)答案,其中XX = 01返咱、02钥庇、03、......咖摹、16评姨。你的答案必須和標(biāo)準(zhǔn)答案一樣才算成功(進(jìn)程號可以是不同的,因?yàn)樗鼈兪请S機(jī)的),例如:
make test03
這里會輸出你的答案
make rtest03
這里是標(biāo)準(zhǔn)答案
- 在tsh.c文件中吐句,老師已經(jīng)幫我們實(shí)現(xiàn)了很多輔助函數(shù)胁后,在代碼中我會標(biāo)識它的作用(讀一讀是很有幫助的),并且已經(jīng)定義了一些全局變量嗦枢,大大降低了實(shí)驗(yàn)難度攀芯,我們需要充分理解這些變量和函數(shù)。如下文虏,這些是已經(jīng)準(zhǔn)備好的函數(shù)或者變量:
extern char **environ; /* defined in libc 這個時環(huán)境變量侣诺,exec的參數(shù),老師已經(jīng)安排好了*/
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
/*進(jìn)程結(jié)構(gòu)體氧秘,保管著所有tsh所有子進(jìn)程的信息年鸳,必要的時候,我們必須要修改它*/
struct job_t { /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
int parseline(const char *cmdline, char **argv); /*解析命令行的函數(shù)丸相,和書上一樣*/
void sigquit_handler(int sig); /*quit信號處理*/
void clearjob(struct job_t *job); /*清理進(jìn)程鏈表搔确,退出的時候用*/
void initjobs(struct job_t *jobs); /*初始化進(jìn)程鏈表*/
int maxjid(struct job_t *jobs); /*找到最大的進(jìn)程組號*/
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline); /*添加進(jìn)程,這個是需要我們手動添加的*/
int deletejob(struct job_t *jobs, pid_t pid); /*刪除進(jìn)程已添,依然需要我們手動刪除*/
pid_t fgpid(struct job_t *jobs); /*如果有前臺工作進(jìn)程妥箕,返回1,否則返回0*/
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); /*通過pid獲得對于的進(jìn)程結(jié)構(gòu)體指針*/
struct job_t *getjobjid(struct job_t *jobs, int jid); /*通過jid獲得對于的進(jìn)程結(jié)構(gòu)體指針*/
int pid2jid(pid_t pid); /*返回對于pid進(jìn)程的jid*/
void listjobs(struct job_t *jobs); /*打印進(jìn)程信息*/
void usage(void); /*不用管他*/
void unix_error(char *msg); /*打印錯誤信息*/
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
用戶鍵入的命令行應(yīng)由一個名稱和零個或多個參數(shù)組成更舞,所有參數(shù)以一個或多個空格分隔畦幢。如果name是內(nèi)置命令,則tsh應(yīng)該立即處理它并等待下一個命令行缆蝉。否則宇葱,tsh應(yīng)該假定名稱是可執(zhí)行文件,它會在初始子進(jìn)程的上下文中加載并運(yùn)行(在這種情況下刊头,工作一詞是指此初始子流程黍瞧。有幾個需要注意的地方:
?tsh不需要支持管道(|)或I / O重定向(<和>)。
?鍵入ctrl-c(ctrl-z)應(yīng)該會導(dǎo)致SIGINT(SIGTSTP)信號發(fā)送到當(dāng)前的前地面作業(yè)以及該作業(yè)的任何后代(例如原杂,它派生的任何子進(jìn)程)印颤。如果沒有前臺作業(yè),則該信號應(yīng)該沒有任何作用穿肄。
?如果命令行以&結(jié)束年局,則tsh應(yīng)該在后臺運(yùn)行作業(yè)。否則咸产,它應(yīng)該在前臺運(yùn)行作業(yè)矢否。
?每個作業(yè)都可以由進(jìn)程ID(PID)或作業(yè)ID(JID)標(biāo)識,該ID是一個正整數(shù)
由tsh分配脑溢。 JID應(yīng)該在命令行上以前綴“%”表示僵朗。例如,“%5” 表示JID 5,“ 5”表示PID5验庙。(我們已為您提供了所需的所有例程處理工作清單顶吮。)
?tsh應(yīng)該支持以下內(nèi)置命令:
– quit命令終止tsh程序。
– jobs命令列出所有后臺作業(yè)粪薛。
– bg <job>命令通過向其發(fā)送SIGCONT信號來重新啟動<job>云矫,然后在
的背景。 <job>參數(shù)可以是PID或JID汗菜。
– fg <job>命令通過向其發(fā)送SIGCONT信號來重新啟動<job>让禀,然后在
前景。 <job>參數(shù)可以是PID或JID陨界。正如前面所說的巡揍,我們的答案必須要和標(biāo)準(zhǔn)答案相同,所以在特定的位置我們需要輸出特定的語句菌瘪。例如腮敌,在后臺工作運(yùn)行時,我們要打印它的一系列參數(shù)等等等俏扩。
實(shí)驗(yàn)代碼
Fork函數(shù)
這個不是要求的糜工,但我們?nèi)匀话b好它:
/*自定義安全的Fork*/
pid_t Fork(void){
pid_t pid;
if ((pid = fork()) < 0)
unix_error("fork error!");
return pid;
}
eval函數(shù)
這個函數(shù)的重點(diǎn)就是要小心同步并發(fā)流中的競爭,此外录淡,setpgid也是很有必要的捌木,它將子進(jìn)程組與tsh進(jìn)程組分開,避免tsh收到莫名的信號而停止嫉戚。
/*利用書上已有的框架和543提到的避免并發(fā)競爭發(fā)生*/
void eval(char *cmdline)
{
char* argv[MAXARGS];
char* buf[MAXLINE];
int bg;
pid_t pid;
sigset_t mask_all, mask_one, prev_one;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL)
return;
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
if (!builtin_cmd(argv)){
sigprocmask(SIG_BLOCK, &mask_one, &prev_one);/*必須要鎖住刨裆,防止addjob和信號處理競爭*/
if ((pid = Fork()) == 0){
/*setpgid將子進(jìn)程組和tsh進(jìn)程組分開來,避免停止子進(jìn)程組把tsh一起停止掉彬檀,同時子進(jìn)程組id就等于pid帆啃,發(fā)送消息很方便*/
/*請注意進(jìn)程組id并不等于題目中的jid*/
if (setpgid(0, 0) < 0){
perror("setpgid error!");
exit(0);
}
sigprocmask(SIG_SETMASK, &prev_one, NULL);/*子進(jìn)程中不需要堵住它,但父進(jìn)程需要*/
if (execve(argv[0], argv, environ) < 0){
printf ("%s: Command not found\n", argv[0]);
exit(0);
}
}
if (!bg){
sigprocmask(SIG_BLOCK, &mask_all, NULL);/*鎖住一切信號窍帝,避免addjob處理程序中斷*/
addjob(jobs, pid, FG, cmdline);
sigprocmask(SIG_SETMASK, &prev_one, NULL);
waitfg(pid);
}
else{
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, BG, cmdline);
printf ("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
sigprocmask(SIG_SETMASK, &prev_one, NULL);/*打印全局變量努潘,仍然需要加塞,防止途中被中斷坤学,可能造成還未讀(寫)內(nèi)存而內(nèi)存的值卻被修改的情況*/
}
}
//printf ("eve return\n");
return;
builtin_cmd函數(shù)
這個函數(shù)我們按要求解析4個內(nèi)置命令就好了
int builtin_cmd(char **argv)
{
//printf ("cmd\n");
if (strcmp(argv[0], "quit") == 0)
exit(0);
else if (strcmp(argv[0], "&") == 0)
return 1;
else if (strcmp(argv[0], "fg") == 0)
do_bgfg(argv);
else if (strcmp(argv[0], "bg") == 0)
do_bgfg(argv);
else if (strcmp(argv[0], "jobs") == 0)
listjobs(jobs); /*這個是老師寫好的輔助函數(shù)疯坤,實(shí)現(xiàn)起來也很簡單*/
else
//printf ("cmd before return :%s \n",argv[0]);
return 0;/*如果不是內(nèi)置命令,返回1*/
return 1; /* not a builtin command */
}
do_bgfg函數(shù)
這個函數(shù)發(fā)生繼續(xù)信號到指定進(jìn)程中拥峦,并指定是以fg模式還是bg模式運(yùn)行贴膘,這意味著原來的bg進(jìn)程可能會變成fg進(jìn)程卖子,所以要注意修改信息略号。
void do_bgfg(char **argv)
{
//printf ("bgfg\n");
struct job_t* job = NULL;
if (argv[1] == NULL){
printf ("%s command requires PID or %jobid argument\n", argv[0]);
return;
}
int idex;
/*解析pid*/
if (sscanf(argv[1], "%d", &idex) > 0){
if ((job = getjobpid(jobs, idex)) == NULL){
printf ("%s: No such process\n", argv[1]);
return;
}
}
/*解析jid*/
else if (sscanf(argv[1], "%%%d", &idex) > 0){
if ((job = getjobjid(jobs, idex)) == NULL){
printf ("%s: No such process\n", argv[1]);
return;
}
}
/*都失敗的話,打印錯誤消息*/
else{
printf ("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
/*發(fā)送信號,這里要求發(fā)送到進(jìn)程組玄柠,所以采用負(fù)數(shù)*/
/*子進(jìn)程的進(jìn)程組id和pid是一致的突梦,請不要將jid和進(jìn)程組id搞混了*/
kill(-(job->pid), SIGCONT);
if (strcmp(argv[0], "bg") == 0){
job->state = BG;/*設(shè)置狀態(tài)*/
printf ("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}
else{
job->state = FG;/*設(shè)置狀態(tài)*/
waitfg(job->pid);
}
//printf ("bgfg return");
return;
}
waitfg函數(shù)
等待前臺任務(wù)結(jié)束,可以利用老師給定函數(shù)判定羽利,注意這里不能使用簡單的pause宫患,因?yàn)樾盘柨赡軙趫?zhí)行pause前到來(恰好),這樣就會形成競爭關(guān)系这弧,如果程序這個時候執(zhí)行了pause的話娃闲,就會等待著一個永遠(yuǎn)不會到來的信號(可能永遠(yuǎn)不會醒過來)。所以要使用sigsuspend函數(shù)匾浪,它等價于三條語句(請翻書)皇帮,第一條語句與pause是原子屬性的,它是不可中斷的蛋辈,在此之前我們堵塞SIGCHLD信號属拾,然后在執(zhí)行sigsuspend第一條語句時,我們短暫的解除堵塞冷溶,然后原子的立即執(zhí)行pause渐白,如果在執(zhí)行sigsuspend之前SIGCHLD信號發(fā)送過來,它會被堵塞逞频,而當(dāng)sigsuspend執(zhí)行時纯衍,它會等在pause執(zhí)行時被釋放,這會喚醒pause苗胀,同時陷入處理程序托酸,設(shè)置while循環(huán)條件,結(jié)束循環(huán)柒巫,如果沒有信號到來励堡,那么pause就會正確執(zhí)行。
當(dāng)然這里也可以簡單的用sleep語句堡掏,但是書上545頁也陳述了它的缺點(diǎn)应结,最好還是利用sigsuspend指令。
void waitfg(pid_t pid)
{
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &prev);
while (fgpid(jobs) != 0){
sigsuspend(&mask);
//printf ("wait here\n");
}
sigprocmask(SIG_SETMASK, &prev, NULL);
return;
}
sigchld_handler函數(shù)
這個函數(shù)要處理三種情況泉唁,一是處理正常中止的情況鹅龄,二是收到信號中止的情況,三是被信號所暫時停止的情況亭畜,前兩種情況都要顯示的將進(jìn)程從進(jìn)程表中刪除扮休,第三種情況卻不用,但是卻要更改狀態(tài)拴鸵。為了防止del程序被中斷玷坠,我們?nèi)匀恍枰尤?/p>
void sigchld_handler(int sig)
{
int olderrno = errno;
int status;
pid_t pid;
struct job_t *job;
sigset_t mask, prev;
sigfillset(&mask);
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
sigprocmask(SIG_BLOCK, &mask, &prev);
if (WIFEXITED(status)){
deletejob(jobs, pid);
}
else if (WIFSIGNALED(status)){
printf ("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if (WIFSTOPPED(status)){
printf ("Job [%d] (%d) stoped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
job = getjobpid(jobs,pid);
job->state = ST;
}
sigprocmask(SIG_SETMASK, &prev, NULL);
}
errno = olderrno;
//printf ("chldreturn\n");
return;
}
sigint_handler函數(shù)和sigtstp_handler函數(shù)
最后兩個函數(shù)比較簡單蜗搔,我們只要負(fù)責(zé)將信號發(fā)送給前臺作業(yè)即可。因?yàn)檫@兩個信號都是有默認(rèn)行為的八堡,不需要我們瞎操心樟凄。
void sigint_handler(int sig)
{
pid_t pid;
if ((pid = fgpid(jobs)) > 0)
kill(-pid, sig);
return;
}
void sigtstp_handler(int sig)
{
pid_t pid;
if ((pid = fgpid(jobs)) > 0)
kill(-pid, sig);
return;
}
總結(jié)
雖然只需要我們實(shí)現(xiàn)7個函數(shù),但是閱讀老師給定幫助函數(shù)是非常有幫助的兄渺。做這個實(shí)驗(yàn)可能更多的要處理好并發(fā)造成的問題吧缝龄,并發(fā)編程是非常容易出錯的,我們必須小心小心再小心挂谍。
完整的tsh.c文件下載:
https://github.com/happysnaker/CSAPPLabs/blob/CSAPP/tsh.c