簡介 CSAPP實驗介紹 學生實現他們自己的帶有作業控制的Unix Shell程式,包括Ctrl + C和Ctrl + Z按鍵,fg,bg,和 jobs命令。這是學生第一次接觸併發,並且讓他們對Unix的進程式控制制、信號和信號處理有清晰的瞭解。 什麼是Shell? Shell就是用戶與操作系統內核 ...
簡介
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