欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

操作系统(linux)中信号工作的原理分析

程序员文章站 2022-07-12 10:30:22
...

信号

首先我们先理解一下信号是什么?在linux下我们先看看都有那些信号,
我们输入kill -l 就会出现
操作系统(linux)中信号工作的原理分析
这就是信号的全部种类,总共有62种信号,其中1到31是普通信号,也是这篇主要理解的,后面34到64的信号为实时信号。

信号是干什么的呢?
我们举个例子:
最简单的理解,在linux下我们在运行某个进程的时候,通常在shell下启动一个前台进程,但是我们进程运行过程中我们,按下Ctrl+c,这个时候我们的进程就会停止或者说结束。其实,这就是信号起了作用,在我们按下Ctrl+c的时候,将会产生硬件中断,从而cpu转到内核去处理中处理,同时终端会将Ctrl+c被解析成一个SIGINT信号,发送给进程PBC,这个时候PCB中的会被记录下一个SIGINT信号,当某个时候cup从内核切回到用户空间时候,所以会处理这个进行中记录的信号,而这个信号的默认处理动作是结束进程。

信号的产生

关于信号的产生有三种情况:
1)键盘按键产生信号
键盘按键产生信号很好理解,比如我按的 Ctrl+c 产生信号SIGINT,按下Ctrl+z 产生SIGSTP 信号,Ctrl+\ 产生SIGOUT信号。这里要说一一下Ctrl+\ 这个产生信号,将程序终止掉后会产生core dump文件。

core dump文件干什么用的?core dump 是在程序运行过程中收到信号,或者在程序异常终止后,系统会把程序在内核中程序终止时候的信息保存到core dump文件中。我们可以用ulimit -c 来进行对core dump文件大小进行调整。在不修改的前提下默认的core dump文件的大小为0。
操作系统(linux)中信号工作的原理分析
2)调用系统函数
调用系统函数向进程发送信号,我们看看系统调用函数。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);

上面的两个函数,我们来解释一下,
kill函数两个参数,pid为给哪个进程要发信号,signo为哪中信号。
raise函数其实是kill函数的封装,raise函数是自己给自己发送任意信号。
上面两个函数都是成功返回1,失败返回0。

还有个函数,是c语言中的函数

#include <stdlib.h>
void abort(void);

abort函数是给自己发送abort信号。就像exit函数一样,但是abort总会成功。

3)软件产生信号
如果了解进程间的通信,我们就会知道有个匿名管道,如果我们在打开管道,如果打开写端关闭读端,系统会自动的发送一个SIGPIPE信号,关闭管道。

信号的阻塞

信号阻塞我们就需要了解信号在PCB中是怎么样的一种方式来表示它的信号。
在进程中当一个进程收到一个信号的时候,信号其实是先到该进程中的PCB中找到一个表,我们就叫未决信号表,其实是二进制表示的位图,先将 sigset_t 动作叫做信号递达。

我们用张图来理解:
操作系统(linux)中信号工作的原理分析
我们要注意

进程可以选择阻塞某个信号
如果在信号未决后,我们设置了阻塞,那么该信号会在未决表中等待解除阻塞
在信号中我们要明白,信号的阻塞和忽略不同,忽略是信号的处理方式,而阻塞只是信号在传递过程中,对信号的一种延后处理的行为。

信号操作函数

在这里我们了解几个信号集操作的函数

#include <signal.h>
int sigemptyset(sigset_t* set); // 将全部的比特为0
int sigfillset(sigset_t* set); // 全部比特为1
int sigaddset(sigset_t* set, int signo); // 在某一为上设置1
int sigdelset(sigset_t* set, int signo); // 某一位设置为0
int sigismember(const sigset_t* set, int signo);//查看是否被设置

这五个函数中,前面四个函数,都是成功返回1,失败返回0.
最后一个函数是一个bool的如果被设置返回1,没有返回0;

信号屏蔽

在看过对信号集操作函数后,我们就来看看信号阻塞函数。
函数不仅仅是可以阻塞信号,还可以读取和修改。

#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);

返回值是,成功返回0,失败则返回1;
参数说明,how是采用那种方式,set是输入型参数,oset是输出型参数。
这里的how有这么几个要参数:

  • SIG_BLOCK 相当于在原有的信号屏蔽字中添加新的屏蔽字。
  • SIG_UNBLOCK 解除set的当前信号屏蔽字,只解除set对应的。
  • SIG_SETMASK 设置当前屏蔽字,就不用管原来的字是什么,就设置成set

这里我们用一个例子来验证一下:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void Handle(int sig) // 当信号捕捉后,就会跳到这个函数中
{
    printf("sig = %d\n",sig);
}

void PrintSigSet(sigset_t* set)
{
    int i = 1;
    for (; i <= 31; ++i)
    {   
        if (sigismember(set,i)) 
        {   
            printf("1");
        }   
        else
        {   
            printf("0");
        }   
    }   
    printf("\n");
}

int main()
{
    // 捕捉SIGINT信号
    signal(SIGINT, Handle); // 捕捉函数
    // 再屏蔽SIGINT信号
    sigset_t set;
    sigemptyset(&set); // 把要设置的set全部bit位设置为0
    sigaddset(&set, SIGINT); // 添加要设置的位,SIGINT是2号信号
    sigprocmask(SIG_BLOCK, &set, NULL); // 把set设置到信号屏蔽字中
    // 循环读取未决信号集
    while (1)
    {
        sigset_t pending_set;

        //这个函数是读取未决信号集,且只能读,不能修改,
        //因为我们屏蔽了2号信号,所以在未决信号集中会在2号位置出现1
        sigpending(&pending_set);

        PrintSigSet(&pending_set);
        sleep(1);
    }                           
    return 0;
}

