《UNIX网络编程 卷1》 笔记: 竞争条件!

来源:互联网 发布:手机淘宝客户端 编辑:程序博客网 时间:2024/06/09 19:33

本节书中将race condition翻译成竞争状态,并定义为:当有多个进程访问共享的数据,而正确的结果取决于进程的执行顺序时 ,我们称这些进程处于竞争状态。而我更喜欢将这个词称为竞争条件:如果一个程序的执行结果取决于进程(运行中的程序称为进程)的执行顺序,那么我们说这个程序包含有竞争条件。

进程的执行顺序是什么意思呢?我们知道Linux是多任务操作系统,系统中同时运行着多个进程(任务)。当我们执行一个程序时,它并不是一直独占CPU运行到结束,而是由内核中的进程调度模块负责调度。这个调度模块可能先让我们的进程运行一段时间,然后又让我们的进程睡眠转而让其他的进程运行。由于进程调度频率非常高,我们甚至感觉不到它的存在。在单处理机系统上,这种高频率的进程调度就让我们感觉到“同时”有多个进程在运行。所以说程序的执行结果取决于进程的执行顺序实际是指结果取决于内核的进程调度。

另外在这里必须要简单讲讲Linux的信号机制,它非常重要!一个信号的产生表明发生了一个异步事件,异步是指这个事件在进程运行过程中任意时刻都可能发生。举例来说,alarm函数允许我们设定一个定时器,在超时后产生SIGALRM信号。这个信号实际是由内核产生并递交给进程,然后进程捕获该信号并执行信号处理函数。在信号被内核递交给进程之前,如果该信号被进程阻塞了,则信号就被保留在内核中,直到进程解除阻塞该信号才被内核递交。

那么信号与进程调度有什么关系?简单来说内核递交信号给进程之前必然会发生进程调度。

下面我们看看上节包含竞争条件的dg_cli函数片段:

while (Fgets(sendline, MAXLINE, fp) != NULL) { /*从标准输入读取数据*/Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); /*发送给服务器*/alarm(5); /*设定5秒的定时器*/for ( ; ; ) {len = servlen;n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len); /*读取服务器回射的数据*/if (n < 0) {if (errno == EINTR) /*系统调用被信号中断则退出for循环*/break;elseerr_sys("recvfrom error");} else {recvline[n] = 0;printf("from %s: %s", Sock_ntop_host(preply_addr, len), recvline);}}}
我们本意是发送数据后设置一个5秒的定时器,超时后产生SIGALRM信号打断recvfrom调用,退出for循环,然后再从标准输入获取数据发送。但实际上可能会发生这种情况:recvfrom函数已经成功读取服务器回射的最后一行数据,正在else分支执行某个语句。此时定时器超时了,产生了SIGALRM信号并立即被进程处理,然后进程又执行for循环,阻塞在recvfrom调用中。由于已经没有数据可读取而且信号已经被处理,recvfrom调用会永久地阻塞,换言之,进程永久地进入了睡眠状态。

该问题的根本原因就是SIGALRM信号可以在进程运行的任意时刻被递交(进程的运行可以在任意时刻被内核进程调度模块中断,然后递交信号),而不是我们想的进程在执行recvfrom调用时被递交。为了解决这个问题,我们想到可以在执行recvfrom调用前先解阻塞SIGALRM信号,执行之后再阻塞SIGALRM信号,这样即使信号不是在执行recvfrom调用时产生,也会被内核保留直到解除阻塞(调用recvfrom之前)后才被递交。于是我们写出了下面的代码:

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){int n;const int on = 1;char sendline[MAXLINE], recvline[MAXLINE + 1];sigset_t sigset_alrm;socklen_t len;struct sockaddr *preply_addr;preply_addr = Malloc(servlen);/*设置允许发送广播报文选项*/Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));Sigemptyset(&sigset_alrm);Sigaddset(&sigset_alrm, SIGALRM);Signal1(SIGALRM, recvfrom_alarm);while (Fgets(sendline, MAXLINE, fp) != NULL) {Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);alarm(5);for ( ; ; ) {len = servlen;Sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL); /*解阻塞SIGALRM信号*/n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);Sigprocmask(SIG_BLOCK, &sigset_alrm, NULL); /*阻塞SIGALRM信号*/if (n < 0) {if (errno == EINTR)break;elseerr_sys("recvfrom error");} else {recvline[n] = 0;printf("from %s: %s", Sock_ntop_host(preply_addr, len), recvline);}}}free(preply_addr);}
很不幸,这样的代码还是不正确的。因为信号可以在执行recvfrom调用和执行sigprocmask调用(阻塞SIGALRM信号)之间被递交。根本原因是从recvfrom调用返回到执行sigprocmask调用这需要执行两个系统调用,这两种操作不是原子的操作,在这之间可能发生信号的递交。

