实验报告
实 验(七)
题 目 TinyShell
微壳
专 业 计算机类
学 号 **********
班 级 1903003
学 生 李*涵
指 导 教 师 郑**
实 验 地 点 G709
实 验 日 期 2021.6.4
计算机科学与技术学院
目 录
X64 CPU; 1.80GHz; 12G RAM; 512GHD SSD; - 4 -
windows10 家庭中文版;Vmware 15; Ubuntu 20.04LTS 64位... - 4 -
codeblocks; vim+gcc; gdb; - 4 -
2.1 进程的概念、创建和回收方法(5分)... - 6 -
2.3 信号的发送方法、阻塞方法、处理程序的设置方法(5分)... - 7 -
2.4 什么是shell,功能和处理流程(5分)... - 9 -
3.1.1 void eval(char *cmdline)函数(10分)... - 10 -
3.1.2 int builtin_cmd(char **argv)函数(5分)... - 11 -
3.1.3 void do_bgfg(char **argv) 函数(5分)... - 11 -
3.1.4 void waitfg(pid_t pid) 函数(5分)... - 11 -
3.1.5 void sigchld_handler(int sig) 函数(10分)... - 12 -
第1章 实验基本信息
1.1 实验目的
理解现代计算机系统进程与并发的基本知识
掌握linux 异常控制流和信号机制的基本原理和相关系统函数
掌握shell的基本原理和实现方法
深入理解Linux信号响应可能导致的并发冲突及解决方法
培养Linux下的软件系统开发与测试能力
1.2 实验环境与工具
1.2.1 硬件环境
X64 CPU; 1.80GHz; 12G RAM; 512GHD SSD;
1.2.2 软件环境
windows10 家庭中文版;Vmware 15; Ubuntu 20.04LTS 64位
1.2.3 开发工具
codeblocks; vim+gcc; gdb;
1.3 实验预习
上实验课前,必须认真预习实验指导书(PPT或PDF)
了解实验的目的、实验环境与软硬件工具、实验操作步骤,复习与实验有关的理论知识。
了解进程、作业、信号的基本概念和原理
了解shell的基本原理
熟知进程创建、回收的方法和相关系统函数
熟知信号机制和信号处理相关的系统函数
Kill命令
kill –l:列出信号
kill –SIGKILL 17130: 杀死pid为17130的进程
kill -9 17130 :杀死pid为17130的进程,或者:
kill -9 -17130:杀死进程组17130中的每个进程
killall -9 pname: 杀死名字为pname的进程进程状态
D 不可中断睡眠 (通常是在IO操作) 收到信号不唤醒和不可运行, 进程必须等待直到有中断发生
R 正在运行或可运行(在运行队列排队中)
S 可中断睡眠 (休眠中, 受阻, 在等待某个条件的形成或接受到信号)
T 已停止的 进程收到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU信号后停止运行
W 正在换页(2.6.内核之前有效)
X 死进程 (未开启)
Z 僵尸进程a defunct (”zombie”) process
< 高优先级(not nice to other users)
N 低优先级(nice to other users)
L 页面锁定在内存(实时和定制的IO)
s 一个信息头
l 多线程(使用 CLONE_THREAD,像NPTL的pthreads的那样)
+ 在前台进程组
第2章 实验预习
总分20分
2.1 进程的概念、创建和回收方法(5分)
进程的经典定义就是一个执行中程序的实例。
一个执行程序中的实例,提供给我吗一种错觉:我们的程序好像是系统中当前运行的唯一程序,我们的程序独占使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序的代码和数据好像是系统中内存唯一的对象。
每次用户通过向shell输人一个可执行目标文件的名字,运行程序时,shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
创建进程:
使用fork函数,父进程调用fork函数创建一个新的运行的子进程,子进程得到和父进程用户及虚拟地址空间完全相同的一个副本。
回收方法:
(1)当一个进程由于某种原因终止时,进程保持在一种已经终止的状态,直到被他的父进程回收,当父进程回收他的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已经终止的进程,从此开始,该进程就不存在了。一个僵死了但还未被会回收的进程被称为僵死进程。
(2)如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养分,init进程将会回收这些孤儿进程。
使用waitpid函数来等待子进程终止或者停止。
2.2信号的机制、种类(5分)
信号:一个信号就是一条消息,它通知进程系统中发生了一个某种类型的事件。每种信号类型都对应某种系统的事件。
以Linux信号为例,它时一种软件层面的异常。
信号提供了一种机制,通知用户进程发生了由内核异常处理程序处理的底层的硬件异常。
发送信号:内核通过更新目的进程上下文的某种状态,发送一个信号给目的进程。
接收信号:目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了这种信号。
Linux信号如下:
2.3 信号的发送方法、阻塞方法、处理程序的设置方法(5分)
信号的发送方法:
- 用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号
- 从键盘发送信号
在键盘上输入Ctrl+C会导致内核发送一个SIGINT信号到前台进程中的每个进程,默认情况下,结果是是终止前台作业。
在键盘上输入Ctrl+Z会发送一个SIGTSTP信号到前台进程中的每个进程,默认情况下,结果是是停止(挂起)前台作业。
(3) 用kill函数发送信号给其他进程(包括他们自己)
(4) 使用alarm函数发送信号
进程可以通过调用 alarm 函数在指定 secs 秒后发送一个 SIGALRM 信号给调用进程。
信号的阻塞方法:
- 隐式阻塞机制:内核默认阻塞任何当前处理程序正在处理信号类和待处理信号。
- 显示阻塞进制:应用程序可以调用 sigprocmask 函数和它的辅助函 数,明确地阻塞和解除阻塞选定的信号。
处理程序的设置方法:
- 进程可以通过使用signal函数修改和信号相关联的默认行为。唯一的例外是SIGSTOP和SIGKILL,它们的默认行为是不能修改的。
signal函数可以通过下列三种方法之- .来改变和信号signum相关联的行为:
如果handler是SIG_ IGN,那么忽略类型为signum 的信号。
如果handler是SIG_ DFL,那么类型为signum的信号行为恢复为默认行为。
否则,handler 就是用户定义的函数的地址,这个函数被称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。调用信号处理程序被称为捕获信号。执行信号处理程序被称为处理信号。
当一个进程捕获了一个类型为k的信号时,会调用为信号k设置的处理程序,一个整数参数被设置为k。这个参数允许同一个处理函数捕获不同类型的信号。
- 因为 signal 的语义各有不同,系统调用可以被中断。要解决这些问题,Posix 标准定义了sigaction 函数,它允许用户在设置信号处理时,明确指定他们想要的信号处理语义。
- 一个更简洁的方式,最初是由 Richard Stevens提出的,就是定义一个包装函数,称为Signal,它调用sigaction。
2.4 什么是shell,功能和处理流程(5分)
shell:一个交互型应用级程序,代表用户运行其他程序。
功能:shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序执行
5)shell 应该接受键盘输入信号,并对这些信号进行相应处理
第3章 TinyShell的设计与实现
总分45分
3.1 设计
3.1.1 void eval(char *cmdline)函数(10分)
函数功能:解析和解释命令行的主例程。
参 数:char *cmdline
处理流程:
(1) 调用 parseline 函数。将 cmdline 字符串切分为参数数组 argv,返回 bg得知该操作是否需要后台运行。
(2) 调用 buildin_cmd 判断该函数是否为内置函数。如果是内置函数则立即执 行,执行后返回。
(3) 如果不是,先设置阻塞信号集,阻塞预先识别的信号,防止shell捕获并处理信号。
(4) 创建一个子进程,解锁子进程的阻塞,将子进程放进一个进程组,在子进程中运行命令行输入的可执行文件(调用 execve 函数)。
(5) 将进程放进jobs列表。
(6) 判断是否是后台程序,不是后台程序,shell界面暂时挂起,执行waitfg;
如果是后台程序,打印部分信息。
要点分析:
- 将命令行的处理封装并集中
- 阻塞shell的预识别信号,防止shell捕获并处理,避免竞争。
- 父进程先记录addjob,后解除了阻塞,在阻塞期间捕获到的sigchild在此时被处理,同时在任务列表中删除,避免了竞争现象的出现。
- ctrl-c会给shell(bash)下前台进程组中的所有进程发送SIGINT信号,包括tsh和tsh创建的进程——这样显然不对。
解决办法:
- 在fork后、execve前,子进程调用函数setpgid(0, 0)将自己放到一个新的进程组中(进程组ID与子进程的PID相同)。在前台进程组中只有一个进程tsh。
- 当键入Ctrl-C, shell(bash)将捕获产生的SIGINT,然后转发给tsh, tsh收到SIGINT后,转发给适当的前台作业(更准确的说法:前台作业的进程组)
- tsh是在Linux 的shell(bash)下运行的,tsh在前台进程组中运行,此时,tsh创建的子进程也默认在前台进程组中。所以通过阻塞完成。
3.1.2 int builtin_cmd(char **argv)函数(5分)
函数功能:识别并解释内置命令: quit, fg, bg和jobs
参 数:char **argv
处理流程:数个并列的判断语句,其中有一个符合,则运行相应函数,并返回1(退出不返回);若均不符合,则在函数尾返回0。
要点分析:每次只是简单的函数调用,除了退出的处理不同,其他都是类似的实现。函数模块化思想及其突显。
3.1.3 void do_bgfg(char **argv) 函数(5分)
函数功能:实现内置命令bg和fg
参 数:char **argv
处理流程:
- 先判断是否有参数传入,没有参数直接输出提示信息并返回,有参数执行后续步骤
- 判断输入的是pid还是%jid,无论哪种都在jobs里查找,过程中出现问题则提示并返回,否则顺序执行
- 匹配是后台运行还是前台运行。后台运行,向job所在进程组发送SIGCONT信号,更改state为bg;前台运行,向job所在的进程组发送SIGCONT信号,更改job的state为fg,然后等待当前的程序运行直到当前的job不再是前台程序。
要点分析:
- 错误输入的处理在每一个匹配环节都要考虑
- 区分传入pid还是%jid
- 按照bg和fg特性,发送信号让程序持续运行。
- fg需要将目标程序在前台运行,调用waitfg等待程序停止或终止。
3.1.4 void waitfg(pid_t pid) 函数(5分)
函数功能:等待一个前台作业结束
参 数:pid_t pid
处理流程:调用fgpid函数,判断pid有没有被改变,即所给进程是否结束。
- 最简单的方式是pause();缺点:浪费资源,引入竞争
- 其次是sleep(),优势是不浪费资源,但以秒为单位,效率低
- 采用sigsuspend函数,同时不阻塞任何信号。比前两种方法都实用。
要点分析:
- pause可能引入竞争,直接被排除。
- sleep效率低,在调试过程中,有无法解决的问题时,用sleep虽低效,但最为保险。
- sigsuspend函数虽然性能及其优异,但对于我们来说不是很熟悉,有些bug调试不出来。
- 跳出循环的方式是fgpid,而fgpid发生改变是在shell捕获并处理信号之后,所以对于信号的阻塞一定要考虑清楚。
3.1.5 void sigchld_handler(int sig) 函数(10分)
函数功能: 父进程中接收到SIGCHLD信号的处理函数
参 数:int sig
处理流程:
- 保存errno,防止过程中的改变影响进程上下文的运行。
- 使用while循环回收所有的子进程,防止僵死
- 对waitpid返回的情况进行匹配
- 正常退出:删除子任务
- 暂时停止:输出提示信息
- 未被捕获的信号终止:输出提示信息并删除(kill就不能被捕获)
- 恢复errno
要点分析:
- while使用的waitpid是立即返回,没有停止或终止则返回0,主要是实时回收,不会等待某个进程执行完。
- 在删除子任务时要阻塞所有信号,防止修改全局变量是接受信号对进程的影响。
- 防止调用修改errno,开始保存,结尾恢复。
- 对不同的停止做不同处理。
3.2 程序实现(tsh.c的全部内容)(10分)
重点检查代码风格:
- 用较好的代码注释说明——5分
- 检查每个系统调用的返回值——5分
/*
* tsh - A tiny shell program with job control
*
* <Put your name and login ID here>
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
/* Misc manifest constants */
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1 /* running in foreground */
#define BG 2 /* running in background */
#define ST 3 /* stopped */
/*
* Jobs states: FG (foreground), BG (background), ST (stopped)
* Job state transitions and enabling actions:
* FG -> ST : ctrl-z
* ST -> FG : fg command
* ST -> BG : bg command
* BG -> FG : fg command
* At most 1 job can be in the FG state.
*/
/* Global variables */
extern char **environ; /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0; /* if true, print additional output */
int nextjid = 1; /* next job ID to allocate */
char sbuf[MAXLINE]; /* for composing sprintf messages */
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 */
/* End global variables */
/* Function prototypes */
/* Here are the functions that you will implement */
void eval(char *cmdline);
int builtin_cmd(char **argv);
void do_bgfg(char **argv);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv);
void sigquit_handler(int sig);
void clearjob(struct job_t *job);
void initjobs(struct job_t *jobs);
int maxjid(struct job_t *jobs);
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *</