Libev源码分析06:异步信号同步化--sigwait、sigwaitinfo、sigtimedwait和signalfd
一:信号简述
信号是典型的异步事件。内核在某个信号出现时有三种处理方式:
a:忽略信号,除了SIGKILL和SIGSTOP信号不能忽略外,其他大部分信号都可以被忽略;
b:捕捉信号,也就是在信号发生时调用一个用户函数,注意不能捕捉SIGKILL和SIGSTOP;
c:执行系统默认动作,注意大多数信号的系统默认动作是终止进程。
调用execve执行一个新的进程时,新进程的信号处理方式要么是忽略,要么是系统默认方式。如果调用进程忽略该信号,则新进程也忽略该信号,如果调用进程捕捉该信号,或者执行系统默认动作,新进程则按照默认行为处理该信号,这是因为调用进程的信号处理函数在新进程中已经无效了。
如果是调用fork产生子进程,则子进程的信号处理方式完全继承父进程的处理方式。这是因为子进程复制了父进程的地址空间,因此父进程中的信号处理函数在子进程中同样被复制了。
当信号产生以后,对信号采取了动作时,称为向进程“递送”了一个信号,在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)。
进程可以“阻塞”信号,如果为进程产生了一个阻塞的信号,而且对该信号的动作是调用信号处理函数捕捉该信号,或者是系统默认动作,则会为该进程将此信号保持为未决状态,直到a:对此信号解除了阻塞,或者b:对此信号的动作改为忽略。内核在递送一个原来被阻塞的信号给进程时(而不是产生信号时),才决定对它的处理方式,因此,进程在信号被递送之前仍可改变对该信号的动作。
如果在进程解除某个信号的阻塞之前,该信号发生了多次,则系统可以递送该信号一次或多次。如果递送多次,则称对这些信号进行了排队。但是除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队,也就是指递送该信号一次。
如果有多个信号要递送给一个进程,POSIX.1并未规定这些信号的递送顺序。
每个进程都有一个信号屏蔽字,它规定了当前要阻塞的信号集。POSIX.1使用数据类型sigset_t表示一个信号集,进程可以调用sigprocmask来检测和更改当前信号屏蔽字。
对信号的处理是以进程为单位的,也就是说在多线程环境中,信号的处理是进程中所有线程共享的。但是每个线程可以有自己的信号屏蔽字,所以,单个线程可以阻塞某些信号,当某个线程修改了某个信号的处理方式后,所有线程共享这个处理方式的改变。
进程中的信号是递送到单个线程的。如果信号与硬件故障相关,该信号就被递送到引起该事件的线程中去,而其他信号则被递送到任意一个没有阻塞该信号的线程。sigprocmask的行为在多线程中没有定义,线程必须使用相应的pthread_sigmask。
除了使用信号处理函数异步的捕捉信号之外,还可以将这种异步行为变得同步,有两种方法:
a:调用sigwaitinfo、sigtimedwait或sigwait,这些函数会阻塞调用线程,直到信号集set中的某个信号被递送为止,这些函数都会返回递送信号的信息。
b:调用signalfd,它返回一个文件描述符,针对该描述符的read的操作将会阻塞,直到signalfd指定的信号集set中的某个信号递送给调用者为止,此时read返回一个描述该信号的结构。
二:sigwaitinfo、sigtimedwait和sigwait
1:sigwait
#include <signal.h>
int sigwait(const sigset_t *set, int *sig);
sigwait从set中选择一个未决信号(pending),从进程的未决信号集合中移除该信号,并在sig中返回该信号值。如果set中的所有信号都不是pending状态,则sigwait会阻塞调用它的线程,直到set中的信号变为pending。
为了避免错误动作发生,线程在调用sigwait之前,必须阻塞那些它正在等待的信号,否则行为是未定义的。sigwait函数会自动取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait将恢复线程的信号屏蔽字。如果信号在sigwait调用的时候没有被阻塞,则在sigwait调用之前会出现一个时间窗,在这个窗口期,某个信号可能在线程调用sigwait之前就被递送了。
sigwait只是取消set中信号的阻塞,其他信号则维持原状态,比如,在sigwait调用之前阻塞了所有信号,而set中只包含了SIGINT,则发送SIGABRT也不会有效果。
如果多个线程在sigwait调用时,等待的是同一个信号,当信号递送的时候,只有一个线程可以从sigwait中返回,具体是那个线程则是未定义的。
如果信号被捕获(进程通过使用sigaction建立了一个信号处理程序),而且线程正在sigwait调用中等待同一信号,那么这时将由操作系统实现来决定以何种方式递送信号。在这种情况下,操作系统实现可以让sigwait返回,也可以激活信号处理程序,但不可能出现两者皆可的情况。
sigwait成功返回0,失败时返回表示错误的值。
使用sigwait的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后安排专用线程作信号处理。这些专用线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常的线程环境,而非传统的信号处理程序,传统信号处理程序通常会中断线程的正常执行。
2:sigwaitinfo和sigtimedwait
#include <signal.h> int sigwaitinfo(const sigset_t *set, siginfo_t *info);
int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);
除了返回信息方面,sigwaitinfo的行为基本上与sigwait类似。sigwait在sig中返回触发的信号值;而sigwaitinfo的返回值就是触发的信号值,并且如果info不为NULL,则sigwaitinfo返回时,还会在siginfo_t *info中返回更多该信号的信息,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 Linux 2.6.32)*/
}
比如:info->si_signo是信号值;info->si_code是该信号产生的原因
sigtimedwait的行为又与sigwaitinfo的行为类似,只是它多了一个超时参数timeout,也就是可以设置该函数的最大阻塞时间。struct timespec的定义如下:
struct timespec
{
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
}
如果该结构体中的成员都置为0的话,则sigtimedwait将会立即返回,此时如果确实有pending信号的话,就返回该信号的信息,否则返回超时错误。
POSIX没有规定,如果timeout为NULL的话sigtimedwait的行为,Linux下,timeout为NULL时,sigtimedwait的行为与sigwaitinfo是一样的。
三:signalfd
#include <sys/signalfd.h> int signalfd(int fd, const sigset_t *mask, int flags);
signalfd用来创建一个接收信号的文件描述符,该文件描述符可用于select、poll和epoll。mask参数中指定了调用者希望接收的信号集,一般而言,信号集中的信号在调用signalfd之前,应该使用sigprocmask进行阻塞,这样这些信号就不会按照原来的处理方式进行处理了。注意不能通过signalfd的文件描述符接收SIGKILL或SIGSTOP信号,如果在mask中指定了这俩信号,则会默认忽略掉。
如果fd参数为-1,则signalfd创建新的文件描述符,并将信号集mask与该文件描述符相关联。如果fd不为-1,则fd必须是一个已经存在的signalfd文件描述符,mask用于更新与之对应的信号集。
在Linux2.6.26之前的版本,flags参数是无用的,必须置为0。从Linux2.6.27开始,可以在flags中指定下面的值以改变signalfd的行为:
SFD_NONBLOCK:在新创建的文件描述符上设置O_NONBLOCK文件状态标志;
SFD_CLOEXEC:在新建的文件描述符上设置FD_CLOEXEC标志。
注意上面的两个标志,都是针对新建描述符而言的(fd为-1),如果是一个已经存在的描述符,设置这两个标志都没有作用(已通过程序验证)。
signalfd创建描述符支持下列操作:
read:如果mask中的一个或多个信号是未决状态(pending)的话,则read将会在其buffer参数中返回一个或多个描述信号的signalfd_siginfo结构。read操作尽可能多的返回未决信号,并将其信息填充到buffer。buffer至少应该是sizeof(struct signalfd_siginfo) 个字节,read的返回值就是读取的字节数。
在read之后,read返回的信号就被消费掉了,不再处于未决状态(也就是不能再由信号处理函数捕捉,也不能使用sigwaitinfo接收)。
如果mask中的信号都不是未决状态,若文件描述符置为非阻塞的话,read返回EAGAIN错误,否则read会一直阻塞,直到mask中的信号产生。
select、poll、epoll等:如果mask中有一个或多个信号处于未决状态的话,则这些文件描述符就是可读的,从而可以使得select、poll或者epoll返回。
close:当不再需要该文件描述符时,应该使用close关闭。与其他文件描述符一样,如果底层signalfd对象关联的所有文件描述符都关闭之后,则该对象的资源就会被内核释放掉。
结构体signalfd_siginfo的定义如下:
struct signalfd_siginfo
{
uint32_t ssi_signo; /* Signal number */
int32_t ssi_errno; /* Error number (unused) */
int32_t ssi_code; /* Signal code */
uint32_t ssi_pid; /* PID of sender */
uint32_t ssi_uid; /* Real UID of sender */
int32_t ssi_fd; /* File descriptor (SIGIO) */
uint32_t ssi_tid; /* Kernel timer ID (POSIX timers) */
uint32_t ssi_band; /* Band event (SIGIO) */
uint32_t ssi_overrun; /* POSIX timer overrun count */
uint32_t ssi_trapno; /* Trap number that caused signal */
int32_t ssi_status; /* Exit status or signal (SIGCHLD) */
int32_t ssi_int; /* Integer sent by sigqueue(3) */
uint64_t ssi_ptr; /* Pointer sent by sigqueue(3) */
uint64_t ssi_utime; /* User CPU time consumed (SIGCHLD) */
uint64_t ssi_stime; /* System CPU time consumed (SIGCHLD) */
uint64_t ssi_addr; /* Address that generated signal (for hardware-generated signals) */
uint8_t pad[X]; /* Pad size to 128 bytes (allow for additional fields in the future) */
};
该结构体中的每个成员都类似于结构体siginfo_t中的成员。对于一个特定的信号,read返回的signalfd_siginfo结构体中,并非所有的成员都是有效的;具体哪些成员是有意义的,可以通过成员ssi_code来检测,该成员类似于siginfo_t中的si_code成员。
fork之后,子进程拥有了父进程的signalfd文件描述符的副本,在子进程的文件描述符上调用read,将会返回子进程的未决信号。
类似于其他文件描述符,除非设置了close-on-exec标志,否则signalfd文件描述符在execve之后还是保持打开的,在execve之前可读的信号,在新的进程中依然可读。
在多线程环境中,signalfd文件描述符与标准的多线程中信号的语义是一样的。也就是说,当一个线程读取signalfd文件描述符时,它会读取到针对该线程的信号,以及针对整个进程的信号。
调用signalfd成功时,返回一个signalfd文件描述符,该描述符要么是一个新的signalfd描述符(fd为-1的情况下),要么就是fd(fd已经是个有效的signalfd描述符的情况下)。
调用signalfd错误时,返回-1,并设置相应的errno。
signalfd是linux特有的,自linux2.6.22才开始引入,在glibc的2.8版本中开始被支持。
一个进程可以创建多个signalfd文件描述符,这样可以在不同的signalfd描述符上等待不同的信号。如果多个文件描述符中的mask上具有同一个信号,当信号发生时,所有描述符都变得可读,可以从任意一个描述符读取该信号。这种情况下,如果使用select、poll或者epoll等待这些signalfd描述符,则这些描述符都会变成可读的,从其中之一读取之后,该信号就被消费掉,读取其他的描述符就会阻塞,所以要避免这种情况(已通过程序验证)。
例子:
#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h> #define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0) int main(int argc, char *argv[])
{
sigset_t mask;
int sfd;
struct signalfd_siginfo fdsi;
ssize_t s; sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT); if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
handle_error("sigprocmask"); sfd = signalfd(-1, &mask, 0);
if (sfd == -1)
handle_error("signalfd"); for (;;) {
s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
if (s != sizeof(struct signalfd_siginfo))
handle_error("read"); if (fdsi.ssi_signo == SIGINT) {
printf("Got SIGINT\n");
} else if (fdsi.ssi_signo == SIGQUIT) {
printf("Got SIGQUIT\n");
exit(EXIT_SUCCESS);
} else {
printf("Read unexpected signal\n");
}
}
}
结果:
$ ./signalfd_demo
^C # Control-C generates SIGINT
Got SIGINT
^C
Got SIGINT
^\ # Control-\ generates SIGQUIT
Got SIGQUIT
参考:
《Unix环境高级编程》
man手册
Libev源码分析06:异步信号同步化--sigwait、sigwaitinfo、sigtimedwait和signalfd的更多相关文章
- [转]Libev源码分析 -- 整体设计
Libev源码分析 -- 整体设计 libev是Marc Lehmann用C写的高性能事件循环库.通过libev,可以灵活地把各种事件组织管理起来,如:时钟.io.信号等.libev在业界内也是广受好 ...
- Libev源码分析08:Libev中的信号监视器
Libev中的信号监视器,用于监控信号的发生,因信号是异步的,所以Libev的处理方式是尽量的将异步信号同步化.异步信号的同步化方法主要有:signalfd.eventfd.pipe.sigwaiti ...
- Libev源码分析09:select突破处理描述符个数的限制
众所周知,Linux下的多路复用函数select采用描述符集表示处理的描述符.描述符集的大小就是它所能处理的最大描述符限制.通常情况下该值为1024,等同于每个进程所能打开的描述符个数. 增大描述符集 ...
- Android8.1 MTK平台 SystemUI源码分析之 网络信号栏显示刷新
SystemUI系列文章 Android8.1 MTK平台 SystemUI源码分析之 Notification流程 Android8.1 MTK平台 SystemUI源码分析之 电池时钟刷新 And ...
- jQuery源码分析(九) 异步队列模块 Deferred 详解
deferred对象就是jQuery的回调函数解决方案,它解决了如何处理耗时操作的问题,比如一些Ajax操作,动画操作等.(P.s:紧跟上一节:https://www.cnblogs.com/grea ...
- Spring AMQP 源码分析 06 - 手动消息确认
### 准备 ## 目标 了解 Spring AMQP 如何手动确认消息已成功消费 ## 前置知识 <Spring AMQP 源码分析 04 - MessageListener> ## 相 ...
- Libev源码分析01:Libev中的监视器结构(C结构体实现继承)
在Libev的源码中,用到了一种用C实现类似C++中继承的技巧,主要是用宏和结构体实现. 在Libev中,最关键的数据结构就是各种监视器,比如IO监视器,信号监视器等等.这些监视器的多数成员都是一样的 ...
- libevent2源码分析之三:信号的初始化流程
libevent2对信号的响应也进行了封装,使之与socket操作一样对外提供统一的接口.这里的信号一般指linux的信号.由于信号与socket相关的编程接口有较大的不同,因此在内部实现也有一些区别 ...
- Libev源码分析08:Libev中的内存扩容方法
在Libev中,如果某种结构的数组需要扩容,它使用array_needsize宏进行处理,比如: array_needsize (int, fdchanges, fdchangemax, fdchan ...
随机推荐
- linux把普通用户添加到sudo组
一.linux下把普通用户添加到sudo组的方式: 1. root权限下, 先cd到/etc目录下 2. 由于sudoers文件为只读权限,所以需要添加写入权限,chmod u+w sudoers 3 ...
- windows服务器nginx日志分割
编写一个bat文件 @echo off rem @echo off rem 取1天之前的日期 echo wscript.echo dateadd(,date) >%tmp%\tmp.vbs fo ...
- IO流4 --- IO流体系 --- 技术搬运工(尚硅谷)
- oracle function学习1
oracle function学习基层: 函数就是一个有返回值的过程. 首先 知道oracle 使用限制: 函数调用限制: 1. SQL语句中只能调用存储函数(服务器端),而不能调用客户端 ...
- System V启动脚本启动的服务
/etc/rc.d/init.d/目录下的内容如下:这些常用的服务器都是System v的服务,要控制System V 的服务,我们可以使用以下命令 #/etc/rc.d/init.d/script ...
- Codeforces 1150D(字符串dp)
反思 三维的dp压根没看出来,看题解以后思路又很直观,找几道字符串dp练练才行 序列自动机和优化一维略 /* __ __ * ____| |_____| |____ * | | * | __ | * ...
- Spring 社区的首个国产开源项目顺利毕业
相信大家对上周的 <来自 Spring Cloud 官方的消息,Spring Cloud Alibaba 即将毕业>文章记忆犹新.本周,Spring Cloud Alibaba 正式毕业, ...
- 导入pymysql模块出错:No module named 'pymysql'
前提: 使用的版本为:Python 3.6.4 pymysql已经被成功安装了,并通过命令行的方式验证已成功安装. 但在pycharm中运行工程时候时候报错:No module named 'pymy ...
- Nginx教程(四) Location配置与ReWrite语法 (转)
Nginx教程(四) Location配置与ReWrite语法 1 Location语法规则 1.1 Location规则 语法规则: location [=|~|~*|^~] /uri/ {… } ...
- 【OI】二分图最大匹配
所谓二分图,是可以分为两个点集的图: 所谓二分图最大匹配,是两个点集之间,每两个不同点集的点连接,每个点只能连一个点,最大的连接数就是最大匹配. 如何解最大匹配,需要用到匈牙利算法. 另:本文写了很多 ...