TinyShell(CSAPP实验)
简介
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:实现内置命令bg,fg。
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-c(ctrl-z)应该导致SIGINT(SIGTSTP)信号被发送到当前的前台作业,及其该作业的子孙作业(例如,它创建的任何子进程)。如果没有前台工作,那么信号应该没有效果。
如果命令行以&结尾,则tsh在后台运行该作业;否则,在前台运行该作业
可以用进程ID(PID)或tsh赋予的正整数作业ID(job ID,JID)标识一个作业。JID用前缀%,例如%5标识作业ID为5的作业,5表示PID为5的作业。
已经提供了处理作业列表所需的所有函数
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中有如下选项:
- WNOHANG:若当前没有等待集合中的子进程终止,则立即返回0
- WUNTRACED:等待直到某个等待集合中的子进程停止或返回,并返回这个子进程的pid。
- WCONTINUED:等待直到某个等待集合中的子进程重新开始执行或终止。
- 组合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号信号)
安全的信号处理
目的
让信号处理程序和主程序它们可以安全地,无错误地,按照我们预期地并发地运行。
方法
处理程序尽可能简单。
在处理程序只调用异步信号安全的函数。
- 可重入的(只访问局部变量)。
- 不能被信号处理程序中断。
保存和恢复errno。避免干扰其他依赖于errno的部分。解决方法是用局部变量存储,再恢复。
void Example(int sig)
{
int olderrno = errno;
/*
this is your code
*/
errno = olderrno;
}
- 阻塞所有信号,保护对共享全局变量数据结构的访问。
- 用volatile声明全局变量。
- 用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.
注意
- 不可以用信号来对其他进制中发生的事件计数。
- 使用原子(atomic)函数如sigsuspend函数消除潜在的竞争并提高效率。
实验
eval
要点分析:
创建子进程前需要阻塞信号,防止竞争。
将子进程加入到jobs后,需要恢复,即解除阻塞。
创建子进程时,为子进程创建一个新的进程组。
/*
* 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
要点分析:
需要保证参数正确,即将不正确的情况排除。
区别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
要点分析:
在等待的循环不使用可能会无限休眠的pause,也不使用太慢的sleep。
在等待的循环中使用sigsuspend函数,因为它是原子的。
在等待前,需阻塞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
要点分析:
删除作业信息时,属于访问全局变量,需要阻塞全部信号。
保存恢复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实验)的更多相关文章
- CSAPP实验——DataLab
任务:按照要求补充13个函数,会限制你能使用的操作及数量 bitXor(x,y) 只使用 ~ 和 & 实现 ^ tmin() 返回最小补码 isTmax(x) 判断是否是补码最大值 allOd ...
- CSAPP实验attacklab
attacklab 实验报告和答案文件都在 https://github.com/thkkk/attacklab
- CSAPP Lab2: Binary Bomb
著名的CSAPP实验:二进制炸弹 就是通过gdb和反汇编猜测程序意图,共有6关和一个隐藏关卡 只有输入正确的字符串才能过关,否则会程序会bomb终止运行 隐藏关卡需要输入特定字符串方会开启 实验材料下 ...
- CSAPP:Lab1 -DataLab 超详解
写在前面 之前考研的时候csapp的书有刷过5,6遍,所以对书本知识还算比较了解.恰逢最近在学c++的时候,顺带刷一下大名鼎鼎的csapp实验. 0. 环境准备 最好准备一个纯净的Linux系统这里建 ...
- CSAPP:bomblab
BOMBLAB实验总结 CSAPP实验BOMB,很头疼,看不懂,勉强做完了. 答案是这样的: Border relations with Canada have never been better. ...
- CSAPP缓冲区溢出攻击实验(上)
CSAPP缓冲区溢出攻击实验(上) 下载实验工具.最新的讲义在这. 网上能找到的实验材料有些旧了,有的地方跟最新的handout对不上.只是没有关系,大体上仅仅是程序名(sendstring)或者參数 ...
- CSAPP 六个重要的实验 lab5
CSAPP && lab5 实验指导书: http://download.csdn.net/detail/u011368821/7951657 实验材料: http://downlo ...
- CSAPP缓冲区溢出攻击实验(下)
CSAPP缓冲区溢出攻击实验(下) 3.3 Level 2: 爆竹 实验要求 这一个Level的难度陡然提升,我们要让getbuf()返回到bang()而非test(),并且在执行bang()之前将g ...
- 《CSAPP》实验二:二进制炸弹
二进制炸弹是第三章<程序的机器级表示>的配套实验,这章主要介绍了x64汇编,包括:操作数的表示方式,数据传送指令,算术和逻辑指令,控制流跳转指令,过程(procedure)的实现与运行时栈 ...
- 《CSAPP》实验一:位操作
<CSAPP>号称程序员圣经,虽然中文译名为<深入理解计算机系统>,但其实没那么"深",只是覆盖面很广,一般用作计算机专业大一导论课的教科书.早就听闻书上配 ...
随机推荐
- SVM公式详尽推导,没有思维跳跃。
假定数据集\(T=\{(x_1,y_1),(x_2,y_2),...,(x_n,y_n)\},x_n \in R_k, y_n \in \{1,-1\}\)线性可分,SVM的优化目标是: 优化一个超平 ...
- C#-5 类和继承
一 类继承 继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一.继承呈现了面向对象程序设计的层次结构. 通过继承,我们能以一个已经存在的类为基础来定义一个新类.已存在的类称为基类或父类 ...
- 自学Spring
Spring官网地址https://spring.io/ springManven官网地址:https://mvnrepository.com------------------------ spri ...
- 220726 T3 最优化问题 (树状数组)
题目描述 在同学们的努力下, 高匀感受到了 alb 的快乐. 高勺意犹未尽,找来了一个长度为 nn 的序列 a_1,a_2,-.,a_na1,a2,-.,an . 她想要删除这个序列中的 kk ...
- POJ3585 Accumulation Degree (树形DP-二次扫描与换根)
本题属于不定根的树形DP,若以每个节点为根求解一次,复杂度太高,所以可以用换根的技巧. d[u]表示以u为根向下可以流的最大流量,这个是比较好求的,直接遍历到叶子节点,由子节点信息更新父节点.然后进行 ...
- SpringBoot (四) - 整合Mybatis,逆向工程,JPA
1.SpringBoot整合MyBatis 1.1 application.yml # 数据源配置 spring: datasource: driver-class-name: com.mysql.c ...
- JavaScript基本语法(JavaScript代码嵌入方式与声明和使用变量)
.JavaScript代码嵌入方式 #①HTML文档内 JavaScript代码要写在script标签内 script标签可以写在文档内的任意位置 为了能够方便查询或操作HTML标签(元素)scrip ...
- 手写自定义springboot-starter,感受框架的魅力和原理
一.前言 Springboot的自动配置原理,面试中经常问到,一直看也记不住,不如手写一个starter,加深一下记忆. 看了之后发现大部分的starter都是这个原理,实践才会记忆深刻. 核心思想: ...
- Java多线程(4):ThreadLocal
您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来- 为了提高CPU的利用率,工程师们创造了多线程.但是线程们说:要有光!(为了减少线程创建(T1启动)和销毁(T3切换)的时间),于是工程师们又接着 ...
- 驱动开发:内核监控Register注册表回调
在笔者前一篇文章<驱动开发:内核枚举Registry注册表回调>中实现了对注册表的枚举,本章将实现对注册表的监控,不同于32位系统在64位系统中,微软为我们提供了两个针对注册表的专用内核监 ...