IO多路复用之epoll总结
1、基本知识
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。 此外,内核使用了slab机制,为epoll提供了快速的数据结构:在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。 epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。
如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。
1.执行epoll_create时,创建了红黑树和就绪list链表。
2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。
#include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /*epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示: EPOLL_CTL_ADD:注册新的fd到epfd中; EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd; 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件:struct epoll_event结构如下struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }/* ;events可以是以下几个宏的集合: EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 */ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); /* 等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size, 参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。*/
3、工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
4、数据结构分析:
/* //*************************************************************************************************** //分隔符 //fs/eventpoll.c //epoll有三级锁 // 1) epmutex (mutex) // 2) ep->mtx (mutex) // 3) ep->wq.lock (spinlock) //获取顺序是上面的从1到3 //我们需要一个ep->wq.lock(spinlock)自旋锁,因为我们从poll回调内部 //操作对象,这可能是wake_up()触发的,而wake_up()又可能从中断请求上下文中调用。 //所以我们无法在poll回调中sleep,因此我们需要一个自旋锁。必须要非常小心使用的锁, // 尤其是调用spin_lock_irqsave()的时候, 中断关闭, 不会发生进程调度, // 被保护的资源其它CPU也无法访问。 这个锁是很强力的, 所以只能锁一些 // 非常轻量级的操作。 //在事件传输循环期间(从内核空间到用户空间),我们可能因为copy_to_user()而终于sleep, //所以,我们需要一个允许我们sleep的锁。这个锁是mutex(ep->mtx)。它是 //在epoll_ctl(EPOLL_CTL_DEL)期间的事件传输循环期间和eventpoll_release_file()的事件传输循环期间获取。 //eventpoll_release_file是用来清除已经close的fd却还没DEL的(当一个fd被加入到epoll中,然后close(fd),而没有先调用epoll_ctl(EPOLL_CTL_DEL)时) //然后我们还需要一个全局互斥锁来序列化eventpoll_release_file()和ep_free()。 //ep_free在epollfd被close时调用来清理资源 //https://www.cnblogs.com/l2017/ //这个互斥锁是在epoll文件清理路径中由ep_free()获取的,它也可以被eventpoll_release_file()获取, //将epoll fd插入另一个epoll中也会获得。 //我们这样做是为了让我们遍历epoll树时确保这个插入不会创建epoll文件描述符的闭环, //这可能导致死锁。我们需要一个全局互斥锁来防止两个fd同时插入(A插到B和B插到A) //来竞争和构建一个循环,而不需要插入观察它是去。 //当一个epoll fd被添加到另一个epoll fd时,有必要立即获得多个“ep-> mtx”。(最多4层嵌套) //在这种情况下,我们总是按嵌套顺序获取锁(即在* epoll_ctl(e1,EPOLL_CTL_ADD,e2)之后,e1->mtx将始终在e2->mtx之前获取)。 //由于我们不允许epoll文件描述符的循环,这可以确保互斥锁是有序的。 //为了将这个嵌套传递给lockdep,当走遍epoll文件描述符的树时,我们使用当前的递归深度作为lockdep子键。 //可以删除“ep-> mtx”并使用全局mutex“epmutex”(与“ep-> wq.lock”一起)使其工作, //但是“ep-> mtx”将使界面更具可扩展性。 需要持有“epmutex”的事件非常罕见, //而对于正常操作,epoll私有“ep-> mtx”将保证更好的可扩展性。 //介绍RCU //后面有些变量的命名带有rcu //RCU(Read - Copy Update),顾名思义就是读 - 拷贝修改,它是基于其原理命名的。 //对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它, //但写者在访问它时首先拷贝一个副本,然后对副本进行修改, //最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。 //这个时机就是所有引用该数据的CPU都退出对共享数据的操作。 //用于释放内存 //介绍poll机制 //在用户空间应用程序向驱动程序请求数据时,有以下几种方式: //1、不断查询,条件不满足的情况下就是死循环,非常耗cpu //2、休眠唤醒的方式,如果条件不满足,应用程序则一直睡眠下去 //3、poll机制,如果条件不满足,休眠指定的时间,休眠时间内条件满足唤醒,条件一直不满足时间到达自动唤醒 //4、异步通知,应用程序注册信号处理函数,驱动程序发信号。类似于QT的信号与槽机制。 //在深入了解epoll的实现之前, 先来了解内核的3个方面. //https://www.cnblogs.com/watson/p/3543320.html // 1. 等待队列 waitqueue // 我们简单解释一下等待队列: // 队列头(wait_queue_head_t)往往是资源生产者, // 队列成员(wait_queue_t)往往是资源消费者, // 当头的资源ready后, 会逐个执行每个成员指定的回调函数, // 来通知它们资源已经ready了, 等待队列大致就这个意思. // 2. 内核的poll机制 // 被Poll的fd, 必须在实现上支持内核的Poll技术, // 比如fd是某个字符设备,或者是个socket, 它必须实现 // file_operations中的poll操作, 给自己分配有一个等待队列头. // 主动poll fd的某个进程必须分配一个等待队列成员, 添加到 // fd的对待队列里面去, 并指定资源ready时的回调函数. // 用socket做例子, 它必须有实现一个poll操作, 这个Poll是 // 发起轮询的代码必须主动调用的, 该函数中必须调用poll_wait(), // poll_wait会将发起者作为等待队列成员加入到socket的等待队列中去. // 这样socket发生状态变化时可以通过队列头逐个通知所有关心它的进程. // 这一点必须很清楚的理解, 否则会想不明白epoll是如何 // 得知fd的状态发生变化的. // 3. epollfd本身也是个fd, 所以它本身也可以被epoll, // (最多4层嵌套)EP_MAX_NESTS //建议先了解内核的list数据结构以及,不然很多list相关的东西看起来会很奇怪 #define EP_PRIVATE_BITS (EPOLLWAKEUP | EPOLLONESHOT | EPOLLET | EPOLLEXCLUSIVE) //如果设置了EPOLLONESHOT标志位,则设置epi->event.events &= EP_PRIVATE_BITS, //后续根据EP_PRIVATE_BITS判断不再加入ep->rdllist或者ep->ovflist。 #define EPOLLINOUT_BITS (EPOLLIN | EPOLLOUT) #define EPOLLEXCLUSIVE_OK_BITS (EPOLLINOUT_BITS | EPOLLERR | EPOLLHUP | EPOLLWAKEUP | EPOLLET | EPOLLEXCLUSIVE) #define EP_MAX_NESTS 4//指最多4层epoll嵌套 #define EP_MAX_EVENTS (INT_MAX / sizeof(struct epoll_event)) #define EP_UNACTIVE_PTR ((void *) -1L) #define EP_ITEM_COST (sizeof(struct epitem) + sizeof(struct eppoll_entry)) //记录file跟fd struct epoll_filefd { struct file *file; int fd; }; //用于跟踪可能的嵌套调用的结构,用于过深的递归和循环周期,epoll嵌套 struct nested_call_node { struct list_head llink; void *cookie; void *ctx; }; //此结构用作嵌套调用的收集器,以进行检查最大递归部分和循环周期,epoll嵌套 struct nested_calls { struct list_head tasks_call_list; spinlock_t lock; }; //添加到eventpoll接口的每个文件描述符都有一个链接到“rbr”RB树的epitem结构。 //避免增加此结构的大小,因为服务器上可能有数千个这样的结构,我们不希望存在多个缓存行 //epitem 表示一个被监听的fd struct epitem { union { struct rb_node rbn; //rb_node, 当使用epoll_ctl()将多个fds加入到某个epollfd时, 内核会用slab分配 //多个epitem与fds对应, 而且它们以红黑树的形式组织起来, //tree的root保存在eventpoll rbr中. struct rcu_head rcu; //用于释放epitem结构,rcu前面有提到 }; struct list_head rdllink;//事件就绪队列 //对应eventpoll的rdllist //当epitem对应的fd的存在已经ready的I/O事件, //则ep_poll_callback回调函数会将该epitem链接到eventpoll中的rdllist循环链表中去, struct epitem *next;//用于主结构体中的链表 //对应eventpoll的ovflist上 //ovflist是一个临时的就绪单链表。充当单链表的next struct epoll_filefd ffd;//这个结构体对应的被监听的文件描述符信息 //epitem对应的fd和struct file /* Number of active wait queue attached to poll operations */ int nwait; //poll操作中事件的个数 //附加到poll轮询中的等待队列(eppoll_entry)个数 //pwqlist里的个数 /* List containing poll wait queues */ struct list_head pwqlist;//双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table //对应eppoll_entry的llink //双向链表,保存着被监视文件对应的eppoll_entry(等同等待队列), //按道理应该是一个文件就只有一个等待队列。 //但poll某些文件的时候,需要添加两次等待队列,如/dev/bsg/目录下面的文件。所有不是一个指针而是链表 // 同一个文件上可能会监视多种事件, // 这些事件可能属于不同的wait_queue中 // (取决于对应文件类型的实现), // 所以需要使用链表 struct eventpoll *ep;//该项属于哪个主结构体(多个epitm从属于一个eventpoll) //当前epitem属于哪个eventpoll //双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点 struct list_head fllink; //对应目标file的f_ep_links //双向链表,用来链接被监视的file。 //被监控的file里有f_ep_link(list头),用来链接所有监视这个文件的epitem(list节点)结构,即把监听该file的epitem串起来 //把一个file加到了两个epoll中(file 的 f_ep_links链表中会有两个epitem的fllink) /* wakeup_source used when EPOLLWAKEUP is set */ struct wakeup_source __rcu* ws; //????不知道什么时候加的 不知道 //注册的感兴趣的事件,也就是用户空间的epoll_event struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event,这个数据是调用epoll_ctl时从用户态传递过来 }; /* * This structure is stored inside the "private_data" member of the file * structure and represents the main data structure for the eventpoll * interface. * * Access to it is protected by the lock inside wq. */ //这个结构存储在file->private_data。是每个epoll fd(epfd)对应的主要数据结构 //eventpoll在epoll_create时创建。 struct eventpoll { /* * This mutex is used to ensure that files are not removed * while epoll is using them. This is held during the event * collection loop, the file cleanup path, the epoll file exit * code and the ctl operations. */ struct mutex mtx;//对本数据结构的访问,防止使用时被删除 //防止这个结构在使用时被删除 //添加, 修改或者删除监听fd的时候, 以及epoll_wait返回, 向用户空间传递数据时都会持有这个互斥锁, //所以在用户空间可以放心的在多个线程中同时执行epoll相关的操作, 内核级已经做了保护. wait_queue_head_t wq;//sys_epoll_wait() 使用的等待队列 //sys_epoll_wait()使用的等待队列,用于保存有哪些进程在等待这个epoll返回。 /* Wait queue used by file->poll() */ wait_queue_head_t poll_wait; //file->poll()使用的等待队列 //file->poll()使用的等待队列,这个用于epollfd本身被poll的时候 struct list_head rdllist; //事件满足条件的链表 //对应epitem的rdllist //用于收集已经就绪了的epitem的对象(有个函数可用于结构内成员变量的地址得到该结构指针,后面会看到) //链表中的每个结点即为epitem中的rdllink,rdllist中链接的所有rdllink对应的epitem有事件ready struct rb_root_cached rbr;//用于管理所有fd的红黑树(树根) //用于管理所有epitem(fd)的红黑树(树根) //查找删除更改都是log(N) struct epitem *ovflist; //对应epitem的next //ovflist链表也是用来收集就绪了item对象的,epitem的next就是这个地方用来串成单链表的 //是在对rdllink成员进行扫描操作获取就绪事件返还给用户态时被用来存放扫描期间就绪的事件的。 //因为在对rellist扫描期间需要保证数据的一致性,如果此时又有新的就绪事件发生,那么就需要提供临时的空间来存储 /* wakeup_source used when ep_scan_ready_list is running */ struct wakeup_source *ws; //?? struct user_struct *user; //这里保存了一些用户变量, 比如fd监听数量的最大值等等 struct file *file; //存放对应的file,在create中被创建的那个 /* used to optimize loop detection check */ int visited; //?? struct list_head visited_list_link; //?? }; /* Wait structure used by the poll hooks */ //完成一个epitem和ep_poll_callback的关联,同时eppoll_entry会被插入目标文件file的等待头队列中 //在ep_ptable_queue_proc函数中,引入了另外一个非常重要的数据结构eppoll_entry。 //eppoll_entry主要完成epitem和epitem事件发生时的callback(ep_poll_callback)函数之 //间的关联。首先将eppoll_entry的whead指向fd的设备等待队列(waitlist), //然后初始化eppoll_entry的base变量指向epitem,最后通过add_wait_queue将epo //ll_entry(wait)挂载到fd的设备等待队列上(waitlist)(把eppoll_entry添加到sk->sk_wq->wait的头部)。 //完成这个动作后,epoll_entry已经被挂载到fd的设备等待队列。 //然后还有一个动作必须完成,就是通过pwq->llink将eppoll_entry挂载到epitem的pwqlist尾部。 struct eppoll_entry { /* List header used to link this structure to the "struct epitem" */ struct list_head llink; //对应epitem的pwqlist //把这个结构跟epitem连接起来,挂载到epitem的pwqlist尾部。 /* The "base" pointer is set to the container "struct epitem" */ struct epitem *base; //指向epitem /* * Wait queue item that will be linked to the target file wait * queue head. */ wait_queue_entry_t wait; //挂载到fd的设备等待队列上,后面会指定跟ep_poll_callback绑定起来。为唤醒时的回调函数 /* The wait queue head that linked the "wait" wait queue item */ wait_queue_head_t *whead; //指向fd的设备等待队列头,用于将wait挂载到这个设备等待队列上面。 }; */
* Flags for epoll_create1. */ #define EPOLL_CLOEXEC O_CLOEXEC //EPOLL_NONBLOCK 它是fd的一个标识说明,用来设置文件close-on-exec状态的。 //当close-on-exec状态为0时,调用exec时,fd不会被关闭; //状态非零时则会被关闭,这样做可以防止fd泄露给执行exec后的进程。 /* Valid opcodes to issue to sys_epoll_ctl() */ #define EPOLL_CTL_ADD 1 #define EPOLL_CTL_DEL 2 #define EPOLL_CTL_MOD 3 /* Epoll event masks */ #define EPOLLIN 0x00000001 //表示关联的fd可以进行读操作了。(包括对端Socket正常关闭) #define EPOLLPRI 0x00000002 //表示关联的fd有紧急优先事件可以进行读操作了。 #define EPOLLOUT 0x00000004//表示关联的fd可以进行写操作了。 #define EPOLLERR 0x00000008 //表示关联的fd发生了错误, //这个事件是默认的 后续代码有 epds.events |= EPOLLERR | EPOLLHUP; #define EPOLLHUP 0x00000010 //表示关联的fd挂起了, //这个事件是默认的 后续代码有 epds.events |= EPOLLERR | EPOLLHUP; #define EPOLLRDNORM 0x00000040 #define EPOLLRDBAND 0x00000080 #define EPOLLWRNORM 0x00000100 #define EPOLLWRBAND 0x00000200 #define EPOLLMSG 0x00000400 #define EPOLLRDHUP 0x00002000//表示套接字关闭了连接,或者关闭了正写一半的连接。 /* Set exclusive wakeup mode for the target file descriptor */ #define EPOLLEXCLUSIVE (1U << 28) /* * Request the handling of system wakeup events so as to prevent system suspends * from happening while those events are being processed. * * Assuming neither EPOLLET nor EPOLLONESHOT is set, system suspends will not be * re-allowed until epoll_wait is called again after consuming the wakeup * event(s). * * Requires CAP_BLOCK_SUSPEND */ #define EPOLLWAKEUP (1U << 29) //设置关联的fd为one-shot的工作方式。 //表示只监听一次事件,如果要再次监听,需要重置这个socket上的EPOLLONESHOT事件。 //使用场合:多线程环境 //如果主线程在epoll_wait返回了套接字conn,之后子线程1在处理conn,主线程回到epoll_wait, //但还没等到子线程1返回conn又可读了,此时主线程epoll_wait返回,又分配给另一个线程,此时两个线程同时使用一个套接字,这当然是不行的, //出现了两个线程同时操作一个socket的局面。 //可以使用epoll的EPOLLONESHOT事件实现一个socket连接在任一时刻都被一个线程处理。 //作用: // 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多出发其上注册的一个可读,可写或异常事件,且只能触发一次。 //使用: // 注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕, // 该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时, // (使用该线程的没重置此套接字前即:主线程不允许返回任何关于此套接字的事件,这样就做到同一时刻只可能有一个线程处理该套接字) // 其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个sockt。 //效果: // 尽管一个socket在不同事件可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。 //也可以使用add,并忽略epoll_ctl()返回的错误码EEXIST来重置。(??还没试过) //EPOLLONESHOT优先于水平触发(默认)的处理,即同时设置水平触发和EPOLLONESHOT并不会把epi添加到ready链表。 //如果设置了EPOLLONESHOT标志位,则设置epi->event.events &= EP_PRIVATE_BITS, //其定义如下#define EP_PRIVATE_BITS (EPOLLWAKEUP | EPOLLONESHOT | EPOLLET), //后续根据EP_PRIVATE_BITS判断不再加入ep->rdllist或者ep->ovflist。 //注意设置了EPOLLONESHOT触发一次后并没有删除epi, //因而通过epoll_ctl进行ADD操作后会提示File exists错误 /* Set the One Shot behaviour for the target file descriptor */ #define EPOLLONESHOT (1U << 30) /* Set the Edge Triggered behaviour for the target file descriptor //设置关联的fd为ET的工作方式,epoll的默认工作方式是LT。(LT/ET触发)LT水平触发 ET边缘触发*/ #define EPOLLET (1U << 31) //LT模式是epoll默认的工作方式 //LT模式状态时,主线程正在epoll_wait等待事件时,请求到了,epoll_wait返回后没有去处理请求(recv), //那么下次epoll_wait时此请求还是会返回(立刻返回了); //而ET模式状态下,这次没处理,下次epoll_wait时将不返回(所以我们应该每次一定要处理) //本质的区别在设置了EPOLLET的fd在wait发送到用户空间之后,会重新挂回到就绪队列中。 //等待下次wait返回(会重新查看每个socket是否真的有数据,并不是挂上去就绪队列了就返回) //可查找这段代码if (!(epi->event.events & EPOLLET)) 仔细研读 看清楚有个!
//epoll三种锁/互斥量 // 1) epmutex (mutex) // 2) ep->mtx (mutex) // 3) ep->wq.lock (spinlock) //获取顺序是上面的从1到3 //我们需要一个ep->wq.lock(spinlock)自旋锁,因为我们从poll回调内部 //操作对象,这可能是wake_up()触发的,而wake_up()又可能从中断请求上下文中调用。 //所以我们无法在poll回调中sleep,因此我们需要一个自旋锁。必须要非常小心使用的锁, // 尤其是调用spin_lock_irqsave()的时候, 中断关闭, 不会发生进程调度, // 被保护的资源其它CPU也无法访问。 这个锁是很强力的, 所以只能锁一些 // 非常轻量级的操作。 //在事件传输循环期间(从内核空间到用户空间),我们可能因为copy_to_user()而终于sleep, //所以,我们需要一个允许我们sleep的锁。这个锁是mutex(ep->mtx)。它是 //在epoll_ctl(EPOLL_CTL_DEL)期间的事件传输循环期间和eventpoll_release_file()的事件传输循环期间获取。 //eventpoll_release_file是用来清除已经close的fd却还没DEL的(当一个fd被加入到epoll中,然后close(fd),而没有先调用epoll_ctl(EPOLL_CTL_DEL)时) //然后我们还需要一个全局互斥锁来序列化eventpoll_release_file()和ep_free()。 //ep_free在epollfd被close时调用来清理资源 //https://www.cnblogs.com/l2017/ //这个互斥锁是在epoll文件清理路径中由ep_free()获取的,它也可以被eventpoll_release_file()获取, //将epoll fd插入另一个epoll中也会获得。 //我们这样做是为了让我们遍历epoll树时确保这个插入不会创建epoll文件描述符的闭环, //这可能导致死锁。我们需要一个全局互斥锁来防止两个fd同时插入(A插到B和B插到A) //来竞争和构建一个循环,而不需要插入观察它是去。 //当一个epoll fd被添加到另一个epoll fd时,有必要立即获得多个“ep-> mtx”。(最多4层嵌套) //在这种情况下,我们总是按嵌套顺序获取锁(即在* epoll_ctl(e1,EPOLL_CTL_ADD,e2)之后,e1->mtx将始终在e2->mtx之前获取)。 //由于我们不允许epoll文件描述符的循环,这可以确保互斥锁是有序的。 //为了将这个嵌套传递给lockdep,当走遍epoll文件描述符的树时,我们使用当前的递归深度作为lockdep子键。 //可以删除“ep-> mtx”并使用全局mutex“epmutex”(与“ep-> wq.lock”一起)使其工作, //但是“ep-> mtx”将使界面更具可扩展性。 需要持有“epmutex”的事件非常罕见, //而对于正常操作,epoll私有“ep-> mtx”将保证更好的可扩展性。 //介绍RCU //后面有些变量的命名带有rcu //RCU(Read - Copy Update),顾名思义就是读 - 拷贝修改,它是基于其原理命名的。 //对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它, //但写者在访问它时首先拷贝一个副本,然后对副本进行修改, //最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。 //这个时机就是所有引用该数据的CPU都退出对共享数据的操作。 //用于释放内存 //介绍poll机制 //在用户空间应用程序向驱动程序请求数据时,有以下几种方式: //1、不断查询,条件不满足的情况下就是死循环,非常耗cpu //2、休眠唤醒的方式,如果条件不满足,应用程序则一直睡眠下去 //3、poll机制,如果条件不满足,休眠指定的时间,休眠时间内条件满足唤醒,条件一直不满足时间到达自动唤醒 //4、异步通知,应用程序注册信号处理函数,驱动程序发信号。类似于QT的信号与槽机制。 //在深入了解epoll的实现之前, 先来了解内核的3个方面. //https://www.cnblogs.com/watson/p/3543320.html // 1. 等待队列 waitqueue // 我们简单解释一下等待队列: // 队列头(wait_queue_head_t)往往是资源生产者, // 队列成员(wait_queue_t)往往是资源消费者, // 当头的资源ready后, 会逐个执行每个成员指定的回调函数, // 来通知它们资源已经ready了, 等待队列大致就这个意思. // 2. 内核的poll机制 // 被Poll的fd, 必须在实现上支持内核的Poll技术, // 比如fd是某个字符设备,或者是个socket, 它必须实现 // file_operations中的poll操作, 给自己分配有一个等待队列头. // 主动poll fd的某个进程必须分配一个等待队列成员, 添加到 // fd的对待队列里面去, 并指定资源ready时的回调函数. // 用socket做例子, 它必须有实现一个poll操作, 这个Poll是 // 发起轮询的代码必须主动调用的, 该函数中必须调用poll_wait(), // poll_wait会将发起者作为等待队列成员加入到socket的等待队列中去. // 这样socket发生状态变化时可以通过队列头逐个通知所有关心它的进程. // 这一点必须很清楚的理解, 否则会想不明白epoll是如何 // 得知fd的状态发生变化的. // 3. epollfd本身也是个fd, 所以它本身也可以被epoll, // (最多4层嵌套)EP_MAX_NESTS //建议先了解内核的list数据结构以及,不然很多list相关的东西看起来会很奇怪 #define EP_PRIVATE_BITS (EPOLLWAKEUP | EPOLLONESHOT | EPOLLET | EPOLLEXCLUSIVE) //如果设置了EPOLLONESHOT标志位,则设置epi->event.events &= EP_PRIVATE_BITS, //后续根据EP_PRIVATE_BITS判断不再加入ep->rdllist或者ep->ovflist。 #define EPOLLINOUT_BITS (EPOLLIN | EPOLLOUT)
参考:https://www.cnblogs.com/l2017/p/10830391.html
http://blog.chinaunix.net/uid-28541347-id-4238524.html
https://www.cnblogs.com/apprentice89/archive/2013/08/03/3234677.html
https://www.imooc.com/article/40708