You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程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;
}

关键修改点说明

  1. 移除线程代码:select本身就是用来单线程处理多个IO事件的,不需要多线程,避免竞态问题。
  2. 每次循环重置fd_set:因为select会修改传入的fd集合,把未就绪的fd移除,所以必须每次循环重新初始化并添加要监听的fd。
  3. 用FD_ISSET判断就绪fd:select返回后,遍历所有监听的fd,用FD_ISSET检查哪个fd在就绪集合中,然后处理对应的读取和输出。
  4. 处理select返回值:区分错误、超时、就绪三种情况,让逻辑更健壮。
  5. 检查recvfrom返回值:避免在读取失败(比如被信号中断)时做无效的后续处理。

额外提示

如果你的fd数量较多(比如超过1024),select会因为fd_set的大小限制而失效,这时候建议用epoll(Linux)或者kqueue(BSD/macOS)这些更高效的多路复用机制,它们没有fd数量限制,性能也更好。

内容的提问来源于stack exchange,提问作者user786

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:07:04