TinyShell(CSAPP實驗)

江水為竭發表於2022-12-05

簡介

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:實現內建命令bgfg

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-cctrl-z)應該導致SIGINTSIGTSTP)訊號被髮送到當前的前臺作業,及其該作業的子孫作業(例如,它建立的任何子程式)。如果沒有前臺工作,那麼訊號應該沒有效果。

  • 如果命令列以&結尾,則tsh在後臺執行該作業;否則,在前臺執行該作業

  • 可以用程式ID(PID)tsh賦予的正整數作業IDjob IDJID)標識一個作業。JID用字首%,例如%5標識作業ID5的作業,5表示PID5的作業。

  • 已經提供了處理作業列表所需的所有函式

  • 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中有如下選項:

  1. WNOHANG:若當前沒有等待集合中的子程式終止,則立即返回0
  2. WUNTRACED:等待直到某個等待集合中的子程式停止或返回,並返回這個子程式的pid。
  3. WCONTINUED:等待直到某個等待集合中的子程式重新開始執行或終止。
  4. 組合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號訊號)

安全的訊號處理

目的

讓訊號處理程式和主程式它們可以安全地,無錯誤地,按照我們預期地併發地執行。

方法

  1. 處理程式儘可能簡單。

  2. 在處理程式只呼叫非同步訊號安全的函式。

    1. 可重入的(只訪問區域性變數)。
    2. 不能被訊號處理程式中斷。
  3. 儲存和恢復errno。避免干擾其他依賴於errno的部分。解決方法是用區域性變數儲存,再恢復。

void Example(int sig) 
{
	int olderrno = errno;
    /*
    this is your code
    */
    errno = olderrno;
}
  1. 阻塞所有訊號,保護對共享全域性變數資料結構的訪問。
  2. volatile宣告全域性變數。
  3. 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.

注意

  1. 不可以用訊號來對其他進位制中發生的事件計數。
  2. 使用原子(atomic)函式如sigsuspend函式消除潛在的競爭並提高效率。

實驗

eval

要點分析:

  1. 建立子程式前需要阻塞訊號,防止競爭。

  2. 將子程式加入到jobs後,需要恢復,即解除阻塞。

  3. 建立子程式時,為子程式建立一個新的程式組。

/* 
 * 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

要點分析:

  1. 需要保證引數正確,即將不正確的情況排除。

  2. 區別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

要點分析:

  1. 在等待的迴圈不使用可能會無限休眠的pause,也不使用太慢的sleep。

  2. 在等待的迴圈中使用sigsuspend函式,因為它是原子的。

  3. 在等待前,需阻塞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

要點分析:

  1. 刪除作業資訊時,屬於訪問全域性變數,需要阻塞全部訊號。

  2. 儲存恢復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組例子。