第九章 IO复用

Huan Lee Lv5

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

  • 客户端程序要同时处理多个socket。.比如本章将要讨论的非阻塞connect技术。
  • 客户端程序要同时处理用户输入和网络连接。比如本章将要讨论的聊天室程序。
  • TCP服务器要同时处理监听socket和连接socket。这是I/O复用使用最多的场合。后续章节将展示很多这方面的例子。
  • 服务器要同时处理TCP请求和UDP请求。比如本章将要讨论的回射服务器。
  • 服务器要同时监听多个端口,或者处理多种服务。比如本章将要讨论的xinetd服务器。

需要指出的是,/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

9.1 select系统调用

select系统调用的用途是: 在一段指定时间内, 监听用户感兴趣的文件描述符上的可读, 可写和异常等事件.

1
2
3
#include<sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,
struct timeval* timeout);
  • nfds 监听的文件描述符总数

  • readfds, writefds, exceptfds分别指向可读, 可写和异常事件对应的文件描述符集合. select调用返回时, 内核将修改他们来通知应用程序哪些文件描述符已经就绪.

    • fd_set结构体仅包含一个整型数组, 该数组的每个元素的每一位标记一个文件描述符
1
2
3
4
5
// 由于位操作过于烦琐, 可以使用一系列宏简化
FD_ZERO(fd_set *fdset ); /*清除fdset的所有位*/
FD_SET(int fd, fd_set *fdset ); /*设置fdset的位fd*/
FD_CLR(int fd, fd_set *fdset ); /*清除fdset的位fd*/
int FD_ISSET(int fd, fd_set*fdset); /*测试fdset的位fd是否被设置*/
  • timeout可以设置超时时间(精确到微秒). 如果传入NULL, 则select将一直阻塞, 知道某个文件描述符就绪.
  • select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-l并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1, 并设置errno为EINTR。

文件描述符就绪条件

可读:

Untitled

可写:

Untitled

带外数据

socket收到带外数据也将使select返回, 不同于接收到普通数据的socket, 前者处于异常状态

1
2
3
4
5
6
7
8
9
10
// 对于接收普通数据的就绪socket
if (FD_ISSET(connfd, &read_fds)){
ret = recv(connfd, buf, sizeof(buf)-1, 0);
...
}
// 对于接收带外数据的就绪socket
if (FD_ISSET(connfd, &exception_fds)) {
ret = recv(connfd, buf, sizeof(buf)-1, MSG_OOB);
...
}

9.2 poll系统调用

poll与select类似, 也是在指定时间内轮询一定数量的文件描述符, 以测试其中是否有就绪者.

1
2
#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

Untitled

Untitled

Untitled

  • timeout的单位是毫秒, 值为-1时, poll将永远阻塞, 直至某个事件发生.

9.3 epoll系列系统调用

epoll是Linux特有的IO复用函数, 在实现和使用上与select, poll有很大差异.

  • epoll使用一组函数来完成任务,而不是单个函数。
  • epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
1
2
#include<sys/epoll.h>
int epoll_create(int size);
  • size参数暂时不起作用, 只是提示内核这个事件表需要多大, 该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数, 以指定访问的内核事件表.
1
2
3
4
5
6
7
8
9
10
11
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
__uint32_t events; // epoll 事件
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

fd指明要操作的文件描述符, op则指定操作类型:

  • EPOLL_CTL_ADD: 往事件表中注册fd上的事件
  • EPOLL_CTL_MOD: 修改fd上的注册事件
  • EPOLL_CTL_DEL: 删除fd上的注册事件

epoll支持的事件类型与poll类似, 在poll对应的宏前加上’E’. 但epoll有额外的两个事件类型(EPOLLET和EPOLLONESHOT)

epoll_data_t是一个联合体, 使用最多的fd, 用于指定事件所丛书的目标文件描述符; ptr指定与fd相关的用户数据.

epoll_wait函数

epoll系列的主要接口, 在一段超时时间内等待一组文件描述符上的事件

1
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • timeout单位是毫秒
  • maxevents 指定最多监听多少个事件, 必须大于0
  • epoll_wait函数如果检测到事件, 就将所有就绪的事件从内核事件表复制到events指向的数组中(这个数组只用于存储就绪事件, 不用于输入).

LT和ET模式

epoll对文件描述符的操作有两种模式:LT(Level Trigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式。LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。

对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

EPOLLONESHOT事件

在并发模式中, 可能出现某个连接的新数据被另一个线程操作的情况, 即两个线程同时操作一个socket. 为避免这种情况, 可以使用epoll的EPOLLONESHOT事件实现.

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。

Untitled

Untitled

Untitled

Untitled

工作线程的核心部分. 用休眠5s模拟处理socket之前的请求, 结束之后, 工作线程等待5s后仍然没有收到socket上的下一批客户数据, 则退出, 否则继续为它服务.

9.4 三组IO复用函数的比较

select, poll, epoll三组IO复用都能同时监听多个文件描述符.

Untitled

epoll_wait采取回调方式来检测就绪事件, 其算法时间复杂度为O(1), 但是当活动连接较多时, epoll_wait的效率未必比select和poll高, 因为此时回调函数被触发地过于频繁. 所以epoll_wait适用于连接数量多, 但是活动连接比较少地情况.

9.5 IO复用地高级应用1: 非阻塞connect

当非阻塞地socket调用connect, 而连接又没有立即建立时, 会一个errno值为EINPROGRESS的错误. 这种情况下, 可以通过select, poll等监听这个连接失败的socket上的可写事件, 当函数返回后, 在利用getsockopt来读取错误码和清除该socket上的错误. 如果错误码是0, 则表示连接成功.

  • 示例代理基于select系列实现

9.6 IO复用的高级应用2: 聊天室程序

该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两个部分。

客户端程序有两个功能:

  1. 从标准输入终端读入用户数据,并将用户数据发送至服务器:

  2. 往标准输出终端打印服务器发送给它的数据。

服务器的功能: 接收客户数据,并把客户数据发送给每一个登录到该服务器上的客户端(数据发送者除外)。

  • 示例代码基于poll系列实现

9.7 IO复用的高级应用3: 同时处理TCP和UDP服务

一个socket只能监听一个端口, 因此, 服务器如果要同时监听多个端口, 就必须创建多个socket, 并将它们分别绑定到各个端口上. 同理, 同一个端口, 同时处理TCP和UDP也需要创建两个socket.

  • 示例代码基于epoll实现
  • 使用epoll的事件表同时管理多个socket, 针对不同socket的就绪事件采取不同的措施.

9.8 超级服务xinetd

xinetd管理的子服务中有的是标准服务,比如时间日期服务daytime、回射服务echo和丢弃服务discard。xinetd服务器在内部直接处理这些服务。还有的子服务则需要调用外部的服务器程序来处理。xinetd通过调用fork和exec函数来加载运行这些服务器程序。比如telnet、fp服务都是这种类型的子服务。

Untitled

  • Title: 第九章 IO复用
  • Author: Huan Lee
  • Created at : 2023-08-20 08:08:09
  • Updated at : 2024-02-26 04:53:15
  • Link: https://www.mirthfullee.com/2023/08/20/notion-第九章 IO复用-b67d62ab/
  • License: This work is licensed under CC BY-NC-SA 4.0.