Linux 服务器编程学习笔记⑦
# I/O复用
# SELECT
在一段指定时间内,通过轮询监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
# API 简介
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
nfds 被监听的文件描述符总数(通常设置为所有文件描述符中最大的加一)。
readfds、writefds、exceptfds 分别指向可读、可写、异常等事件对应的文件描述符集合。
timeout 用于设置 select 超时的时间(调用失败时,其值是不确定的;传 NULL 值将会一直阻塞)。
select 成功时返回就绪文件总数,超时返回0,失败返回 -1 并设置 errno。若在 select 等待期间程序接收到信号,则 select 立即返回 -1 并设置 errno 为 EINTR。
文件描述符可读就绪条件:
socket 内核接受缓存区字节数大于等于低水位标志 SO_RCVLOWAT。读操作返回值大于 0。
socket 对方关闭连接。读操作返回值等于 0。
监听 socket 上有新的连接请求。
socket 上有未处理的错误,可以通过 getsocketopt 来读取和清除该错误。
文件描述符可写就绪条件:
socket 内核发送缓存区字节数大于等于低水位标志 SO_SNDLOWAT。写操作返回值大于 0。
socket 写操作被关闭。对关闭了写操作的 socket 执行写将触发SIGPIPE信号。
socket 使用非阻塞 connect 连接成功或失败之后。
socket 上有未处理错误,可以通过 getsocketopt 来读取和清除该错误。
select 只能处理一种异常情况——接收到带外数据(使用带 MSG_OOB 标志的 recv 函数读取带外数据)。
select 通过以下宏来访问 fd_set 中的位(即 select 通过存放 fd 标志位的数据结构来表征文件描述符,此数据结构会在内核和用户空间中拷贝)。
#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是否被设置
# 编程 Demo
int main()
{
struct timeval timeout;
int max_fd = 0; // 用于记录最大的 fd,在轮询中时刻更新即可
fd_set* read_fds;
// 初始化 read_fds
init_read_fd(&read_fd, &max_fd);
while (true)
{
// 阻塞获取 每次需要把 fd 从用户态拷贝到内核态
nfds = select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
// 每次需要遍历所有fd,判断有无读写事件发生
for (int i = 0; i <= max_fd && nfds; ++i)
{
// 只读已就绪的文件描述符,不用过多遍历
if (FD_ISSET(i, &read_fds))
{
// 这里处理read事件
}
}
}
return 0;
}
# POLL
本质上与SELECT类似,在指定时间内通过轮询一定数量的文件描述符测试其中是否有就绪事件。
# API 简介
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds,int timeout);
struct pollfd
{
int fd; // 文件描述符
short events; // 注册的事件
short revents; // 实际发生的事件,内核填充
}
fds 是一个 pollfd 结构类型的数组,指定所有我们感兴趣的文件描述符上的可读、可写和异常事件。
nfds 指定监听集合 fds 的大小。
timeout 超时单位毫秒,其值为 -1 时 poll 将永远阻塞。
poll事件类型:
POLLIN——数据可读;
POLLPRI——高级优先级数据可读(TCP带外数据);
POLLOUT——数据可写;
POLLRDHUP——TCP对方关闭了连接;
POLLERR——错误;
POLLHUP——挂起;
POLLNVAL——文件描述符未打开。
POLLRDNORM——普通数据可读;
POLLRDBAND——优先级带数据可读(Linux不支持);
POLLWRNORM——普通数据可写;
POLLWRBAND——优先级数据可写;
# 编程 Demo
int main()
{
struct pollfd fds[POLL_LEN];
unsigned int nfds = 0;
// 初始化 fds
init_fds(&fds, &nfds);
while (true)
{
res = poll(fds, nfds, -1);
for (int i = 0; i < nfds; ++i)
{
if(fds[i].revents & (POLLIN | POLLERR))
{
// 读操作或处理异常等
// 退出遍历循环条件
if(--res <= 0)
{
break;
}
}
}
}
return 0;
}
# EPOLL
SELECT 和 POLL 都会随着监听 fd 数目的增多而导致效率线性下降;而 EPOLL 不会。
# API 简介
一共有三个函数:
epoll_create:
int epoll_create(int size);
该函数生成一个 epoll 专用的文件描述符。
size 参数告诉内核这个监听的数目一共有多大,是对内核初始分配内部数据结构的一个建议。(linux 2.6.8 之后,size 参数是被忽略的,即可以填大于 0 的任意值。)
成功,返回poll 专用的文件描述符; 失败,返回-1。
epoll_ctl:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll 的事件注册函数。epoll 不同于 select 是在监听事件时告诉内核要监听什么类型的事件,而是通过这个函数预先注册要监听的事件类型。
epfd 参数是 epoll 专用的文件描述符,即 epoll_create() 的返回值;
op 参数表示动作,用三个宏来表示:
- EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
fd 参数表示需要监听的文件描述符。
event 参数告诉内核要监听什么事件:
EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的;
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里;
成功,返回 0; 失败,返回-1。
epoll_wait:
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
等待事件的产生,收集在 epoll 监控的事件中已经发送的事件。
epfd 参数是 epoll 专用的文件描述符,即 epoll_create() 的返回值;
events 参数是分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
maxevents 参数告诉内核这个 events 有多少个 。
timeout 参数即为超时时间,单位为毫秒,为 -1 时,函数阻塞。
成功,返回需要处理的事件数目(0 表示超时);失败,返回 -1。
# 编程 Demo、
int main()
{
const int MAX_EVENT_NUMBER = 10000; //最大事件数
// 初始化红黑树和事件链表结构 rdlist 结构
epoll_event events[MAX_EVENT_NUMBER];
// 创建 epoll 实例
int m_epollfd = epoll_create(5);
if(m_epollfd==-1)
{
printf("fail to epoll create!");
return m_epollfd;
}
// 创建节点结构体将监听连接句柄
epoll_event event;
event.data.fd = m_listenfd;
// 设置该句柄为边缘触发(数据没处理完后续不会再触发事件,水平触发是不管数据有没有触发都返回事件),
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
// 添加监听连接句柄作为初始节点进入红黑树结构中,该节点后续处理连接的句柄
epoll_ctl(m_epollfd, EPOLL_CTL_ADD, fd1, &event);
//进入服务器循环
while(true)
{
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
printf( "epoll failure");
break;
}
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
// 属于处理新到的客户连接
if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
// 处理相关异常
}
else if (events[i].events & EPOLLIN)
{
// 处理读事件
}
else if (events[i].events & EPOLLOUT)
{
// 处理写事件
}
//else if 其他处理逻辑
}
}
return 0;
}
# 三种 IO 复用接口对比
SELECT | POLL | EPOLL | |
---|---|---|---|
性能 | 随着连接数的增加,性能急剧下降,处理大量并发连接时,性能很差 | 随着连接数的增加,性能急剧下降,处理成千上万的并发连接数时,性能很差 | 随着连接数的增加,性能基本没有变化 |
连接数 | 1024 | 无限制 | 无限制 |
内存拷贝 | 每次调用 select 拷贝 | 每次调用 poll 拷贝 | 调用 epoll_ctl 拷贝,每次调用epoll_wait 不拷贝 |
内部数据结构 | bitmap | 数组 | 红黑树 |
处理机制 | 线性轮询 | 线性轮询 | 事件回调 |
时间复杂度 | O(n) | O(n) | O(1) |