簡介
CSAPP實驗介紹
學生實作他們自己的帶有作業控制的Unix Shell程式,包括Ctrl + C和Ctrl + Z按鍵,fg,bg,和 jobs命令,這是學生第一次接觸并發,并且讓他們對Unix的行程控制、信號和信號處理有清晰的了解,
什么是Shell?
? Shell就是用戶與作業系統內核之間的介面,起著協呼叫戶與系統的一致性和在用戶與系統之間進行互動的作用,
? Shell最重要的功能是命令解釋,從這種意義上說,Shell是一個命令解釋器,
? Linux系統上的所有可執行檔案都可以作為Shell命令來執行,當用戶提交了一個命令后,Shell首先判斷它是否為內置命令,如果是就通過Shell內部的解釋器將其解釋為系統功能呼叫并轉交給內核執行;若是外部命令或實用程式就試圖在硬碟中查找該命令并將其調入記憶體,再將其解釋為系統功能呼叫并轉交給內核執行,在查找該命令時分為兩種情況:
(1)用戶給出了命令的路徑,Shell就沿著用戶給出的路徑進行查找,若找到則調入記憶體,若沒找到則輸出提示資訊;
(2)用戶沒有給出命令的路徑,Shell就在環境變數Path所制定的路徑中依次進行查找,若找到則調入記憶體,若沒找到則輸出提示資訊,
關于本次實驗
本次實驗需要我們熟讀CSAPP第八章例外控制流,
需要設計和實作的函式:
- eval 函式:決議命令列,
Evaluate the command line that the user has just typed in.
- builtin_cmd:判斷是否為內置 shell 命令
If the user has typed a built-in command then execute it immediately.
- do_bgfg:實作內置命令bg,fg,
Execute the builtin bg and fg commands.
- waitfg:等待前臺作業完成,
Block until process pid is no longer the foreground process
- sigchld_handler:捕獲SIGCHLD信號,
- sigint_handler:捕獲SIGINT信號,
- sigtstp_handler:捕獲SIGTSTP信號,
TinyShell輔助函式:
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); //決議命令列引數
void sigquit_handler(int sig);//退出的處理函式
/*jobs是全域變數,存盤每一個行程的資訊,*/
/*jid為job編號ID,pid為行程ID*/
void clearjob(struct job_t *job);//清除所有作業
void initjobs(struct job_t *jobs);//初始化作業結構體
int maxjid(struct job_t *jobs); //回傳jobs中jid的最大值
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//添加job
int deletejob(struct job_t *jobs, pid_t pid); //洗掉job
pid_t fgpid(struct job_t *jobs);//回傳前臺運行job的pid
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);//回傳對應pid的job
struct job_t *getjobjid(struct job_t *jobs, int jid); //回傳jid對應的job
int pid2jid(pid_t pid); //pid轉jid
void listjobs(struct job_t *jobs);//遍歷
void usage(void);//幫助資訊
void unix_error(char *msg);//報錯unix-style error routine
void app_error(char *msg);//報錯application-style error routine
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);//信號設定
實驗要求
-
tsh的提示符:tsh>
-
用戶輸入的命令列應該包括一個名字、0或多個引數,并用一個或多個空格分隔,
-
如果名字是內置命令,tsh立即處理并等待用戶輸入下一個命令列,否則,假定這個名字是一個可執行檔案的路徑,tsh在初始子行程的背景關系中加載和運行它,
-
tsh不需要支持管(|)或I/O重定向(<和>),
-
鍵入ctrl-c(ctrl-z)應該導致SIGINT(SIGTSTP)信號被發送到當前的前臺作業,及其該作業的子孫作業(例如,它創建的任何子行程),如果沒有前臺作業,那么信號應該沒有效果,
-
如果命令列以&結尾,則tsh在后臺運行該作業;否則,在前臺運行該作業
-
可以用行程ID(PID)或tsh賦予的正整數作業ID(job ID,JID)標識一個作業,JID用前綴%,例如%5標識作業ID為5的作業,5表示PID為5的作業,
-
已經提供了處理作業串列所需的所有函式
-
tsh支持以下內置命令:
- quit:終止tsh程式
- jobs:列出所有后臺job
- bg:后臺運行程式
- fg:前臺運行程式
回顧
fork
pid_t fork(void)
在函式呼叫處創建子行程,
父行程函式回傳子行程的PID,
子行程函式回傳0,
waitpid
一個行程可以通過waitpid函式來等待它的子行程終止或者停止,
pid_t waitpid(pid_t pid, int *statusp, int options);
pid:判定等待集合的成員
- 當pid > 0時,waitpid等待行程ID為pid的行程;
- 當pid = -1時,waitpid等待所有它的子行程,
options:修改默認行為
options中有如下選項:
- WNOHANG:若當前沒有等待集合中的子行程終止,則立即回傳0
- WUNTRACED:等待直到某個等待集合中的子行程停止或回傳,并回傳這個子行程的pid,
- WCONTINUED:等待直到某個等待集合中的子行程重新開始執行或終止,
- 組合WNOHANG | WUNTRACED:立即回傳,如果等待集合中的子行程都沒有被停止或終止,則回傳0,如果有,則回傳PID,
statusp:檢查已回收子行程的退出狀態
如果statusp引數非空,那么waitpid就會在status中放入關于導致回傳的子行程的狀態資訊,status是statusp指向的值,
- WIFEXITED(status):如果子行程通過呼叫exit或者回傳(return)正常終止,就回傳真,
- ········
kill函式
int kill(pid_t pid, int signo);
- pid > 0,信號發送給pid行程;
- pid == 0,把信號發送給本行程(自己)所在的行程組中所有行程,不包括系統行程;
- pid < 0,把信號發送給組id 為 -pid 的行程組中所有行程;
- pid == -1,把信號發送給所有行程,除系統行程外(有些行程不接受9和19號信號)
安全的信號處理
目的
讓信號處理程式和主程式它們可以安全地,無錯誤地,按照我們預期地并發地運行,
方法
-
處理程式盡可能簡單,
-
在處理程式只呼叫異步信號安全的函式,
- 可重入的(只訪問區域變數),
- 不能被信號處理程式中斷,
-
保存和恢復errno,避免干擾其他依賴于errno的部分,解決方法是用區域變數存盤,再恢復,
void Example(int sig)
{
int olderrno = errno;
/*
this is your code
*/
errno = olderrno;
}
- 阻塞所有信號,保護對共享全域變數資料結構的訪問,
- 用volatile宣告全域變數,
- 用sig_atomic_t宣告標志,
例:在添加job時,阻塞信號,因為jobs是全域變數,
This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP signals until we can add the job to the job list. This eliminates some nasty races between adding a job to the job list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.
注意
- 不可以用信號來對其他進制中發生的事件計數,
- 使用原子(atomic)函式如sigsuspend函式消除潛在的競爭并提高效率,
實驗
eval
要點分析:
-
創建子行程前需要阻塞信號,防止競爭,
-
將子行程加入到jobs后,需要恢復,即解除阻塞,
-
創建子行程時,為子行程創建一個新的行程組,
/*
* eval - Evaluate the command line that the user has just typed in
*
* If the user has requested a built-in command (quit, jobs, bg or fg)
* then execute it immediately. Otherwise, fork a child process and
* run the job in the context of the child. If the job is running in
* the foreground, wait for it to terminate and then return. Note:
* each child process must have a unique process group ID so that our
* background children don't receive SIGINT (SIGTSTP) from the kernel
* when we type ctrl-c (ctrl-z) at the keyboard.
*/
void eval(char *cmdline)
{
/* $begin handout */
char *argv[MAXARGS]; /* argv for execve() */
int bg; /* should the job run in bg or fg? */
pid_t pid; /* process id */
sigset_t mask; /* signal mask */
/* Parse command line */
bg = parseline(cmdline, argv);
if (argv[0] == NULL)
return; /* ignore empty lines */
if (!builtin_cmd(argv)) {
/*
* This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP
* signals until we can add the job to the job list. This
* eliminates some nasty races between adding a job to the job
* list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.
*/
if (sigemptyset(&mask) < 0)
unix_error("sigemptyset error");
if (sigaddset(&mask, SIGCHLD))
unix_error("sigaddset error");
if (sigaddset(&mask, SIGINT))
unix_error("sigaddset error");
if (sigaddset(&mask, SIGTSTP))
unix_error("sigaddset error");
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
unix_error("sigprocmask error");
/* Create a child process */
if ((pid = fork()) < 0)
unix_error("fork error");
/*
* Child process
*/
if (pid == 0) {
/* Child unblocks signals */
sigprocmask(SIG_UNBLOCK, &mask, NULL);
/* Each new job must get a new process group ID
so that the kernel doesn't send ctrl-c and ctrl-z
signals to all of the shell's jobs */
if (setpgid(0, 0) < 0)
unix_error("setpgid error");
/* Now load and run the program in the new job */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
/*
* Parent process
*/
/* Parent adds the job, and then unblocks signals so that
the signals handlers can run again */
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
/* $end handout */
return;
}
builtin_cmd
要點分析:
呼叫listjobs時,屬于訪問全域變數,需要阻塞和解除阻塞,
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if(*argv == NULL)
{
return 0;
}
sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");
}
if(! strcmp(argv[0], "quit"))
{
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//訪問全域變數需要阻塞
{
unix_error("sigprocmask error!");
}
int i;
for(i = 0; i < MAXJOBS; i ++ )//退出時終止所有所有的子行程
{
if(jobs[i].pid)
{
kill(- jobs[i].pid, SIGINT);
}
}
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
exit(0);//Shell exit
}else if(! strcmp(argv[0], "jobs"))
{
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//同理,訪問全域變數
{
unix_error("sigprocmask error!");
}
listjobs(jobs);//遍歷
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
return 1;
}else if(! strcmp(argv[0], "&"))
{
return 1;// &也是內置命令,需要回傳1
}else if(! strcmp(argv[0], "fg") || ! strcmp(argv[0], "bg"))
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg
要點分析:
-
需要保證引數正確,即將不正確的情況排除,
-
區別jid和pid,
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
/* $begin handout */
struct job_t *jobp = NULL;
/* Ignore command if no argument */
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
/* Parse the required PID or %JID arg */
if (isdigit(argv[1][0])) {
pid_t pid = atoi(argv[1]);
if (!(jobp = getjobpid(jobs, pid))) {
printf("(%d): No such process\n", pid);
return;
}
}
else if (argv[1][0] == '%') {
int jid = atoi(&argv[1][1]);
if (!(jobp = getjobjid(jobs, jid))) {
printf("%s: No such job\n", argv[1]);
return;
}
}
else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
/* bg command */
if (!strcmp(argv[0], "bg")) {
if (kill(-(jobp->pid), SIGCONT) < 0)
unix_error("kill (bg) error");
jobp->state = BG;
printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
}
/* fg command */
else if (!strcmp(argv[0], "fg")) {
if (kill(-(jobp->pid), SIGCONT) < 0)
unix_error("kill (fg) error");
jobp->state = FG;
waitfg(jobp->pid);
}
else {
printf("do_bgfg: Internal error\n");
exit(0);
}
/* $end handout */
return;
}
waitfg
要點分析:
-
在等待的回圈不使用可能會無限休眠的pause,也不使用太慢的sleep,
-
在等待的回圈中使用sigsuspend函式,因為它是原子的,
-
在等待前,需阻塞chld信號,
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
sigset_t mask, prev_mask;
if(sigemptyset(&mask))
{
unix_error("sigempty error!");
}
if(sigaddset(&mask, SIGCHLD))
{
unix_error("sigaddset error!");
}
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//訪問jobs先阻塞chld信號
{
unix_error("sigprocmask error!");
}
while(fgpid(jobs) == pid)
{
sigsuspend(&prev_mask);//消除競爭
}
//
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
return;
}
sigchld_handler
要點分析:
-
洗掉作業資訊時,屬于訪問全域變數,需要阻塞全部信號,
-
保存恢復errno,
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig)
{
int olderrno = errno;
pid_t pid;
int status;
sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");
}
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//訪問全域變數前阻塞所有信號
{
unix_error("sigprocmask error!");
}
struct job_t *temp = getjobpid(jobs, pid);
if(WIFEXITED(status))//正常結束
{
deletejob(jobs, pid);
}else if(WIFSIGNALED(status))//被未捕獲的信號終止
{
int jid = pid2jid(pid);
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
deletejob(jobs, pid);
}else if(WIFSTOPPED(status))//停止的信號
{
temp->state = ST;
int jid = pid2jid(pid);
printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status));
}
fflush(stdout);//之前printf輸出,所以重繪
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
}
errno = olderrno;
return;
}
sigint_handler
/*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
*/
void sigint_handler(int sig)
{
int olderrno = errno;//保存和恢復errno
sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");//阻塞所有信號
}
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
{
unix_error("sigprocmask error!");
}
pid_t pid = fgpid(jobs);
if(pid != 0)//對行程組發送SIGINT
kill(-pid, sig);
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
errno = olderrno;
return;
}
sigtstp_handler
/*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
*/
void sigtstp_handler(int sig)
{
int olderrno = errno;
sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");//阻塞所有信號來訪問全域變數
}
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
{
unix_error("sigprocmask error!");
}
pid_t pid = fgpid(jobs);
if(pid != 0)//向行程組發送SIGTSTP
kill(-pid, sig);
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
errno = olderrno;
return;
}
測驗
對比tsh和參考shell程式tshref,測驗了16組例子,
本文來自博客園,作者:江水為竭,轉載請注明原文鏈接:https://www.cnblogs.com/Az1r/p/16951915.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/539215.html
標籤:其他
