shell-lab是csapp的配套实验之一,它要求我们实现一个功能和unix shell类似的tiny shell,在源文件tsh.c中已给出了基本框架,剩下的只需要完成实现指定的函数即可,该实验对应csapp的第8章内容。
Unix Shell
Shell可以认为是一个可交互的命令解释器,它替代用户运行程序,shell会从标准输入流stdin
等待输入命令行,根据命令行内容的指示执行对应操作。命令行是由空格分隔的ascii单词序列组成,其中第一个单词为内置命令或可执行文件的路径(或环境变量path目录下的可执行文件相对路径),其余的单词就是命令行的参数了。如果是执行内置命令,那么shell将在当前的进程立即执行该命令;如果执行其它可执行文件,shell就会fork一个子进程,然后在该子进程的上下文加载并运行该程序,用于解释单个命令而创建的子进程统称为job(作业)
。当命令行以&
结尾时,作业将在后台运行,即shell打印提示并等待下一个命令之前,不会等待作业终止,否则作业将在前台运行,这样shell在等待下一个命令之前要等待作业终止,所以任何时候前台只能运行一个作业,而后台可以运行多个作业。
Unix Shell也支持 作业控制 的概念,允许用户切换作业为前台或后台运行,也允许通过信号修改作业的状态(running、stopped、terminated)。通过键盘可以产生信号,例如CTRL+C产生SIGINT
(interrupt)终止信号,会终止前台作业并使其变为后台terminated状态;CTRL+Z产生SIGTSTP
(stop)暂停信号,会暂停前台作业并使其变为后台stopped状态。通过命令也可以产生信号,例如bg和fg命令产生SIGCONT
(continue)继续信号,会继续运行被暂停的作业并使其变为后台(前台)running状态,如果对应作业本身就为running状态,那么这两个命令只有切换前后台的作用。
tsh
我们需要按照题目的要求实现一个tiny shell,只有unix shell的部分功能,其要求如下:
- 提示前缀为”
tsh>
“。 - 用户键入的命令由一个name和0至多个参数构成,他们均由空格分隔。如果name是一个内置指令,则tsh要立即处理它并继续等待下一个命令;否则tsh假定name是可执行文件的路径,并创建一个子进程(作业),在该子进程的上下文中加载该文件并运行。
- tsh不需要支持管道(
|
)和I/O重定向(>和<
)。 - 输入
CTRL+C(CTRL+Z)
应当发送SIGINT(SIGTSTP)
信号给当前的前台作业和其任何子进程,如果没有前台作业则该信号无效。 - 当命令行以
&
结尾时,tsh应当运行后台作业,否则将会在前台运行这个作业。 - 每个作业都应当由进程ID(PID)或作业ID(JID)标识,JID是tsh分配的正整数,且JID在命令行需要加上前缀
%
。 - tsh需要支持如下内置指令:
quit
命令终止tsh进程jobs
命令列出所有后台进程bg <job>
命令会向作业发送SIGCONT
信号来重启job,并作为后台作业运行,参数可以是PID或JIDfg <job>
同上,唯一区别是job以前台作业运行
- tsh需要回收所有其子僵尸进程,如果有任何作业由于接收到未捕获的信号而终止,那么tsh需要识别此事件并打印一条带有该作业PID的消息以及对该问题信号的描述。
代码实现
tsh.c已经将主体的框架和非关键部分的函数实现好,我们只要完成eval、builtin_cmd、do_bgfg、waitfg这四个函数加上sigchld_handler、sigtstp_handler、sigint_handler这三个信号处理函数即可,很多代码可以参考书里第八章的部分代码,具体实现如下:
/* * eval - Evaluate the command line that the user has just typed in * * If the user has requested a built-in command (quit, jobs, bg or fg) * then execute it immediately. Otherwise, fork a child process and * run the job in the context of the child. If the job is running in * the foreground, wait for it to terminate and then return. Note: * each child process must have a unique process group ID so that our * background children don't receive SIGINT (SIGTSTP) from the kernel * when we type ctrl-c (ctrl-z) at the keyboard. */
void eval(char *cmdline)
{
sigset_t mask_all, mask_one, prev_one;
Sigfillset(&mask_all);
Sigemptyset(&mask_one);
Sigaddset(&mask_one, SIGCHLD);
char *argv[MAXARGS];
bool bg = parseline(cmdline, argv);
if(argv[0]==NULL){
return;
}
if(builtin_cmd(argv)){
return;
}
else{
//fork之前阻塞child信号
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
pid_t pid = Fork();
if(pid == 0){
//子进程
//子进程给自己设置独立的进程组
Setpgid(0,0);
//子进程继承了block位向量,所以需要恢复
Sigprocmask(SIG_SETMASK, &prev_one, NULL);
//在子进程上下文加载对应程序
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?BG:FG, cmdline);
Sigprocmask(SIG_SETMASK, &prev_one, NULL); //恢复block向量到最初始状态
//printf("%d\n", bg);
if(!bg){
waitfg(pid);
}
else{
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}
}
}
/* * builtin_cmd - If the user has typed a built-in command then execute * it immediately. */
int builtin_cmd(char **argv)
{
char *name = argv[0];
if(name == NULL) {
return 0;
}
if(strcmp(name,"quit")==0){
exit(0);
}
else if(strcmp(name,"jobs")==0){
listjobs(jobs);
}
else if(strcmp(name, "bg")==0 || strcmp(name, "fg")==0){
do_bgfg(argv);
}
else return 0; /* not a builtin command */
return 1;
}
/* * do_bgfg - Execute the builtin bg and fg commands */
void do_bgfg(char **argv)
{
char *name = argv[0];
char *id = argv[1];
if(id==NULL){
printf("%s command requires PID or %%jobid argument\n", name);
return;
}
struct job_t *pjob;
int pid;
if(id[0]=='%'){
//jid
int jid = atoi(id+1);
if(jid==0){
printf("%s: argument must be PID or %%jobid\n", name);
return;
}
pjob = getjobjid(jobs, jid);
if(pjob==NULL){
printf("%d: No such job\n", jid);
return;
}
pid = pjob->pid;
}
else{
//pid
pid = atoi(id);
if(pid==0){
printf("%s: argument must be PID or %%jobid\n", name);
return;
}
pjob = getjobpid(jobs, pid);
if(pjob==NULL){
printf("%d: No such process\n", pid);
return;
}
}
Kill(-pid, SIGCONT);
if(strcmp(name, "bg")==0){
pjob->state = BG;
printf("[%d] (%d) %s",pid2jid(pid), pid, pjob->cmdline);
}
else if(strcmp(name, "fg")==0){
pjob->state = FG;
waitfg(pid);
}
}
/* * waitfg - Block until process pid is no longer the foreground process */
void waitfg(pid_t pid)
{
sigset_t mask_empty, mask_all, prev_all;;
Sigemptyset(&mask_empty);
Sigfillset(&mask_all);
//等待直到没有前台作业(只等待不回收)
while(true){
Sigsuspend(&mask_empty); //保证原子性,等待信号中断并返回
//访问jobs之前阻塞所有信号
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
if(fgpid(jobs)==0){
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
break;
}
else{
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
}
}
/* * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever * a child job terminates (becomes a zombie), or stops because it * received a SIGSTOP or SIGTSTP signal. The handler reaps all * available zombie children, but doesn't wait for any other * currently running children to terminate. */
void sigchld_handler(int sig)
{
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
int oldErrno = errno;
pid_t pid;
int status;
//访问jobs之前阻塞所有信号
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
while((pid = waitpid(-1, &status, WNOHANG|WUNTRACED))>0){
//子进程正常终止
if(WIFEXITED(status)){
deletejob(jobs, pid);
}
//收到信号终止
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 *pjob = getjobpid(jobs, pid);
pjob->state = ST;
}
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
//options传WNOHANG|WUNTRACED则不需要对errno检测
// if(errno!=ECHILD){
// Sio_error("waitpid error\n");
// }
errno = oldErrno;
}
/* * sigint_handler - The kernel sends a SIGINT to the shell whenver the * user types ctrl-c at the keyboard. Catch it and send it along * to the foreground job. */
void sigint_handler(int sig)
{
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
int oldErrno = errno;
//访问jobs之前阻塞所有信号
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t target_pid = fgpid(jobs);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
if(target_pid!=0){
//不能使用包装函数Kill、传入负值表示进程组id
if(kill(-target_pid, SIGINT)<0){
Sio_error("Kill Error\n");
}
}
errno = oldErrno;
}
/* * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever * the user types ctrl-z at the keyboard. Catch it and suspend the * foreground job by sending it a SIGTSTP. */
void sigtstp_handler(int sig)
{
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
int oldErrno = errno;
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t target_pid = fgpid(jobs);
Sigprocmask(SIG_BLOCK, &prev_all, NULL);
if(target_pid!=0){
if(kill(-target_pid, SIGTSTP)<0){
Sio_error("Kill Error\n");
}
}
errno = oldErrno;
}
实验提供了trace01~trace16十六个文本文件,作为sdrive.pl的输入驱动我们编写的tsh输出对应结果,模拟命令行执行的结果,开发阶段可以对tsh手动输入trace文本中的命令进行调试。另外实验也提供了tshref作为参考二进制文件,我们的目标就是使tsh的表现与tshref完全一致,makefile文件中给出了测试指令,使用make test和make rtest指令即可对tsh和tshref进行快速测试,tshref.out文件为参考输出结果。
注意
- 由于我们引入了csapp源文件,所以会出现一些重复定义的编译错误,相应的需要修改源文件tsh.c,将重复定义的符号MAXLINE、unix_error、app_error、Signal注释掉;makefile文件也要修改加上对csapp的依赖,因为csapp.c用到的pthread库不是标准linux库,所以要在编译tsh文件时要加上
-lpthread
参数。makefile中添加如下内容:
tsh: tsh.c csapp.c csapp.h
$(CC) $(CFLAGS) -o tsh csapp.c tsh.c -lpthread
- 代码中大量使用了csapp.c提供的 错误处理包装函数(如Kill,Sigprocmask),而这些函数又使用了unix_error函数,其调用的exit函数不是 异步信号安全 的函数,所以在信号处理程序中不能使用,而要重新包装换用
sio_error
函数。也就是存在一种可能(尽管可能性很低),主程序在执行exit函数的过程中(因为其非原子过程)被信号中断,而信号处理程序中也恰好调用了exit函数,则可能会产生状态不一致的问题。但是代码中省略了重新包装的步骤,特此注明一下。 - 不论是主程序还是信号处理程序在访问全局共享变量时(如jobs),都要临时阻塞所有信号,待访问结束后再解除阻塞,将访问操作变为不可中断的原子操作,避免信号中断引起的状态不一致。
- 注意在信号处理程序中保存和恢复
errno
的状态。 - 为了方便,在信号处理程序中使用的printf函数,其实应当替换为异步信号安全的
sio_printf
函数。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/38280.html