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


相关文章
|
13天前
|
存储 安全 网络安全
云计算与网络安全:技术融合的双刃剑
在数字化浪潮中,云计算如同一股不可阻挡的力量,推动着企业和个人用户步入一个高效、便捷的新时代。然而,随之而来的网络安全问题也如影随形,成为制约云计算发展的阿喀琉斯之踵。本文将探讨云计算服务中的网络安全挑战,揭示信息保护的重要性,并提供实用的安全策略,旨在为读者呈现一场技术与安全的较量,同时指出如何在享受云服务带来的便利的同时,确保数据的安全和隐私。
23 6
|
11天前
|
存储 人工智能 安全
云计算与网络安全:技术融合与挑战
在数字化时代的浪潮中,云计算和网络安全已成为推动社会进步的两大关键技术。本文将探讨云计算服务的发展,网络安全的重要性,以及信息安全技术的演进。我们将通过实例分析,揭示云服务如何增强数据保护,网络安全措施如何应对新兴威胁,以及信息安全技术的创新如何为企业带来竞争优势。文章旨在为读者提供对云计算和网络安全领域的深入理解,并展示它们如何共同塑造我们的未来。
|
17天前
|
供应链 安全 物联网安全
NIST(美国国家标准与技术研究院)在网络安全领域进行了多项创新
NIST(美国国家标准与技术研究院)在网络安全领域进行了多项创新
37 10
|
10天前
|
监控 安全 网络安全
云计算与网络安全:技术挑战与解决方案
随着云计算技术的飞速发展,其在各行各业的应用越来越广泛。然而,随之而来的网络安全问题也日益凸显。本文将从云服务、网络安全和信息安全等技术领域出发,探讨云计算面临的安全挑战及相应的解决方案。通过实例分析和代码示例,旨在帮助读者更好地理解云计算与网络安全的关系,提高网络安全防护意识。
|
13天前
|
存储 监控 安全
云计算与网络安全:云服务、网络安全、信息安全等技术领域的融合与挑战
本文将探讨云计算与网络安全之间的关系,以及它们在云服务、网络安全和信息安全等技术领域中的融合与挑战。我们将分析云计算的优势和风险,以及如何通过网络安全措施来保护数据和应用程序。我们还将讨论如何确保云服务的可用性和可靠性,以及如何处理网络攻击和数据泄露等问题。最后,我们将提供一些关于如何在云计算环境中实现网络安全的建议和最佳实践。
|
15天前
|
存储 安全 网络安全
云计算与网络安全:云服务、网络安全、信息安全等技术领域的融合与挑战
随着云计算技术的飞速发展,越来越多的企业和个人开始使用云服务。然而,云计算的广泛应用也带来了一系列网络安全问题。本文将从云服务、网络安全、信息安全等方面探讨云计算与网络安全的关系,分析当前面临的挑战,并提出相应的解决方案。
44 3
|
20天前
|
存储 安全 网络安全
云计算与网络安全:技术融合的双刃剑
本文深入探讨了云计算和网络安全之间的复杂关系。通过分析云服务的基本架构,我们揭示了它们在提供便利的同时,如何引入新的安全挑战。文章不仅讨论了这些挑战,还提供了应对策略,旨在帮助读者理解并加强他们的网络安全防护。
|
16天前
|
存储 安全 网络安全
云计算与网络安全:技术融合与挑战
本文深入探讨了云计算在提供便捷服务的同时,如何应对日益复杂的网络安全问题。文章首先概述了云计算的基本概念及其服务模型,随后详细分析了网络安全的重要性以及云计算环境中存在的安全挑战。通过具体实例,本文进一步讨论了云服务提供商和用户如何共同维护网络环境的安全,并提出了未来网络安全发展的可能方向。旨在为读者提供一个关于云计算与网络安全相互作用的全面视角,同时强调在这一过程中采取主动防御措施的重要性。
28 1
|
18天前
|
存储 安全 网络安全
云计算与网络安全:技术融合的新篇章
随着云计算技术的飞速发展,企业和个人越来越依赖于云服务来存储和处理数据。然而,随之而来的网络安全问题也日益凸显,成为制约云计算发展的关键因素之一。本文将深入探讨云计算与网络安全的关系,分析当前面临的主要安全挑战,并提出相应的解决方案。通过技术创新和安全管理的结合,我们有望构建一个更加安全、可靠的云计算环境。
|
10天前
|
存储 安全 网络安全
云计算与网络安全:技术融合下的挑战与机遇
随着云计算技术的飞速发展,网络安全问题也日益凸显。本文将探讨云计算环境下的网络安全挑战,以及如何通过技术创新来应对这些挑战。我们将分析云服务的安全特性,讨论信息安全的最佳实践,并展望未来云计算与网络安全的发展趋势。