CSAPP:shell Lab 笔记
该篇文章用于记录我在做shell实验的心得
c
int strcmp(const char *__s1,const char *__s2);
该函数是在builtin_cmd函数用到,用于比较内置指令与输出指令的,常见的实现方式是s1的字典序-s2的字典序,只有为0 时表示两字符串相等,而不是返回布尔值。
c
pid_t waitpid(pid_t pid,int *status,int option);
-
阻塞等待
option=0 -
不阻塞
option=WNOHANG -
pid=-1等待所有子进程终止
| option | 含义 |
|---|---|
0 |
阻塞等待 |
WNOHANG |
不阻塞等待,没有等待到,返回0 |
WUNTRACED |
除了等待子进程正常退出,如果子进程因为收到信号(如SIGSTOP)而暂停,waitpid也会立刻返回 |
WCONTINUE |
如果子进程之前是暂停状态,现在收到SIGCONT信号恢复执行,waitpid也会返回 |
| `WNOHANG | WNUTRACED` |
由于option均是位掩码,故可以使用|进行任意组合
| status | 含义 |
|---|---|
WIFEXITED |
子进程通过exit或者return退出为真 |
WIFSIGNALED |
子进程是因为一个未被捕获的信号终止的,如CTRL+C |
WIFSTOPPED |
子进程当前停止 |
不阻塞轮询
c
pid_t ret;
int status;
while(1)
{
ret = waitpid(pid,&status,WNOHANG);
if(ret == pid)
{
printf("等待完成");
break;
}else if(ret == 0)
{
// 避免CPU空转
sleep(1);
}else{
fprintf(stderr,"出现错误");
}
}
保护操作不被信号打断的示例代码
c
sigset_t mask,prev_mask;
sigfillset(&mask); // 位掩码,对所有位填充1,表示拦截所有信号
// 与他相反的是sigemptyset,对所有位置0
// 配合sigaddset(&mask,SIGINT),添加信号到集合中
// sigdelset(&mask,SIGINT),删除在集合中的指定信号
// SIGINT 是一个例子
sigprocmask(SIG_BLOCK,&mask,&prev_mask);
// 临界区代码,保证临界区代码不会被信号打断,除了SIGKILL,SIGSEGV(段错误)
sigprocmask(SIG_SETMASK,&prev,NULL);
// 上面所有函数,成功返回0,失败返回-1
// 查找集合内是否有该信号,有返回1,没有0,出错-1
sigismember(&mask,int signum)
为了保证父进程添加jobs时,子进程在父进程后面执行完,(即父进程在这一时刻不会处理SIGCHILD信号
使用异步信号安全的函数
在信号处理函数中,无须使用信号屏蔽函数,sigprocmask是信号安全的,但是直接在信号处理函数中直接修改信号可能会导致死锁,直接使用waitpid
trace04 BUG
输入命令
textile
#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &
正确结果
textile
pol@pol-Legion-Y7000P-IRX9:~/桌面/shlab-handout$ make rtest04
./sdriver.pl -t trace04.txt -s ./tshref -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[1] (6380) ./myspin 1 &
我的结果
textile
pol@pol-Legion-Y7000P-IRX9:~/桌面/shlab-handout$ make test04
./sdriver.pl -t trace04.txt -s ./tsh -a "-p"
#
# trace04.txt - Run a background job.
#
tsh> ./myspin 1 &
[2] (6119) ./myspin 1 &
可以看到,jid分配错误,所以我怀疑执行echo进程没有被回收,通过dedug,进入sigchld_handler,发现waitpid返回值始终是-1,而errno的值是ECHILD。
源码如下:
c
void sigchld_handler(int sig)
{
int status;
int olderno = errno;
pid_t pid;
// char err_msg[] = "sigchild_handler\n";
do{
pid = waitpid(-1,&status,0);
deletejob(jobs,pid);
}while(pid>0);
// while((pid = waitpid(-1,&status,WNOHANG | WUNTRACED))>0){
// deletejob(jobs,pid);
// }
if(errno != ECHILD){
// write(STDOUT_FILENO,err_msg,sizeof(err_msg));
_exit(1);
}
errno = olderno;
}
本质原因是因为waitfg中调用了一次waitpid,而且还是阻塞调用,导致了信号处理函数在调用waitpid时,自然拿不到pid,但又必须等待前台任务返回后执行,故修改后采用了busy loop with sleep
c
void waitfg(pid_t pid)
{
// 如果相等,表示当前前台任务没有被回收
while(fgpid(jobs) == pid){
sleep(1);
}
}
trace05 BUG
输入命令
textile
#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &
/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &
/bin/echo tsh> jobs
jobs
正确结果
textile
pol@pol-Legion-Y7000P-IRX9:~/桌面/shlab-handout$ make rtest05
./sdriver.pl -t trace05.txt -s ./tshref -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (16002) ./myspin 2 &
tsh> ./myspin 3 &
[2] (16004) ./myspin 3 &
tsh> jobs
[1] (16002) Running ./myspin 2 &
[2] (16004) Running ./myspin 3 &
我的结果
textile
pol@pol-Legion-Y7000P-IRX9:~/桌面/shlab-handout$ make test05
./sdriver.pl -t trace05.txt -s ./tsh -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[1] (13301) ./myspin 2 &
tsh> ./myspin 3 &
会发现在执行./myspin 3 &时,shell进程被杀死了,其原因在sigchld_handler函数,在回收进程时,由于对全局变量errno判断,导致了错误触发了_exit(1),下面是修改后的代码
c
void sigchld_handler(int sig)
{
int status;
int olderno = errno;
pid_t pid;
while((pid = waitpid(-1,&status,WNOHANG | WUNTRACED))>0){
deletejob(jobs,pid);
}
// if(errno != ECHILD){
// write(STDERR_FILENO,err_msg,sizeof(err_msg));
// char buf[64];
// int len = sprintf(buf, "errno = %d\n", errno);
// write(STDERR_FILENO, buf, len);
// _exit(1);
// }
errno = olderno;
}
在多进程中,不轻易对全局变量(如errno)判断
下面是导致trace05 BUG流程图
每次子进程状态的变化都会发送SIGCHLD,进入sigchld_handler程序 ,但是SIG_CHLD只占有一个bit位,也就是说不体现次数,这也是使用while回收子进程的原因,一次性回收。更完善处理函数如下:
c
void sigchld_handler(int sig)
{
int status;
int olderno = errno;
pid_t pid;
while((pid = waitpid(-1,&status,WNOHANG | WUNTRACED))>0){
if(WIFEXITED(status) || WIFSIGNALED(status))
deletejob(jobs,pid);
else if(WIFSTOPPED(status)){
struct job_t* job = getjobpid(jobs,pid);
job->state = ST;
}
}
errno = olderno;
}
trace11 BUG
输入
textile
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4
SLEEP 2
INT
/bin/echo tsh> /bin/ps a
/bin/ps a
正确答案
textile
pol@pol-Legion-Y7000P-IRX9:~/桌面/shlab-handout$ make rtest11
./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
tsh> ./mysplit 4
Job [1] (13795) terminated by signal 2
tsh> /bin/ps a
PID TTY STAT TIME COMMAND
2675 tty2 Ssl+ 0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
2677 tty2 Sl+ 3:58 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
2811 tty2 Sl+ 0:00 /usr/libexec/gnome-session-binary --session=ubuntu
3888 pts/0 Ss+ 0:00 bash
4135 pts/1 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
4389 pts/2 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
4616 pts/3 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
5319 pts/4 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
6257 pts/5 Ss 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
13790 pts/5 S+ 0:00 make rtest11
13791 pts/5 S+ 0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
13792 pts/5 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tshref -a -p
13793 pts/5 S+ 0:00 ./tshref -p
13813 pts/5 R 0:00 /bin/ps a
我的答案
textile
pol@pol-Legion-Y7000P-IRX9:~/桌面/shlab-handout$ make test11
./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
tsh> ./mysplit 4
Job [1] (13932) terminated by signal 2
tsh> /bin/ps a
PID TTY STAT TIME COMMAND
2675 tty2 Ssl+ 0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
2677 tty2 Sl+ 4:02 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
2811 tty2 Sl+ 0:00 /usr/libexec/gnome-session-binary --session=ubuntu
3888 pts/0 Ss+ 0:00 bash
4135 pts/1 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
4389 pts/2 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
4616 pts/3 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
5319 pts/4 Ss 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
6257 pts/5 Ss+ 0:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
13927 pts/4 S+ 0:00 make test11
13928 pts/4 S+ 0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
13929 pts/4 S+ 0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tsh -a -p
13930 pts/4 S+ 0:00 ./tsh -p
13933 pts/4 S 0:00 ./mysplit 4
13950 pts/4 R 0:00 /bin/ps a
可以看到我的答案./mysplit 4没有被回收,原因是我只是杀了一个进程,而不是一个进程组
进程组
对进程组操作,发送信号kill(-pid,sig)
信号处理函数职责单一原则
c
/*****************
* Signal handlers
*****************/
/*
* 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 status;
int olderno = errno;
pid_t pid;
while((pid = waitpid(-1,&status,WNOHANG | WUNTRACED))>0){
if(WIFEXITED(status)){
deletejob(jobs,pid);
}else if(WIFSIGNALED(status)){
char s[] = "Job [&] (&) terminated by signal 2\n";
int i[] = {pid2jid(pid),pid};
sio_putsif(s,i);
deletejob(jobs,pid);
if(write(STDOUT_FILENO,s,sio_strlen(s))<sio_strlen(s))
_exit(0);
}else if(WIFSTOPPED(status)){
char s[] = "Job [&] (&) stopped by signal 20\n";
int i[] = {pid2jid(pid),pid};
sio_putsif(s,i);
if(write(STDOUT_FILENO,s,sio_strlen(s))<sio_strlen(s))
_exit(0);
struct job_t* job = getjobpid(jobs,pid);
job->state = ST;
}
}
errno = olderno;
}
/*
* 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)
{
pid_t pid = fgpid(jobs);
if(pid<=0)
return;
kill(-pid,SIGINT);
}
/*
* 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)
{
pid_t pid = fgpid(jobs);
if(pid<=0)
return;
kill(-pid,SIGTSTP);
}
参考资料
- 教材 :Randal E. Bryant, David R. O'Hallaron. Computer Systems: A Programmer's Perspective (Third Edition). 第 8 章《异常控制流》(Exceptional Control Flow).
- 实验来源 :Carnegie Mellon University (CMU) 15-213 / 18-213 / 15-513: Introduction to Computer Systems . Lab Assignment L5: Writing Your Own Unix Shell (shlab).
- 环境:Ubuntu 24.04 / GCC / VSCode + GDB 调试.