对select()参数fdset的完整理解

虽然写了很多代码,但select我就从没有完整理解过,要用时不过copy paste而已。惭愧!


今天决定要对select()参数fdset有一个完整理解。Go!


先上一段代码(代码1-1),这段代码做的事情是

1. 创建一个socket来listen请求

2. 调用select等待新请求、等待已有请求的数据收发状态READY

3. 当有新连接请求来的时候就调用accpet接受请求

4. 当有数据可读可写的时候就调用do_read/do_write执行数据收发

5. loop回去

注意:上面所有的操作都是在一个线程上做的。


看完代码,再看for循环内几个细节。

细节1. select前每次都要对三个fdset进行初始化。具体初始化策略就是:

   - listen fd总是要set,这表明希望select关心listen fd的变化

   - 已经accept了的fd总是要set,这表明希望select关心这些fd上是否可读可写

细节2. select后会对三个fdset的状态进行检查。具体检查结果是:

  - listen fd如果被set了,表明有新的连接请求来了,需要调用accept

 - readfdset中如果有fd被set了,表明那个fd上有新数据可读

 - writefdset中如果有fd被set了,表明那个fd上可以写数据进去


从上面两个细节中可以发现

        if (select(maxfd+1, &readset, &writeset, &exset, NULL) < 0) {

select的三个fdset参数是in/out型的参数,既负责输入、也负责输出。select内部只关心fdset上被置位为1的操作符状态,对于为0的它一概不关心。

想一下,既然fdset上置位为0,表明应用根本不关心,应用都不关心,内核何必死乞白赖的去查一下那个为0的状态位对应的fd的状态呢?

我之前之所以对select函数存在疑惑,就是对这一点不理解。我原来的理解方式是:

1. 应用只需要跟内核说:喂兄弟,我这个进程内哪些fd的状态变了呢?我也不知道我关心哪些fd,你帮我看着办吧,别漏掉了就行。

2. 内核就把系统中所有状态变了的fd都收集到一起,全部奉送给应用

3. 应用就根据这个集合来进行数据处理


现实世界的处理方式是:

1. 应用跟内核说:为兄弟,我关心a、b、c、d这几个fd的状态,您帮我查查?

2. 内核乖乖把a、b、c、d这几个fd的状态查了一把,其余fd统统忽略,反正查了也没用,还是省省吧

3. 应用拿到a、b、c、d这几个fd的状态后,根据他们的状态就行对应处理。


因此,应用需要关心这么两个问题:

1. 知道自己需要关心哪些fd,这需要用一个数据结构来跟踪。

2. 应用需要关心的fd有两类:一是自己的listen fd,二是自己accept过的fd,没有其它的了。


总算是明白select的猫腻了。


代码1-1 (来自http://www.wangafu.net/~nickm/libevent-book/01_intro.html)

void run(void)
{
    int listener;
    struct fd_state *state[FD_SETSIZE];
    struct sockaddr_in sin;
    int i, maxfd;
    fd_set readset, writeset, exset;

    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(40713);

    for (i = 0; i < FD_SETSIZE; ++i)
        state[i] = NULL;

    listener = socket(AF_INET, SOCK_STREAM, 0);
    make_nonblocking(listener);

    if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return;
    }

    if (listen(listener, 16)<0) {
        perror("listen");
        return;
    }

    FD_ZERO(&readset);
    FD_ZERO(&writeset);
    FD_ZERO(&exset);

    while (1) {
        maxfd = listener;

        FD_ZERO(&readset);
        FD_ZERO(&writeset);
        FD_ZERO(&exset);

        FD_SET(listener, &readset);

        for (i=0; i < FD_SETSIZE; ++i) {
            if (state[i]) {
                if (i > maxfd)
                    maxfd = i;
                FD_SET(i, &readset);
                if (state[i]->writing) {
                    FD_SET(i, &writeset);
                }
            }
        }

        if (select(maxfd+1, &readset, &writeset, &exset, NULL) < 0) {
            perror("select");
            return;
        }

        if (FD_ISSET(listener, &readset)) {
            struct sockaddr_storage ss;
            socklen_t slen = sizeof(ss);
            int fd = accept(listener, (struct sockaddr*)&ss, &slen);
            if (fd < 0) {
                perror("accept");
            } else if (fd > FD_SETSIZE) {
                close(fd);
            } else {
                make_nonblocking(fd);
                state[fd] = alloc_fd_state();
                assert(state[fd]);/*XXX*/
            }
        }

        for (i=0; i < maxfd+1; ++i) {
            int r = 0;
            if (i == listener)
                continue;

            if (FD_ISSET(i, &readset)) {
                r = do_read(i, state[i]);  //ref http://www.wangafu.net/~nickm/libevent-book/01_intro.html
            }
            if (r == 0 && FD_ISSET(i, &writeset)) {
                r = do_write(i, state[i]); // ref same as above
	    }
            if (r) {
                free_fd_state(state[i]);
                state[i] = NULL;
                close(i);
            }
        }
    }
}

我对于select的理解,离不开对内核源码的了解。是看了源码之后才豁然开朗。也贴一下内核源码(一些无关细节被省略了,更完整的代码参考http://bbs.chinaunix.net/thread-1926858-1-1.html),我在代码中另外加了一些注释,呼应上面的分析。

int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
        ktime_t expire, *to = NULL;
        struct poll_wqueues table;
        poll_table *wait;
        int retval, i, timed_out = 0;
        unsigned long slack = 0;

        rcu_read_lock();
        retval = max_select_fd(n, fds);
        rcu_read_unlock();

        if (retval < 0)
                return retval;
        n = retval;

        poll_initwait(&table);
        [color=Red]wait = &table.pt;[/color]
        if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
                wait = NULL;
                timed_out = 1;
        }

        if (end_time && !timed_out)
                slack = estimate_accuracy(end_time);

        retval = 0;
        for (;;) {
                unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

		// fdset的输入参数,表明关心哪些fd
                inp = fds->in; outp = fds->out; exp = fds->ex;

		// fdset的输出参数,表明哪些关心的fd状态变成了可读可写可连接
                rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
		
		// 这里看看n,就是外面传进来的max_fd,就很能理解为什么max_fd需要加1了
                for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
                        unsigned long in, out, ex, all_bits, bit = 1, mask, j;
                        unsigned long res_in = 0, res_out = 0, res_ex = 0;
                        const struct file_operations *f_op = NULL;
                        struct file *file = NULL;

                        in = *inp++; out = *outp++; ex = *exp++;
                        all_bits = in | out | ex;  
                        if (all_bits == 0) {
				// 性能优化代码。一次判断4个字节,32个fd,如果都不关心,则可以一次跳过
                                i += __NFDBITS;
                                continue;   
                        }

			 // 32个fd中至少有一个fd是被用户关心的,得瞧瞧
                        for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {  
                                int fput_needed;
                                if (i >= n)
                                        break;
                                if (!(bit & all_bits))
                                        continue;
                                file = fget_light(i, &fput_needed);
                                if (file) {
                                        f_op = file->f_op;
                                        mask = DEFAULT_POLLMASK;
                                        if (f_op && f_op->poll)   //调用pool看这个fd是否ready
                                                mask = (*f_op->poll)(file, retval ? NULL : wait);
                                        fput_light(file, fput_needed);
                                        if ((mask & POLLIN_SET) && (in & bit)) {
                                                res_in |= bit; 
                                                retval++;
                                        }
                                        if ((mask & POLLOUT_SET) && (out & bit)) {
                                                res_out |= bit;
                                                retval++;
                                        }
                                        if ((mask & POLLEX_SET) && (ex & bit)) {
                                                res_ex |= bit;
                                                retval++;
                                        }
                                }
                        }
                        if (res_in)
                                *rinp = res_in;
                        if (res_out)
                                *routp = res_out;
                        if (res_ex)
                                *rexp = res_ex;
                        cond_resched();
                }
        return retval;
}


