简介

CSAPP实验介绍

学生实现他们自己的带有作业控制的Unix Shell程序,包括Ctrl + C和Ctrl + Z按键,fg,bg,和 jobs命令。这是学生第一次接触并发,并且让他们对Unix的进程控制、信号和信号处理有清晰的了解。

什么是Shell?

​ Shell就是用户与操作系统内核之间的接口,起着协调用户与系统的一致性和在用户与系统之间进行交互的作用。

​ Shell最重要的功能是命令解释,从这种意义上说,Shell是一个命令解释器。

​ Linux系统上的所有可执行文件都可以作为Shell命令来执行。当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。在查找该命令时分为两种情况:

(1)用户给出了命令的路径,Shell就沿着用户给出的路径进行查找,若找到则调入内存,若没找到则输出提示信息;

(2)用户没有给出命令的路径,Shell就在环境变量Path所制定的路径中依次进行查找,若找到则调入内存,若没找到则输出提示信息。

关于本次实验

本次实验需要我们熟读CSAPP第八章异常控制流

需要设计和实现的函数:

  • eval 函数:解析命令行。

Evaluate the command line that the user has just typed in.

  • builtin_cmd:判断是否为内置 shell 命令

If the user has typed a built-in command then execute it immediately.

  • do_bgfg:实现内置命令bgfg

Execute the builtin bg and fg commands.

  • waitfg:等待前台作业完成。

Block until process pid is no longer the foreground process

  • sigchld_handler:捕获SIGCHLD信号。
  • sigint_handler:捕获SIGINT信号。
  • sigtstp_handler:捕获SIGTSTP信号。

TinyShell辅助函数:

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); //解析命令行参数
void sigquit_handler(int sig);//退出的处理函数
/*jobs是全局变量,存储每一个进程的信息。*/
/*jid为job编号ID,pid为进程ID*/
void clearjob(struct job_t *job);//清除所有工作
void initjobs(struct job_t *jobs);//初始化工作结构体
int maxjid(struct job_t *jobs); //返回jobs中jid的最大值
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//添加job
int deletejob(struct job_t *jobs, pid_t 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);//遍历
void usage(void);//帮助信息
void unix_error(char *msg);//报错unix-style error routine
void app_error(char *msg);//报错application-style error routine
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);//信号设置

实验要求

  • tsh的提示符:tsh>

  • 用户输入的命令行应该包括一个名字、0或多个参数,并用一个或多个空格分隔。

  • 如果名字是内置命令,tsh立即处理并等待用户输入下一个命令行。否则,假定这个名字是一个可执行文件的路径,tsh在初始子进程的上下文中加载和运行它。

  • tsh不需要支持管(|)或I/O重定向(<>)。

  • 键入ctrl-cctrl-z)应该导致SIGINTSIGTSTP)信号被发送到当前的前台作业,及其该作业的子孙作业(例如,它创建的任何子进程)。如果没有前台工作,那么信号应该没有效果。

  • 如果命令行以&结尾,则tsh在后台运行该作业;否则,在前台运行该作业

  • 可以用进程ID(PID)tsh赋予的正整数作业IDjob IDJID)标识一个作业。JID用前缀%,例如%5标识作业ID5的作业,5表示PID5的作业。

  • 已经提供了处理作业列表所需的所有函数

  • tsh支持以下内置命令:

    • quit:终止tsh程序
    • jobs:列出所有后台job
    • bg:后台运行程序
    • fg:前台运行程序

回顾

fork

pid_t fork(void)

在函数调用处创建子进程。

父进程函数返回子进程的PID

子进程函数返回0

waitpid

一个进程可以通过waitpid函数来等待它的子进程终止或者停止。

pid_t waitpid(pid_t pid, int *statusp, int options);

pid:判定等待集合的成员

  • pid > 0时,waitpid等待进程ID为pid的进程;
  • pid = -1时,waitpid等待所有它的子进程。

options:修改默认行为

options中有如下选项:

  1. WNOHANG:若当前没有等待集合中的子进程终止,则立即返回0
  2. WUNTRACED:等待直到某个等待集合中的子进程停止或返回,并返回这个子进程的pid。
  3. WCONTINUED:等待直到某个等待集合中的子进程重新开始执行或终止。
  4. 组合WNOHANG | WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回0。如果有,则返回PID。

statusp:检查已回收子进程的退出状态

如果statusp参数非空,那么waitpid就会在status中放入关于导致返回的子进程的状态信息,status是statusp指向的值。

  • WIFEXITED(status):如果子进程通过调用exit或者返回(return)正常终止,就返回真。
  • ········

