Golang是怎麼利用epoll的

2021-02-21 LinuGo

    使用Golang可以輕鬆地為每一個TCP連接創建一個協程去服務而不用擔心性能問題,這是因為Go內部使用goroutine結合IO多路復用實現了一個「異步」的IO模型,這使得開發者不用過多的關注底層,而只需要按照需求編寫上層業務邏輯。這種異步的IO是如何實現的呢?下面我會針對Linux系統進行分析。

    在Unix/Linux系統下,一切皆文件,每條TCP連接對應了一個socket句柄,這個句柄也可以看做是一個文件,在socket上收發數據,相當於對一個文件進行讀寫,所以一個socket句柄,通常也用表示文件描述符fd來表示。可以進入/proc/PID/fd/查看進程佔用的fd。

    系統內核會為每個socket句柄分配一個讀(接收)緩衝區和一個寫(發送)緩衝區,發送數據就是在這個fd對應的寫緩衝區上寫數據,而接收數據就是在讀緩衝區上讀數據,當程序調用write或者send時,並不代表數據發送出去,僅僅是把數據拷貝到了寫緩衝區,在時機恰當時候(積累到一定數量),會將數據發送到目的端。

Golang runtime還是需要頻繁去檢查是否有fd就緒的,嚴格說並不算真正的異步,算是一種非阻塞IO復用。

IO模型

    借用教科書中幾張圖

阻塞式IO

    程序想在緩衝區讀數據時,緩衝區並不一定會有數據,這會造成陷入系統調用,只能等待數據可以讀取,沒有數據讀取時則會阻塞住進程,這就是阻塞式IO。當需要為多個客戶端提供服務時,可以使用線程方式,每個socket句柄使用一個線程來服務,這樣阻塞住的則是某個線程。雖然如此可以解決進程阻塞,但是還是會有相當一部分CPU資源浪費在了等待數據上,同時,使用線程來服務fd有些浪費資源,因為如果要處理的fd較多,則又是一筆資源開銷。


非阻塞式IO

    與之對應的是非阻塞IO,當程序想要讀取數據時,如果緩衝區不存在,則直接返回給用戶程序,但是需要用戶程序去頻繁檢查,直到有數據準備好。這同樣也會造成空耗CPU。

IO多路復用

    而IO多路復用則不同,他會使用一個線程去管理多個fd,可以將多個fd加入IO多路復用函數中,每次調用該函數,傳入要檢查的fd,如果有就緒的fd,直接返回就緒的fd,再啟動線程處理或者順序處理就緒的fd。這達到了一個線程管理多個fd任務,相對來說較為高效。常見的IO多路復用函數有select,poll,epoll。select與poll的最大缺點是每次調用時都需要傳入所有要監聽的fd集合,內核再遍歷這個傳入的fd集合,當並發量大時候,用戶態與內核態之間的數據拷貝以及內核輪詢fd又要浪費一波系統資源(關於select與poll這裡不展開)。

epoll介紹

    接下來介紹一下epoll系統調用

    epoll相比於select與poll相比要靈活且高效,他提供給用戶三個系統調用函數。Golang底層就是通過這三個系統調用結合goroutine完成的「異步」IO。

//用於創建並返回一個epfd句柄,後續關於fd的添加刪除等操作都依據這個句柄。
int epoll_create(int size);
//用於向epfd添加,刪除,修改要監聽的fd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
//傳入創建返回的epfd句柄,以及超時時間,返回就緒的fd句柄。
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);


調用epoll_create會在內核創建一個eventpoll對象,這個對象會維護一個epitem集合,可簡單理解為fd集合。

調用epoll_ctl函數用於將fd封裝成epitem加入這個eventpoll對象,並給這個epitem加了一個回調函數註冊到內核,會在這個fd狀態改變時候觸發,使得該epitem加入eventpoll的就緒列表rdlist。

當相應數據到來,觸發中斷響應程序,將數據拷貝到fd的socket緩衝區,fd緩衝區狀態發生變化,回調函數將fd對應的epitem加入rdlist就緒隊列中。

調用epoll_wait時無需遍歷,只是返回了這個就緒的rdlist隊列,如果rdlist隊列為空,則阻塞等待或等待超時時間的到來。

大致工作原理如圖

異步IO

    當用戶程序想要讀取fd數據時,系統調用直接通知到內核並返回處理其他的事情,內核將數據準備好之後,通知用戶程序,用戶程序再處理這個fd上的事件。

