目录
2023/5/19 20:14紧急修复:
刚刚我发现了一个bug那就是如果我直接运行我自己的shell(在终端直接输入./tsh),他会报出一个段错误,应该是eval函数出现了问题,我修改了一下,发现这个问题目前消失了。
——————————————————————————————————————————
2023/5/19 23:31:
不好意思今晚在看MSI,恭喜全华班BLG击败GenG挺近败者组决赛!!
——————————————————————————————————————————-
CSAPP-ShellLab
本实验代码我已经通过了16个trace的验证,可以放心食用,但在本实验报告中暂未给出trace的验证(累了,晚上再写)。
shlab-overview
在给出的所有文件中有一个shlab-overview的pdf文件,但它是英文的,翻译成中文之后发现其实是本实验的真正说明书;
实验任务:编写自己的Shell
介绍
本次任务的目的是让您更熟悉进程控制和信号的概念。您将通过编写一个支持作业控制的简单Unix Shell程序来实现这一目的。
发放指令
首先,将文件shlab-handout.tar复制到您计划进行工作的受保护目录(实验目录)中。然后按照以下步骤进行操作:
• 输入命令tar xvf shlab-handout.tar来解压缩tar文件。
• 输入命令make来编译和链接一些测试例程。
• 在tsh.c的顶部标题注释中输入您的团队成员姓名和Andrew ID。
查看tsh.c(tiny shell)文件,您会发现它包含了一个简单Unix Shell的功能框架。为了帮助您入手,我们已经实现了一些较不重要的函数。您的任务是完成下面列出的剩余空白函数。为了让您进行自我检查,我们在参考解决方案中列出了每个函数的大致代码行数(包括许多注释)。
• eval:解析和解释命令行的主要程序。[70行]
• builtin cmd:识别和解释内置命令:quit、fg、bg和jobs。[25行]
• do bgfg:实现bg和fg内置命令。[50行]
• waitfg:等待前台作业完成。[20行]
• sigchld handler:捕获SIGCHILD信号。[80行]
• sigint handler:捕获SIGINT(ctrl-c)信号。[15行]
• sigtstp handler:捕获SIGTSTP(ctrl-z)信号。[15行]
每次修改tsh.c文件后,输入make重新编译它。要运行您的shell,请在命令行中输入tsh:
unix> ./tsh
tsh> [在此处输入您的shell命令]
Unix Shell的一般概述
Shell是一个交互式的命令行解释器,代表用户运行程序。Shell重复打印提示符,等待来自stdin的命令行,然后根据命令行的内容执行一些操作。
命令行是由空格分隔的ASCII文本单词序列。命令行中的第一个单词可以是内置命令的名称或可执行文件的路径名。其余的单词是命令行参数。如果第一个单词是内置命令,shell立即在当前进程中执行该命令。否则,该单词被认为是可执行程序的路径名。在这种情况下,shell会fork一个子进程,然后在子进程的上下文中加载和运行程序。作为解释单个命令行的结果创建的子进程被称为作业。一般来说,作业可以由多个通过Unix管道连接的子进程组成。
如果命令行以一个“&”符号结尾,那么作业在后台运行,这意味着shell在打印提示符并等待下一个命令行之前不会等待作业终止。否则,作业在前台运行,这意味着shell在等待作业终止之前等待下一个命令行。因此,在任何时候,最多只能有一个作业在前台运行。然而,任意数量的作业可以在后台运行。
例如,输入以下命令行:
tsh> jobs
会使shell执行内置的jobs命令。输入命令行
tsh> /bin/ls -l -d
会在前台运行ls程序。按照惯例,shell确保当程序开始执行其主函数
int main(int argc, char *argv[])
时,argc和argv参数具有以下值:
• argc == 3,
• argv[0] == ‘/bin/ls’,
• argv[1] == ‘-l’,
• argv[2] == ‘-d’。
或者,输入命令行
tsh> /bin/ls -l -d &
会在后台运行ls程序。
Unix Shell支持作业控制的概念,允许用户在后台和前台之间移动作业,并更改作业中进程的状态(运行、停止或终止)。按下ctrl-c会导致向前台作业中的每个进程发送SIGINT信号。SIGINT的默认操作是终止进程。类似地,按下ctrl-z会导致向前台作业中的每个进程发送SIGTSTP信号。SIGTSTP的默认操作是将进程置于停止状态,直到收到SIGCONT信号唤醒。Unix Shell还提供了各种支持作业控制的内置命令。例如:
• jobs:列出正在运行和停止的后台作业。
• bg <job>:将一个停止的后台作业更改为运行的后台作业。
• fg <job>:将一个停止或正在运行的后台作业更改为前台运行。
• kill <job>:终止一个作业。
tsh规范
您的tsh shell应具有以下功能:
• 提示符应为字符串“tsh>”。
• 用户输入的命令行应由一个名称和零个或多个参数组成,它们之间用一个或多个空格分隔。如果名称是一个内置命令,那么tsh应立即处理它并等待下一条命令行。否则,tsh应假设名称是一个可执行文件的路径,在一个初始子进程的上下文中加载和运行它(在这个上下文中,术语”作业”指的是这个初始子进程)。
• tsh不需要支持管道(|)或I/O重定向(<和>)。
• 按下ctrl-c(ctrl-z)应该导致一个SIGINT(SIGTSTP)信号发送到当前的前台作业,以及该作业的任何后代(例如,它派生的任何子进程)。如果没有前台作业,则该信号不会产生任何效果。
• 如果命令行以&结尾,则tsh应在后台运行作业。否则,它应该在前台运行作业。
• 每个作业可以通过进程ID(PID)或作业ID(JID)来标识,作业ID是由tsh分配的
正整数
。在命令行上,
JID应以“%”作为前缀表示
。例如,“%5”表示JID 5,“5”表示PID 5。(我们为操作作业列表提供了您所需的所有例程。)
• tsh应支持以下内置命令:
– quit命令终止shell。
– jobs命令列出所有后台作业。
– bg <job>命令通过发送SIGCONT信号来重新启动<job>,然后在后台运行它。 <job>参数可以是PID或JID。
– fg <job>命令通过发送SIGCONT信号来重新启动<job>,然后在前台运行它。 <job>参数可以是PID或JID。
• tsh应收集并回收所有僵尸子进程。如果任何作业终止,因为它接收到一个未捕获的信号,那么tsh应该识别这个事件并打印一个带有作业的PID和有关违规信号的描述的消息。
检查您的工作
我们提供了一些工具来帮助您检查您的工作。
参考解决方案。Linux可执行文件tshref是shell的参考解决方案。运行此程序以解决您对您的shell应该如何行为的任何疑问。您的shell应该产生与参考解决方案相同的输出(当然,除了PID,因为它们在每次运行时都会改变)。
Shell驱动程序。sdriver.pl程序将shell作为子进程执行,根据跟踪文件的指示发送命令和信号,并捕获和显示来自shell的输出。
使用-h参数查找sdriver.pl的用法:
unix> ./sdriver.pl -h
用法:sdriver.pl [-hv] -t <trace> -s <shellprog> -a <args> 选项:
-h 打印此消息
-v 显示更多详细信息
-t <trace> 跟踪文件
-s <shell> 要测试的 Shell 程序
-a <args> Shell 参数
-g 生成用于自动评分的输出
我们还提供了 16 个跟踪文件(trace01-16.txt),您将与 shell driver 一起使用这些文件来测试您的 shell 的正确性。较低编号的跟踪文件进行非常简单的测试,较高编号的跟踪文件进行更复杂的测试。
您可以使用跟踪文件 trace01.txt(例如)来运行 shell driver,命令如下:
unix> ./sdriver.pl -t trace01.txt -s ./tsh -a “-p” (-a “-p” 参数告诉您的 shell 不要显示提示符)
或者 unix> make test01
类似地,要将您的结果与参考 shell 进行比较,可以运行跟踪驱动程序并指定参考 shell,命令如下:
unix> ./sdriver.pl -t trace01.txt -s ./tshref -a “-p”
或者unix> make rtest01
供您参考,tshref.out 提供了参考解决方案在所有跟踪文件上的输出。这可能比手动运行 shell driver 在所有跟踪文件上更方便。
跟踪文件的好处是它们生成与您以交互方式运行 shell 时相同的输出(除了标识跟踪的初始注释)。例如:
提示
(这个部分对于编写后面的程序至关重要!!)
• 请先阅读第8章(异常控制流)的每个单词。
• 使用跟踪文件来指导您的 shell 的开发。从 trace01.txt 开始,确保您的 shell 产生与参考 shell 相同的输出。然后继续处理 trace02.txt,以此类推。
• waitpid、kill、fork、execve、setpgid 和 sigprocmask 函数非常有用。waitpid 的 WUNTRACED 和 WNOHANG 选项也会很有用。
• 在实现信号处理程序时,确保使用 ” -pid ” 而不是 ” pid ” 参数向整个前台进程组发送 SIGINT 和 SIGTSTP 信号,使用 kill 函数。sdriver.pl 程序会测试这个错误。
• 本任务的一个棘手之处是决定 waitfg 和 sigchld 处理程序之间的工作分配。我们建议采用以下方法:
– 在 waitfg 中,使用一个围绕 sleep 函数的忙循环。
– 在 sigchld 处理程序中,只使用一次 waitpid 调用。
虽然还有其他的解决方案,比如在 waitfg 和 sigchld 处理程序中都调用 waitpid,但这可能非常令人困惑。在处理程序中进行所有收割操作更简单。
• 在 eval 中,父进程在 fork 子进程之前必须使用 sigprocmask 阻塞 SIGCHLD 信号,然后在通过调用 addjob 将子进程添加到作业列表后再解除阻塞这些信号,再次使用 sigprocmask。由于子进程继承了父进程的阻塞向量,子进程在执行新程序之前必须确保解除阻塞 SIGCHLD 信号。
父进程需要以这种方式阻塞 SIGCHLD 信号,以避免出现以下竞态条件:在父进程调用 addjob 之前,子进程由 sigchld 处理程序收割(因此从作业列表中删除)。
• more、less、vi 和 emacs 等程序在终端设置方面做了一些奇怪的事情。不要从您的 shell 中运行这些程序。请使用简单的基于文本的程序,如 /bin/ls、/bin/ps 和 /bin/echo。
• 当您从标准 Unix shell 运行您的 shell 时,您的 shell 正在运行在前台进程组中。如果您的 shell 创建了一个子进程,默认情况下,该子进程也将成为前台进程组的成员。由于键入 ctrl-c 会向前台组中的每个进程发送 SIGINT 信号,因此键入 ctrl-c 会向您的 shell 以及您的 shell 创建的每个进程发送 SIGINT,显然这是不正确的。
以下是解决方法:在 fork 之后但在 execve 之前,子进程应调用 setpgid(0, 0),将子进程放入一个新的进程组,其组 ID 与子进程的 PID 相同。这确保在前台进程组中只有一个进程,即您的 shell。当您键入 ctrl-c 时,shell 应该捕获结果的 SIGINT,然后将其转发给适当的前台作业(或更准确地说,包含前台作业的进程组)。
祝您好运!
题目要求
这个实验是大家在本课程第一次体验系统级编程,涉及过程,过程控制和信号的相关知识。
-
你需要干什么? 你需要构建一个简单的类Unix/Linux Shell。基于已经提供的“微Shell”框架tsh.c,完成部分函数和信号处理函数的编写工作。使用sdriver.pl可以评估你所完成的shell的相关功能。
-
准备工作 使用命令tar xvf shelab-handout.tar 解压缩文件; 使用命令 make 去编译和链接一些测试例程;
你要实现的重要函数列出如下: eval 主例程,用以分析和解释命令行(好消息:该函数原型在教材一第8章8.4节中可以找到!); builtin_cmd 执行bg和fg内置命令; waitfg 等待前台作业执行; sigchld_handler 响应处理SIGCHILD信号 sigint_handler 响应处理SIGINT(ctrl-c)信号 sigtstp_handler 相应处理SIGSTP(ctrl-z)信号
-
注意 每次修改了tsh.c文件,都需要make它以重新编译。在你的Linux终端中直接运行tsh(./tsh)就可以进入你所编写完成的tiny shell tsh>了。
-
如何证明你完成了这个实验 在你的Linux终端运行./tshref 这个已经实现的shell,将其输出结果与你所实现的./tsh 输出结果比较,是否一致。 相关比较命令行,参见shelab-overview文件。
-
请在实验报告体现你解决本实验目标的详细过程,仅仅贴图(图中只有代码)可能会导致“无个人工作,仅仅是复制粘贴”的极低分判定。
Love & Peace!
signal信号讲解
这一部分我主要参考了小破站博主的视频:
【深入理解计算机系统 实验4 CSAPP】Shell Lab 实现 CMU 详细讲解 shelllab哔哩哔哩bilibili
大多是对于代码的铺垫,如果不想看可以直接跳到实现tsh节;
1.1 Signal -> man 7 signal
singnal handler -> singnal
handler_t Signal_INT_HANDLER(){
}
可以自定义一些功能;
fork()
比较重要的一段阐释:A child created via fork(2) inherits a copy of its parent’s signal dispositions. During an execve(2),
易混淆的一个信号:kill
kill–发送一个信号,并不是直接将进程杀死
kill(pid, SIGINT); //this process
kill(-pid, SIGINT); //process group
wait for signal to be caught
Signal mask and Pending signals
信号屏蔽
block(SIGINT);
··
··// uninterruped by SIGINT
··
unblock(SIGINT);
A child created via fork(2) inherits a copy of its parent’s signal mask; the signal mask is preserved across ex‐ ecve(2).
function -> asyn-signal-safety
void handler(){
while(1){
print("hi there\n");
}
}
int main(){
while(1){
print("hello\n");
}
}
1.2 waitpid
等待进程改变状态
man waitpid 查看说明文档;
defination: wait for process to change state
-
terminated
-
stopped
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options){
}
A state change is considered to be:
-
the child terminated; 结束
-
the child was stopped by a signal; 停止
-
the child was resumed by a signal;恢复
In the case of a terminated child, performing a wait allows the system to release the resources associated with the child;
结束就及时回收,否则变僵尸:
if a wait is not performed, then the terminated child remains in a “zombie” state
pid
option
The value of options is an OR of zero or more of the following constants:
WNOHANG
return immediately
if no child has exited.
WUNTRACED also return if a child has
stopped
(but not traced via ptrace(2)). Status for traced children which have stopped is provided even if this option is not specified.
WCONTINUED (since Linux 2.6.10) also return if a stopped child has been
resumed
by delivery of SIGCONT.
wstatus
if(WIFEXITED(wstatus)){//exit(0), exit(1)
WEXITSTATUS(wstatus) //This macro should be employed only if WIFEXITED returned true.
}
else{
}
if(WIFSIGNALED(wstatus)){ // terminated by signal
int signal = WTERMSIG(wstatus);// sigkill, sigint
}
if(WIFSTOPPED(wstatus)){ // stopped by signal
}
if(WIFCONTINUED(wstatus)){ // resume
}
ECHILD -> no child process
A child that terminates, but has not been waited for becomes a “zombie”. The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child. As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes. If a parent process terminates, then its “zombie” children (if any) are adopted by init(1), (or by the nearest “subreaper” process as defined through the use of the prctl(2) PR_SET_CHILD_SUBREAPER operation); init(1) automatically performs a wait to remove the zombies.
1.3 kill
man kill 查看说明文档;
defination: send a signal to a process
The default signal for kill is TERM.
Particularly useful signals include HUP, INT, KILL, STOP, CONT, and 0.
1.4 sigprocmask
用于 signal blocking unblocking, setmask.
利用man sigprocmask 指令查看;
定义:审视和改变blocked signal
process -> set of blocked signals
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *_Nullable restrict newset, sigset_t *_Nullable restrict oldset);
函数参数:
how
-
SIG_BLOCK
-
SIG_UNBLOCK
-
SIG_SETMASK
!! A child created via fork(2) inherits a copy of its parent’s signal mask; the signal mask is preserved across execve(2).
阅读tsh
在CMU官方授课的这个章节中给出了充分的提示:
其次,在CMU官方的CSAPP课程网站中也有这一部分的PPT,在PPT上也有一些关于该实验的提示:
实现tsh
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.
* 每当用户在键盘上键入ctrl-c时,内核会向 shell 发送 SIGINT 信号。
* 捕获该信号并将其传递给前台作业。
*/
void sigint_handler(int sig) //#1
{
pid_t pid = fgpid(jobs);/* fgpid - Return PID of current foreground job, 0 if no such job */
if(pid != 0){
kill(-pid, sig);
}
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.
* 每当用户在键盘上键入 ctrl-z 时,内核会向 shell 发送 SIGTSTP 信号。
* 捕获该信号并通过发送 SIGTSTP 信号来暂停前台作业。
*/
void sigtstp_handler(int sig) //#2
{
pid_t pid = fgpid(jobs);/* fgpid - Return PID of current foreground job, 0 if no such job */
if(pid != 0){
kill(-pid, sig);
}
return;
}
sigchld_handler
/*
* 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.
* 每当子作业终止(成为僵尸进程)或停止,因为它接收到了 SIGSTOP 或 SIGTSTP 信号时,
* 内核会向 shell 发送 SIGCHLD 信号。
* 处理程序会回收所有可用的僵尸子进程,但不会等待任何其他当前正在运行的子进程终止。(即非阻塞式)
*/
void sigchld_handler(int sig) // 参考waitpid三个参数;#3
{
int oldErrno = errno;
int status;
pid_t pid;
while ((pid = waitpid(fgpid(jobs), &status, WNOHANG|WUNTRACED)) > 0) { //以下几种状态可以用man waitpid查看
if (WIFSTOPPED(status)){
//change state if stopped--停止
getjobpid(jobs, pid)->state = ST;
int jid = pid2jid(pid);
printf("Job [%d] (%d) Stopped by signal %d\n", jid, pid, WSTOPSIG(status));
}
else if (WIFSIGNALED(status)){
//delete is signaled--被信号杀死
int jid = pid2jid(pid);
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if (WIFEXITED(status)){
//exited--正常退出
deletejob(jobs, pid);
}
//还有一个continue的状态,不过我们不关心;
}
errno = oldErrno;
return;
}
函数中运用到了课本中对于waitpid的讲解,在课本P495-496:
waitfg
/*
* waitfg - Block until process pid is no longer the foreground process
阻塞,直到进程 PID 不再是前台进程。
*/
void waitfg(pid_t pid) //#4
{
struct job_t* job;
job = getjobpid(jobs,pid);
//check if pid is valid
if(job == NULL){
return;
}
if(job != NULL){
//spin--不断自旋,同时这样实现而不用waitpid也是防止waitpid可能带来的竞争情况;
while(pid==fgpid(jobs)){
}
}
return;
}
builtin_command
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
* 如果用户输入了内置命令,则立即执行该命令。
*/
int builtin_cmd(char **argv) //#5
{
if (!strcmp(argv[0], "quit")) {
exit(0);
}
else if (!strcmp("&", argv[0])){//就是一个特判
return 1;
}
else if (!strcmp("jobs", argv[0])) {
listjobs(jobs);
return 1;
}
else if (!strcmp("bg", argv[0]) || !(strcmp("fg", argv[0]))) {
//call bgfg
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
在所给的shelab-overview中:
do_bgfg
首先我们要明确bg和fg指令的作用;
/*
* do_bgfg - Execute the builtin bg and fg commands
执行内置的 bg 和 fg 命令。
*/
void do_bgfg(char **argv) //#6
{
struct job_t *job;
char *tmp;
int jid;
pid_t pid;
tmp = argv[1];
// if id does not exist
if(tmp == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
// if it is a jid
if(tmp[0] == '%') {
jid = atoi(&tmp[1]);
//get job
job = getjobjid(jobs, jid);
if(job == NULL){
printf("%s: No such job\n", tmp);
return;
}else{
//get the pid if a valid job for later to kill
pid = job->pid;
}
}
// if it is a pid
else if(isdigit(tmp[0])) { //对不合法行为的特判,比如bg 9999 fg a
//get pid
pid = atoi(tmp);
//get job
job = getjobpid(jobs, pid);
if(job == NULL){
printf("(%d): No such process\n", pid);
return;
}
}
else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//kill for each time
kill(-pid, SIGCONT);
if(!strcmp("fg", argv[0])) {
//wait for fg
job->state = FG;
waitfg(job->pid);
}
else{
//print for bg
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
job->state = BG;
}
return;
}
_eval
在教材P503有原版函数实现;
同时在shlab overview中:
针对代码中第42行开个新组的来源:
/*
* 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.
*
*
* 如果用户请求了一个内置命令(quit、jobs、bg 或 fg),则立即执行该命令。
* 否则,fork 一个子进程,并在子进程的上下文中运行作业。
* 如果作业在前台运行,等待它终止然后返回。
* 注意:每个子进程必须有一个唯一的进程组 ID,以便我们的后台子进程在键盘上按下 ctrl-c(ctrl-z)时不会从内核接收到 SIGINT(SIGTSTP)信号。
*/
void eval(char *cmdline)
{
char* argv[MAXARGS]; //execve()函数的参数
int state = UNDEF; //工作状态,FG或BG
sigset_t set;
pid_t pid; //进程id
// 处理输入的数据
if(parseline(cmdline, argv) == 1) //解析命令行,返回给argv数组
state = BG;
else
state = FG;
if(argv[0] == NULL) //命令行为空直接返回
return;
// 如果不是内置命令
if(!builtin_cmd(argv))
{
if(sigemptyset(&set) < 0)
unix_error("sigemptyset error");
if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
unix_error("sigaddset error");
//在它派生子进程之前阻塞SIGCHLD信号,防止竞争
if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
if((pid = fork()) < 0) //fork创建子进程失败
unix_error("fork error");
else if(pid == 0) //fork创建子进程
{
// 子进程的控制流开始
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0) //解除阻塞
unix_error("sigprocmask error");
if(setpgid(0, 0) < 0) //设置子进程id
unix_error("setpgid error");
if(execve(argv[0], argv, environ) < 0){
printf("%s: command not found\n", argv[0]);
exit(0);
}
}
// 将当前进程添加进job中,无论是前台进程还是后台进程
addjob(jobs, pid, state, cmdline);
// 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
unix_error("sigprocmask error");
// 判断子进程类型并做处理
if(state == FG)
waitfg(pid); //前台作业等待
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); //将进程id映射到job id
}
return;
}