多线程Socket场景下select()必要性及就绪文件描述符检测问询
关于你的Raw Socket+Select+线程代码的疑问解答
首先直接说结论:你当前代码里的select不仅完全没必要,而且用法错误,根本起不到预期作用,咱们来一步步拆解原因,再给你实现“主线程检测就绪fd并输出”的正确方案。
为什么当前的select没用?
你的代码逻辑存在几个核心问题:
- 你创建了两个子线程,每个线程拿着一个raw socket在无限循环调用
recvfrom——这意味着只要fd有数据,子线程会立刻把数据读走,主线程的select根本没机会检测到fd的就绪状态,因为数据已经被线程取走了。 - 你的select用法有致命错误:每次调用
select后,内核会修改传入的fd_set,把未就绪的fd从集合中移除,但你在循环里没有重新把fd1和fd2加回readfds,所以第一次select之后,后续的select其实是在监听一个空集合(或者只剩第一次就绪的fd),完全起不到持续检测的作用。 - 同一个fd被主线程的select和子线程的
recvfrom同时操作,会产生竞态条件:比如select刚检测到fd就绪,正要处理,结果子线程的recvfrom已经把数据读走了,这时候主线程再去读就会阻塞,反之亦然,逻辑完全混乱。
如何在主线程检测就绪的文件描述符?
要实现主线程统一处理所有IO事件,你需要去掉子线程,用select(或者更高效的epoll/poll)做IO多路复用,单线程管理所有fd,这样既不会有竞态,也能准确检测哪个fd就绪。
修改后的代码示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #include <sys/select.h> int main() { // 创建两个Raw Socket int fd1 = socket(PF_INET, SOCK_RAW, IPPROTO_TCP); int fd2 = socket(PF_INET, SOCK_RAW, IPPROTO_TCP); if (fd1 < 0 || fd2 < 0) { perror("socket() error"); exit(-1); } // 设置IP_HDRINCL选项 int one = 1; const int *val = &one; if(setsockopt(fd1, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0) { perror("setsockopt() error on fd1"); exit(-1); } if(setsockopt(fd2, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0) { perror("setsockopt() error on fd2"); exit(-1); } // 确定select需要监听的最大fd+1 int nfds = (fd1 > fd2 ? fd1 : fd2) + 1; struct timeval timeout; timeout.tv_sec = 10; timeout.tv_usec = 0; while(1) { // 每次循环都要重新初始化fd_set,因为select会修改它 fd_set readfds; FD_ZERO(&readfds); FD_SET(fd1, &readfds); FD_SET(fd2, &readfds); // 调用select等待就绪的fd int ret = select(nfds, &readfds, NULL, NULL, &timeout); if (ret < 0) { perror("select() error"); continue; } else if (ret == 0) { printf("Main: select timed out\n"); continue; } // 检查fd1是否就绪 if (FD_ISSET(fd1, &readfds)) { char buffer[64000]; struct sockaddr_in saddr_in; socklen_t in_len = sizeof(saddr_in); ssize_t recv_len = recvfrom(fd1, buffer, sizeof(buffer), 0, (struct sockaddr*)&saddr_in, &in_len); if (recv_len > 0) { struct iphdr *h = (struct iphdr *)buffer; struct sockaddr_in t; t.sin_addr.s_addr = h->saddr; char *str = inet_ntoa(t.sin_addr); printf("fd1 received from %s\n", str); } } // 检查fd2是否就绪 if (FD_ISSET(fd2, &readfds)) { char buffer[64000]; struct sockaddr_in saddr_in; socklen_t in_len = sizeof(saddr_in); ssize_t recv_len = recvfrom(fd2, buffer, sizeof(buffer), 0, (struct sockaddr*)&saddr_in, &in_len); if (recv_len > 0) { struct iphdr *h = (struct iphdr *)buffer; struct sockaddr_in t; t.sin_addr.s_addr = h->saddr; char *str = inet_ntoa(t.sin_addr); printf("fd2 received from %s\n", str); } } printf("Main: completed one select cycle\n"); sleep(1); } // 关闭socket(虽然循环不会退出,但好习惯) close(fd1); close(fd2); return 0; }
关键修改点说明
- 移除线程代码:select本身就是用来单线程处理多个IO事件的,不需要多线程,避免竞态问题。
- 每次循环重置fd_set:因为select会修改传入的fd集合,把未就绪的fd移除,所以必须每次循环重新初始化并添加要监听的fd。
- 用FD_ISSET判断就绪fd:select返回后,遍历所有监听的fd,用
FD_ISSET检查哪个fd在就绪集合中,然后处理对应的读取和输出。 - 处理select返回值:区分错误、超时、就绪三种情况,让逻辑更健壮。
- 检查recvfrom返回值:避免在读取失败(比如被信号中断)时做无效的后续处理。
额外提示
如果你的fd数量较多(比如超过1024),select会因为fd_set的大小限制而失效,这时候建议用epoll(Linux)或者kqueue(BSD/macOS)这些更高效的多路复用机制,它们没有fd数量限制,性能也更好。
内容的提问来源于stack exchange,提问作者user786
相关产品推荐
相关产品推荐