kill函数

int kill(pid_t pid, int signo);

  • pid > 0,信号发送给pid进程;
  • pid == 0,把信号发送给本进程(自己)所在的进程组中所有进程,不包括系统进程;
  • pid < 0,把信号发送给组id 为 -pid 的进程组中所有进程;
  • pid == -1,把信号发送给所有进程,除系统进程外(有些进程不接受9和19号信号)

安全的信号处理

目的

让信号处理程序和主程序它们可以安全地,无错误地,按照我们预期地并发地运行。

方法

  1. 处理程序尽可能简单。

  2. 在处理程序只调用异步信号安全的函数。

    1. 可重入的(只访问局部变量)。
    2. 不能被信号处理程序中断。
  3. 保存和恢复errno。避免干扰其他依赖于errno的部分。解决方法是用局部变量存储,再恢复。

void Example(int sig)
{
int olderrno = errno;
/*
this is your code
*/
errno = olderrno;
}
  1. 阻塞所有信号,保护对共享全局变量数据结构的访问。
  2. volatile声明全局变量。
  3. sig_atomic_t声明标志。

例:在添加job时,阻塞信号,因为jobs是全局变量。

This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP signals until we can add the job to the job list. This eliminates some nasty races between adding a job to the job list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.

注意

  1. 不可以用信号来对其他进制中发生的事件计数。
  2. 使用原子(atomic)函数如sigsuspend函数消除潜在的竞争并提高效率。

实验

eval

要点分析:

  1. 创建子进程前需要阻塞信号,防止竞争。

  2. 将子进程加入到jobs后,需要恢复,即解除阻塞。

  3. 创建子进程时,为子进程创建一个新的进程组。

/*
* 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)
{
/* $begin handout */
char *argv[MAXARGS]; /* argv for execve() */
int bg; /* should the job run in bg or fg? */
pid_t pid; /* process id */
sigset_t mask; /* signal mask */ /* Parse command line */
bg = parseline(cmdline, argv);
if (argv[0] == NULL)
return; /* ignore empty lines */ if (!builtin_cmd(argv)) { /*
* This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP
* signals until we can add the job to the job list. This
* eliminates some nasty races between adding a job to the job
* list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.
*/ if (sigemptyset(&mask) < 0)
unix_error("sigemptyset error");
if (sigaddset(&mask, SIGCHLD))
unix_error("sigaddset error");
if (sigaddset(&mask, SIGINT))
unix_error("sigaddset error");
if (sigaddset(&mask, SIGTSTP))
unix_error("sigaddset error");
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
unix_error("sigprocmask error"); /* Create a child process */
if ((pid = fork()) < 0)
unix_error("fork error"); /*
* Child process
*/ if (pid == 0) {
/* Child unblocks signals */
sigprocmask(SIG_UNBLOCK, &mask, NULL); /* Each new job must get a new process group ID
so that the kernel doesn't send ctrl-c and ctrl-z
signals to all of the shell's jobs */
if (setpgid(0, 0) < 0)
unix_error("setpgid error"); /* Now load and run the program in the new job */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
} /*
* Parent process
*/ /* Parent adds the job, and then unblocks signals so that
the signals handlers can run again */
addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL); if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
/* $end handout */
return;
}

builtin_cmd

要点分析:

调用listjobs时,属于访问全局变量,需要阻塞和解除阻塞。

/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if(*argv == NULL)
{
return 0;
}
sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");
}
if(! strcmp(argv[0], "quit"))
{ if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问全局变量需要阻塞
{
unix_error("sigprocmask error!");
}
int i;
for(i = 0; i < MAXJOBS; i ++ )//退出时终止所有所有的子进程
{
if(jobs[i].pid)
{
kill(- jobs[i].pid, SIGINT);
}
}
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
exit(0);//Shell exit
}else if(! strcmp(argv[0], "jobs"))
{
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//同理,访问全局变量
{
unix_error("sigprocmask error!");
}
listjobs(jobs);//遍历
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
return 1;
}else if(! strcmp(argv[0], "&"))
{
return 1;// &也是内置命令,需要返回1
}else if(! strcmp(argv[0], "fg") || ! strcmp(argv[0], "bg"))
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}

do_bgfg

要点分析:

  1. 需要保证参数正确,即将不正确的情况排除。

  2. 区别jid和pid。

