Prerequisites Signal Signal是一种内核与进程之间交互的信息。诸如熟悉的Ctrl+C、Ctrl+Z等终止程序或休眠程序的实质都是发送相应的signal。Signal是异常控制流的其中一种,可以在本地异步运行。
贴一些可能见得多的signal:
SIGABRT
: abort
SIGFPE
: floating point exception
SIGINT
: interrupt (when pressing Ctrl+C
)
SIGSEGV
: segment violation (a.k.a. segment fault)
SIGTSTP
: stopped (when pressing Ctrl+Z
)
SIGCHLD
: Terminated or stopped child
Relative C Functions 在命令行里面,如果要给一个进程(或进程组)添加信号,可以用kill
。kill
并不只是用来杀进程的,实际上是添加信号。
在C里面也有一个叫kill
的函数,需要指定pid和信号,用法大体类似。这就是我们可以用C写shell的原因之一。
kill
可以作用于一个进程上,也可以作用于一整个进程组上,具体区别在pid的值:
当pid为正,则作用于单个进程
当pid为负,则作用于这个进程所在的进程组
相信见过像Vim等的一些按Ctrl+C退不出的程序,之所以退不出不是因为SIGINT
无法发出,而是因为signal的handler是可以由我们来自定义的。
在C里面,我们可以通过调用signal
函数,install一个signal的handler,覆盖原有的默认handler。signal大多数都能覆盖,除了SIGKILL
跟SIGSTOP
等。
signal handler需要自己去声明定义一个函数,可以形象地理解成比较底层的try-catch,catch相应的signal,执行handler里面的内容。
如果想要写shell,相信需要在程序里面创建进程,执行命令等等。相应的C函数也介绍一下:
int fork()
: create a new process (child process). called once, return twice. return 0 in child process, return pid in parent process.
int execve(argv[0], argv, environ)
: load and run another program. called once, never return(if sucessfully executed).
void exit(int status)
: terminate the current process with status. Then it becomes a zombie process, waiting to be reaped by its parent.
如果我们fork了一次,父子两个进程的执行速度是无法确定的,有时儿子快有时儿子慢,这就导致了一次运行的结果可能不同。形象地,我们可以理解成在这种无约束的异步进程中,进程之间会相互比赛,竞争运行速度(卷起来了),我们称这种现象为race。
如果不想出现这种情况,需要显式地叫其中一个进程去等其他的进程,这种操作可以用waitpid
来完成。
signal可以被屏蔽,即可以选择block掉指定类型的signal,以确保race不会在其中发生。分析可能发生的race是最复杂的部分。
Lab Introduction 在这个lab里面我们需要实现一个小型的shell,名叫tsh(tiny shell),因为不用实现现代shell的功能,所以功能确实很朴素。
实验需要我们写的只有tsh.c
一个文件,需要你定义若干个已经声明的函数。
在tsh.c
中也温馨地提供了几个helper functions。
用户运行了shell之后,会在stdin
等待用户的输入。用户的输入以空格为分隔,第一个称为name,后面则为参数。
如果name是built-in command,则直接在前台运行,运行完直接等待下一个输入。
如果name是一个binary executable,则新开一个进程(或者说是add a new job),在子进程中执行命令。
命令有前台后台运行之分,如果最后带有一个&
,则为后台运行,此时shell不用去等待job运行完毕,可以继续执行shell的功能。前台等待job完成的内容需要在waitfg
函数之中完成。
这里有一个细节,因为我们的shell是一个程序,而默认我们创建的job会跟shell是同一个进程组,那么如果子进程被杀掉的话shell也会被杀掉,这不符合shell的性质,所以我们需要保证创建的子进程都跟shell自己处于不同的进程组。这部分是通过setpgid(0, 0)
来完成的。
在子进程执行完成之后,子进程会变成一个zombie,不再会被执行,但会一直占着地方,它们需要被shell回收。相关的信号是SIGCHLD
。这部分的工作是通过定义SIGCHLD
的handler来完成的。
pid跟jid的管理我们不用操心,已经写好了,我们只需要调用addjob
跟deletejob
这样的API来维护进程。
eval
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 void eval (char *cmdline) { char *argv[MAXARGS]; int bg = parseline(cmdline, argv); if (argv[0 ] == NULL ) return ; int builtin = builtin_cmd(argv); if (builtin) return ; sigset_t mask, prev_mask, mask_all; sigfillset(&mask_all); sigemptyset(&mask); sigaddset(&mask, SIGCHLD); sigprocmask(SIG_BLOCK, &mask, &prev_mask); pid_t pid = fork(); if (pid == 0 ) { sigprocmask(SIG_SETMASK, &prev_mask, NULL ); setpgid(0 , 0 ); if (execve(argv[0 ], argv, environ) < 0 ) { printf ("%s: Command not found\n" , argv[0 ]); exit (0 ); } } else { sigprocmask(SIG_BLOCK, &mask_all, NULL ); addjob(jobs, pid, bg + 1 , cmdline); if (bg) { printf ("[%d] (%d) %s" , pid2jid(pid), pid, cmdline); } else { waitfg(pid); } sigprocmask(SIG_SETMASK, &prev_mask, NULL ); } }
builtin_cmd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int builtin_cmd (char **argv) { if (!strcmp (argv[0 ], "quit" )) { exit (0 ); } else if (!strcmp (argv[0 ], "jobs" )) { listjobs(jobs); return 1 ; } else if (!strcmp (argv[0 ], "bg" ) || !strcmp (argv[0 ], "fg" )) { do_bgfg(argv); return 1 ; } else if (!strcmp (argv[0 ], "&" )) { return 1 ; } else { return 0 ; } }
do_bgfg
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 void do_bgfg (char **argv) { if (argv[1 ] == NULL ) { printf ("%s command requires PID or %%jobid argument\n" , argv[0 ]); return ; } struct job_t *job = NULL ; if (argv[1 ][0 ] == '%' ) { int jid = atoi(argv[1 ] + 1 ); job = getjobjid(jobs, jid); if (job == 0 ) { printf ("%s: No such job\n" , argv[1 ]); return ; } } else if (isdigit (argv[1 ][0 ])) { int pid = atoi(argv[1 ]); job = getjobpid(jobs, pid); if (job == 0 ) { printf ("(%s): No such process\n" , argv[1 ]); return ; } } else { printf ("%s: argument must be a PID or %%jobid\n" , argv[0 ]); return ; } kill(-(job->pid), SIGCONT); if (!strcmp (argv[0 ], "bg" )) { job->state = BG; printf ("[%d] (%d) %s" , job->jid, job->pid, job->cmdline); } else { job->state = FG; waitfg(job->pid); } }
waitfg
1 2 3 4 5 6 7 8 9 10 11 void waitfg (pid_t pid) { sigset_t mask; sigemptyset(&mask); while (fgpid(jobs) > 0 ) { sigsuspend(&mask); } sigprocmask(SIG_SETMASK, &mask, NULL ); }
signal handlers 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 void sigchld_handler (int sig) { int prev_errno = errno; sigset_t mask, prev; sigfillset(&mask); pid_t pid; int status; while ((pid = waitpid(-1 , &status, WNOHANG | WUNTRACED)) > 0 ) { if (WIFEXITED(status)) { sigprocmask(SIG_BLOCK, &mask, &prev); deletejob(jobs, pid); sigprocmask(SIG_SETMASK, &prev, NULL ); } else if (WIFSIGNALED(status)) { struct job_t *job = getjobpid(jobs, pid); sigprocmask(SIG_BLOCK, &mask, &prev); printf ("Job [%d] (%d) Terminated by signal %d\n" , job->jid, job->pid, WTERMSIG(status)); deletejob(jobs, pid); sigprocmask(SIG_SETMASK, &prev, NULL ); } else { struct job_t *job = getjobpid(jobs, pid); sigprocmask(SIG_BLOCK, &mask, &prev); job->state = ST; printf ("Job [%d] (%d) Stopped by signal %d\n" , job->jid, job->pid, WSTOPSIG(status)); sigprocmask(SIG_SETMASK, &prev, NULL ); } } errno = prev_errno; } void sigint_handler (int sig) { int prev_errno = errno; pid_t pid = fgpid(jobs); if (pid > 0 ) { kill(-pid, sig); } errno = prev_errno; } void sigtstp_handler (int sig) { int prev_errno = errno; pid_t pid = fgpid(jobs); if (pid > 0 ) { kill(-pid, sig); } errno = prev_errno; }
Result 最后实现的就是可以一个可以跑绝对路径的shell,支持前后台运行以及信号级别的进程交互。
最后发现tshref
也连vim都打不开,我就平衡了(