從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時所返回的文件描述符
  操作: 有這么三種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相關


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM