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 函数,解决竞争问题