【Linux】信号概念、信号产生、信号处理、信号三张表、可重入函数

信号就是软中断。

信号提供了异步处理事件的一种方式。例如,用户在终端按下结束进程键,使一个进程提前终止。

1、信号概念

每一个信号都有一个名字,它们的名字都以SIG打头。例如,每当进程调用了abort函数时,都会产生一个SIGABRT信号。

每一个信号对应一个正整数,定义在头文件

2、信号产生的场景

  • 当用户在终端按下特定的键时,会产生信号。例如,当用户按下DELETE按键(或Control-C)时,会产生一个中断信号(interrupt signal,SIGINIT),该信号使得一个运行中的程序终止。
  • 硬件异常可以产生信号。会引发硬件异常的情况如除以0,非法内存引用(invalid memory reference)等。这种情况会被硬件检测到,并通知内核,然后内核产生相应的信号通知对应的运行进程。例如,当一个进程执行了一个非法的内存引用,会触发SIGSEGV信号。
  • kill函数允许当前进程向其他的进程或者进程组发送任意的信号。当然,这种方法存在限制:我们必须是信号接收进程的所有者,或者我们必须是超级用户(superuser)。
  • kill命令的作用和kill函数类似。这个命令多用户杀死后台进程。
  • 软件异常可以根据不同的条件产生不同的信号。例如:网络连接中接受的数据超出边界时,会触发SIGURG信号。

对于进程来说,信号是随机产生的,所以进程不能简单地根据检测某个变量是否改变来判断信号是否发生,而应该告诉内核“当这个信号发生时,做下面的这些事情”。

3、信号的处理方式

对于进程来说,不能判别是否出现一个信号,而是必须要告诉内核信号出现的时候,执行下列操作。
信号的处理方式有三种:
1. 忽略此信号
2. 执行信号的默认处理动作。
3. 提供自定义行为,要求处理该信号的时候切换到用户态执行这个处理函数,也叫做捕捉一信号。

注:捕捉信号的时候需要注意不能捕捉SIGKILL信号和SIGSTOP信号。当捕捉到SIGCHLD信号,这个时候标识一个子进程已经终止,所以这个时候我们可以调用waitpid函数来取得该子进程的进程ID以及它的终止状态。

对于一些信号发生时,会造成进程终止,同时生成一个core文件,该core文件记录了该进程终止时的内存情况,可以帮助调试和调查进程的终止状态。

有几种情况不会生成core文件:
* 如果进程设置了suid位(chmod u+s file),并且当前用户不是程序文件的所有者;
* 如果进程设置了guid位(set-group-ID),并且当前用户不是程序文件的组所有者;
* 如果过户没有当前工作目录的写权限;
* 如果core文件已经存在,并且用户没有该文件的写权限;
该core文件太大(由参数RLIMIT_CORE限制)

4、产生信号

(1)终端产生信号
首先提出一个概念叫做 core dump,我想在linux下写c,肯定不少发现错误的时候报这个错误接下来我们先来看看这个东西到底是个什么。

core dump叫做核心转储,也叫做核心文件(core file),是操作系统在进程收到某些信号而终止运行时,将此时进程的地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件,这个信息我们常常用于调试程序。

默认的linux系统当中是不生成这个文件的,我们可以使用 ulimit -a 查看系统中这个文件的大小。
这里写图片描述
我们可以使用命令 ulimit -c xxxx 设置生成的core dump的大小。
这里写图片描述

默认情况下,生成的core dump文件的格式是core.xxx,后面一般都是pid。并且生成在当前目录下。

现在我们模拟生成一下这样的core dump文件,我们首先写出一个死循环。

int main()
{
    printf("hello world\n");
    while(1);

    return 0;
}

我们运行这个程序,然后操作,Ctrl+\,这样就会出现:

这里写图片描述
从上图我们可以看到我们操作过程中从键盘Ctrl+,这样就会产生一个信号SIGQUIT,这个信号传递给运行的进程,然后进程得到这个信号引发终止进程并引发核心转储。

接下来我们来看看如何利用这个coredump文件进行调试
我们直接gdb test文件和core文件就好8,在终端输入 gdb test core.6437 得到:
这里写图片描述

可以很快定位到错误之处。
(2)通过系统调用产生信号

我们可以通过系统调用来产生信号。这里我们先来看一下kill函数,

int kill(pid_t pid, int sig);

