Fork me on GitHub

Linux信号

今天我们说一说Linux下信号的概念和产生~

信号的概念

在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
简而言之,信号用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:

  • 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
  • 第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。
  • 第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信 号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
  • 在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。

那么信号在什么条件下产生呢?

信号的产生

  1. 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
  2. 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误,比如除以0。
  3. 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
  4. 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
  5. 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。当然在终端也可以用kill指令,这种方法存在限制:我们必须是信号接收进程的所有者,或者我们必须是超级用户(root)。
  6. 与终端交互相关的信号。如用户关闭一个终端,或按下ctrl+c键等情况。
  7. 跟踪进程执行的信号。

我们可以用kill -l指令查看信号列表

超过34的信号是实时信号,我们暂时不讨论
下面是Linux下的信号表(内容来自百度)

信号 处理动作 发出信号的原因
SIGHUP 1 A 终端挂起或者控制进程终止
SIGINT 2 A 键盘中断(如break键被按下)
SIGQUIT 3 C 键盘的退出键被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)发出的退出指令
SIGFPE 8 C 浮点异常
SIGKILL 9 AEF Kill信号
SIGSEGV 11 C 无效的内存引用
SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道
SIGALRM 14 A 由alarm(2)发出的信号
SIGTERM 15 A 终止信号
SIGUSR1 30,10,16 A 用户自定义信号1
SIGUSR2 31,12,17 A 用户自定义信号2
SIGCHLD 20,17,18 B 子进程结束信号
SIGCONT 19,18,25 / 进程继续(曾被停止的进程)
SIGSTOP 17,19,23 DEF 终止进程
SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键
SIGTTIN 21,21,26 D 后台进程企图从控制终端读
SIGTTOU 22,22,27 D 后台进程企图从控制终端写

下面的信号没在POSIX.1中列出,而在SUSv2列出

信号 处理动作 发出信号的原因
SIGBUS 10,7,10 C 总线错误(错误的内存访问)
SIGPOLL / A Sys V定义的Pollable事件,与SIGIO同义
SIGPROF 27,27,29 A Profiling定时器到
SIGSYS 12,-,12 C 无效的系统调用 (SV/ID)
SIGTRAP 5 C 跟踪/断点捕获
SIGURG 16,23,21 B Socket出现紧急条件(4.2 BSD)
SIGVTALRM 26,26,28 A 实际时间报警时钟信号(4.2 BSD)
SIGXCPU 24,24,30 C 超出设定的CPU时间限制(4.2 BSD)
SIGXFSZ 25,25,31 C 超出设定的文件大小限制(4.2 BSD)

(对于SIGSYS,SIGXCPU,SIGXFSZ,以及某些机器体系结构下的SIGBUS,Linux缺省的动作是A (terminate),SUSv2 是C (terminate and dump core))
  下面是其它的一些信号

信号 处理动作 发出信号的原因
SIGIOT 6 C IO捕获指令,与SIGABRT同义
SIGEM 7,-,7 / /
SIGSTKFLT -,16,- A 协处理器堆栈错误
SIGIO 23,29,22 A 某I/O操作现在可以进行了(4.2 BSD)
SIGCLD -,-,18 A 与SIGCHLD同义
SIGPWR 29,30,19 A 电源故障(System V)
SIGINFO 29,-,- A 与SIGPWR同义
SIGLOST -,-,- A 文件锁丢失
SIGWINCH 28,28,20 B 窗口大小改变(4.3 BSD, Sun)
SIGUNUSED -,31,- A 未使用的信号(will be SIGSYS)

(在这里,- 表示信号没有实现;有三个值给出的含义为,第一个值通常在Alpha和Sparc上有效,中间的值对应i386和ppc以及sh,最后一个值对应mips。信号29在Alpha上为SIGINFO / SIGPWR ,在Sparc上为SIGLOST。)

处理动作一项中的字母含义如下

字母 对应动作
A 缺省的动作是终止进程
B 缺省的动作是忽略此信号
C 缺省的动作是终止进程并进行内核映像转储(core dump)
D 缺省的动作是停止进程
E 信号不能被捕获
F 信号不能被忽略

注意:

  • 信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。
  • 信号SIGIOT与SIGABRT是一个信号。可以看出,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值。

信号的处理

对于进程来说,不能判别是否出现一个信号,而是必须要告诉内核信号出现的时候,执行下列操作。
信号的处理方式有三种:

  1. 忽略此信号
  2. 执行信号的默认处理动作(就是上面表里的动作)。
  3. 提供自定义行为,要求处理该信号的时候切换到用户态执行这个处理函数,也叫做捕捉一信号(如上,有些信号无法捕捉)。

信号递达

