關於
I / O複用
,可以先查閱《UNIX網路程式設計》第六章select
和poll
相關內容。
select
、poll
、epoll
都是I / O
複用的機制,在《UNIX網路程式設計》
裡重點講了select
、poll
的機制,但select
、poll
並不是現代高效能伺服器的最佳選擇。包括現在的Node.js
中的事件迴圈機制(event loop)
也是基於epoll
實現的。
select和poll的缺點
按照《UNIX網路程式設計》
中所述,poll
與select
類似,沒有解決以下的問題:
- 每次呼叫
select
,都需要把fd
集合從使用者態拷貝到核心態,這個開銷在fd
很多時會很大 - 同時每次呼叫
select
都需要在核心遍歷傳遞進來的所有fd
,這個開銷在fd
很多時也很大 select
支援的檔案描述符數量太小了,預設是1024
epoll對於上述缺點的改進
epoll
既然是對select
和poll
的改進,就應該能避免上述的三個缺點。那epoll
都是怎麼解決的呢?在此之前,我們先看一下epoll
和select
和poll
的呼叫介面上的不同,select
和poll
都只提供了一個函式——select
或者poll
函式。而epoll
提供了三個函式,epoll_create
,epoll_ctl
和epoll_wait
,epoll_create
是建立一個epoll
控制程式碼;epoll_ctl
是註冊要監聽的事件型別;epoll_wait
則是等待事件的產生。
對於第一個缺點,epoll
的解決方案在epoll_ctl
函式中。每次註冊新的事件到epoll
控制程式碼中時(在epoll_ctl
中指定EPOLL_CTL_ADD
),會把所有的fd
拷貝進核心,而不是在epoll_wait
的時候重複拷貝。epoll
保證了每個fd
在整個過程中只會拷貝一次。
對於第二個缺點,epoll
的解決方案不像select
或poll
一樣每次都把current
輪流加入fd
對應的裝置等待佇列中,而只在epoll_ctl
時把current
掛一遍(這一遍必不可少)併為每個fd
指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,就會呼叫這個回撥函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列)。epoll_wait
的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd
(利用schedule_timeout()
實現睡一會,判斷一會的效果,和select
實現中的第7步是類似的)。
對於第三個缺點,epoll
沒有這個限制,它所支援的FD
上限是最大可以開啟檔案的數目,這個數字一般遠大於2048
,舉個例子,在1GB
記憶體的機器上大約是10萬
左右,一般來說這個數目和系統記憶體關係很大。
epoll介面
epoll操作過程需要三個介面,分別如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
複製程式碼
epoll_create
方法
#include <sys/epoll.h>
int epoll_create(int size);
複製程式碼
建立一個epoll
的控制程式碼,size
用來告訴核心這個監聽的數目一共有多大,這個引數不同於select()
中的第一個引數,給出最大監聽的fd+1
的值,引數size
並不是限制了epoll
所能監聽的描述符最大個數,只是對核心初始分配內部資料結構的一個建議。
當建立好epoll
控制程式碼後,它就會佔用一個fd
值,在linux
下如果檢視/proc/
程式id/fd/
,是能夠看到這個fd
的,所以在使用完epoll
後,必須呼叫close()
關閉,否則可能導致fd
被耗盡。
#include <sys/epoll.h>
#define FDSIZE 1024
// ...
int main(int argc,char *argv[])
{
int epollfd = epoll_create(FDSIZE); // 這裡並不是指最大檔案描述符數量為1024,而是給核心初始化資料結構的一個建議。
return 0;
}
複製程式碼
epoll_ctl
方法
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
複製程式碼
epoll_ctl
方法是epoll
的事件註冊函式,它不同與select()
是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。
epfd
:是epoll_create()
的返回值。op
:表示對對應的fd
檔案描述符的操作,一般情況下表示想要監聽事件、刪除事件和修改事件處理函式,用三個巨集來表示:EPOLL_CTL_ADD
,表示對於對應的fd
檔案描述符新增一組事件監聽;EPOLL_CTL_DEL
,表示對於對應的fd
檔案描述符刪除該組事件監聽;EPOLL_CTL_MOD
,表示對於對應的fd
檔案描述符修改該組事件監聽為新的events
;
fd
:表示需要監聽的fd
(檔案描述符)event
:是告訴核心需要監聽的事件集合,傳入一個指標,指向事件集合的第一項,struct epoll_event
的結構如下:
struct epoll_event {
__uint32_t events; // 表示一類epoll事件
epoll_data_t data; // 使用者傳遞的資料
}
複製程式碼
因為events
表示一類epoll
事件,它可以是以下幾個巨集的集合:
EPOLLIN
:表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);EPOLLOUT
:表示對應的檔案描述符可以寫;EPOLLPRI
:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);EPOLLERR
:表示對應的檔案描述符發生錯誤;EPOLLHUP
:表示對應的檔案描述符被結束通話;EPOLLET
: 將EPOLL
設為邊緣觸發(Edge Triggered
)模式,這是相對於水平觸發(Level Triggered
)來說的;EPOLLONESHOT
:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket
的話,需要再次把這個socket
加入到EPOLL
佇列裡;
例如,如果想讓epoll
對於對應的檔案描述符fd
新增一組事件,監聽對應的檔案描述符可讀的情況:
static void add_event_epoll_in(int epollfd, int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
}
複製程式碼
epoll_wait
方法
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
複製程式碼
epoll_wait
方法等待事件的產生,類似於select()
呼叫,返回需要處理的事件數目。
epfd
:是epoll_create()
的返回值;events
:用來從核心得到事件的集合,我們一般把需要處理的事件對應的檔案描述符fd
放到events
結構體下data
引數內,這樣我們可以在事件處理函式中取到對應的檔案描述符fd
,執行對應操作(例如對於TCP
套接字,我們呼叫read
,write
等);maxevents
:告之核心這個events
的數量,這個maxevents
的值不能大於建立epoll_create()
時的size
,否則會造成溢位的風險;timeout
:超時時間,以毫秒為單位,如果設定為-1
,表示一直等待,設定為0
表示不等待;
舉例:應用程式一般阻塞與epoll_wait
呼叫,一旦events
中任意一事件觸發,epoll_wait
執行,等待指定的timeout
超時時間,如果I / O
完成則立即返回需要處理的事件數目。
static void do_epoll(int listenfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
int ret;
char buf[MAXSIZE];
memset(buf,0,MAXSIZE);
//建立一個描述符
epollfd = epoll_create(FDSIZE);
//新增監聽描述符事件
add_event_epoll_in(epollfd, listenfd);
for ( ; ; )
{
// 獲取已經準備好的描述符事件數目
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd, events, ret, listenfd, buf);
}
close(epollfd);
}
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
int i;
int fd;
//進行選好遍歷
for (i = 0;i < num;i++)
{
// 在這裡取到需要處理的檔案描述符
fd = events[i].data.fd;
//根據描述符的型別和事件型別進行處理
if ((fd == listenfd) &&(events[i].events & EPOLLIN))
handle_accpet(epollfd,listenfd);
else if (events[i].events & EPOLLIN)
do_read(epollfd,fd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,buf);
}
}
複製程式碼
使用epoll
重構伺服器回射程式
服務端
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>
#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXSIZE 1024
#define LISTENQ 5
#define FDSIZE 1000
#define EPOLLEVENTS 100
//函式宣告
//建立套接字並進行繫結
static int socket_bind(const char* ip,int port);
//IO多路複用epoll
static void do_epoll(int listenfd);
//事件處理函式
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);
//處理接收到的連線
static void handle_accpet(int epollfd,int listenfd);
//讀處理
static void do_read(int epollfd,int fd,char *buf);
//寫處理
static void do_write(int epollfd,int fd,char *buf);
//新增事件
static void add_event(int epollfd,int fd,int state);
//修改事件
static void modify_event(int epollfd,int fd,int state);
//刪除事件
static void delete_event(int epollfd,int fd,int state);
int main(int argc,char *argv[])
{
int listenfd;
listenfd = socket_bind(IPADDRESS,PORT);
listen(listenfd,LISTENQ);
do_epoll(listenfd);
return 0;
}
static int socket_bind(const char* ip,int port)
{
int listenfd;
struct sockaddr_in servaddr;
listenfd = socket(AF_INET,SOCK_STREAM,0);
if (listenfd == -1)
{
perror("socket error:");
exit(1);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,ip,&servaddr.sin_addr);
servaddr.sin_port = htons(port);
if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
{
perror("bind error: ");
exit(1);
}
return listenfd;
}
static void do_epoll(int listenfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
int ret;
char buf[MAXSIZE];
memset(buf,0,MAXSIZE);
//建立一個描述符
epollfd = epoll_create(FDSIZE);
//新增監聽描述符事件
add_event(epollfd,listenfd,EPOLLIN);
for ( ; ; )
{
//獲取已經準備好的描述符事件
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,listenfd,buf);
}
close(epollfd);
}
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
int i;
int fd;
//進行選好遍歷
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
//根據描述符的型別和事件型別進行處理
if ((fd == listenfd) &&(events[i].events & EPOLLIN))
handle_accpet(epollfd,listenfd);
else if (events[i].events & EPOLLIN)
do_read(epollfd,fd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,buf);
}
}
static void handle_accpet(int epollfd,int listenfd)
{
int clifd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1)
perror("accpet error:");
else
{
printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
//新增一個客戶描述符和事件
add_event(epollfd,clifd,EPOLLIN);
}
}
static void do_read(int epollfd,int fd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else if (nread == 0)
{
fprintf(stderr,"client close.\n");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else
{
printf("read message is : %s",buf);
//修改描述符對應的事件,由讀改為寫
modify_event(epollfd,fd,EPOLLOUT);
}
}
static void do_write(int epollfd,int fd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
delete_event(epollfd,fd,EPOLLOUT);
}
else
modify_event(epollfd,fd,EPOLLIN);
memset(buf,0,MAXSIZE);
}
static void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
複製程式碼
客戶端
// client.c
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define MAXSIZE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8787
#define FDSIZE 1024
#define EPOLLEVENTS 20
static void handle_connection(int sockfd);
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_write(int epollfd,int fd,int sockfd,char *buf);
static void add_event(int epollfd,int fd,int state);
static void delete_event(int epollfd,int fd,int state);
static void modify_event(int epollfd,int fd,int state);
int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//處理連線
handle_connection(sockfd);
close(sockfd);
return 0;
}
static void handle_connection(int sockfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
char buf[MAXSIZE];
int ret;
epollfd = epoll_create(FDSIZE);
add_event(epollfd,STDIN_FILENO,EPOLLIN);
for ( ; ; )
{
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,sockfd,buf);
}
close(epollfd);
}
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)
{
int fd;
int i;
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
if (events[i].events & EPOLLIN)
do_read(epollfd,fd,sockfd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,sockfd,buf);
}
}
static void do_read(int epollfd,int fd,int sockfd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
}
else if (nread == 0)
{
fprintf(stderr,"server close.\n");
close(fd);
}
else
{
if (fd == STDIN_FILENO)
add_event(epollfd,sockfd,EPOLLOUT);
else
{
delete_event(epollfd,sockfd,EPOLLIN);
add_event(epollfd,STDOUT_FILENO,EPOLLOUT);
}
}
}
static void do_write(int epollfd,int fd,int sockfd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
}
else
{
if (fd == STDOUT_FILENO)
delete_event(epollfd,fd,EPOLLOUT);
else
modify_event(epollfd,fd,EPOLLIN);
}
memset(buf,0,MAXSIZE);
}
static void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
複製程式碼
執行結果:
Node.js的Event Loop
眾所周知,Node.js
是單執行緒的,可用作高效能伺服器。顯然,若僅僅限定為1024
個描述符,對於高併發的請求顯然是不支援的。Node.js
內部的Event Demultiplexer(事件多路分解器)
藉助epoll
來實現事件迴圈機制。其基本步驟如下:
- 應用程式通過向
Event Demultiplexer(事件多路分解器)
提交請求來生成新的I / O
操作。應用程式還指定一個處理程式,當操作完成時將呼叫該處理程式。向Event Demultiplexer(事件多路分解器)
提交新請求是一種非阻塞呼叫,它立即將控制權返回給該應用程式。 - 當一組
I / O
操作完成時,事件多路分解器將新的事件推入Event Queue(事件佇列)
。 - 此時
Event Loop
遍歷Event Queue
的專案。 - 對於每個事件,呼叫關聯的處理程式。
- 處理程式是應用程式程式碼的一部分,當它執行完成時將控制權返回給
Event Loop
。但是,在處理程式執行過程中可能會請求新的非同步操作,從而導致新的操作被插入Event Demultiplexer(事件多路分解器)
- 當
Event Loop
中的所有專案被處理完時,迴圈將再次阻塞Event Demultiplexer(事件多路分解器)
,當有新事件可用時,Event Demultiplexer(事件多路分解器)
將觸發另一個週期。
通過epoll
解析上述步驟操作:
- 事件多路分解器即為通過
epoll_create()
建立的epoll
控制程式碼的抽象,在Node.js
啟動時,事件多路分解器會阻塞於epoll_wait
呼叫。 - 對於第一步,註冊事件處理程式時,呼叫
epoll_ctl()
方法,設定op
引數為EPOLL_CTL_ADD
,向事件多路分解器新增一組事件。 - 對於第二步,一旦
I / O
完成,又呼叫epoll_ctl()
方法,設定op
引數為EPOLL_CTL_DEL
,刪除對應事件,此時把控制權返還給應用程式。 - 事件迴圈遍歷事件佇列,只要沒有事件,就阻塞於
epoll_wait()
。 - 不斷重複上述步驟,實現Node.js`的事件迴圈機制。
參考資料:
- Linux IO模式及 select、poll、epoll詳解
- Linux Programmer's Manual epoll
- 《Node.js設計模式》第一章
- 《UNIX網路程式設計——卷1:套接字聯網API(第三版)》