作者:LeanCloud 资深后端工程师 郭瑞

IO 模型

IO 模型相关内容首要参阅自:The Sockets Networking API:Unix Network Programming Volume1 第三版第六章,以下 IO 模型阐明图均复制自该书的 Oreilly Safari 版。

一般来说 IO 模型有如下这些:

  • blocking I/O
  • nonblocking I/O
  • I/O multiplexing (select and poll)
  • signal driven I/O (SIGIO)
  • asynchronous I/O (the POSIX aio_functions)

拿读数I 2 – u 6 S J据来说,首要包含的作业有:

  1. 等候数据抵达;
  2. 将抵达的数} 7 : , } ? c y j据复制到 kernel 的 buffer,再从 kernel buffer 复制到应用在| Z . = c User Space 的 buffer

Blocking IO

这儿为了简略用 UDP 做比方,然后履行 read 操作时分有数据就回来,没数据就等着,由于每个数据是完好的一块一块发来。TCP 的话 read 是否能回来还会有类似于 SO_RCVLOWAT 影响。

这儿首要看到 Blocking IO 是直到数据真的全复制至 UseC o 7 H p r Space 后才回来。

Non-Blocking IO

装备 Socket 为 Non-Bloc5 0 C kig z y 7 l 5 A t Zng 形式,o M w r ` y Q O之后不断去 kerny – ( E Uel 做 Polling,询问比方读操作是否完结,没完结则 read() 操作会回来 EWOUDBLOCK,需求过一会再来测验履行一次 read()。这种形式下会耗费许多 CPU。

IO Mul! Q S C p X c jtiplexing

之前( } l k N等候时刻首要! 4 M M A是耗费在等数据抵达上。IO Multiplexing 则是将等候数据到来和读取实践数据两个作业分开,好处是经过 select() 等 IO Mult ? h ! $ $ z S Otiplexing 的接口一次能够等候在多个 Socket 上。select() 回来后,处于 Ready 状况的 Socket 履行读操作时分也会堵塞,仅仅只堵塞将数据从 Kernel 复制到 User 的时刻。

相对于之前的 IO 模型来说 IO Multiplexing 实践做的作业没U d L改变乃至更低效,由于需求两次 System8 – 8 @ Ca5 L w E Nll 才完结一个读操作。但它的好处便是在或许耗时最长最不可控的等候数据抵达的时刻上,能够一口气等候多个 Socket,不必轮询耗费 CPU,在多线程形式下还能够让一个线程持续履行 select() 操作,用另一个线程池只履行不会堵k ^ – B D塞的,复制数据到 User Space 的作业。

实践上 IO Multiplexing 和 Blocking IO 是很像的。

Sign D v a D #al-Driven I/O

首先注册处理函数到 SIGIO 信号上,^ d V o在等候数据到来进程完毕后,体系触发 SIGIO 信号,之R H Q ^ D H A 1 1后能够在信号处理函数中履行读数据操作,再唤醒 Main Thread 或直接唤醒 Main Thread 让它去完结数据读取。整个进程没有一处是堵塞的。

看上去很好,但实践简直没什么人运用,为什么呢?这篇文章给出了一些原因,大致上是说在 TCP 下,衔接断开,衔接可读,衔接可写等等都会发生 Signal,而且在 Signal 上没有供给很好的方法去差异f * & V S w这些 SigV $ W ? |nal 到底为什么被触发。所以现在还在运用B i U V Signal DrT G V _ K A 1iven IO 的基本是 UDP 的。{ f y

Asynchronous I/O

AIO 看上去和 Signal Dris [ % 5 i } p f Nven IO 很类似,但差异在于 Signal Driver IO 是在数据可读后就经过 SIGIO 信号告诉应用程序数据可读了,之后由应用s . 7 1 v W V W a程序来实践读取数据,复制数据到 User Space。而 AIO 是注册一个读使命后,直到读使命真的彻底完结后才会告诉应用层。

这个 IO 模型看着很高档但也最杂乱,完结时分坑也最多。比方怎样去3 G / a – Canu * S A ~ h :cel 一个读使命。安置读使命时分一开端就需求传递应用层的 buffer,以及确定 buffer ? n ~ ? , V 巨细。之后 Kernel 会复制读取到的数据到% h p 9 X / v u这个 buffer。那读取进程中$ B T x Z q E 4这个应用层 buffer 假设改变了怎样样?比方变小了,被开释了。假设设置读g x R g U 7 8使命时分说读取 512 字节,但实践在复制数据进程中,有更多新数据到来了怎样办?正常来说这种状况下 AIO 是不能读更多数据的。不过 IO Multiplexing 能够。比方 select() 回来后,只表明 Socket 有数据可读,比方有 512 字节数据可h u ~ I ) 7 i ^读,但真履行读取时分假设有更多数据到来也是能读出来的。但 AIO 下或许用户态 buffer 是不可变的,那复制数V 7 H据时分假设有更多数据到来就只能下次再读了。

IO 模型比较

POSIX 对同步 IO 和异步 IO 的界说如下:

  • A synchronous IO operation causes the request3 h Ping process$ ] A g o z = 3 to be blocked until that IO operatix & ? I f Con complK Q * q *etesD – Z.
  • An a} x T ] w 7 Jsynchronous IO operation does not cause the ro $ 7 Iequesting process to be blocked.

所以按这个界说,上面除了 AIO 是异步 IO 外,其它满是同步 IO。Non-Blocking 称为 Non-Blocking 但它仍然是同步的。同步非堵塞。所以需求差异同步、异步、堵塞、非堵塞的概念。同步纷K o =歧定非要跟堵塞绑定,异步也纷歧定非要跟非M f G u ` l @ E堵塞绑定。

后续首要介绍 IO Multiplexing 相关内容。

IO; M y t F i 多路复用接口

上面 IO 模型里已经+ n b s介绍过 IO Multiplexing 含义t F W , N i,这儿记载一下完结 IO Multiplw P F O Cexing 的 API。

select

select 运用– k 3 A文档在:select(2) – Linux manud . ~ 0 g 7 f u 8al page

select 接口如下:

i% i 6ntselect(intnfds,fd_set*readfds,fd_set*writefds,
fd_set*exceptfds,structtimeval*timeout)
;

