Linux:浅析信号(一)

本文深入浅出地介绍了Linux系统中的信号机制,包括信号的产生方式、处理动作以及信号的阻塞概念。并通过实例展示了如何使用信号来控制进程的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

信号,这个元素在我们的生活中可是非常常见的,比如说是红绿灯,比如说是手机铃声。所谓信号就是在人或事情感受到这个元素产生以后会做出相应的处理动作,这就是信号。而在我们Linux下,什么是信号呢?

我们知道进程结束情况有三个,也在前面关于进程的博客中说了,分别就是诸葛亮、司马懿、周瑜。先不谈诸葛亮与司马懿的情况,就说说周瑜这个情况。
进程出现周瑜这个情况无例外肯定都是进程崩了,然后挂掉了,进程结束。而进程怎么崩的呢?比如说,我们代码中出现野指针,这个野指针非法访问不该访问的内存,这时候操作系统能忍?肯定立马把这个进程杀掉!相当于外面一个陌生人,在你家里瞎吃瞎喝,折腾一遭,你能忍?换我我忍不了!那你怎么做?肯定把他赶出去呀。这就相当于操作系统挂掉进程的过程,而这个过程中操作系统怎么让进程挂掉的呢?就是发送信号!!!!


信号的产生

在Linux终端下,我们常常在键盘上输入Ctrl-C来终止某个进程(这个按键只能用于终止前台进程,如果运行进程之前加上&那这个进程就到了后台运行,那就没效果了),而我们的Ctrl-C是怎么让进程终止的呢?其实这就是在用户按下Ctrl-C的时候,由操作系统向进程发送SIGINT信号,然后该进程对这个信号进行了响应,进而进程终止。其实我们在谈到进程状态的时候,其中谈到kill -9这个操作,这个操作实质也是一种信号。

在Linux下其实信号的产生无非就是四种大类型:

  1. 按键组合,比如说Ctrl-C与Ctrl-/这都是按键组合
  2. 硬件异常,比如是野指针问题
  3. 操作系统指令:比如kill命令
  4. 软件调用:比如说在我们进程间通信的时候管道通信时,如果管道一端不进行写入,并且关闭了写操作,那么这时候就会发送信号SIGPIPE,来终止进程。还有就是alarm函数也是这样(后面会说alarm函数)

按键组合产生信号

我们在终端通过按下组合键位产生相应的信号来发送给进程,这就是按键组合产生信号的原理。这里不做过多赘述。
这里写图片描述
我们写下一个死循环,接着运行它,最后利用Ctrl-C去结束它,这个过程就是对进程发送信号的过程。

硬件异常产生信号

要说硬件异常而产生的信号的话,我们先来进行一个实践。

这里写图片描述
这里我们看到了段错误(由于本人Linux安装了中文版所以这里显示错误为段错误(吐核)),其实这里的段错误就是Core Dump,我们平常在写代码中也常常会遇到这个错误。而当操作系统发现到代码中有这个问题的时候,操作系统就会向进程发送信号,SIGQUIT来终止这个进程。所以这时候就会提示Core Dump。

Core Dump

Core Dump又称核心转储,当我们进程遇到问题,收到操作系统发送的信号,需要终止进程时,可以选择把进程的用户空间的数据全部保存到磁盘上,文件名通常是core开头的,后面加上进程的pid,这就叫做Core Dump。
进程异常终止,常常是因为有Bug的存在,如前面所说的野指针非法访问内存,导致的段错误,事后我们可以用调试器来检查core文件,找到进程错误的原因。
一个进程允许产生多大的core文件取决于进程Recource Limit(这个信息保存在PCB当中),在Linux操作系统下是默认不允许产生core文件的,因为core文件中可能包含着用户的密码等敏感信息,是不安全的。不过为了我们的实践,我们可以利用指令ulimit来改变这个限制,允许让其产生core文件。
演示如下:
这里写图片描述

我们发现,在修改了ulimit之后,运行刚才的段错误文件,这时候产生了一个core文件。我们可以看看这个core文件多大。
这里写图片描述
可以看到,这个core文件是相当的大。所以系统默认将其关闭是有原因的。
如果一旦出现段错误,然后产生core文件后,我们就可以用gdb test core.41011来进行调试,最终找到错误。

操作系统指令产生信号

说到操作系统指令产生信号,我们就不由的想到了kill指令,首先来看看kill指定都有哪些选项:
这里写图片描述
这里我们主要学习的是前三十二个指令,从1到31。其实kill指令的实质是调用了kill函数来实现的,kill函数可以给一个指定的进程发送指定的信号。而raise函数可以给函数调用者发送指定信号。

#include <signal.h>
int kill(pid_t pid, int signo);
in raise(int signo);
//这两个函数都是成功返回0,失败返回-1

void abort(void);
//这个函数如同exit函数一样,永远成功,没有返回值。它可以使当前进程接收到信号而异常终止

软件调用函数产生信号

在这里主要介绍的是alarm函数,调用alarm函数可以在指定的时间内开始倒计时,时间到达,给进程发送SIGALRM信号,进而终止进程。

#include <unsitd.h>
unsigned int alarm(unsigned int seconds);
//调用alarm相当于给进程设定了一个闹钟,闹钟时间到达的时候,便会发送信号,其信号默认动作是终止当前进程

这个函数的返回值是0或者是以前设定的闹钟还剩余的秒数。比如说,闹钟预定时间为20秒,不过在闹钟还没有到达20秒的时候,这时候人为的向进程发生SIGALRM信号,这时候进程终止,但是并不是由于闹钟的原因终止的,所以这时候闹钟的返回值就为其剩余的秒数。

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

