epoll详解_从源代码角度看epoll在Go中的使用(二)

4bf4051a568d80dd8ea45d9035b8be63.png

上一篇文章跟踪了ListenAccept接口的实现流程,本文将继续分析epoll在runtime层的运作,文中内容会集中在runtime层,若有不当之处请指出。

poll

runtime/netpoll.go是poll的抽象,它规范poll层和runtime层之间的交互接口。

poll_runtime_pollServerInit

func poll_runtime_pollServerInit() {
    netpollinit()
    atomic.Store(&netpollInited, 1)
}

poll初始化,初始化网络轮询器。

poll_runtime_isPollServerDescriptor

判断给定的fd是否是当前epoll中使用的fd。

poll_runtime_pollOpen

func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
    pd := pollcache.alloc()
    // ...
    var errno int32
    errno = netpollopen(fd, pd)
    return pd, int(errno)
}

开启网络轮询器。pollcache.alloc()pollcache创建pollDescpollDescpollcache中以链表的方式存储。将pollDesc和fd绑定起来,netpollopen将在下面解释。

poll_runtime_pollClose

关闭某个连接,需当前连接无读写行为。

poll_runtime_pollReset

重置某个连接,即重置pollDesc

poll_runtime_pollWait

就地等待读信号或者写信号,该函数在前一篇文章详解过。

poll_runtime_pollSetDeadline

设置到期时间。网络请求过程中存在很高的不确定性,大部分情况我们需要有到期时间来标记某个操作已截止。

func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) {
    // 并发访问加锁
    lock(&pd.lock)
    if pd.closing {
        unlock(&pd.lock)
        return
    }

    rd0, wd0 := pd.rd, pd.wd
    combo0 := rd0 > 0 && rd0 == wd0

    // 计算过期时间点
    if d > 0 {
        d += nanotime()
        if d <= 0 {
            d = 1<<63 - 1
        }
    }
    // 将过期时间根据mode存到rd和wd上
    if mode == 'r' || mode == 'r'+'w' {
        pd.rd = d
    }
    if mode == 'w' || mode == 'r'+'w' {
        pd.wd = d
    }
    combo := pd.rd > 0 && pd.rd == pd.wd
    // timer回调函数
    rtf := netpollReadDeadline
    if combo {
        rtf = netpollDeadline
    }
    // 读timer
    if pd.rt.f == nil {
        if pd.rd > 0 {
            pd.rt.f = rtf
            pd.rt.when = pd.rd
            // seq的作用就是在timer到期的时候,和原pollDesc.rseq比较,
            // 如果不同,则重用描述符或重置计时器
            pd.rt.arg = pd
            pd.rt.seq = pd.rseq
            addtimer(&pd.rt)
        }
    } else if pd.rd != rd0 || combo != combo0 {
        // 重置当前正在进行中的计时器
        pd.rseq++
        if pd.rd > 0 {  // 修改计时器
            modtimer(&pd.rt, pd.rd, 0, rtf, pd, pd.rseq)
        } else {    // 删除计时器
            deltimer(&pd.rt)
            pd.rt.f = nil
        }
    }
    // 写计时器
    // ...

    // 获取正在进行IO操作的读goroutine地址或写goroutine地址
    var rg, wg *g
    if pd.rd < 0 || pd.wd < 0 {
        // 内存操作
        atomic.StorepNoWB(noescape(unsafe.Pointer(&wg)), nil) 
        // 获取已被阻塞的goroutine地址
        if pd.rd < 0 {
            rg = netpollunblock(pd, 'r', false)
        }
        if pd.wd < 0 {
            wg = netpollunblock(pd, 'w', false)
        }
    }
    unlock(&pd.lock)
    // 唤醒对应的goroutine
    if rg != nil {
        netpollgoready(rg, 3)
    }
    if wg != nil {
        netpollgoready(wg, 3)
    }
}

还有另外一个类似实现接口netpolldeadlineimpl,实际上大多数情况下都是调用netpollDeadlinenetpollReadDeadlinenetpollWriteDeadline完成。

netpollready

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)
    }
    if rg != nil {
        toRun.push(rg)
    }
    if wg != nil {
        toRun.push(wg)
    }
}

netpollready是epoll上报事件的接口,通过mode取到当前读写goroutine地址将之推送到即将执行队列。

netpollunblock