其间 nfds 是& ` c k X F y d readfds、writefds、exceptfds 中编号最大的那个文件描述符加一。readfds 是监听读 X T ( *操作的文件描述符列表,当被监听的文^ r W件描述符有能够不堵塞就读取的数据时 ( 读不出来数据也算,比方 end-of-file),select 会回来并将读安排妥当的描述符放在 readfds 指向的数组内。writefds 是监听写操作的文件描述符列表,当被监听的文件描述符中能能够不堵塞就写数据时(假设一口气写的数据太大实践! H C W / O H t s也会堵塞),select 会回来* x s q并将w w _ % # 1 m安排妥当的描述符放在 writefds 指向的数组内。exceptfds 是监听呈现反常的文件描述符列表,什么是反常需求看一下文档,与咱们一般了解7 t i Z e *的反常并不太相同。timeout 是 se = wlect 最大堵塞时刻长度,装备的最小时刻精度是毫秒。

select 回来条件:

  • 有文件描述符安排妥当,可读、可写或反常;
  • 线程被 interrupt;
  • timeout 到了

select 的问题:

  • 监听的文件描述符有上限 FD_SETSIx H ~ PZE,一般是 1024。由于 fd_set 是个 bitmax 3 6 p,它为最多 nfZ * 0 V / ( ( Tds 个描述符都用一个 bit 去表明是否监听,即便相应位置的描述符不需求监听在 fd_set 里也有它的 bit 存在。nfds 用于创立这个 bitmap 所以 fd_set 是有限巨细的。
  • 在用户侧,select 回来后它并不是只回来处于 ready 状况的描述符,而是会Z F F A C k ? _回来传入的一切的描述符列表集合,包含 ready 的和非 ready 的描述符,用户侧需求去遍历一切 readfds、writefds、exceptfds 去看哪个描述符是 ready 状Y Z K 8 B P _况,再做接下来的处理。还要整理这个 ready 状况,做完 IO 操作后再塞给 select 准备履行下q H ) 一轮 IO 操作
  • 在 Kernel 侧,select 履行后每次都要堕入内核遍历三个描述符集合数组4 ~ | / 6 X 6 R为文件描述符注册监听,即在描述N c T = [ 8 $符指向的 Socket 或文件等上面设置处理函数,然后在文件 readV u @ w 8 u j [y 时能调用处理函数。等有文件描述符 ready 后,在 select 回来退出之前,kernel 还需求再次遍历描述符集合,将设置的这些处理函数撤除Y z Z L j再回来
  • 有惊群问题。假定一个文件描述E d [ – v ` o – 3符 123 被多个进程或线程注册H & S 3 R u & V o在自己的 select 描述符集合内,当这个文件描述符 ready 后会将一切监听它的进程或线程悉数唤醒
  • 无法动态添加描述符,比方一个线程已经在履行 select 了,突然想写数据到某个新P : [描述符上,就只能等前一个 select 回来后从头设置 FD Set 从头履行 select

select 也有个长处,便是跨渠道更简略。完结这个接口的 OS 更多。

参阅:Select is# z e ( Z e F fundamentally broken

poll

运用文档在:poll(2) – Linux manual page

接口如下:

intpoll(structpollfd*fds,nfds_tnfds,inttimeout);

nfds 是 fds 数组的长度,s9 v ~ @ - E | 7truct pollfd 界说如下] S + 7 |

struct2 C h 5pollfd{
intfd;/*filedescriptor*/
shorteven2 Q z . u I # 6 Qts;/*requestedevents*/
shortrevenK - T zts;/*return3 B $ Y 5edevents*/
};

pol, t 2l 的回来条t t Q ` t @件与 selec| k i Y F * 2 a zt 相同。

看到 fdU p Z 5 Us 仍是关注的描述符列表,} # G N 9 _仅仅在 poll 里更先进一些,将 events 和 r6 ^ y A – 4 qeevents 分开了,所以假设关注的 events 没l V & N w 4 i 8有发生改变就能够重用 fds,poll 只修正 revenJ M T I D 6 n Rts 不会– w / ! } 6 Q Y l动 events。再有 fds 是个数组,不是 fds_set,没有了上限。

相对于 select 来说,poll 处理了 fds 长度上限问题,处理了监听描述符无法复用问题,但m : y B M j e m {仍然需求在 poll 回来后遍历 fds 去找 ready 的描述符,也需求整理 ready 描述符对应的 revents,Kernel 也同样是每次 poll 调用需求去遍历 fds 注册监听,poll 回来E E p u ` H y S时分撤除监听,也仍然有与 sele{ j a i A u w 9ct 相同的惊群问题,也有无法动态修正描述符的问题。

epoll& 2 H l

运用文档在:

  • epoll(7) – Linux manual page
  • epoll_create(2) – Linux manual page
  • epoll_ctl(2) – Linux manual page
  • epoll_wait(2) – Linux manual page

接口如下:

intepoll_create(intsize);
intepoll_ctl(intepfd,intop,( O ) U /intfd,structepoll_event*event);
intepoll_wait(intepfd,structepoll_e, Y uventi I 9 @*ez y X z P q ; 9 events,intmaxevents,inttimeout);

其间 s6 5 ) J o u Y 2 StrF s ( suct epoll_event 如下:

typedefunionepoll_data{
void*ptr;
intfd;
uint32_tu32;
uint64_tu64;
}epoll_data_t;
structepoll_event{
uint3* I P | W ? Q ( c2_tevee @ E I @ } z m Hnts;/*EpD t V : v {ollevents*/
epoll_data_tdata;/*UserdatavarV y 6 _ 8 A x 3 %iaC D h E s l wble*/
};

杂乱了许多。运用进程:

  1. epoll_create 创立 epoll 的描述符;
  2. epoll_ctl 将一个个需求监听的描述符以及监听的作业类型用 epoll_ctl 注册在 epoll 描述符上;
  3. 履行 epo= $ { } s Q 5 W -ll_wait 等着被监听的描述符 Ready,ex } % p X [poll_wait 回来后遍历 Ready 的$ E { 0 h描述符,依据 Ready 的作业类型处理作业
  4. 假设某个U K ) : c = * Z K被监听的描述符不再需求了,需求用 epollq 6 q_ctl 将它与 epoll 的描述符解绑
  5. 当 epoll 描述符不再需求时需求Q V M 9自动 close,像封闭一个文件相同开释资源

epoll(7) – Linux manua& h B 2 L Ul page 有运用示例。

epolR 4 B u ] 1l 长处:

  • 监听的描述符没有上限;
  • epoll_wait 每次只会回来 Ready 的描述符,不必完好遍历一切被监听的描述符M h W ) @ v R * !
  • 监听的描述符被注册到 epoll 后会与 epoll 的描述符绑定,维护在内核G O 2B v ; k c q ? C i不自动X ! k , O k ;经过 epoll_ctl 履行删去不会自动被整理,所以每次履行 epoll_wait 后用户侧不必从头装备监# I B ] y z –听,Kernel 侧J I $ee = -poll_wait 调用前后也不会重复注册和撤除描述符的监听;
  • 能够经过 eh d vpoll_ctl 动态增减监听的描述符,即便有另一个线程已经在履行 epoll_wait
  • epoll_ctl 在注册监听的时分还能传递自界说的 event_data,一般是传描述符,但应用能够= i 依据自己状况传其他;
  • 即便没线程等在 epoll_wait 上,Kernel 由于知道一切被监听的描述符,所以在这些描述符 Readh h r q ey 时分就能做处理,等下次有线程调用 epoll_wait 时分直接回来。这也协助 epoll 去完结 IO Edge Trig– m + d Qger,即 IO Ready 时分 Kernel 就符号描述符为 ReadP 3 2 Y o q C uy 之后在描述符被读空或写空前不再去监听它,后面胪陈;
  • 多个不同$ + Z ] t E V的线程能同时调用 epoll_wait 等在同一个 epoll 描述符上,有描述符 Ready 后它们就去履行;

epoll 缺陷:

  • epoll_ctl 是个体系调用,每次修正监听作业,添加监听描述符时分都是一次体系调用,而且没有批量操作的方法。比方一口气要监p G S U | x ,听一万个描述符,要把一万个描述– } Y | 9 a z l a符从监听! k ^ T g , 1读改到监听写等就会很耗时,很低效;
  • 对于服务器上许多连上又断开的衔接处理功率低,即 accept() 履行后生v ` ! W d j成一个新的描述符需求履行 epoll_ctl 去注册新 Socket 的监听,之后 epoll_wait 又是一次体系调用,G w X A * d假设 Socket 当即断开了 epoll_waiW u 3 m ; % !t 会当即回来V d p p # } Z H I,又需求再用 epoll_ctl 把它删掉;
  • 仍然有惊群问题,需求配合运用方法防止,后面胪陈;

kqueue

运用文档在:kquw d u 0 & d x Teue(2)

接口如下:

intkqueue(vo0 = 0 X L kid);
intkevent(intkq,conststructkeq s + 8 : O ] Tvent*changelist,intnchanges,structkevent*eventlist,iu j S Z b } ontnevents,
conststructtimespec*- Y ! $ ) u 6timeout)
;

其间 stru. E U , ect kevent 结构如下:

structkevent{
uintptr_tident;/*identifierforthisevent*/
shortfilter;/*filterforevent*/
u_shortflags;/*actionflagsforkqueue*/
u_intfflaR e Zgs;/*filterflagvalue*4 2 % X M v y/
int64_tdb b Xata;/*filterdatavalue*/
void*udata;/*opaqueuse1 7 b g hrdataidentifier*/
uint64_text[4];/*extensions*/
};

kqueue 跟u : u 8 ~ epoll 有些类似,运用方法上很附近,不再N 9 *叙述,在 kqueue(2) 有示例。但 kqueue 总体上要高档许多。首先是看到 kevent 有 changelist 参数用于传递关怀的 event,nchanges 用于传递 changelist 的巨细。eventlist 用于寄存当有作业发生后,将发生的作业放在这儿。nevents 用于传递 eventlist 巨细。timeout 便是超时时刻。

这儿 kC i U } ( Uqu c H q 5ueue 高档的当地在于,它监听的纷歧定非要是 Socket,纷歧定非要是文件,能够是一系列作业,所以 struct kevent 内参数叫 filter,用于过滤出关怀的作业。它能够去监听非文件,比方 Ss u k wignal,Timer,乃至进程。能够完结比方监K W g听某个进程退出。对于磁盘上的一般文件 kqueuZ } _ j s Ze 也支撑的更好,比方能够在某个文件数据加载到内存后触发 event,然后能够真正 non-blocking 的读文# J : Z件。而 epoll 去监听一般磁盘文件时就以为文件一定是 Ready 的,可是实践读取的时分假设文件数据不在内存缓存_ C N & k Q ? L中的话 read() 仍是会堵塞住等候数据从磁盘读出来。

能够说s i 5 * ] 0 kqueue 有 epoll 的一切长处,乃至还能经过 changelist1 U ? s 一口气注册多个关怀的 event,不需求像 epoll 那样每次调用 epoll_ctl 去装备。当然还有上面说到的,由于接口更 2 X W e m s Z 5笼统,能监听的M 4 u E H作业更多。可T { G是它也有 epoll 的惊群; ~ M ^ 2 F 7 0问题,也需求在运用时分经过装备g D f c 7 Q l i 9参数等方法防止。

比照 epoll 和 kqueue 的功能的话,一般以为 kqueue 功能要好一些,但首要原X l d k [ 8 ; 4 ]因仅仅由于 kqueue 支撑一口气注册一组 event,能削减体系调用次数。其他 kqueue 没有 Edge Trigger,但能经过 EV_CLEAR 参数完结 Edge Trigger 语义。

这篇文章介绍了 epoll 和 kqueue 的比照,感觉还能够,可惜原始链接a q A被破坏了:Scalable Event Multiplexingj M C Q i: epolt _ / , Z #l vs. kqueue,我找到一份复制在这儿:Scalable Event Multiplexing:E 0 B V , 2 epoll vs. kqueue – 后端 – 6HU网

Windows IO CompletionA i + I p Ports (IOCP)

IOCP 是 Windows 下异步 IO 的接口,按说放这儿是不合适的,但我之所以把它放这儿是由于在不了解 IOCP 的时分简略把它和 Linux 下 se. : z xlect, poll, epoll 混在一同,以为 IOCP 是 Windows 上做 IO Multiplexing 的接口,把 IOCP 和e 9 v epoll 之类的放在一同称为是 Windows 上的 epoll,而 IOCP 和 epoll 实践是不同的 IO 模型。在 Java 上,为了跨渠道特性, Java 的 NIO 笼统出来叫 Selector 的概念去完结 IO Multiplex/ f s F /ing,Java 的? 6 b 1 J运用者能够不论 Java 跑在什么渠道上,都用 Selector 去完结 I6 H t o l y * z FO 多路复用,而 JVM 会帮你在用渠道相关的接口去完结 Selector 功能。咱们会知道在 Linux 上它背面实践便是 epoll,但许多人会以为在 windows 上 Selector 背面便是 IOCP,实践不是这样的。在 Windows 上 Selector 背面对应的是 select function (winsock2.h) – Win32 apps | Microsoft Docs,接口和运用都和 Unix 上s W , u !select 很类g 4 @ –似。Java 下用到 IOCP 的是 NIO.2 的 AIO,AsynchronousChannelGroup (Java Platform SE 8 ),也便是说 AIO 在 Windows 上才对应着 IOCP。2 s f _ 3 G

IOCP 的一个阐明在这儿:I/O Completion Ports – WiB } j h ^ O z +n32 apps | Microsoft Docs

还有一篇很好的文章介绍 epoll 和 IOCP 的差异:Practical difference between epoll and; X a % i L 3 ? Windows IO Completion PoS ? 9 Y C @ 3 + vrts (IOCP) | UlduzSoft。

我计划后面专门写一个关于异步 IO 的文章,那个时分在记载更多关于 IOCP 的东西。

io_submit

io_submit 是 Linux 供给的 AIO 接口。首要服务于 AIO 需求,可是一向以来 Linux 的 AIO 问题多多,最首# ; y i ~ ^ 6 H要的; O [ 9 I J /是简略呈J r _ t 0现原本以为是异步的操作实践在履行时分是同步履行的,即经常不满意 AIO 要求。再有是 Linux AIO 开端首要为磁盘类操作而规划,直到 Linux 4.18 之后,io_submit 才支撑了 Polling 操作:A new kernel polling interface LWN.net。对于 AIO 的阐明和 Wiv o [ q I , s F *ndows IOCPt m ( 4 相同,我计划专门写一个关于 AIO 的文章,这儿只介绍一下用 io_submit 替换 ep v N Boll 的方法。

AIO 简略讲便是有接口去让咱们能供给要干什么作业,干完之后成果该放在哪里。还会有个接口用于等候异步使命完结,异步使命完结后会告诉咱们有哪些异步使命完结了,成果分别是什么。告诉 AIO 该做什么作t n ^ S 6 B 业经过 Op Code 完结,io_9 R X {submit 新供给了 IOCB_CMD_POLL 这么个Code 去完结 Socket; I Polling,运$ m ^用起来大致如下:

//sd是被v ` c操作的Socket的m 4 a N h C P n *FD
//aio_buf传递的是poll作业类型,比方POLLINPOLLOUT等拜见http://( / u E Rman7.org/linux/man-pages/man2/poll.2.html
structiocbcb={.aio_fildes=@ . N I Csd,
.aio_lio_op{ / Rcode=$ I H ^ $ 6 c gIOCB_CMD_POLL,
., . ^ M Saio_b| u !uf=POLLIN};
structiocb*list_of_ioX h ^cb[1]={&cb};

//注册作业,ctx是io_setup()时回来的一个context
r=io_submit(ctx,1,list_of_iocb);
//ioK U ^ g Z_getevents()回来后,+ ^ |events内是处于Ready状况的FD
r=io_getevents(ctx,1,1,events,NULL);

IOCB_CMD_POLLone-shot 且是 Level Triz R – [ s ; } I Agger 的] 7 ~ j s。它相对 Epoll 的优势首o l 8 . B 4 ) d要是在于一口气能传递多个监听的 FD,而不需求经过 epoM d p * F ( [ll_ctl 挨个添加。能够在部分场景下替换 Epoll。

CloudFlare 有个文章介绍的这种运用方法,在:io_submit: The epoll alternative you’ve never heard about

更多 Epoll] p s j

前面大致S 9 ?介绍了 Epoll 的运用方法以及它和 select、poll 比照的优缺陷,本节再多介绍一些 Epoll 相关的细节。

什么是 Eage-Trif – o Sgger,什么是 Level-Trigger?

Epoll 有_ 5 : f c k两种触发形式,一种叫 Ex H V ? . + , hage Trigger 简称 ET,一种* u Z % u # &叫 Level Trigger 简称 LT。每一个运用 epolb ) S - q c 9l_ctl 注册在 epoll 描述符上的被监听的描述符都能独自装备自己的触发形式。

对于这两种触发形式的差异从运用的角度上来说,ET 形式下当一个 FD (文件描述符) Ready 后,需求以 No8 9 I * w g Pn-Blocking 方法一向操作这个 FD 直到操作回来 EAGAIN 过错停止,期间 Ready 这个作业只会触发 epoll_wait 一次回来。而假设是 LT 形式,假设 FD 上的作业一向处在 Ready 状况没处理完,则每次调用 epoll_wait 都会当即回来。

这两种触发形式在 epoll(7_ ; K G ! v A) – Linux6 e N W % p manual page 文档中举了一个挺好的比方,在这儿大致记载一下。假定场景如下:

  1. 一个& v m V p Socket 注册在 epoll FD 上,监. 6 N U _ H p听它读作业;
  2. Socket 另一端发送了 2 KB 数据到这个 Socket;
  3. epoll_wait 回来,并带着这个 Socket 的 FD 说它读 Ready;
  4. Socketp ( A 的 Reader 只从 Sockea f *t 读了 1 KB 的数据;
  5. 再次履行 epoll_waT 4 `it

假设这个 Socket 注册在 epoll FD 上时带着 EPOLLET flag,` b i U即 ET 形式下,即便 Socket 还有 1 KB 数据没读` – j W,第五步 eI % # o H L Ppoll_wait 履行时也不会当即回来,会一向堵塞下去直到再有新数据抵达这个 Socket。由于这个 Socket 上的数据一向没有读完,其 Ready 状况在上一次触发 epoll_wait 回来后一向没被整理。需求等这个 Socket 上一切可读的数据悉数被@ ^ k Z [ y X x读洁净,read()R z t e @ 9 操作回来 EAGAIN 后,再次履行 epoll_wait 假设再有新数据抵达 Socket,epoll_^ I B 2 4 2wait 才会当即由于 Socket 读 Ready 而回来。

而假设运用的是 LT 形式,Socket 还剩 1 KB 数据没读,第五步履行 epoll_wA ~ [ F B C X $ait 后它也会带着这个 SockJ P | = P H g pet 的 FD 当即回来,event 列表内会记载这个 Socket 读 Ready。

这儿是以读数据为例,但实践上比方写数据,履行 acceH 1 H Kpt() 等都适用。此外,两者还在唤醒线程上有差异。比方一个进程经过 fork()& P v p , bN T e法承继了父进程的 epoll FD,上面注册了一些监听的 FD。当某个 FD Ready 时,假设是 ET 形式下则只会唤– r h d醒父子进程中的一个,假设是 LT 形式,则会将父子进程都唤醒。

需求弥补阐? ! w = | @ . R R明的是,ET 形式下假设数据是分好几个部分到来的,j ` v u y – V则即便T Z ] @是处于读 Ready 状况且 Socket 还未读空状况/ X 2 M X下,每个新抵达的数据部分都会触发一次 epoll_wait 回来,除非 Socket 的 FD 在注册到 epoll Fw T 2 F u `DH 1 1 ` * y 的时分设置 EPOLLONESHOT flag,这样 Socket 只需触发过一次 epol# { T u S 6 :l_wait 回来后不论再有; z Y !多少数据到X R X P `来,Socket 有没有读空,都不会再触发 epoll_wait 回来,有必要自动Q – * w a d m带着 EPOLL_CTL_MOD 再履行一次 epoll_ctl 把 Socket 的 FD 从头设置到 ep5 Q u ? |oll 的 FD 上,这个 Socket 才会触发下一次读 Ready 让 epoll_wait 回来。

Edge T` g 0 . u E Arigger 有什么好处呢@ p W F?我了解一个是多线程履行 epoll_wait 时能不需求把一切线程都唤醒,再有单线j N J o j 0程状况下也能削减 epoll_wait 被唤醒次数,能够完结尽量均匀的为一切 Socket 履行 IO 操作。比方6 T ) s m ? G y :有 1000 个 Socket 被监听,其间有一个 Socket 发来数据量特别大,其它 Socket 发来的数据都很少,假设是 Level Tri2 b ~ Y 3gge{ W y k & &r,处理线程有必要把数据量特别大的这个 Socket 上数据全处理洁净,epoll_wait 才能堵# L W .塞住,否则每次履行都会当即回来。但 Edge Trigger 下,我能够只从数据量大的 SocketO D L F 读一点数据并记载下这个 Socket 还有数据没读完,之后带着 timeout 去履行 epoll_wait ,回来C | K l Y j x后能够先处理其他 Socket 上的数据,再回头处理数据$ # g 7 O ? O x量大的那个 Socket 的数据,然后公正的履行一切 Socket 上的 IO 操作。

epoll 怎Q u I c Q ^ Q a样完u : C O结守时器

这个倒不是 Epoll 专属,select,poll 等也能用。这些 IO Multiplexing 接口都供给了 timeout 参数,用i c ( y b 4 j以限制等候 FD Ready 的最大时长。可是这个 tiS i – z R y :meout 的精度都是 1ms,假设设置更小的 timeout 就需求 timerfd 的协助。它也是一个 FE ] 6 q | , #D 仅仅能够在G 5 (上面绑定一个高精度的超时时刻,时刻到了今后 Kernel 会经过向这个 timer fd 写数据的方法让, O 3 _ H它自动进入读 Ready 状况。将这个 timer FD 放入 IO Multiplexing 接口监听,然后在N . v 4 8 I时刻到了今后就能唤醒堵塞在 select, poll, epoll 上的线程,完结精度更高的 timeout。

其他需求留意的是,ti{ p h T _mer| ( 8 I q w * l FD 届时后,Kernel 会写数据到这个 FD 上,所以对于 LT 触发方法的 epoll 不光是要监控这个 FD,监控完了还需求实践去读里面B G l L ` X 0 . D的数据。否则下一次 epoli O j Sl_wait 会当即回来,timer FD 失去守时功能。假设是 ET 形式,不去读 Socket 的话,下一次 epoll_wait 不会由于 timer FD L 0 ) ?D 回来,由于之前的数据没读洁净。下一次 timer FD! T = B 到期后新写入 timer FD 的数据才会让 epoll_wait 回来,所以在 ET 形式下是不必读 timer FD 的。其他 timer FD 配合 epoll 比配合 select poll 它们运用起来更方便一些,由于 epoll 能装备为 ET 形式,不必每次 timeout 后还得去读 timer fd。

