實(shí)驗(yàn)之前
這個(gè)實(shí)驗(yàn)難度比較適中,當(dāng)然前提是你第八章認(rèn)真研究過(guò)了幾遍,在做這個(gè)實(shí)驗(yàn)之前,請(qǐng)必須閱讀以便官網(wǎng)的writeup文檔,否則你可能不明白這個(gè)實(shí)驗(yàn)要實(shí)現(xiàn)干點(diǎn)什么,然后下載官網(wǎng)的實(shí)驗(yàn)材料到你的電腦上:
Write文檔:
Writeup幫助文檔鏈接
實(shí)驗(yàn)材料:
實(shí)驗(yàn)材料下載鏈接
實(shí)驗(yàn)說(shuō)明
你只能修改tsh.c文件來(lái)完成其中的7個(gè)函數(shù):
?eval:解析和解釋命令行的主例程。 [大約70行]
?builtin_cmd:識(shí)別并解釋內(nèi)置命令:quit,fg,bg和job。 [大約25
行]
?do_bgfg:實(shí)現(xiàn)bg和fg內(nèi)置命令。 [大約50行]
?waitfg:等待前臺(tái)作業(yè)完成。 [大約20行]
?sigchld處理程序:處理SIGCHILD信號(hào)。 [大約80行]
?sigint處理程序:處理SIGINT(ctrl-c)信號(hào)。 [大約15行]
?sigtstp處理程序:處理SIGTSTP(ctrl-z)信號(hào)。 [大約15行]每次修改tsh.c文件后,你都需要鍵入單獨(dú)的一個(gè)
make命令來(lái)完成一系列準(zhǔn)備工作,官方提供了16個(gè)測(cè)試來(lái)檢驗(yàn)?zāi)愕拇鸢?,你需要鍵入make testXX來(lái)輸出自己的答案,然后輸入make rtestXX來(lái)比對(duì)標(biāo)準(zhǔn)答案,其中XX = 01、02、03、......、16。你的答案必須和標(biāo)準(zhǔn)答案一樣才算成功(進(jìn)程號(hào)可以是不同的,因?yàn)樗鼈兪请S機(jī)的),例如:
make test03
這里會(huì)輸出你的答案
make rtest03
這里是標(biāo)準(zhǔn)答案
- 在tsh.c文件中,老師已經(jīng)幫我們實(shí)現(xiàn)了很多輔助函數(shù),在代碼中我會(huì)標(biāo)識(shí)它的作用(讀一讀是很有幫助的),并且已經(jīng)定義了一些全局變量,大大降低了實(shí)驗(yàn)難度,我們需要充分理解這些變量和函數(shù)。如下,這些是已經(jīng)準(zhǔn)備好的函數(shù)或者變量:
extern char **environ; /* defined in libc 這個(gè)時(shí)環(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)程的信息,必要的時(shí)候,我們必須要修改它*/
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ù),和書(shū)上一樣*/
void sigquit_handler(int sig); /*quit信號(hào)處理*/
void clearjob(struct job_t *job); /*清理進(jìn)程鏈表,退出的時(shí)候用*/
void initjobs(struct job_t *jobs); /*初始化進(jìn)程鏈表*/
int maxjid(struct job_t *jobs); /*找到最大的進(jìn)程組號(hào)*/
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline); /*添加進(jìn)程,這個(gè)是需要我們手動(dòng)添加的*/
int deletejob(struct job_t *jobs, pid_t pid); /*刪除進(jìn)程,依然需要我們手動(dòng)刪除*/
pid_t fgpid(struct job_t *jobs); /*如果有前臺(tái)工作進(jìn)程,返回1,否則返回0*/
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); /*通過(guò)pid獲得對(duì)于的進(jìn)程結(jié)構(gòu)體指針*/
struct job_t *getjobjid(struct job_t *jobs, int jid); /*通過(guò)jid獲得對(duì)于的進(jìn)程結(jié)構(gòu)體指針*/
int pid2jid(pid_t pid); /*返回對(duì)于pid進(jìn)程的jid*/
void listjobs(struct job_t *jobs); /*打印進(jìn)程信息*/
void usage(void); /*不用管他*/
void unix_error(char *msg); /*打印錯(cuò)誤信息*/
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
用戶(hù)鍵入的命令行應(yīng)由一個(gè)名稱(chēng)和零個(gè)或多個(gè)參數(shù)組成,所有參數(shù)以一個(gè)或多個(gè)空格分隔。如果name是內(nèi)置命令,則tsh應(yīng)該立即處理它并等待下一個(gè)命令行。否則,tsh應(yīng)該假定名稱(chēng)是可執(zhí)行文件,它會(huì)在初始子進(jìn)程的上下文中加載并運(yùn)行(在這種情況下,工作一詞是指此初始子流程。有幾個(gè)需要注意的地方:
?tsh不需要支持管道(|)或I / O重定向(<和>)。
?鍵入ctrl-c(ctrl-z)應(yīng)該會(huì)導(dǎo)致SIGINT(SIGTSTP)信號(hào)發(fā)送到當(dāng)前的前地面作業(yè)以及該作業(yè)的任何后代(例如,它派生的任何子進(jìn)程)。如果沒(méi)有前臺(tái)作業(yè),則該信號(hào)應(yīng)該沒(méi)有任何作用。
?如果命令行以&結(jié)束,則tsh應(yīng)該在后臺(tái)運(yùn)行作業(yè)。否則,它應(yīng)該在前臺(tái)運(yùn)行作業(yè)。
?每個(gè)作業(yè)都可以由進(jìn)程ID(PID)或作業(yè)ID(JID)標(biāo)識(shí),該ID是一個(gè)正整數(shù)
由tsh分配。 JID應(yīng)該在命令行上以前綴“%”表示。例如,“%5” 表示JID 5,“ 5”表示PID5。(我們已為您提供了所需的所有例程處理工作清單。)
?tsh應(yīng)該支持以下內(nèi)置命令:
– quit命令終止tsh程序。
– jobs命令列出所有后臺(tái)作業(yè)。
– bg <job>命令通過(guò)向其發(fā)送SIGCONT信號(hào)來(lái)重新啟動(dòng)<job>,然后在
的背景。 <job>參數(shù)可以是PID或JID。
– fg <job>命令通過(guò)向其發(fā)送SIGCONT信號(hào)來(lái)重新啟動(dòng)<job>,然后在
前景。 <job>參數(shù)可以是PID或JID。正如前面所說(shuō)的,我們的答案必須要和標(biāo)準(zhǔn)答案相同,所以在特定的位置我們需要輸出特定的語(yǔ)句。例如,在后臺(tái)工作運(yùn)行時(shí),我們要打印它的一系列參數(shù)等等等。
實(shí)驗(yàn)代碼
Fork函數(shù)
這個(gè)不是要求的,但我們?nèi)匀话b好它:
/*自定義安全的Fork*/
pid_t Fork(void){
pid_t pid;
if ((pid = fork()) < 0)
unix_error("fork error!");
return pid;
}
eval函數(shù)
這個(gè)函數(shù)的重點(diǎn)就是要小心同步并發(fā)流中的競(jìng)爭(zhēng),此外,setpgid也是很有必要的,它將子進(jìn)程組與tsh進(jìn)程組分開(kāi),避免tsh收到莫名的信號(hào)而停止。
/*利用書(shū)上已有的框架和543提到的避免并發(fā)競(jìng)爭(zhēng)發(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和信號(hào)處理競(jìng)爭(zhēng)*/
if ((pid = Fork()) == 0){
/*setpgid將子進(jìn)程組和tsh進(jìn)程組分開(kāi)來(lái),避免停止子進(jìn)程組把tsh一起停止掉,同時(shí)子進(jìn)程組id就等于pid,發(fā)送消息很方便*/
/*請(qǐng)注意進(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);/*鎖住一切信號(hào),避免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);/*打印全局變量,仍然需要加塞,防止途中被中斷,可能造成還未讀(寫(xiě))內(nèi)存而內(nèi)存的值卻被修改的情況*/
}
}
//printf ("eve return\n");
return;
builtin_cmd函數(shù)
這個(gè)函數(shù)我們按要求解析4個(gè)內(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); /*這個(gè)是老師寫(xiě)好的輔助函數(shù),實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單*/
else
//printf ("cmd before return :%s \n",argv[0]);
return 0;/*如果不是內(nèi)置命令,返回1*/
return 1; /* not a builtin command */
}
do_bgfg函數(shù)
這個(gè)函數(shù)發(fā)生繼續(xù)信號(hào)到指定進(jìn)程中,并指定是以fg模式還是bg模式運(yùn)行,這意味著原來(lái)的bg進(jìn)程可能會(huì)變成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;
}
}
/*都失敗的話,打印錯(cuò)誤消息*/
else{
printf ("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
/*發(fā)送信號(hào),這里要求發(fā)送到進(jìn)程組,所以采用負(fù)數(shù)*/
/*子進(jìn)程的進(jìn)程組id和pid是一致的,請(qǐng)不要將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ù)
等待前臺(tái)任務(wù)結(jié)束,可以利用老師給定函數(shù)判定,注意這里不能使用簡(jiǎn)單的pause,因?yàn)樾盘?hào)可能會(huì)在執(zhí)行pause前到來(lái)(恰好),這樣就會(huì)形成競(jìng)爭(zhēng)關(guān)系,如果程序這個(gè)時(shí)候執(zhí)行了pause的話,就會(huì)等待著一個(gè)永遠(yuǎn)不會(huì)到來(lái)的信號(hào)(可能永遠(yuǎn)不會(huì)醒過(guò)來(lái))。所以要使用sigsuspend函數(shù),它等價(jià)于三條語(yǔ)句(請(qǐng)翻書(shū)),第一條語(yǔ)句與pause是原子屬性的,它是不可中斷的,在此之前我們堵塞SIGCHLD信號(hào),然后在執(zhí)行sigsuspend第一條語(yǔ)句時(shí),我們短暫的解除堵塞,然后原子的立即執(zhí)行pause,如果在執(zhí)行sigsuspend之前SIGCHLD信號(hào)發(fā)送過(guò)來(lái),它會(huì)被堵塞,而當(dāng)sigsuspend執(zhí)行時(shí),它會(huì)等在pause執(zhí)行時(shí)被釋放,這會(huì)喚醒pause,同時(shí)陷入處理程序,設(shè)置while循環(huán)條件,結(jié)束循環(huán),如果沒(méi)有信號(hào)到來(lái),那么pause就會(huì)正確執(zhí)行。
當(dāng)然這里也可以簡(jiǎn)單的用sleep語(yǔ)句,但是書(shū)上545頁(yè)也陳述了它的缺點(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ù)
這個(gè)函數(shù)要處理三種情況,一是處理正常中止的情況,二是收到信號(hào)中止的情況,三是被信號(hào)所暫時(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ù)
最后兩個(gè)函數(shù)比較簡(jiǎn)單,我們只要負(fù)責(zé)將信號(hào)發(fā)送給前臺(tái)作業(yè)即可。因?yàn)檫@兩個(gè)信號(hào)都是有默認(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個(gè)函數(shù),但是閱讀老師給定幫助函數(shù)是非常有幫助的。做這個(gè)實(shí)驗(yàn)可能更多的要處理好并發(fā)造成的問(wèn)題吧,并發(fā)編程是非常容易出錯(cuò)的,我們必須小心小心再小心。
完整的tsh.c文件下載:
https://github.com/happysnaker/CSAPPLabs/blob/CSAPP/tsh.c