int main()
{
  alarm(1);
  while(1)
  {
    printf("一秒过去了\n");
    sleep(1);
  }

  return 0;
}

这里写图片描述

信号产生后的处理动作

在信号产生以后,一般进程会有三种处理动作

  • 执行该信号的默认处理动作
  • 忽略此信号
  • 自定义动作,即信号捕捉

这里介绍一个信号捕捉的函数signal

sighandler_t signal(int signum, sighandler_t handler);
//这个函数第一个参数为要捕捉的信号,第二个参数为要执行的默认动作,这是一个函数指针

示例:

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

void handler(int signo)
{
  printf("get signo %d\n",signo);
}

int main()
{
  signal(2,handler);
  while(1)
  {
    printf("runing!\n");
    sleep(1);
  }

  return 0;
}

这里写图片描述

这里我们对二号信号进行了捕捉,二号信号为按键输入Ctrl-C便可产生。发现这时候并不执行二号信号应该有的动作终止进程,而改为了执行自定义动作,这就完成了一次信号捕捉。但是这里要说的是,并不是所有信号都能够捕捉,其中kill -9九号信号是无法捕捉的,九号信号是程序员的底线,不容侵犯,底线哪里是说能捕捉就能捕捉的呢?

阻塞信号

在前面我们讲述了信号的产生,信号的处理动作,这些都过于口头化了,接下来我们细细的捋一捋信号的产生到处理的过程,这里面会引入一些新的名词。

信号可以由按键组合、软件调用、硬件异常、操作系统产生这四种方式产生,产生信号后,操作系统会在进程的PCB中修改一个名叫信号位图上的内容,信号位图可以理解成一个拥有31个比特位的位图,每一个信号都对应一个比特位,所以是31个比特位,而操作系统发信号给进程,实质上是经过某些调用改变信号位图上对应比特位由0改为1,这样信号就发送了。而这个时候信号处于未决状态(从产生到修改位图上的对应比特位),当进程收到信号时,会在合适的时间进程处理,实际上进程去执行信号的动作称为信号的递达。而进程可以选择阻塞某一个信号,当信号被进程阻塞时,操作系统向进程发送信号,此时信号只会未决而永远不肯递达,直到解除阻塞。
注:这里的阻塞与前面信号处理动作中的忽略不同,忽略的时候信号已经递达,而阻塞根本没有递达!!!!

这里写图片描述

实际上,在PCB中存放着两个位图,一个是递达位图另外一个则阻塞位图,如上图,从上至下分别是一号信号到31号信号,此时二号信号已经未决,所以它的二号比特位由0变为1,意味着操作系统已经向该进程发送了信号,而进程对二号信号进行进行了阻塞,那么这个时候二号信号是永远不能递达的,因为受到了进程的阻塞,除非解除阻塞,才能够递达,如果一直无法递达,那么未决表上二号比特位永远都是1。
而第三张表示handler表,它就是进程在递达时所能够执行的动作,SIG_DFL为默认动作,SIG_IGN为忽略该信号,最后的handler则为自定义动作。

现在一个问题来了,倘若在二号信号被阻塞时,如果操作系统不断的向进程发送二号信号,由于被阻塞,所以二号信号无法被递达,所以pending表上面二号比特位也一直是1,这时候不断发送的其他信号呢?该如何改变比特位?

Linux是这样实现的:常规信号在递达之前如果产生多次同一种信号,那么只记一次,如果是实时信号,那么将这些多次产生的实时信号进行保存,保存到一个链式队列内。

sigset_t

从我们上面的图来看,无论是未决表还是阻塞表,它们只有一个比特位来判断是否未决或阻塞,即0或1,并不记录该信号产生了多少次,阻塞标志也是这样的。这个时候引入一个新的数据类型叫做sigget_t,这个数据类型称作信号集,这个类型可以表示每个信号的有效或者是无效的状态,在未决信号集中,有效与无效的含义代表着该信号是否处于未决状态,而阻塞信号集中,有效与无效代表着该信号是否被阻塞。阻塞信号集也叫作当前进程的信号屏蔽字,这里的屏蔽意为阻塞。

信号集操作函数

sigset_t信号集中,每个信号只用一个比特位来表示,0与1代表它的信号是否未决或阻塞,我们并不关心它的内部是如何存储的,但是我们对它们的操作只能运用信号集操作函数来操作。而无法直接对其内部的元素进行访问操作。

#include <signal.h>

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);

sigemptyset函数是初始化set所指向的信号集,使set所指向的信号集内所有比特位设置为0,表示该信号集不包含任何信号。
sigfillset函数与sigemptyset函数恰好相反,它将set所指向的信号集内全部比特位设置为1,也就是表示该信号集内所有信号都有效
在使用sigset_t类型的变量之前,必须对其进行初始化,这样可以保证其内部的比特位是确定的,方便后面的操作,初始化之后便可以使用sigaddset或sigdelset函数,对信号集中添加或者删除某种有效信号。
sigismember是一个布尔函数,它可以判断某个信号集中的有效信号是否包含某个信号,如果包含返回1,不包含返回0,出错返回-1.。

sigprocmask函数

#include <signal.h>

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

作用:读取或者更改进程信号集内的屏蔽字(阻塞信号集)
返回值:如果成功则为0,如果出错返回-1
参数:
how:为设置选项,可以选择对信号集的操作动作是什么。
set:如果set为非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
oset:作用是将之前的屏蔽字备份至oset,属于输出型参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值