IO复用之——select

本文详细介绍了Linux下Select函数的应用,包括如何同时监听多个IO事件,提高数据等待效率。通过具体示例展示了如何使用Select函数处理多个连接请求,并讨论了Select的一些局限性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. select

    前面提到Linux下的五种IO模型中有一个是IO复用模型,这种IO模型是可以调用一个特殊的函数同时监听多个IO事件,当多个IO事件中有至少一个就绪的时候,被调用的函数就会返回通知用户进程来处理已经ready事件的数据,这样通过同时等待IO事件来代替单一等待一个IO窗口数据的方式,可以大大提高系统的等待数据的效率;而接下来,就要讨论在Linux系统中提供的一个用来进行IO多路等待的函数——select;



二. select函数的用法

    首先在使用select之前,要分清在IO事件中,往往关心的不是数据的读取就是数据的发送,也就是数据的,当然也有同时关心读写的,没有任何一个IO事件既不关系读也不关心写的,因此,在对于使用select对多个IO事件进行监听检测的时候,就要对这些事件进行读写的分类,以便日后在select返回时通过检测能够得知当前事件是读发生了还是写发生了;


wKioL1dG6cKhl8kIAAAZ5JVGFv0428.png

函数参数中,

nfds表示当前最大文件描述符值+1;

readfds表示当前的事件中有多少是关心数据的读取的;

writefds表示当前的事件中有多少是关心数据的写入的;

excptfds表示当前事件中关心异常发生的事件集,也是数据的写入;


其中,fd_set是一个文件符集的数据类型

对于fd_set文件描述符集的设置,系统提供了四个函数来进行操作:

FD_CLR是对文件描述符集中的所有文件描述符进行清除;

FD_ISSET是判断某个文件描述符是否已经被设置进某个文件描述符集中;

FD_SET是将某个文件描述符设置进某个文件描述符集中;

FD_ZERO是对某个文件描述符集进行初始化;


timeout是时间的设定,表示当超过设定的时间仍然没有事件就绪时就超时返回不再等待;

timeout的结构体类型如下:

wKiom1dG7M2A-dOWAAAn8rMmYzg457.png

tv_sec是秒的设置;

tv_usec是微秒的设置;


对于select函数的返回值:

当返回值为-1的时候,表示函数出错并会置相应的错误码;

当返回值为0的时候,表示超时返回;

当返回值大于0的时候,表示至少已经有一个事件已经就绪可以处理其数据了;



三. 栗子时间

    前面有一篇本人写的博客是基于TCP协议的socket编程,其中一个服务器为了能处理多个连接请求将listen监听和accept处理连接请求分开,每当listen到一个连接请求的时候就fork出一个子进程让子进程去处理,或者使用多线程,这样就不耽误对网络中连接请求的监听了;

    但是同样是单进程,可以使用select的IO复用模型来解决对多个连接的数据处理,程序设计如下:


