前置要求
理解csapp异常处理整章内容,获取了实验资料并且具备实验环境。
实验内容
这个实验要求实现一个简单的shell,具备类似于linux系统上shell的功能,使用者能够在shell上运行自己的程序,使用shell提供的内置命令。
要求实现的shell文件名为tsh.c,在这个文件中大部分函数已经被写好,我们只需完成一部分函数,要实现的函数如下:
eval // 对输入进行处理
builtin_cmd// 完成内置命令
do_bgfg // 完成bg,fg内置命令
waitfg // 阻塞直到给定pid任务不在前台
sigchld_handler //捕获 SIGCHLD
sigint_handler // 捕获 SIGINT
sigtstp_handler // 捕获 SIGTSTP
最终目标为我们实现的tsh和实验文件中的tshref具有同样的输出。
程序结构
要实现这个实验,最重要的一点是理解整个shell的结构,分析代码可以发现,shell要维护一个job队列,这个队列保存当前运行在shell上的程序,我们的任务就是在处理用户输入的时候同时维护好这个队列。job的定义如下:
struct job_t { /* The job struct */
pid_t pid; /* job PID */
int jid; /* job ID [1, 2, ...] */
int state; /* UNDEF, BG, FG, or ST */
char cmdline[MAXLINE]; /* command line */
};
关于队列的操作我们不需要关心,实验文件中已经实现好了。我们要注意的是,这个队列是全局变量,一定要注意资源冲突问题。从c语言的角度来考虑就是在访问的时候要注意屏蔽信号,站在其他高级语言的角度容易发现几个handler同时对资源进行操作和多线程类似(不完全相同),其中一个线程在访问资源的时候要“加锁”。
eval
这个函数的实现相对简单,代码如下:
void eval(char *cmdline)
{
static char *argv[MAXARGS];
int run_in_bg = parseline(cmdline, argv);
if (argv[0] == NULL) {
return;
}
static sigset_t mask_all, prev_all;
sigemptyset(&mask_all);
sigaddset(&mask_all, SIGCHLD);
pid_t pid;
if (!builtin_cmd(argv)) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);/* Eliminate the race.*/
if ((pid = fork()) == 0) { /* Child runs user job.*/
setpgid(0, 0);
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* Parent */
int state;
if (run_in_bg) {
state = BG;
} else {
state = FG;
}
int jid = 0;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, NULL);/* Eliminate the race in jobs*/
addjob(jobs, pid, state, cmdline);
jid = pid2jid(pid);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (!run_in_bg) {
waitfg(pid);
} else {
printf("[%d] (%d) %s", jid, pid, cmdline);
}
}
return;
}
其中有几个需要注意的地方:
1.为了确保执行的顺序为“创建子进程->添加队列->删除队列”, 我们需要在创建子进程之前屏蔽SIGCHLD信号,并且在添加队列的时候屏蔽所有的信号。如果在创建子进程的时候就屏蔽所有信号会出现异常,具体原因碍于知识水平我也不清楚。
2.在创建子进程的时候需要重新设置子进程的process group pid(pgid).调用setpgid(0, 0), 将子进程的pid作为对应的pgid.
3.在操作完毕后将屏蔽信号还原。
4.cmdline自带换行符,输出时不需要再换行
builtin_cmd
int builtin_cmd(char **argv)
{
static sigset_t mask_all, prev_all;
sigfillset(&mask_all);
int i;
if (!strcmp(argv[0], "quit")) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
for (i = 0; i < MAXJOBS; i++) {
if (jobs[i].pid > 0) {
kill(jobs[i].pid, SIGKILL);/* Kill all child processes.*/
}
}
sigprocmask(SIG_SETMASK, &prev_all, NULL);
exit(0); /* Never reached.*/
}
else if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
do_bgfg(argv);
return 1;
} else if (!strcmp(argv[0], "jobs")) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
listjobs(jobs);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
return 1;
}
return 0; /* not a builtin command */
}
注意点:在访问队列的时候注意屏蔽信号。
do_bgfg
void do_bgfg(char **argv)
{
char *number = argv[1];
if (number == NULL) {
printf("%s command requires PID or \%jobid argument\n", argv[0]);
return;
}
if (!(number[0] == '%' || isdigit(number[0]))) {
printf("%s: argument must be a PID or \%jobid\n", argv[0]);
return;
}
int len = (int)(strlen(argv[1]));
int id = 0;
if (number[0] != '%') {
id = number[0] - '0';
}
for (int i = 1; i < len; i++) {
if (!isdigit(number[i]))
return;
else {
id = id * 10 + number[i] - '0';
}
}
struct job_t *job;
static sigset_t mask_all, prev_all;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
if (number[0] == '%') {
job = getjobjid(jobs, id);
} else {
job = getjobpid(jobs, id);
}
if (job == NULL) {
if (number[0] == '%')
printf("%s No such job\n", number);
else
printf("(%d) No such job\n", id);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
return;
}
pid_t pid = job->pid;
if (!strcmp(argv[0], "bg")) {
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
job->state = BG;
sigprocmask(SIG_SETMASK, &prev_all, NULL);
kill(-pid, SIGCONT);
} else {
job->state = FG;
sigprocmask(SIG_SETMASK, &prev_all, NULL);
kill(-pid, SIGCONT);
waitfg(pid);
}
return;
}
注意点
1.关于字符串的各种条件判断
2.jid和pid的区别处理。
3.使用kill发送信号的时候pid前加负号,表示向这组进程发信号。
4.处理FG命令后应调用waitfg等待。
waitfg
void waitfg(pid_t pid)
{
static sigset_t mask_all, prev_all;
sigfillset(&mask_all);
while (1) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t *job = getjobpid(jobs, pid);
int is_fg = ((job != NULL) && (job->state == FG));
if (!is_fg) {
sigprocmask(SIG_SETMASK, &prev_all, NULL);
break;
}
sigprocmask(SIG_SETMASK, &prev_all, NULL);
sleep(1);
}
return;
}
注意点:
1.访问队列前屏蔽信号
2.调用sleep之前要将信号解锁,而且这里不能使用pause(),因为pause()需要信号来将其唤醒,如果信号没来就会一直阻塞。在信号屏蔽解除后,pause()执行前,可能会出现我们所期待的SIGCHLD信号,但是这个信号被handler捕获后,pause就接收不到了。
sigchld_handler
void sigchld_handler(int sig)
{
int olderrno = errno;
static sigset_t mask_all, prev_all;
sigfillset(&mask_all);
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
int jid = pid2jid(pid);
if (!WIFSTOPPED(status)) {
if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
printf("Job [%d] (%d) terminated by signal %d\n"
, jid, pid, sig
);
}
deletejob(jobs, pid);
} else {
printf("Job [%d] (%d) stopped by signal %d\n"
, jid, pid, SIGTSTP
);
getjobpid(jobs, pid)->state = ST;
}
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
errno = olderrno;
return;
}
注意点:
1.按理来说在handler执行前都需要屏蔽对应的信号,但是这一点我们在代码中不需要考虑。因为在main函数中程序已经调用了Signal函数对这个功能进行了跨平台实现(potable).
2.调用waitpid的时候,我们希望同时对终止和暂时停止的程序做出非阻塞反应,所以要使用参数WHOHANG | WUNTRACED.
3.调用WIFSTOPPED宏判断是终止还是暂停,调用WIFSIGNALED判断是正常结束还是由于信号结束,调用WTERMSIG获取对应的信号.
4.按照csapp上的内容,在signal handler中不应该使用异步不安全的函数,而printf恰好就是。这点在重要的项目中一定要注意,但是在这里几乎不会有影响(16个test全部能过),而且用write写起来也很麻烦,所以我不打算改。
5.如果使用异步安全函数,要保存errno变量,我在代码中写了。
sigint_handler
void sigint_handler(int sig)
{
int olderrno = errno;
static sigset_t mask_all, prev_all;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (pid != 0) {
kill(-pid, SIGINT);
}
errno = olderrno;
return;
}
注意点:屏蔽信号
sigtstp_handler
void sigtstp_handler(int sig)
{
int olderrno = errno;
static sigset_t mask_all, prev_all;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs);
if (pid != 0) {
kill(-pid, sig);
}
sigprocmask(SIG_SETMASK, &prev_all, NULL);
waitfg(pid);
errno = olderrno;
return;
}
注意点:
1.屏蔽信号
2.执行顺序应为“sigtstp_handler处理暂停信号->sigchld_handler处理信号->下一条命令”,所以一定要调用waitfg进行等待,避免提前处理下一条命令。