• 忘掉天地
  • 仿佛也想不起自己
  • 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2021-12-11 12:33 Aet 隐藏边栏 |   抢沙发  7 
文章评分 3 次,平均分 5.0

概述

  1. I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能很重要。
    通常,网络程序在下列情况下需要使用I/O复用技术:

    1. 客户端程序要同时处理多个socket。
    2. 客户端程序要同时处理用户输入和网络连接。
    3. TCP服务器要同时处理监听socket和连接socket。
    4. 服务器要同时处理TCP请求和UDP请求。
    5. 服务器要同时监听多个端口,或者处理多种服务。

select系统调用

select

  1. select系统调用的用途是,在一段时间内,监听用户感兴趣的文件描述符上的可读、可写或异常等事件。
    1. nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中最大值加1,因为文件描述符是从0开始计数的。
    2. readfds,writefds和exceptfds参数分别指向可读、可写和异常事件对应的文件描述符集合。
  2. 应用程序调用select时,通过这3个参数传入自己感兴趣的文件描述符。
    select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。

  1. 可以看到,fd_set结构体仅包含一个整形数组,该数组的每一个元素的每一位标记一个文件描述符。
    fd_set能容纳的文件描述符的数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。

  1. timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。
    不过不能完全信任select调用返回的timeout值,比如调用失败时timeout的值是不确定的。

    1. 这两个参数都传0,select将立即返回。
    2. 如果给timeout传NULL,select将一直阻塞,直到某个文件描述符就绪。

  1. select成功时返回就绪(可读、可写和异常)的文件描述符的总数。
    如果在超时时间内没有任何文件描述符就绪,select将返回0。
    select失败时返回-1并设置errno。
    如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。

文件描述符就绪条件

  1. 在网络编程中,下列情况下socket可读:
    1. socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT。
      此时可以无阻塞地读socket,并且读操作返回的字节数大于0。
    2. socket通信的对方关闭连接。
      此时对该socket的读操作将返回0。
    3. 监听socket上有新的连接请求。
    4. socket上有未处理的错误。
      此时可以用getsockopt来读取和清除该错误。
  2. 下列情况下socket可写:
    1. socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。
      此时可以无阻塞的写该socket,并且写操作返回的字节数大于0。
    2. socket的写操作被关闭。
      对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
    3. socket使用非阻塞connect连接成功或失败(超时)之后。
    4. socket上有未处理的错误。
      此时可以使用getsockopt来读取和清楚该错误。
  3. 网络程序中,select能处理的异常只有一种:
    1. socket上接收到带外数据。

处理带外数据

  1. socket上接收到普通数据和带外数据都将使socket返回,但socket处于不同的就绪状态:
    1. 接收到普通数据处于可读状态
    2. 接收到带外数据处于异常状态

poll系统调用

  1. poll系统调用和select系统调用类似,也是在指定时间内沦陷一定数量的文件描述符,以测试其中是否有就绪者。
  2. fd成员指定文件描述符
    events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或
    revents成员则由内核修改,以通知应用程序fd上实际上发生了哪些事件(下图)。
    nfds参数指定被监听事件集合fds的大小
    timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
  3. poll系统调用的返回值的含义与select相同。

  1. 上表中:POLLRDNORM,POLLRDBAND,POLLWRNORM,POLLWRBAND由XOPEN规范定义。
    它们实际上是将POLLIN事件和POLLOUT事件分得更细,以区别对待普通数据和优先数据。但Linux并不完全支持它们。
  2. 通常,应用程序需要recv调用的返回值来区分接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。
    不过,自Linux2.6.17开始,GNU为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发。这为我们区分上述两种情况提供了一种更简单的方式。但使用POLLRDHUP事件时,我们需要在代码最开始处定义_GNU_SOURCE。

epoll系统调用

内核事件表

  1. epoll是Linux特有的I/O复用函数。
    它在实现和使用上和select,poll有很大差异。

    1. 首先,epoll使用一组函数来完成任务,而不是单个函数。
    2. 其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和epoll那样每次调用都要重复传入文件描述符集或事件集。
    3. 不同的是,epoll需要一个额外的文件描述符,来唯一标识内核中的这个事件表(使用下面的epoll_create来创建这个额外的文件描述符)。
  2. epoll_create
    1. size参数并不起作用,只是给内核一个提示,告诉它事件表需要多大。
    2. 该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

  1. 操作内核事件表的函数:
    1. fd是要操作的文件描述符
    2. op是指定操作类型
      EPOLL_CTL_ADD:往事件表中注册fd上的事件
      EPOLL_CTL_MOD:修改fd上的注册事件
      EPOLL_CTL_DEL:删除fd上的注册事件
    3. 结构体events成员描述事件类型。epoll支持的事件类型和poll基本相同。但epoll有两个额外的事件类型:EPOLLET和EPOLLONESHOT。它们对epoll的高效运作非常关键。
    4. epoll_data_t联合体,4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。
      ptr成员可用来指定与fd相关的用户数据。
      由于epoll_data_t是一个联合体,所以不能同时使用fd和ptr,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。
    5. epoll_ctl成功时返回0,失败则返回-1并设置errno。

epoll_wait

  1. epoll_wait系统调用的主要接口是epoll_wait函数。
    它在一段超时时间内等待一组文件描述符上的事件。
  2. 调用成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。

  1. epoll_wait如果检测到事件,就将所有就绪事件从内核事件表(由epfd指定)中复制到它的第二个参数events指向的数据中。
    这个数据只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。
    这就极大地提高了应用程序索引就绪文件描述符的效率。

LT和ET

  1. epoll对文件描述符的操作有两种模式:
    1. LT:水平触发
    2. ET:边沿触发
  2. LT模式是默认的工作模式,这种模式下的epoll相当于一个效率价高的poll。
    当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。
  3. ET模式是epoll的高效工作模式。
  4. 对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。
    这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。
  5. 而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即注册该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
  6. 可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT效率高。

EPOLLONESHOT事件

  1. 即使使用ET模式,一个socket上的某个事件还是可能被触发多次。
    这在并发程序中可能会引起一个问题。比如一个线程(或进程)在读取完某个socket上的数据后开始处理这些数据,而在数据处理过程中该socket上又有新的数据可读(EPOLLIN再次被触发),此时另外一个线程(或进程)被唤醒来读取这些新的数据。
    于是,就出现了两个线程同时操作一个socket的局面。然而,这并不是我们期望的。我们期望的是,一个socket连接在任一时刻都只能被一个线程处理。
    这一点,可以使用epoll的EPOLLONESHOT事件实现。
  2. 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,触发我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
  3. 这样,对于当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。
    反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享