104-信号引起的竞争错误

1. 引言

之前我们学习过使用 alarm 信号这种奇技淫巧来实现带超时的 IO 函数,一直以来,我们写的这种程序都带有一个隐含的 bug.

举例来说,我们可能经常会写下面这样的代码:

alarm(2); for(;;) {   addrlen = sizeof(cliaddr);    // 1. 如果信号在 recvfrom 执行前产生   nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &addrlen);      // 2. 如果信号在 recvfrom 返回后产生                                           if (nr < 0) {     if (errno == EINTR) break;           ERR_EXIT("recvfrom");   }   // ... }

如果,alarm 信号在注释 1 和 2 两者描述的情况中产生,这意味着 recvfrom 永远都不会被 alarm 信号打断。看起来这种情况似乎不太可能发生,但是谁又能保证一定不会发生呢?哪怕只有万分之一的可能性,我们也得避免。

2. 修改方案

一个直观的想法就是在 recvfrom 调用前后将信号阻塞掉,看起来像下面这样(伪代码):

// 0. 添加阻塞 sigprocmask(SIG_BLOCK, SIGALRM); alarm(2); for(;;) {   addrlen = sizeof(cliaddr);    // 1. 解除阻塞   sigprocmask(SIG_UNBLOCK, SIGALRM);   nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &addrlen);      // 2. 添加阻塞   sigprocmask(SIG_BLOCK, SIGALRM);    if (nr < 0) {     if (errno == EINTR) break;           ERR_EXIT("recvfrom");   }   // ... }

看起来似乎没什么问题,但是,在 sigprocmask <-> recvfrom <-> sigprocmask 之间,仍然有一个非常小的时间窗(time window),我们仍然不能保证在此期间不产生 alarm 信号,要是 sigprocmask <-> recvfrom <-> sigprocmask 是一个整体那就完美了——换句话说,我们希望第一次 sigprocmask 和 recvfrom 能同时执行,在 recvfrom 返回时同时执行第二次 sigprocmask。

这似乎不太可能,但 linux 提供了额外的方案,那就是 pselect 函数。

2.2 pselect 函数

pselect 函数只比 select 函数多一个参数

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

前面那些参数我们都非常熟悉了,这里只讲最后一个 sigmask,它是一个信号集。

该参数非常类似 sigsuspend 的参数,如果该 sigmask 不为 NULL,pselect 做两件事(原子的):

  • 将当前阻塞信号集替换成 sigmask 指定的信号集
  • 执行 select 函数

当 pselect 返回时,会恢复旧的阻塞信号集。

2.2 使用 pselect 修改竞争错误

sigset_t stalarm = {SIGALARM}; sigset_t stempty = {}; // 空 rfds = {sockfd}; maxfd = sockfd;  sigprocmask(SIG_BLOCK, &stalarm); // 先阻塞 alarm(2); for(;;) {   addrlen = sizeof(cliaddr);         FD_SET(sockfd, &rfds);   // 当调用 pselect 时会阻塞,同时愿意接收 alarm 信号,这两步是原子的。   // 直接有数据到来或者被信号打断。   ret = pselect(maxfd + 1, &rfds, NULL, NULL, NULL, &stempty);   if (ret < 0) {     if (errno == EINTR) break;           ERR_EXIT("pselect");   }   nr = recvfrom(sockfd, buf, 4096, 0, (struct sockaddr*)&cliaddr, &addrlen);   // ... } 

3. 实验

本文使用的程序工具托管在 gitos 上:http://git.oschina.net/ivan_allen/unp

本文实验程序路径:unp/program/broadcast/raceconditions

3.1 实验步骤

上一篇文章一样,在三台不同主机上开启 udp 服务器,然后在其中一台机器上发广播。

为了能演示出竞争错误,你可以在代码中加入 sleep 函数(当然,我们这个正确的版本是不会出现问题的):


这里写图片描述
图1 加入 sleep,等等信号发生


这里写图片描述
图2 仍然能够正常运行

你要是想看到有问题的版本,你可以在上一篇文章中相应的位置加入 sleep 函数,就能看到竞争错误的情况。

4. 总结

  • 知道信号带来的竞争错误
  • 知道为什么为产生竞争错误
  • 掌握 pselect 函数,解决竞争问题