1. 程式人生 > 其它 >從Go程式設計看IO多路複用Epoll

從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相關