/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
/* $begin handout */
struct job_t *jobp = NULL; /* Ignore command if no argument */
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
} /* Parse the required PID or %JID arg */
if (isdigit(argv[1][0])) {
pid_t pid = atoi(argv[1]);
if (!(jobp = getjobpid(jobs, pid))) {
printf("(%d): No such process\n", pid);
return;
}
}
else if (argv[1][0] == '%') {
int jid = atoi(&argv[1][1]);
if (!(jobp = getjobjid(jobs, jid))) {
printf("%s: No such job\n", argv[1]);
return;
}
}
else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
} /* bg command */
if (!strcmp(argv[0], "bg")) {
if (kill(-(jobp->pid), SIGCONT) < 0)
unix_error("kill (bg) error");
jobp->state = BG;
printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline);
} /* fg command */
else if (!strcmp(argv[0], "fg")) {
if (kill(-(jobp->pid), SIGCONT) < 0)
unix_error("kill (fg) error");
jobp->state = FG;
waitfg(jobp->pid);
}
else {
printf("do_bgfg: Internal error\n");
exit(0);
}
/* $end handout */
return;
}

waitfg

要点分析:

  1. 在等待的循环不使用可能会无限休眠的pause,也不使用太慢的sleep。

  2. 在等待的循环中使用sigsuspend函数,因为它是原子的。

  3. 在等待前,需阻塞chld信号。

/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
sigset_t mask, prev_mask;
if(sigemptyset(&mask))
{
unix_error("sigempty error!");
}
if(sigaddset(&mask, SIGCHLD))
{
unix_error("sigaddset error!");
}
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问jobs先阻塞chld信号
{
unix_error("sigprocmask error!");
} while(fgpid(jobs) == pid)
{
sigsuspend(&prev_mask);//消除竞争
}
//
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
} return;
}

sigchld_handler

要点分析:

  1. 删除作业信息时,属于访问全局变量,需要阻塞全部信号。

  2. 保存恢复errno。

