從Go程式設計看IO多路複用Epoll
IO多路複用使得一個執行緒就可就可以處理多個網路連線,無需要建立多個執行緒來處理多個socket連線,減少不必要的資源開銷,但是Select還是Poll、Epoll模式都有著不同的區別;
上篇在介紹Select模式是也介紹了Select模式存在的種種問題,如大量FD集從使用者態拷貝到核心態、FD集合的遍歷問題、通知機制、Select預設只支援1024個檔案描述符的問題等;
Epoll介紹
Epoll基本使用流程為:
1、使用EpollCreate1函式建立Epoll
2、使用EpollCtl函式在Epoll上註冊需要監聽的事件
3、使用EpollWait函式等待事件就緒
在Go中函式定義
Epoll事件物件 type EpollEvent struct { Events uint32 Fd int32 Pad int32 } 建立Epoll func EpollCreate(size int) (fd int, err error) 註冊監聽 func EpollCtl(epfd int, op int, fd int, event *EpollEvent) (err error) 等待就緒 func EpollWait(epfd int, events []EpollEvent, msec int) (n int, err error)
1、EpollCreate
建立epoll例項檔案描述符,不使用時需關閉以便核心銷燬例項釋放資源; size引數為核心fd佇列大小,核心2.6.8後已升級為動態佇列該引數意義不大,但值需大於0;
另有一個EpollCreate1函式, 引數flag:值為0時與EpollCreate一致。 還有一個取值EPOLL_CLOEXEC,設定檔案描述符的標誌,FD_CLOEXEC,指fork的子程序執行exec時關閉此fd;
2、EpollCtl
註冊監聽事件(epfd,操作,監聽的fd,需監聽的事件),向核心註冊、修改、刪除檔案描述符;
epfd: 上一步建立epoll時所返回的檔案描述符
操作:
監聽的fd: 需要監聽的檔案描述符本篇文章裡就是建立Socket或建立連線返回的FD
監聽的事件: 也就是EpollEvent 物件,此物件中主要使用兩個欄位:FD與Events,表示監聽的檔案描述符、與監聽的具體事件;
Events事件型別的取值有:
EPOLLIN:檔案描述符可讀;
EPOLLOUT:檔案描述符可寫;
EPOLLERR:發生錯誤;
EPOLLOHUP:檔案描述符被結束通話;
EPOLLET:將EPOLL設為邊緣觸發 (Edge Triggered) 模式;
EPOLLPRI:檔案描述符有緊急的資料可讀;
EPOLLONESHOT:一次監聽,監聽事件發生後,如還需要監聽fd,需再次fd加入到EPOLL佇列裡;
3、EpollWait
等待epfd上IO事件就緒,引數events:從核心獲取的事件集合,msec:超時時間,-1 阻塞,返回值:就緒事件數目,-1為出錯;
LT與ET觸發模式
Epoll預設為LT觸發模式,Select與Poll只有該模式;
LT觸發(Level triggered 水平觸發): epoll_wait檢測描述符事件發生時將事件通知程式,程式可不立即處理事件。下次呼叫epoll_wait時,會再次響應程式通知此事件。
只要緩衝區有資料呼叫EpollWait時都會立即返回事件就緒,直到緩衝區所有資料處理完;
ET觸發(Edge triggered 邊緣觸發): epoll_wait檢測描述符事件發生時將事件通知程式,程式須立即處理該事件。如不處理,下次呼叫epoll_wait時不會再次響應程式通知此事件。
不管快取區是否有資料,只有新資料到來才觸發,需一次性處理完所有資料,所以ET只支援非阻塞模式,否則當緩衝區沒資料時Read會阻塞;
LT支援Block與Non-Block Socket,ET只支援Non-Block Socket,ET比LT效能更好,其事件觸發少效率高;
Golang中Epoll的使用
func epoll(fd int) {
var event syscall.EpollEvent
//建立epoll例項檔案描述符,不使用時需關閉以便核心銷燬例項釋放資源; size引數為核心fd佇列大小,核心2.6.8後已升級為動態佇列該引數意義不大,但值需大於0
epfd, e := syscall.EpollCreate(1)
if e != nil {
log.Println("epoll_create: ", e)
os.Exit(1)
}
defer syscall.Close(epfd)
//設定事件模式
event.Events = syscall.EPOLLIN
event.Fd = int32(fd) //設定監聽描述符
//註冊監聽事件(epfd,事件動作,監聽的fd,需監聽的事件)
if e = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, fd, &event); e != nil {
log.Println("epoll_ctl: ", e)
os.Exit(1)
}
epollWait(fd, epfd, event)
}
func epollWait(fd, epfd int, epollEvent syscall.EpollEvent) {
var events [10]syscall.EpollEvent
connect = &Connect{map[int]string{}}
for {
nevents, e := syscall.EpollWait(epfd, events[:], -1) //等待獲取就緒事件
if e != nil {
log.Println("EpollWait: ", e)
}
for ev := 0; ev < nevents; ev++ {
event := events[ev].Events
efd := events[ev].Fd
//處理連線
if int(efd) == fd && event == syscall.EPOLLIN {
handConn(fd, epfd, &epollEvent)
} else if event == syscall.EPOLLIN { //可讀
handMsg(epfd, int(efd))
}
//可寫
if events[ev].Events == syscall.EPOLLOUT {
}
}
}
}
獲得就緒事件後
在通過EpollWait獲得就緒事件後,通過對比檔案描述符fd與事件型別可以進行對應邏輯處理,如是新連線或是讀取資料;
1、新連線: 呼叫syscall.Accept獲取連線的檔案描述符,並通過呼叫syscall.EpollCt函式監聽此檔案描述符的事件;
2、讀取資料: 呼叫syscall.Read獲取緩衝區的資料,這裡需注意是LT觸發還是ET觸發,如是ET觸發需要在此次IO就緒事件中通過一次或多次呼叫syscall.Read函式讀取完所有資料;
這裡介紹的Epoll模式則完全沒有Select模式的所有缺點,比Select更靈活且沒有檔案描述符限制,將檔案描述符事件放入到核心事件表中,通過回撥而不是輪詢來實現事件通知;並沒有所監聽的檔案描述符數不受限制;
對比Select與poll模式Epoll通過回撥而不是輪詢來檢查就緒狀態狀態的FD使得效能有很大提升;
文章首發地址:https://mp.weixin.qq.com/s/mcOgZIv0B3bLyoTbvRw5YQ
參考資料:Epoll相關