TinyShell(CSAPP實驗)

2022-12-05 15:00:42

簡介

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組例子。