/*
* 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)
{
int olderrno = errno; pid_t pid;
int status; sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");
} while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问全局变量前阻塞所有信号
{
unix_error("sigprocmask error!");
}
struct job_t *temp = getjobpid(jobs, pid);
if(WIFEXITED(status))//正常结束
{
deletejob(jobs, pid);
}else if(WIFSIGNALED(status))//被未捕获的信号终止
{ int jid = pid2jid(pid); printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
deletejob(jobs, pid);
}else if(WIFSTOPPED(status))//停止的信号
{
temp->state = ST;
int jid = pid2jid(pid);
printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status));
}
fflush(stdout);//之前printf输出,所以刷新
if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
} errno = olderrno;
return;
}

sigint_handler

/*
* 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)
{
int olderrno = errno;//保存和恢复errno sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");//阻塞所有信号
}
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
{
unix_error("sigprocmask error!");
} pid_t pid = fgpid(jobs);
if(pid != 0)//对进程组发送SIGINT
kill(-pid, sig); if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
errno = olderrno;
return;
}

sigtstp_handler

/*
* 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)
{
int olderrno = errno;
sigset_t mask, prev_mask;
if(sigfillset(&mask))
{
unix_error("sigfillset error!");//阻塞所有信号来访问全局变量
}
if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))
{
unix_error("sigprocmask error!");
}
pid_t pid = fgpid(jobs);
if(pid != 0)//向进程组发送SIGTSTP
kill(-pid, sig); if(sigprocmask(SIG_SETMASK, &prev_mask, NULL))
{
unix_error("sigprocmask error!");
}
errno = olderrno;
return;
}

测试

对比tsh和参考shell程序tshref,测试了16组例子。

TinyShell(CSAPP实验)的更多相关文章

  1. CSAPP实验——DataLab

    任务:按照要求补充13个函数,会限制你能使用的操作及数量 bitXor(x,y) 只使用 ~ 和 & 实现 ^ tmin() 返回最小补码 isTmax(x) 判断是否是补码最大值 allOd ...

  2. CSAPP实验attacklab

    attacklab 实验报告和答案文件都在 https://github.com/thkkk/attacklab

  3. CSAPP Lab2: Binary Bomb

    著名的CSAPP实验:二进制炸弹 就是通过gdb和反汇编猜测程序意图,共有6关和一个隐藏关卡 只有输入正确的字符串才能过关,否则会程序会bomb终止运行 隐藏关卡需要输入特定字符串方会开启 实验材料下 ...

  4. CSAPP:Lab1 -DataLab 超详解

    写在前面 之前考研的时候csapp的书有刷过5,6遍,所以对书本知识还算比较了解.恰逢最近在学c++的时候,顺带刷一下大名鼎鼎的csapp实验. 0. 环境准备 最好准备一个纯净的Linux系统这里建 ...

  5. CSAPP:bomblab

    BOMBLAB实验总结 CSAPP实验BOMB,很头疼,看不懂,勉强做完了. 答案是这样的: Border relations with Canada have never been better. ...

  6. CSAPP缓冲区溢出攻击实验(上)

    CSAPP缓冲区溢出攻击实验(上) 下载实验工具.最新的讲义在这. 网上能找到的实验材料有些旧了,有的地方跟最新的handout对不上.只是没有关系,大体上仅仅是程序名(sendstring)或者參数 ...

  7. CSAPP 六个重要的实验 lab5

    CSAPP  && lab5 实验指导书: http://download.csdn.net/detail/u011368821/7951657 实验材料: http://downlo ...

  8. CSAPP缓冲区溢出攻击实验(下)

    CSAPP缓冲区溢出攻击实验(下) 3.3 Level 2: 爆竹 实验要求 这一个Level的难度陡然提升,我们要让getbuf()返回到bang()而非test(),并且在执行bang()之前将g ...

  9. 《CSAPP》实验二:二进制炸弹

    二进制炸弹是第三章<程序的机器级表示>的配套实验,这章主要介绍了x64汇编,包括:操作数的表示方式,数据传送指令,算术和逻辑指令,控制流跳转指令,过程(procedure)的实现与运行时栈 ...

  10. 《CSAPP》实验一:位操作

    <CSAPP>号称程序员圣经,虽然中文译名为<深入理解计算机系统>,但其实没那么"深",只是覆盖面很广,一般用作计算机专业大一导论课的教科书.早就听闻书上配 ...

随机推荐

  1. 创建一个 autocomplete 输入系统 - 前端 + 后端

    文章转载自:https://mp.weixin.qq.com/s/uqchdrkhdFsof0ZFtECujg 我们经常在网站搜索输入时,会帮我们提醒自动完成的功能,比如: 图片 当我们在百度上搜索 ...

  2. 分布式文件存储 CephFS的应用场景

    块存储 (适合单客户端使用) 典型设备:磁盘阵列,硬盘. 使用场景: a. docker容器.虚拟机远程挂载磁盘存储分配. b. 日志存储. 文件存储 (适合多客户端有目录结构) 典型设备:FTP.N ...

  3. 【前端必会】tapable、hook,webpack的灵魂

    背景 什么是tapable.hook,平时做vue开发时的webpack 配置一直都没弄懂,你也有这种情况吗? 还是看源码,闲来无聊又看一下webpack的源码,看看能否找到一些宝藏 tapable和 ...

  4. 【软件学习】怎么在Word里面设置MathType分隔符,使公式按照章节自动编号

    前提 确保已安装好了MathType,且在设置为Word加载项 若没有安装或设置为加载项,请转至博客另一篇文章: [软件学习]如何下载安装Mathtype,并将其加载至Word 第一步 点击MathT ...

  5. 通过linux-PAM实现禁止root用户登陆的方法

    前言 在linux系统中,root账户是有全部管理权限的,一旦root账户密码外泄,对于服务器而言将是致命的威胁:出于安全考虑,通常会限制root账户的登陆,改为配置普通用户登陆服务器后su切换到ro ...

  6. 关于Springboot启动报错 Whitelabel Error Page: This application has no explicit mapping

    Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as ...

  7. 13.MongoDB系列之分片简介

    1. 分片概念 分片是指跨机器拆分数据的过程,有时也会用术语分区.MongoDB既可以手工分片,也支持自动分片 2. 理解集群组件 分片的目标之一是由多个分片组成的集群对应用程序来说就像是一台服务器. ...

  8. HTML基础知识(3)浮动、塌陷问题

    1.浮动 1.1 代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> & ...

  9. 日志处理logging

    前言 什么是日志?有什么作用?日志是跟踪软件运行时所发生的事件的一种方法,简单来说它可以记录某时某刻运行了什么代码,当出现问题时可以方便我们进行定位. 由python内置了一个logging模块,用户 ...

  10. python3使用mutagen进行音频元数据处理

    python版本:python 3.9   mutagen版本:1.46.0 mutagen是一个处理音频元数据的python模块,支持多种音频格式,是一个纯粹的python库,仅依赖python标准 ...