1.I/O模型
Linux平台下有5中I/O模型,分别为阻塞式I/O、非阻塞式I/O、I/O复用、信号驱动式I/O及异步I/O。本节内容参考自《UNIX网络编程》。
(1)阻塞式I/O模型
最流行的I/O模型是阻塞式I/O模型。一般情况下,默认的IO都是阻塞的。以UDP套接字为例,当进程调用recvfrom系统调用,直到数据被复制到应用进程的缓冲区或者发生错误才返回,常见的系统调用是被系统中断。若内核中的数据还未准备好,则调用recvfrom的进程被阻塞。
(2)非阻塞式I/O模型
非阻塞式I/O需要显示的设置,可通过fcntl函数将描述符设置为非阻塞(O_NONBLOCK)。非阻塞式IO发起IO请求后,不管数据是否准备好,进程不会阻塞,若数据没有准备好,将会返回一个EWOULDBLOCK错误。如下图所示,应用进程通过recvform发起4次I/O请求,前三次数据还未准备好,recvform返回EWOULDBLOCK错误,第四次数据准备好了,则将数据复制到应用进程缓冲区并返回复制的字节数。
应用进程频繁的发起非阻塞式IO请求被称为轮训(polling)。轮训的方式有利有弊,需要根据使用场景评估。
(3)I/O复用模型(select、pselect、poll及epoll)
I/O复用属于一种高级轮训技术,一次可以轮训多个描述符,如果某个描述符就绪,则内核会通知应用层进程,随后,应用层进程发起I/O操作。I/O复用本质上是一种阻塞式I/O,但其阻塞在I/O复用的系统调用上,而不是阻塞在I/O系统调用上。I/O多路复用最大的优势是以较高的效率实现了单线程管理多个描述符的功能。Linux平台上提供了4个I/O复用接口,分别为select、pselect、poll及epoll,后续将进行介绍。如下图所示,应用进程调用select检测多个描述符,若所有描述符没有就绪,则阻塞在select上,若有描述符就绪,则立即返回,应用进程可对就绪描述符发起I/O操作。I/O复用的系统调用阻塞时间可以设置,若时间为0,则无论有没有描述符就绪,都立即返回,通过返回值可确定是否有描述符就绪。
(4)信号驱动式I/O模型(SIGIO)
前三种I/O模型都是应用进程主动轮训描述符,查看描述符是否就绪。信号驱动式I/O不需要应用进程主动轮训,只需要开启描述符的信号驱动式I/O功能,并通过sigaction系统调用注册一个信号处理函数即可,当描述符就绪时,内核会给应用进程发送SIGIO信号,随后信号处理函数被调用,应用进程可以在信号处理函数中发起I/O操作。如下图所示,应用进程开启信号驱动式I/O功能和注册信号处理函数后,就可以继续处理其他事情了,不用阻塞等待描述符就绪,当描述符就绪后,内核向应用进程发送信号,信号处理函数被调用,在信号处理函数中通过recvform发起I/O操作。
(5)异步I/O模型(POSIX的aio_系列函数)
上述四种I/O模型的I/O操作的发起都是由应用进程主动发起,而异步I/O模型的I/O操作由内核帮助发起,当I/O操作完成后通知应用进程。如下图所示,应用进程调用aio_read函数向内核传递描述符、缓冲区指针、缓冲区大小及文件偏移,并告诉内核I/O操作完成时如何通知应用进程。aio_read会立即返回,不会被阻塞,应用进程可继续处理其他事情。当内核完成I/O操作后向应用进程发送信号,此时数据已经被复制到应用进程的缓冲区了,不需要应用主动发起I/O操作。异步I/O发送信号时数据已被复制到应用进程缓冲区,这是与信号驱动I/O的最大区别。
2.各种I/O模型的比较
阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型、信号驱动式I/O模型的主要区别在第一阶段,即获取描述符就绪的方式不同,第二阶段都一致,都是由应用进程主动发起I/O操作,将数据复制到应用缓冲区。异步I/O的第二个阶段由内核完成,这是与其他四种I/O模型的最大区别。5种I/O模型的具体差别如下图所示。
(1)同步I/O操作(synchronous I/O opetation)会导致请求进程阻塞,直到I/O操作完成。
(2)异步I/O操作(asynchronous I/O opetation)不会导致请求进程阻塞。
根据上述定义,阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型、信号驱动式I/O模型都是同步I/O模型。
3.I/O复用系统调用接口
在TCP服务器中,I/O复用模型得到了广泛的应用,单个线程可处理多个socket描述符,大大的提高了服务器的并发效率。
3.1.select
在所有POSIX兼容的平台上,select都可以执行I/O多路复用功能。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds-最大描述符值加1,用来确定检测的描述符数量,最大为FD_SETSIZE,一般为1024,在glibc中有限制
readfds-指向可读描述符集合的指针,每次都要重新设置
writefds-指向可写描述符集合的指针,每次都要重新设置
exceptfds-指向可写或异常条件的描述符集合,每次都要重新设置
timeout-select超时的时间,传入NULL则会一直等待,直到有描述符就绪或者被信号打断,秒和微妙都为0时,
测试完所有描述符后立即返回,不会等待,某些情况下时间会被改变,最好每次调用select之前都设置一下时间
返回值-大于0:就绪的描述符数量,0:超时,-1:出错
fd_set是一个位图,使用下面的宏进行操作,FD_CLR将set中的描述符fd清除,FD_ISSET测试set中是否设置了描述符fd,FD_SET将描述符fd设置到set中,FD_ZERO将set全部清0.
#include <sys/select.h>
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
select有如下的缺点:
(1)检测的描述符数量有限制,一般最大为1024
(2)只能得到就绪的描述符数量,不知道具体哪些描述符就绪了,需要使用FD_ISSET宏遍历测试
(3)用户空间的描述符集合要被复制到内核中,内核中的就绪描述符集合要被复制到用户空间,来回的数据复制造成了额外的开销
3.2.pselect
pselect除了timeout和sigmask参数和select不一致,其他都一样。pselect是Linux平台特有,移植性较差,应用不多。
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
timeout-包含了秒和纳秒,时间精度相比select更高并且时间不会改变
sigmask-信号屏蔽字,若为NULL,则在信号方面与select相同,若不为NULL,则调用pselect时,会以原子的方式安装该信
号屏蔽字,在pselect返回时恢复以前的信号屏蔽字。
pselect的缺点和select的缺点一致。
3.3.poll
poll在select的基础上做了一些改进,不会限制检测的描述符数量,可设置检测描述符的事件类型。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds-检测的描述符数组
nfds-描述符数组的长度
timeout-超时时间,单位为毫秒
返回值-大于0:就绪的描述符数量,0:超时,-1:出错
描述符数组的类型为struct pollfd,fd为检测的描述符,events为检测的事件类型,由应用程序设置,revents为发生的事件类型,由内核设置。事件类型详见《UNIX环境编程》
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
poll相比select,没有了描述符数量的限制,但还是具有select其他缺点。
3.4.epoll
为了解决select的缺点,在Linux内核2.6版本中引入了epoll接口。同其他I/O复用接口一样,epoll可以检查多个文件描述符的状态(也可检查其他epoll的文件描述符)。epoll由三个系统调用组成,分别为epoll_create、epoll_ctl和epoll_wait。
epoll_create和epoll_create1用于创建一个epoll实例,但epoll_create1去掉了多余的参数size,增加了一个可用来修改系统调用行为的flags参数。
#include <sys/epoll.h>
int epoll_create(int size);
size-通过epoll实例添加描述符的最大数量,此参数已被废弃
返回值-大于0:epoll实例的描述符,不使用时需要调用close关闭,小于0:失败
int epoll_create1(int flags);
flags-系统调用相关参数,目前支持执行即关闭标志EPOLL_CLOEXEC(fork的子进程执行exec函数时将会关闭epoll实例)
返回值-大于0:epoll实例的描述符,不使用时需要调用close关闭,小于0:失败
epoll_ctl操作epoll实例检测的事件列表,可以向epoll实例中添加、删除及修改检测的描述符和事件类型,描述符被close后会自动从epoll实例中删除。使用epoll_ctl向内核添加描述符时都要占用一小段不能交换的内核内存空间,由于内存容量的限制,添加的描述符数量存在最大值。内核将最大值max_user_watches导出到了用户空间,在/proc/sys/epoll目录下,默认的上限值根据系统可用的内存计算得出,用户可修改。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd-epoll_create创建的epoll描述符
op-指定操作方式,EPOLL_CTL_ADD:添加描述符,EPOLL_CTL_MOD:修改描述符,EPOLL_CTL_DEL:删除描述符
fd-操作的描述符
event-操作的数据
返回值-0成功,小于0出错
struct epoll_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中events字段的事件掩码如下表所示,epoll_ctl列中填是的标记则由用户设置,epoll_wait列中填是的标记由内核设置。EPOLLERR和EPOLLHUP标记调用epoll_wait后内核可能设置,因此在epoll_wait返回后应用程序要检查。
位掩码 | epoll_ctl | epoll_wait | 描述 |
---|---|---|---|
EPOLLIN | 是 | 是 | 可读取非高优先级的数据 |
EPOLLPRI | 是 | 是 | 可读取高优先级数据 |
EPOLLRDHUP | 是 | 是 | 套接字对端关闭 |
EPOLLOUT | 是 | 是 | 普通数据可写 |
EPOLLET | 是 | 采用边缘触发事件通知 | |
EPOLLONESHOT | 是 | 在完成事件通知之后禁用检查 | |
EPOLLERR | 是 | 有错误发生 | |
EPOLLHUP | 是 | 出现挂断 | |
EPOLLWAKEUP | 是 | 有事件发生时,确保系统不休眠 | |
EPOLLEXCLUSIVE | 是 | 在水平触发模式中进行互斥性唤醒 |
EPOLLWAKEUP:如果设置了单次命中和ET模式,而且进程有休眠唤醒能力,当事件被挂起和处理时,此选项确保系统不进入暂停或休眠状态。自Linux内核3.5版本添加。
EPOLLEXCLUSIVE:在水平触发模式中进行互斥性唤醒,避免epoll LT模式的惊群效应,只能在EPOLL_CTL_ADD操作时设置。自Linux内核4.5版本添加。
EPOLLONESHOT标志:
默认情况下,一旦通过epoll_ctl的EPOLL_CTL_ADD操作将文件描述符添加到epoll实例中,描述符将保持激活状态(该描述符就绪会发出通知),直到通过epoll_ctl的EPOLL_CTL_DEL操作将描述符从epoll实例中删除。如果希望在某个特定的描述符上只得到一次通知,那么可以在传给epoll_ctl的ev.events中指定EPOLLONESHOT标志。如果指定了这个标志,那么在后续调用epoll_wait时,该文件描述符处于非激活状态,就算有事件发生,也不会通知用户进程,直到通过epoll_ctl的EPOLL_CTL_MOD重新激活描述符(去除EPOLLONESHOT标志)后才会通知应用进程。
水平(LT)触发和边缘(ET)触发:
默认情况下epoll提供的是水平触发通知,这表示epoll会告诉我们何时能在文件描述符上以非阻塞的方式执行I/O操作,这和poll和select所提供的通知类型相同。如果指定了EPOLLET标志,则该描述符将会以边缘触发的方式进行通知。边缘触发会将多个I/O事件合并只通知一次,通知后,不管应用进程是否处理I/O事件,后续不会再重复通知,除非再产生新的事件,而水平触发则相反,本次未处理的I/O事件,下次会接着通知。边缘触发方式也被称为高速模式。
epoll_wait返回与epoll实例相关联的就绪事件列表。events指向的结构体数组中返回的是有关就绪时间的信息,结构体数组空间由调用者申请,包含的元素最大个数由maxevents指定,就绪事件的数量由返回值确定。在数组events中,单个epoll_event元素的events成员表示发生事件的掩码,data字段是用户注册的数据指针,用户进程可根据此指针找到就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd-epoll_create创建的epoll文件描述符
events-存储就绪事件的指针,events成员为发生的事件掩码,data为注册的用户数据
maxevents-events指向的内存最epoll_event元素的最大个数
timeout-超时时间,单位为毫秒。timeout为:1,调用者将一直阻塞,直到注册的事件发生或者捕获了一个信号才返回。
timeout:0,执行一次非阻塞检查,timeout>0:最多阻塞timeout毫秒,如有事件发生或者捕获了一个信号会提前返回。
返回值-大于0:就绪事件的数量,0:超时,-1:出错
下面是一个简单的单线程TCP服务器,使用了I/O复用接口epoll,可以并发的处理多个TCP客户端的连接。
......
// 分配epoll_event缓冲区
evlist = (struct epoll_event*)malloc(sizeof(struct epoll_event) * evlist_size);
// EPOLL_CLOEXEC:当执行exec系列函数时会关闭从父进程继承的描述符
epfd = epoll_create1(EPOLL_CLOEXEC);
ev.events = (EPOLLIN | EPOLLRDHUP); // 设置epoll事件,描述符可读、对端sock关闭事件
ev.data.ptr = fd_node; // fd_node为用户数据结构指针
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev); // 将服务端描述符加入到epoll检测事件中
while (true) {
errno = 0;
ready = epoll_wait(epfd, evlist, evlist_size, TCP_SERVER_EPOLL_MS_TIME_OUT);
if (ready < 0) { // epoll_wait出错
if (EINTR != errno) { // 判断是否被信号打断,不是则线程直接退出
ret = ready;
goto quit;
}
} else if (ready > 0) {
for (i = 0; i < ready; i++) {
fd_node = (struct Fd_node*)evlist[i].data.ptr;
if(evlist[i].events & EPOLLIN) { // I/O事件处理
// 如果是服务端描述符,则说明有客户端连接,则接收连接
if (fd_node->fd == server_fd) {
client_fd = accept(server_fd, (struct sockaddr*)&addr, &len);
if (client_fd < 0) {
// 接收连接出错处理
......
} else {
......
ev.data.ptr = fd_node; // 设置用户数据结构
// 将接收的描述符添加到epoll检测的epfd中
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
......
}
} else { // 有数据要读取
// 客户端主动close, 会触发EPOLLIN和EPOLLRDHUP事件,此时recv会返回0
recv(fd_node->fd); // 进行I/O操作
if (ret <= 0) { // recv或send返回小于等于0,直接close描述符
close(fd_node->fd);
......
}
}
} else if (evlist[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
// 对端sock关闭、出现挂断、有错误发生处理
......
}
}
} else { // 超时处理
......
}
}
......
epoll系统调用有如下的优点:
(1)当检查大量的文件描述符时,epoll的性能延展性比select和poll高很多。
(2)epoll即支持水平触发也支持边缘触发,与之相反,select和poll只支持水平触发,而信号驱动式I/O只支持边缘触发。
epoll和信号驱动I/O的性能相近,但是epoll还有一些优于信号驱动I/O的地方:
(1)可以避免复杂的信号处理流程(必须信号队列溢出时的处理)。
(2)灵活性高,可以指定我们希望检查的事件类型。
4.I/O复用模型内核源码分析
select、pselect、poll的差别较小,基本原理一致,因此只分析select和epoll。Linux内核版本为4.6。
4.1.select
select对应的系统调用在arch/arm/kernel/calls.S文件中,声明为CALL(sys_select),具体的系统调用定义在fs/select.c文件中,定义如下:
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
内核中,select检测的事件位掩码如下表所示:
位掩码 | 输入事件 | 输出事件 | 描述 |
---|---|---|---|
POLLIN | 是 | 是 | 可读取非高优先级的数据 |
POLLRDNORM | 是 | 是 | 等同于POLLIN |
POLLRDBAND | 是 | 是 | 可读取优先级数据 |
POLLPRI | 是 | 是 | 可读取高优先级数据 |
POLLRDHUP | 是 | 是 | 对端套接字关闭 |
POLLOUT | 是 | 是 | 普通数据可写 |
POLLWRNORM | 是 | 是 | 等同于POLLOUT |
POLLWRBAND | 是 | 是 | 优先级数据可写入 |
POLLERR | 是 | 有错误发生 | |
POLLHUP | 是 | 出现挂断 | |
POLLNVAL | 是 | 文件描述符未打开 | |
POLLMSG | Linux中不使用(SUSv3中未指定) |
下面分析一下select系统调用在内核中的执行流程:
// 输入事件
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
// 输出事件
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
// 异常事件
#define POLLEX_SET (POLLPRI)
SYSCALL_DEFINE5(select, ......)
if (tvp) {
copy_from_user // 将时间参数拷贝到内核空间
// 将用户空间传入的时间转换为结束时间,即内核的当前时间加上超时时间
// 用户空间使用timeval表示时间,内核使用timespec表示时间,精度较用户空间高
poll_select_set_timeout
}
core_sys_select
// 在栈上分配256字节内存,当描述符较少时,直接使用栈上的内存以提高效率
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)]
// 若传入的最大描述符超过了进程打开的最大描述符,则取进程的最大描述符
rcu_read_lock();
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds; // 获取最大描述符
rcu_read_unlock();
if (n > max_fds)
n = max_fds;
// 分配位图,总共分配6个,in/out/ex个对应两个,一个用于拷贝用户空间传入的参数
// 一个用于保存内核设置的结果
size = FDS_BYTES(n); // 计算每个位图需要分配多少字节
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
ret = -ENOMEM;
// 若栈上分配的内存不够,则调用kmalloc在堆上分配
bits = kmalloc(6 * size, GFP_KERNEL);
......
}
......
// 将用户空间的fd_set拷贝到内核空间
if ((ret = get_fd_set(n, inp, fds.in)) ||
(ret = get_fd_set(n, outp, fds.out)) ||
(ret = get_fd_set(n, exp, fds.ex)))
......
do_select(n, &fds, end_time)
// 是否开启busy poll模式,如开启busy_flag为POLL_BUSY_LOOP
// busy poll由CONFIG_NET_RX_BUSY_POLL宏控制
unsigned int busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
max_select_fd // 检查传入的描述符是否有效并获取最大的描述符
poll_initwait // 初始化轮训队列poll_wqueues
// 初始化等待函数,指向__pollwait
init_poll_funcptr(&pwq->pt, __pollwait)
// 如果超时时间为0,则取消等待函数__pollwait
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait->_qproc = NULL;
timed_out = 1;
}
for (;;) {
// 位图的大循环,每次移动unsigned long
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
// 位图小循环,每次移动一个bit,bit是位图中的位,和描述符对应
for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
......
// 设置等待时的key
wait_key_set(wait, in, out, bit, busy_flag);
wait->_key = POLLEX_SET | ll_flag; // 设置异常的key
if (in & bit)
// 设置输入的key
wait->_key |= POLLIN_SET;
if (out & bit)
// 设置输出的key
wait->_key |= POLLOUT_SET;
// 对每一个描述符循环调用poll函数,poll函数和描述符对应文件的类型相关
// poll函数在文件系统层和驱动层都要有支持,返回值为发生事件的位掩码
mask = (*f_op->poll)(f.file, wait);
......
// 获取发生的输入事件
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit; // 将发生输入事件的描述符设置到返回的输入位图中
retval++; // 就绪事件加1
wait->_qproc = NULL;
}
// 获取发生的输出事件
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit; // 将发生输出事件的描述符设置到返回的输出位图中
retval++; // 就绪事件加1
wait->_qproc = NULL;
}
// 获取发生的异常事件
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit; // 将发生异常事件的描述符设置到返回的异常位图中
retval++; // 就绪事件加1
wait->_qproc = NULL;
}
......
// 若需要调度,则进行(轻量级)进程切换
cond_resched();
}
}
......
// 若有事件发生、超时或者信号挂起,则跳出循环返回
if (retval || timed_out || signal_pending(current))
break;
// 若出错,则返回错误,不在检查描述符
if (table.error) {
retval = table.error;
break;
}
/* only if found POLL_BUSY_LOOP sockets && not out of time */
if (can_busy_loop && !need_resched()) {
if (!busy_end) {
busy_end = busy_loop_end_time(); // 第一次busy poll时获取结束时间
continue;
}
// 若busy poll的时间未耗尽,则继续进行busy poll
if (!busy_loop_timeout(busy_end))
continue;
}
......
// 若是第一次循环并且有超时时间,则将时间类型timespec转换为时间类型ktime
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}
// 所有的描述符都没有事件发生,则进行可中断的睡眠
// poll_schedule_timeout返回0表示超时时间被耗尽,则设置超时标志,超时时间到,
// 会再扫描一次全部的描述符,扫描完毕,不管有没有事件发生,都返回
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,to, slack))
timed_out = 1;
}
......
// 将发生事件的描述符位图拷贝到用户传入的描述符位图中
if (set_fd_set(n, inp, fds.res_in) ||
set_fd_set(n, outp, fds.res_out) ||
set_fd_set(n, exp, fds.res_ex))
// 将剩余的超时时间拷贝到用户空间,这也是为什么调用select之前要重新设置超时时间的原因,
// 因为内核会修改用户空间的时间
poll_select_copy_remaining
对于socket描述符,do_select调用的poll函数由网络模块提供。其位于/net/socket.c文件中,在socket_file_ops文件中定义,poll函数指向sock_poll函数。下面分析一下sock_poll函数的执行流程,函数原型如下:
static unsigned int sock_poll(struct file *file, poll_table *wait)
sock_poll
// 如果socket支持busy poll并且设置了POLL_BUSY_LOOP,则进行busy poll
if (sk_can_busy_loop(sock->sk)) {
busy_flag = POLL_BUSY_LOOP;
if (wait && (wait->_key & POLL_BUSY_LOOP))
sk_busy_loop(sock->sk, 1);
}
// 调用struct proto_ops的poll函数,这和具体的网络协议相关,不再具体分析
return busy_flag | sock->ops->poll(file, sock, wait);
当poll函数发现没有事件发生时,将回调wait参数中的__pollwait函数,该函数的主要作用是将poll进程挂到底层的等待队列当中,当poll的描述符有时间发生,将唤醒poll进程
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
__pollwait
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
struct poll_table_entry *entry = poll_get_entry(pwq);
......
// 将poll进程挂到等待队列wait_address中,若有事件发生,将被唤醒
add_wait_queue(wait_address, &entry->wait);
busy_poll最早命名为Low Latency Sockets,是为了改善内核处理报文的延时问题。其主要思想就是在做socket系统调用时,如read操作时,在指定时间内由socket层直接调用驱动层方法去poll读取报文,大概可以提升几倍的PPS处理能力。busy_poll有两个系统层面的配置,第一个是/proc/sys/net/core/busy_poll,其设置的是select和poll系统调用时执行busy poll的超时时间,单位为us。第二个是/proc/sys/net/core/busy_read,其设置读取操作时的busy_poll的超时时间,单位也是us。从测试结果上看,busy_poll的效果很明显,但其也有局限性。只有当每个网卡的接收队列有且只有一个应用会读取时,才能提高性能。如果有多个应用同时都在对一个接收队列执行busy poll时,就需要引入调度器进行裁决,额外增加了性能损耗。
经过上面的分析,select的系统调用流程可以总结如下:
(1)如果设置了超时时间,则将时间参数从用户空间拷贝进内核空间,并将时间类型由timeval转化为timespec
(2)若描述符数量较少,则直接在栈上分配6个描述符位图,否则在堆上分配,3个位图用于拷贝用户空间传入的位图,3个用于保存就绪的描述符
(3)将用户空间的描述符位图拷贝到内核空间
(4)循环测试每个描述符,查看是否有事件发生,若该描述符没有事件发生,则会将进程挂到底层的等待队列中,若该描述符后续有事件发生,则将会将此进程唤醒。查看事件发生是通过底层提供的poll方法
(5)每次循环都会检测所有的描述符,若循环测试发现描述符有事件发生,则将描述符设置到位图中,循环完毕后会退出
(6)若所有描述符没有事件发生,超时时间为0,则直接返回,若超时时间大于0,则进程进入睡眠状态
(7)若描述符有事件发生或超时时间到,则select进程被唤醒,唤醒后会再次循环检测所有描述符,若有事件发生或超时时间到或有信号挂起则返回,否则继续睡眠等待事件发生
(8)返回时,会将保存就绪描述符的位图拷贝到用户空间
4.2.epoll
epoll对应的系统调用在arch/arm/kernel/calls.S文件中,声明如下:
CALL(sys_epoll_create1)
CALL(sys_epoll_create)
// sys_oabi_epoll_ctl是兼容API的系统调用接口
CALL(ABI(sys_epoll_ctl, sys_oabi_epoll_ctl))
// sys_oabi_epoll_wait是兼容API的系统调用接口
CALL(ABI(sys_epoll_wait, sys_oabi_epoll_wait))
具体的系统调用定义在fs/eventpoll.c文件中,定义如下:
SYSCALL_DEFINE1(epoll_create, int, size)
SYSCALL_DEFINE1(epoll_create1, int, flags)
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout)
4.2.1.epoll相关数据结构
epoll的核心数据结构为struct eventpoll,调用epoll_create时创建,数据成员如下:
struct eventpoll {
spinlock_t lock; // 保护此数据结构的自旋锁
struct mutex mtx; // 保护描述符在epoll使用时不被删除
wait_queue_head_t wq; // sys_epoll_wait使用的等待队列
// epoll也可检测其他epoll的描述符(只读事件),此时执行ep_eventpoll_poll
// 函数进行轮训,其在poll_wait队列上等待
wait_queue_head_t poll_wait;
struct list_head rdllist; // 就绪文件描述符链表
struct rb_root rbr; // 红黑树根节点,保存epoll检测的所有描述符
struct epitem *ovflist; // 暂存的就绪epitem
/* wakeup_source used when ep_scan_ready_list is running */
struct wakeup_source *ws;
/* The user that created the eventpoll descriptor */
struct user_struct *user;
struct file *file;
/* used to optimize loop detection check */
int visited;
struct list_head visited_list_link;
};
当调用epoll_ctl向内核添加检测的描述符时,内核会创建一个struct epitem结构体并设置相关信息后,插入到管理的红黑树中。
struct epitem {
union {
struct rb_node rbn; // 用于串联在红黑树上的节点
struct rcu_head rcu; // 用于释放struct epitem
};
struct list_head rdllink; // 用于连接到就绪链表
struct epitem *next; // 用于挂在ovflist链表的节点
struct epoll_filefd ffd; // 此epitem对应的文件描述符信息
int nwait; // 附加到poll操作的等待队列数量
struct list_head pwqlist; // 等待队列
struct eventpoll *ep; // 指向epoll的核心数据结构
struct list_head fllink; // 连接检测文件描述符对用的struct file结构体
/* wakeup_source used when EPOLLWAKEUP is set */
struct wakeup_source __rcu *ws;
/* The structure that describe the interested events and the source fd */
struct epoll_event event; // 保存此描述符检测事件和注册的私有数据
};
底层poll方法回调epoll注册的回调函数时会创建一个struct eppoll_entry结构体,主要用于epoll进程的唤醒。
struct eppoll_entry {
struct list_head llink; // 用于串联到struct epitem的pwqlist链表中
struct epitem *base; // 指向struct epitem
wait_queue_t wait; // 挂入底层等待队列,底层事件发生后通过此等待队列唤醒epoll进程
wait_queue_head_t *whead; // 保存底层等待队列头
};
4.2.2.epoll初始化
内核在启动的时候会初始化epoll模块,epoll的初始化函数为eventpoll_init,在/fs/eventpoll.c文件中。主要工作有初始化文件系统相关内容、初始化递归检查队列、初始化slab内存分配器,slab内存分配器可加快小块内存的分配。初始化函数原型如下:
static int __init eventpoll_init(void)
下面分析函数调用过程:
#define PAGE_SHIFT 12
// 添加一个描述符需要消耗内存的字节数
#define EP_ITEM_COST (sizeof(struct epitem) + sizeof(struct eppoll_entry))
eventpoll_init
si_meminfo // 获取系统内存信息
// 根据低端内存容量的%4计算一个用户的epoll最多检测的描述符数量
// si.totalram-si.totalhigh相减为页表数量,左移PAGE_SHIFT转换为字节数
max_user_watches = (((si.totalram-si.totalhigh)/25)<<PAGE_SHIFT)/EP_ITEM_COST
// 初始化递归检查队列
ep_nested_calls_init(&poll_loop_ncalls)
ep_nested_calls_init(&poll_safewake_ncalls)
ep_nested_calls_init(&poll_readywalk_ncalls)
// 初始化slab分配器,用于分配struct epitem结构体
epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),
0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL)
// 初始化slab分配器,用于分配struct eppoll_entry结构体
pwq_cache = kmem_cache_create("eventpoll_pwq",
sizeof(struct eppoll_entry), 0, SLAB_PANIC, NULL)
4.2.3.epoll_create
epoll_create内部调用的是epoll_create1。epoll_create返回epoll文件描述符,在内核中,可以通过epoll文件描述符找到对应的struct file结构体,从而得到epoll的操作方法和数据结构。
SYSCALL_DEFINE1(epoll_create, int, size)
if (size <= 0) // 如果size小于0,返回错误,但内核没有使用size参数
return -EINVAL;
sys_epoll_create1(0) // 调用epoll_create1,传入参数为0
SYSCALL_DEFINE1(epoll_create1, int, flags) // epoll_create1系统调用
// 编译时检查EPOLL_CLOEXEC和O_CLOEXEC值是否一样,
// 内核中EPOLL_CLOEXEC标志和O_CLOEXEC标志的作用是一样的
BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
// flags只能是0或者EPOLL_CLOEXEC标志,其他的将返回EINVAL错误
if (flags & ~EPOLL_CLOEXEC)
return -EINVAL;
ep_alloc // 分配struct eventpoll数据结构
kzalloc
spin_lock_init
mutex_init
init_waitqueue_head
init_waitqueue_head
// 分配描述符,设置传入的参数,读写属性
get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC))
__alloc_fd
// 创建epoll的文件描述符实例,即struct file结构体,实例的名称为[eventpoll],
// 实例的操作函数集合为eventpoll_fops,私有数据为struct eventpoll
anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC))
alloc_file // 分配struct file,并设置epoll操作函数集合和标志
// O_ACCMODE:取出文件标志的低两位,O_NONBLOCK:非阻塞
ile->f_flags = flags & (O_ACCMODE | O_NONBLOCK)
// 设置struct file的私有数据
file->private_data = priv
ep->file = file
// 将文件描述符和struct file结构体关联起来,之后可通过文件描述符访问struct file
fd_install(fd, file)
// 向应用层返回epoll的文件描述符
return fd
4.2.4.epoll_ctl
epoll_ctl完成epoll检测描述符的添加、修改和删除,对应用三个函数,分别为ep_insert、ep_remove和ep_modify。
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
// 添加或者修改描述符,则将用户空间的epoll_event拷贝到内核空间
copy_from_user
fdget(epfd) // 获取epoll文件描述符对应的struct fd结构体
fdget(fd) // 获取fd文件描述符对应的struct fd结构体
// epoll检测的描述符是否支持poll,不支持则返回错误
if (!tf.file->f_op->poll)
goto error_tgt_fput;
// 检查是否支持EPOLLWAKEUP标记
if (ep_op_has_event(op))
ep_take_care_of_epollwakeup(&epds);
// 检查epfd文件描述符是否是epoll专有的,不是返回错误
// epfd文件描述符必须由epoll_create创建,检测的描述符只要支持poll方法就行
if (f.file == tf.file || !is_file_epoll(f.file))
goto error_tgt_fput;
// EPOLLEXCLUSIVE只能在EPOLL_CTL_ADD操作时设置
if (epds.events & EPOLLEXCLUSIVE) {
if (op == EPOLL_CTL_MOD)
goto error_tgt_fput;
if (op == EPOLL_CTL_ADD && (is_file_epoll(tf.file) ||
(epds.events & ~EPOLLEXCLUSIVE_OK_BITS)))
goto error_tgt_fput;
}
// 获取epoll描述符对应的struct file的私有数据
// 此私有数据被epoll_create设置为eventpoll_fops
ep = f.file->private_data;
......
// 将要EPOLL_CTL_ADD的描述符的struct file挂入到tfile_check_list链表
list_add(&tf.file->f_tfile_llink, &tfile_check_list)
......
// 在红黑树中查找要操作的描述符,找到返回描述符所在的数据结构指针,否则返回NULL
epi = ep_find(ep, tf.file, fd)
......
switch (op) {
case EPOLL_CTL_ADD: // 添加操作
if (!epi) { // 若红黑树中无重复的描述符,则将描述符插入到红黑树中
// POLLERR和POLLHUP事件默认添加,epoll返回就绪描述符时需要检查
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else
error = -EEXIST;
......
break;
case EPOLL_CTL_DEL: // 删除操作
if (epi) // 若在红黑树中找到描述符。则删除描述符
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD: // 修改操作
if (epi) { // 若在红黑树中找到描述符。则修改描述符事件
// EPOLLEXCLUSIVE标记只能在EPOLL_CTL_ADD操作中添加
if (!(epi->event.events & EPOLLEXCLUSIVE)) {
// POLLERR和POLLHUP事件默认添加,epoll返回就绪描述符时需要检查
epds.events |= POLLERR | POLLHUP;
error = ep_modify(ep, epi, &epds);
}
} else
error = -ENOENT;
break;
}
首先分析ep_insert函数。ep_insert创建一个epitem结构体,调用检测描述符的poll函数,若有检测的事件发生,则将epitem结构体插入到eventpoll的rdllink链表中同时唤醒等待事件发生的进程,最后将epitem插入到红黑树中,具体流程如下:
ep_insert
// 读取当前用户下epoll检测的描述符数量
user_watches = atomic_long_read(&ep->user->epoll_watches);
// 大于最大描述符数量,返回错误
if (unlikely(user_watches >= max_user_watches))
return -ENOSPC;
// 使用slab分配器分配struct epitem结构体
epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)
// epitem结构体中的ep成员指向所属的eventpoll结构体
epi->ep = ep
// 将检测的描述符及描述符对应的file设置到ffd成员中,这样epitem结构体保存了检测描述符信息
ep_set_ffd(&epi->ffd, tfile, fd)
// 保存检测描述符事件信息和用户设置的数据
epi->event = *event
......
// 设置poll方法队列的回调函数,回调函数为ep_ptable_queue_proc
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc)
// 调用描述符对应的poll方法,返回发生事件的位掩码
revents = ep_item_poll(epi, &epq.pt)
pt->_key = epi->event.events // 设置事件掩码
// 调用检测描述符的poll函数
epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events
......
// 将检测描述符的file结构体挂到epitem的fllink成员中
list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links)
......
// 将分配的epitem结构体插入到eventpoll的rbr成员管理的红黑树中
ep_rbtree_insert(ep, epi)
......
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
// 若有事件发生且epitem结构体的rdllink为空,则将epitem添加到eventpoll的rdllist链表中
list_add_tail(&epi->rdllink, &ep->rdllist);
......
// 唤醒调用sys_epoll_wait时在wq上等待的进程
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq); // 带锁的唤醒
// 检查是否有其他epoll进程在poll_wait多列上等待
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
// 增加epoll检测的描述符数量
atomic_long_inc(&ep->user->epoll_watches)
if (pwake) // 唤醒在poll_wait队列上等待的进程
ep_poll_safewake(&ep->poll_wait);
ep_ptable_queue_proc函数在底层的poll_wait函数中调用,其主要作用是将执行poll的进程添加到底层的等待队列中并注册唤醒回调函数。
ep_ptable_queue_proc
// 从pwq_cache slab分配器中分配eppoll_entry结构体
pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)
// 初始化等待队列并注册等待队列的唤醒回调函数,当检测事件发生时,若有进程在底层等待队列上等待
// 则调用ep_poll_callback唤醒改进程
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback)
pwq->whead = whead; // 指向底层等待队列的队列头
pwq->base = epi; // 指向epoll传入的poll_table结构体
// 将epoll进程添加到底层的等待队列中,若有独占性标记,则进行设置
if (epi->event.events & EPOLLEXCLUSIVE)
add_wait_queue_exclusive(whead, &pwq->wait)
wait->flags |= WQ_FLAG_EXCLUSIVE // 添加独占性标志
__add_wait_queue_tail
else
add_wait_queue(whead, &pwq->wait)
wait->flags &= ~WQ_FLAG_EXCLUSIVE // 清除独占性标志
__add_wait_queue_tail
// 将分配的eppoll_entry结构体添加到epitem的pwqlist链表中
list_add_tail(&pwq->llink, &epi->pwqlist)
epi->nwait++ // 增加等待进程数量
当进行EPOLL_CTL_DEL操作删除描述符时,ep_remove函数得到调用。ep_remove的工作有将描述符从等待队列、红黑树及就绪列表中删除,最后释放内存。
ep_remove
ep_unregister_pollwait // 释放底层的pwd等待队列
rb_erase // 从红黑树中删除此描述符对用的epitem
spin_lock_irqsave(&ep->lock, flags); // 加锁
if (ep_is_linked(&epi->rdllink)) // 如果在就绪链表中,则删除
list_del_init(&epi->rdllink);
spin_unlock_irqrestore(&ep->lock, flags);
call_rcu(&epi->rcu, epi_rcu_free)
epi_rcu_free
kmem_cache_free(epi_cache, epi) // 释放epitem结构体
atomic_long_dec // 减小epoll检测描述符的数量
当进行EPOLL_CTL_MOD操作修改描述符时,ep_modify函数得到调用。ep_modify函数将要修改的事件掩码和用户数据设置到epitem结构体当中,然后调用一次poll方法,若有事件发生,则唤醒等待的进程。
ep_modify
epi->event.events = event->events; /* 设置修改的事件 */
epi->event.data = event->data; /* 设置修改的用户数据 */
smp_mb // SMP
ep_item_poll // 重新执行一次poll函数
epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events
// 检查返回的事件,若有检测事件发生,则将此epitem加入到就绪链表中
if (revents & event->events) {
spin_lock_irq(&ep->lock);
if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
/* 唤醒等待进程 */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
spin_unlock_irq(&ep->lock);
}
if (pwake)
ep_poll_safewake(&ep->poll_wait);
4.2.5.epoll_wait
epoll_wait等待描述符就绪,然后返回描述符就绪的事件,
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *,
events, int, maxevents, int, timeout)
// 检查用户空间缓冲区是否可写,若不可写,则返回错误
if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event)))
return -EFAULT;
f = fdget(epfd) // 获取epfd对应的struct fd
// 开始轮训描述符
error = ep_poll(ep, events, maxevents, timeout)
if (timeout > 0) {
// 若设置了阻塞时间,则将阻塞时间转换为内核时间类型ktime_t
struct timespec end_time = ep_set_mstimeout(timeout);
slack = select_estimate_accuracy(&end_time);
to = &expires;
*to = timespec_to_ktime(end_time);
} else if (timeout == 0) {
// 若时间为0,则不阻塞,直接进行轮训,轮训结束后返回
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
goto check_events;
}
fetch_events:
spin_lock_irqsave // 加锁
// 判断是否有事件就绪,判断的标准有两个:就绪链表rdllist非空
// 或暂存就绪事件ovflist链表非空
if (!ep_events_available(ep)) { // 没有事件就绪且设置了超时时间,则准备睡眠
// 初始化等待队列
init_waitqueue_entry(&wait, current);
// 采用默认的唤醒函数
q->func = default_wake_function
__add_wait_queue_exclusive(&ep->wq, &wait);
// 设置独占性唤醒标志,当唤醒的进程含有WQ_FLAG_EXCLUSIVE标志时,
// 则不再唤醒后续的进程
wait->flags |= WQ_FLAG_EXCLUSIVE;
// 将进程添加到eventpoll的等待队列wq的末尾
__add_wait_queue(q, wait);
for (;;) {
// 设置进程的状态为可中断的睡眠状态
set_current_state(TASK_INTERRUPTIBLE);
// 再次判断是否有检测事件就绪或超时时间到,如满足则跳出循环准备退出
if (ep_events_available(ep) || timed_out)
break;
// 若有信号挂起,则跳出循环准备退出
if (signal_pending(current)) {
res = -EINTR;
break;
}
// 释放锁
spin_unlock_irqrestore(&ep->lock, flags);
// 睡眠。返回0:睡眠时间到期,返回负值:出现错误,可能被信号打断
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
}
// 从for循环中跳出并且从等待队列中删除
__remove_wait_queue(&ep->wq, &wait);
// 设置进程状态为运行状态
__set_current_state(TASK_RUNNING);
}
check_events:
// 检查是否有事件就绪
eavail = ep_events_available(ep);
spin_unlock_irqrestore(&ep->lock, flags);
// 若无信号发生且有事件就绪,则调用ep_send_events将事件拷贝到用户空间
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out)
// 若超时时间为到期,则跳转到fetch_events继续等待事件发生
goto fetch_events;
// 返回就绪描述符的数量
return res;
接下来分析一下内核是如何将就绪事件从内核空间传送到用户空间,此任务主要在回调函数ep_send_events_proc中完成。首先定义一个临时链表txlist,将就绪链表从rdllist中移除,然后添加到临时链表txlist中。接着进入到ep_send_events_proc函数总,遍历就绪链表,对每个描述符调用poll函数轮训是否有事件发生,若有事件发生,则将发生的事件信息拷贝到用户空间。在轮训就绪链表上的描述符期间,有可能重新产生事件,此时就绪描述符暂存在ovflist链表中,轮训结束将ovflist上的就绪描述符添加带就绪链表rdllist上。最后将txlist链表添加到rdllist上(正常情况下txlist为空,发生错误的情况下txlist上可能还有未处理的描述符,因此要重新加入到就绪链表中,便于下次再处理)。
关于描述符被加入就绪链表的规则如下:
(1)就绪链表中的描述符,本次未发生事件,则从就绪链表中移除,不会再加入到就绪链表中
(2)就绪链表中的非水平触发的描述符,不管本次是否发生事件,都不会加入就绪链表
(3)就绪链表中的水平触发的描述符,本次发生了事件,则拷贝完事件信息后会重新加入就绪链表
// 若描述符设置了EPOLLONESHOT标志,则在拷贝就绪事件信息后会设置EP_PRIVATE_BITS标志
#define EP_PRIVATE_BITS (EPOLLWAKEUP | EPOLLONESHOT | EPOLLET | EPOLLEXCLUSIVE)
ep_send_events
esed.maxevents = maxevents // 保存用户空间传入的事件最大数量
esed.events = events // 保存用户空间传入的存储事件的指针
// 扫描就绪链表,
ep_scan_ready_list(ep, ep_send_events_proc, &esed, 0, false)
spin_lock_irqsave // 加锁
// 将就绪链表从rdllist中移除,然后添加到临时链表txlist中
list_splice_init(&ep->rdllist, &txlist)
// 将暂存就绪链表清空,若在轮训期间有事件发生,则可将就绪描述符暂存到ovflist链表中
ep->ovflist = NULL // 将暂存就绪链表清空,
spin_unlock_irqrestore // 释放锁
// 调用传入的回调函数ep_send_events_proc,将就绪事件信息拷贝到用户空间
error = (*sproc)(ep, &txlist, priv)
init_poll_funcptr(&pt, NULL) // 初始化poll_table
// 遍历
for (eventcnt = 0, uevent = esed->events;
!list_empty(head) && eventcnt < esed->maxevents;) {
// 获取就绪事件epitem结构体
epi = list_first_entry(head, struct epitem, rdllink);
......
// 将就绪事件epitem结构体从rdllink链表中移除
// 若不发生事件,则不会再加入到就绪链表rdllist中
list_del_init(&epi->rdllink);
// 对就绪链表的描述符,调用对应的poll方法,轮训发生的事件
revents = ep_item_poll(epi, &pt);
pt->_key = epi->event.events;
// poll返回发生事件的位掩码
return epi->ffd.file->f_op->poll(epi->ffd.file, pt)
& epi->event.events;
if (revents) {
// 将发生事件的位掩码和对应的用户数据拷贝到用户空间
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
......
}
eventcnt++; // 拷贝事件数量加1
uevent++; // 移动用户空间缓冲区指针
// 如果有EPOLLONESHOT标志,则设置EP_PRIVATE_BITS标志
// EP_PRIVATE_BITS标志的作用在唤醒回调函数ep_poll_callback中解释
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
else if (!(epi->event.events & EPOLLET)) {
// 若描述符不是边缘触发,则将epitem重新加入到就绪链表rdllist中
list_add_tail(&epi->rdllink, &ep->rdllist);
......
}
}
// 就绪链表中的描述符,本次未发生事件,则从就绪链表中移除,不会再加入到就绪链表中
}
return eventcnt // 返回拷贝的就绪事件数量
spin_lock_irqsave // 加锁
// 在轮训就绪事件期间,可能会重新产生就绪事件,此时就绪事件保存在ovflist链表中
// 遍历ovflist链表,将就绪事件添加到就绪链表rdllist中
for (nepi = ep->ovflist; (epi = nepi) != NULL;
nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
}
}
// 将ovflist链表设置为不活跃状态,此后不能再向ovflist中添加就绪描述符
ep->ovflist = EP_UNACTIVE_PTR;
// 将txlist链表接到就绪链表rdllist的后边
list_splice(&txlist, &ep->rdllist)
// 若就绪链表为空,则唤醒等待的进程
if (!list_empty(&ep->rdllist)) {
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
spin_unlock_irqrestore // 解锁
// 唤醒在epoll上等待的epoll
if (pwake)
ep_poll_safewake(&ep->poll_wait);
若底层有事件发生了且有进程在底层等待队列上等待,则调用ep_poll_callback函数唤醒等待进程,主要的流程总结如下:
(1)若设置了EPOLLONESHOT标志,则此事件不会再加入到ovflist或就绪链表rdllist中,除非应用EPOLL_CTL_MOD修改了描述符
(2)若ovflist不等于EP_UNACTIVE_PTR,说明进程正在轮训描述符,不能将就绪的描述符加入到就绪链表rdllist中,需要暂存到ovflist链表中,等进程轮训完毕,会将ovlist链表中的就绪事件迁移到rdllist中
(3)若就绪描述符没有加入到就绪链表中,则会加入到就绪链表中
(4)若等待队列wq上有进程在等待,则唤醒进程
(5)若等待队列poll_wait上有进程在等待,则唤醒进程
ep_poll_callback
spin_lock_irqsave(&ep->lock, flags) // 加锁
// 此处的作用是判断描述符是否设置了EPOLLONESHOT标志,若设置了,
// 则直接跳到out_unlock处,不会将描述符添加到就绪链表中,
// 也不会唤醒在wq上等待的进程,除非应用EPOLL_CTL_MOD修改了描述符
if (!(epi->event.events & ~EP_PRIVATE_BITS))
goto out_unlock;
// 发生的事件是否是感兴趣事件,若不是,则不会将描述符添加到就绪链表中
if (key && !((unsigned long) key & epi->event.events))
goto out_unlock;
// ovflist链表用来暂存轮训期间发生的描述符,ovflist为NULL时可暂存,
// 为EP_UNACTIVE_PTR不可暂存。
if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
if (epi->next == EP_UNACTIVE_PTR) {
// 将发生事件的epi插入到ovflist链表的第一项
epi->next = ep->ovflist;
ep->ovflist = epi;
......
}
// 若在ovflist中,则准备退出,不会再加入到就绪链表rdllist中
goto out_unlock;
}
if (!ep_is_linked(&epi->rdllink)) {
// 若epi不在就绪链表中,则将其加入到就绪链表中
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
}
// 若执行epoll_wait的进程在wq队列上等待,则唤醒
if (waitqueue_active(&ep->wq)) {
......
wake_up_locked(&ep->wq);
}
if (waitqueue_active(&ep->poll_wait))
pwake++;
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
/* We have to call this outside the lock */
if (pwake)
ep_poll_safewake(&ep->poll_wait);
if (epi->event.events & EPOLLEXCLUSIVE)
return ewake;
return 1;
4.2.6.关闭描述符
检测的描述符关闭时,内核会将对应的struct epitem结构体从epoll的红黑树中删除,调用流程如下:
close
sys_close
__close_fd
filp_close
fput
____fput
__fput
eventpoll_release
// 从红黑树中删除epitem
ep_remove
关闭epoll实例时,内核最终调用到ep_eventpoll_release函数,其会销毁整个红黑树并释放epoll的数据结构,调用流程如下:
close
sys_close
__close_fd
filp_close
fput
____fput
__fput
// 应用调用close时,内核中调用文件系统的release函数
file->f_op->release(inode, file)
// epoll_create1或epoll_create创建的描述符,其release
// 函数对应于ep_eventpoll_release
ep_eventpoll_release
ep_free
// 遍历整个红黑树,释放所有epitem
while ((rbp = rb_first(&ep->rbr)) != NULL) {
epi = rb_entry(rbp, struct epitem, rbn);
ep_remove(ep, epi);
}
// 释放epoll对应的数据结构
kfree(ep)
5.测试
测试结果如下图所示:
5.1.VxWorks平台
(1)一个连接一个任务模型的最大瓶颈在内存上,创建任务会消耗较多的内存。当建立75个连接时,内存基本被消耗完,不能再继续建立连接,此时CPU的占用率只有54%
(2)I/O复用模型,只创建一个任务,同时完成接收客户端的连接和数据的收发,其最大的瓶颈在CPU上,最多可建立240个连接,此时CPU占用率为97%,而内存占用却大大降低
5.2.Linux平台
(1)一个连接一个任务模型最多可以建立450个连接,超过后连接不稳定,易中断
(2)I/O复用模型最多可以建立900个连接,CPU占用率接近100%,相比一个连接一个任务模型,内存占用率有明显降低
(3)50毫秒发送一次,TCP数据长度为200字节,select效率比epoll稍高,说明在通信频率较高的情况下,select效率和epoll效率差别不大
(4)TCP数据长度为200字节,连接数量为900,发送间隔分别为100毫秒、200毫秒、400毫秒、600毫秒。随着发送间隔的增大,epoll的CPU占用率渐比select的CPU占用率降低,说明,在通信不频繁的场合,epoll的效率要比select的效率高。
5.3.Linux系统进程/线程数限制
一个系统能够支持创建的进程和线程数目是有上限的,资源上限一般是系统的可用内存大小。但是我们常常遇到内存剩余很多却仍然创建线程失败的情况,这就和系统的一些配置直接相关了
(1)ulimit -u 用户级别:决定单个用户能够创建的进程数量上限,系统默认为threads-max的一半
(2)/proc/sys/kernel/threads-max 系统级别:根据物理内存决定整个系统能够创建的进程数量上限
(3)/proc/sys/kernel/pid_max 系统级别:决定整个系统能够创建的进程id值的上限
(4)/proc/sys/vm/max_map_count 进程级别:决定单个进程能够做内存映射的数目上限,由于多线程程序的栈空间依赖mmap实现,从而间接决定了单个进程能够创建的线程数上限
6.总结
6.1. select、poll、pselect
(1)每次调用,都要将描述符从用户空间拷贝到内核空间,返回时要将描述符从内核空间拷贝到用户空间
(2)就绪描述符使用位图表示,需要遍历位图的所有位才能确定就绪的描述符
(3)添加的描述符数量有限制,glibc中通常限制为1024
(4)内核中需要对每一个描述符调用poll方法,在事件产生频率较低的情况下,效率较低
6.2. epoll
(1)只有添加描述符时才拷贝,轮训时不拷贝描述符
(2)返回全部就绪描述符
(3)添加的描述符无明确限制,只和内存大小相关
(4)采用回调函数通知模式,事件发生后会挂到就绪链表中,只需要对就绪链表中的描述符执行poll方法
(5)支持更多的模式,能适用多样化的通信环境
模型,内存占用率有明显降低
(3)50毫秒发送一次,TCP数据长度为200字节,select效率比epoll稍高,说明在通信频率较高的情况下,select效率和epoll效率差别不大
(4)TCP数据长度为200字节,连接数量为900,发送间隔分别为100毫秒、200毫秒、400毫秒、600毫秒。随着发送间隔的增大,epoll的CPU占用率渐比select的CPU占用率降低,说明,在通信不频繁的场合,epoll的效率要比select的效率高。