my_shell中的信号处理函数

引言

shell这个程序完成已有三周了 但是今天又对shell中的一些部分有了新的理解 遂进行记录 也给后面写这个程序的朋友提供一些新的思路

信号处理函数是一个说容易容易 说麻烦麻烦的点 在shell中的基础要求是对 SIGINT 信号进行屏蔽 其中的坑点我已经在另外一篇博客进行分析

编写安全的信号处理函数

今天来说说另外一个值得注意的地方 进程执行完之后的处理 其实说白了就是处理僵尸进程 但是我们应该如何处理 我们又该在何时处理

我们先来看一个普通的版本

int pid; //保证信号处理函数中对全局变量的读写是有效的

void chld_handler(int signo)
{
    pid=waitpid(-1,NULL,0);
} 

void int_handler(int signo)
{
    //在简单shell的实现中 这个函数中需要进行输出
    //注意只能使用异步安全的函数 printf strlen都不是
    //参考另外一篇博客 
}

int main(int argc ,char **argv)
{
    signal(SIGCHLD,chld_handler);
    signal(SIGINT,int_handler);

    sigemptyset(&mask);
    sigaddset(&mask,SIGCHLD);//将信号加入屏蔽集
    
    while(1){
        if(fock()==0)
        {
            printf("子进程处理shell请求\n");
            exit(1);
        }
        pid=0;
        while(!pid)
            pasue();

        sigprocmask(SIG_SETMASK,&prev,NULL);//恢复信号屏蔽字
    }
    return 0;
}

我们先来解释下这段代码 当我们收到一个shell请求时我们需要去开一个进程去处理请求,我们可以看到子进程进入if 父进程进入while 为了少耗费系统资源而挂起程序 子进程结束后发送 SIGCHLD信号 触发父进程信号处理函数 改变pid 进入下一次循环 一切好像没什么问题 但实则问题百出。
 我们可以想象一种这样的情况 若父进程还未运行到 pid=0 子进程已经退出呢 这时会发生什么情况 信号处理函数已经触发 pid值已经改变(真的是这样吗 我们放到后面说) 运行到 pid=0 pid的值又发生改变 随即进入死循环  这种bug出现的概率其实是很低的 但是出现后往往极难发现 我们该如何解决呢 一种可行的方法是设置信号屏蔽集 在运行到while之前将可能造成影响的信号屏蔽 在进入循环后再解除屏蔽 这样就有效的避免了上述情况 我们再来看一段代码

int pid;

void chld_handler(int signo)
{
    pid=waitpid(-1,NULL,0);
}

void int_handler(int signo)
{
    //在简单shell的实现中 这个函数中需要进行输出
    //注意只能使用异步安全的函数 printf strlen都不是
    //参考另外一篇博客 
}

int main(int argc ,char **argv)
{
    sigset_t mask,prev; //型号集

    signal(SIGCHLD,chld_handler);
    signal(SIGINT,int_handler);

    sigemptyset(&mask);
    sigaddset(&mask,SIGCHLD);//将信号加入屏蔽集

    sigprocmask(SIG_BLOCK,&mask,&prev); //把当前屏蔽集设置为参数一二的并集
    //防止在fock之后 信号处理函数之前信号到达 那样程序就会永远阻塞
    while(1){
        if(fock()==0)
        {
            printf("子进程处理shell请求\n");
            exit(1);
        }
        pid=0;
        while(!pid){
        	sigprocmask(SIG_SETMASK,&prev,NULL);
        	pause();
        }

        sigprocmask(SIG_SETMASK,&prev,NULL);//恢复信号屏蔽字
    }
    return 0;
}

这样看起来就清晰多了 我们避免了while之前接收到SIGCHLD信号 这个信号形成未决队列 当我们解除屏蔽时 程序对其进行处理 从而进行下一轮处理 精彩! 完成了吗?我们再用刚才的思想去思考这个问题 再想象一种极端情况 信号在sigprocmask之后 pause之前发生了 这会发生什么呢 我们的pid虽然修改了(是吗?) 但是程序在pause处会发生阻塞 根本不会运行到while处了 处理方案是什么呢 就是sigsuspend 这个函数其实就是以下函数的缩写

