1. 使用情景
- 处理多个socket
- 同时处理数据输入和网络连接
- 同时处理监听socket和连接socket
- 同时处理TCP和UDP请求
- 监听多个端口, 处理多种服务
2. select 系统调用
用途: 监听用户感兴趣的文件描述符上的可读, 可写, 异常事件
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
- nfds指定被监听的文件描述符总数, 因为文件描述符从0开始, 它通常被设置为最大的文件描述符值加1
- readfds, writefds, exceptfds分别为可读, 可写, 异常事件对应的文件描述符集合, 调用select时传入, 返回时, 内核会修改集合
- fd_set结构体能容纳的文件描述符数量由FD_SETSIZE指定, 限制了select能同时处理文件描述符的数量
#include <sys/select.h>
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参数设置超时时间, 如果成员都为0, select会立刻返回, 如果timeout传入NULL, 则阻塞直到有文件描述符就绪
struct timeval
{
long tv_set; /* 秒 */
long tv_usec; /* 微妙 */
};
- 返回就绪的文件描述符总数
文件描述符可读就绪条件:
- socket内核接收缓冲区字节可以无阻塞的读, 并且读操作返回的字节数大于0
- 对方关闭连接, 此时socket读操作返回0
- 监听socket上有新的连接
- socket有未处理的错误, 此时用getsockopt读取和清除错误
文件描述符可写就绪条件:
- socket内核发送缓冲区可用字节可以无阻塞的写该socket, 并且写操作返回的字节数大于0
- 写操作被关闭, 对关闭的socket执行写操作会触发SIGPIPE信号
- socket使用非阻塞connet成功或者失败
- socket上有未处理错误, 同上4
3. poll 系统调用
用途: 和select类似, 轮询一定数量的文件描述符, 以测试是否有就绪者
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
- fds是结构体数组, 指定要监听的文件描述符以及感兴趣的事件
struct pollfd
{
int fd; /* 文件描述符 */
short events; /* 注册的事件 */
short revents; /* 实际发生的事件, 内核填充 */
};
- events, revents由一系列事件按位或, 事件类型如下:
POLLIN: 数据(普通和优先)可读
POLLPRI: 高优先级数据可读
POLLOUT: 数据(普通和优先)可写
POLLRDHUP: 对方关闭TCP连接或者关闭写端
POLLERR: 错误
POLLHUP: 挂起
POLLNVAL: 文件描述符没有打开
- 应该根据recv调用的返回值区分socket接收到的是有效数据还是对方关闭连接的请求
- nfds指定fds数组的大小 5.timeout单位为ms, 为-1时调用将阻塞直到有事件发生,为0则立刻返回
- 返回就绪的文件描述符总数
4. epoll系统调用
由一组函数完成任务, 把用户关心的文件描述符上的事件放在内核里的一个事件表中, 无需每次调用重传. epoll需要一个额外的文件描述符标识内核中的这个事件表
#include <sys/epoll.h>
int epoll_create(int size);
size并不起作用, 只是提示内核事件表需要多大, 返回内核事件表的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
fd是要操作的文件描述符, op是操作类型:
EPOLL_CTL_ADD: 往事件表中注册fd上的事件
EPOLL_CTL_MOD: 修改fd上注册的事件
EPOLL_CTL_DEL: 删除fd上注册的事件
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;
epoll事件类型的宏是在poll对应的宏前加E ptr可用来指向用户自定义类数据, 并包含fd以此将文件描述符和用户数据关联起来
epoll_ctl 成功返回0 失败返回-1 并设置errno
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
成功返回就绪的文件描述符个数, 失败返回-1并设置errno
timeout 和poll中的一样, maxevents指定最多监听的事件数,必须大于0
就绪事件从内核事件表复制到events指向的数组中
5. LT 和 ET 模式
LT: Level Trigger 水平触发, 对于读操作, 读缓冲区内容不为空, 每次调用都会返回该文件描述符读就绪; 对于写操作, 只要写缓冲区不满, 每次调用都会返回该文件描述符写就绪. 应用程序可以不立即处理该事件.
ET: Edge Trigger 边缘触发, 当有就绪事件时应用程序必须立刻处理该事件, 后续的调用将不再触发该文件描述符的就绪事件. 此模式降低了事件被触发的次数.
ET模式的注册的文件描述符应该是非阻塞的, 否则, 读写操作会阻塞.
6. select poll epoll区别
事件集合:
select: 通过3个参数分别传入可读,可写,异常事件对应的文件描述符, 每次调用都要重置这三个参数.
poll: 通过pollfd.events传入文件描述符和感兴趣的事件, 内核修改pollfd.revents反馈就绪的事件.
epoll: 内核通过事件表管理文件描述符及其事件, 每次调用无需重复传入, 通过events参数反馈就绪的事件
索引就绪文件的时间复杂度:
select: O(n)
poll: O(n)
epoll: O(1)
最大支持的文件描述符数:
select: 一般有最大值限制
poll: 65535 epoll: 65535
工作模式:
select: LT
poll: LT
epoll: LT和ET
内核实现和工作效率:
select: 轮询的方式检测就绪事件, 时间复杂度O(n)
poll: 同上
epoll: 回调的方式检测就绪事件, O(1)