一、系统调用中断(EINTR错误)
- 系统调用中断:如果进程在执行一个低速系统调用,并且该系统调用是阻塞形式的,如果阻塞期间捕捉到一个信号,那么该系统调用就会被中断,然后出错返回,并将errno设置为EINTR
- 备注:这里必须区分系统调用和函数。当捕捉到某个信号时,被中断的是内核中执行的系统调用
读写函数返回值的历史变更(读写部分数据)
- 对于read()、write()这样读写数据的系统调用,历史上对它们返回值做过一系列的变更。下面我们以read()和write()函数为例,recv()、send()等函数也是相同的道理
- 读数据:如果read系统调用已经接收并将数据保存到应用程序缓冲区,但尚未接收到应用程序请求的全部数据,此时被中断:
- 一种处理方式是:认为该系统调用失败,并将errno设置为EINTR
- 另一种处理方式是:允许该系统调用成功返回,返回值是已接收到的数据量(现代编程就是这样使用的)
- 写数据:与read类似,如果write只向应用程序缓冲区传输了部分数据,此时被中断:
- 一种处理方式是:认为该系统调用失败,并将errno设置为EINTR
- 另一种处理方式是:允许该系统调用成功返回,返回值是已写入的数据量(现代编程就是这样使用的)
- 历史发展:
- 历史上,对于处理已read、write部分数据量的相应系统调用,早期版本允许实现自行选择。例如从System V派生的实现将这种系统调用视为失败,而BSD派生的实现则处理为部分返回成功
- 2001版POSIX.1标准之后其采用BSD风格的语义
- 备注:此处我们讨论的是读写部分数据的情况,对于没有读写任何数据的情况,仍然是出错返回并将errno设置为EINTR
二、低速系统调用(慢系统调用)
- 我们将系统调用分为两类:低速系统调用和其他系统调用
- 低速系统调用是可能使进程永远阻塞的一类系统调用,包括:
- 如果某些类型文件(如读管道、终端设备和网络设备)的数据不存在,则读操作(例如调用recv())可能会使调用者永远阻塞
- 如果数据不能被相同的类型文件立即接收,则写操作(例如调用send())可能会使调用者永远阻塞
- 在某种条件发生之前打开某些类型文件,可能会发生阻塞(例如要打开一个终端设备,需要先等待与之连接的调制解调器应答)
- pause函数(按照定义,它使调用进程休眠直至捕捉到一个信号)和wait函数
- 某些ioctl操作
- 某些进程间通信函数
- 在这些低速系统调用中,一个值得注意的例外是与磁盘I/O有关的系统调用:虽然读、写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,I/O操作总是很快返回,并使调用者不再处于阻塞状态
三、系统调用自动重启
- 可以自动重启的意思是:系统调用接收到中断后出错返回,并将errno设置为-1,那么在后面的代码中我们仍然可以再次调用该函数(并不是系统自动帮我们调用,是要我们手动去调用的,请别被“自动”两个字迷惑了)
四、不同系统对“系统调用自动重启”的处理
- 4.2BSD:
- 为了帮助应用程序使其不必处理被中断的系统调用,4.2BSD引进了某些被中断系统调用的自动重启动。自动重启动的系统调用包括:ioctl、read、readv、write、writev、wait、waitpid(如前所述,前5个函数只有对低速设备进行操作时才会被信号中断,后两个函数在捕捉到信号时总是被中断)
- 4.2BSD引入自动重启动功能的一个理由是:有时用户并不知道所使用的输入、输出设备是否是低速设备。如果我们编写的程序可以用交互方式运行,则它可能读、写终端低速设备。如果在程序中捕捉信号,而且系统并不提供重启动功能,则对每次读、写系统调用就要进行是否出错返回的测试,如果是被中断的,则再调用读、写系统调用
- 4.3BSD:因为系统调用自动重启的处理方式也会带来问题,某些应用程序并不希望这些函数被中断后重启,因此4.3BSD允许进程基于每个信号禁用此功能
- sigaction()函数:sigaction()函数所处理的struct sigaction结构体有一个sa_flags成员,当该成员SA_RESTART标志设置时才重启系统调用。SA_INTERRUPT标志是用来设置不重启系统调用
- signal()函数:历史上,使用signal()函数建立信号处理程序时,对于如何处理被中断的系统调用,各种实现的做法不同:
- System V的默认工作方式是从不重启系统调用
- BSD则重启被信号中断的系统调用
- FreeBSD 8.0、Linux 3.2.0、Mac OS 10.6.8中,当信号处理程序是用signal函数时,被中断的系统调用会重启动
- Solaris 10的默认方式是出错返回,将errno设置为EINTR
- 下面列出了几种实现所提供的与信号有关的函数及他们的语义:
- 备注:其他厂商提供的UNIX系统可能不同于上图的情况。例如,SunOS 4.1.2中的sigaction默认方式是重启动被中断的系统调用
附加
五、演示案例(以accept为例)
- 下面我们以accrpt()函数为例,如果其在阻塞等待客户端连接期间接收到信号,那么出错返回之后再次进行accept(),而不是让其退出程序或不再进行accept()
// 持续接收客户端连接
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR) //如果accept阻塞期间接收到信号, 那么就继续accept()
continue;
else // 如果是其他错误, 那么就退出循环
err_sys("accept error");
}
六、connect不能够重新启动
- connect函数我们不能重启:如果该函数返回EINTR,我们就不能再次调用它,否则将立即返回一个错误
- 当connect被一个捕获的信号中断而且不自动重启(TCPv2第466页)时,我们必须调用select来等待连接完成
七、信号与可重入函数
信号中断可能存在的问题
- 进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如没有调用exit或longjmp),则继续执行在捕捉时进程正在执行的正常序列(这类似于法神硬件中断时所做的)
- 但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。例如:
- 如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,该信号处理程序中又调用了malloc,这时会发生什么?结果可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时,进程可能正在更改此链表
- 又例如,若进程正在执行getpwnam这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数,这时又会发生什么呢?结果可能是返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖
可重入函数
- Single UNIX Specification说明了信号处理程序中保证调用安全的函数。这些函数是可重入的并被称为"异步信号安全的"。除了可重入以外,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送
- 下图列出了这些异步信号安全的函数
- 没有位于上图的大多数函数是不可重入的,因为:
- (a)已知它们使用静态数据结构
- (b)它们调用malloc或free
- (c)它们是标准I/O函数。标准I/O库的很多实现都以不可重入方式使用全局数据结构
- 注意:上图没有包括longjmp和siglongjmp。这是因为主例程以非可重入方式正在更新一个数据结构时可能产生信号。如果不是从信号处理程序返回而是调用siglongjmp,那么该数据结构可能是部分更新的。如果应用程序将要更新全局数据结构这样的事情,而同时要捕捉某些信号,而这些信号的处理程序又会引起执行siglongjmp,则在更新这种数据结构时要阻塞此类信号
errrno的处理
- 应当了解,即使信号处理程序调用的上图中的函数,但是由于每个线程只有一个errno变量(参阅https://blog.csdn.net/qq_41453285/article/details/88924769中对errno和线程的讨论),所以信号处理程序可能会修改其原先值
- 考虑一个信号处理程序,它恰好在main刚设置errno之后被调用。如果该信号处理程序调用read这类函数,则它可能更改errno的值,从而取代了刚由main设置的值
- 因此,作为一个通用的规则,当在信号处理程序中调用上图的函数时,应当在调用前保存errno,在调用后恢复errno。(应当了解,经常被捕捉到的信号是SIGCHLD,其信号处理程序通常要调用一种wait函数,而各种wait函数都能改变errno)
演示案例
- 下面给出了一个程序,这段程序从信号处理程序my_alarm调用非可重入函数getpwnam,而my_alarm每秒钟被调用一次(alarm函数每秒产生一次SIGALRM信号)
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pwd.h>
static void my_alarm(int signo)
{
struct passwd *rootptr;
printf("in signal handler\n");
if((rootptr = getpwnam("root")) == NULL)
perror("getpwnam(root) error");
alarm(1);
}
int main()
{
struct passwd* ptr;
// 绑定SIGALRM信号处理函数
signal(SIGALRM, my_alarm);
// 1秒之后产生一个SIGALRM信号
alarm(1);
for(;;)
{
if((ptr = getpwnam("ubuntu")) == NULL)
perror("getpwnam error");
if(strcmp(ptr->pw_name, "ubuntu") != 0)
printf("return value corrupted!, pw_name = %s\n", ptr->pw_name);
}
return 0;
}
- 运行该程序时,结果具有随机性。通常,在信号处理程序经多次迭代返回时,该程序将由SIGSEGV信号终止。检查core文件,从中可以看到main函数已调用getpwnam,但当getpwnam调用free时,信号处理程序中断了它的运行,并调用getpwnam,但当getpwnam调用free时,信号处理程序中断了它的运行,并调用getpwnam,进而再次调用free。在信号处理程序调用free而主进程也在调用free时,malloc和free维护的数据结构就出现了损坏,偶然,此程序会运行若干秒,然后因产生SIGSEGV信号而终止。在捕捉到信号后,若main函数仍正确运行,其返回值却有时错误,有时正常
- 从这个例子可以看出,如果在信号处理程序中调用一个非可重入函数,则其结果是不可预知的