Golang異步IO實現思路

    我們都知道,協程的資源佔有量很小,而且協程也擁有多種狀態如阻塞,就緒,運行等,可以使用一個協程服務一個fd不用擔心資源問題。將監聽fd的事件交由runtime來管理,實現協程調度與依賴fd的事件。當要協程讀取fd數據但是沒有數據時,park住該協程(改為Gwaiting),調度其他協程執行。

    在執行協程調度時候,去檢查fd是否就緒,如果就緒時,調度器再通知該park住的協程fd可以處理了(改為Grunnable並加入執行隊列),該協程處理fd數據,這樣既減少了CPU的空耗,也實現了消息的通知,用戶層面上看實現了一個異步的IO模型。

    Golang netpoll的大致思想就是這樣,接下來看一下具體代碼實現,本文基於go1.14。

具體實現

    接下來看下Golang netpoll對其的使用。


實驗案例

    跟隨一個很簡單的demo探索一下。

func main() {
  fmt.Println("服務端進程id:",os.Getpid())
  lister, err := net.Listen("tcp", "0.0.0.0:9009")
  if err != nil {
    fmt.Println("連接失敗", err)
    return
  }
  for {
    conn, err := lister.Accept() //等待建立連接
    if err != nil {
      fmt.Println("建立連接失敗", err)
      continue
    }
     //開啟協程處理
    go func() {
      defer conn.Close()
      for {
        buf := make([]byte, 128)
        n, err := conn.Read(buf)
        if err != nil{
          fmt.Println("讀出錯",err)
          return
        }
        fmt.Println("讀取到的數據:",string(buf[:n]))
      }
    }()
  }
}

net.Listen的內部調用

    net.Listen依次調用lc.Listen->sl.listenTCP->internetSocket->socket到fd.listenStream函數創建了一個監聽9009的tcp連接的socket接口,也就是創建了socket fd,

    接下來為了監聽該socket對象就需要把這個socket fd加入到eventpoll中了。

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
    .
  //綁定該socket接口
  if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
    return os.NewSyscallError("bind", err)
  }
  //監聽該socket
  if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
    return os.NewSyscallError("listen", err)
  }
  //初始化fd,也就是把socket放入epoll中,進入
  if err = fd.init(); err != nil {
    return err
  }
  lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
  fd.setAddr(fd.addrFunc()(lsa), nil)
  return nil
}

func (fd *FD) Init(net string, pollable bool) error {
  .
  //將socket fd加到poll,進入
  err := fd.pd.init(fd)
  .
  return err
}

//最終跳轉到該處,主要關注兩個函數runtime_pollServerInit,runtime_pollOpen,
//這兩個函數都是runtime實現的,將epoll交由runtime來管理
func (pd *pollDesc) init(fd *FD) error {
  //sync.once方法,調用epoll_create創建eventpoll對象
  serverInit.Do(runtime_pollServerInit)
  //將當前的fd加到epoll中,底層調用epollctl函數
  ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
  //如果出錯,處理相應的fd,刪除epoll中fd以及解除狀態等操作
  if errno != 0 {
    if ctx != 0 {
      runtime_pollUnblock(ctx)
      runtime_pollClose(ctx)
    }
    return errnoErr(syscall.Errno(errno))
  }
  pd.runtimeCtx = ctx
  return nil
}

   查看runtime_pollServerInit,是對epoll_create的封裝。

func poll_runtime_pollServerInit() {
  //初始化全局epoll對象
  netpollinit()
  /全局標誌位設置為1
  atomic.Store(&netpollInited, 1)
}
func netpollinit() {
  //系統調用,創建一個eventpoll對象
  epfd = epollcreate1(_EPOLL_CLOEXEC)
  if epfd >= 0 {
    return
  }
  .
}

    查看一下runtime_pollOpen方法,將當前監聽的socket fd加入eventpoll對象中。實際上是對epoll_ctl的封裝。

