在工作中遇到过两类异常场景。
其一是在系统启动的时候,sleep调用被打断。导致原本预期延时睡眠1分钟,但实际却不到1分钟就继续执行的情况,导致一些模块提前启动的情况,彼时采用循环sleep的方式规避。
for(i= 0;i<60;i++){
sleep(1);
}
其二是sem_timedwait系统调用中断的问题,因使用sleep延时显然是一种不严谨的做法。故而引入了sem_timedwait按照一定地超时时长等待信号量。但实际使用中却出现还没有调用sem_post,也没有到达超时时间,但是sem_timedwait却已经往下执行的情况,看打印发现是 sem_timedwait: Interrupted system call。在网上搜索,从stackoverflow( https://stackoverflow.com/questions/52331735/understanding-sem-timedwait
)上找到发现是需要在系统调用被中断时手动恢复,介绍的最佳实践如下:
while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR)
continue; /* Restart if interrupted by handler */
可以理解为在系统错误码为EINTR,即中断错误时,继续执行。这里我最疑惑的地方在于理论上我们的主进程不太涉及到信号的使用,只捕捉了段错误信号,忽略了SIGPIPE等信号。但此处并未发生段错误,究竟是触发了什么信号导致了这个现象呢?。
这让我联想到了在使用gdb运行程序时,会存在信号挂起的问题,需要调用handle SIG32 nostop和 handle SIG33 nostop来使得在收到信号时不挂起。这里的32和33信号大概率应该就是导致上述系统调用被中断的原因。
适当查询资料可知(参考https://www.cnblogs.com/gnivor/p/11719009.html) kill -l中没有32和33信号。原因是
这是因为NPTL。由于它是GNU C库的一部分,几乎每个现代Linux发行版都不再使用前两个实时信号了。 NPTL是POSIX Threads的实现。 NPTL内部使用前两个实时信号。
这部分在 man 7 signal中可以确认如下
The Linux kernel supports a range of 33 different real-time signals, num‐
bered 32 to 64. However, the glibc POSIX threads implementation inter‐
nally uses two (for NPTL) or three (for LinuxThreads) real-time signals
(see pthreads(7)), and adjusts the value of SIGRTMIN suitably (to 34 or
35). Because the range of available real-time signals varies according to
the glibc threading implementation (and this variation can occur at run
time according to the available kernel and glibc), and indeed the range of
real-time signals varies across UNIX systems, programs should never refer
to real-time signals using hard-coded numbers, but instead should always
refer to real-time signals using the notation SIGRTMIN+n, and include
suitable (run-time) checks that SIGRTMIN+n does not exceed SIGRTMAX.
故而按照上述第二句话的说明,glibc的posix线程实现在内部使用了 两个或者 三个实时信号。在这里使用两个的话便是32和33了。
如下linux手册页面给出了部分答案(https://www.onitroad.com/jc/linux/man-pages/linux/man7/nptl.7.html)
NPTL(本地POSIX线程库)是在现代Linux系统上使用的GNU C库POSIX线程实现。
NPTL内部使用前两个实时信号(信号编号32和33)。这些信号之一用于支持线程取消和POSIX计时器(请参见timer_create(2))。另一个用作机制的一部分,以确保进程中的所有线程始终具有POSIX要求的相同的UID和GID。这些信号不能在应用中使用。
在Linux内核级别,凭据(用户和组ID)是每个线程的属性。但是,POSIX要求进程中的所有POSIX线程都具有相同的凭据。为了适应此需求,NPTL实现将所有更改流程凭据的系统调用包装为功能,这些功能除了调用基础系统调用之外,还安排流程中的所有其他线程也更改其凭据。
每个这些系统调用的实现都涉及使用实时信号,该信号被发送(使用tgkill(2))到必须更改其凭据的每个其他线程。在发送这些信号之前,正在更改凭据的线程将保存新的凭据,并将正在使用的系统调用记录在全局缓冲区中。接收线程中的信号处理程序会获取此信息,然后使用相同的系统调用来更改其凭据。
为setgid(2),setuid(2),setegid(2),seteuid(2),setregid(2),setreuid(2),setresgid(2),setresuid(2)和setgroup(2)提供了采用此技术的包装函数。
为实验该问题,以实际工程中的应用为例,在不调用handle SIG32 nostop和 handle SIG33 nostop的情况下通过gdb启动程序,在收到信号时调用thread apply all bt 打印所有的线程栈来观察实际结果。
可以看到在收到信号33时,线程栈的部分打印如下,
Program received signal SIG33, Real-time event 33.
(gdb) thread apply all bt
Thread 110 (LWP 8443):
#0 0x0000007fb7e57088 in setxid_signal_thread () from /lib//libpthread.so.0
#1 0x0000007fb7e57798 in __nptl_setxid () from /lib//libpthread.so.0
#2 0x0000007fb7bd6840 in setgid () from /lib//libc.so.6
...
可见因为调用了setgid函数,与上述linux手册页的描述一致。故而如果在系统中的任意模块存在调用修改用户属性的调用时,系统都是有可能收到33信号的,32信号的场景此处暂未捕捉到。
关于我们在何时需要关注EINTR错误,何时又不需要关注,如下这篇文章做了一部分的解释
https://developer.aliyun.com/article/374822
如果需要使用到注册信号处理函数,那么需要再注册signal handler时设置 SA_RESTART flag,保证在信号处理结束后,所有相关的系统调用都会被重庆新启动。否则的话,就会是以errno=EINTR的形式失败。
如果是要考虑类似上述33和32的暂停信号对系统调用或者库函数的影响。个人认为man 7 signal中的最后部分也是可以重点参考的
Interruption of system calls and library functions by stop signals
On Linux, even in the absence of signal handlers, certain blocking interfaces can fail with the error EINTR after the process is stopped by one of the stop signals and then resumed via SIGCONT.
This behavior is not sanctioned by POSIX.1, and doesn't occur on other systems.
The Linux interfaces that display this behavior are:
* "Input" socket interfaces, when a timeout (SO_RCVTIMEO) has been set on the socket using setsockopt(2): accept(2), recv(2), recvfrom(2), recvmmsg(2) (also with a non-NULL timeout argument),
and recvmsg(2).
* "Output" socket interfaces, when a timeout (SO_RCVTIMEO) has been set on the socket using setsockopt(2): connect(2), send(2), sendto(2), and sendmsg(2), if a send timeout (SO_SNDTIMEO) has
been set.
* epoll_wait(2), epoll_pwait(2).
* semop(2), semtimedop(2).
* sigtimedwait(2), sigwaitinfo(2).
* Linux 3.7 and earlier: read(2) from an inotify(7) file descriptor
* Linux 2.6.21 and earlier: futex(2) FUTEX_WAIT, sem_timedwait(3), sem_wait(3).
* Linux 2.6.8 and earlier: msgrcv(2), msgsnd(2).
* Linux 2.4 and earlier: nanosleep(2).
上述列举的系统调用或者库函数,包括设置了超时的输入或者输出套接字上,epoll相关系统调用,inotify调用,以及一定版本之前的sem_timedwait调用和nanosleep调用。目前使用的版本为linux4.4但还是存在该问题。nanosleep调用也会被中断。该点根因尚未可知。上述函数在调用时,都需要考虑 errno=INTR形式的错误。同时如开篇所述,stackoverflow 已经给出了一个比较好的实践,可考虑迁移到其他同样存在该被中断问题的调用上。
while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR)
continue; /* Restart if interrupted by handler */