kill函数可以给指定的进程发送信号。
这个函数当中,第一个参数是进程的pid,第二个参数是我们需要发送给pid进程的一个信号的序号,比如我们传sig为9,那我们就发送信号SIGKILL。

接下来需要介绍的一个函数叫做raise函数 :

int raise(int sig);

这个函数是用来给当前进程发送信号的。

abort函数使得当前进程接收到信号而异常中止。

void abort(void);

这个函数会产生SIGABRT信号,这个信号是夭折信号。
(3)软件产生信号
软件产生信号这里我们首先来说一个函数alarm函数:

unsigned int alarm(unsigned int seconds);

里面的变量seconds所给的是一个时间,单位是秒。这个函数的意思就是类似闹钟的形式,alarm(1)的意思让操作系统在1秒钟以后结束这个进程alarm的默认行为动作就是终止这个进程。alarm函数的信号SIGALRM信号,这个信号的默认动作就是终止这个进程,当使用alarm(0)的意思就是取消以前设定的闹钟,返回值就是所剩余的时间。调用alarm函数会产生SIGALRM信号。

5、阻塞信号

(1)阻塞的概念
阻塞信号我们首先提出一些概念,

信号递达:正在执行信号处理的动作,

信号产生与信号递达之间叫做信号未决,也叫做pending。

当信号阻塞的时候不会递达,接触阻塞,信号才能递达。

关于信号,我们首先需要从内核的角度来看看信号。
在内核当中,当一个进程接收到信号,会对应的在进程的PCB当中有三个相关的结构,
这里写图片描述

因为我们现在有31个普通信号,所以这个时候我们可以想下我们前期所说的位图,我们也就可以利用一个整形就够了,每一个信号对应一个比特位。

另外因为是bit位,所以这里注意,即使你产生了多个信号,这里的信号位也只是从0变为1,不记录信号产生了多少次。

pending表标识信号未决表,表示信号是否产生,block阻塞表,表示当前进程与信号屏蔽相关内容。我们也把阻塞信号集叫做当前进程的信号屏蔽字。

注意阻塞和忽略是两回事,阻塞只是屏蔽了信号,而忽略是对信号的一种处理方式。

(2)信号集相关的函数
在linux下信号我们定义成为sigset_t类型的,sigset_t我们叫做信号集,这种类型经过我的测试大小是128个字节。
信号集下面有一些函数。

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); 

这里的函数都放在signal.h当中,sigemptyset函数用来初始化set所指向的信号集,使得信号集所有信号的对应的bit位清空。
sigfillset函数标识对set所指向的信号集的所有位进行置位操作。
注意,使用信号集之前一定得先试用sigemptyset或者是sigfillset进行初始化信号集。
sigaddset是对set所指向的信号集进行进行添加一个信号signo。
sigdelset函数是对信号集进行删除有效的信号。
sigismember函数是用来判断是否在set所指向的信号集当中包含signo信号。

说完看这些函数我们再说一个和信号屏蔽字相关的函数,sigprocmask函数,

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

这个函数是用来进行读取或者修改进程的信号屏蔽字这里的how说的是如何进行更改,set指向你要修改的当前信号屏蔽字,oldset指向修改前你的信号屏蔽字。
how参数:
这里写图片描述
注意:如果调用了sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少会将其中的一个信号递达。