func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
  //返回一個存儲在Go程序中的一個fd對應的結構體,算是用於記錄
  //goroutine與fd之間的關係,後面會分析到
  pd := pollcache.alloc()
  //加鎖,防止並發問題
  lock(&pd.lock)
  if pd.wg != 0 && pd.wg != pdReady {
    throw("runtime: blocked write on free polldesc")
  }
  if pd.rg != 0 && pd.rg != pdReady {
    throw("runtime: blocked read on free polldesc")
  }
  pd.fd = fd
  pd.closing = false
  pd.everr = false
  pd.rseq++
  pd.rg = 0
  pd.rd = 0
  pd.wseq++
  pd.wg = 0
  pd.wd = 0
  unlock(&pd.lock)

  var errno int32
  //epoll_ctl系統調用
  errno = netpollopen(fd, pd)
  return pd, int(errno)
}

func netpollopen(fd uintptr, pd *pollDesc) int32 {
  var ev epollevent
  //註冊event事件,這裡使用了epoll的ET模式,相對於ET,ET需要每次產生事件時候就要處理事件,
  //否則容易丟失事件。
  ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
  //events記錄上pd的指針
  *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
  //系統調用將該fd加到eventpoll對象中,交由內核監聽
  return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}


Accept的內部調用

   接下來返回到主函數。

func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
  .
   //檢查fd狀態是否變化
  if err := fd.pd.prepareRead(fd.isFile); err != nil {
    return -1, nil, "", err
  }
  for {
    //accept系統調用,如果有對監聽的socket的連接請求,則直接返回發起連接的socket文件描述符
    //,否則返回EAGAIN錯誤,被下面捕獲到
    s, rsa, errcall, err := accept(fd.Sysfd)
    if err == nil {
      return s, rsa, "", err
    }
    switch err {
    case syscall.EAGAIN:
      if fd.pd.pollable() {
         //進入waitRead方法,內部
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    case syscall.ECONNABORTED:
      continue
    }
    return -1, nil, errcall, err
  }
}


func (pd *pollDesc) wait(mode int, isFile bool) error {
  if pd.runtimeCtx == 0 {
    return errors.New("waiting for unsupported file type")
  }
   //進入runtime_pollWait方法內部,該方法會跳轉到runtime包下,條件滿足會park住goroutine
  res := runtime_pollWait(pd.runtimeCtx, mode)
  return convertErr(res, isFile)
}

func poll_runtime_pollWait(pd *pollDesc, mode int) int {
  .
   //進入netpollblock函數,該函數內部會阻塞住該goroutine
  for !netpollblock(pd, int32(mode), false) {
    err = netpollcheckerr(pd, int32(mode))
    if err != 0 {
      return err
    }
  }
  return 0
}

func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
  gpp := &pd.rg
  if mode == 'w' {
    gpp = &pd.wg
  }
    .
  if waitio || netpollcheckerr(pd, mode) == 0 {
    //gark住該g,此時傳參主要關注前兩個,一個netpollblockcommit函數,一個gpp為當前pd的rg或者wg,
    //用於後面記錄fd對應的阻塞的goroutine
    gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
  }
  old := atomic.Xchguintptr(gpp, 0)
  if old > pdWait {
    throw("runtime: corrupted polldesc")
  }
  return old == pdReady
}

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
  .
  //主要關注兩個傳參,lock是gpp指針
  mp.waitlock = lock
  //unlockf為netpollblockcommit函數
  mp.waitunlockf = unlockf
    .
  //切換到g0棧去執行park_m
  mcall(park_m)
}

func park_m(gp *g) {
  //獲取當前goroutine
  _g_ := getg()
  //修改狀態為Gwaiting,代表當前的goroutine被park住了
  casgstatus(gp, _Grunning, _Gwaiting)
  //解除m和g關聯
  dropg()
  if fn := _g_.m.waitunlockf; fn != nil {
     //調用剛傳入的函數參數,也就是netpollblockcommit
    ok := fn(gp, _g_.m.waitlock)
     //調用完清除
    _g_.m.waitunlockf = nil
    _g_.m.waitlock = nil
    if !ok {
      if trace.enabled {
        traceGoUnpark(gp, 2)
      }
      casgstatus(gp, _Gwaiting, _Grunnable)
      execute(gp, true) // Schedule it back, never returns.
    }
  }
  //調度新的g到m上來
  schedule()
}

func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
  //把當前g的指針存為gpp指針,gpp為pd的rg或wg
  r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
  if r {
    //將全局變量改為1,代表系統有netpoll的等待者
    atomic.Xadd(&netpollWaiters, 1)
  }
  return r
}

    到此時,accept函數就被阻塞住了,系統會在這個監聽的socket fd事件(0.0.0.0:9009的這個fd)的狀態發生變化時候(也就是有新的客戶端請求連接的時候),將該park住的goroutine給ready。