上面就是我们在用函数 sigprocmask 把信号的屏蔽字中的2号信号SIGINT设置为屏,所以我们在程序运行时候,观察未决信号集表中,当我们按下Ctrl+c时候,未决信号集表中2号位置,会变成1,当时就不会变回去,是因为我们没有解除对二号信号的屏蔽,当我们解除屏蔽,信号才能递达。

我们来看看结果:
操作系统(linux)中信号工作的原理分析

信号屏蔽字中对信号的解除

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void Handle(int sig) // 当信号捕捉后,就会跳到这个函数中
{
    printf("sig = %d\n",sig);
}

void PrintSigSet(sigset_t* set)
{
    int i = 1;
    for (; i <= 31; ++i)
    {   
        if (sigismember(set,i)) 
        {   
            printf("1");
        }   
        else
        {   
            printf("0");
        }   
    }   
    printf("\n");
}

int main()
{
    // 捕捉SIGINT信号
    signal(SIGINT, Handle); // 捕捉函数
    // 再屏蔽SIGINT信号
    sigset_t set; // 定义一个set用来设置信号屏蔽字
    sigemptyset(&set); // 把要设置的set全部bit位设置为0
    sigaddset(&set, SIGINT); // 添加要设置的位,SIGINT是2号信号
    sigprocmask(SIG_BLOCK, &set, NULL); // 把set设置到信号屏蔽字中
    // 循环读取未决信号集
    int i = 10;
    while (1)
    {
        sigset_t pending_set;

        //这个函数是读取未决信号集,且只能读,不能修改,
        //因为我们屏蔽了2号信号,所以在未决信号集中会在2号位置出现1
        sigpending(&pending_set);
        if (i == 0)
        {
            // 3秒后就会解除信号屏蔽字中的SIGINT,未决表中也会变成0
            // 解除信号屏蔽字
            sigprocmask(SIG_UNBLOCK, &set, NULL); 
            --i;
        }
        PrintSigSet(&pending_set);
        sleep(1);
    }                           
    return 0;
}

这里我们为了更清楚的演示信号被解除,就是信号未决表中的2号位置的1被置为0,所以我们不然程序立马收到SIGINT信号退出,所以我们用了信号捕捉。
我们来看看上面代码的运行结果:
操作系统(linux)中信号工作的原理分析
图中的1,是我们按下Ctrl+c产生SIGINT信号
途中的2,是我们再次按下Ctrl+c,为了验证我们用了信号的捕捉。
图中的3,是我们再10秒后信号解除函数执行,未决表中全为0,因为信号已经被处理。

信号的捕捉

在看完阻塞,我们还需要知道信号的的处理方式,信号的处理方式有三种:
1)忽略
2)默认
3)捕捉
信号的忽略我们可以用

#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);

其中hangdler是一个函数指针。
只要将handler参数填写成常数SIGIGN,就会忽略相对应的signum信号,但是在信号中会有几个函数是不能被忽略或者不能被捕捉。

我们主要介绍的是函数的捕捉。
函数捕捉,我们用图来了解一下在一个程序执行的过程中收到信号,然后程序自己进行对信号的捕捉的过程。
操作系统(linux)中信号工作的原理分析
上图中我们就可以看出,在捕获过程中进行了四次的用户与内核之间的交互。
要注意的是:
子啊主控制流的收到信号、异常、或者中断时候,会把当前进程的阻塞,一直等处理完信号。

SIGCHLD信号

这个信号我们来单独认识一下。
它在linux中用kill -l我们就可以看出来,是一个17号信号,为什么说它呢?
因为在对与父进程与子进程间的进程等待问题。
如果父进程没有读取子进程的返回码,就会产生僵尸进程,造成内存泄漏,所以父进程就需要进程等待。

这个信号会在子进程终止时候,父进程发送一个SIGCHLD信号,但是父进程对该信号默认处理的是忽略的,所以我们改变信号的处理方式为捕捉,在捕捉函数中我们就可以wait子进程的退出码。所以父进程就不必关心子进程会变成僵尸进程。

下面我们就用一段代码来具体实现一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void handler(int sig)
{
    (void)sig;
    while (1) 
    {   
        // 这里采用轮巡的等待方式是因为,有可能,
        // 在触发信号的子进程有很多
        // 但是未决信号表只能表现为一次,
        // 所以我们采用waitpid轮巡的去等待
        pid_t id = waitpid(-1, NULL, WNOHANG);
        if (id > 0)
        {
            printf("wait success\n");
        }   
        if (id < 0)
        {   
            break;
        }   
    }   
}
int main()
{
    signal(SIGCHLD, handler); // 用来接受信号并对其进行捕捉
    pid_t id = fork();
    if (id == 0)
    {
        printf("i am child\n");
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("i am father\n");
        sleep(1);
    }
    return 0;
}                       

我们来看代码的结果:
操作系统(linux)中信号工作的原理分析
我们开始创建father和child进程,当子进程3秒后,退出,触发sigchld信号,程序到捕捉函数中,执行完成后,又回到主线程中。

如有错误,多多指导,谢谢!