对select的理解欲是被昨晚看的《编程人生》第一篇文章激发的, Jamie Zawinski:好奇心是驱动一个程序员的终极原动力,唯有好奇心才能让你对code了如指掌。


我的微博:http://weibo.com/raywill2



<think> 嗯,用户的问题是关于使用select函数处理fd_set后如何获取某个socket缓冲区的内容。用户提供了多个引用片段,我需要结合这些信息给出准确的解答。 用户引用的内容显示select的工作原理:通过fd_set监控多个socket,当有事件发生时,内核会标记就绪的socket,然后用户态需要遍历fd_set找出就绪的socket(引用[2][3][4])。引用[5]提到socket读写操作的基本函数是recv()和send()。 关键点在于:当select返回后,程序需要遍历所有被监控的socket,用FD_ISSET检查哪些socket处于可读状态。对于可读的socket,就可以用recv()函数读取其接收缓冲区的内容。这里要注意几个技术细节: 1. FD_ISSET的检查必须放在循环中遍历整个fd_set 2. recv()应该使用非阻塞模式(通过fcntl设置O_NONBLOCK) 3. 要考虑TCP粘包问题,可能需要循环读取直到EAGAIN错误 用户可能正在编写网络服务器程序,需要高效处理多个客户端连接。ta的重点似乎在于"如何精确获取指定socket的数据",而不仅仅是select机制本身。因此需要强调FD_ISSET和recv的配合使用,以及缓冲区管理的注意事项。 考虑到引用[2]提到select效率问题,虽然用户没直接问优化,但在回答中可以埋入epoll的提示作为延伸。不过当前问题还是聚焦select方案。 用户提供的代码片段很有价值,特别是引用[1]展示了socket建立过程,引用[5]给出了读写函数用法。这让我可以给出具体的代码示例。要注意的是,实际recv调用需要处理返回值:正数表示接收到的字节数,0表示连接关闭,负数需要根据errno判断是否重试。 </think> 在使用 `select` 处理 `fd_set` 后获取特定 socket 缓冲区内容的核心步骤如下(结合引用内容): ### 处理流程 1. **`select` 返回后遍历 fd_set** 使用 `FD_ISSET(fd, &readfds)` 检查目标 socket 是否在可读集合中: ```c for (int fd = 0; fd <= maxfd; fd++) { if (FD_ISSET(fd, &readfds)) { // 检查socket是否就绪[^3][^4] if (fd == target_sock) { // 定位目标socket handle_target_socket(fd); } } } ``` 2. **读取缓冲区内容** 对就绪的 socket 调用 `recv()` 读取数据(引用[5]): ```c char buffer[BUFFER_SIZE]; ssize_t nbytes = recv(target_sock, buffer, sizeof(buffer) - 1, 0); if (nbytes > 0) { buffer[nbytes] = '\0'; // 添加终止符 printf("Received: %s\n", buffer); } else if (nbytes == 0) { // 连接关闭 close(target_sock); FD_CLR(target_sock, &master_set); // 移除监控[^4] } else { perror("recv error"); } ``` ### 关键细节说明 1. **`fd_set` 的处理机制** - `select` 返回时会**修改传入的 fd_set**,仅保留就绪的 socket[^3][^4] - 必须使用 `FD_ISSET` 逐个检查,不可直接访问 fd_set 内部 - 典型做法:维护独立的 `master_set` 保存所有监控的 socket,每次调用 `select` 前复制到临时集合[^4] 2. **非阻塞读取建议** ```c // 设置非阻塞模式(引用[5]扩展) int flags = fcntl(target_sock, F_GETFL, 0); fcntl(target_sock, F_SETFL, flags | O_NONBLOCK); ``` 避免 `recv()` 阻塞线程,尤其在高并发场景下。 3. **粘包处理** 循环读取直至返回 `EAGAIN`/`EWOULDBLOCK`: ```c while ((nbytes = recv(target_sock, ptr, remaining, MSG_DONTWAIT)) > 0) { ptr += nbytes; remaining -= nbytes; if (remaining <= 0) break; // 缓冲区满 } ``` ### 完整流程示意 ```c fd_set readfds; int maxfd = target_sock; // 实际应取监控集合中最大值[^4] while (1) { FD_ZERO(&readfds); FD_SET(target_sock, &readfds); // 加入监控集合 int ready = select(maxfd + 1, &readfds, NULL, NULL, NULL); if (ready < 0) { /* 错误处理 */ } if (FD_ISSET(target_sock, &readfds)) { // 检查目标socket // 读取缓冲区内容 ssize_t n = recv(target_sock, ...); /* 数据处理逻辑 */ } } ``` > **注意**:`select` 需要遍历所有监控的 socket 才能定位就绪项,当连接数较大时效率较低(引用[2][^3]),此时可考虑 `epoll` 等更高效的 I/O 多路复用方案[^2]。 --- ### 相关问题 1. 如何用 `select` 同时监控多个 socket 的可读和可写事件? 2. `recv()` 返回 `EAGAIN` 错误时应如何处理? 3. 相比 `select`,`epoll` 在哪些场景下性能更高? 4. TCP 粘包问题的解决方案有哪些? [^1]: Socket 建立连接的基础步骤 [^2]: `select` 的轮询机制缺陷 [^3]: `select` 的内核-用户态数据拷贝过程 [^4]: `fd_set` 的监控集管理方法 [^5]: Socket 通信基本操作流程
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值