wakeup callback机制
Linux通过socket的睡眠队列(sleep_list)来管理所有等待socket的某个事件的进程(task), select、poll、epoll_wait 函数操作会陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前task构建一个wait_entry节点,然后插入到每个socket的sleep_list里,直到超时或事件发生,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的task,通知task相关事件发生,每一个sleep_list上的wait_entry都拥有一个callback,wakeup逻辑在唤醒睡眠队列时,会遍历该队列链表上的每一个wait_entry,直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止,调用每一个wait_entry的callback,并将当前task的wait_entry节点从socket的sleep_list中删除。
要高效的处理网络IO数据,不可能为每个socket 创建一个进程task,进程创建是一种高昂的性能损耗,所以采用一个task来监控多个socket,但这一个task不可能去阻塞式的监控某一个socket的事件发生,我们应该block在关心的N个socket中一个或多个socket有数据可读的事件,意味着当block解除的时候,我们一定可以找到一个或多个socket上有可读的数据(至少一个可读),select将这个task放到每个 socket的sleep_list,等待任意一个socket可读事件发生而被唤醒,当task被唤醒的时候,其callback里面应该有个逻辑去检查具体哪些socket可读了。然后把这些事件反馈给用户程序,select为每个socket引入一个poll逻辑,该逻辑用于收集socket发生的事件。
当receive queue不为空的时候,我们就给这个socket的sk_event添加一个POLL_IN事件,用来表示当前这个socket可读。将来task遍历到这个socket,发现其sk_event包含POLL_IN的时候,就说明这个socket已是可读的。当用户task调用select的时候,select会将需要监控的readfds集合拷贝到内核空间,然后遍历自己监控的socket,挨个调用socket的poll逻辑以便检查该socket是否有可读事件。遍历完所有的socket后,如果没有任何一个sk_event可读,那么select会调用schedule,使得task进入睡眠。如果在timeout时间内某个socket上有数据可读了,或者等待timeout了,则调用select的task会被唤醒。唤醒后select就是遍历监控的socket集合,挨个收集可读事件并返回给用户了。
select模型带来的问题:
1.由于监控socket事件发生是在内核态,是一个比较底层的操作,所以每次select都需要将需要监控的文件描述符集合从用户态copy到内核态,内核并将ready的描述符集合再从内核态copy到用户态,如果socket很大,会有很大的上下文切换的损耗。
2.由于readfds是长度为32的整型数组,32*32=1024,bitmap机制来表示的fd最多可表示1024个,socket连接有上限
3.每次都是O(n)复杂度遍历所有socket收集有事件的socket。
4.每次都是O(n)复杂度(n是最大的fd值)遍历从内核态返回来的ready的fdset(文件描述符集合)
poll模型和select很像,区别如下:
1.poll不需要每次都重新构建需要监控的fd set,但还是会有引起上下文切换的内存copy
2.poll不需要像select那样需要用户计算fd的最大值+1,作为select函数的第一个参数
3.poll减少了fd的遍历,在select中监控的某socket所对应的fd值为1000,那么需要做1000次循环
4.poll 解除了select对于fd数量1024的限制
epoll
重点:epoll比poll的优点:支持水平触发(level-triggered)和边沿触发(edge-triggered)两种方案
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。
fds的变化频率并不高,所以每次都去做fds的遍历其实很没有比要。所以epoll模型引入了epoll_ctl函数和epoll_wait函数,epoll_wait用来处理高频率就绪的fd可读的fdset集合返回,高频epoll_wait的可读就绪的fd集合返回的拷贝问题,epoll通过内核与用户空间mmap同一块内存来解决。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。epoll_ctl处理需要增删查改的fds的,这里用红黑树来存储fd,以便快速查找和修改。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//fd注册 修改函数
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事
遍历就绪的fds集合
通过上面的socket的睡眠队列唤醒逻辑我们知道,socket唤醒睡眠在其睡眠队列的wait_entry的时候会调用wait_entry的回调函数callback,并且,我们可以在callback中做任何事情。为了做到只遍历就绪的fd,我们需要有个地方来组织那些已经就绪的fd。
为此,epoll引入了一个中间层,一个双向链表ready_list,一个单独的睡眠队列single_epoll_wait_list,并且,与select或poll不同的是,epoll的task不需要同时插入到多路复用的socket集合的所有睡眠队列中,相反task只是插入到中间层的epoll的单独睡眠队列中(即single_epoll_wait_list),task睡眠在epoll的单独队列上,等待事件的发生。同时,引入一个中间的wait_entry_sk,它与某个socket密切相关,wait_entry_sk睡眠在socket的睡眠队列上,其callback函数逻辑是将当前socket排入到epoll的ready_list中,并唤醒epoll的single_epoll_wait_list。而single_epoll_wait_list上睡眠的task的回调函数就明朗了:遍历ready_list上的所有socket,挨个调用socket的poll函数收集事件,然后唤醒task从epoll_wait返回。
select VS poll VS epoll:
1.epoll 减少了用户态和内核态间的内存copy
2.epoll有着高效的fd操作的红黑树结构
3.epoll基本没有fd数量限制
4.epoll每次只需遍历ready_list中就绪的socket即可