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時所返回的文件描述符
操作: 有這么三種EPOLL_CTL_ADD:新注冊fd監聽到epfd中, EPOLL_CTL_MOD:修改已注冊的fd事件監聽,EPOLL_CTL_DEL:從epfd中刪除一個對fd監聽事件;
監聽的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相關