LINUX kernel research ---- IO multiplexing function epoll kernel source code in-depth analysis

There are two efficiency bottlenecks for select and poll

    1. Every time these functions are called, the monitored fd and the events to be monitored need to be copied from user space to kernel space, which greatly affects efficiency.

And epoll is to save the fd copied in user space and the events that need to be monitored. It only needs to copy all fd and events to be monitored from user space to kernel space only once when calling epoll_ctl.

    2. Both select and poll use linear polling in the kernel to check the active fds in the entire array (poll is a linked list), which wastes unnecessary time for many fds without data.

If we no longer check the active fd , but the active fd automatically calls a callback function and hangs itself in the ready queue. Isn't that so simple?

EPOLL automatically puts the ready file descriptors into a ready list through the callback function without traversing the file descriptors. Return the ready file descriptor to the user through the epoll_wait() function.

 

Scenarios where EPOLL is widely used :

The number of people connected is very large, the number of active people is relatively small, and the CPU utilization rate is significantly improved, which is the advantage of epoll.

In the case of multi-link and multi-active, the efficiency improvement is not obvious, and the three multi-channel IO transfers are not very different. If there is a large amount of activity, the callback function will be called repeatedly, which will affect the efficiency.

The maximum open file descriptor set at this time can be changed to the hardware level. The software level is generally 65535. epoll needs to break through the system resource setting of 65535, and add hardware limit and software limit settings in the configuration file r.

$ulimit –n 90000

by command

$cat /pro/sys/fs/fs/file-max

View the maximum number of open files.

 

Epoll kernel implementation:

The problem analysis process is more complicated, please click on the picture to expand the detailed view! ! ! The following is just a brief summary:


Kernel preparations for EPOLL :

This module registers a new filesystem called "eventpollfs" (in the eventpoll_fs_type structure) during kernel initialization (operating system startup), and then mounts the filesystem.

In addition, two kernel caches are created (in kernel programming, if you need to allocate small blocks of memory frequently, kmem_cahe should be created as a "memory pool"), which are used to store struct epitem and eppoll_entry respectively.

This kernel high-speed cache area is to create a continuous physical memory page, that is, to physically allocate a memory object of the size you want, and use the free allocated memory each time it is used.

Now think about why epoll_create returns a new fd?

Because it creates a new file in this filesystem called "eventpollfs"! What is returned is the fd index of the file. Well follow the Linux everything is a file feature.

int epoll_create(int size);

When epoll_create, in addition to helping us create a new file node in the epoll file system, the kernel returns the node to the user. And build a red- black tree in the kernel cache to store the fds that need to be monitored later from epoll_ctl, these fds will be saved in the kernel cache in the form of red-black tree nodes to support fast search, insert, delete operations .

int epoll_wait(int epfd,struct epoll_event *events,int maxevents, int timeout);

The kernel will also establish a list list to store the event-ready fd. The kernel will copy the ready event to the user space in the events passed in the parameter. The event array events of the ready queue need to be created by yourself and passed in as a parameter so that you can receive the set of descriptors that need to be processed when epoll_wait returns.

当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。

有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已因为在基数大,活跃量少的情况下应用优势明显,如果活跃量很大的话回调函数反复调用影响效率。所以,epoll_wait仅需要从内核态copy少量的fd到用户态. 所以,epoll_wait非常高效。

 

int epoll_ctl(int epfd, intop, int fd, struct epoll_event *event);

那么准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socketf放到epoll对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个socketf的中断了也就是有数据要处理时调用回调函数,这个回调函数也就把fd放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd插入到准备就绪list链表里了。

如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。

1.执行epoll_create时,创建了红黑树

2.执行epoll_ctl时,创建就绪list链表,如果增加fd添加到红黑树上,然后向内核注册有事件到来时的回调函数,当设备上的中断事件来临时这个回调函数会向list链表中插入就绪的fd。

