CSAPP:Shell Lab笔记

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);
  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 子进程当前停止

不阻塞轮询

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流程图

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

每次子进程状态的变化都会发送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没有被回收,原因是我只是杀了一个进程,而不是一个进程组

进程组

flowchart TD Shell --> |setgpid:0,0<br>表示由Parent fork出来的子进程均<br>属于同一个进程组|Parent[(&#34;进程组长,&#34;)] Parent --> |内部 fork|Child1[(&#34;子进程1<br>PID=102<br>PGID=101&#34;)] Parent --> |内部 fork|Child2[(&#34;子进程1<br>PID=102<br>PGID=101&#34;)] Parent --> |内部 fork|Child3[(&#34;子进程1<br>PID=102<br>PGID=101&#34;)] Parent[(&#34;进程组长<br>PID=101&#34;)]

对进程组操作,发送信号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);
}

参考资料

  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 调试.

相关推荐
会周易的程序员1 天前
Libnodave S7 通信库:架构设计与实现解析
linux·c++·物联网·架构·c·s7·工业协议
wdfk_prog5 天前
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计
java·linux·开发语言·面试·职场和发展·架构·c
shx_wei12 天前
从函数调用到进程替换:使用 execl、CMake 与 PowerShell 构建一个多可执行程序
linux·c语言·windows·c
piaoyiren12 天前
普中A6 STC8A 动态数码管显示讲解
嵌入式硬件·51单片机·c
C++ 老炮儿的技术栈14 天前
MFC 自定义纯色居中文字进度条控件
c语言·数据库·c++·windows·算法·c·visual studio
liulilittle15 天前
KCC 拥塞控制算法缺陷修复记录(26/07/05)
网络·tcp/ip·计算机网络·c·信息与通信·tcp·通信
半条-咸鱼1 个月前
【STM32】I2C协议原理、HAL读写与OLED显示操作
嵌入式硬件·c·信息与通信
Irissgwe1 个月前
map/set/multimap/multiset 的底层逻辑与实现
数据结构·c++·算法·二叉树·stl·c·红黑树
Irissgwe1 个月前
AVL树详解
数据结构·c++·算法·二叉树·c·二叉搜索树·avl