//上面提到過的accept函數,根據序號順序分析
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
    .
  for {
    //2.使用accept系統調用能獲取到新的連接,linux會為新的連接分配一個新的fd,
    //這個函數會返回新的連接的socket fd對應的進程描述符
    s, rsa, errcall, err := accept(fd.Sysfd)
    if err == nil {
      //3.返回新的進程描述符
      return s, rsa, "", err
    }
    switch err {
    case syscall.EAGAIN:
      if fd.pd.pollable() {
         //1.剛才阻塞到了這個goroutine,後來新的連接請求,該goroutine被喚醒
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    .
    }
        .
  }
}

//返回上一層的函數
func (fd *netFD) accept() (netfd *netFD, err error) {
    //此時獲取到了新的fd
  d, rsa, errcall, err := fd.pfd.Accept()
  .
  //創建新的fd結構體
  if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
    poll.CloseFunc(d)
    return nil, err
  }
  //init函數又會進入func (pd *pollDesc) init(fd *FD) error函數,並將新的socket連接通過epoll_ctl傳入
  //epoll的監聽事件
  if err = netfd.init(); err != nil {
    fd.Close()
    return nil, err
  }
  //系統調用,可以獲得客戶端的socket的ip信息等
  lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
  netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
  return netfd, nil
}


喚醒park住的協程

    go會在調度goroutine時候執行epoll_wait系統調用,檢查是否有狀態發生改變的fd,有的話就把他取出,喚醒對應的goroutine去處理。該部分對應了runtime中的netpoll方法。

    源碼調用runtime中的schedule() -> findrunnable() -> netpoll()

func findrunnable() (gp *g, inheritTime bool) {
  _g_ := getg()
   //分別從本地隊列和全局隊列尋找可執行的g
  .
  //判斷是否滿足條件,初始化netpoll對象,是否等待者,以及上次調用時間
  if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
    //netpoll底層調用epoll_wait,傳參代表epoll_wait時候是阻塞等待或者非阻塞直接返回
    //這裡是非阻塞模式,會立即返回內核eventpoll對象的rdlist列表
    if list := netpoll(false); !list.empty() {
      gp := list.pop()
       //將可運行G的列表注入調度程序並清除glist
      injectglist(&list)
       //修改gp狀態
      casgstatus(gp, _Gwaiting, _Grunnable)
      if trace.enabled {
        traceGoUnpark(gp, 0)
      }
            //返回可運行的g
      return gp, false
    }
  }
    ..

  stopm()
  goto top
}

//對epoll_wait的進一步封裝
func netpoll(block bool) gList {
  if epfd == -1 {
    return gList{}
  }
  waitms := int32(-1)
  if !block {
    waitms = 0
  }
  //聲明一個epollevent事件,在epoll_wait系統調用時候,會給該數組賦值並返回一個索引位,
  /之後可以遍歷數組取出就緒的fd事件。
  var events [128]epollevent
retry:
  //陷入系統調用,取出內核eventpoll中的rdlist,返回就緒的事件
  n := epollwait(epfd, &events[0], int32(len(events)), waitms)
  if n < 0 {
    if n != -_EINTR {
      println("runtime: epollwait on fd", epfd, "failed with", -n)
      throw("runtime: netpoll failed")
    }
    goto retry
  }
  var toRun gList
  //遍歷event事件數組
  for i := int32(0); i < n; i++ {
    ev := &events[i]
    if ev.events == 0 {
      continue
    }
    var mode int32
    //是否有就緒的讀寫事件,放入mode標誌位
    if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
      mode += 'r'
    }
    if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
      mode += 'w'
    }
    if mode != 0 {
      //取出存入的pollDesc的指針
      pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
      pd.everr = false
      if ev.events == _EPOLLERR {
        pd.everr = true
      }
      //取出pd中的rg或wg,後面放到運行隊列
      netpollready(&toRun, pd, mode)
    }
  }
  if block && toRun.empty() {
    goto retry
  }
  return toRun
}



