关于golang:epoll在Golang中的应用

3次阅读

共计 16052 个字符,预计需要花费 41 分钟才能阅读完成。

应用 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)
}

局部零碎调用

抓了一部分零碎调用剖析一下上述程序与内核交互的大抵过程。

$ strace -f ./server

局部零碎调用函数如下。

#.... 省略内存治理局部以及线程治理局部
#执行到 fmt.Println("服务端过程 id:",os.Getpid())
[pid 30307] getpid() = 30307
[pid 30307] write(1, "346234215345212241347253257350277233347250213id357274232 30307n", 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> "512n", 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 ...>
........

参考资料

  • 《后盾开发核心技术与利用实际》第七章:网络 IO 模型
  • 《Unix 环境高级编程》第十四章: 高级 IO
  • 《Go 语言设计与实现》https://draveness.me/golang/d…
  • 《Go netpoller 原生网络模型之源码全面揭秘》https://mp.weixin.qq.com/s/3kqVry3uV6BeMei8WjGN4g
正文完
 0