// ioready为false表示此次调用并非底层epoll事件上报
func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {
    gpp := &pd.rg
    if mode == 'w' {
        gpp = &pd.wg
    }

    for {
        // (1)
        old := *gpp
        if old == pdReady {
            return nil
        }
        // (2)
        if old == 0 && !ioready {
            // Only set READY for ioready. runtime_pollWait
            // will check for timeout/cancel before waiting.
            return nil
        }
        var new uintptr
        if ioready {
            new = pdReady
        }
        // (3)
        if atomic.Casuintptr(gpp, old, new) {
            if old == pdReady || old == pdWait {
                old = 0
            }
            return (*g)(unsafe.Pointer(old))
        }
    }
}

netpollunblock尝试获取在netpollblock中被gopark的goroutine,通过抽象数据结构g返回。(1) old == pdReady即已唤醒,可以直接使用遂直接返回nil(2) 初始化状态时候,当前既没Ready的goroutine也没有Wait的goroutine也直接返回nil(3) 通过原子操作重置并拿到当前正在被gopark的goroutine地址,抽象数据结构g返回。

runtime-epoll

epoll在runtime中的部分在runtime/netpoll_epoll.go文件中实现。上文中涉及到两个函数:netpollinitnetpollopen,实际上是调用到了epoll中。

netpollinit

func netpollinit() {
    epfd = epollcreate1(_EPOLL_CLOEXEC)
    if epfd >= 0 {
        return
    }
    epfd = epollcreate(1024)
    if epfd >= 0 {
        closeonexec(epfd)
        return
    }
    println("runtime: epollcreate failed with", -epfd)
    throw("runtime: netpollinit failed")
}

首先调用epoll_create1创建epoll handle,若epoll_create1失败再调用epoll_create

netpollopen

func netpollopen(fd uintptr, pd *pollDesc) int32 {
    var ev epollevent
    ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
    *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
    return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}

epoll事件注册,注册这个epoll里关心的事件,并将user data设置为runtime.pollDesc,这也就是为什么netpoll系列函数均以pollDesc为参数。

netpollclose

func netpollclose(fd uintptr) int32 {
    var ev epollevent
    return -epollctl(epfd, _EPOLL_CTL_DEL, int32(fd), &ev)
}

从epoll中剔除某个不再关心的fd,应用于主动关闭或超时关闭。

netpoll

netpoll中调用了epoll中第三个API:epoll_wait

func netpoll(block bool) gList {
    if epfd == -1 {
        return gList{}
    }
    waitms := int32(-1)
    if !block {
        waitms = 0
    }
    var events [128]epollevent
retry:
    // (1)
    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
    }
    // (2)
    var toRun gList
    for i := int32(0); i < n; i++ {
        ev := &events[i]
        if ev.events == 0 {
            continue
        }
        var mode int32
        // 通过netpollopen注册的epoll关心事件确定是否读写事件
        if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
            mode += 'r'
        }
        if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
            mode += 'w'
        }
        if mode != 0 {
            // 由netpollopen可知,此处的&ev.data是pollDesc
            pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
            pd.everr = false
            if ev.events == _EPOLLERR {
                pd.everr = true
            }
            // 唤醒goroutine
            netpollready(&toRun, pd, mode)
        }
    }

    if block && toRun.empty() {
        goto retry
    }
    // 返回可以执行事件的goroutine地址集合
    return toRun
}

(1) 调用epoll_wait获取事件,当次最多获取128个epoll事件。(2) 根据事件类型唤醒读写goroutine。

从整个流程上来看,分别调用了epoll中的三个API:epoll_createepoll_ctl以及epoll_wait,通过层级化的封装使用epoll完成IO多路复用。这里很多人可能会好奇,netpoll是在哪里调用的?

实际上netpoll是在runtime.proc.go被底层多处调用,以Go1.13为例,runtime.proc.go中有四处调用netpoll,分别是:func startTheWorldWithSema(emitTraceEvent bool) int64 func findrunnable() (gp *g, inheritTime bool)func pollWork() boolfunc sysmon()
以上均涉及到底层轮询器和调度器。

4cdfdb3908a214f13d0a76894c091ca7.png
netpoll调用

小结

通过前面的内容,我们清楚了epoll在Go中是如何封装的,对用户接口层简化了ListenAcceptReadWrite等接口,简单友好的接口给用户层的逻辑代码提供相当大的便利。

而从net包的整体实现来看,对于用户而言:net的实现是基于epoll的nonblock模式的一些列fd操作。网络操作未Ready时切换goroutine,Ready后等待调度的goroutine加入运行队列,实现了网络操作既不阻塞又是同步执行,这也就是前一篇文章所说的epoll+goroutine。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值