timer FD 变成读 Ready 后,读取这个 FD 的数N q 8 b K $ v e g据会得到从上一次读这个 FD 到现在@ D Q H ; | (总共 timeout 了多少次。能计算周期数目。

事实上这T P r种唤醒堵塞在 epoll_wait 线程的方法比较常见,也是一种最佳实践。比方能够自己创立一个 FD,以 ET 方法注册在 epoll FD 上,等要唤醒堵塞在 epoll_wait 线程时,写一些数据到这个自建的 FD 上就能够了。为啥不能用 Interrupt 呢?由于 Interrupt 不保险,不高雅。中止{ e 6 E v时不承认线程到底是不是正堵塞在 epoll_wait 上,万一堵塞在其他当地这么去中止线程或许导致问题。比方万一在写文件,这么中止一下文件就写错了。

Epoll 与 File Descriptor

操作体系内 File Descriptor 和 File Description 以及和 inode 的联系如下图所示,下B Z i 7 W 8 ^ ,图截取自:Oreilly Safari 版 The Linux Programming Interface5 , 8 + C ~ 图 5-2。

看到每个进程有自己的 File Descriptor,也即前面一向说的文件描述符,或简称 FD。每个 File Descriptor 指向体系级的 File Description。每个 File Description 又指向体系维护的 I-node。进程 A 内 FD 1 和 FD 20 指向同一个 File Description,Z } $ F ^ 6 + y !一般经过 dup()dup2() 等完结。进程 B 的 FD 2 和进程 A 的 FD 2 指向同一个 File Description,一般经过 fork(# m : 1 H) 完结。进程( W ) 3 c L d ( A 的 FD 0 和进程 B 的 FD 3 指向不同的 File Description 但指向相同的 I-c V _ ) 9node,一般经过 open() 同一个文件完% j . ; S v结。

epoll_create() 履行后,Kernel 担任创立一个 In-memory 的 I-node 用于维护 epoll 的 Interest List,还会用一个 File Description 指向这个 In-Memory Inode。再为履行 epoll_create() 的进程创立 File Descriptor 指向 epoll 的 File Description。4 c W 7

在履行 epoll_ctl 后,被监听的 FD 及其指向的 Fil[ n j | ~ M E c /e Description 一同被放入 epoll 的 Interest List。能够将 (FD, File Descriptio# , _n) 看做是主键,后续相同 FD 再次履行 epoll_c$ 4 J 6 O f * ttl 会报错回来 EEXIST 说已经监听过了。但假设履行一次 dup() 如下图,在 FD 1 履行 dup() 后得到 FD 2,它俩都会指向之前 FDm / % 1 的 File Description 1,则 dup 出来的 FD 2 还能再次履行 epollq N I ! E X B 1 j_ctl 被监听。这么做的原因或许是为了让两个不同的 FD 都注册在 epoll_ctl 里去监听不同的 events。

但这么一来问题就来了。假设一个注册在 Epoll 监听列表的 File Description 只要一个 File Descriptor 指向,那当这个仅有的 File Descriptoro @ a B G 2 X 封闭的时分,会自动从 epoll 里注销出去。但假设是上图的姿态,对 FD 1 履行 close() 后,由于 File Description 1 还有个 FDY 2 _ n P 2 在指o ! x L ? z向,所以0 v K B 4 Q . r 7注册进入 epoll 的 (FD 1, File Description 1) tuple 不会被整理,会一向留在 e+ 7 p g Npoll 内,会呈现:

  1. 再有Q g z ^ 0 A 5 { event 来了今后还会触发 epo| g Jll_wait() 回来;
  2. 带着 EPOLL_CTL_DEL履行 epoll_ctl 去整理 FD 1 会失败,由于 epoll_ctl 要求被操作的 FD 有必要是个有用的 FD,而 FD 1 已经被封闭了
  3. 封闭 FD 2 也没用,由于注册在 epoll 的是 FD 1;
  4. 带着 EPOLL_CTL_DEL 对 FD 2 去履行epoll_ctl 也不可,由于 FD 2 并没有被 epoll 监听过
  5. 将 FD 2 参加 epoll 再从 e= L _ + ` B dpoll 删去也没用u { M b,由于 epoll 内还保留有 (FD 1, File Description 1) 的记载;

也便是说:

Thus it is possible to close an fd, and afterwards forever receive evy ` hents for it, and you can’t do anything about that.

呈现上述问题的代码4 _ & w大致这样:

rfd,wfd=pipe()
write(wfd,"a")#MaketheL / y 1 ` & 5 m"rfd"readable

epfb e 3d=ey Y d Ipoll_create# w K a 6 v s 8()
epoll_ctl(epfd,EPOLL_CTL_ADD,rfd,(EPOLLIN,rfd))

rfd2=dup(rfd)
close(rfd)

r=epoll_wait(epfd,-1ms)#Whatwi^ e [ i 2 (llhaS ( A 5 4ppen?

上面的 epoll_wait 每次调用都e o c S d U z m S会当即回来。由所以 Level Trigger 的,且有数据,且读不出来。这种时分处理方法只能是重建 epoll 实例。所以运用 E& y Y # ppoll 的时分一定要先2 P . G # G c调用 epoll_ctl(EPOLL_CTL_D8 . ( z 9 Y UEL) 再调用 FD 的 close()

上述内容参阅自:Epoll; F h Y . * is fundamentall% – E d 1 8 4 e !y broken 2/2 — Idea of the day 以及 The Linux Programming Interface,63.4.4 节。

还有一个有意思的是 epoll_create1t Z ? U & B b B 7()它创立 epoll 实例的时分能够传 EPOLL_CLOEXEC 参数,然后在 fork() 时,子进程并不会承继 epoll 实例的 FD,而 epoll 的 FD 只在, z k – j Z 0 5父进程可用。epoll_create1(2): open epoll file descriptor – Linux man page

怎样处理惊群问题?

惊群问题词条q x F M F a:Thundering herd problem – Wikipedia

上面说到的 IO Multiplexing API 都有惊群问题,后续只以 epoll 为例来叙述。惊群问题起源在哪里呢,在运用 epr k A # p . A Loll 时分假设只要一个线程去做 epoll_wait 是没惊群问题的,但假设想 scale,想引进多线程去做P H E b P + v W epo? L + lll_wait去向理 IO 作业,就或许遇到惊群问题了,这便是惊群问题的起源。epoll 完结中一点一点引进新参数去向理惊群问题也能看出来 epJ H Qoll 开端规划时是只为一个线程处理 IO 作业规划的,后来为了 scale 引进多线程才开端发现有各种问题,所} n U 6 &以为D a m & X了处理问题又引进一些新参数。

多线程去向理 IO 作业或许有两种方法:

  1. 一个翻开的文件 (Socket) 发生一个 FD 注册在 epoll FD 后,父进程经过 fork() 和子进程) % gD W r ?享一切翻开的 FD,父子进程一同处理翻开文件的 IO 作业。当文件有作业时或许会将父子进程都唤醒,所以呈现惊群;
  2. 指向同一个文件 (! } + : % X 7Socket) 的 FD 被注册到多个不同的 epoll 实例上,且每个 epoll 实例由不同线程担任调用 epoll_wait,这些线程一同处理文件的 IO 作业。此0 a :刻文件有数据K n ~ d v J s W后或许会在两个 epolM f 7 [l 实例上将等在 epoll_waitw e B T D两个线程都唤醒,所以呈现惊群;

为了处理上面的惊群问题,一个处理方法便是运用 Edx H [ U N Dge Trigger。Edge` . ! # { 0 L ( Triggerf : W y 确保在第一种场景下只会唤醒一个进程或线 w N :程去向理作} e , 7 x E 0 D ]业,不过对第二D ` w 7 N 4 O 2个问题场景力不从心。另一个处理方法便是带着 EP| | 6 U j vOLLEXCLUSIVE 该参数在 Linux 4.15 引进,在上述两个场景下都能确保同一个文件发生 I0 } b P ` J 1O 作业后只唤醒一个+ : :线程来处理。

看上去是只需, K V L b z运用 Edge Trigger 而且带着 EPOLLEXCLUSIVE 惊群问题就处理了,但实践问题仍然特别多。

多线程经过 Epoll 处理 accept()

先以 accept() 操作为例,运用 Edge Trigger 且带着 EPOLLEXCLUSIVE参数,或许有如下运b ! 3 )转流程导致有处于 ep a d s ? ypoll_wait() 的线程被无效唤醒:

Kernel:收到第一个衔接,有两个Thread在等候处理accept,由所以ET形式,假定ThreadA被唤醒
ThreadA:从epolF x 8 s Zl_wait()回来
ThreadA:履行accept()操作,正常完毕
Kernel:accept行列空了,将Socket从"readable"切换到"non-readable",所以下/ w % ~ (一次再有& S L , S o } p衔接到来,Kernel会再次触发Event
Kernel:又收到一个新衔接,这是第二个衔` m c L f Y _ e &
Kernel:现在只要一个线程等在`epoll_wait()`上,所以Kernel唤醒ThreadB
ThreadA:持续履行accept()由于它v 1 T ,并不知道Kernel收到多少衔接,需求接连履行accept()直到回来EAGAIN停止。所以它accept了第二个衔接
ThreadB:履行accept()可是收到EAGAIN,也即ThreadB被无效唤醒
ThreadA:再次履行accept()得到EAGAIN,ThreadA回去等在`epoll_wait`

除了无效唤醒外还会遇到饥饿:

Kernel:收到两个衔接,当时有两个线程在处理accept,由所以ET形式,假定ThreadA被唤醒
ThreadAZ ; ::从epoll_] z | F j G Await()回来
ThreadA:履行accept()操作,正常完毕
Kernel:收到第三个衔接恳求,Socket是"readable"状况,持续坚持该状况,不触发Event
ThreadA:持续履行accept()直到遇到EGAIN,所以又正常履行accept,拿到一个新Socket
Kernel:又收到一个新衔接,第四个衔接,持续不触发Event
ThreadA:持续履行accept()直到遇到EGAIS & ? GN,所以又正0 q D {常履行accept,拿到一个新Sp 0 l l |ocket

循环进程能够这么永无止境的持续下V [ C # F / {去,Thread B 即便存在也不能被唤醒。不过这个问题也能够看做是 Accept? 7 K K . x 操作| F ~ 0 F压力不够大,一个线程就扛住恳求量了,假设 connect 的衔接再多,ThrT h read B 仍是会参与 Accept 操作。其他我对这个饥饿发生的场景也比较疑惑,按说 Edge Trigger 下新来数据(这儿是衔接)也会触发 epoll_wa# T 0 b p % Wit 回来来着,但这儿却说由于 Accepk s . gt 行列非空,新衔接来了不触` W m j [ 0 X发 Event。不过这个也不算很重要。

处理方法是运用 Level Trigger,且带着 EPOLLEXCLUS3 | y 1 a 7 k ( eIVE 参数。能够推演一下上面两个场景会发现是能处理问题的。假设是老的 Linux 没有 EPOLLEXCLUSIVEw 4 P 则只能用 Edge Trigger 配合 EPOLLONESHOT 来处理了。便是说一个 Socket 只会发生一个 accept 作业,之后即便再有衔接过来也不会再触发 Event。但每次处理完 Accer e ^ E U ;pt 作业后需求从头用 epll_ct0 4 G ; z C C l 去重置 Socket 对应的 FD。

假设能不必 Epoll 的话还能够引进 SO_REUSEPORT 让多个进程监听同一个端口,经过 OS 来完结 Accept 作. $ % p [ / B k业的负载均衡。缺陷是当一个进程封闭` z Y ] f ] K n Socket 时分,在 Socket 上 Accept[ g k T 5 o 行列排队的恳求会悉数被丢弃。一般来说 Nginx 是用 SO_REUSEPORT 来做 Accept 的负载均衡的。

多线程经过 Epoll 处理 read()

多线程m 8 * A ( , l +经过 Epoll 处理 re7 0 E 9 1 D N 5 ?ad() 操作比处理 accept() 更杂乱。比方在 Level Trigge$ s ! M P k b 8r 下即便配合 EPOLLEXCLUSIVE 也有问题:

Kernel:A ^ O L d }收到2047个字节的数据
Kernel:假定有两个线程等在epoll上,由于有EPOLLEXCLUSIVE所以只唤醒ThreadA
ThreadA:从epoll_wait()回来
Kernel:又收到2字节数据
Kernel:只要一个线程等在eH C b R Spoll上,将其唤醒,即ThreadB被唤醒
Threx 3 yadAd r y ( P * K . J:履行read(2048)读出来2048字节数据
ThreadB:履行read(2048)读出来最后1字节数据

同一个 Socm 4 [ u & ( Wket 的数据分布在两个不同线程,即有了 Race Condition,得U 7 B e a让两个线程同步去向理数据,确保数据不乱序。

Edge Trigger 也有问题:

Kernel:收到2048字节数据
Kernel:Y W y u K 4 |由所以EdgeTrigger只唤醒ThreadA
ThreadA:从epoll_wait()回来
ThreadA:履行read(2048d + ! o d = b)读出悉数的2048字节数据
Kernel:Socketbuffer空了,所以Kernel从头装备Socket的FileDescriptor,下次再有数据时再次发生作业
Kernel:收到1字节数据
Kernw = 3 u 0 [el:只要一个线程等在epoll上,将其唤醒,即ThreadB被唤醒
ThreadB:从epoll_wait()回来
ThreadB:履行read(2048)并读出1字节数据
ThreadA:由所以EdgeTrigger需求再次履行read(2048),回来EAGAIN后不再重试

此刻也是同一个 Socket 的数据被放在了两个不同的线程上,也有 Race Condition。

这儿 req . %ad() 操作不论用 LT 仍是 ET 形式都有问题首要原因是同一个 Socket 的数据或许/ , V会被两个不同的线程同时处理,所以怎样搞都有问题。而且上面说到的 Race Condi} : 5tion 简直无法被处理,不是加个锁就完了。由于两个w 6 J | m线程拿了同一个 Socket 上的两段数据,两个线程根本无法去判别这两段数据谁先谁后,该怎样拼接。现在仅有处理方法便是带上 EPOLLONESHOT,Socket 有数据后只唤醒一个线程,之后这个 Socket 再有数o 5 | %据也不会唤醒其他线程,直到数据被悉数处理完,从头经过 epoll_ctl 参加 epoll 实例后这个 Socketn t g _ C 4 Q 才或许在再次有数据时被分配给其他 Thread。

总归 epoll 想运用正确不简略,特别是想给 Epoll 操作引进多线程的时分愈加杂乱。得清晰B } .的了解 ET,L@ u ) 1 PT 形式,了解 EPOLLONESHOTEPOLLEXCLUSIVE 参数。

本节首要内容都来自:Epoll is fundamentally broken 1/2 — Idea of the day

Java_ r P b 的 Selector

Java 的 NIO 供给了一个叫 Selector的类,用于跨渠道的完结 Soce C U o a aket Polling,也即 IO 多路复用。比方在 BSDT m t r ! { P _ 体系上它背面对应的便是 Kqueue,在 Windows 上对应的是 Select,在 Linux 上对应的是 Level Trigger 的 Epoll。L( A g A t r F cinux 上为什么非要是 Level Trigg ) @ –er 呢?首要是为了跨渠道一致,在 Windows 上背面是 Select,它是 Level Trigger 的,那为了同一套代码多处运转,在 Linux 上也只能是 Level Trigger 的,t S 2 X ! (否则运用方法就不同了。

这也是为什么 Netty 自己又为 Linux 独( ! 8 ] b * 5自完结了一套 EpollEventLoop 而不仅仅供给 NioEventLoop 就完了。由于 N~ – h m Letty 想支撑 Edge Trigger,而且1 / u f Q M g ^ P还有许多 Epoll 专有参数想支撑。参看这儿 Netty 的维护者的回答:nio – Why native epollg | S W u r G R support is introduced in Netty? – Stack Overflow

简略举例一下 SeD { d P y Q [ ,lector 的运用:

  1. 先经过 Selector.open() 创立出来 Selecto% g ? V + G ( 5r;
  2. 创立q { 6 F z出来 SelectableChannel (能够了解为 Socket),装备 Channel 为 Non-Blocking
  3. 经过 Channel 下的 register() 接口注册 Channel 到 Selector,注册时能够带上关怀的作业比方 OP_READ,OJ M W h Z $ oP_ACCEPT, OP_WRITE 等;
  4. 调用 Selector 上的 select() 等候有 Channel 上有 Eventl R W _ p . v 发生
  5. select() 回来后= B P u k V x阐明有 Channel 有 Event 发生,经过 Selector 获取 S5 R ^ K 3electionKey 即哪些 Channel 有什么作业发生了;
  6. 遍历一切获取的 SelectionKey 检查发生了什么作业,是 OP_READ 仍是 OP_WRITE 等,之后处理 Channel 上的作业;
  7. select() 回来的 Iterator 中移除处理完的 SelectionKey

能够看到整个运用进程和运用 select, poll, epoll 的进程是能对应起来的。再弥补一下,Selector 是经过 SPI 来完结不同渠道运用不同 Selector 完结的。SPI 内容请参看 [[Java Service Provider Interfa? s / 1ce (S} } v c r pPI) 和类加载机制]]

实践看看5 B 2 a M G f @ F NeC [ N otty 怎样运J W R 6 C E & *用 Epoll

Netty 对 Linux 的 Epoll 接口做了一层封装,封装I S W _为 JNI 接口供上层 JVM 来调用。以下内容以 Netty 4.1.48,且运用默许的 Edge Trigger 形S r Z w w ~式为例。

怎样写数据

按照之前说的运用方法,写数据前需求先E S @ p经过 epoll_ctl, n ] o i V h正 Interesg l at List 为方针 Socket 的 FD 添加 EPOLLOUT作业监听。等 epoll_wait 回来后表明 Socket 可写,咱们开端用力写数据,直到 write() 回来 EAGAIN 停止。之后咱们要再次运用 epoll_ctl 去掉 Socket 的 EPOLLOUT 作业o O ? r监听,否则下次咱们或许并没有数据要写,可 epoll_wait 还会被过错唤醒一次。能够数一下这种运用方法至少有四次体系调用开销,假设每次写一条数据都这么多体系调用的话功能是上不去的。

那 Netty 是怎样做的呢,最中心的当地在这个 do8 ~ W ~Write()。能够看到最要害的是每次有数据要写 Soc– c 3ket 时并不是当即去注册监听 EPOLLOUT 写数据,而是用 Busy Loop 的方法直接测验调用 write() 去写 Socket,写失败了就重试,能写多少写多少/ P O ` 1 _。假设 Busy Loop 时数据写完了,就直接回来。这种状况下是最优的,彻底省去了 epoll_ctlL Y / V % D 5 Mepoll_wait 的调用。

假设 Busy Loop 屡次后没写完,则分两种状况。一种是下游 Socket 仍然可写,一种是下游 Socke% K 4 I x Ct 已经不能写了 write() 回来了 Error。对于第一种状况,_ , f v & v h W用于操控 Loop 次数的 writeSpinCount 能到 0,由W = Q $ 9 _ Q a于下游仍然可写咱们退出 Busy Loop 仅仅为了不为这一个 Socket 卡住 EventLoop 线程太久,所以此刻仍然不必设置 EPOLLOUT 监听,直接回来即可,这种状况也是最优的。弥补阐明一下,Netty 里一个 EventLoop 对应一个线z j 3 K ( B 9 ^ G程,每个线程会处理一批 Socket 的 IO 操作,还会处理 su( J Tbmit() 进来的 Task,所以线程不能为某个 Socket 处理 IO 操作处理` V ^ p $太久,否则会D z 4 v Z c ,影响到其它 Socket 的运Y o t转。比方我管理了 10000 个衔接,其间有一个衔接数据量超级大,2 g v $ d假设线程都忙着处理这个数据超级大的衔接上的数据,其它衔接的 IO 操作就有延迟了。这也是为什么即便 So$ 5 T kcket 仍然可写,Netty 仍然I . a & L g ) + g在写出一定次数音讯后就退出 Busy Loop 的原因。

只要 Busy LoopE 5 j / ( m A ^ D 写数据时分发现 Socket 写不下去了,这种时分才会装备 EPOLLOUT 监听,才会运用 epoll_ctl,下一次等 epoll_waite s Y H v 3– w B V D )来后会整理 EPOLLOUT 也有一次 epoll_ctl 的开销。

经过以上方法能够看到 Netty 已经尽或许削减 epoll_ctl 体系调用的履行了,然后进步写音讯功能。上面的 doWrite. ? d C :() 下还有许多能够看的东西,比方写数据时分会差异是写一条音讯,仍是能进行批量写,批量写的时分为了调用 JNI 更优k ) J,还要把音讯复制到一个独自的数P D 0 t k v组等。

怎样读数据

原本读操作相} T q @ 4 M = X对写操作来说或许更简略一些,每次 Accept 一个 Socket 后就能够把 Socket 对应的 FD 注册到 Epoll 上监听 EPOLLIN 作业,u ; i A #每逢有作业发生就用力读 Socket 直到遇到 EAGAIN。也便是说整个 Socket 生命周期里都能够不必 epoll_ctl 去修正监听的作业类型。可是对 Netty 来说它支撑一个叫做 Auto Read 的装备,默许是 Auto Read 的,但能& ; ( x V $够封闭。封闭后有必要上层事务自动调用 Channes E ] o 5 A t $ Kl 上的 read() 才能真的读数据进来。这就违反了 Edge Tm g v Vrigger 的约好。所以对于 Netty 在读O C K + 5 A c操作& H * $ [ 4 l上有这么几个看点:

  1. 每次 Accept 一个 Socket 后 Netty 是怎样为每个 Sock8 K % t ~ Q p N Oet 设置 EPOLLIN 监听的;
  2. 每次有读作业后,Edge Trigge( E L w yr 形式下 Netty 是怎样读取数据的,能满意一向读取 Socket 直到 read() 回来 EAGAIN
  3. Edge Trigger 下 Netty 怎样确保不同 Socket 之间是公正的,即不能呈现比方一个 Socket 上一向有S Q . f数据要读而 EventLoop 就一向在读这一个 Socket 让其它 Socket 饥饿;
  4. Netty 的 Auto Reads L H 在 Edge Trigger 形式下是怎样作业的
Accept Socket 后怎样装备 EPOLLIN
  1. Epoll 的` % J 4 c ^ x Server Channel 遇到 EPOLLIN; Y M 作业时便是去( w 3 E 3 D H 5 J履行 Accept 操作,创立新 Socket 也即 Channel 并 触发 Pipeline 的 Read
  2. ServerBootstrap 在 bin| 1 i =d 一个地址时会给 Server Channel 绑定一3 6 , a L个 ServerBootstrapAcceptor handler,每次 Server Channel 有 Read 作业时会用这个 Handler 做处理;
  3. ServerBootstrapAcceptor 内会将新来的 Channel 和一个 EventLoop 绑定
  4. 新 Channel 和 EvR j [entLoop 绑定后会 触发新 Channel 的 Active 作业
  5. 新 Channel Active 后假设敞开了 AutoE ? A Read,会 当即履行一次 channel.read() 操作。默许是 Auto Read 的,假设自动关掉 Auto Read 则每次 Channel Active 后需求事务自动去调用一次 read()
  6. Channel 在履行 read() 时会走到 doBeginRead()
  7. 对 Epoll 来说在 doBeginRead() 内就会 为 Channel 注册 EPOLLIN 作业监听
Channel 在K c G N有 EPOLLIN 作业后n & T ; y怎样处理
  1. Channel 在有 EPOLLIN作业后,会走到i O 0 P d _ l U 一个 Loop 内从 Channel 读取数据;
  2. 看到 Loop 内的 allocHandle 它便是 NettP 1 l y ( k E /y 操控读数据操作的要害。每次履行 read() 后会将回来成果更新在 allocHandle 内,比方读了多少字节数据?成功履行了几次读取?当时 Channel 是不是 Edge Trigger 等。
  3. Epoll Stre6 5 I Kam Channel 的 allocHandlev $ ( q _DefaultMaxMessagesRecvByteBufAllocator 这个类,每次以 Loop 方法从 Channel 读取数据后都会履行. C 3 b { c continueReading 看是否还要持续读。从 continueReading完结能看到循环完毕条件是是否封闭了 Auto Read[ % a _ : @ y ,,是否读了太多音讯,是否是 Edge Trigger 等。默许b ; f X L Y H O最大读取音讯数i { X 0 E $ 8量是 16,也便p B * D是说每个 Chann, w ! f 3 { Mel 假设能接连读取出来数据的话,最多读 16 次就不读了,会切换到其他 Channel 上去读;
  4. 每次循环读取完数据,会走到6 M ) V 3 L ? epollInFinally(),在这儿判别是否 Channel 还有数据没读完,是的话需求 Schedule 一个 Task 过一会持续m c W e z来读这个 Channel 上的数据。由于 Netty 上会分配 IO 操作和 Task 操作比例,一般是一半一半,等m Q ` L / | 2 2 IO 履行完后才会去履行 Task,且 Task 履行时刻是有限的,所以不会呈现比方一个 Channel 数据特别多导致 EventLoop 即便分配了 Task 实践仍是一向在读取同一个 Channel 的数据没有时刻处理其他 Channel 的 IO 操作;
  5. 假设x : = .数据读完了,且 Auto Read 为封闭状况,则会在 epollInFinally()v E 7 H 内去掉 EPOLLIN 监听,在下一次用户调用 read() 时在 doBeginR^ h Oead() 内再次为 Channel 注册 EPOLLIN 作业监听

这么一来读音讯进程就理清了,前面说到的问题也有答案了。简略] i B ; B说便是 NetP w ty 每次| 4 A o读数据会限制每个 Channel 上读取的音讯数量,Edge Trigger 形式下会接连履行 read()y E Q / V p =到读取操作次数到达Z g q H上限,h C _ – ~ eP ! : i设还有数据剩下则经过 Schedule 一个 Task 过一会再{ + D | ^回来读 Socket;Level Trigger, g O 1 s 则一般只读一次。假设 Auto Read 封闭了则会在每次处理完 EPOLLIN 作业后会撤销 Channel 的 EPOLLIN 作业监听,等下一次用户自动调用 Channel 的 read() 时再从头注册 EPOLLIN

参阅资料

  • 本文是我在准备 Lm g r G u $ r [ _eanCloud 内部的一个分享时写的,感兴趣的读者能够查看视频(上、下)。
  • 介绍 select,* ( @ w ) B f k &poll,epoll 差异,挺全的:select / poll / epoll: pra; [ : e _ P h E ^ctical dV ! D hifference for system architects | UlduzSoft
  • 这个介绍 epoll 介绍的挺全:The method to epoll’s madness – Cindy Sridharan – Medium
  • 能够随意看看:Async IO on Lin: L , ; . b #ux: select, poll, and ep& x 5 b Y s 2olT % 8 &l – Julia Evans
  • 这个超级棒,帮你从上到下理清 Linux 网络层,不过跟本文如同没什么联系,可是是我写本文的时分搜到的,也列在这儿吧:GitHub – leandromoreira/linux-network-performance-parameters: Learn where some of the network sysctl variables fit into theM C % A } N O [ q Linux/Kernel network flow