ShellLab
实验要用到的文件(注意查看时间线,我把完成的作业也push上来了)
注意此实验要在linux下完成。
我们首先仔细阅读几遍README.md,把陌生的概念搞懂。
这里强调一下第八章异常控制流的一些内容:
- 信号中的输出全用封装后的Sio,而不是printf,因为异步信号安全问题
- 所有的注册信号处理函数都要用大写S的那个,它已经给我们实现了可移植的信号处理。
sdriver.pl :.pl 文件是 Perl 脚本文件的扩展名。这是一种编程语言。
然后我们看tsh.c
里面内置的英文注释已经解释很多了。
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}
这是用来解析参数的
./tsh -h
# 比如说这一条是打印帮助信息
这里注意,在运行tsh之前,先输入make来编译它。
由于没有实现quit,所以想退的话,直接把终端删了重新运行吧。
我们需要实现:
void eval(char *cmdline) //分析命令,并派生子进程执行 主要功能是解析cmdline并运行
int builtin_cmd(char **argv)//解析和执行bulidin命令,包括 quit, fg, bg, and jobs
void do_bgfg(char **argv) //执行bg和fg命令
void waitfg(pid_t pid)//实现阻塞等待前台程序运行结束
void sigchld_handler(int sig)//SIGCHID信号处理函数
void sigint_handler(int sig)//信号处理函数,响应 SIGINT (ctrl-c) 信号
void sigtstp_handler(int sig)//信号处理函数,响应 SIGTSTP (ctrl-z) 信号
以及提供给我们的函数:
int parseline(const char *cmdline, char **argv); //获取参数列表,返回是否为后台运行命令
void sigquit_handler(int sig); //处理SIGQUIT信号
void clearjob(struct job_t *job); //清除job结构体
void initjobs(struct job_t *jobs); //初始化任务jobs[]
int maxjid(struct job_t *jobs); //返回jobs链表中最大的jid号。
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline); //向jobs[]添加一个任务
int deletejob(struct job_t *jobs, pid_t pid); //在jobs[]中删除pid的job
pid_t fgpid(struct job_t *jobs); //返回当前前台运行job的pid号
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); //根据pid找到对应的job
struct job_t *getjobjid(struct job_t *jobs, int jid); //根据jid找到对应的job
int pid2jid(pid_t pid); //根据pid找到jid
void listjobs(struct job_t *jobs); //打印jobs
以及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 */
};
struct job_t jobs[MAXJOBS]; /* The job list */
状态的宏定义
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
按理说,这7个函数可以先写简单的,但是如果不写eval根本没法测试,所以就先写它。
eavl
我们参考一下教材中的框架(深入理解计算机系统黑皮书第三版525页)
void eval(char *cmdline) {
char* argv[MAXARGS]; /* 参数列表 execve() */
char buf[MAXLINE]; /* 将command line复制到这个数组 */
int bg; /* 标志程序是前台还是后台 */
pid_t pid; /* 创建的进程id */
strcpy(buf, cmdline); //复制到buf
bg = parseline(buf, argv); //检查是前台命令还是后台命令,将结果存入bg,同时在argv中存入参数列表
if (argv[0] == NULL)
return; /* 无视空的输入 */
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) { //子进程运行该程序
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* 接下来父进程检查是否是后台命令 */
if (!bg) { //如果不是,就等到子进程结束返回
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
}
else//如果是后台命令,打印
printf("%d%s", pid, cmdline);
}
return;
}
然后我们开始修改。
void eval(char *cmdline) {
char* argv[MAXARGS]; //参数列表
char buf[MAXLINE]; //将cmdline复制到此数组
pid_t pid;
int state = UNDEF; //启动的进程状态
sigset_t mask, prev_mask; //用于信号阻塞和恢复
strcpy(buf, cmdline);//将cmdline复制到buf
//构建阻塞信号集合
if (sigemptyset(&mask) < 0) {
unix_error("sigemptyset error");
}
if (sigaddset(&mask, SIGINT) < 0) {
unix_error("sigaddset error");
}
if (sigaddset(&mask, SIGTSTP) < 0) {
unix_error("sigaddset error");
}
if (sigaddset(&mask, SIGCHLD) < 0) {
unix_error("sigaddset error");
}
//构建阻塞信号集合(SIGINT、SIGTSTP、SIGCHLD)完成
//读取命令的前台后台
if (parseline(buf, argv)) {
state = BG;
}
else {
state = FG;
}
//前台后台读取完成
//如果命令是空行,直接返回
if (argv[0] == NULL)
return;
//如果不是内置命令
if (!builtin_cmd(argv)) {
//阻塞信号,防止竞争
if (sigprocmask(SIG_BLOCK, &mask, &prev_mask) < 0) {
unix_error("sigprocmask error");
}
//创建进程
if ((pid = fork()) < 0) {
unix_error("fork error");
}
if (pid == 0) { //子进程控制流开始
//在子进程中恢复信号
if (sigprocmask(SIG_SETMASK, &prev_mask, NULL) < 0) {
unix_error("sigprocmask error");
}
//设置前台进程组,以便SIGINT信号的实现
if (setpgid(0, 0) < 0) {
unix_error("setpgid error");
}
//加载程序
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
//将当前进程加入到job list中
addjob(jobs, pid, state, cmdline);
//shell进程恢复阻塞的信号
if (sigprocmask(SIG_SETMASK, &prev_mask, NULL) < 0) {
unix_error("sigprocmask error");
}
if (state == FG) { //前台作业等待执行
waitfg(pid);
}
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); //后台作业打印
}
return;
}
增加了信号,以及添加进joblist中。
注:
1.每个子进程必须要设置进程组id,否则一个子进程和它的父进程在同一个进程组,这是很麻烦的,因为SIGINT和SIGTSTP要发送信号到前台进程组。而他们不能也把这个微型shell也终止了,所以子进程组和shell要区分。同时,我们不能因为前台进程只有一个,而仅仅将信号发到那一个进程,万一那个前台进程又fork了呢?发送到那一组,才可以使所有的前台进程都收到。
2.我们一定要在fork之前进行信号阻塞,这是一个非常经典的同步流错误,具体看第八章教材或者我的笔记
builtin_cmd
此函数的功能是识别和解释内置命令:quit、fg、bg和jobs
int builtin_cmd(char **argv)
也就是,如果argv是内置命令,就执行(有返回就返回true),如果不是,返回false
- quit命令终止shell。
- jobs命令列出所有后台作业。
- bg 命令通过发送SIGCONT信号重新启动,然后在后台运行它。 参数可以是PID或JID。
- fg 命令通过发送SIGCONT信号重新启动,然后在前台运行它。 参数可以是PID或JID。
int builtin_cmd(char **argv) {
if (!strcmp(argv[0], "quit")) { //如果是quit,退出shell
exit(0);
}
else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) { //如果是bg或者fg命令,执行do_fgbg函数
do_bgfg(argv);
}
else if (!strcmp(argv[0], "jobs")) { //如果命令是jobs,列出所有后台作业
listjobs(jobs);
}
else {
return 0;
}
return 1;
}
这个很容易
do_bgfg
bg
:将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动 ,然后在后台运行它。 参数可以是PID,也可以是JID。ST -> BG fg
:将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启 ,然后在前台运行它。 参数可以是PID,也可以是JID。ST -> FG,BG -> FG
fg和bg必须要有参数,而且这两个指令都有输出,以下是参考输出
tsh> ./bogus
./bogus: Command not found
tsh> ./myspin 4 &
[1] (26326) ./myspin 4 &
tsh> fg
fg command requires PID or %jobid argument
tsh> bg
bg command requires PID or %jobid argument
tsh> fg a
fg: argument must be a PID or %jobid
tsh> bg a
bg: argument must be a PID or %jobid
tsh> fg 9999999
(9999999): No such process
tsh> bg 9999999
(9999999): No such process
tsh> fg %2
%2: No such job
tsh> fg %1
Job [1] (26326) stopped by signal 20
tsh> bg %2
%2: No such job
tsh> bg %1
[1] (26326) ./myspin 4 &
tsh> jobs
[1] (26326) Running ./myspin 4 &
下面给出函数实现。
void do_bgfg(char **argv) {
int id;
struct job_t* obj;
//如果没有参数,打印以下内容
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
//检测参数
if (argv[1][0] == '%') {//如果是jid
if (sscanf(&argv[1][1], "%d", &id) != 1) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]); //读取失败,打印
return;
}
if ((obj = getjobjid(jobs, id)) == NULL) {
printf("%%%d: No such process\n", id); //jid找不到进程
return;
}
}
else {//如果是pid
if (sscanf(&argv[1][0], "%d", &id) != 1) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);//读取失败,打印
return;
}
if ((obj = getjobpid(jobs, id)) == NULL) {
printf("(%d): No such process\n", id); //pid找不到进程
return;
}
}
//参数检测完成
//如果是bg,则在后台运行
if (!strcmp(argv[0], "bg")) {
obj->state = BG; //设置状态
if (kill(-obj->pid, SIGCONT) < 0) { //使用-pid表示整个进程组
unix_error("kill error");
}
printf("[%d] (%d) %s", obj->jid, obj->pid, obj->cmdline);
}
//如果是fg,则在前台运行
else if (!strcmp(argv[0], "fg")) {
obj->state = FG; //设置状态
if (kill(-obj->pid, SIGCONT) < 0) { //使用-pid表示整个进程组
unix_error("kill error");
}
//前台运行的程序应该等待其运行结束
waitfg(obj->pid);
}
else {
exit(0);
}
return;
}
waitfg
waitfg - Block until process pid is no longer the foreground process
void waitfg(pid_t pid) {
struct job_t* obj;
sigset_t mask, prev_mask;
//阻塞信号的集合
if (sigemptyset(&mask) < 0) {
unix_error("sigemptyset error");
}
if (sigaddset(&mask, SIGINT) < 0) {
unix_error("sigaddset error");
}
if (sigaddset(&mask, SIGTSTP) < 0) {
unix_error("sigaddset error");
}
if (sigaddset(&mask, SIGCHLD) < 0) {
unix_error("sigaddset error");
}
//阻塞信号
if (sigprocmask(SIG_BLOCK, &mask, &prev_mask) < 0) {
unix_error("sigprocmask error");
}
obj = getjobpid(jobs, pid);
//如果前台进程没有结束
if (obj) {
while (obj->state == FG) {
sigsuspend(&prev_mask); //通过sigsuspend来实现显式等待信号(如果当前子进程的状态没有发生改变,则休眠)
//sleep(1);
}
}
//解除阻塞
if (sigprocmask(SIG_SETMASK, &prev_mask, NULL) < 0) {
unix_error("sigprocmask error");
}
return;
}
我们通过sigsuspend来实现显式等待信号。
sigchld_handler
要求:当子进程终止(变为僵尸进程)或因接收到SIGSTOP或SIGTSTP信号而停止时,内核会向shell发送一个SIGCHLD信号。该处理程序会收获所有可用的僵尸子进程,但不会等待任何其他当前正在运行的子进程终止。
使用waitpid,其中选项为WNOHANG,表示当子进程中没有终止的进程时,返回0,另一个选项是WUNTRACED,表示当子进程除了终止,停止的时候也会返回子进程的pid。因为子进程停止的时候,也会发送SIGCHILD信号。
我们也要使用教材里提供的异步信号安全的Sio函数
而且也要保存全局变量errno的值
void sigchld_handler(int sig) {
int olderrno = errno; //由于该信号处理函数使用系统调用,所以要保存全局变量errno的值
pid_t pid;
int status, jid;
struct job_t* obj;
//WNOHANG:若子进程仍然在运行,则返回0 (可以实现“不会等待运行的子进程终止,而仅仅只处理僵尸进程”)
//WUNTRACED : 如果子进程由于传递信号而停止,则马上返回pid(可以实现,当有子进程停止的时候也会返回其pid)
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
//当子进程的job已经删除了,表示有错误发生
if ((obj = getjobpid(jobs, pid)) == NULL) {
Sio_puts("lack job\n");
errno = olderrno; //返回前重新设置errno
return;
}
//接下来判断子进程发送信号时的状态
jid = obj->jid;
//子进程暂停了
if (WIFSTOPPED(status)) {
obj->state = ST; //设置挂起状态
Sio_puts("Job ["); //使用异步信号安全的输出函数
Sio_putl(jid);
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") stopped by signal ");
Sio_putl(WSTOPSIG(status));
Sio_puts("\n");
}
//子进程调用exit或者从main返回导致终止,不输出内容
else if (WIFEXITED(status)) {
deletejob(jobs, pid);
}
//子进程收到信号而终止
else if (WIFSIGNALED(status)) {
deletejob(jobs, pid);
Sio_puts("Job [");
Sio_putl(jid);
Sio_puts("] (");
Sio_putl(pid);
Sio_puts(") terminated by signal ");
Sio_putl(WTERMSIG(status));
Sio_puts("\n");
}
}
//返回前重新设置errno
errno = olderrno;
return;
}
sigint_handler
当用户在键盘上按下 ctrl-c 时,内核会向 shell 发送一个 SIGINT 信号。捕获这个信号并将其传递给前台作业。
这个简单
void sigint_handler(int sig) {
pid_t pid;
int olderrno = errno;//保护errno
//得到前台进程的pid
pid = fgpid(jobs);
if (pid) {
//向前台进程组发送信号
if (kill(-pid, SIGINT) < 0)
unix_error("kill (sigint) error");
}
errno = olderrno;
return;
}
sigtstp_handler
当用户在键盘上按下 ctrl-z 时,内核会向 shell 发送一个 SIGTSTP 信号。捕获这个信号,并通过向前台作业发送一个 SIGTSTP 信号来挂起它。
void sigtstp_handler(int sig)
{
pid_t pid;
int olderrno = errno; //保护errno
//得到前台进程的pid
pid = fgpid(jobs);
if (pid) {
//向前台进程组发送信号
if (kill(-pid, SIGTSTP) < 0)
unix_error("kill (sigtstp) error");
}
errno = olderrno;
return;
}
这样,一个个测试样例即可,应该是都可以过的。
最后还有Sio,并没有给出,不过可以在csapp.c中找到
/* Sio (Signal-safe I/O) routines */
ssize_t sio_puts(char s[]);
ssize_t sio_putl(long v);
void sio_error(char s[]);
/* 经过封装的sio */
ssize_t Sio_puts(char s[]);
ssize_t Sio_putl(long v);
void Sio_error(char s[]);
//这些是智慧树中的异步信号安全的输出函数(csapp.h)
//下面是异步信号安全的Sio的实现(csapp.c)
static void sio_reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}
/* sio_ltoa - Convert long to base b string (from K&R) */
static void sio_ltoa(long v, char s[], int b)
{
int c, i = 0;
do {
s[i++] = ((c = (v % b)) < 10) ? c + '0' : c - 10 + 'a';
} while ((v /= b) > 0);
s[i] = '\0';
sio_reverse(s);
}
/* sio_strlen - Return length of string (from K&R) */
static size_t sio_strlen(char s[])
{
int i = 0;
while (s[i] != '\0')
++i;
return i;
}
/* $end sioprivate */
/* Public Sio functions */
/* $begin siopublic */
ssize_t sio_puts(char s[]) /* Put string */
{
return write(STDOUT_FILENO, s, sio_strlen(s)); //line:csapp:siostrlen
}
ssize_t sio_putl(long v) /* Put long */
{
char s[128];
sio_ltoa(v, s, 10); /* Based on K&R itoa() */ //line:csapp:sioltoa
return sio_puts(s);
}
void sio_error(char s[]) /* Put error message and exit */
{
sio_puts(s);
_exit(1); //line:csapp:sioexit
}
/* $end siopublic */
/*******************************
* Wrappers for the SIO routines
******************************/
ssize_t Sio_putl(long v)
{
ssize_t n;
if ((n = sio_putl(v)) < 0)
sio_error("Sio_putl error");
return n;
}
ssize_t Sio_puts(char s[])
{
ssize_t n;
if ((n = sio_puts(s)) < 0)
sio_error("Sio_puts error");
return n;
}
void Sio_error(char s[])
{
sio_error(s);
}