本系列手册是通过参考一些网络资料和本人使用经验整理而写,固然简单,但是也是花了心血而成,适用于初学
者和了解一些信号编程但始终没有理解,没有动手写过程序的朋友们.也适用于高手们添加内容或者纠错smile.gif但是不能
用于任何商业用途,可自由转载但是必须著明出处[url][/url]和作者大漠孤星.
有任何意见和建议请mail to :diablo_tina@163.com或者QQ 6299745联系.
一,前言
信号是进程之间互传消息的一种方法俗称软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种
处理异步事件的方法:终端用户键入中断键,则会通过信号机构停止一个程序。所以,信号可以说是进程控制的一部分。
在Redhat 7.3上kill -l 得到
信号出现在 UNIX 的早期版本中 ,但早期的信号模型是不可靠的, 信号可能被丢失,
也很难处理关键段。UNIX 的两个重要分支 BSD 和 System V 分别对早期的信号进行了扩展 ,
但这两个系统的扩展并不兼容POSIX 统一了这两种实现, 最终提供了可靠的信号模型。
信号的产生条件
. 当用户按某些终端键时,产生信号。
. 硬件异常产生信号:除数为0、无效的存储访问等等。
. 进程用kill函数可将信号发送给另一个进程或进程组
. 用户可用kill命令将信号发送给其他进程
. 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。
接到信号的处理办法
1,忽略,但是SIGKILL和SIGSTOP不能忽略。
2,捕捉。
3,执行系统的默认处理。
信号的分类
非可靠信号:早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应
以及信号可能丢失。
可靠信号: 信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号
克服了信号可能丢失的问题。
信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。
当然也可以称为实时信号或者非实时信号,非实时信号都不支持排队,都是不可靠信号;
实时信号都支持排队,都是可靠信号。
这里我说一下信号的生命周期,对于理解信号的分类有很大的帮助。
从信号发送到信号处理函数的执行完毕。对于一个完整的信号生命周期(从信号发送到相应的处
理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号产生;
信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间
间隔构成信号生命周期的一个阶段。
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,
信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决
信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,
并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册);
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。
因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个
sigqueue结构(一个非实时信号产生后,1如果发现相同的信号已经在目标结构中注册,则不再注册,对于
进程来说,相当于不知道本次信号发生,信号丢失;2如果进程的未决信号中没有相同信号,则在进程中注册自己)。
需要注意的要点是:
1)信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,
只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。
2)在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,
每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。
当然还有有些需要知道的概念比如 低速系统调用,中断系统调用,可重入函数等等概念,请查阅环境高级编程。
一些概念性的东西大概就这些东西吧,下面就分类介绍一下具体的函数。
我下面介绍的,如果没有特殊说明,都是对于linux上的实现,当然都是符合POSIX标准喽。
二,函数详细介绍
A:信号的发送
发送信号的主要函数有:kill(),raise(),sigqueue(),alarm(),setitimer()以及abort()。
1, int kill(pid_t pid,int signo)
用到的头文件:
#include
#include
参数pid的值 信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程
Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,
以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同
一个session或者同一个用户的进程发送信号)。
kill最常用于pid>0时的信号发送,调用成功返回 0; 否则,返回 -1。注:对于pid<0时的情况,对于哪些进程将接受信号,
各种版本说法不一,其实很简单,参阅内核源码kernal/signal.c。
2, int raise(int signo)
用到的头文件:
#include
向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。
3, int sigqueue(pid_t pid, int sig, const union sigval val)
用到的头文件:
#include
#include
调用成功返回 0;否则,返回 -1。
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,
与函数sigaction()配合使用。sco 5.05没有此函数。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,
指定了信号传递的参数,即通常所说的4字节值。
typedef union sigval
{
int sival_int;
void *sival_ptr;
}sigval_t;
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果sig为0,
将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指的是信号处理函数由sigaction安装,
并设定了sa_sigaction指针,稍后将阐述)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用
支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。
注:sigqueue()发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数; sigqueue()发送非实时信号时,仍然不支持排队,
即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号。
4, unsigned int alarm(unsigned int seconds)
用到的头文件:
#include
专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,
任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。
函数返回是这样的,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
5, int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
用到的头文件:
#include
setitimer()比alarm功能强大,支持3种类型的定时器:
ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;
Setitimer()第一个参数which指定定时器类型(上面三种之一);第二个参数是结构itimerval的一个实例,结构itimerval。
结构itimerval:
struct itimerval
{
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
第三个参数可不做处理。
Setitimer()调用成功返回0,否则返回-1。
这是我man setitimer时看到的
getitimer and setitimer are not part of any currently supported standard;
they were developed at the University of California at Berkeley and are
used by permission.
6, void abort(void);
用到的头文件:
#include
向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。
即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。
B:信号的捕获与安装(设置信号关联动作)
如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值
的动作之间的映射关系,
即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现,
是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的
函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与
sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现
在支持信号带有参数。
1, void (*signal(int signum, void (*handler))(int)))(int);
#include
如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,
a,可以忽略该信号(参数设为SIG_IGN);
b,可以采用系统默认方式处理信号(参数设为SIG_DFL);
b,也可以自己实现处理方式(参数指定一个函数地址)。
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。
2, int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
#include
sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何
一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例
的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向
的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。
sigaction结构定义如下:
struct sigaction
{
union
{
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int,struct siginfo *, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
}
其中,sa_restorer,已过时,POSIX不支持它,不应再被使用。
a,联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自
定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
b,由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是指定的
信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。
第一个参数为信号值,第三个参数没有使用(POSIX没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含
信号携带的数据值,参数所指向的结构如下:
siginfo_t {
int si_signo; /* 信号值,对所有信号有意义*/
int si_errno; /* errno值,对所有信号有意义*/
int si_code; /* 信号产生的原因,对所有信号有意义*/
union{ /* 联合数据结构,不同成员适应不同信号 */
//确保分配足够大的存储空间
int _pad[SI_PAD_SIZE];
//对SIGKILL有意义的结构
struct{
...
}...
... ...
... ...
//对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
struct{
...
}...
... ...
}
}
注:为了更便于阅读,在说明问题时常把该结构表示为如下所表示的形式。
siginfo_t {
int si_signo; /* 信号值,对所有信号有意义*/
int si_errno; /* errno值,对所有信号有意义*/
int si_code; /* 信号产生的原因,对所有信号有意义*/
pid_t si_pid; /* 发送信号的进程ID,对kill(2),实时信号以及SIGCHLD有意义 */
uid_t si_uid; /* 发送信号进程的真实用户ID,对kill(2),实时信号以及SIGCHLD有意义 */
int si_status; /* 退出状态,对SIGCHLD有意义*/
clock_t si_utime; /* 用户消耗的时间,对SIGCHLD有意义 */
clock_t si_stime; /* 内核消耗的时间,对SIGCHLD有意义 */
sigval_t si_value; /* 信号值,对所有实时有意义,是一个联合数据结构,可以为一个整数(由si_int标示,也可以为一个指针,由si_ptr标示)*/
void * si_addr; /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义*/
int si_band; /* 对SIGPOLL信号有意义 */
int si_fd; /* 对SIGPOLL信号有意义 */
}
实际上,除了前三个元素外,其他元素组织在一个联合结构中,在联合数据结构中,又根据不同的信号组织成不同的结构。
注释中提到的对某种信号有意义指的是,在该信号的处理函数中可以访问这些域来获得与信号相关的有意义的信息,只不过特定信号只
对特定信息感兴趣而已。
siginfo_t结构中的联合数据成员确保该结构适应所有的信号,比如对于实时信号来说,则实际采用下面的结构形式:
typedef struct {
int si_signo;
int si_errno;
int si_code;
union sigval si_value;
} siginfo_t;
结构的第四个域同样为一个联合数据结构:
union sigval {
int sival_int;
void *sival_ptr;
}
采用联合数据结构,说明siginfo_t结构中的si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了与信号相关的数据。
在信号的处理函数中,包含这样的信号相关数据指针,但没有规定具体如何对这些数据进行操作,操作方法应该由程序开发人员根据具体
任务事先约定。
前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将
拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。
c,sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定
SA_NODEFER或者SA_NOMASK标志位。
注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。
d,sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志
位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler
指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样
不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。
注:很多文献在阐述该标志位时都认为,如果设置了该标志位,就必须定义三参数信号处理函数。实际不是这样的,验证方法很简单:自己实现
一个单一参数信号处理函数,并在程序中设置该标志位,可以察看程序的运行结果。实际上,可以把该标志位看成信号是否传递参数的开关,如果设置该位,
则传递参数;否则,不传递参数。
C:信号集及信号集操作函数:
信号集被定义为一种数据类型:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t
信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为
信号集操作定义的相关函数:
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum)
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
头文件
#include
sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;
sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;
sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;
sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;
sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。
D:信号阻塞与信号未决:
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞
相关的几个函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask));
头文件:
#include
sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:
参数how 进程当前信号集
SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号
SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
SIG_SETMASK 更新进程阻塞信号集为set指向的信号集
sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。
sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。
sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。
阅读(1945) | 评论(0) | 转发(0) |