Fork me on GitHub
0%

IO多路复用

文件描述符

简介

文件描述符:File Descriptor,简称FD,当应用程序请求内核打开/新建一个文件时,内核会返回一个文件描述符用于对应这个打开/新建的文件。FD本质上就是一个非负整数的索引值。

每一个进程只能看到自己的文件描述符,每个进程的文件描述符的编号都是从0开始,进程启动,默认都会打开标准输入(fd=0),标准输出(fd=1),标准错误这三个文件(fd=2),之后再打开的文件的描述符从编号3开始

最大个数限制

image

按照对于文件描述符概念的理解,其最大个数限制应该取决于系统资源的使用情况。但是内核通常会有系统级限制,对单个进程打开最大文件数做限制(通常是1024,可以通过 ulimit -n 命令查看)

在使用完文件描述符之后需要将其释放(close函数)给操作系统,否则文件描述符将一直存在。

操作函数

1
2
3
4
5
6
7
8
9
10
sizeof(struct fd_set) = 1024	// 按bit操作

// 初始化函数,初始化一个set集合,将set 1024个标志位置0
void FD_ZERO(fd_set *set);
// 将文件描述符fd对应的标志位在set集合中清空(=0)
void FD_CLR(int fd, fd_set *set);
// 将文件描述符fd对应的标志位在set集合中置位(=1)
void FD_SET(int fd, fd_set *set);
// 判断set集合中fd对应的标志位是否置位(==1?)
int FD_ISSET(int fd, fd_set *set);

网络文件描述符

在网络编程中,对于服务器端有两类文件描述符:监听文件描述符(Listen FD) 通信文件描述符(Communication FD),而对于每一种文件描述符又对应两个读写缓冲区:Read Buffer、Write Buffer

Listen FD Communication FD
作用 监听客户端的连接请求,检测到之后调用 accept 就可以建立新的连接 负责和建立连接的客户端数据通信
个数 1个 取决于与服务器建立连接的客户端(N个)
流程 调用accept函数,监听FD的读缓冲区是否有数据。
- 有数据说明客户端有请求,接触阻塞,建立连接
- 没有数据,阻塞,继续监听
1. 发送数据(write / send)
将数据写入对应的文件描述符对应的写缓冲区,内核检测到写缓冲区有数据,将数据发送到客户端
2. 接收数据(read / recv)
始终监听对应的文件描述符的读缓冲区,当检测到有数据,读出!

image

IO多路复用方法对比

select poll epoll
底层原理 线性表(轮询) 线性表(轮询) 红黑树(事件通知触发)
效率 较低 较低 最高
连接上限 1024 取决于系统 取决于系统
平台限制 跨平台(linux / window / mac…) linux linux

select函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

// The time structures involved are defined in <sys/time.h>
// struct timeval {
// long tv_sec; /* seconds */
// long tv_usec; /* microseconds */
// };

参数

  • nfds(指示内核需要检测最大文件描述符个数+1)

    select默认最大检测个数是1024,这是由于内核进程最大可以维护1024个文件描述符(可以修改)。但是指定第一个参数select函数在遍历文件描述符的时候不用全部遍历,只需要遍历nfds个即可。在windows中,这个参数是无效的,指定-1即可

  • readfds(读缓冲区的文件描述符的个数)

    只检测这个文件描述符的读缓冲区是否可读

  • writefds(写缓冲区的文件描述符的个数)

    只检测这个文件描述符的读缓冲区是否可写

  • exceptfds(需要异常检测文件描述符的个数)

  • timeout(超时时长)

    该函数本身是阻塞的,当加入timeout参数之后即使没有可读、可写、异常的文件描述符,达到timeout时长函数也将解除阻塞返回。当被设置为NULL,将阻塞直至检测到文件描述符状态改变

返回值

readfds、writefds、execptfds是传入传出参数。传入fd_set类型指针,最终可读、可写、异常的文件描述符同样写回用户传入的指针指向的空间内(一定小于等于传入文件描述符的个数)

  • 大于0:表示三个集合中总共被置位的位数之和
  • 等于0:timeout=0 && 三个集合没有被检测到
  • 小于0:select出错,根据errno判断出错原因

流程

  1. 初始化read、write、except三个文件描述符集合(FD_ZERO)
  2. 将文件描述符设置到select函数中,交付内核检测
  3. 内核首先一份需要检测的文件描述符,在检测对应的读写缓冲区
  4. 当检测到可读/可写将文件描述符对应的标志位写回fd_set对应的文件描述符集合中
  5. 用户处理

poll函数

1
2
3
4
5
6
7
8
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */ (传入)要监视的事件(输入事件、输出事件、错误事件等)
short revents; /* returned events */ (传出)实际发生的事件(由系统填充)
};

参数

  • struct pollfd fds
    • fd:被检测的文件描述符
    • events:委托给内核检测的事件
    • revents:实际发生的事件,内核的返回值
  • nfds:fds数组中的结构体数量。
    • POLLIN:表示有数据可读(输入事件)。
    • POLLOUT:表示可以写数据(输出事件)。
    • POLLERR:表示发生错误。
    • POLLHUP:表示发生挂起事件。
    • POLLNVAL:表示文件描述符无效。
  • timeout::指定超时时间(ms)
    • -1:永久阻塞,直到有事件发生。
    • 0:立即返回,无论是否有事件发生。
    • 大于0:表示超时时间,poll函数会等待指定的毫秒数,如果超过该时间还没有事件发生,则返回。

epoll

流程

  1. 创建epoll实例(epoll_create)
  2. 注册文件描述符和事件(epoll_ctl)
  3. 等待事件发生(epoll_wait)
  4. 处理事件(自定义)

相关函数及结构体

  • epoll_create

    1
    2
    3
    #include <sys/epoll.h>

    int epoll_create(int size);
    • 作用:创建一个epoll实例,也就是一颗红黑树
    • 参数size:Linux 2.6.8之后这个参数被忽略,大于0即可。之前的版本该参数指定了红黑树节点个数
    • 返回值:一个epoll实例
  • epoll_ctl

    1
    2
    3
    #include <sys/epoll.h>

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    • 作用:将要监视的文件描述符和关注的事件注册到epoll实例中
    • 参数:
      • epfd:epoll实例,即通过epoll_create创建出来的返回值
      • op:操作类型,可以是EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符)或EPOLL_CTL_DEL(删除文件描述符)
      • fd:要操作的文件描述符
      • event:指定关注的事件类型(见下文)
  • epoll_wait

    1
    2
    3
    #include <sys/epoll.h>

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    • 作用:等待事件的发生,该函数会阻塞程序执行,直到有事件发生或超时。

    • 参数

      • epfd:epoll_create实例的文件描述符
      • events::指向struct epoll_event结构体数组的指针,用于存储事件的结果(传出参数)
      • maxevents:events数组的大小,表示最大可以存储多少个事件。
      • timeout:等待超时时间(ms)。传入-1表示永久等待,传入0表示立即返回,传入正整数表示等待指定的毫秒数。
  • struct epoll_event

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct epoll_event {
    uint32_t events; // 事件类型
    epoll_data_t data; // 用户自定义数据
    };

    typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
    } epoll_data_t;

    • 参数
      • events:事件类型,EPOLLIN / EPOLLOUT / EPOLLRDHUP / EPOLLPRI / EPOLLERR
      • data:用户自定义数据(联合体结构,只能使用一个,通常使用fd)。这个参数必须是用户来指定,最终返回的时候,可以根据传入参数判断文件描述符的index或者其它数据类型

参考资料