实验内容
实现一个简易的shell,包含以下功能:
- 通过shell运行程序,可选前台运行或后台运行
- 实现:jobs(打印所有前台和后台的任务信息),bg(将某一任务调整为后台运行),fg(将某一任务调整为前台运行),quit(退出shell)
- 接收ctrl + c和ctrl + z发出的信号,并传给前台进程
整个shell的框架已经搭好,需要补齐核心的函数即可,实现时需要用到信号和进程控制。
代码
首先对一些函数进行错误处理包装,函数声明如下:
pid_t Fork();
void Kill(pid_t pid, int sig);
void Sigfillset(sigset_t* set);
void Sigemptyset(sigset_t* set);
void Sigaddset(sigset_t* set, int sig);
void Sigprocmask(int how, sigset_t* mask, sigset_t* prev);
eval
主要对命令行解析,遇到内置命令则立即执行,否则则创建子进程运行指定的程序。
void eval(char *cmdline)
{
char* argv[MAXARGS];
/* paresline为提供的解析函数 */
int bg = parseline(cmdline, argv);
if (argv[0] == NULL) {
return;
}
/* 准备在访问全局变量时信号阻塞 */
sigset_t mask_one, mask_all, prev_one;
Sigemptyset(&mask_one);
Sigaddset(&mask_one, SIGCHLD);
Sigfillset(&mask_all);
/* 若不是内置命令 */
if (!builtin_cmd(argv)) {
/* 判断路径是否有制定的文件 */
if (access(argv[0], F_OK) < 0) {
printf("%s: Command not found\n", argv[0]);
return;
}
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
pid_t pid;
/* child */
if ((pid = Fork()) == 0) {
/* 子进程要立刻解除阻塞 */
Sigprocmask(SIG_SETMASK, &prev_one, NULL);
execve(argv[0], argv, environ);
}
/* 每个子进程单独一个进程组 */
setpgid(pid, pid);
Sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, (bg == 0 ? FG : BG), cmdline);
if (bg == 1) {
printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);
}
Sigprocmask(SIG_SETMASK, &prev_one, NULL);
/* 若为前台任务则waitfg中会等待任务执行 */
waitfg(pid);
}
return;
}
waitfg
对于pid进程,如果在jobs中是前台任务,则等待其停止执行(可能执行完,也可能被信号终止或停止)。
void waitfg(pid_t pid) {
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t* job = getjobpid(jobs, pid);
while (job->pid == pid && job->state == FG) {
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
sleep(1);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
sigint_handler
处理父进程收到的SIGINT信号,需要将该信号发送给前台任务进程。
void sigint_handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
/* 找出前台任务的pid */
pid_t pid = fgpid(jobs);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (pid > 0) {
/* -pid表示发给pid所在的进程组(实验要求发给整个进程组) */
Kill(-pid, SIGINT);
}
errno = olderrno;
}
sigstp_handler
处理父进程收到的SIGTSTP信号,需要将该信号发送给前台任务进程,实现与上相同。
sigchld_handler
当父进程shell收到子进程发出的SIGCHLD后需要对之进行处理,在回收进程时,需要根据status对不同状态的进程进行不同的处理。
void sigchld_handler(int sig) {
int olderrno = errno;
pid_t pid;
int status;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
/* 回收子进程 */
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
struct job_t* job = getjobpid(jobs, pid);
if (WIFEXITED(status)) {
/* 通过exit或return正常结束的进程 */
deletejob(jobs, pid);
} else if (WIFSIGNALED(status)) {
/* 被信号终止的进程 */
printf("Job (%d) terminated by siganl %d\n", pid, WTERMSIG(status));
deletejob(jobs, pid);
} else if (WIFSTOPPED(status)) {
/* 被信号停止的进程 */
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(status));
job->state = ST;
} else if (WIFCONTINUED(status)) {
/* ... */
}
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno;
}
builtin_cmd
主要为对内置命令的处理,分为quit,jobs,fg和bg,如果是内置命令,则返回0,否则返回1。
int builtin_cmd(char **argv)
{
if (!strcmp(argv[0], "quit")) {
exit(1);
}
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
}
if ((!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg"))) {
if (argv[1] != NULL) {
do_bgfg(argv);
} else {
printf("%s ", argv[0]);
printf("command requires PID or %%jobid argument\n");
}
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg
执行bg或者fg命令,向对应的进程发送一个SIGCONT信号即可,需要注意的错误处理,判断给出的命令是否正确。
void do_bgfg(char **argv) {
char* buf = argv[1];
int err = 0;
struct job_t* job;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
/* 判断给出的是%jid还是pid */
if (buf[0] == '%') {
int jid;
if ((jid = atoi(++buf)) > 0) {
job = getjobjid(jobs, jid);
} else {
err = 1;
}
} else {
int pid;
if ((pid = atoi(buf)) > 0) {
job = getjobpid(jobs, pid);
} else {
err = 1;
}
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
/* 给出的id有非数字字符 */
if (err == 1) {
printf("%s: ",argv[0]);
printf("argument must be a PID or %%jobid\n");
return;
}
/* 给出的id合法,但是并不在jobs中 */
if (job == NULL) {
printf("(%s): No such job\n", argv[1]);
return;
}
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
if ((job->state = (!strcmp(argv[0], "fg")) ? FG : BG) == BG) {
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
/* 给目标任务进程发送SIGCONT信号,让其继续执行 */
Kill(-job->pid, SIGCONT);
waitfg(job->pid);
}
问题记录
在写SIGTSTP信号处理的时候遇到了一个问题,想了挺久,当时的代码如下:
void sigtstp_handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs);
if (pid > 0) {
struct job_t* job = getjobpid(jobs, pid);
job->state = ST;
Kill(-pid, SIGTSTP);
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno;
}
在测试时,当父进程接收SIGTSTP并发送给子进程后,父进程没有打印出"job is stopped..."信息,测试程序就结束了。原因在于,我直接在父进程的SIGTSTP信号处理中把子进程任务状态给修改了,而一旦前台状态被修改,waitfg就不会再等待这个任务。
父进程与子进程的执行顺序是不确定的,如果处理器先执行的是父进程,那么有可能waitfg已经退出了,但是子进程连SIGTSTP的处理程序都还没执行,也就更不可能给父进程发送SIGCHLD信号了。这就导致父进程waitfg退出后的一小段时间内,测试程序将结束测试。然而此时子进程还没有把SIGCHLD信号发送给父进程,所以父进程就不能在SIGCHLD处理程序中打印相关的信息,也就是少了一个"job is stopped..."的信息。
解决方法很简单,父进程不要在SIGTSTP处理中修改子进程状态,只在SIGCHLD处理中修改子进程状态即可,这就能保证在SIGCHLD处理中子进程状态修改、打印相关信息后,waitfg才会退出循环。