3.执行epoll_wait时立刻返回准备就绪链表里的数据即可

 

epoll的优点:

1)支持一个进程打开大数目的socket描述符(FD)

select最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核。

不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2)IO效率不随FD数目增加而线性下降

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的,但是select/poll每次调用都会线性轮询扫描全部的fd集合,导致效率呈现线性下降。

epoll不存在这个问题,它只会对”活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的回调函数实现的。那么,只有”活跃”的socket才会主动的去调用callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在Linux内核。

3)两种触发模式,ET模式减少epoll_wait()的调用次数

EPOLL ET 边沿触发:只触发一次,无论缓冲区中是否还有剩余数据,直到有新的数据到达才会被触发,再去读取缓冲区里面的数据。

EPOLL LT 水平触发(默认): LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket,每次缓冲区都有数据都要触发。

Epoll可以监控管道文件,任意文件,不仅仅是socket文件


边沿触发的应用场景之一

客户端给我写的数据带有自己设计的协议头

而我只需要读取客户端数据的协议头,判断是否需要继续往下读取

如果不需要则不继续读取剩下的数据,增加程序运行的效率。

epoll工作在ET模式的时候,必须使用非阻塞套文件读写,以避免由于一个文件句柄的阻塞读/阻塞写操作容易阻塞在read函数时,因为没有读取到需要的字节数,而服务器又不能脱离read的阻塞状态去调用epoll函数接收客户端的数据造成死锁。

解决方法

1、非阻塞读取用fcntl 修改文件描述符的非阻塞读属性。

2、在open时指定非阻塞打开属性

Epoll-ET的非阻塞IO模型::read非阻塞轮询+边沿触发

去读取文件描述符的数据,直到为0,最效率的方法。

减少epoll_wait的调用次数提高程序的效率。

支持ET模式的原理,具体看最后面的内核的分析!!!!

当一个fd上有事件发生时,内核会把该fd插入上面所说的准备就绪rdlist链表,这时我们调用epoll_wait,会把准备就绪的fd拷贝到用户态内存,然后清空准备就绪list链表。

  最后,epoll_wait检查这些fd,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪rdlist链表了。

所以,非ET的句柄,只要它上面还有事件,epoll_ wait每次都会返回。而ET模式的句柄,除非有新中断到,即使fd上的事件没有处理完,也是不会次次从epoll_wait返回的

4能处理EPOLLONESHOT事件

当我们使用ET模式,一个socket上的某个事件还是可能被触发多次,这在并发程序中就会引起一个问题。

比如在线程池或者进程池模型中,某一个线程或进程正在处理一个有事件的socket文件描述符时,在这个时候这个socket文件描述符又有新的数据可以读取,此时另外一个线程被唤醒来读取这些新的数据,于是就出现了连个线程或者进程同时操作一个文件描述符的情况,这个时候就容易发生错误因为某个线程调度的时间和顺序是不能确定的,很有可能一个线程或者进程已经把数据读取完成,而且另外的一个进程或者线程还在读取这个文件描述符。

    这是我们所不希望的,我们希望在任意时刻都只有一个线程或者进程在操作一个文件描述符,关于这一点要求epoll提供了一个叫做EPOLLONESHOT事件的实现。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其注册的一个可读可写或者异常事件,且只触发一次。除非我们使用epoll_ctl()函数重置该文件描述符上注册的EPOLLONESHOT事件。

反过来思考的话,注册EPOLLONESHOT事件的文件描述符一旦被某个线程或者进程处理完成后,该线程或进程就应该立即重置这个sock文件描述符上的EPOLLONESHOT事件,以确保这个sockfd文件描述符下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个sockfd文件描述符.。

图片中是剖析内核中整个EPOLL源码的过程,强烈推介的技术干货,剖析完就觉得自己的编程能力实在是渣的不行,也会学习和收获到前辈的璀璨思想,不可言传只可意会。




Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326842455&siteId=291194637