之前我们学习过使用 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 信号打断。看起来这种情况似乎不太可能发生,但是谁又能保证一定不会发生呢?哪怕只有万分之一的可能性,我们也得避免。
一个直观的想法就是在 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 函数。
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 做两件事(原子的):
当 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);
// ...
}
本文使用的程序工具托管在 gitos 上:http://git.oschina.net/ivan_allen/unp
本文实验程序路径:unp/program/broadcast/raceconditions
上一篇文章一样,在三台不同主机上开启 udp 服务器,然后在其中一台机器上发广播。
为了能演示出竞争错误,你可以在代码中加入 sleep 函数(当然,我们这个正确的版本是不会出现问题的):
你要是想看到有问题的版本,你可以在上一篇文章中相应的位置加入 sleep 函数,就能看到竞争错误的情况。