CSAPP - Shell Lab
写一个模拟Unix Shell的Tiny Shell程序
包括内置命令的处理、信号处理、作业管理的技术
一些用于字符串处理的C函数
char *strchr( const char *str, int ch );
返回ch在str中第一次出现的位置,如果没有,返回NULL
注意在原始程序中注册了SIGINT信号的处理,需要实现,否则用ctrl+c停止不了(只能用kill -9)
信号
- SIGCONT:继续进程如果该进程处于停止状态
- SIGTSTP:来自终端的停止信号
- SIGINT:来自键盘的中断(ctrl+c)
内核为每个进程再pending位向量中维护着待处理信号的集合,blocked位向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中第k位,而只要接受了一个类型为k的信号,内核就会清楚pending中的第k位。
当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号的集合(pending & ~blocked)。如果该集合非空,内核会选择集合中的某个信号k(通常是最小的k),并且强制p接受信号k。
进程同步
由于tsh中的子进程和父进程是并发执行的,每次增加一个后台作业,需要addjob,为了防止addjob在deletejob之后运行,需要通过设置blocked位向量对信号的接收进行同步(sigprocmask)。
利用显式的阻塞信号(SIGCHLD),实现原子操作(增减任务列表)
ctrl+c杀死前台进程
为了让SIGINT信号只送到前台进程(组),需要在fork后设置子进程的进程组ID,用setpgid(0, 0),使用当前进程(fork出来的子进程)的pid作为进程组ID;之后就能用 kill(-pid, SIGINT) 发送给进程组|-pid|(pid的绝对值)中的每个进程。
waitpid行为
pid_t waitpid(pid_t pid, int *statusp, int options); // 通过options修改默认行为
默认行为:挂起调用进程,直到有子进程终止;
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。
前台进程和后台进程
shell需要等待前台进程的结束(调用waitfg()),而后台子进程被创建后父进程直接返回。
一些坑
在waitfg函数中,最好使用忙循环(busy loop)来检查是否jobs中还有需要等待的前台进程,由于在SIGCHLD信号的处理函数中已经调用了waitpid处理,不能再在父进程的waitfg函数中调用waitpid了否则会:
➜ shlab-handout git:(master) ✗ ./tsh
tsh> ./myspin 3
waitfg: waitpid error: No child processes
下面展示最终实现的函数,在写的时候可以参考tshref.out文件的输出。
eval
负责创建子进程,增加jobs列表,解析命令行
void eval(char *cmdline)
{
char *argv[MAXARGS];
int bg;
pid_t pid = 0; // process id
sigset_t mask_all, mask_one, prev_one;
sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
bg = parseline(cmdline, argv);
if (argv[0] == NULL) {
return; // ignore the empty lines
}
if (!builtin_cmd(argv)) {
sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // Block SIGCHLD
if ((pid = fork()) == 0) { // child runs user job
setpgid(0, 0); // 使用当前进程(fork出来的子进程)的pid作为进程组ID
sigprocmask(SIG_SETMASK, &prev_one, NULL); // Unblock SIGCHLD 子进程会继承父进程的屏蔽信号集,所以取消子进程对SIGCHLD的屏蔽
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
addjob(jobs, pid, bg == 1 ? BG : FG, cmdline); // 成功地加入job后,再开始监听SIGCHLD信号
sigprocmask(SIG_SETMASK, &prev_one, NULL); // Unblock SIGCHLD
if (!bg) { // parent wait for foreground job
waitfg(pid);
} else {
printf("[%d] (%d) %s", maxjid(jobs), pid, cmdline);
}
}
}
buildin_cmd
对于tsh的内置命令,直接执行
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "jobs")) {
// listing the running and stopping background jobs
Dprint("Listing jobs...\n");
listjobs(jobs);
return 1;
}
if (!strcmp(argv[0], "quit")) {
exit(0);
}
if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
do_bgfg(argv);
return 1;
}
if (!strcmp(argv[0], "kill")) {
// Terminate a job
pid_t pid = 0;
int jid = 0;
if (argv[1][0] == '%') {
jid = atoi(argv[1]+1);
} else {
pid = atoi(argv[1]);
jid = pid2jid(pid);
}
Dprintf("kill the job:pid = %d, jid = %d\n", pid, jid);
struct job_t *job = getjobjid(jobs, jid);
if (!job)
return 1;
kill(job->pid, SIGKILL);
deletejob(jobs, pid);
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg
使得暂停的进程继续执行,如果是指定为fg,tsh等待这个任务的执行
void do_bgfg(char **argv)
{
pid_t pid = 0;
int jid = 0;
int input_is_jid = 0;
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (argv[1][0] == '%') {
for (int i = 1; i < strlen(argv[1]); ++i) {
if (!isdigit(argv[1][i])) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
}
jid = atoi(argv[1]+1);
input_is_jid = 1;
} else {
for (int i = 0; i < strlen(argv[1]); ++i) {
if (!isdigit(argv[1][i])) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
}
pid = atoi(argv[1]);
jid = pid2jid(pid);
}
struct job_t* job = NULL;
if (!(job = getjobjid(jobs, jid))) {
if (input_is_jid) {
printf("%s: No such job\n", argv[1]);
} else {
printf("(%s): No such process\n", argv[1]);
}
return;
}
Dprintf("Continue: pid = %d, jid = %d\n", job->pid, job->jid);
// 发送信号使得进程继续运行
kill(-job->pid, SIGCONT);
if (!strcmp(argv[0], "fg")) {
// change a stopped or running background job to a running in the foreground
job->state = FG;
waitfg(job->pid);
} else { // bg
// Change a stopped background job to a running background job.
job->state = BG;
printf("[%d] (%d) %s", jid, pid, job->cmdline);
}
}
waitfg
tsh等待前台进程的终止或停止
void waitfg(pid_t pid)
{
// int olderrno = errno;
// int status;
// if (waitpid(pid, &status, WUNTRACED) < 0) {
// unix_error("waitfg: waitpid error");
// }
//
// if (errno != ECHILD)
// unix_error("waitpid error");
// errno = olderrno;
while( pid == fgpid(jobs) ) {
sleep(1);
}
}
三个信号的处理
sigchld_handler
void sigchld_handler(int sig)
{
// int olderrno = errno;
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WUNTRACED|WNOHANG)) > 0) { // 解决信号不排队的问题(即多个子进程同时结束),不等待后台进程
if (WIFEXITED(status)) {
// 子进程正常终止
deletejob(jobs, pid);
Dprint("Handler reaped child\n");
} else if (WIFSIGNALED(status)) {
// 子进程被信号终止
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
} else if (WIFSTOPPED(status)) {
// 子进程停止
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
struct job_t* job = getjobpid(jobs, pid);
if (job) {
job->state = ST;
}
return;
} else {
Dprintf("child %d terminated abnormally\n", pid);
}
}
// if (errno != ECHILD)
// unix_error("waitpid error");
// errno = olderrno;
}
sigint_handler
void sigint_handler(int sig)
{
Dprint("ctrl-c pressed.\n");
pid_t fg = fgpid(jobs);
if (fg == 0) {
Dprint("No fg process.\n");
return;
} else {
kill(-fg, sig); // pid = -fg, 发送给进程组|pid|(pid的绝对值)中的每个进程
}
}
sigtstp_handler
void sigtstp_handler(int sig)
{
Dprint("ctrl-z pressed.\n");
pid_t fg = fgpid(jobs);
if (fg == 0 ) {
return;
} else {
kill(-fg, sig);
struct job_t* job = getjobpid(jobs, fg);
job->state = ST;
}
}