递达之前。。

通过系统调用产生信号

  • 我们可以通过系统调用来产生信号。这里我们先来看一下kill函数,
    1
    int kill(pid_t pid, int sig);

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

  • 接下来需要介绍的一个函数叫做raise函数 :
    1
    int raise(int sig);

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

  • abort函数使得当前进程接收到信号而异常中止。
    1
    void abort(void);

这个函数会产生SIGABRT信号,这个信号是夭折信号。

软件信号

软件产生信号这里我们首先来说一个函数alarm函数:

1
unsigned int alarm(unsigned int seconds);

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

信号阻塞

阻塞概念

阻塞信号我们首先提出一些概念,
信号递达:正在执行信号处理的动作,
信号产生与信号递达之间叫做信号未决,也叫做pending。
当信号阻塞的时候不会递达,接触阻塞,信号才能递达。
关于信号,我们首先需要从内核的角度来看看信号。
在内核当中,当一个进程接收到信号,会对应的在进程的PCB当中有三个相关的结构,

  • 因为我们现在有31个普通信号,所以这个时候我们可以想下我们前期所说的位图,我们也就可以利用一个整形就够了,每一个信号对应一个比特位。
  • 另外因为是bit位,所以这里注意,即使你产生了多个信号,这里的信号位也只是从0变为1,不记录信号产生了多少次。
  • pending表标识信号未决表,表示信号是否产生,block阻塞表,表示当前进程与信号屏蔽相关内容。我们也把阻塞信号集叫做当前进程的信号屏蔽字。
  • 注意阻塞和忽略是两回事,阻塞只是屏蔽了信号(block阻塞表对应位置置1),而忽略是对信号的一种处理方式(可以通过空handler函数实现)。

    信号集

    在linux下信号我们定义成为sigset_t类型的,sigset_t我们叫做信号集,这种类型经过我的测试大小是128个字节。
    信号集下面有一些函数。
    1
    2
    3
    4
    5
    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函数,

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

这个函数是用来进行读取或者修改进程的信号屏蔽字这里的how说的是如何进行更改,set指向你要修改的当前信号屏蔽字,oldset指向修改前你的信号屏蔽字。
how参数:

参数 含义
SIG_BLOCK 将set中信号加入信号屏蔽字
SIG_UNBLOCK 将set中信号移出信号屏蔽字
SIG_SETMASK 设置信号屏蔽字为set

注意:如果调用了sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少会将其中的一个信号递达。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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的位置的比特位。

信号捕捉

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

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

struct sigaction的定义:

1
2
3
4
5
6
7
8
struct sigaction
{
void (* sa_handler)(int);
void (* sa_sigaction)(int, siginfo_t * , void * );
sigset_t sa_mask;
int sa_flags;
void (* sa_restorer)(void);
};

参数 含义
sa_handler 早期的捕捉函数
sa_sigaction 新添加的捕捉函数,由sa_flags决定是哪个函数
sa_mask 在执行捕捉函数时,设置阻塞其他信号,退出时还原
sa_flags SA_SIGINFO或者0
sa_restorer 保留,应经过时

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

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

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

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

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

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

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

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

信号、可重入函数与线程安全

有了信号以后,会去调用喜好处理函数,这个时候你的程序就是异步执行,这个时候就引入了一个问题就是可重入函数的问题,
对于一个函数,当多个执行流进入函数,运行期间会出现问题的就叫做不可重入函数,不会出现的问题就是可重入函数。
信号捕捉函数内部禁止调入不可重入函数。
另外可重入函数还会和线程安全有联系:

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

四类不可重入函数:

  1. 不保护共享变量的函数
  2. 保持跨越多个调用的状态函数
  3. 返回指向静态变量指针的函数。
  4. 调用线程不安全的函数。

可重入函数是线程安全函数的一种,特点在于它们被多个线程调用的时候,不会引用任何共享数据。
对于不可重入函数的处理,我们通常采用的方法就是重写函数。
另外就是有些以_r结尾的函数就是那个函数的可重入版本。

信号与竞态条件

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

1
int pause(void);

关于pause函数,是用来使得调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause函数不返回,如果处理动作是忽略,pause函数也不返回。如果处理动作是信号捕捉,则调用捕捉函数,然后返回-1。
然后这里我们使用alarm和pause模拟实现一个sleep函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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函数。

1
int sigsuspend(const sigset_t *mask);

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

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

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

所以我们先阻塞信号,保存当前信屏蔽字,然后直到最后进程回到我当前进程,然后我解除SIGALRM信号的屏蔽,这样信号就会递达这样就确保了alarm和pause之间的操作都是原子的。
而对于sigsuspend函数来说:sigsuspend用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止。

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!