Shell Lab
本实验的目的是让学生更加熟悉过程控制和信号的概念,您可以通过编写一个简单的Unix shell程序来实现这一点,该程序支持作业控制
实验文件
- tsh.c:实验主体文件,代码写在这里
- tshref.out:包含参考
shell
程序的所有测试数据的输出结果 - tshref:示例程序
开始实验前需要补充一些知识(可以一边看函数,一边学习这些知识)
进程 & 线程 & 任务
进程
进程是指一个具有 独立功能 的程序在 某个数据集合上 的一次动态执行过程,它是操作系统进行资源分配和调度的基本单元
一次任务的运行可以发多个进程,这些进程相互合作来完成该任务的一个最终目标
每个进程都拥有自己的数据段,代码段和堆栈段,这就造成了进程在进行切换时操作系统的开销比较大,为了提高效率,操作系统又引入了另一个概念——线程
线程
线程是进程上下文中执行的代码序列,又称为轻量级的进程,它是操作系统能够调度的最小单元
线程可以对进程的内存空间和资源进行访问,并与同一进程中的其他线程共享,因此,线程的上下文切换的开销比进程小得多
一个进程可以拥有多个线程,其中每个线程共享该进程所拥有的资源,要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响,由此可知,多线程中的同步是非常重要的问题
任务
任务是一个逻辑概念,指由一个软件完成的活动,或者是为实现某个目的的一系列操作
通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务是进程或者是线程
进程控制
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能
在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位
在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态
在五态模型中,进程分为新建态,终止态,运行态,就绪态,阻塞态
下面是实现进程控制的部分函数:
1 | pid_t getpid(void); |
getpid 函数返回调用进程的 PID
getppid 函数返回它的父进程的 PID
1 | void exit(int status); |
exit 函数以 status 退出状态来终止进程
1 | pid_t fork(void); |
父进程通过调用 fork 函数创建一个新的运行的子进程
// 新创建的子进程几乎但不完全与父进程相同
fork 函数是有趣的(也常常令人迷惑),因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中,在父进程中,fork 返回子进程的 PID,在子进程中,fork 返回 0,因为子进程的 PID 总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行
1 | pid_t waitpid(pid_t pid, int *statusp, int options); |
一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止
- 默认情况下(当 options=0 时),waitpid 挂起调用进程的执行,直到它的 等待集合(wait set)中的一个子进程终止。
- 如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么 waitpid 就立即返回
在这两种情况中,waitpid 返回导致 waitpid 返回的已终止子进程的 PID,此时,已终止的子进程已经被回收,内核会从系统中删除掉它的所有痕迹
1 | unsigned int sleep(unsigned int secs); |
sleep 函数将一个进程挂起一段指定的时间
1 | int pause(void); |
pause 函数让调用函数休眠,直到该进程收到一个信号
1 | int execve(const char *filename, const char *argv[], |
execve 函数在当前进程的上下文中加载并运行一个新程序
1 | char *getenv(const char *name); |
getenv 函数在环境数组中搜索字符串 “name=value”,如果找到了,它就返回一个指向 value 的指针,否则它就返回 NULL
如果环境数组包含一个形如 “name=oldva1ue” 的字符串,那么 unsetenv 会删除它,而 setenv 会用 newvalue 代替 oldvalue,但是只有在 overwirte 非零时才会这样,如果 name 不存在,那么 setenv 就把 “name=newvalue” 添加到数组中
1 | int setpgid(pid_t pid, pid_t pgid); |
将参数 pid 指定进程所属的组识别码设为参数 pgid 指定的组识别码
- 如果参数pid 为 0,则会用来设置目前进程的组识别码
- 如果参数pgid 为 0,则会以目前进程的进程识别码来取代
信号集函数
我们已经知道,我们可以通过信号来终止进程,也可以通过信号来在进程间进行通信,程序也可以通过指定信号的关联处理函数来改变信号的默认处理方式,也可以屏蔽某些信号,使其不能传递给进程
- SIGINT:程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程
- SIGQUIT:和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制,进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号
- SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理,通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号(如果进程终止不了,我们才会尝试SIGKILL)
- SIGSTOP:停止(stopped)进程的执行,注意它和terminate以及interrupt的区别,该进程还未结束,只是暂停执行,本信号不能被阻塞,处理或忽略
- SIGCHLD:告知父进程回收自己,但该信号的默认处理动作为忽略,因此父进程仍然不会去回收子进程,需要捕捉处理实现子进程的回收
- SIGTSTP:停止进程的运行,但该信号可以被处理和忽略,用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
那么我们应该如何设定我们需要处理的信号,我们不需要处理哪些信号等问题呢?信号集函数就是帮助我们解决这些问题的,下面是信号函数集:
1 | int sigfillset(sigset_t * set); /* 填充 */ |
sigfillset 用来将参数 set 信号集初始化,然后把所有的信号加入到此信号集里
1 | int sigemptyset(sigset_t *set); /* 清空 */ |
该函数的作用是将信号集初始化为空
1 | int sigaddset(sigset_t *set, int signo); /* 添加 */ |
该函数的作用是把信号 signo 添加到信号集 set 中,成功时返回 0,失败时返回 -1
1 | int sigdelset(sigset_t *set, int signo); /* 删除 */ |
该函数的作用是把信号 signo 从信号集 set 中删除,成功时返回 0,失败时返回 -1
1 | int sigismember(sigset_t *set, int signo); /* 是否是成员 */ |
判断给定的信号 signo 是否是信号集中的一个成员,如果是返回 1,如果不是,返回 0,如果给定的信号无效,返回 -1
1 | int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset ); |
检测或更改其信号屏蔽字
- SIG_BLOCK 在本进程的阻塞列表中,添加 set 指向的阻塞列表
- SIG_UNBLOCK 在本进程的阻塞列表中,删除 set 指向的阻塞列表
- SIG_SETMASK 将目前的阻塞列表设成参数 set 指定的阻塞列表,如果参数 oset 不是 NULL ,那么目前的阻塞列表会由此指针返回(存储在 oset 中)
1 | int sigpending(sigset_t *set); /* 代办(发出但没有没处理) */ |
将被阻塞的信号中 “停留在待处理状态” 的一组信号,写到参数 set 指向的信号集中,成功调用返回 0,否则返回 -1,并设置 errno 表明错误原因(获取被设置为SIG_BLOCK的信号集)
1 | int sigsuspend(const sigset_t *sigmask); /* 挂起 */ |
通过将进程的屏蔽字替换为由参数 sigmask 给出的信号集,然后挂起进程的执行(在一个原子操作中先恢复信号屏蔽字,然后使进程休眠),如果接收到信号终止了程序,sigsuspend 就不会返回,如果接收到的信号没有终止程序,sigsuspend 就返回 -1,并将 errno 设置为 EINTR
// 注意操作的先后顺序,是先替换再挂起程序的执行
另外还有一个关键的结构体,以及其同名函数:
1 |
|
- sa_handler :代表新的信号处理函数
- sa_mask :用来设置在处理该信号时暂时将 sa_mask 指定的信号搁置
- sa_flags :用来设置信号处理的其他相关操作
- sa_restorer :此参数没有使用
函数 sigaction 会依参数 signum 指定的信号编号来设置该信号的处理函数,参数 signum 可以指定 SIGKILL 和 SIGSTOP 以外的所有信号
例子:
1 |
|
1 | ➜ [/home/ywhkkx/桌面] ./test |
实验要求
- tsh 的提示符为
tsh>
- 用户的输入分为第一个的
name
和后面的参数,之间以一个或多个空格隔开,如果name
是一个tsh 内置的命令,那么 tsh 应该马上处理这个命令然后等待下一个输入,否则,tsh 应该假设name
是一个路径上的可执行文件,并在一个子进程中运行这个文件(这也称为一个工作、job) - tsh 不需要支持管道和重定向
- 如果用户输入
ctrl-c
(ctrl-z
),那么SIGINT
(SIGTSTP
) 信号应该被送给每一个在前台进程组中的进程,如果没有进程,那么这两个信号应该不起作用 - 如果一个命令以“&”结尾,那么tsh应该将它们放在后台运行,否则就放在前台运行(并等待它的结束)
- 每一个工作(job)都有一个正整数PID或者job ID(JID)JID通过”%”前缀标识符表示,例如,“%5”表示JID为5的工作,而“5”代笔PID为5的进程
- tsh 应该有如下内置命令:
- quit: 退出当前shell
- jobs: 列出所有后台运行的工作
- bg
: 这个命令将会向 代表的工作发送SIGCONT信号并放在后台运行, 可以是一个PID也可以是一个JID - fg
: 这个命令会向 代表的工作发送SIGCONT信号并放在前台运行, 可以是一个PID也可以是一个JID
- tsh 应该回收(reap)所有僵尸子进程,如果一个工作是因为收到了一个它没有捕获的(没有按照信号处理函数)而终止的,那么tsh应该输出这个工作的PID和这个信号的相关描述
解析已有代码
下面是实验已经给出的代码:
1 | /* |
接下来就一个一个分析已有的函数(不包括 main)
Parseline
1 | int main(int argc, char** argv) |
argv[]:表示的是一个指针数组,一共有 argc 个元素,其中存放的是指向每一个参数的指针
argc:参数个数
1 | int parseline(const char* cmdline, char** argv) |
parseline 函数解析了以空格分隔的命令行参数(跳过所有空格和单引号,获取其中的有效指令),并构造最终会传递给 execve 的 argv 向量
// ‘&’ 表示后台运行,parseline 函数把是否在后台运行的信息存储在bg中,并返回
Struct job_t
1 | struct job_t { /* The job struct */ |
全局结构体 job_t 是与“任务”有关的结构体
基于它,出现了以下的函数:
1 | /* clearjob - Clear the entries in a job struct */ |
清空一个任务
1 | /* initjobs - Initialize the job list */ |
任务初始化
1 | /* maxjid - Returns largest allocated job ID */ |
获取最大的任务ID
1 | /* addjob - Add a job to the job list */ |
添加一个新任务
1 | /* deletejob - Delete a job whose PID=pid from the job list */ |
删除一个任务
1 | /* fgpid - Return PID of current foreground job, 0 if no such job */ |
返回某个任务的 进程ID
1 | /* getjobpid - Find a job (by PID) on the job list */ |
根据 进程ID 获取 任务地址
1 | /* getjobjid - Find a job (by JID) on the job list */ |
根据 任务ID 获取 任务地址
1 | /* pid2jid - Map process ID to job ID */ |
根据 进程ID 获取 任务ID
1 | /* listjobs - Print the job list */ |
打印任务的 “信息”,“状态” 和 “命令行”
Signal
对于程序的信号处理,先给出了 Signal:
1 | /* |
总而言之,Signal 用于初始化信号处理机制,通过调用 sigemptyset 来清空 sa_mask,通过调用 sigaction 把特定信号和特定处理程序绑定
最后给出了几个处理程序:
1 | void unix_error(char* msg) |
1 | void app_error(char* msg) |
1 | void sigquit_handler(int sig) |
其他的处理程序就要自己完成了
编写目标代码
补全tsh.c
中剩余的代码:
void eval(char *cmdline)
:解析并执行命令int builtin_cmd(char **argv)
:检测命令是否为内置命令quit
、fg
、bg
、jobs
void do_bgfg(char **argv)
:实现bg
、fg
命令void waitfg(pid_t pid)
:等待前台命令执行完成void sigchld_handler(int sig)
:处理SIGCHLD
信号,即子进程停止或终止void sigint_handler(int sig)
:处理SIGINT
信号,即来自键盘的中断ctrl-c
void sigtstp_handler(int sig)
:处理SIGTSTP
信号,即终端停止信号ctrl-z
Eval
void eval(char* cmdline):解析并执行命令
1 | void eval(char* cmdline) |
builtin_cmd
int builtin_cmd(char **argv):检查 cmdline 是否为内置命令,并实现 jobs,quit
1 | int builtin_cmd(char** argv) |
do_bgfg
实现 bg命令 (让后台工作继续在后台执行) 和 fg命令 (把后台命令恢复在前台执行)
1 | void do_bgfg(char** argv) |
waitfg
等待前台命令执行完成
1 | void waitfg(pid_t pid) |
sigchld_handler
- SIGCHLD:告知父进程回收自己,但该信号的默认处理动作为忽略,因此父进程仍然不会去回收子进程,需要捕捉处理实现子进程的回收
1 | void sigchld_handler(int sig) |
sigint_handler
- SIGINT:程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程
1 | void sigint_handler(int sig) |
sigtstp_handler
- SIGTSTP:停止进程的运行,但该信号可以被处理和忽略,用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
1 | void sigtstp_handler(int sig) |
完整实验代码
1 | /* |
实验验证
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace01.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace02.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace03.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace04.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace05.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace06.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace07.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace08.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace09.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace10.txt -s ./tsh -a "-p" |
1 | # |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace12.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace13.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace14.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace15.txt -s ./tsh -a "-p" |
1 | ➜ [/home/ywhkkx/shlab-handout] ./sdriver.pl -t trace16.txt -s ./tsh -a "-p" |