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>号称程序员圣经,虽然中文译名为<深入理解计算机系统>,但其实没那么"深",只是覆盖面很广,一般用作计算机专业大一导论课的教科书.早就听闻书上配 ...
随机推荐
- 使用Prometheus和Grafana监控nacos集群
官方文档:https://nacos.io/zh-cn/docs/monitor-guide.html 按照部署文档搭建好Nacos集群 配置application.properties文件,暴露me ...
- Rust学习入门
介绍 特性: 高性能,内存利用率高,没有运行时和垃圾回收 可靠 , 丰富的类型系统和所有权模型保证内存和线程安全,编译器可以消除各种错误 生产力, 包管理器.构建工具一流, 多编辑器支持自动补齐和格式 ...
- Filter 筛选器(三)之 自定义一个启动事务的 TransactionScopeFilter
如果一个方法内有多个写入操作,比如 写入A表,然后用A表的自增id 去写入B表,假如A表写入成功,但B表因为某种原因写入失败!(这就导致A表写入了脏数据) 这时候 我们可以自定义 一个Filter 进 ...
- Pep9课下作业
(1)编写一个伪代码算法 Set sum to 0 Input num1 Read num1 Set sum to sum + num1 Input num2 Read num2 Set sum to ...
- Laravel-Easy-Admin 快速搭建数据后台 web管理后台
基于PHP + Laravel + element-admin-ui 搭建的快速数据后台,只在解决系列后台增删改查等日常操作.快速搭建,在生成业务的同时可以花更多的时间关注技术本身,提高程序员自身进阶 ...
- vue中动态引入图片为什么要是require, 你不知道的那些事
相信用过vue的小伙伴,肯定被面试官问过这样一个问题:在vue中动态的引入图片为什么要使用require 有些小伙伴,可能会轻蔑一笑:呵,就这,因为动态添加src被当做静态资源处理了,没有进行编译,所 ...
- 现代GPGPU 架构汇总
本篇是GPGPU 架构汇总的总章,参考的是AMD公布OpenCL 手册,该手册总结了AMD. Nvdia 早年的GPGPU体系架构,以及Opencl 各个API 与硬件结构的映射关系.本篇除了整理这两 ...
- maven-入门到入土
详情见代码重工:连接http://heavy_code_industry.gitee.io/code_heavy_industry/pro002-maven/
- Nginx 使用自签名证书实现 https 反代 Spring Boot 中碰到的页面跳转问题
问题一:页面自动跳转到 80 端口 问题描述 最近在使用Nginx反代一个Spring Boot项目中碰到了一个问题,使用 Spring Boot 中的 redirect: 进行页面跳转的时候,通过 ...
- 通过Jenkins,执行远程服务器的自动化脚本
通过Jenkins,可自动执行写好的应用部署.自动化测试等的脚本工具,实现测试环境的应用自动定时更新.自动执行测试等. 1. 安装SSH插件 进入[系统管理]-[插件管理]-[可用插件],搜索Publ ...