sigprocmask(SIG_BLOCK,&mask,&prev);
pause();
sigprocmask(SIG_SETMASK,&prev,NULL);

是不是有种条件变量的感觉

那为什么我们不写成这三个函数呢 简洁当然是一个方面 还有一个重要原因是sigsusoend保证前两个操作为原子性 也就是说我们的第二个问题解决了 接下来 我们再来看一段代码

int pid;

void chld_handler(int signo)
{
    pid=waitpid(-1,NULL,0);
}

void int_handler(int signo)
{
    //在简单shell的实现中 这个函数中需要进行输出
    //注意只能使用异步安全的函数 printf strlen都不是
    //参考另外一篇博客 
}

int main(int argc ,char **argv)
{
    sigset_t mask,prev; //型号集

    signal(SIGCHLD,chld_handler);
    signal(SIGINT,int_handler);

    sigemptyset(&mask);
    sigaddset(&mask,SIGCHLD);//将信号加入屏蔽集

    sigprocmask(SIG_BLOCK,&mask,&prev); //把当前屏蔽集设置为参数一二的并集
    //防止在fock之后 信号处理函数之前信号到达 那样程序就会永远阻塞
    while(1){
        if(fock()==0)
        {
            printf("子进程处理shell请求\n");
            exit(1);
        }
        pid=0;
        while(!pid)
            sigsuspend(&prev);

        sigprocmask(SIG_SETMASK,&prev,NULL);//恢复信号屏蔽字
    }
    return 0;
}

这个函数离我们完全版已经很接近了 接下来我们再说最后两个问题 一就是pid的问题 我们前面一直在强调pid到底会变化吗 当然是不会的 你可能会奇怪 为什么呢 明明就是一个简单的全局变量 怎么会不改变呢 因为当程序对一个变量使用频繁的时候 它就会缓存在寄存器中 那我们修改的就只是寄存器中的副本 所以我们要每一次都从内存中读取 第二个问题就是多个进程同时访问一个全局变量 会发生什么呢 谁也不知道 这取决与内核调度算法 所以程序员要去确保稳定 那我们该怎么办呢 来介绍两个变量 volatile 与 sig_atomic_t 这两个变量是什么意思呢

  • volatile 每次从内存中读取变量
  • sig_atomic_t 保证对此函数读写均为原子操作

接下来就是我们的最终版了

//在这个程序中其实看到了信号与锁一样都是实现同步的几种方法之一 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<errno.h>
#include<signal.h>

volatile sig_atomic_t pid; //保证信号处理函数中对全局变量的读写是有效的
//volatile 保证每次从都从内存中读取
//sig_atomic_t 保证读写操作均为原子操作 也就是说不需要暂时屏蔽触发信号处理函数的信号 防止数据同时间被多次修改

void chld_handler(int signo)
{
    int old = errno; //在信号处理函数中有可能会修改全局变量errno 从而影响外面的函数 
    pid=waitpid(-1,NULL,0);
    errno = old;
} //防止子进程成为僵尸进程

void int_handler(int signo)
{
    //在简单shell的实现中 这个函数中需要进行输出
    //注意只能使用异步安全的函数 printf strlen都不是
    //参考另外一篇博客 
}

int main(int argc ,char **argv)
{
    sigset_t mask,prev; //型号集

    signal(SIGCHLD,chld_handler);
    signal(SIGINT,int_handler);

    sigemptyset(&mask);
    sigaddset(&mask,SIGCHLD);//将信号加入屏蔽集

    sigprocmask(SIG_BLOCK,&mask,&prev); //把当前屏蔽集设置为参数一二的并集
    //防止在fock之后 信号处理函数之前信号到达 那样程序就会永远阻塞
    while(1){
        if(fock()==0)
        {
            printf("子进程处理shell请求\n");
            exit(1);
        }
        pid=0;
        while(!pid)
            sigsuspend(&prev);

        sigprocmask(SIG_SETMASK,&prev,NULL);//恢复信号屏蔽字
    }
    return 0;
}

最近比较忙 没有去改以前的代码 这个就是最初版了
my_shell

posted @ 2022-07-02 13:18  李兆龙的博客  阅读(30)  评论(0编辑  收藏  举报