server服务器端:

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <sys/select.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#define _BACKLOG_ 5//设置监听队列里面允许等待的最大值int fds[20];//用于集存需要进行处理的IO事件void usage(const char *argv)//进行命令行参数的差错判断{    printf("%s   [ip]   [port]\n", argv);    exit(0);}int creat_listen_sock(int ip, int port)//创建listen socket{    int sock = socket(AF_INET, SOCK_STREAM, 0);     if(sock < 0)    {           perror("socket");        exit(1);    }       struct sockaddr_in server;//设置本地server端的网络地址信息    server.sin_family = AF_INET;    server.sin_port = htons(port);    server.sin_addr.s_addr = ip;     if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定端口号和网络地址信息    {           perror("bind");        exit(3);    }        if(listen(sock, _BACKLOG_) < 0)//进行监听    {        perror("listen");        exit(2);    }    return sock;}int main(int argc, char *argv[]){    if(argc != 3)        usage(argv[0]);    int port = atoi(argv[2]);    int ip = inet_addr(argv[1]);    int listen_sock = creat_listen_sock(ip, port);//获取监听端口号    struct sockaddr_in client;//创建对端网络地址信息结构体用于保存对端信息    socklen_t client_len = sizeof(client);    size_t fds_num = sizeof(fds)/sizeof(fds[0]);    size_t i = 0;    for(; i < fds_num; ++i)//将存放文件描述符的数组进行初始化        fds[i] = -1;    fds[0] = listen_sock;//首先将listen socket添加进去    fd_set read_fd;//创建读事件文件描述符集    fd_set write_fd;//创建写事件文件描述符集    int max_fd = fds[0];//首先将最大的文件描述符集设定为listen socket        while(1)    {        FD_ZERO(&read_fd);//将两个文件描述符集进行初始化        FD_ZERO(&write_fd);        struct timeval timeout = {10, 0};//设定超时时间        size_t i = 0;        for(; i < fds_num; ++i)//每次循环都要将数组中的文件描述符进行重新添加设置        {            if(fds[i] > 0)            {                FD_SET(fds[i], &read_fd);                if(fds[i] > max_fd)                    max_fd = fds[i];            }        }        switch(select(max_fd+1, &read_fd, &write_fd, NULL, &timeout))//进行select等待        {            case -1://出错                perror("select");                break;            case 0://超时                printf("time out...\n");                break;            default://至少有一个IO事件已经就绪                {                    size_t i = 0;                    for(; i < fds_num; ++i)                    {                    //当为listen socket事件就绪的时候,就表明有新的连接请求                        if(FD_ISSET(fds[i], &read_fd) && (fds[i] == listen_sock))                        {                            int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len);                            if(accept_sock < 0)                            {                                perror("accept");                                continue;                            }                            char *client_ip = inet_ntoa(client.sin_addr);                            int client_port = ntohs(client.sin_port);                            printf("connect with a client...  [ip]:%s  [port]:%d\n", client_ip, client_port);                            size_t i = 0;                            for(; i < fds_num; ++i)//将新的连接请求的文件描述符添加进数组保存                            {                                if(fds[i] == -1)                                {                                    fds[i] = accept_sock;                                    break;                                }                            }                            if(i == fds_num)                                close(accept_sock);                        }                        //除了listen socket就是别的普通进行数据传输的文件描述符                        else if(FD_ISSET(fds[i], &read_fd) && (fds[i] > 0))                        {                            char buf[1024];                            ssize_t  size = read(fds[i], buf, sizeof(buf)-1);                            if(size < 0)                                perror("read");                            else if(size == 0)                            {//当client端关闭就关闭相应的文件描述符                                printf("client closed...\n");                                close(fds[i]);                                fds[i] = -1;                            }                            else                            {                                buf[size] = '\0';                                printf("client# %s\n", buf);                            }                        }                        else                        {}                    }                }                break;        }    }    return 0;}


因为客户端的程序和前面的TCP的程序一样,这里就不再多写;


上面的程序可以分为如下步骤:

  1. 创建监听套接字并绑定本地网络地址信息进行监听;

  2. 创建一个全局的数组用于存放已有事件的文件描述符,便于重新进行整理;

  3. 创建读、写事件集,这里忽略异常事件集;

  4. 循环等待各个事件的就绪,每次都重新初始化事件集和重新添加设置,因为select会将没有就绪的事件清为0;

  5. select完成进行返回值的一个判断:如果是-1,则出错返回;如果是0,则超时返回;如果是大于零的值,则表明至少有一个事件就绪,转到第6步;

  6. 将数组中的事件拿出一一进行判断:如果是listen socket就绪表明有新的连接请求,新创建一个文件描述符用于处理数据的传输,并将其添置进数组中;如果是别的文件描述符就绪表明有数据传输过来需要读取,转第7步;

  7. 读取数据时,如果判断client端关闭就将数组中相应位置还原回无效值并且关闭相应的socket文件描述符,读取成功输出数据,继续循环;


运行程序:

wKiom1dHBFKRBrL0AAA9lENErsI024.png


    可以注意到上面的程序中sever端只将所有的连接请求都作为读事件添加进去了,而并没有关心写事件,事实上socket支持全双工的通信,因此,将上面的程序改为server端读取数据的同时将数据再写回给client端,以此来告知client端server端已经成功收到了数据,程序改进如下:


在循环每一次重新整理数组中的文件描述符集的时候将不是listen socket的文件描述符集同时添加进读事件集和写事件集:

        FD_ZERO(&read_fd);        FD_ZERO(&write_fd);        FD_SET(listen_sock, &read_fd);//先将listen socket添加进读事件集        struct timeval timeout = {10, 0};         size_t i = 1;//循环跳过listen socket从1开始        for(; i < fds_num; ++i)        {            if(fds[i] > 0)            {                FD_SET(fds[i], &read_fd);//同时添加进读事件集和写事件集                FD_SET(fds[i], &write_fd);                if(fds[i] > max_fd)                    max_fd = fds[i];            }           }


而当数据就绪进行读取完毕之后,再将同一个缓冲区中的数据写回client端,这里因为读写事件中使用的是同一个文件描述符,因此,当一个socket的读事件准备就绪的时候,说明写事件同样也是就绪的,而且使用同一个缓冲区中相同的数据:

else if(FD_ISSET(fds[i], &read_fd) && (FD_ISSET(fds[i], &write_fd)) && (fds[i] > 0)){     char buf[1024];     ssize_t  size = read(fds[i], buf, sizeof(buf)-1);     if(size < 0)         perror("read");     else if(size == 0)     {         printf("client closed...\n");         close(fds[i]);         fds[i] = -1;         break;      }      else      {         buf[size] = '\0';         printf("client# %s\n", buf);      }      if(FD_ISSET(fds[i], &write_fd))      {          size = write(fds[i], buf, strlen(buf));          if(size < 0)               perror("write");      }      else         printf("can not write back...\n");}


因此,在client端也需要进行读取;

运行程序:

wKiom1dG_0nirB9zAAAQw1CAM34776.png

    总结如上,虽然select实现IO复用在等待数据的效率看来要比单一的等待高,但是不难发现当需要等待多个事件的时候,是需要不断地进行复制和循环判断的,这也同样增加了时间复杂度增加了系统的开销,而且,作为一个数据类型的fd_set是由上限的,我的当前机器sizeof(fd_set)值为128,而一个字节能添加8个文件描述符,也就是总共只能添加128*8=1024个文件描述符,这个数目还是有些小的,无疑也是一个缺点。


《完》

本文出自 “敲完代码好睡觉zzz” 博客,请务必保留此出处http://2627lounuo.blog.51cto.com/10696599/1783654

<think>嗯,用户这次问的是关于I/O复用中的select和poll机制。之前用户已经连续问了两个关于C语言sizeof的问题,从指针和数组的区别到参数传递时的退化行为,看起来是在系统编程领域有深入学习的需求。这次转向网络编程基础,可能是正在学习高并发服务器开发。 用户的问题虽然简短,但涉及操作系统底层的I/O模型核心概念。需要解释清楚两者的工作原理和差异,同时注意用户可能是初学者,要避免过于晦涩的内核实现细节。从历史背景切入会比较好,因为select和poll正是为了解决C10K问题而演进的。 注意到用户提供的示例格式要求很严格,必须包含3-5个相关问题且以特定格式结尾。之前两次回答都遵守了这个规则,这次也要保持一致性。考虑到用户对技术细节的追求,准备在相关问题中引入更高级的epoll和io_uring机制作为延伸。 用户没有指定具体应用场景,但这类问题通常出现在网络服务器开发中。可以推测ta可能正在编写需要处理大量连接的代码,或者准备相关面试。回答时需要强调实际应用中的限制,比如select的1024句柄上限这种容易踩坑的点。 在比较两种机制时,要突出poll改进select的主要方面:数据结构更灵活、没有文件描述符数量限制。但也要指出两者共有的根本缺陷——每次调用都需要全量扫描描述符集合,这正是后来epoll优化的关键点。</think>### I/O复用:`select` 与 `poll` 详解 #### 核心机制 1. **`select`** - **功能**:同时监控多类文件描述符(读/写/异常),返回就绪描述符数量。 - **关键结构**: ```c fd_set read_fds; // 读描述符集合 FD_ZERO(&read_fds); // 清空集合 FD_SET(sockfd, &read_fds); // 添加sockfd到集合 ``` - **调用**: ```c int ret = select(maxfd+1, &read_fds, NULL, NULL, &timeout); ``` - **局限**: - 描述符上限:`FD_SETSIZE`(通常1024) - 每次调用需重新初始化集合 - 线性扫描所有描述符(O(n)效率) 2. **`poll`** - **功能**:通过动态数组监控描述符,解决`select`的集合大小限制。 - **关键结构**: ```c struct pollfd { int fd; // 监控的描述符 short events; // 监控的事件(POLLIN/POLLOUT) short revents; // 实际发生的事件 }; struct pollfd fds[10]; fds[0].fd = sockfd; fds[0].events = POLLIN; ``` - **调用**: ```c int ret = poll(fds, nfds, timeout); // nfds为数组长度 ``` - **优势**: - 无硬性描述符数量限制 - 无需每次重置整个集合 #### 对比总结 | **特性** | `select` | `poll` | |-------------------|------------------------------|----------------------------| | **描述符上限** | 固定(FD_SETSIZE) | 无硬限制(依赖系统资源) | | **数据结构** | 位图(fd_set) | 结构体数组(pollfd) | | **事件分离** | 输入/输出集合分离 | 单结构体整合事件 | | **效率** | O(n)扫描所有描述符 | O(n)扫描所有描述符 | | **可移植性** | 广泛支持 | 多数UNIX系统支持 | #### 适用场景 - **`select`**:兼容性要求高、描述符数量少的场景(<1024)。 - **`poll`**:需监控大量描述符(如 >1024)且无需跨平台兼容时。 > **两者共同缺陷**: > 1. 每次调用需**全量传递**监控集合,内核和用户空间频繁拷贝数据; > 2. 就绪事件检测需**遍历所有描述符**,高并发下效率低。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值