网络编程之epoll理解

来源:互联网 发布:右下角激活windows去除 编辑:程序博客网 时间:2024/06/10 00:04

1.文件描述符 .

linux的内核将所有外部设备都看作一个文件操作, 对一个文件的读写操作会调用内核提供的系统命令, 返回一个file descriptor (fd, 文件描述符) . 而对socket的读写也会有相应的描述符 . 称为socketfd (socket描述符) . 描述符就是一个数字, 它指向内核的一个结构体 (文件路径, 数据区的一些属性) .
每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。
1. 进程级的文件描述符表
2. 系统级的打开文件描述符表
3. 文件系统的i-node表
这里写图片描述


2.epoll详解 .

(1) select() 和 poll() IO多路复用模型 .
select的缺点 :
<1> 单个进程能够监视的文件描述符存在最大限制, 通常是1024, 当然可以更改数量, 但是由于select采用轮训方式扫描文件描述符, 文件描述符越多, 性能越差;
<2> 内核 / 用户空间内存拷贝的问题, select需要赋值大量的句柄数据结构, 产生巨大开销;
<3> select返回的是含有整个句柄的数组, 应用程序需要遍历整个数组才能发现哪些句柄发生事件 .
<4> select触发方式是水平出发, 应用程序如果没有完成一个就绪的文件描述符进行IO操作, 那么之后每次select调用还是会将这些文件描述符通知进程 .
相比select模型, poll使用链表保存文件描述符, 因此没有监视文件数量限制, 但是其他三个缺点还是存在 .
假设我们服务器需要支持100万的并发连接, 则在1024情况下, 我们至少需要开辟1K个进程才能实现100万的并发连接 . 除了进程间上下文切换时间消耗, 从内核/用户空间大量的内存拷贝, 数组轮询, 是系统难以承受 . 因此基于select模型的服务器程序, 要达到10万级别的并发访问, 是很难完成的 .
因此epoll出厂了 .

(2) epoll实现多路复用的机制 .
设想一个如下场景, 有100万个客户端同事与一个服务器进程保持TCP连接 . 而每一时刻, 通常只有几百个上千个TCP连接是活跃的, 如何实现这样的高并发 .
在select/poll时代, 服务器进程每次将100万个连接告诉操作系统 (从用户态复制句柄数据结构到内核态), 让操作系统检查这些套接字是否有事件发生, 轮训完成后, 再将句柄数据结构复制到用户态, 让服务器应用程序轮训已经发生的网络事件, 这一过程资源消耗很大, 因此select/poll一般只能处理几千并发连接 .
epoll通过在linux内核申请一个简易的文件系统, 把原先select/poll调用分为3部分 .
<1> 调用epoll_create() 建立一个epoll对象 (在epoll文件系统中为这个句柄对象分配资源) .
<2> 调用epoll_ctl 向 epoll对象中添加这100万个连接的套接字 .
<3> 调用epoll_wait收集发生的事件的连接 .
要实现上面的场景只需要在进程启动时建立一个epoll对象, 然后在需要时候向这个epoll对象中添加或者删除连接, 同时, epoll_wait的效率非常高, 因为调用epoll_wait时, 并没有一股脑箱操作系统赋值这100万连接的句柄数据, 内核也不需要遍历全部连接 .

下面看看linux内核具体的epoll实现思路 :
当某一进程调用epoll_create方法时, linux内核会创建一个eventpoll结构体, 这个结构体会有两个成员和epoll使用方式密切相关 :
这里写图片描述
每一个epoll对象都会有一个独立的eventpoll结构体, 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件, 这些事件都会挂载红黑树中因此重复添加的事件会通过红黑树而高效的识别出来(红黑树插入时间效率是lgn) .
而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系, 也就是说, 当相应的事件发生时会调用这个回调方法, 这个回调方法在内核叫做ep_poll_callback, 会将发生的事件添加到rdlist双链表中 .
epoll中, 对于每一个事件, 都会建立一个epitem结构体 :
这里写图片描述
当调用epoll_wait检查是否有事件发生时, 只需要检查eventpoll对象中的rdlist双链表是否有epitem元素即可, 如果rdlist不为空, 就把发生事件复制到用户态, 同时将事件数量返回给用户 .
这里写图片描述

