Linux 信号量大全
Linux 信号量大全
一、内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
(By default, the signal handler is invoked on the normal process stack. It is possible to arrange that the signal handler uses an alternate stack; see sigaltstack(2) for a discussion of how to do this and when it might be useful.)
5. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

上图出自ULK
二、sigaction函数
#include <signal.h>
int sigaction(int signo,const srtuct sigaction *act,struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。
Signo是指定要操作信号的编号。
若act 指针非空,则根据act修改该信号的处理动作。
若oact指针非空,则通过oact传出该信号原来的处理动作。
Act和oact指向sigaction结构体:
struct sigaction {
void (*sa_handler)(int ); /*//老类型的信号处理函数指针,与single函数一样*/
void (*sa_sigaction)(int, siginfo_t *, void*); /* // 新类型的信号处理函数指针*/
sigset_t sa_mask; // 将要被阻塞的信号集合
int sa_flags; // 信号处理方式
void (*sa_restorer)(void); // 保留
};
void (*sa_handler)(int):将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
sigset_t sa_mask;:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
int sa_flags:sa_flags有几个选项,比较重要的有两个:SA_NODEFER 和SA_SIGINFO,当SA_NODEFER设置时在信号处理函数执行期间不会屏蔽当前信号;当SA_SIGINFO设置时与sa_sigaction 搭配出现,sa_sigaction函数的第一个参数与sa_handler一样表示当前信号的编号,第二个参数是一个siginfo_t 结构体,第三个参数一般不用。当使用sa_handler时sa_flags设置为0即可。
void (*sa_restorer)(void):
需要注意的是sa_restorer 参数已经废弃不用,sa_handler主要用于不可靠信号(实时信号当然也可以,只是不能带信息),sa_sigaction用于实时信号可以带信息(siginfo_t),两者不能同时出现。
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since kernel 2.6.32) */
}
需要注意的是并不是所有成员都在所有信号中存在定义,有些成员是共用体,读取的时候需要读取对某个信号来说恰当的有定义的部分。
下面用sigaction函数举个小例子:
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void handler(int sig);
int main(int argc, char *argv[])
{
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(SIGINT, &act, NULL) < 0)
ERR_EXIT("sigaction error");
for (; ;)
pause();
return 0;
}
void handler(int sig)
{
printf("rev sig=%d\n", sig);
}
simba@ubuntu:~/Documents/code/linux_programming/APUE/signal$ ./sigaction
^Crev sig=2
^Crev sig=2
^Crev sig=2
即按下ctrl+c 会一直产生信号而被处理打印recv语句。
其实我们在前面文章说过的signal 函数是调用sigaction 实现的,而sigaction函数底层是调用 do_sigaction() 函数实现的。可以自己实现一个my_signal 函数,如下:
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<signal.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
void handler(int sig);
/* 系统调用signal()实际上调用了sigaction() */
__sighandler_t my_signal(int sig, __sighandler_t handler);
int main(int argc, char *argv[])
{
my_signal(SIGINT, handler);
for (; ;)
pause();
return 0;
}
__sighandler_t my_signal(int sig, __sighandler_t handler)
{
struct sigaction act;
struct sigaction oldact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(sig, &act, &oldact) < 0)
return SIG_ERR;
return oldact.sa_handler; // 返回先前的处理函数指针
}
void handler(int sig)
{
printf("rev sig=%d\n", sig);
}
输出测试是一样的,需要注意的是 signal函数成功返回先前的handler,失败返回SIG_ERR。而sigaction 是通过oact 参数返回先前的handler,成功返回0,失败返回-1。
三、Linux信号量大全
|
编号 |
信号名称 |
缺省动作 |
说明 |
|
1 |
SIGHUP |
终止 |
终止控制终端或进程 |
|
2 |
SIGINT |
终止 |
键盘产生的中断(Ctrl-C) |
|
3 |
SIGQUIT |
dump |
键盘产生的退出 |
|
4 |
SIGILL |
dump |
非法指令 |
|
5 |
SIGTRAP |
dump |
debug中断 |
|
6 |
SIGABRT/SIGIOT |
dump |
异常中止 |
|
7 |
SIGBUS/SIGEMT |
dump |
总线异常/EMT指令 |
|
8 |
SIGFPE |
dump |
浮点运算溢出 |
|
9 |
SIGKILL |
终止 |
强制进程终止 |
|
10 |
SIGUSR1 |
终止 |
用户信号,进程可自定义用途 |
|
11 |
SIGSEGV |
dump |
非法内存地址引用 |
|
12 |
SIGUSR2 |
终止 |
用户信号,进程可自定义用途 |
|
13 |
SIGPIPE |
终止 |
向某个没有读取的管道中写入数据 |
|
14 |
SIGALRM |
终止 |
时钟中断(闹钟) |
|
15 |
SIGTERM |
终止 |
进程终止 |
|
16 |
SIGSTKFLT |
终止 |
协处理器栈错误 |
|
17 |
SIGCHLD |
忽略 |
子进程退出或中断 |
|
18 |
SIGCONT |
继续 |
如进程停止状态则开始运行 |
|
19 |
SIGSTOP |
停止 |
停止进程运行 |
|
20 |
SIGSTP |
停止 |
键盘产生的停止 |
|
21 |
SIGTTIN |
停止 |
后台进程请求输入 |
|
22 |
SIGTTOU |
停止 |
后台进程请求输出 |
|
23 |
SIGURG |
忽略 |
socket发生紧急情况 |
|
24 |
SIGXCPU |
dump |
CPU时间限制被打破 |
|
25 |
SIGXFSZ |
dump |
文件大小限制被打破 |
|
26 |
SIGVTALRM |
终止 |
虚拟定时时钟 |
|
27 |
SIGPROF |
终止 |
profile timer clock |
|
28 |
SIGWINCH |
忽略 |
窗口尺寸调整 |
|
29 |
SIGIO/SIGPOLL |
终止 |
I/O可用 |
|
30 |
SIGPWR |
终止 |
电源异常 |
|
31 |
SIGSYS/SYSUNUSED |
dump |
系统调用异常 |
四、SIGALRM信号
信号有好多种,为什么偏偏要讲SIGALRM信号,因为这种信号在实际研发中的实用价值高,在使用中有两个函数可以产生这个信号,它们是alarm和setitimer,它们的区别是alarm相当于单次定时器,setitimer相当于循环定时器。
具体实现如下:
4.1 // alarm函数原型
unsigned int alarm(unsigned int seconds);
// alarm函数使用
alarm(6);
// 6秒以后给当前进程发SIGALRM信号。
4.2 // setitimer函数原型
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
// setitimer函数使用
struct itimerval new_value;
struct itimerval old_value;
new_value.it_interval.tv_sec = 1;
new_value.it_interval.tv_usec = 0;
// 定时器间隔,定时器到0以后,重新填充的值。
new_value. it_value.tv_sec = 1;
new_value. it_value.tv_usec = 0;
// 当前值,定时器从当前值开始递减到0。
setitimer(ITIMER_REAL, &new_value, &old_value);
// 如果已经设置了定时器,那么旧值存到old_value中。
4.3 signal 系统调用
系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:
void (*signal(int signum, void (*handler)(int)))(int);
上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义):
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。
在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是
SIG_IGN:忽略参数signum所指的信号。
SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR。下面来看一个简单的例子:
4.4 Kill系统调用
系统调用kill用来向进程发送一个信号。该调用声明的格式如下:
int kill(pid_t pid, int sig);
该系统调用可以用来向任何进程或进程组发送任何信号。如果参数pid是正数,那么该调用将信号sig发送到进程号为pid的进程。如果pid等于0,那么信 号sig将发送给当前进程所属进程组里的所有进程。如果参数pid等于-1,信号sig将发送给除了进程1和自身以外的所有进程。如果参数pid小于- 1,信号sig将发送给属于进程组-pid的所有进程。如果参数sig为0,将不发送信号。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应 的错误代码errno。下面是一些可能返回的错误代码:
EINVAL:指定的信号sig无效。
ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。
EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误 表示组中有成员进程不能接收该信号。
4.5 pause系统调用
系统调用pause的作用是等待一个信号。该调用的声明格式如下:
int pause(void);
该调用使得发出调用的进程进入睡眠,直到接收到一个信号为止。该调用总是返回-1,并设置错误代码为EINTR(接收到一个信号)。
4.6 alarm系统调用
系统调用alarm的功能是设置一个定时器,当定时器计时到达时,将发出一个信号给进程。该调用的声明格式如下:
unsigned int alarm(unsigned int seconds);
注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。
4.7 关于signal函数的返回值问题
今天看到了信号有关的章节,对其中的返回值的描述不太理解,也包括后面例程中提供的正规写法:
if (signal(SIGINT, sigint_handler)) == SIG_ERR)
觉得这条if语句也有点搞不懂 原文说的是:若成功则返回指向前次处理程序的指针,若出错则为SIG_ERR 。当时想不清楚signal调用后到底返回了什么,这条if语句怎么判断,后来用google找到一篇国外的博客解释的不错。其实主要是记住signal返回的是一个指向某个函数的指针,而这个函数就是上次处理这个信号的信号处理函数,如果未处理过,那就是NULL。测试代码如下:
#include <signal.h>
#include <assert.h>
#include <stdio.h>
void catch1(int signo) {
printf("catch1 received signal %d\n", signo);
}
void catch2(int signo) {
printf("catch2 received signal %d\n", signo);
}
int main(void) {
sig_t prev_sigint_handler1 = signal(SIGINT, catch1);
assert(prev_sigint_handler1 == NULL);
sig_t prev_sighup_handler1 = signal(SIGHUP, catch2);
assert(prev_sighup_handler1 == NULL);
raise(SIGINT); // calls catch1
raise(SIGHUP); // calls catch2
// Now let's swap the handlers
sig_t prev_sigint_handler2 = signal(SIGINT, catch2);
assert(prev_sigint_handler2 == catch1);
sig_t prev_sighup_handler2 = signal(SIGHUP, catch1);
assert(prev_sighup_handler2 == catch2);
raise(SIGINT); // calls catch2
raise(SIGHUP); // calls catch1
return 0;
}
% ./a.out
catch1 received signal 2
catch2 received signal 1
catch2 received signal 2
catch1 received signal 1
不难看出一开始两个handler1都为NULL,分别发送SIGINT和SIGHUP信号后,就指向了catch1和catch2。而再次调用signal后会分别得到两个handler,于是可以让新的handler2交换捕获的信号,就和指针的赋值一样了。

浙公网安备 33010602011771号