计算机网络 | IO多路转接技术 | select详解

简介: 计算机网络 | IO多路转接技术 | select详解

1. 什么是IO多路转接

IO操作方式有两种

  • 阻塞等待
  • 优点:不占用CPU时间片

  • 缺点:同一时刻只能处理一个操作,效率低下

  • 非阻塞(忙轮询)
  • 优点是提高了程序的执行效率,缺点是需要占用更多的CPU和系统资源
  • 只有一个任务时

  • 多个任务

对于非阻塞方式多任务的场景,也就是上图中的情况,解决方法是使用IO多路转接技术,常用的IO多路转接技术包括select/poll/epoll。

  • select/poll —— 实现方式为线性表遍历
  • 在通信的时候,委托内核去检测连接到server的client,有哪些client是在通信的,比如说有10个client连接,但是只有6个发送了数据,要把这6个client找出来,这个工作由内核去做。但是内核只能给出发送数据的client的个数6,至于是哪6个client,需要进程自己去遍历。

  • 在这两种方式下,可以这么理解,select 代收员比较懒, 她只会告诉你有几个快递到了,但是具体是哪个快递,你需要挨个遍历一遍。
  • 实际上,多路转接就是进程委托内核去做一些事情,在进程中只要调用select/poll/epoll就可以了,这样就实现了多任务的处理。
  • epoll —— 通过红黑树实现
  • epoll代收快递员很勤快,她不仅会告诉你有几个快递到了,还会告诉你是哪个快递公司的快递。

通过上面介绍已经大体了解了多路转接是什么,那么多路转接技术是怎么工作的呢?

  • 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中。(类似于阻塞信号集)

  • 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回。(select/poll/epoll)
  • 该函数为阻塞函数
  • 函数对文件描述符的检测操作是由内核完成的
  • 在返回时,它告诉进程有多少(哪些)描述符要进行I/O操作。
  • 文件描述符对应的是内核缓冲区,监听文件描述符,实际上就是监听内核缓冲区的read区,因为read区有数据就说明有进程给我发送数据。
  • select/poll会返回发生IO操作的进程个数;
  • epoll返回发生IO操作的进程个数,以及是哪些进程。

2. IO多路转接技术——select详解

(1)select()函数详解

  • 函数原型
int select( int nfds, 
      fd_set *readfds,  /*传入传出参数 | 传入传出参数:传入函数之前,指针指向的内存就已经有值了,函数执行完毕后,这个内存的值可能发生变化,并通过指针传递出来。*/
      fd_set *writefds,
            fd_set *exceptfds, 
      struct timeval *timeout );
  • 函数参数
  • nfds:要检测的文件描述符中最大的fd+1 —— 可以直接传1024(文件描述符最大是1023,+1就是1024),因为内核要做遍历,所以它需要一个最大值来作为遍历的终点。
  • readfds:读集合,重点关注,因为判断其他进程有没有给当前发送数据就是看读缓冲区有没有数据,读缓冲区有数据说明有进程连接并发送数据通信,这是被动的,是当前进程无法预知的,所以要把文件描述符放入到读集合中,让内核检测读缓冲区什么时候有数据。也就是告诉内核,只检测文件描述符对应的读缓冲区。——我们想知道对方有没有发数据,所以让内核检测文件描述符对应的读缓冲区是否有数据,所以要把文件描述符放到读集合中。读集合的类型是一个fd_set(fd_set数据类型在内核中是用一个数组实现的,数组大小是1024),这个集合所能存放的文件描述符的个数最大是1024个。内核检测的方式,是把这些文件描述符放到一个线性表中,然后遍历线性表。
  • 文件描述符集类型:fd_set readfds;fd_set数据类型的内核代码如下,通过下面的内核代码可以看出,使用select多路转接的时候,最多只能委托内核检测1024个文件描述符,这是内核决定的。

  • writefds: 写集合,写是进程主动动作,不需要去检测,一般传NULL。(写集合作用:让内核只检测文件描述符对应的写缓冲区)
  • exceptfds: 异常集合,不关心异常传NULL(让内核只检测文件描述符是否发生异常),如果想要捕捉对文件描述符的异常操作就要把它加到异常集合中。
  • timeout: 设置select是否阻塞
  • NULL: 永久阻塞
  • 当检测到fd变化的时候返回(缓冲区数据变化)
  • struct timeval timeout;
  • timeout.tv_sec = 10,阻塞10s,10s后不管fd是否变换,都会返回,也就是说,只有到达指定时间才会返回。
  • timeout.tv_usec = 0;
