CSAPP:shell Lab笔记

CSAPP:shell Lab 笔记

该篇文章用于记录我在做shell实验的心得

intstrcmp(constchar*__s1,constchar*__s2);

该函数是在builtin_cmd函数用到,用于比较内置指令与输出指令的,常见的实现方式是s1的字典序-s2的字典序,只有为0时表示两字符串相等,而不是返回布尔值

pid_twaitpid(pid_tpid,int*status,intoption);
  1. 阻塞等待option=0

  2. 不阻塞option=WNOHANG

  3. pid=-1等待所有子进程终止

option含义
0阻塞等待
WNOHANG不阻塞等待,没有等待到,返回0
WUNTRACED除了等待子进程正常退出,如果子进程因为收到信号(如SIGSTOP)而暂停,waitpid也会立刻返回
WCONTINUE如果子进程之前是暂停状态,现在收到SIGCONT信号恢复执行,waitpid也会返回
WNOHANG | WNUTRACED立刻返回,检查子进程是否正常退出或暂停

由于option均是位掩码,故可以使用|进行任意组合

status含义
WIFEXITED子进程通过exit或者return退出为真
WIFSIGNALED子进程是因为一个未被捕获的信号终止的,如CTRL+C
WIFSTOPPED子进程当前停止

不阻塞轮询

pid_tret;intstatus;while(1){ret=waitpid(pid,&status,WNOHANG);if(ret==pid){printf("等待完成");break;}elseif(ret==0){// 避免CPU空转sleep(1);}else{fprintf(stderr,"出现错误");}}

保护操作不被信号打断的示例代码

sigset_tmask,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,出错-1sigismember(&mask,intsignum)

为了保证父进程添加jobs时,子进程在父进程后面执行完,(即父进程在这一时刻不会处理SIGCHILD信号

使用异步信号安全的函数

在信号处理函数中,无须使用信号屏蔽函数,sigprocmask是信号安全的,但是直接在信号处理函数中直接修改信号可能会导致死锁,直接使用waitpid

trace04 BUG

输入命令

# # trace04.txt - Run a background job. # /bin/echo -e tsh> ./myspin 1 \046 ./myspin 1 &

正确结果

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 &

我的结果

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

源码如下:

voidsigchld_handler(intsig){intstatus;intolderno=errno;pid_tpid;// 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

voidwaitfg(pid_tpid){// 如果相等,表示当前前台任务没有被回收while(fgpid(jobs)==pid){sleep(1);}}

trace05 BUG

输入命令

# # 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

正确结果

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 &

我的结果

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),下面是修改后的代码

voidsigchld_handler(intsig){intstatus;intolderno=errno;pid_tpid;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流程图

子进程 (myspin 3)子进程 (myspin 2)Shell (父进程)测试驱动 (sdriver.pl)子进程 (myspin 3)子进程 (myspin 2)Shell (父进程)测试驱动 (sdriver.pl)脚本模式(极速并发,触发 EINTR)addjob() 将 JID 设为 1就在 fork() 系统调用内部(复制内存、分配 PID 时)中断发生!fork() 被信号打断,返回 -1内核设置 errno = EINTR (4)C3 变成孤儿进程被 init 收养1. 发送 ./myspin 2 &fork() 创建后台任务2. 几乎同时发送 ./myspin 3 &开始 fork() 创建 C33. myspin 2 运行结束内核发送 SIGCHLD 信号4. 立即跳转执行 sigchld_handlerwaitpid() 尝试回收(因 C2 已死,返回 -1, errno=ECHILD 或保持 EINTR)Shell 进程崩溃测试中断,无 [2] 输出

每次子进程状态的变化都会发送SIGCHLD,进入sigchld_handler程序 ,但是SIG_CHLD只占有一个bit位,也就是说不体现次数,这也是使用while回收子进程的原因,一次性回收。更完善处理函数如下:

voidsigchld_handler(intsig){intstatus;intolderno=errno;pid_tpid;while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0){if(WIFEXITED(status)||WIFSIGNALED(status))deletejob(jobs,pid);elseif(WIFSTOPPED(status)){structjob_t*job=getjobpid(jobs,pid);job->state=ST;}}errno=olderno;}

trace11 BUG

输入

# # 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

正确答案

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

我的答案

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没有被回收,原因是我只是杀了一个进程,而不是一个进程组

进程组

setgpid:0,0
表示由Parent fork出来的子进程均
属于同一个进程组

内部 fork

内部 fork

内部 fork

Shell

进程组长
PID=101

子进程1
PID=102
PGID=101

子进程1
PID=102
PGID=101

子进程1
PID=102
PGID=101

对进程组操作,发送信号kill(-pid,sig)

信号处理函数职责单一原则

/***************** * 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. */voidsigchld_handler(intsig){intstatus;intolderno=errno;pid_tpid;while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0){if(WIFEXITED(status)){deletejob(jobs,pid);}elseif(WIFSIGNALED(status)){chars[]="Job [&] (&) terminated by signal 2\n";inti[]={pid2jid(pid),pid};sio_putsif(s,i);deletejob(jobs,pid);if(write(STDOUT_FILENO,s,sio_strlen(s))<sio_strlen(s))_exit(0);}elseif(WIFSTOPPED(status)){chars[]="Job [&] (&) stopped by signal 20\n";inti[]={pid2jid(pid),pid};sio_putsif(s,i);if(write(STDOUT_FILENO,s,sio_strlen(s))<sio_strlen(s))_exit(0);structjob_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. */voidsigint_handler(intsig){pid_tpid=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. */voidsigtstp_handler(intsig){pid_tpid=fgpid(jobs);if(pid<=0)return;kill(-pid,SIGTSTP);}

参考资料

  1. 教材:Randal E. Bryant, David R. O’Hallaron.Computer Systems: A Programmer’s Perspective(Third Edition). 第 8 章《异常控制流》(Exceptional Control Flow).
  2. 实验来源:Carnegie Mellon University (CMU) 15-213 / 18-213 / 15-513:Introduction to Computer Systems. Lab Assignment L5:Writing Your Own Unix Shell (shlab).
  3. 环境:Ubuntu 24.04 / GCC / VSCode + GDB 调试.