总结 :
从上面讲解可知 : 通过红黑树和双链表数据结构, 并结合回调机制, 造就了epoll高效 .
我们就很容易理解epoll用法了 :
<1> epoll_create() 系统调用, 由调用返回一个句柄, 之后所有使用都依靠这个句柄标示 .
<2> epoll_ctl系统调用 . 通过此调用向epoll对象中添加, 删除, 修改感兴趣的事件, 返回0表示标示成功, 返回-1标示失败 .
<3> epoll_wait系统调用, 通过此调用收集在epoll监控中已经发生的事件 .


3.我是如何理解这几种方式的 .

最近A餐馆开业了, 老板雇了一批服务员来进行餐馆服务 . 刚开始来一位客人就会派出一个服务员进行伺候 , 这名服务员就一直在客人旁边等候客人点菜, 而不能去做别的事 . 饭店刚开始这种模式还可以维持, 但是随着慢慢壮大, 客人越来越多 . 老板发现来一个客人就派出一名服务员就一直伺候这种方式不行, 服务员显然是不够用的 . (这种方式就是同步阻塞IO模型, 一个线程负责一个连接请求, 如果这个连接请求没有IO请求, 那么这个线程就一直阻塞着) .
后来老板咨询同行, 采用下面的方式 : 老板专门设立一个角色A和一个角色B, A在客人进入餐厅后, 会记录客人坐下的座位号 . 记录在一个小本子上 . 这时候只要有客人喊”服务员, 我们要点菜” . 这时候角色A就会喊角色B过来”把所有座位号抄下来, 去问问哪位客人需要服务” . 这时候B就会按着本子上记得桌号, 一个个问客人”请问是您需要服务了么?”, 当找到需要服务的客人这时候角色B才会叫一名服务员过来为这个客人进行服务 . 老板刚开始感觉这种方式真不错, 节省好多人力 . 但是有一天角色A说”老板, 你给的本子每次只能记录1024个桌号, 再多就记不了了, 而且每次让角色B去一桌桌问客人, 可把他累坏了, 几百桌一个个人, 一般人谁能受得了, 我看您还是另请高明吧” 于是角色A和B辞职不干了. ( 这种方式是IO多路复用的select机制, 这里的”座位号”相当于文件描述符fd, 标示一个socket/文件. 角色A就是select模型, 角色A将桌号抄到角色B本子上就是将文件描述符从内核态拷贝到用户态. 当然角色B就是应用系统, 需要遍历fd, 查看是否有IO请求) .
老板顿时懵逼了, 这可咋办 . 于是他去了世界上先进的餐厅, 并从他们那学习并引进了先进的机制 : 这里还是有角色A和角色B, 但是此时角色A面前放了一个软件系统C . 这个系统有两个界面1和2 . 每当有客人进来都会将客人的桌号记录在界面1中, 而且这个系统和每一个餐桌都联网了, 桌上有一个按钮, 当客人需要服务的时候只需要按下那个按钮, 系统C中界面1就会将需要服务的桌号添加到界面2中. 角色A会不时看界面2中是不是有新添的桌号 . 如果发现有, 就会把界面2中的桌号抄到一个小本子上交给角色B . 此时角色B不需要一桌桌问客人是否需要服务, 因为本子上记得都是需要服务的客人桌号 . 只需要叫几个服务员按着本子上桌号一个个服务客人即可 . 老板发现这种方式太棒了, 员工也没有吐槽, 自己的生意也一天天红火, 最后老板将这种模式介绍给其他人, 所以之后所有餐饮界都流行这种方式 .
(这种方式是多路复用中的epoll模式, 角色A和系统C组成epoll机制, 系统C中的界面1就是epoll中的红黑树结构记录所有的fd, 界面2就是epoll中的链表结构, 记录有IO请求的epitem是一个结构体包含fd . 而角色A只需要监视着界面2中是否有新的epitem出现, 如果有就将这些epitem从内核态拷贝到用户态, 这里的拷贝只有发生IO请求的一部分 . 还有每个餐桌上的按钮相当于设备(网卡) , 这些按钮和系统C连接相当于回调, 当有IO请求就会触发设备和网卡进而触发回调, 同理当有客人有需求时候就会点击按钮, 当哪个餐桌按钮响了就会通知系统C将这个对应桌号信息和其他信息, 放到界面2中 . 所以回调这种方式很重要, 造就了epoll的强大)


参考 :
http://www.open-open.com/lib/view/open1410403215664.html
书籍 : netty权威指南

原创粉丝点击