golang socket读写同时_epoll在Golang的应用
使用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)}//监听该socketif 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 = ctxreturn nil
}
查看runtime_pollServerInit,是对epoll_create的封装。
func poll_runtime_pollServerInit() {//初始化全局epoll对象netpollinit()/全局标志位设置为1atomic.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 = fdpd.closing = falsepd.everr = falsepd.rseq++pd.rg = 0pd.rd = 0pd.wseq++pd.wg = 0pd.wd = 0unlock(&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住goroutineres := runtime_pollWait(pd.runtimeCtx, mode)return convertErr(res, isFile)
}
func poll_runtime_pollWait(pd *pollDesc, mode int) int {......//进入netpollblock函数,该函数内部会阻塞住该goroutinefor !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.rgif mode == 'w' {gpp = &pd.wg}......if waitio || netpollcheckerr(pd, mode) == 0 {//gark住该g,此时传参主要关注前两个,一个netpollblockcommit函数,一个gpp为当前pd的rg或者wg,//用于后面记录fd对应的阻塞的goroutinegopark(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_mmcall(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 {//调用刚传入的函数参数,也就是netpollblockcommitok := fn(gp, _g_.m.waitlock)//调用完清除_g_.m.waitunlockf = nil_g_.m.waitlock = nilif !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或wgr := 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) {//此时获取到了新的fdd, 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的列表注入调度程序并清除glistinjectglist(&list)//修改gp状态casgstatus(gp, _Gwaiting, _Grunnable)if trace.enabled {traceGoUnpark(gp, 0)}//返回可运行的greturn 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 = falseif 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 *gif 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 = 0if 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)//释放对应的pdpollcache.free(pd)
}
//调用epoll_ctl系统调用,删除该fd在eventpoll上对应的epitem
func netpollclose(fd uintptr) int32 {var ev epolleventreturn -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
相关文章:
HTTP 2.0与OkHttp
HTTP 2.0是对1.x的扩展而非替代,之所以是“2.0”,是因为它改变了客户端与服务器之间交换数据的方式。HTTP 2.0增加了新的二进制分帧数据层,而这一层并不兼容之前的HTTP 1.x服务器及客户端——是谓2.0。 在正式介绍HTTP 2.0之前,…

根据“坐标”生成趋势图
数据库环境:SQL SERVER 2008R2 有一“坐标”表t,表结构如下: id int, num int 字段id是序号,递增且连续,字段num是数值类型。id可以看成是坐标轴的横轴,num则跟纵轴有关系&…

Winform程序怎么降低占用的内存?
1 Winform程序怎么降低占用的内存?winform程序占用的内存数一直居高不下,提供给用户的手册中说明内存不能大于50MB,但是每次运行的时候,内存都会飙高到100多MB. 2 3 后来终于发现了一个方法,可以解决这个问题: …

mysql关系表控制_mysql表关系
一、表的详细操作1.修改表名alter table 旧表名 rename 新表名;2.修改表的引擎与字符编码alter table 表名 engine"引擎名" charset"编码名";3.复制表 *#结构create table 新表名 like 旧表名;eg:1create table nt like tt;#将tt的表结构复制到新表nt中…

【Python3爬虫】常见反爬虫措施及解决办法(二)...
【Python3爬虫】常见反爬虫措施及解决办法(二) 这一篇博客,还是接着说那些常见的反爬虫措施以及我们的解决办法。同样的,如果对你有帮助的话,麻烦点一下推荐啦。 一、防盗链 这次我遇到的防盗链,除了前面说…

【原创】ListView快速滚动至新添加一行(自动滚动)
在C#开发中我们经常要开发一些日志系统,尤其是基于ListView的日志显示系统。但是当日志增多是你是否有一些困扰,就是它为什么不会自动滚动至最后一行。以下是一小段代码,希望可以帮助你. public void addLog(string logString) { lock (_lock…

MFC调用CFileDialog之后目录居然会改变,调试了好久终于发现是这个问题
MFC调用CFileDialog之后目录居然会改变,调试了好久终于发现是这个问题,上网搜了下,发现也有人和我出现相同的问题。他的博客如下: http://www.programlife.net/current-directory-changed-after-using-cfiledialog.html MFC调用C…

mysqlls_mysql基本命令
1、Mysql启动命令:命令行内容为:\>net start mysql运行情况如图1所示:图1(Mysql启动命令)2、连接Mysql服务器:命令行内容为:\>mysql -u root -h hostaddress -p password其中,root为Mysql的用户名&a…

2019年3月
分包加载 使用公众号登录微信提示 "公众号暂不支持此种登录方式" 使用已经注册过的手机号注册新的微信账号提示 "你申请注册的手机号已被其他微信号绑定,暂时不能使用该手机号注册" https://github.com/witcat/LayaWxCacheFromZip /******/ (functio…

8天学通MongoDB——第三天 细说高级操作
原文地址:http://www.cnblogs.com/huangxincheng/archive/2012/02/21/2361205.html 今天跟大家分享一下mongodb中比较好玩的知识,主要包括:聚合,游标。 一: 聚合 常见的聚合操作跟sql server一样,有:count&…

UVA 10954 Add All
UVA_10954 看了别人解题报告之后发现累加的过程可以这样操作,每次取最小的两个元素加和,然后把和当作一个新元素放进集合,直到剩下一个元素,然后把中间结果加起来就是要求的结果。实际上这个题目就是哈弗曼编码,在LRJ树…

Java将mysql输出csv,如何从Java中的Access数据库导出表并将其保存到.csv
I am trying to export a lot of large tables from a MS Access db with java using the jdbc:odbc bridge. I wanted to save these tables to a CSV file first was wondering what would the best way to do this would be? any help would be appreciated.解决方案Fetch …

windows下nodejs express安装及入门网站,视频资料,开源项目介绍
windows下nodejs express安装及入门网站,视频资料,开源项目介绍,pm2,supervisor,npm,Pomelo,Grunt安装使用注意事项等总结 第一步:下载安装文件下载地址:官网http://www.nodejs.org/download/ 第二步:安装nodejs下载完…

python 之 pip、pypdf2 安装与卸载
pip是个啥? pip 是一个现代的,通用的 Python 包管理工具。提供了对 Python 包的查找、下载、安装、卸载的功能。 第一步:pip 下载:https://pypi.org/project/pip/#files 第二步:解压,进入目录python pip\pi…

eclipse 3.55安装j2ee开发工具
选择help--->install new software -->work width --选择下拉框选择要安装插件转载于:https://www.cnblogs.com/yjhrem/articles/2309602.html

mysql中没有内置函数_[mysql]MySQL中的内置函数
用在select 语句,以及子句where order by hacing 中 update delete函数中可以将字段名作为字段来用,变量的值就是这个列对应的每一行记录。一、字符串函数php中用到的函数,mysql中大部分也提供了1、CONCAT(”字符串”,字段&…

tiny210V2 Uboot kernel filesystem 烧写和启动
1.sd启动 将u-boot镜像写入SD卡 将SD卡通过读卡器接上电脑(或直接插入笔记本卡槽),通过"cat /proc/partitions"找出SD卡对应的设备,我的设备节点是/dev/sdb.执行下面的命令$sudo dd iflagdsync oflagdsync iftiny210-ub…

Linux下Shell日期的格式
2019独角兽企业重金招聘Python工程师标准>>> 不管是哪种语言,日期/时间都是一个非常重要的值。比如我们保存日志的时候,往往是某个前缀再加上当前时间,这样日志文件名称就可以做到唯一。在Shell环境里,我们获取时间的命…

usaco 6.1
6.1.2 rectbarn 首先要注意空间的消耗,3000*3000 大概10m的样子(最多16m),只够开个char,本想套用big barn的dp方法,定义struct [i,j]{int l;int h}来表示以(i,j)为右上顶点的矩形,貌似这样会爆,只好考虑其它解法(参考wc2003王知昆的论文). 大概思路: 定义h[i,j],l[i,j],r[i,j]分…

docker mysql详解_Docker轻松入门(详解)
一 Docker简介Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙…

[恢]hdu 2014
2011-12-12 05:46:08 地址:http://acm.hdu.edu.cn/showproblem.php?pid2014 题意:中文题。 mark:wa了3次!!!因为敲错变量!!!min敲成了num,各种二。可能是困了…

java在继承中父类的成员变量是否会被子类所覆盖
假如 父类 int num 7;子类 int num 9;父类是否会被子类所覆盖? 给你看两个例子: 第一个例子: 第二个例子: 这两个例子的区别只有一句话 由此证明了子类从父类继承的时候 如果有同名的成员变量 默认情况下 父类的成…

长连接及在Node中的应用——HTTP/1.1 keep-alive
HTTP请求都要经过TCP三次握手建立连接,四次分手断开连,如果每个HTTP请求都要建立TCP连接的话是极其费时的,因此HTTP/1.1中浏览器默认开启了Connection: keep-alive。 请求头中的这个属性的作用可以在请求完成后,保持TCP连接一段时…
python 桑基图 地理坐标_【转载】Python数据可视化-实现Sankey桑基图
根据不完整统计,90%想用sankey图的朋友都是因为被它炫酷的外表所吸引,举个例子:在这里插入图片描述关于sankey图的定义是这样描述的:即桑基能量分流图,也叫桑基能量平衡图。它是一种特定类型的流程图,图中延…

[恢]hdu 2015
2011-12-14 05:49:09 地址:http://acm.hdu.edu.cn/showproblem.php?pid2015 题意:中文,忒麻烦了。 代码: # include <stdio.h>int main (){int n, m, flag ;int i, sum, cnt ;while (~scanf ("%d%d", &n, &a…

http://www.shanghaihaocong.com-WORDPRESS开发的企业主题站
wordpress是世界上使用最多的php开源博客系统,功能强大,而且拥有众多的插件,可扩展性强。 最近,我也用它做了一个企业网站,欢迎浏览:http://www.shanghaihaocong.com,上海灏璁实业有限公司转载于…

蓝桥杯 扑克序列(全排列)
扑克序列 A A 2 2 3 3 4 4, 一共4对扑克牌。请你把它们排成一行。要求:两个A中间有1张牌,两个2之间有2张牌,两个3之间有3张牌,两个4之间有4张牌。 请填写出所有符合要求的排列中,字典序最小的那个。 例如&a…

tensorflow with求导_3.4tensorflow2.x自动求导原理函数详解
自己开发了一个股票智能分析软件,功能很强大,需要的点击下面的链接获取:1.1 tensorflow2.x自动求导1.1.1 自动求导GradientTape类GradientTape的作用就是用于自动求导,需要有自变量x和因变量y,调用gradient(y…
WinRAR也能实现智能备份
日志 唐山郎¥ 一切随缘,顺其自然.加博友 关注他 最新日志 2012年意味着机会还是灾难墙壁网线插座的接法佛度有缘人我~想~你,但不。会找你asp.net"服务器应用程序不可超惊艳! 古装美女超精美剪辑该作者的其他文章 博主推荐 相关日志 随机阅读 首页推…

MediaCodeC解码视频指定帧,迅捷、精确
原创文章,转载请联系作者 若待明朝风雨过,人在天涯!春在天涯 原文地址 提要 最近在整理硬编码MediaCodec相关的学习笔记,以及代码文档,分享出来以供参考。本人水平有限,项目难免有思虑不当之处,…