[译]pselect()系统调用
Linux Linux
Lastmod: 2019-04-09

译自 [The new pselect() system call] https://lwn.net/Articles/176911/

诸如网络服务器等的应用需要使用select()poll(),或者epoll_wait()(Linux独有)等系统调用来对多个文件描述符进行监控,但有时会面临这样一个问题:如何等待到其中一个文件描述符变为ready状态,或者传递一个signal(比如SIGINT)。事实证明,这些系统调用不能很好地与信号进行交互。

一个看似显而易见的解决方案是为信号写一个空的处理器,以便传递信号以中断select()的调用。

static void handler(int sig) {}
    
int main(int argc, char *argv[])
{
	fd_set readfds;
	struct sigaction sa;
	int nfds, ready;

	sa.sa_handler = handler;     /* Establish signal handler */
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sigaction(SIGINT, &sa, NULL);
	/* ... */    
	ready = select(nfds, &readfds, NULL, NULL, NULL);
	/* ... */

select()返回之后,我们就可以通过查看函数的返回值以及错误代码errno来确定发生了什么。如果errnoEINTR,我们就知道select()的调用被信号给中断,并能做出相应的行动。但这种解决方案存在竞态条件:如果信号SIGINT是在调用了sigaction()函数之后,但在调用select()之前传递过去的,那么它将无法中断select()调用,信号已经丢失了。

我们也可以尝试其他的方案,比如在信号处理器里设置一个全局标志,然后在主程序里监控这个标志,并使用sigprocmask()来阻塞那个信号直到调用了select()。然而,这些技术都不能完全消除竞态条件:因为在select()调用开始之前,总是有一段时间间隔(无论多么短)可以处理信号。

这个问题的传统解决方案是所谓的自管(self-pipe)技巧,通常认为是由D J Bernstein首创的。使用这个技术,程序建立一个信号处理器,该信号处理器将一个字节写入一个特别建立的管道之中,该管道的读取端也将由select()进行监控。self-pipe技术很巧妙的解决了安全的等待文件描述符变为ready状态或者需要传递信号的问题。然而,这需要通过大量的代码来解决一个本质上很简单的需求。(例如,一个健壮的解决方案需要同时标记管道的读写端。)

出于此因,POSIX.1g委员会设计了一个select()的增强版本,也就是pselect()select()pselect()之间最主要的区别就是后者有一个sigset_t类型的信号掩码(sigmask)作为额外的参数:

int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
            const struct timespec *timeout,
            const sigset_t *sigmask);

sigmask参数指定了一个应该在pselect()调用期间期间阻塞的信号集合,它会在调用期间覆盖当前的信号掩码。当我们做以下调用时:

 ready = pselect(nfds, &readfds, &writefds, &exceptfds, timeout, &sigmask);

内核会执行一系列步骤,这相当于原子地执行以下系统调用:

sigset_t sigsaved;

sigprocmask(SIG_SETMASK, &sigmask, &sigsaved);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &sigsaved, NULL);

一段时间以来,glibc提供了pselect()的库实现,它实际上也使用了上面的系统调用序列。问题是,这种实现依然很容易受到pselect()用来避免竞态条件的设计的影响,因为单独的系统调用并不是原子的。

使用pselect(),我们可以安全地等待信号传递或者文件描述符进入ready状态,只需将我们前面的示例代码修改为:

sigset_t emptyset, blockset;

sigemptyset(&blockset);         /* Block SIGINT */
sigaddset(&blockset, SIGINT);
sigprocmask(SIG_BLOCK, &blockset, NULL);

sa.sa_handler = handler;        /* Establish signal handler */
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);
    
/* Initialize nfds and readfds, and perhaps do other work here */
/* Unblock signal, then wait for signal or ready file descriptor */

sigemptyset(&emptyset);
ready = pselect(nfds, &readfds, NULL, NULL, NULL, &emptyset);
... 

这段代码可以工作,因为SIGINT信号只有在控制传递给内核之后才会被解除阻塞。因此,在pselect()执行之前,无法传递信号。如果信号是在pselect()被阻塞时生成的,那么与select()一样,系统调用将被中断,且信号将在系统调用返回之前传递完成。

虽然pselect()是几年前构思的,也已经在1998由W. Richard Stevens在其著作*Unix Network Programming, vol. 1, 2nd ed.*中公布,但实际的实现却很晚才出现。随着2.6.16版本的释出,以及更新的glibc2.4,Linux才可以使用pselect()

Linux 2.6.16同时也包含了一个新的(但不是标准的)ppoll()系统调用,它也是在传统的poll()接口中增加了一个信号掩码参数:

int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *timeout, 
             const sigset_t *sigmask);

ppoll()增添了与pselect()相对于select()相同的功能。为了不受冷落,epoll维护者在pipeline中添加了补丁,以新的epoll_pwait()系统调用的形式添加类似的功能。

pselect()ppoll()与传统方法相比还有一些其他的细微差别。例如超时的类型为:

struct timespec {
	long tv_sec;        /* Seconds */
	long tv_nsec;       /* Nanoseconds */
};

这允许用比以前的系统调用更高的精度来指定超时间隔。

pselect()select()glibc wrappers还隐藏了一些底层系统调用细节:

首先,系统调用实际上期望信号掩码由两个参数来进行描述,其中一个参数是一个指向sigset_t结构的指针,而另一个参数是一个以字节表示该结构大小的整数。这使得将来可能会有更大的sigset_t类型。

底层系统调用还修改了他们的超时参数,以便在函数提前返回时(文件描述符进入ready状态,或者传递了信号),调用方能够知道剩余的超时时间。但是,各个wrapper函数通过创建超时参数的本地副本并将该副本传递给底层系统调用来隐藏其详情。(Linux的select()也修改了其超时参数,并且对应用程序是可见的。不过许多其他的select()实现并没有修改此参数。POSIX.1承认他们之中的任何一种实现。)

有关pselect()ppoll()的详细信息可以在man page中查询select(2)poll(2)

原文:

The new pselect() system call - [LWN.net]