信号的概念
在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
简而言之,信号用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:
- 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
- 第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。
- 第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信 号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
- 在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。
那么信号在什么条件下产生呢?
信号的产生
- 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
- 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误,比如除以0。
- 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
- 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
- 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。当然在终端也可以用kill指令,这种方法存在限制:我们必须是信号接收进程的所有者,或者我们必须是超级用户(root)。
- 与终端交互相关的信号。如用户关闭一个终端,或按下ctrl+c键等情况。
- 跟踪进程执行的信号。
我们可以用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是一个信号。可以看出,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值。
信号的处理
对于进程来说,不能判别是否出现一个信号,而是必须要告诉内核信号出现的时候,执行下列操作。
信号的处理方式有三种:
- 忽略此信号
- 执行信号的默认处理动作(就是上面表里的动作)。
- 提供自定义行为,要求处理该信号的时候切换到用户态执行这个处理函数,也叫做捕捉一信号(如上,有些信号无法捕捉)。
信号递达
递达之前。。
通过系统调用产生信号
- 我们可以通过系统调用来产生信号。这里我们先来看一下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
5int 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.c1
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
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
8struct 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
2typedef void (* sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
它的第一个参数是信号的编号,第二个参数是指向自定义函数的指针,就是当你捕捉到这个信号,不让它去做它的默认操作,而是去做你想要让它做函数,这个参数是一个返回值为void,参数为int的一个函数指针。
signal是C标准库提供的信号处理函数,
接下来说一说信号捕捉的时候的状态转换:
从上面这张图就可以看出整个状态的转换,
- 首先当你遇到中断、异常或者系统调用的时候进入内核态。
- 然后产生信号,这样由内核态切换用户态,这个过程当中需要去PCB检查那三张表,然后发现有递达的信号,然后这个时候就去处理信号对应的操作。也就是信号处理函数。
- 处理信号处理函数的时候,这个时候为了安全的问题,这个时候为用户态。
- 信号处理函数结束后,然后从用户态切换到内核态。
- 然后由内核态切换到中断异常执行处的用户态。
所以总共有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
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替换进程的信号掩码,并暂停进程执行,直到收到信号为止。