应用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]epolleventretry:  //陷入零碎调用,取出内核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上对应的epitemfunc 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