正确办法之一是使用pselect函数,它先设置信号掩码(解除阻塞SIGALRM信号),然后阻塞监听描述符,返回后又恢复信号掩码(阻塞SIGALRM信号),这三个动作在调用进程看来都是原子操作,中间不会发生信号的递交。代码如下:

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){int n;const int on = 1;char sendline[MAXLINE], recvline[MAXLINE + 1];fd_set rset;sigset_t sigset_alrm, sigset_empty;socklen_t len;struct sockaddr *preply_addr;preply_addr = Malloc(servlen);/*设置允许发送广播报文选项*/Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));Sigemptyset(&sigset_empty);Sigemptyset(&sigset_alrm);Sigaddset(&sigset_alrm, SIGALRM);Signal1(SIGALRM, recvfrom_alarm);while (Fgets(sendline, MAXLINE, fp) != NULL) {Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);/*阻塞SIGALRM信号*/Sigprocmask(SIG_BLOCK, &sigset_alrm, NULL);alarm(5);for ( ; ; ) {/*监听套接字描述符*/FD_SET(sockfd, &rset);n = pselect(sockfd + 1, &rset, NULL, NULL, NULL, &sigset_empty);if (n < 0) {if (errno == EINTR)break;elseerr_sys("pselect error");} else if (n != 1)err_sys("pselect error: returned %d", n);len = servlen;n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);recvline[n] = 0;printf("from %s: %s", Sock_ntop_host(preply_addr, len), recvline);}}free(preply_addr);}

这样信号只能在执行pselect时被递交(pselect系统调用被打断),recvfrom调用也不会被阻塞。

第二种方法是使用管道,在for循环中使用select监听套接字和管道描述符,在超时信号处理函数中,向管道发送1字节数据,这样不论信号在什么时候被递交,select都能监听到管道描述符的读事件,从而退出循环。代码如下:

static void recvfrom_alarm(int);static int pipefd[2];void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){int n, maxfdp1;const int on = 1;char sendline[MAXLINE], recvline[MAXLINE + 1];fd_set rset;socklen_t len;struct sockaddr *preply_addr;preply_addr = Malloc(servlen);/*设置允许发送广播报文选项*/Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));Pipe(pipefd);maxfdp1 = max(sockfd, pipefd[0]) + 1;FD_ZERO(&rset);Signal1(SIGALRM, recvfrom_alarm);while (Fgets(sendline, MAXLINE, fp) != NULL) {Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);alarm(5);for ( ; ; ) {/*监听套接字描述符*/FD_SET(sockfd, &rset);/*监听管道读事件*/FD_SET(pipefd[0], &rset);if ((n = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {if (errno == EINTR)continue;elseerr_sys("select error");}if (FD_ISSET(sockfd, &rset)) { /*有数据可读取*/len = servlen;n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);recvline[n] = 0;printf("from %s: %s", Sock_ntop_host(preply_addr, len), recvline);}if (FD_ISSET(pipefd[0], &rset)) { /*超时*/Read(pipefd[0], &n, 1);break;}}}free(preply_addr);}static void recvfrom_alarm(int signo){Write(pipefd[1], "", 1);return;}
第三种方法是使用非局部跳转函数siglongjmp,使用该函数可以从信号处理函数中跳到主程序for循环,然后退出循环。代码如下:

static void recvfrom_alarm(int);static sigjmp_buf jmpbuf;void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){int n;const int on = 1;char sendline[MAXLINE], recvline[MAXLINE + 1];socklen_t len;struct sockaddr *preply_addr;preply_addr = Malloc(servlen);/*设置允许发送广播报文选项*/Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));Signal1(SIGALRM, recvfrom_alarm);while (Fgets(sendline, MAXLINE, fp) != NULL) {Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);alarm(5);for ( ; ; ) {if (sigsetjmp(jmpbuf, 1) != 0) /*设定siglongjmp跳转的位置*/break;len = servlen;n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);recvline[n] = 0;printf("from %s: %s", Sock_ntop_host(preply_addr, len), recvline);}}free(preply_addr);}/*当信号被递交时,调用siglongjmp。这会使dg_cli函数中的sigsetjmp返回, 返回值为siglongjmp的第二个参数,它必须是一个非0值*/static void recvfrom_alarm(int signo){/*跳转到sigsetjmp函数的位置执行sigsetjmp函数*/siglongjmp(jmpbuf, 1);}
这种方法虽然缺少健壮性(请参考书中注解),但是有必要让读者知道,有时使用非局部跳转技术非常方便。下节中,我们就会使用它来实现一个可靠的UDP发送函数。

原创粉丝点击