1. 概述
I/O复用,这里有两个关键词
- I/O:就是指网络I/O,即客户端与服务器之间的网络通信
- 复用:就是指重复使用,那重复使用什么呢?重复使用一个进程或线程
一台服务器上的某个进程可以被多个客户端访问,而通信时服务器需要和每个客户端之间构建socket连接,每个socket连接就是一个网络I/O,客户端与服务器之间的所有数据通信都要经过这个socket,I/O复用就是用一个进程来处理这些socket连接上发生的事件,如数据的发送、接收。 I/O复用使得服务器上的某个程序能同时监听多个文件描述符。
网络程序需要使用I/O复用的情况主要包括:
- 客户端程序要同时处理多个socket;
- 客户端程序要同时处理用户输入和网络连接;
- TCP服务器要同时处理监听socket(listenfd)和连接socket(connfd);
- 服务器要同时处理TCP请求和UDP请求;
- 服务器要同时监听多个端口,或者处理多种服务。
Linux下实现I/O复用的系统调用主要有:
select
、poll
和epoll
。
2. select 系统调用
select
系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
#include <sys/select.h>
/* Check the first NFDS descriptors each in READFDS (if not NULL) for read
readiness, in WRITEFDS (if not NULL) for write readiness, and in EXCEPTFDS
(if not NULL) for exceptional conditions. If TIMEOUT is not NULL, time out
after waiting the interval specified therein. Returns the number of ready
descriptors, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int select (int __nfds, fd_set *__restrict __readfds,
fd_set *__restrict __writefds,
fd_set *__restrict __exceptfds,
struct timeval *__restrict __timeout);
参数说明:
- nfds:指定被监听的文件描述符的总数,它通常被设置为
select
监听的文件描述符中的最大值加1,因为文件描述符是从0开始计数的;- readfs,writefds、exceptfds:分别指向可读、可写和异常等事件对应的文件描述符集合;
- timeout:设置函数的超时时间。
readfs,writefds、exceptfds 参数都是 fd_set 结构体指针,它的定义如下:
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* Some versions of <linux/posix_types.h> define this macros. */
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) (1UL << ((d) % __NFDBITS)))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
fd_set 结构体内部仅仅包含一个长整型数组fds_bits,数组的每一个元素的每一位(bit)标记一个文件描述符。 fd_set 能容纳的文件描述符数量有 __FD_SETSIZE 指定。
timeout 参数是一个 timeval 结构体指针,其定义如下:
/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
timeval 中 tv_sec 和tv_usec 分别表示秒和微秒。
在
select
系统调用中,若给timeout参数的tv_sec 和tv_usec 都设置为 0 ,则select
将立即返回,如果给timeout参数传递 NULL ,则select
将一直阻塞,直到某个文件描述符就绪;
返回值:
select
成功时返回就绪(可读、可写和异常)文件描述符的总数,失败时返回-1并设置errno。
3. poll 系统调用
poll
系统调用和select
系统调用类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。
#include <sys/poll.h>
/* Poll the file descriptors described by the NFDS structures starting at
FDS. If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
an event to occur; if TIMEOUT is -1, block until an event occurs.
Returns the number of file descriptors with events, zero if timed out,
or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);
参数说明:
- fds: 被监听事件集合,指定所有感兴趣文件描述符上发生的可读、可写和异常等事件;
- nfds:指定被监听事件集合fds的大小;
- timeout:指定
poll
函数的超时时间,当timeout为 -1 时,poll
调用将永远阻塞,直到某个事件发生,当timeout为 0 时,poll
调用将立即返回。
fds是一个 pollfd 结构类型的指针,它的定义如下:
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
成员说明:
- fd:指定文件描述符;
- events:指明
poll
监听fd上的哪些事情,它是一系列事件的按位或,具体事件见头文件:<bits/poll.h>;- revents:由内核修改,以通知应用程序fd上实际发生了哪些事件。
返回值:
poll
成功时返回就绪(可读、可写和异常)文件描述符的总数,失败时返回-1并设置errno。
4. epoll 系列系统调用
4.1 内核事件表
epoll
是Linux特有的I/O复用函数。它在实现和使用上与select
、poll
有很大差异。首先,epoll
使用一组函数来完成任务,而不是单个函数。其次,epoll
把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select
和poll
那样每次调用都要重复传入文件描述符集或事件集。但epoll
需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下epoll_create
函数来创建:
#include <sys/epoll.h>
/* Creates an epoll instance. Returns an fd for the new instance.
The "size" parameter is a hint specifying the number of file
descriptors to be associated with the new instance. The fd
returned by epoll_create() should be closed with close(). */
extern int epoll_create (int __size) __THROW;
参数说明:
- size: 并不起作用,只是给内核一个提示,告诉它事件表需要多大,
epoll_create1
函数已丢弃size参数。
epoll_create
函数返回的文件描述符将用作其他所有epoll
系统调用的第一个参数,以指定要访问的内核事件表。
下面的函数用来操作epoll
的内核事件表:
#include <sys/epoll.h>
/* Manipulate an epoll instance "epfd". Returns 0 in case of success,
-1 in case of error ( the "errno" variable will contain the
specific error code ) The "op" parameter is one of the EPOLL_CTL_*
constants defined above. The "fd" parameter is the target of the
operation. The "event" parameter describes which events the caller
is interested in and any associated user data. */
extern int epoll_ctl (int __epfd, int __op, int __fd,
struct epoll_event *__event) __THROW;
参数说明:
- epfd:内核事件表文件描述符,
epoll_create
函数的返回值;- op:指定操作类型;
- fd:指定要操作的文件描述符;
- event: 指定事件。
参数op的可选操作类型有如下3种:
- EPOLL_CTL_ADD:往事件表中注册fd上的事件;
- EPOLL_CTL_MOD:修改fd上的注册事件;
- EPOLL_CTL_DEL:删除fd上的注册事件。
参数event是 epoll_event 结构指针,其定义如下:
#include <sys/epoll.h>
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
成员说明:
- events:描述事件类型,具体事件见头文件: <sys/epoll.h>里的 EPOLL_EVENTS 枚举类型;
- data:存储用户数据。
成员data是个 epoll_data_t 结构体类型,其定义为:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_data_t 是个联合体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可用来指定与fd相关的用户数据。但由于 epoll_data_t 是个联合体,不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。
返回值:
epoll_ctl
成功时返回0,失败则返回-1并设置errno。
4.2 epoll_wait 函数
epoll
系列系统调用的主要接口是epoll_wait
函数,它在一段时间内等待一组文件描述符上的事件。
#include <sys/epoll.h>
/* Wait for events on an epoll instance "epfd". Returns the number of
triggered events returned in "events" buffer. Or -1 in case of
error with the "errno" variable set to the specific error code. The
"events" parameter is a buffer that will contain triggered
events. The "maxevents" is the maximum number of events to be
returned ( usually size of "events" ). The "timeout" parameter
specifies the maximum wait time in milliseconds (-1 == infinite).
This function is a cancellation point and therefore not marked with
__THROW. */
extern int epoll_wait (int __epfd, struct epoll_event *__events,
int __maxevents, int __timeout);
参数说明:
- epfd:内核事件表文件描述符,
epoll_create
函数的返回值;- events:
epoll_wait
函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到events参数指向的数组中,这个数组只用于输出epoll_wait
检测到的就绪事件;- maxevents:指定最多监听多少个事件;
- timeout:指定
epoll_wait
函数的超时时间,当timeout为 -1 时,epoll_wait
调用将永远阻塞,直到某个事件发生,当timeout为 0 时,epoll_wait
调用将立即返回。
返回值:
epoll_wait
成功时返回就绪的文件描述符的个数,失败则返回-1并设置errno。
5. 三组I/O复用函数的比较
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通过3个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数 | 统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件 |
应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) |
最大支持文件描述符数 | 一般有最大限制 | 65535 | 65535 |
工作模式 | LT | LT | 支持ET高效模式 |
内核实现和工作效率 | 采用轮询方式来检测就绪事件,算法时间复杂度为O(n) | 采用轮询方式来检测就绪事件,算法时间复杂度为O(n) | 采用回调方式来检测就绪事件,算法时间复杂度为O(1) |