settitimer()
struct {
        long    tv_sec;                    
    long    tv_usec;            
};
/*赋值的时候,秒和微秒都要赋值,因为最终结果是二者之和,否则得到的就是一个随机数。*/
  • 函数返回值
  • 检测的文件描述符集合中,只要有一个fd变化了,select函数就返回。
  • 有几个文件描述符发生变化,就返回几,然后再通过遍历,把变化的fd找出来。

(2)文件描述符操作函数

  • 全部清空
  • void FD_ZERO(fd_set *set); //所有标志位清0
  • 从集合中删除某一项
  • void FD_CLR(int fd, fd_set *set); //在set中清除fd
  • 将某个文件描述符添加到集合
  • void FD_SET(int fd, fd_set *set);
  • 判断某个文件描述符是否在集合中
  • int FD_ISSET(int fd, fd_set *set); //fd对应集合中的标志位是0则返回0,是1就返回1

(3)使用select函的优缺点

  • 优点:
  • 跨平台
  • 缺点:
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大(内核态到用户态的频繁切换,以及fd集合从用户态和内核态之间的复制)。
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大,客户端越多select的效率越低,并且随着进程的增多,效率下降的越来越快。——对于前两个缺点,poll和select都有这两个缺点,但是epoll没有,因为select/poll在用户和内核有两块内存,所以需要来回复制,而epoll是内核和用户使用同一块共享内存。
  • select支持的文件描述符数量太小了,默认是1024。poll不受1024的影响,但是poll不可以跨平台,其他方面二者差不多。(select中的fd_set是用数组实现的,而poll用的是链表实现的,所以不受限制。epoll就更厉害了,用的是树来实现的)。——实际上进程中文件描述符最多是1024个,这个数字是可以修改的,只要修改相应的配置文件,重启电脑就好了。

(4)select工作过程分析

首先假设客户端A、B、C、D、E、 F连接到服务器,分别对应文件描述符 3、4、100、101、102、103(fd都是server端的,每有一个client连接到server,都会产生一个用于通信的fd)。

现在,server通过select函数来委托内核去检测客户端ABCDEF是否给server发数据了。

  • fd_set reads, temp; —— 文件描述符表reads,存放在用户空间;内核会拷贝一份,复制到内核区。因为在内核中会修改这个表并覆盖原来的reads,所以我们需要提前备份一下原始表temp。
  • FD_SET(3, &reads); —— 调用6次把3、4、100、101、102、103依次加入reads集合。
  • select(103+1, &reads, NULL, NULL, NULL);
  • 103+1表示要检测的文件描述符中数字最大的fd+1,来指定遍历的终点。
  • reads是传入传出参数,内核会对拿到的初始表进行修改,根据读缓冲区是否有数据将相应的位分别置1或者清0,然后用修改后的表覆盖传入的初始表reads,并作为传出参数传出。

在上面的图中

  • 文件描述符0、1、2分别是标准输入、标准输出、标准错误,所以供我们使用的文件描述符是从数字3开始的。
  • 被修改后的表在内核中,它会再一次拷贝,并放到用户区,且覆盖原来的reads,这时候的reads是内核处理后的(fd变化则保留1,否则清0),所以只要遍历reads,就可以找出发送数据的client,reads相应位值为1的文件描述符对应的client发送了数据。那么我们就对应的执行read操作,去读数据。
  • select中传入的参数nfds是104,所以内核会遍历检测0-103文件描述符,先检测文件描述符标志位是不是1,如果是1再去检测fd对应的读缓冲区有没有数据,有数据说明和该fd通信的client发送数据了。
  • client连接server的时候会进行三次握手,发送FIN数据包到server的监听文件描述符lfd对应的读缓冲区中。所以,要想知道有没有client发出连接请求,就要把lfd放到读集合中,让内核去检测。也就是说,有没有连接请求也是委托内核去检测。

