在linux 沒(méi)有實(shí)現(xiàn)epoll事件驅(qū)動(dòng)機(jī)制之前,我們一般選擇用select或者poll等io多路復(fù)用的方法來(lái)實(shí)現(xiàn)并發(fā)服務(wù)程序。在linux新的內(nèi)核中,有了一種替換它的機(jī)制,就是epoll。
select()和poll() io多路復(fù)用模型
select的缺點(diǎn):
1.單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,通常是1024,當(dāng)然可以更改數(shù)量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數(shù)量越多,性能越差;(在linux內(nèi)核頭文件中,有這樣的定義:#define __fd_setsize 1024)
2.內(nèi)核 / 用戶空間內(nèi)存拷貝問(wèn)題,select需要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生巨大的開(kāi)銷;
3.select返回的是含有整個(gè)句柄的數(shù)組,應(yīng)用程序需要遍歷整個(gè)數(shù)組才能發(fā)現(xiàn)哪些句柄發(fā)生了事件;
4.select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序如果沒(méi)有完成對(duì)一個(gè)已經(jīng)就緒的文件描述符進(jìn)行io操作,那么之后每次select調(diào)用還是會(huì)將這些文件描述符通知進(jìn)程。
相比select模型,poll使用鏈表保存文件描述符,因此沒(méi)有了監(jiān)視文件數(shù)量的限制,但其他三個(gè)缺點(diǎn)依然存在。
假設(shè)我們的服務(wù)器需要支持100萬(wàn)的并發(fā)連接,則在__fd_setsize 為1024的情況下,則我們至少需要開(kāi)辟1k個(gè)進(jìn)程才能實(shí)現(xiàn)100萬(wàn)的并發(fā)連接。除了進(jìn)程間上下文切換的時(shí)間消耗外,從內(nèi)核/用戶空間大量的無(wú)腦內(nèi)存拷貝、數(shù)組輪詢等,是系統(tǒng)難以承受的。因此,基于select模型的服務(wù)器程序,要達(dá)到10萬(wàn)級(jí)別的并發(fā)訪問(wèn),是一個(gè)很難完成的任務(wù)。
epoll io多路復(fù)用模型實(shí)現(xiàn)機(jī)制
由于epoll的實(shí)現(xiàn)機(jī)制與select/poll機(jī)制完全不同,上面所說(shuō)的 select的缺點(diǎn)在epoll上不復(fù)存在。
設(shè)想一下如下場(chǎng)景:有100萬(wàn)個(gè)客戶端同時(shí)與一個(gè)服務(wù)器進(jìn)程保持著tcp連接。而每一時(shí)刻,通常只有幾百上千個(gè)tcp連接是活躍的(事實(shí)上大部分場(chǎng)景都是這種情況)。如何實(shí)現(xiàn)這樣的高并發(fā)?
在select/poll時(shí)代,服務(wù)器進(jìn)程每次都把這100萬(wàn)個(gè)連接告訴操作系統(tǒng)(從用戶態(tài)復(fù)制句柄數(shù)據(jù)結(jié)構(gòu)到內(nèi)核態(tài)),讓操作系統(tǒng)內(nèi)核去查詢這些套接字上是否有事件發(fā)生,輪詢完后,再將句柄數(shù)據(jù)復(fù)制到用戶態(tài),讓服務(wù)器應(yīng)用程序輪詢處理已發(fā)生的網(wǎng)絡(luò)事件,這一過(guò)程資源消耗較大,因此,select/poll一般只能處理幾千的并發(fā)連接。
epoll的設(shè)計(jì)和實(shí)現(xiàn)與select完全不同。epoll通過(guò)在linux內(nèi)核中申請(qǐng)一個(gè)簡(jiǎn)易的文件系統(tǒng)(文件系統(tǒng)一般用什么數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)?b+樹(shù))。把原先的select/poll調(diào)用分成了3個(gè)部分:
1)調(diào)用epoll_create()建立一個(gè)epoll對(duì)象(在epoll文件系統(tǒng)中為這個(gè)句柄對(duì)象分配資源)
2)調(diào)用epoll_ctl向epoll對(duì)象中添加這100萬(wàn)個(gè)連接的套接字
3)調(diào)用epoll_wait收集發(fā)生的事件的連接
如此一來(lái),要實(shí)現(xiàn)上面說(shuō)是的場(chǎng)景,只需要在進(jìn)程啟動(dòng)時(shí)建立一個(gè)epoll對(duì)象,然后在需要的時(shí)候向這個(gè)epoll對(duì)象中添加或者刪除連接。同時(shí),epoll_wait的效率也非常高,因?yàn)檎{(diào)用epoll_wait時(shí),并沒(méi)有一股腦的向操作系統(tǒng)復(fù)制這100萬(wàn)個(gè)連接的句柄數(shù)據(jù),內(nèi)核也不需要去遍歷全部的連接。
epoll實(shí)現(xiàn)機(jī)制
當(dāng)某一進(jìn)程調(diào)用epoll_create方法時(shí),linux內(nèi)核會(huì)創(chuàng)建一個(gè)eventpoll結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體中有兩個(gè)成員與epoll的使用方式密切相關(guān)。eventpoll結(jié)構(gòu)體如下所示:
1
2
3
4
5
6
7
8
|
struct eventpoll{ .... /*紅黑樹(shù)的根節(jié)點(diǎn),這顆樹(shù)中存儲(chǔ)著所有添加到epoll中的需要監(jiān)控的事件*/ struct rb_root rbr; /*雙鏈表中則存放著將要通過(guò)epoll_wait返回給用戶的滿足條件的事件*/ struct list_head rdlist; .... }; |
每一個(gè)epoll對(duì)象都有一個(gè)獨(dú)立的eventpoll結(jié)構(gòu)體,用于存放通過(guò)epoll_ctl方法向epoll對(duì)象中添加進(jìn)來(lái)的事件。這些事件都會(huì)掛載在紅黑樹(shù)中,如此,重復(fù)添加的事件就可以通過(guò)紅黑樹(shù)而高效的識(shí)別出來(lái)(紅黑樹(shù)的插入時(shí)間效率是lgn,其中n為樹(shù)的高度)。
而所有添加到epoll中的事件都會(huì)與設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,也就是說(shuō),當(dāng)相應(yīng)的事件發(fā)生時(shí)會(huì)調(diào)用這個(gè)回調(diào)方法。這個(gè)回調(diào)方法在內(nèi)核中叫ep_poll_callback,它會(huì)將發(fā)生的事件添加到rdlist雙鏈表中。
在epoll中,對(duì)于每一個(gè)事件,都會(huì)建立一個(gè)epitem結(jié)構(gòu)體,如下所示:
1
2
3
4
5
6
7
|
struct epitem{ struct rb_node rbn; //紅黑樹(shù)節(jié)點(diǎn) struct list_head rdllink; //雙向鏈表節(jié)點(diǎn) struct epoll_filefd ffd; //事件句柄信息 struct eventpoll *ep; //指向其所屬的eventpoll對(duì)象 struct epoll_event event; //期待發(fā)生的事件類型 } |
當(dāng)調(diào)用epoll_wait檢查是否有事件發(fā)生時(shí),只需要檢查eventpoll對(duì)象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發(fā)生的事件復(fù)制到用戶態(tài),同時(shí)將事件數(shù)量返回給用戶。
通過(guò)紅黑樹(shù)和雙鏈表數(shù)據(jù)結(jié)構(gòu),并結(jié)合回調(diào)機(jī)制,造就了epoll的高效。
epoll的接口
1.epoll_create
創(chuàng)建epoll句柄
函數(shù)聲明:int epoll_create(int size)
參數(shù):size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大。
返回值:返回創(chuàng)建了的epoll句柄。
當(dāng)創(chuàng)建好epoll句柄后,它就是會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡。
2.epoll_ctl
將被監(jiān)聽(tīng)的描述符添加到epoll句柄或從epool句柄中刪除或者對(duì)監(jiān)聽(tīng)事件進(jìn)行修改。
函數(shù)申明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event*event);
參數(shù):
epfd: epoll_create()的返回值
op:表示要進(jìn)行的操作,其值分別為:
epoll_ctl_add: 注冊(cè)新的fd到epfd中;
epoll_ctl_mod: 修改已經(jīng)注冊(cè)的fd的監(jiān)聽(tīng)事件;
epoll_ctl_del: 從epfd中刪除一個(gè)fd;
fd:需要操作/監(jiān)聽(tīng)的文件句柄
event:是告訴內(nèi)核需要監(jiān)聽(tīng)什么事件,struct epoll_event如下:
1
2
3
4
5
6
7
8
9
10
11
|
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* epoll events */ epoll_data_t data; /* user data variable */ }; |
events可以是以下幾個(gè)宏的集合:
epollin:觸發(fā)該事件,表示對(duì)應(yīng)的文件描述符上有可讀數(shù)據(jù)。(包括對(duì)端socket正常關(guān)閉);
epollout:觸發(fā)該事件,表示對(duì)應(yīng)的文件描述符上可以寫(xiě)數(shù)據(jù);
epollpri:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái));
epollerr:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤;
epollhup: 表示對(duì)應(yīng)的文件描述符被掛斷;
epollet:將epoll設(shè)為邊緣觸發(fā)(edgetriggered)模式,這是相對(duì)于水平觸發(fā)(level triggered)來(lái)說(shuō)的。
epolloneshot: 只監(jiān)聽(tīng)一次事件,當(dāng)監(jiān)聽(tīng)完這次事件之后,如果還需要繼續(xù)監(jiān)聽(tīng)這個(gè)socket的話,需要再次把這個(gè)socket加入到epoll隊(duì)列里。
示例:
1
2
3
4
5
6
7
|
struct epoll_event ev; //設(shè)置與要處理的事件相關(guān)的文件描述符 ev.data.fd=listenfd; //設(shè)置要處理的事件類型 ev.events=epollin|epollet; //注冊(cè)epoll事件 epoll_ctl(epfd,epoll_ctl_add,listenfd,&ev); |
1.epoll_wait
等侍注冊(cè)在epfd上的socket fd的事件的發(fā)生,如果發(fā)生則將發(fā)生的sokct fd和事件類型放入到events數(shù)組中。
函數(shù)原型:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
參數(shù):
epfd:由epoll_create 生成的epoll文件描述符
events:用于回傳代處理事件的數(shù)組
maxevents:每次能處理的最大事件數(shù)
timeout:等待i/o事件發(fā)生的超時(shí)毫秒數(shù),-1相當(dāng)于阻塞,0相當(dāng)于非阻塞。一般用-1即可
epoll的工作模式
et(edgetriggered):高速工作模式,只支持no_block(非阻塞模式)。在此模式下,當(dāng)描述符從未就緒變?yōu)榫途w時(shí),內(nèi)核通過(guò)epoll告知。然后它會(huì)假設(shè)用戶知道文件描述符已經(jīng)就緒,并且不會(huì)再為那個(gè)文件描述符發(fā)送更多的就緒通知,直到某些操作導(dǎo)致那個(gè)文件描述符不再為就緒狀態(tài)了。(觸發(fā)模式只在數(shù)據(jù)就緒時(shí)通知一次,若數(shù)據(jù)沒(méi)有讀完,下一次不會(huì)通知,直到有新的就緒數(shù)據(jù))
lt(leveltriggered):缺省工作方式,支持blocksocket和no_blocksocket。在lt模式下內(nèi)核會(huì)告知一個(gè)文件描述符是否就緒了,然后可以對(duì)這個(gè)就緒的fd進(jìn)行io操作。如果不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知!若數(shù)據(jù)沒(méi)有讀完,內(nèi)核也會(huì)繼續(xù)通知,直至設(shè)備數(shù)據(jù)為空為止!
示例說(shuō)明:
1.我們已經(jīng)把一個(gè)用來(lái)從管道中讀取數(shù)據(jù)的文件句柄(rfd)添加到epoll描述符
2. 這個(gè)時(shí)候從管道的另一端被寫(xiě)入了2kb的數(shù)據(jù)
3. 調(diào)用epoll_wait(2),并且它會(huì)返回rfd,說(shuō)明它已經(jīng)準(zhǔn)備好讀取操作
4. 然后我們讀取了1kb的數(shù)據(jù)
5. 調(diào)用epoll_wait(2)……
et工作模式:
如果我們?cè)诘?步將rfd添加到epoll描述符的時(shí)候使用了epollet標(biāo)志,在第2步執(zhí)行了一個(gè)寫(xiě)操作,第三步epoll_wait會(huì)返回同時(shí)通知的事件會(huì)銷毀。因?yàn)榈?步的讀取操作沒(méi)有讀空文件輸入緩沖區(qū)內(nèi)的數(shù)據(jù),因此我們?cè)诘?步調(diào)用epoll_wait(2)完成后,是否掛起是不確定的。epoll工作在et模式的時(shí)候,必須使用非阻塞套接口,以避免由于一個(gè)文件句柄的阻塞讀/阻塞寫(xiě)操作把處理多個(gè)文件描述符的任務(wù)餓死。
只有當(dāng)read(2)或者write(2)返回eagain時(shí)(認(rèn)為讀完)才需要掛起,等待。但這并不是說(shuō)每次read()時(shí)都需要循環(huán)讀,直到讀到產(chǎn)生一個(gè)eagain才認(rèn)為此次事件處理完成,當(dāng)read()返回的讀到的數(shù)據(jù)長(zhǎng)度小于請(qǐng)求的數(shù)據(jù)長(zhǎng)度時(shí)(即小于sizeof(buf)),就可以確定此時(shí)緩沖中已沒(méi)有數(shù)據(jù)了,也就可以認(rèn)為此事讀事件已處理完成。
lt工作模式:
lt方式調(diào)用epoll接口的時(shí)候,它就相當(dāng)于一個(gè)速度比較快的poll(2),并且無(wú)論后面的數(shù)據(jù)是否被使用,因此他們具有同樣的職能。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
/* * file epolltest.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <netdb.h> #include <fcntl.h> #include <sys/epoll.h> #include <string.h> #define maxevents 64 //函數(shù): //功能:創(chuàng)建和綁定一個(gè)tcp socket //參數(shù):端口 //返回值:創(chuàng)建的socket static int create_and_bind ( char *port) { struct addrinfo hints; struct addrinfo *result, *rp; int s, sfd; memset (&hints, 0, sizeof ( struct addrinfo)); hints.ai_family = af_unspec; /* return ipv4 and ipv6 choices */ hints.ai_socktype = sock_stream; /* we want a tcp socket */ hints.ai_flags = ai_passive; /* all interfaces */ s = getaddrinfo (null, port, &hints, &result); if (s != 0) { fprintf (stderr, "getaddrinfo: %s\n" , gai_strerror (s)); return -1; } for (rp = result; rp != null; rp = rp->ai_next) { sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (sfd == -1) continue ; s = bind (sfd, rp->ai_addr, rp->ai_addrlen); if (s == 0) { /* we managed to bind successfully! */ break ; } close (sfd); } if (rp == null) { fprintf (stderr, "could not bind\n" ); return -1; } freeaddrinfo (result); return sfd; } //函數(shù) //功能:設(shè)置socket為非阻塞的 static int make_socket_non_blocking ( int sfd) { int flags, s; //得到文件狀態(tài)標(biāo)志 flags = fcntl (sfd, f_getfl, 0); if (flags == -1) { perror ( "fcntl" ); return -1; } //設(shè)置文件狀態(tài)標(biāo)志 flags |= o_nonblock; s = fcntl (sfd, f_setfl, flags); if (s == -1) { perror ( "fcntl" ); return -1; } return 0; } //端口由參數(shù)argv[1]指定 int main ( int argc, char *argv[]) { int sfd, s; int efd; struct epoll_event event; struct epoll_event *events; if (argc != 2) { fprintf (stderr, "usage: %s [port]\n" , argv[0]); exit (exit_failure); } sfd = create_and_bind (argv[1]); if (sfd == -1) abort (); s = make_socket_non_blocking (sfd); if (s == -1) abort (); s = listen (sfd, somaxconn); if (s == -1) { perror ( "listen" ); abort (); } //除了參數(shù)size被忽略外,此函數(shù)和epoll_create完全相同 efd = epoll_create1 (0); if (efd == -1) { perror ( "epoll_create" ); abort (); } event.data.fd = sfd; event.events = epollin | epollet; //讀入,邊緣觸發(fā)方式 s = epoll_ctl (efd, epoll_ctl_add, sfd, &event); if (s == -1) { perror ( "epoll_ctl" ); abort (); } /* buffer where events are returned */ events = calloc (maxevents, sizeof event); /* the event loop */ while (1) { int n, i; n = epoll_wait (efd, events, maxevents, -1); for (i = 0; i < n; i++) { if ((events[i].events & epollerr) || (events[i].events & epollhup) || (!(events[i].events & epollin))) { /* an error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */ fprintf (stderr, "epoll error\n" ); close (events[i].data.fd); continue ; } else if (sfd == events[i].data.fd) { /* we have a notification on the listening socket, which means one or more incoming connections. */ while (1) { struct sockaddr in_addr; socklen_t in_len; int infd; char hbuf[ni_maxhost], sbuf[ni_maxserv]; in_len = sizeof in_addr; infd = accept (sfd, &in_addr, &in_len); if (infd == -1) { if (( errno == eagain) || ( errno == ewouldblock)) { /* we have processed all incoming connections. */ break ; } else { perror ( "accept" ); break ; } } //將地址轉(zhuǎn)化為主機(jī)名或者服務(wù)名 s = getnameinfo (&in_addr, in_len, hbuf, sizeof hbuf, sbuf, sizeof sbuf, ni_numerichost | ni_numericserv); //flag參數(shù):以數(shù)字名返回 //主機(jī)地址和服務(wù)地址 if (s == 0) { printf ( "accepted connection on descriptor %d " "(host=%s, port=%s)\n" , infd, hbuf, sbuf); } /* make the incoming socket non-blocking and add it to the list of fds to monitor. */ s = make_socket_non_blocking (infd); if (s == -1) abort (); event.data.fd = infd; event.events = epollin | epollet; s = epoll_ctl (efd, epoll_ctl_add, infd, &event); if (s == -1) { perror ( "epoll_ctl" ); abort (); } } continue ; } else { /* we have data on the fd waiting to be read. read and display it. we must read whatever data is available completely, as we are running in edge-triggered mode and won't get a notification again for the same data. */ int done = 0; while (1) { ssize_t count; char buf[512]; count = read (events[i].data.fd, buf, sizeof (buf)); if (count == -1) { /* if errno == eagain, that means we have read all data. so go back to the main loop. */ if ( errno != eagain) { perror ( "read" ); done = 1; } break ; } else if (count == 0) { /* end of file. the remote has closed the connection. */ done = 1; break ; } /* write the buffer to standard output */ s = write (1, buf, count); if (s == -1) { perror ( "write" ); abort (); } } if (done) { printf ( "closed connection on descriptor %d\n" , events[i].data.fd); /* closing the descriptor will make epoll remove it from the set of descriptors which are monitored. */ close (events[i].data.fd); } } } } free (events); close (sfd); return exit_success; } |
代碼編譯后,./epolltest 8888 ,在另外一個(gè)終端中執(zhí)行
telnet 192.168.1.161 8888 ,192.168.1.161
為執(zhí)行測(cè)試程序的ip。在telnet終端敲入任何字符敲入enter后,會(huì)在測(cè)試終端顯示敲入的字符。
總結(jié)
以上就是本文關(guān)于linux epoll機(jī)制詳解的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!
原文鏈接:http://blog.csdn.net/u010657219/article/details/44061629