信号的本质
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
因此,在进行上层应用程序设计过程中我们就必须明确哪些函数是可重入性函数(reentrant functions)。可重入性函数通常也一定能够在信号处理程序(signal handler)中被调用。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:
第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
第二种方法是,忽略某个信号,对该信号不做任何处理,就像未发生过一样。
第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
Linux的信号处理流程
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个阶段:
- 信号诞生
- 信号在进程中注册
- 信号的执行和注销
- 收到信号,例如SIGFPE
- 进入signal注册的信号处理函数,此时,SIGFPE自动被加入到进程信号屏蔽字
- 执行信号处理函数
- 信号处理结束,恢复信号屏蔽字,SIGFPE被取消阻塞
- 返回到产生信号地方继续执行
信号的种类(见linux信号概述)
可以从两个不同的分类角度对信号进行分类:
可靠性方面:可靠信号与不可靠信号;
与时间的关系上:实时信号与非实时信号
Linux信号接口函数
信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息
发送信号: kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); //void (* signal( int signo, void (*func)(int) ) )(int); //函数名 :signal //函数参数 :int signo, void (*func)(int) //返回值类型 :void (*)(int);
参数:
signum:需要处理的信号
handler:常量SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数的地址
--handler这个函数必须有一个int类型的参数(即接收到的信号代码),它本身的类型是void
--handler也可以是下面两个特殊值:
① SIG_IGN 向内核表示忽略此信号(记住有两个信号SIGKILL和SIGSTOP不能忽略)
② SIG_DFL 表示接到此信号后的动作是系统默认动作
当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为“捕捉”该信号。
称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)
返回值:
若成功,返回以前的信号处理配置
若出错,返回SIG_ERR
如果查看系统的头文件<signal.h>,则很可能会找到下列形式的声明:
#define SIG_ERR ( void (*) () ) -1 #define SIG_DFL ( void (*) () ) 0 #define SIG_IGN ( void (*) () ) 1
#include <signal.h> int kill(pid_t pid, int signo); //将信号发送给进程或进程组。 int raise(int signo); //允许进程向自身发送信号 #include <stdlib.h> void abort(void); //向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。
参数:
pid:它有以下4种情况:
pid > 0: 将该信号发送给进程ID为pid的进程.
pid == 0: 将该信号发送给与发送进程属于同一进程组的所有进程(不包括内核进程和init进程). 此时, 发送进程必须具有向这些进程发送信号的权限.
pid < 0: 将该信号发给其进程组ID等于pid绝对值的所有进程(不包括内核进程和init进程). 此时, 发送进程必须具有向这些进程发送信号的权限.
pid == -1: 将该信号发送给发送进程有权限向它们发送信号的系统上的所有进程.(不包括内核进程和init进程).
signo:即将发送的信号,
POSIX.1将编号为0的信号定义为空信号. 如果signo参数是0, 则kill仍执行正常的错误检查, 但不发送信号.这被用来确定一个进程是否存在.
value:一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
返回值:
若成功,返回0
若出错,返回-1
#include <sys/types.h> #include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval value); //比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。 //相关结构体 typedef union sigval { int sival_int; void *sival_ptr; }sigval_t;
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。
如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。
由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。
#include<unistd.h> unsigned int alarm(unsigned int seconds) //也称为闹钟函数,alarm()用来设置信号SIGALRM在经过参数seconds指定的秒数后传送给目前的进程。 //如果参数seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。 //要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。 //可对可能阻塞的操作设置时间上限值 int pause(void); //使调用进程(或线程)睡眠,直到接收到信号,要么终止,或导致它调用一个信号捕获函数。 //可与alarm函数一起用,使进组休眠指定时间
参数:
seconds:指定秒数
返回值:
若成功,如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。若出错,返回-1.
pause()函数仅在信号被捕获并返回信号捕获功能时才返回。在这种情况下,返回-1,并将errno设置为EINTR。
#include <sys/time.h> //得到定时器的状态 int getitimer(int which, struct itimerval *curr_value); //设置定时器 int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); //定时器中的参数value用来指明定时器的时间,其结构如下: struct itimerval { struct timeval it_interval; /* 下一次的取值 */ struct timeval it_value; /* 本次的设定值 */ }; //该结构中timeval结构定义如下: struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微秒,1秒 = 1000000 微秒*/ };
参数:
该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:
TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。
ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。
ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程
在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。
定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。
当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:
EFAULT:参数value或ovalue是无效的指针。
EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //struct sigaction 类型用来描述对信号的处理,定义如下: 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 是一个函数指针,包含一个信号处理函数的地址,与signal函数类似 成员sa_sigaction 则是另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。 int iSignNum : 传入的信号 */ //siginfo_t *pSignInfo : 该信号相关的一些信息,他是一个结构体原型如下 siginfo_t { int si_signo; /* 信号值,对所有信号有意义 */ int si_errno; /* errno 值,对所有信号有意义 */ int si_code; /* 信号产生的原因,对所有信号有意义 */ int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ pid_t si_pid; /* 发送信号的进程ID */ uid_t si_uid; /* 发送信号进程的真实用户ID */ int si_status; /* 对出状态,对SIGCHLD 有意义 */ clock_t si_utime; /* 用户消耗的时间,对SIGCHLD有意义 */ clock_t si_stime; /* 内核消耗的时间,对SIGCHLD有意义 */ sigval_t si_value; /* 信号值,对所有实时有意义,是一个联合数据结构,可以为一个整数(由si_int标示,也可以为一个指针,由si_ptr标示) */ 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; /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义 */ long si_band; /* 对SIGPOLL信号有意义 */ int si_fd; /* 对SIGPOLL信号有意义 */ short si_addr_lsb; /* Least significant bit of address (since kernel 2.6.32) */ } /* 当 sa_flags 成员的值包含了 SA_SIGINFO 标志时, 系统将使用 sa_sigaction 函数作为信号处理函数,否则使用 sa_handler 作为信号处理函数。 在某些系统中,成员 sa_handler 与 sa_sigaction 被放在联合体中,因此使用时不要同时设置。 sa_mask:说明了一个信号集,来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被 自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。 sa_flags 成员用于指定信号处理的行为,它可以是一下值的“按位或”组合。 ◆ SA_RESTART:使被信号打断的系统调用自动重新发起。 ◆ SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。 ◆ SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵 尸进程。 ◆ SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。 ◆ SA_RESETHAND:信号处理之后重新设置为默认的处理方式。 ◆ SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。 re_restorer 成员则是一个已经废弃的数据域,不要使用 */
参数:
signum:要操作的信号。
act:要设置的对信号的新处理方式。
oldact:原来对信号的处理方式。
返回值:
若成功,返回0
若出错,返回-1
#include <signal.h> int sigwaitinfo(const sigset_t *set, siginfo_t *info); //暂停调用线程的执行,直到其中一个设置中的信号正在等待 //(如果设置中的一个信号为已经等待调用线程,sigwaitinfo()将立即返回。) //从一组待处理信号中去除信号返回信号号作为其功能结果。 int sigtimedwait(const sigset_t *set, siginfo_t *info,const struct timespec *timeout); //操作方式与sigwaitinfo()完全相同, //除了它有一个额外的参数,timeout指定线程暂停等待信号的时间间隔。 //(此间隔将四舍五入为系统时钟粒度,内核调度延迟意味着间隔可能会超过a少量。)
参数:
如果信息参数不为NULL,则指向其指向的缓冲区返回一个类型为siginfo_t的结构有关信号的信息。
返回值:
若成功,返回返回一个信号数(即,大于零的值)。
若出错,返回-1.并设置errno。
#include <signal.h> void psignal(int signo, const char *msg); //在stderr上显示字符串,后面跟随一个冒号和一个空格,然后再对该信号进行说明,最后是换行符。 //msg(通常是程序名)输出到标准错误文件 //如果msg为NULL,只有信号说明部分输出到标准错误文件,类似perror。 void psiginfo(const siginfo_t *pinfo, const char *s); //如果在sigaction信号处理程序中有siginfo结构,可以此函数打印. void psiginfo(const siginfo_t *pinfo, const char *s); //如果不需要把它写到标准错误文件,可用此函数。 //返回值:描述该信号的字符串。
int setjmp(jmp_buf env); //用于设置跳转的目的位置 void longjmp(jmp_buf env, int val); //进行跳转。
参数:
env:保留了需要返回的位置的堆栈情况。
setjmp的返回值:直接调用该函数,则返回0;
若由longjmp的调用,导致setjmp被调用,则返回val(longjmp的第二个参数)。
详解:c语言异常处理机制:跨跃函数跳转函数 setjmp/sigsetjmp和longjmp/siglongjmp_泡沫o0的博客-CSDN博客
设置信号处理函数代码示例
//提供一个绑定信号处理函数的接口,传入信号名以及函数指针 //void init_sigaction(void(*p)(int)) void init_sigaction(signal_number,void(*p)(int,siginfo_t *,void *)) { struct sigaction sig_action; memset(&sig_action, 0, sizeof(sig_action)); /* Initialize signal set */ sigemptyset(&sig_action.sa_mask); /* Provides in-depth information about the signal */ sig_action.sa_flags |= SA_SIGINFO; /* Select signal processing function */ sig_action.sa_sigaction = p; // tact.sa_handler = p; /* Establish a signal processing mechanism */ sigaction(signal_number, &sig_action, NULL); return ; } //信号处理函数 //void SignalHandler(int signum) void SignalHandler(int signum, siginfo_t *signal_info,void *ucontext){ /** **/ }
Linux线程与信号
- 线程之间共享信号处理函数
- 线程有独立的阻塞信号掩码(信号集)
- 私有挂起信号和共享挂起信号
- 致命信号下, 进程组全体退出