(5)select多路转接代码实现

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out port\n");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);
    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET; // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
    serv_addr.sin_port = htons(port); // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......\n");
    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);
    // 最大的文件描述符
    int maxfd = lfd;
    // 文件描述符读集合
    fd_set reads, temp;
    // init 初始化
    FD_ZERO(&reads);
    FD_SET(lfd, &reads);
    while(1)
    {
        // 委托内核做IO检测
        temp = reads;
        //在Linux下maxfd必须写正确,要及时更新;在Windows下可以随便写
        int ret = select(maxfd+1, &temp, NULL, NULL, NULL);
        if(ret == -1)
        {
            perror("select error");
            exit(1);
        }
        // 客户端发起了新的连接 
        // 用于监听的文件描述符有且只有1个lfd,lfd对应位为1,说明有新的连接请求
        if(FD_ISSET(lfd, &temp))
        {
            // 接受新连接,返回一个用于通信的cfd,并加入到原始的读集合reads(备份)
            // 接受连接请求 - accept不阻塞 //因为只要进入if语句,就说明有新连接
            int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
            if(cfd == -1)
            {
                perror("accept error");
                exit(1);
            }
            char ip[64];
            printf("new client IP: %s, Port: %d\n", 
                   inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                   ntohs(client_addr.sin_port));
            // 将cfd加入到待检测的读集合中 - 下一次就可以检测到了
            // 下次循环的时候,如果cfd发生变化就可以检测到,当前循环是检测不到的,这也说明select是异步的。
            FD_SET(cfd, &reads);
            // 更新最大的文件描述符//maxfd决定了内核遍历检测的范围
            maxfd = maxfd < cfd ? cfd : maxfd;
        }
        // 已经连接的客户端有数据到达
        // 需要遍历去判断哪个client通信的cfd发生了变化(说明通信了),变化则read读取数据。
        // i为啥是从lfd+1开始的?
        // 因为lfd是第一个创建的文件描述符,而文件描述符创建的规则是当前最小空闲,所以lfd+1应该就是第一个用于通信的文件描述符cfd。
        for(int i=lfd+1; i<=maxfd; ++i)
        {
            if(FD_ISSET(i, &temp))
            {
                char buf[1024] = {0};
                int len = recv(i, buf, sizeof(buf), 0);
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("客户端已经断开了连接\n");
                    close(i);
                    // 从读集合中删除
                    FD_CLR(i, &reads);
                }
                else
                {
                    printf("recv buf: %s\n", buf);
                    send(i, buf, strlen(buf)+1, 0);
              //strlen(buf)不包括'\0',所以需要+1,并且前提是buf已经被初始化为0
              //必须把'\0'发出去来表示字符串结束,否则数据可能出错(比实际数据长),出现乱码
                }
            }
        }
    }
    close(lfd);
    return 0;
}

图书推荐 -《精通嵌入式Linux编程》

书名:《精通嵌入式Linux编程》

出版社:清华大学出版社

本书的每一章都介绍嵌入式Linux的一个主要领域。它描述知识背景,以便你可以了解一般原则,它还包括详细的有效示例来说明这些领域中的操作。

京东购买链接:点击直达

当当购买链接:点击直达

内容简介:《精通嵌入式Linux编程》详细阐述了与嵌入式Linux开发相关的基本解决方案,主要包括初识嵌入式Linux开发、关于工具链、引导加载程序详解、配置和构建内核、构建根文件系统、选择构建系统、使用Yocto进行开发、Yocto技术内幕、创建存储策略、现场更新软件、连接设备驱动程序、使用分线板进行原型设计、init程序、使用BusyBoX nunit启动、管理电源、打包Python程序、了解进程和线程、管理内存、使用GDB进行调试、性能分析和跟踪、实时编程等内容。此外,该书还提供了相应的示例、代码,以帮助读者进一步理解相关方案的实现过程。

  《精通嵌入式Linux编程》适合作为高等院校计算机及相关专业的教材和教学参考书,也可作为相关开发人员的自学用书和参考手册。


相关文章
|
2月前
|
调度 Python
探索Python高级并发与网络编程技术。
可以看出,Python的高级并发和网络编程极具挑战,却也饱含乐趣。探索这些技术,你将会发现:它们好比是Python世界的海洋,有穿越风暴的波涛,也有寂静深海的奇妙。开始旅途,探索无尽可能吧!
65 15
|
2月前
|
监控 应用服务中间件 Linux
掌握并发模型:深度揭露网络IO复用并发模型的原理。
总结,网络 I/O 复用并发模型通过实现非阻塞 I/O、引入 I/O 复用技术如 select、poll 和 epoll,以及采用 Reactor 模式等技巧,为多任务并发提供了有效的解决方案。这样的模型有效提高了系统资源利用率,以及保证了并发任务的高效执行。在现实中,这种模型在许多网络应用程序和分布式系统中都取得了很好的应用成果。
90 35
|
2月前
|
监控 算法 JavaScript
基于 JavaScript 图算法的局域网网络访问控制模型构建及局域网禁止上网软件的技术实现路径研究
本文探讨局域网网络访问控制软件的技术框架,将其核心功能映射为图论模型,通过节点与边表示终端设备及访问关系。以JavaScript实现DFS算法,模拟访问权限判断,优化动态策略更新与多层级访问控制。结合流量监控数据,提升网络安全响应能力,为企业自主研发提供理论支持,推动智能化演进,助力数字化管理。
60 4
|
2月前
|
机器学习/深度学习 算法 PyTorch
Perforated Backpropagation:神经网络优化的创新技术及PyTorch使用指南
深度学习近年来在多个领域取得了显著进展,但其核心组件——人工神经元和反向传播算法自提出以来鲜有根本性突破。穿孔反向传播(Perforated Backpropagation)技术通过引入“树突”机制,模仿生物神经元的计算能力,实现了对传统神经元的增强。该技术利用基于协方差的损失函数训练树突节点,使其能够识别神经元分类中的异常模式,从而提升整体网络性能。实验表明,该方法不仅可提高模型精度(如BERT模型准确率提升3%-17%),还能实现高效模型压缩(参数减少44%而无性能损失)。这一革新为深度学习的基础构建模块带来了新的可能性,尤其适用于边缘设备和大规模模型优化场景。
83 16
Perforated Backpropagation:神经网络优化的创新技术及PyTorch使用指南
|
3月前
|
存储 双11 数据中心
数据中心网络关键技术,技术发明一等奖!
近日,阿里云联合清华大学与中国移动申报的“性能可预期的大规模数据中心网络关键技术与应用”项目荣获中国电子学会技术发明一等奖。该项目通过端网融合架构,实现数据中心网络性能的可预期性,在带宽保障、时延控制和故障恢复速度上取得重大突破,显著提升服务质量。成果已应用于阿里云多项产品及重大社会活动中,如巴黎奥运会直播、“双十一”购物节等,展现出国际领先水平。
|
3月前
|
存储 监控 算法
基于 Python 哈希表算法的局域网网络监控工具:实现高效数据管理的核心技术
在当下数字化办公的环境中,局域网网络监控工具已成为保障企业网络安全、确保其高效运行的核心手段。此类工具通过对网络数据的收集、分析与管理,赋予企业实时洞察网络活动的能力。而在其运行机制背后,数据结构与算法发挥着关键作用。本文聚焦于 PHP 语言中的哈希表算法,深入探究其在局域网网络监控工具中的应用方式及所具备的优势。
94 7
|
3月前
|
安全 网络安全 定位技术
网络通讯技术:HTTP POST协议用于发送本地压缩数据到服务器的方案。
总的来说,无论你是一名网络开发者,还是普通的IT工作人员,理解并掌握POST方法的运用是非常有价值的。它就像一艘快速,稳定,安全的大船,始终为我们在网络海洋中的冒险提供了可靠的支持。
107 22
|
4月前
|
缓存 网络协议 API
掌握网络通信协议和技术:开发者指南
本文探讨了常见的网络通信协议和技术,如HTTP、SSE、GraphQL、TCP、WebSocket和Socket.IO,分析了它们的功能、优劣势及适用场景。开发者需根据应用需求选择合适的协议,以构建高效、可扩展的应用程序。同时,测试与调试工具(如Apipost)能助力开发者在不同网络环境下优化性能,提升用户体验。掌握这些协议是现代软件开发者的必备技能,对项目成功至关重要。
|
4月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
166 23
|
4月前
|
机器学习/深度学习 API Python
Python 高级编程与实战:深入理解网络编程与异步IO
在前几篇文章中,我们探讨了 Python 的基础语法、面向对象编程、函数式编程、元编程、性能优化、调试技巧、数据科学、机器学习、Web 开发和 API 设计。本文将深入探讨 Python 在网络编程和异步IO中的应用,并通过实战项目帮助你掌握这些技术。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等