func netpollready(toRun *gList, pd *pollDesc, mode int32) {
  var rg, wg *g
  if mode == 'r' || mode == 'r'+'w' {
    rg = netpollunblock(pd, 'r', true)
  }
  if mode == 'w' || mode == 'r'+'w' {
    wg = netpollunblock(pd, 'w', true)
  }
    //將阻塞的goroutine加入gList返回
  if rg != nil {
    toRun.push(rg)
  }
  if wg != nil {
    toRun.push(wg)
  }
}


conn.Read的內部調用

    回到主函數,我們使用go func形式使用一個協程去處理一個tcp連接,每個協程裡面會有conn.Read,該函數在讀取時候如果緩衝區不可讀,該goroutine也會陪park住,等待socket fd可讀,調度器通過netpoll函數調度它。

func main() {
  .
  //開啟處理
    go func() {
      defer conn.Close()
      for {
        buf := make([]byte, 128)
        //將緩衝區的數據讀出來放到buf中
        n, err := conn.Read(buf)
            .
      }
    }()
  }
}

func (fd *FD) Read(p []byte) (int, error) {
  .
  for {
    //系統調用讀取緩衝區數據,這裡沒有可讀會直接返回,不會阻塞
    n, err := syscall.Read(fd.Sysfd, p)
    if err != nil {
      n = 0
      if err == syscall.EAGAIN && fd.pd.pollable() {
        //不可讀,進入waitRead方法,park住該goroutine,
        //並記錄goroutine到pd的rg中,等待喚醒
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    }
    .
  }
}

    後面會等待緩衝區可讀寫,shchedule函數調用netpoll並進一步調用epoll_wait檢測到並喚醒該goroutine。可以查看上面netpoll,這裡不做重複工作了。


Golang也提供了對於epoll item節點的刪除操作,具體封裝函數poll_runtime_pollClose

//當發生某些情況,如連接斷開,fd銷毀等,會調用到此處
func poll_runtime_pollClose(pd *pollDesc) {
  ..
  netpollclose(pd.fd)
  //釋放對應的pd
  pollcache.free(pd)
}
//調用epoll_ctl系統調用,刪除該fd在eventpoll上對應的epitem
func netpollclose(fd uintptr) int32 {
  var ev epollevent
  return -epollctl(epfd, _EPOLL_CTL_DEL, int32(fd), &ev)
}


部分系統調用

  抓了一部分系統調用分析一下上述程序與內核交互的大致過程。

   部分系統調用函數如下。

#....省略內存管理部分以及線程管理部分
#執行到fmt.Println("服務端進程id:",os.Getpid())
[pid 30307] getpid() = 30307
[pid 30307] write(1, "\346\234\215\345\212\241\347\253\257\350\277\233\347\250\213id\357\274\232 30307\n", 27服務端進程id:30307
) = 27

.由於過多,省略關於socket的系統調用

[pid 30308] <... nanosleep resumed> NULL) = 0
#打開系統文件,該文件定義tcp最大連接數,會被設置成pollable,並加入epoll節點中
[pid 30307] openat(AT_FDCWD, "/proc/sys/net/core/somaxconn", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 30307] <... openat resumed> ) = 4
#調用epoll_ctl,創建一個eventpoll
[pid 30307] epoll_create1(EPOLL_CLOEXEC) = 5
#將fd加到epoll事件
[pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189320, u64=139635855949576}}) = 0
[pid 30307] fcntl(4, F_GETFL) = 0x8000 (flags O_RDONLY|O_LARGEFILE)
[pid 30307] fcntl(4, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
[pid 30308] <... nanosleep resumed> NULL) = 0
[pid 30307] read(4, <unfinished ...>
#執行epoll_wait查看就緒事件
[pid 30308] epoll_pwait(5, <unfinished ...>
[pid 30307] <... read resumed> "512\n", 65536) = 4
[pid 30308] <... epoll_pwait resumed> [{EPOLLIN|EPOLLOUT, {u32=2174189320, u64=139635855949576}}], 128, 0, NULL, 139635812673280) = 1
[pid 30307] read(4, <unfinished ...>
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 30307] <... read resumed> "", 65532) = 0
#將/proc/sys/net/core/somaxconn文件的fd從epoll中刪除
[pid 30307] epoll_ctl(5, EPOLL_CTL_DEL, 4, 0xc00005e8d4) = 0
#關掉打開的somaxconn描述符
[pid 30307] close(4) = 0
#設置監聽的socket描述符
[pid 30307] setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
[pid 30307] bind(3, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
[pid 30307] listen(3, 512 <unfinished ...>
[pid 30308] <... nanosleep resumed> NULL) = 0
[pid 30307] <... listen resumed> ) = 0
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
#將用於監聽的socket fd加入到epoll中
[pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189320, u64=139635855949576}}) = 0
[pid 30307] getsockname(3, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0
#執行accept4發現沒有連接,返回EAGAIN錯誤
[pid 30307] accept4(3, 0xc00005eb98, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
#查看是否有就緒的fd,此次調用是非阻塞,立即返回
[pid 30307] epoll_pwait(5, [], 128, 0, NULL, 0) = 0
[pid 30308] <... nanosleep resumed> NULL) = 0
#查看是否有就緒的fd,此次會阻塞等待,直到有連接進來
[pid 30307] epoll_pwait(5, <unfinished ...>
[pid 30308] futex(0x60dc70, FUTEX_WAIT_PRIVATE, 0, {tv_sec=60, tv_nsec=0} <unfinished ...>
[pid 30307] <... epoll_pwait resumed> [{EPOLLIN, {u32=2174189320, u64=139635855949576}}], 128, -1, NULL, 0) = 1
[pid 30307] futex(0x60dc70, FUTEX_WAKE_PRIVATE, 1) = 1
[pid 30308] <... futex resumed> ) = 0
#新的連接,代表收到了一個客戶端連接,分配了一個fd是4
[pid 30307] accept4(3, <unfinished ...>, <... accept4 resumed> {sa_family=AF_INET6, sin6_port=htons(52082), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 4
#把4加入到epoll中管理
[pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189112, u64=139635855949368}}) = 0
[pid 30307] getsockname(4, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0

.

#後來將client端關掉,此時tcp連接斷掉了,將epoll中的fd移除
[pid 30309] epoll_ctl(5, EPOLL_CTL_DEL, 4, 0xc00005fdd4 <unfinished ...>
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 30309] <... epoll_ctl resumed> ) = 0
[pid 30309] close(4) = 0
[pid 30309] epoll_pwait(5, [], 128, 0, NULL, 824634114048) = 0
#阻塞等待
[pid 30309] epoll_pwait(5, <unfinished ...>
...


參考資料


相關焦點

  • 從linux源碼看epoll
    和select、poll等系統調用相比,epoll在需要監視大量文件描述符並且其中只有少數活躍的時候,表現出無可比擬的優勢。epoll能讓內核記住所關注的描述符,並在對應的描述符事件就緒的時候,在epoll的就緒鍊表中添加這些就緒元素,並喚醒對應的epoll等待進程。
  • epoll原理簡介
    本文主要介紹epoll的實現原理,了解epoll高效背後的魔法。epoll的使用簡介1. epoll_create使用epoll時需要使用epoll_create()創建一個epoll的文件句柄,epoll_create()函數的原型如下:intepoll_create(int size);此接口用於創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。
  • (二十五)深入淺出TCPIP之 epoll和select,poll的區別
    epoll:        poll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都只提供了一個函數——select或者poll函數。
  • epoll和select的區別
    2.epoll的最大好處是不會隨著FD的數目增長而降低效率,在selec中採用輪詢處理,其中的數據結構類似一個數組的數據結構,而epoll是維護一個隊列,直接看隊列是不是空就可以了。epoll只會對「活躍」的socket進行操作---這是因為在內核實現中epoll是根據每個fd上面的callback函數實現的。
  • 介紹一下 Android Handler 中的 epoll 機制?
    epoll 最大連接數無上限;首次調用 epoll_ctl 拷貝 fd,調用 epoll_wait 時不拷貝;回調方式工作時間複雜度為 O(1)epoll APIint epoll_create(int size);創建 eventpoll 對象
  • linux開發各種I/O操作簡析,以及select、poll、epoll機制的對比
    我們再說一下select,poll和epoll這幾個IO復用方式,這時你就會了解它們為什麼是同步IO了,以epoll為例,在epoll開發的伺服器模型中,epoll_wait()這個函數會阻塞等待就緒的fd,將就緒的fd拷貝到epoll_events集合這個過程中也不能做其它事(雖然這段時間很短,所以epoll配合非阻塞IO是很高效也是很普遍的伺服器開發模式
  • Linux使用epoll異步發送http請求
    只需給epoll_wait設置10毫秒超時,用戶是感知不到的,但是同步socket多長時間返回是不確定的。Linux異步socket的步驟:1,在socket()函數獲取文件描述符時設置SOCK_NONBLOCK標示,也可以在之後用fcntl()函數設置。
  • Golang入門教程——基本操作篇
    但是在golang當中支持同時返回多個結果,這將會極大地方便我們的編碼。在golang的設計中設想當中,只需要一種循環,就可以實現所有的功能。從某種程度上來說,也的確如此,golang中的循環有點像是C++和Python循環的結合體,集合兩種所長。首先,我們先來看下for循環的語法,在for循環當中,我們使用分號分開循環條件。
  • Linux使用epoll控制多個socket發送http請求
    在客戶端使用epoll控制多個socket發送數據,與在伺服器上是類似的,也是把一個連續的同步過程拆成多個非阻塞的階段,在一個線程內實現高並發,而不是開多個線程。客戶端使用多個socket異步高並發,一般是對伺服器做壓力測試的代碼。
  • Golang入門教程——map篇
    今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。
  • 吊打面試官 | BAT都在問-從底層理解select和epoll的區別
    select和epoll的區別是一道高頻的面試題。epoll對select的缺進行了哪些改進。所以,epoll將等待隊列和阻塞進程分開了,使用epoll_ctl維護等待隊列,使用epoll_wait阻塞進程。2.對於select不知道哪些socket收到數據,需要遍歷,epoll內部維護了一個就緒隊列,收到數據的socket直接加入就緒隊列,當進程被喚醒,只要獲取就緒隊列rdlist就能知道哪些socket收到數據了。
  • Golang入門教程——面向對象篇
    今天是golang專題的第9篇文章,我們一起來看看golang當中的面向對象的部分。在現在高級語言當中,面向對象幾乎是不可或缺也是一門語言最重要的部分之一。golang作為一門剛剛誕生十年的新興語言自然是支持面向對象的,但是golang當中面向對象的概念和特性與我們之前熟悉的大部分語言都不盡相同。比如Java、Python等,相比之下, golang這個部分的設計非常得簡潔和優雅(仁者見仁),所以即使你之前沒有系統地了解過面向對象,也沒有關係,也一定能夠看懂。
  • golang下文件鎖的使用
    前言題目是golang下文件鎖的使用,但本文的目的其實是通過golang下的文件鎖的使用方法,來一窺文件鎖背後的機制。
  • 使用Golang快速構建WEB應用
    如果發現問題或者有好的建議請回復我我回及時更正。 1.Abstract在學習web開發的過程中會遇到很多困難,因此寫了一篇類似綜述類的文章。作為路線圖從web開發要素的index出發來介紹golang開發的學習流程以及Example代碼。在描述中多是使用代碼來描述使用方法不會做過多的說明。最後可以方便的copy代碼來實現自己的需求。
  • gRPC 實操指南(golang)
    •C/S架構的傳輸業務,如股票軟體,每天需要用戶登陸的時候去伺服器拉取最新的數據,或者較簡單的文件傳輸業務,登陸驗證業務,證書業務都可以使用rpc的方式•跨語言開發的項目,比如web業務使用golang進行開發,底層使用cpp或c,部分腳本使用py,跨語言通信可以通過RPC提供的不同語言的開發機制進行實現。
  • 聊聊golang的defer
    序本文主要研究一下golang的deferdeferreturn先賦值(對於命名返回值),然後執行defer,最後函數返回
  • golang之context使用
    背景golang中並發編程的三種實現方式:chan管道、waitGroup和Context。本篇將重點介紹context的使用,告訴大家基本的使用方式,做到會用。Context概念介紹context譯為上下文,golang在1.6.2的時候還沒有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的庫中。golang 的 Context包,是專門用來處理多個goroutine之間與請求域的數據、取消信號、截止時間等相關操作。
  • Golang:重新認識你的Go應用
    執行如上命令,找到應用程式的入口地址,到golang的runtime包中分析源碼,梳理其啟動過程如下:Goroutine只存在於Go語言的運行時,是Go語言在用戶態提供的線程,作為一種顆粒度更細的資源調度單元,如果使用得當,能夠在高並發的場景下更高效地利用機器的CPU。M--ThreadGo語言並發模型中的M是作業系統線程。
  • 【Golang】圖解channel之數據結構
    channel被設計用來實現goroutine間的通信,按照golang的設計思想:以通信的方式共享內存。