计算机网络 | 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编程》适合作为高等院校计算机及相关专业的教材和教学参考书,也可作为相关开发人员的自学用书和参考手册。


相关文章
|
3天前
|
SQL 安全 算法
网络安全与信息安全:防御前线的关键技术透视
【4月更文挑战第27天】 在数字化时代,网络安全与信息安全已成为维系信息社会正常运作的重要支柱。本文针对当前网络安全领域面临的漏洞问题、加密技术的应用以及提升安全意识的必要性进行深入探讨。通过分析网络攻击者的常见手段和动机,文章揭示了安全漏洞的形成原因及其对个人及企业造成的潜在威胁。进一步地,文中介绍了现代加密技术的种类和作用机制,包括对称加密、非对称加密和哈希算法等,并讨论了这些技术如何在不同场景中保障数据安全。此外,文章还强调了培养全民网络安全意识的重要性,并提出了一系列实用的策略和方法。通过对这些关键技术的综合分析,旨在为读者提供一套全面的网络安全与信息安全防护指南。
|
1天前
|
监控 安全 算法
网络安全与信息安全:防范网络攻击的关键技术与策略
【4月更文挑战第29天】在数字化时代,网络安全与信息安全已成为个人和企业关注的焦点。本文将深入探讨网络安全漏洞的产生、加密技术的应用以及提升安全意识的重要性。通过对这些方面的分析,我们希望帮助读者更好地了解如何保护自己免受网络攻击和数据泄露的威胁。
|
3天前
|
监控 负载均衡 网络协议
|
3天前
|
负载均衡 测试技术 网络虚拟化
快速 PVST+:提升网络性能的关键技术
【4月更文挑战第22天】
18 6
|
3天前
|
安全 算法 网络安全
网络安全与信息安全:防御前线的关键技术
【4月更文挑战第27天】 在数字化时代,数据成为了新的货币,而网络安全则是保护这些数据的金库。本文深入探讨了网络安全漏洞的概念、加密技术的进展以及提升安全意识的重要性。通过对常见网络威胁的分析,我们展现了如何利用多层次防御策略来增强信息系统的抵抗力。文章不仅涉及技术层面的解决方案,还强调了人为因素在维持网络安全中的核心作用。
|
4天前
|
存储 缓存 开发框架
Flutter的网络请求:使用Dart进行HTTP请求的技术详解
【4月更文挑战第26天】了解Flutter网络请求,本文详述使用Dart进行HTTP请求
|
5天前
|
安全 网络安全 数据安全/隐私保护
网络安全与信息安全:防护之道在技术与意识的双重保障
【4月更文挑战第25天】随着信息技术的飞速发展,网络已经成为我们生活和工作中不可或缺的一部分。然而,伴随着网络技术的普及,网络安全问题也日益凸显。本文将从网络安全漏洞、加密技术、安全意识等方面进行探讨,旨在分享如何通过技术和意识的双重保障来维护网络的安全。
|
6天前
|
SQL 监控 安全
网络安全与信息安全:防御前线的关键技术与策略
【4月更文挑战第24天】在数字化时代,数据成为了新的货币,而网络安全则是保护这些宝贵资产不受威胁的保险箱。本文深入探讨了网络安全漏洞的本质、加密技术的进展以及提升个人和企业安全意识的重要性。通过分析当前网络环境中的安全挑战,我们提出了一系列创新的防御机制和实践方法,以期为读者提供一套全面的信息保护方案。
|
6天前
|
存储 监控 安全
网络安全与信息安全:防御前线的技术与意识
【4月更文挑战第24天】在数字化时代,网络和信息安全已成为维护社会稳定、保护个人隐私和企业资产的关键。本文深入探讨了网络安全漏洞的概念、加密技术的进展以及提升安全意识的重要性。通过分析当前网络威胁的多样性,我们强调了持续监控、定期更新系统、使用复杂密码和多因素认证的必要性。同时,文章还介绍了非对称加密、量子加密等先进加密技术的原理和应用。最后,我们讨论了培养全民网络安全意识的策略,包括教育培训、模拟演练和社会工程防护。
|
9天前
|
安全 物联网 网络安全
网络安全与信息安全:防御前线的关键技术与策略
【4月更文挑战第21天】在数字化时代,数据成为了新的货币,而网络安全则是保护这些资产不受威胁的盾牌。本文深入探讨了网络安全漏洞的本质、加密技术的进展以及提升个人和企业安全意识的方法。通过分析当前网络攻击的模式和动机,我们提出了一系列切实可行的防御措施,旨在帮助读者构建更加安全的网络环境。