-
-
[原创]【深入理解计算机系统】 手搓一个tiny shell
-
发表于: 2025-8-2 18:01 446
-
前言
此lab要求实现一个带作业控制的tiny shell,涉及到并发。推荐读者完整阅读过一次CSAPP第八章的内容再尝试此lab。
取得lab
利用命令tar xvf shlab-handout.tar解压文件。
以下所有代码皆在tsh.c中编写。
实验要求
- 用户输入的命令行包含一个
name及零或多个参数,参数由单或多个空格分隔。若name是一个内置命令,则tsh(tiny shell的简称)应立即响应并等待下条命令。否则,应将name视为可执行文件的路径,tsh应将其加载并运行在子进程上下文中。 - 输入
ctrl+c(ctrl+z)会发送一个SIGINT(SIGTSTP)到前台作业以及其子进程中。若没有任何前台作业,则信号应不产生任何效果(不能影响tsh)。 - 若命令行末尾包含
&,则将命令运行在后台,否则运行在前台。 - 每个作业都可以用PID或JID识别。JID应用前辍
%标示,PID则不用任何前辍。 - tsh应支持以下内置命令:
quit:退出tsh。jobs:列出所有作业。bg <job>:传送给job(可用PID或JID指定)一个SIGCONT信号,并将该作业运行在后台。fg <job>:传送给job(可用PID或JID指定)一个SIGCONT信号,并将该作业运行在前台。
- tsh应回收所有僵死(尸)进程。
开始编写
以下是我们需要实现的函数:
eval:解析命令行。built_cmd:运行内置命令。do_bgfg:实现bg和fg指令。waitfg:挂起tsh并等待前台作业。sigchld_handler:处理SIGCHLD信号。sigint_handler:处理SIGINT信号。sigtstp_handler:处理SIGTSTP信号。
错误处理包装函数
为了避免每次调用系统函数前手动编写错误处理代码,我们使用包装函数替代。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | /* Sigprogmask - Wrapper for sigprocmask funciton */int Sigprocmask(int how, sigset_t *set, sigset_t *oldset){ int success; if ( (success = sigprocmask(how, set, oldset)) < 0 ) unix_error("sigprocmask error"); return success;}/* Fork - Wrapper for sigprocmask function */int Fork(){ pid_t pid; if ( (pid = fork()) < 0 ) unix_error("fork error"); return pid;}/* Execve - Wrapper for execve function */int Execve(const char *filename, const char **argv, const char *envp[]){ int success; if ( (success = execve(filename, argv, envp)) < 0) { printf("%s: Command not found\n", filename); exit(0); } return success;}/* Sigemptyset - Wrapper for sigemptyset function */int Sigemptyset(sigset_t *set){ int success; if ( (success = sigemptyset(set)) < 0) unix_error("sigemptyset error"); return success;}/* Sigfillset - Wrapper for sigfillset function */int Sigfillset(sigset_t *set){ int success; if ( (success = sigfillset(set)) < 0) unix_error("sigfillset error"); return success;}/* Sigaddset - Wrapper for sigaddset function */int Sigaddset(sigset_t *set, int signum){ int success; if ( (success = sigaddset(set, signum)) < 0) unix_error("sigaddset error"); return success;}/* Kill - Wrapper for kill function */int Kill(pid_t pid, int sig){ int success; if ( (success = kill(pid, sig)) < 0 && errno != ESRCH) unix_error("kill error"); return success;} |
eval
通过阅读提供的helper函数,我们可以知道parseline在解析命令行的同时,会将该命令是否指定运行在后台作为返回值传回。我们将返回值存放在bg中。
然后调用builtin_cmd(等下编写)确认是否为内置命令。
若不是内置命令,我们需要fork一个子进程出来,然后利用execve运行程序在其上下文中。注意,执行fork前需要先阻塞SIGCHLD信号,直到执行完addjob才恢复。避免因为竞争导致添加了无法删除的作业。想像这种流程,在子进程fork出来后直接运行,并在控制流传递给父进程前执行完毕并发送SIGCHLD信号。此时父进程收到信号并回收子进程,然后再调用addjob添加一个已经被回收的作业,导致该作业永远不会被删除。为了避免该状况,我们需要阻塞信号。
还需要注意的是,为了避免传信号给子进程时也将信号传给tsh,我们使用setpgid(0, 0)创建一个新进程组并将子进程添加进去。
最后,如果是前台进程则调用waitfg挂起tsh直到前台作业返回,否则不挂起tsh在后台运行作业。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /* * 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) { char *argv[MAXARGS]; int bg = parseline(cmdline, argv); /* background job? */ int builtin = builtin_cmd(argv); /* is built-in command? */ sigset_t mask_one, mask_all, prev; Sigemptyset(&mask_one); Sigaddset(&mask_one, SIGCHLD); Sigfillset(&mask_all); if (!builtin) { Sigprocmask(SIG_BLOCK, &mask_one, &prev); /* block SIGCHLD */ pid_t pid; if ( (pid = Fork()) == 0 ) { /* child process */ Sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock SIGCHLD */ setpgid(0, 0); Execve(argv[0], argv, environ); } Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* block all signals for atomic operation */ if (bg) { addjob(jobs, pid, BG, cmdline); struct job_t *j = getjobpid(jobs, pid); printf("[%d] (%d) %s", j->jid, j->pid, j->cmdline); } else { addjob(jobs, pid, FG, cmdline); waitfg(pid); } Sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock SIGCHLD */ } return;} |
builtin_cmd
直接上代码,简单直接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /* * builtin_cmd - If the user has typed a built-in command then execute * it immediately. */int builtin_cmd(char **argv) { if (argv[0] == NULL) return 1; int builtin = 0; char *cmdlist[4] = {"quit", "jobs", "bg", "fg"}; int idx = -1; int i; for (i = 0; i < 4; i++) { if (strcmp(argv[0], cmdlist[i]) == 0) { idx = i; builtin = 1; } } switch (idx) { case 0: exit(0); case 1: listjobs(jobs); break; case 2: do_bgfg(argv); break; case 3: do_bgfg(argv); break; default: break; } return builtin;} |
do_fgbg
需要注意的是,记得取得作业后,依对应命令修改作业的state。以及使用kill传送信号时,应传入-j->pid,带负号-表示将信号传送到|pid|所属的进程组中的所有进程中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /* * do_bgfg - Execute the builtin bg and fg commands */void do_bgfg(char **argv) { if (argv[1] == NULL) { printf("%s command requires PID or %%jobid argument\n", argv[0]); return; } if (isalpha(*argv[1])) { printf("%s: argument must be a PID or %%jobid\n", argv[0]); return; } /* get the job who needs to continue */ struct job_t *j; char *delim = strchr(argv[1], '%'); if (delim == NULL) { /* use PID */ pid_t pid = atoi(argv[1]); j = getjobpid(jobs, pid); if (j == NULL) { printf("(%d): No such process\n", pid); return; } } else { /* use JID */ int jid = atoi(++argv[1]); j = getjobjid(jobs, jid); if (j == NULL) { printf("%%%s: No such job\n", argv[1]); return; } } if (strcmp(argv[0], "bg") == 0) { /* continue in background */ j->state = BG; printf("[%d] (%d) %s", j->jid, j->pid, j->cmdline); Kill(-j->pid, SIGCONT); } else { /* continue in foreground */ j->state = FG; Kill(-j->pid, SIGCONT); waitfg(j->pid); } return;} |
waitfg
我们使用sigsuspend(&mask)挂起进程,其行为等同于以下代码的原子(不可中断)版本,解决了无限挂起的问题:
1 2 3 4 | sigprocmask(SIG_SETMASK, &mask, &prev);/* 若此时收到信号,可能导致进程被无限挂起 */pause();sigprocmask(SIG_SETMASK, &prev, NULL); |
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* * waitfg - Block until process pid is no longer the foreground process */void waitfg(pid_t pid){ sigset_t mask; Sigemptyset(&mask); while (fgpid(jobs) == pid) sigsuspend(&mask); return;} |
sigchld_handler
需要注意,我们需要使用while而不是if来回收。这是因为必须考虑信号不会排队的问题,若有两个进程同时发送SIGCHLD信号,则只会被视为一个。如果使用if回收,则无法收回所有僵死进程。
还需要注意的是,处理函数可能会改变errno的值,所以需要在进入函数时保存旧值,并在退出时恢复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /* * 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; sigset_t mask, prev; Sigfillset(&mask); pid_t pid; int status; while ( (pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { Sigprocmask(SIG_BLOCK, &mask, &prev); /* block all */ if (WIFEXITED(status)) /* job exit */ deletejob(jobs, pid); if (WIFSIGNALED(status)) { /* job interrupt */ printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status)); deletejob(jobs, pid); } if (WIFSTOPPED(status)) { /* job suspend */ struct job_t *j = getjobpid(jobs, pid); if (j) { j->state = ST; printf("Job [%d] (%d) stopped by signal %d\n", j->jid, pid, WSTOPSIG(status)); } else printf("cannot find a job that corresponding to pid: %d", pid); } Sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock all */ } /* waitpid sets errno to ECHILD if there's no more zombie process */ /* be careful: we have to ignore EINTR too */ if (errno != ECHILD && errno != EINTR) unix_error("waitpid error"); errno = olderrno; return;} |
sigint_handler
不难,看看注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /* * 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; pid_t pid = fgpid(jobs); /* we don't need to response when there's no foreground job */ if (pid == 0) { errno = olderrno; return; } sigset_t mask, prev; Sigfillset(&mask); Sigprocmask(SIG_BLOCK, &mask, &prev); /* block all signals */ /* send signal to all the process in the same group to pid */ Kill(-pid, sig); Sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock all signals */ errno = olderrno; return;} |
sigtstp_handler
与sigint_handler差不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /* * 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; pid_t pid = fgpid(jobs); if (pid == 0) { errno = olderrno; return; } sigset_t mask, prev; Sigfillset(&mask); Sigprocmask(SIG_BLOCK, &mask, &prev); /* block all signals */ Kill(-pid, sig); Sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock all signals */ errno = olderrno; return;} |
测试
使用make编译程序,然后使用make test01,make test02......测试程序。
可以使用make rtest01运行标准解答。
对比两份输出可以知道程序是否运行正确。
后记
这个lab是写的最顺的一个,因为遇到问题实在找不到bug就直接去找chatgpt,不像之前几个lab要动脑子不敢问gpt,怕他一不小心就把答案给出来。
关于各种sig_handler书中都有大差不差的代码例子,创建子进程的逻辑书中也写得很清楚,写起来很顺。
因为我是第一次写并发的代码,可能有地方写错或没考虑清楚,如果各位师傅发现任何错误欢迎指正。