接下来说另外的一个函数叫做sigpending,它用来输出pending表中的内容。

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void printfspending(sigset_t *set)
{
    int i=0;
    for(i=0;i<32;i++)
    {
        if(sigismember(set,i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
int main()
{
    sigset_t set,oset;
    sigemptyset(&set);
    printfspending(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,NULL);
    while(1)
    {
        sigpending(&oset);
        printfspending(&oset);
        sleep(1);
    }

    return 0;
}

我们可以从图片当中看到当我们按下Ctrl+c产生SIGINT信号的时候,这个时候就会在未决表改了对应的比特位。SIGINT信号是2号信号,修改了下标为2的位置的比特位。

6、捕捉信号

先来提出一个函数就叫做sigaction函数,这个函数可以修改和信号相关联的动作,实现信号的捕捉。

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

struct sigaction的定义:

struct sigaction 定义:
struct sigaction 
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

这里写图片描述

我们也可以使用signal函数可以实现这个功能。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

它的第一个参数是信号的编号,第二个参数是指向自定义函数的指针,就是当你捕捉到这个信号,不让它去做它的默认操作,而是去做你想要让它做函数,这个参数是一个返回值为void,参数为int的一个函数指针。

signal是C标准库提供的信号处理函数,

接下来说一说信号捕捉的时候的状态转换:

这里写图片描述

从上面这张图就可以看出整个状态的转换,

1.首先当你遇到中断、异常或者系统调用的时候进入内核态。
2.然后产生信号,这样由内核态切换用户态,这个过程当中需要去PCB检查那三张表,然后发现有递达的信号,然后这个时候就去处理信号对应的操作。也就是信号处理函数。
3.处理信号处理函数的时候,这个时候为了安全的问题,这个时候为用户态。
4.信号处理函数结束后,然后从用户态切换到内核态。
5.然后由内核态切换到中断异常执行处的用户态。

所以总共有4次状态的切换。

7、可重入函数

有了信号以后,会去调用喜好处理函数,这个时候你的程序就是异步执行,这个时候就引入了一个问题就是可重入函数的问题,

对于一个函数,当多个执行流进入函数,运行期间会出现问题的就叫做不可重入函数,不会出现的问题就是可重入函数。

信号捕捉函数内部禁止调入不可重入函数。

另外可重入函数还会和线程安全有联系:
1.线程安全不一定是可重入的,可重入的一定是线程安全的。
2.对全局变量或者公共资源进行多线程的进行访问的时候,则这个就既不是线程安全的也不是可重入。
3.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

四类不可重入函数u:
第一类:不保护共享变量的函数
第二类:保持跨越多个调用的状态函数
第三类:返回指向静态变量指针的函数。
第四类:调用线程不安全的函数。

可重入函数是线程安全函数的一种,特点在于它们被多个线程调用的时候,不会引用任何共享数据。

对于不可重入函数的处理,我们通常采用的方法就是重写函数。

另外就是有些以_r结尾的函数就是那个函数的可重入版本。

8、竞态条件

我们先来介绍一个pause函数。

int pause(void);

关于pause函数,是用来使得调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause函数不返回,如果处理动作是忽略,pause函数也不返回。如果处理动作是信号捕捉,则调用捕捉函数,然后返回-1。

然后这里我们使用alarm和pause模拟实现一个sleep函数。

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void sig_alarm(int signo)
{

}
void mysleep(int seconds)
{
    struct sigaction set,oset;
    set.sa_handler=sig_alarm;
    sigemptyset(&set.sa_mask);
    set.sa_flags=0;
    sigaction(SIGALRM,&set,&oset);
    //设置闹钟
    alarm(seconds);
    //这里闹钟到时间发送信号SIGALRM,然后执行信号处理函数,然后pause返回错误码-1,
    pause();
    unsigned int unslept=alarm(0);
    sigaction(SIGALRM,&oset,NULL);
}
int main()
{
    while(1)
    {
        mysleep(2);
        printf("2 seconds success\n");
    }
    return 0;
}

我们这个函数mysleep模拟了sleep函数。但是,我们需要思考一个问题就是在这里存在一个时序竟态的问题,当我们执行完alarm之后,别的进程会竞争夺走了CPU,夺走n秒后,SIGALRM递达了,然后n秒过后,这个时候就去执行pause,这样没有了信号,这样最终就是一直挂起。

所以我们要让alarm和pause的操作是原子的才行。

linux在这里给出了一个函数sigsuspend函数。

int sigsuspend(const sigset_t *mask);

1.通过mask来临时解除对某个信号的屏蔽
2.挂起等待
3.然后当sigsuspend返回的时候,这个时候恢复为原来的值

所以我们应该对这一段代码这样操作才行

    //首先屏蔽SIGALRM信号,不让它递达
    alarm(seconds);
    //解除屏蔽字,SIGALRM递达,
    pause();

所以我们先阻塞信号,保存当前信屏蔽字,然后直到最后进程回到我当前进程,然后我解除SIGALRM信号的屏蔽,这样信号就会递达这样就确保了alarm和pause之间的操作都是原子的。

而对于sigsuspend函数来说:sigsuspend用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。

9、SIGCHLD

最后我们来说一个信号,是SIGCHLD信号,这个信号是我们子进程终止的时候会给父进程传送这个信号。
SIGCHLD信号产生的条件:
1.子进程终止时
2.子进程收到SIGSTOP信号停止的时候。
3.子进程处在停止状态,接受到SIGCONT后唤醒。

父进程接收到了SIGCHLD信号,这个时候的默认动作是忽略,当然你可以去进行信号捕捉。我们能通过信号捕捉可以去处理其他。

猜你喜欢

转载自blog.csdn.net/weixin_38682277/article/details/80152783