Go网络库的基础实现

Go语言的出现,让我见到了一门语言把网络编程这件事情给做“正确”了,当然,除了Go语言以外,还有很多语言也把这件事情做”正确”了。我一直坚持着这样的理念——要做"正确"的事情,而不是"高性能"的事情;很多时候,我们在做系统设计、技术选型的时候,都被“高性能”这三个字给绑架了,当然不是说性能不重要,你懂的。

目前很多高性能的基础网络服务器都是采用的C语言开发的,比如:Nginx、Redis、memcached等,它们都是基于”事件驱动 + 事件回掉函数”的方式实现,也就是采用epoll等作为网络收发数据包的核心驱动。不少人(包括我自己)都认为“事件驱动 + 事件回掉函数”的编程方法是“反人类”的;因为大多数人都更习惯线性的处理一件事情,做完第一件事情再做第二件事情,并不习惯在N件事情之间频繁的切换干活。为了解决程序员在开发服务器时需要自己的大脑不断的“上下文切换”的问题,Go语言引入了一种用户态线程goroutine来取代编写异步的事件回掉函数,从而重新回归到多线程并发模型的线性、同步的编程方式上。

用Go语言写一个最简单的echo服务器:

package main
import (
    "log"
    "net"
)
func main() {
    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
            log.Println(err)
            return
    }
    for {
            conn, err := ln.Accept()
            if err != nil {
                log.Println(err)
                continue
            }
            go echoFunc(conn)
    }
}
func echoFunc(c net.Conn) {
    buf := make([]byte, 1024)
    for {
            n, err := c.Read(buf)
            if err != nil {
                log.Println(err)
                return
            }
            c.Write(buf[:n])
    }
}

main函数的过程就是首先创建一个监听套接字,然后用一个for循环不断的从监听套接字上Accept新的连接,最后调用echoFunc函数在建立的连接上干活。关键代码是:

go echoFunc(conn)

 每收到一个新的连接,就创建一个“线程”去服务这个连接,因此所有的业务逻辑都可以同步、顺序的编写到echoFunc函数中,再也不用去关心网络IO是否会阻塞的问题。不管业务多复杂,Go语言的并发服务器的编程模型都是长这个样子。可以肯定的是,在linux上Go语言写的网络服务器也是采用的epoll作为最底层的数据收发驱动,Go语言网络的底层实现中同样存在“上下文切换”的工作,只是这个切换工作由runtime的调度器来做了,减少了程序员的负担。

弄明白网络库的底层实现,貌似只要弄清楚echo服务器中的Listen、Accept、Read、Write四个函数的底层实现关系就可以了。本文将采用自底向上的方式来介绍,也就是从最底层到上层的方式,这也是我阅读源码的方式。底层实现涉及到的核心源码文件主要有:

net/fd_unix.go 
net/fd_poll_runtime.go
runtime/netpoll.goc 
runtime/netpoll_epoll.c 
runtime/proc.c (调度器)

netpoll_epoll.c文件是Linux平台使用epoll作为网络IO多路复用的实现代码,这份代码可以了解到epoll相关的操作(比如:添加fd到epoll、从epoll删除fd等),只有4个函数,分别是runtime·netpollinit、runtime·netpollopen、runtime·netpollcloseruntime·netpoll。init函数就是创建epoll对象,open函数就是添加一个fd到epoll中,close函数就是从epoll删除一个fd,netpoll函数就是从epoll wait得到所有发生事件的fd,并将每个fd对应的goroutine(用户态线程)通过链表返回。用epoll写过程序的人应该都能理解这份代码,没什么特别之处。

void runtime·netpollinit(void)
{
    epfd = runtime·epollcreate1(EPOLL_CLOEXEC);
    if(epfd >= 0)
        return;
    epfd = runtime·epollcreate(1024);
    if(epfd >= 0) {
        runtime·closeonexec(epfd);
        return;
    }
    runtime·printf("netpollinit: failed to create descriptor (%d)\n", -epfd);
    runtime·throw("netpollinit: failed to create descriptor");
}

runtime·netpollinit函数首先使用runtime·epollcreate1创建epoll实例,如果没有创建成功,就换用runtime·epollcreate再创建一次。这两个create函数分别等价于glibcepoll_create1epoll_create函数。只是因为Go语言并没有直接使用glibc,而是自己封装的系统调用,但功能是等价于glibc的。可以通过man手册查看这两个create的详细信息。

int32 runtime·netpollopen(uintptr fd, PollDesc *pd)
{
    EpollEvent ev;
    int32 res;
    ev.events = EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET;
    ev.data = (uint64)pd;
    res = runtime·epollctl(epfd, EPOLL_CTL_ADD, (int32)fd, &ev);
    return -res;
}

添加fd到epoll中的runtime·netpollopen函数可以看到每个fd一开始都关注了读写事件,并且采用的是边缘触发,除此之外还关注了一个不常见的新事件EPOLLRDHUP,这个事件是在较新的内核版本添加的,目的是解决对端socket关闭,epoll本身并不能直接感知到这个关闭动作的问题。注意任何一个fd在添加到epoll中的时候就关注了EPOLLOUT事件的话,就立马产生一次写事件,这次事件可能是多余浪费的。

epoll操作的相关函数都会在事件驱动的抽象层中去调用,为什么需要这个抽象层呢?原因很简单,因为Go语言需要跑在不同的平台上,有Linux、Unix、Mac OS X和Windows等,所以需要靠事件驱动的抽象层来为网络库提供一致的接口,从而屏蔽事件驱动的具体平台依赖实现。runtime/netpoll.goc源文件就是整个事件驱动抽象层的实现,抽象层的核心数据结构是:

struct PollDesc
{
    PollDesc* link; // in pollcache, protected by pollcache.Lock
    Lock;           // protectes the following fields
    uintptr fd;
    bool    closing;
    uintptr seq;    // protects from stale timers and ready notifications
    G*  rg;         // G waiting for read or READY (binary semaphore)
    Timer   rt;     // read deadline timer (set if rt.fv != nil)
    int64   rd;     // read deadline
    G*  wg;         // the same for writes
    Timer   wt;
    int64   wd;
};

每个添加到epoll中的fd都对应了一个PollDesc结构实例,PollDesc维护了读写此fd的goroutine这一非常重要的信息。可以大胆的推测一下,网络IO读写操作的实现应该是:当在一个fd上读写遇到EAGAIN错误的时候,就将当前goroutine存储到这个fd对应的PollDesc中,同时将goroutine给park住,直到这个fd上再此发生了读写事件后,再将此goroutine给ready激活重新运行。事实上的实现大概也是这个样子的。

事件驱动抽象层主要干的事情就是将具体的事件驱动实现(比如: epoll)通过统一的接口封装成Go接口供net库使用,主要的接口也是:创建事件驱动实例、添加fd、删除fd、等待事件以及设置DeadLine。runtime_pollServerInit负责创建事件驱动实例,runtime_pollOpen将分配一个PollDesc实例和fd绑定起来,然后将fd添加到epoll中,runtime_pollClose就是将fd从epoll中删除,同时将删除的fd绑定的PollDesc实例删除,runtime_pollWait接口是至关重要的,这个接口一般是在非阻塞读写发生EAGAIN错误的时候调用,作用就是park当前读写的goroutine。

runtime中的epoll事件驱动抽象层其实在进入net库后,又被封装了一次,这一次封装从代码上看主要是为了方便在纯Go语言环境进行操作,net库中的这次封装实现在net/fd_poll_runtime.go文件中,主要是通过pollDesc对象来实现的:

type pollDesc struct {
    runtimeCtx uintptr
}

注意:此处的pollDesc对象不是上文提到的runtime中的PollDesc,相反此处pollDesc对象的runtimeCtx成员才是指向的runtime的PollDesc实例。pollDesc对象主要就是将runtime的事件驱动抽象层给再封装了一次,供网络fd对象使用。

var serverInit sync.Once
func (pd *pollDesc) Init(fd *netFD) error {
    serverInit.Do(runtime_pollServerInit)
    ctx, errno := runtime_pollOpen(uintptr(fd.sysfd))
    if errno != 0 {
        return syscall.Errno(errno)
    }
    pd.runtimeCtx = ctx
    return nil
}

pollDesc对象最需要关注的就是其Init方法,这个方法通过一个sync.Once变量来调用了runtime_pollServerInit函数,也就是创建epoll实例的函数。意思就是runtime_pollServerInit函数在整个进程生命周期内只会被调用一次,也就是只会创建一次epoll实例。epoll实例被创建后,会调用runtime_pollOpen函数将fd添加到epoll中。

网络编程中的所有socket fd都是通过netFD对象实现的,netFD是对网络IO操作的抽象,linux的实现在文件net/fd_unix.go中。netFD对象实现有自己的init方法,还有完成基本IO操作的ReadWrite方法,当然除了这三个方法以外,还有很多非常有用的方法供用户使用。

// Network file descriptor.
type netFD struct {
    // locking/lifetime of sysfd + serialize access to Read and Write methods
    fdmu fdMutex
    // immutable until Close
    sysfd       int
    family      int
    sotype      int
    isConnected bool
    net         string
    laddr       Addr
    raddr       Addr
    // wait server
    pd pollDesc
}

通过netFD对象的定义可以看到每个fd都关联了一个pollDesc实例,通过上文我们知道pollDesc对象最终是对epoll的封装。

func (fd *netFD) init() error {
    if err := fd.pd.Init(fd); err != nil {
        return err
    }
    return nil
}

netFD对象的init函数仅仅是调用了pollDesc实例的Init函数,作用就是将fd添加到epoll中,如果这个fd是第一个网络socket fd的话,这一次init还会担任创建epoll实例的任务。要知道在Go进程里,只会有一个epoll实例来管理所有的网络socket fd,这个epoll实例也就是在第一个网络socket fd被创建的时候所创建。

for {
    n, err = syscall.Read(int(fd.sysfd), p)
    if err != nil {
        n = 0
        if err == syscall.EAGAIN {
            if err = fd.pd.WaitRead(); err == nil {
                continue
            }
        }
    }
    err = chkReadErr(n, err, fd)
    break
}

上面代码段是从netFD的Read方法中摘取,重点关注这个for循环中的syscall.Read调用的错误处理。当有错误发生的时候,会检查这个错误是否是syscall.EAGAIN,如果是,则调用WaitRead将当前读这个fd的goroutine给park住,直到这个fd上的读事件再次发生为止。当这个socket上有新数据到来的时候,WaitRead调用返回,继续for循环的执行。这样的实现,就让调用netFD的Read的地方变成了同步“阻塞”方式编程,不再是异步非阻塞的编程方式了。netFD的Write方法和Read的实现原理是一样的,都是在碰到EAGAIN错误的时候将当前goroutine给park住直到socket再次可写为止。

本文只是将网络库的底层实现给大体上引导了一遍,知道底层代码大概实现在什么地方,方便结合源码深入理解。Go语言中的高并发、同步阻塞方式编程的关键其实是”goroutine和调度器”,针对网络IO的时候,我们需要知道EAGAIN这个非常关键的调度点,掌握了这个调度点,即使没有调度器,自己也可以在epoll的基础上配合协程等用户态线程实现网络IO操作的调度,达到同步阻塞编程的目的。

最后,为什么需要同步阻塞的方式编程?只有看多、写多了异步非阻塞代码的时候才能够深切体会到这个问题。真正的高大上绝对不是——“别人不会,我会;别人写不出来,我写得出来。”

总结:

所以归根结底go的网络socket实现调度还是依赖epoll,在某个gocontinue中 如果出现read或者write的阻塞,就将该gocoutinue给gopark休眠,后续在获取gocountinue的时候,就会用runtime·netpoll调用,后获取

findrunnable(void)  
{  
    G *gp;  
    P *p;  
    int32 i;  
top:  
    if(runtime·sched.gcwaiting) {  
        gcstopm();  
        goto top;  
    }  
    if(runtime·fingwait && runtime·fingwake && (gp = runtime·wakefing()) != nil)  
        runtime·ready(gp);  
    // local runq  
    gp = runqget(g->m->p);  
    if(gp)  
        return gp;  
    // global runq  
    if(runtime·sched.runqsize) {  
        runtime·lock(&runtime·sched.lock);  
        gp = globrunqget(g->m->p, 0);  
        runtime·unlock(&runtime·sched.lock);  
        if(gp)  
            return gp;  
    }  
    // poll network  
    gp = runtime·netpoll(false);  // non-blocking  
    if(gp) {  
        injectglist(gp->schedlink);  
        runtime·casgstatus(gp, Gwaiting, Grunnable);  
        return gp;  
    }

然后还有个很重要的全局属性
runtime·sched.nmspinning
该属性只要有一个m线程是自旋的,其他的都不能进行wakeup线程

wakep(void)  
{  
    // be conservative about spinning threads  
    if(!runtime·cas(&runtime·sched.nmspinning, 0, 1))//<span style="color:#ff0000;">为了防止多个gocontinue 都来唤起线程,如果有正在自旋等待任务的</span>  
<span style="color:#ff0000;">                                                               就不需要建立了,该线程回去获取任务执行</span>  
        return;  
    startm(nil, true);  
}  

什么情况下会增加自旋呢,一共两处位置

static void  
handoffp(P *p)  
{  
    // if it has local work, start it straight away  
    if(p->runqhead != p->runqtail || runtime·sched.runqsize) {  
        startm(p, false);  
        return;  
    }  
    // no local work, check that there are no spinning/idle M's,  
    // otherwise our help is not required  
    if(runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) == 0 &&  // TODO: fast atomic  
        <strong><span style="color:#ff0000;">runtime·cas(&runtime·sched.nmspinning, 0, 1)</span></strong>){  
        startm(p, true);  
        return;  
    } 

当系统调用时候将m暂停,重新让m挂上P的时候,如果自己队列没有任务就增加自旋,意味着别的gocoinue如果要起线程此刻退出,就不需要再起线程了,该线程会去获取任务执行。

findrunnable(void)  
{  
    G *gp;  
    P *p;  
    int32 i;  
top:  
    if(runtime·sched.gcwaiting) {  
        gcstopm();  
        goto top;  
    }  
    if(runtime·fingwait && runtime·fingwake && (gp = runtime·wakefing()) != nil)  
        runtime·ready(gp);  
    // local runq  
    gp = runqget(g->m->p);  
    if(gp)  
        return gp;  
    // global runq  
    if(runtime·sched.runqsize) {  
        runtime·lock(&runtime·sched.lock);  
        gp = globrunqget(g->m->p, 0);  
        runtime·unlock(&runtime·sched.lock);  
        if(gp)  
            return gp;  
    }  
    // poll network  
    gp = runtime·netpoll(false);  // non-blocking  
    if(gp) {  
        injectglist(gp->schedlink);  
        runtime·casgstatus(gp, Gwaiting, Grunnable);  
        return gp;  
    }  
    // If number of spinning M's >= number of busy P's, block.  
    // This is necessary to prevent excessive CPU consumption  
    // when GOMAXPROCS>>1 but the program parallelism is low.  
    if(!g->m->spinning && 2 * runtime·atomicload(&runtime·sched.nmspinning) >= runtime·gomaxprocs - runtime·atomicload(&runtime·sched.npidle))  // TODO: fast atomic  
        goto stop;  
    if(!g->m->spinning) {  
        <span style="color:#ff0000;"><strong>g->m->spinning = true;  
        runtime·xadd(&runtime·sched.nmspinning, 1);</strong></span>  
    }  

当自己没任务,全局没任务,网络epoll没任务就把自己自旋起来,让人不用建立线程了,我就是空闲的,此刻接下来他就回去偷任务执行

然后还有个点我们经常看到g = getg(),却找不到定义的g在哪,其实这个函数调用就是m线程结构里面的g0,每次建立线程,会自定义分配堆栈空间,所以该g也建立了空间,获取的g就是线程的g,这样可以关联获取p等,和mlag(g)的continue不一样,其他的是业务gocontinue建立的g

go 内存模型引自:http://studygolang.com/articles/1853

需要注意以下几点:

//   
sp := (*slice)(unsafe.Pointer(&h_spans))  
sp.array = unsafe.Pointer(h.spans)  
sp.len = int(spans_size / ptrSize)  
sp.cap = int(spans_size / ptrSize)  
h_spans的数组表示span分布情况,健值pageid做索引,比如申请的span分了4页,那健值1-4全部都是这个span
[cpp] view plain copy
func mCache_Refill(c *mcache, sizeclass int32) *mspan {  
s := c.alloc[sizeclass]  
if s != &emptymspan {  
s.incache = false //   
}  
!31  
学习笔记.第五版.下册  
// s = mCentral_CacheSpan(&mheap_.central[sizeclass].mcentral)  
c.alloc[sizeclass] = s  
return s  
}  

此处给线程分配cache可以发现s的incache属性标注该s有没有被cache占用,先置我false,申请后设置true,因为该span可能因为满了被新的
span代替

type mcentral struct {  
nonempty mspan //   
empty mspan //   
}  
func mCentral_CacheSpan(c *mcentral) *mspan {  
sg := mheap_.sweepgen  
retry:  
for s = c.nonempty.next; s != &c.nonempty; s = s.next {  
if s.sweepgen == sg-2 && cas(&s.sweepgen, sg-2, sg-1) {  
mSpanList_Remove(s)  
mSpanList_InsertBack(&c.empty, s)  
mSpan_Sweep(s, true)  
goto havespan  
}  
if s.sweepgen == sg-1 {  
continue  
}  
mSpanList_Remove(s)  
mSpanList_InsertBack(&c.empty, s)  
goto havespan  
}  
for s = c.empty.next; s != &c.empty; s = s.next {  
if s.sweepgen == sg-2 && cas(&s.sweepgen, sg-2, sg-1) {  
mSpanList_Remove(s)  
mSpanList_InsertBack(&c.empty, s)  
mSpan_Sweep(s, true)  
if s.freelist.ptr() != nil {  
goto havespan  
}  
goto retry  
}  
if s.sweepgen == sg-1 {  
continue  
}  
break  
}  
s = mCentral_Grow(c)  
mSpanList_InsertBack(&c.empty, s)  
havespan:  
s.incache = true  

新版本实现已经变了,但是这个版本比较容易理解,
先从noempty数组里面找,因为noempty代表该链表下的span有空的,noempty一般是在释放freemcache的阶段将引用的span释放后放到nomempty数组的

noempty如果没有的话,就从empty里面找,因为有些mcache引用span的一部分,但是剩下的不用是浪费,所以他从empty里面重新找到可以用的span分配给新的macache,然后标记incache为true

刚开始阶段noempty和empty都没数据,就需要mcentral_grow去申请,然后申请的span会整个挂到empty后给cache使用。

当下一次来的时候就有可能从empty链表里把这个span找出来,mcentral_grow会把申请的span刮分,也就是会生成s->freelist链表从null进行分配值,同时h_spans赋值

mSpan_Sweep(s *mspan, preserve bool)进行内存回收,当perserve=true的时候,不进行内存合并直接返回供cache使用

如果是false时候会继续往下执行,检查span的object是否全部为空已经释放,如果是放到heap的free链表中,并且可以进行多个span的合并

合并的时候h_spans数据就发挥了作用。

golang的垃圾回收采用的是 标记-清理(Mark-and-Sweep) 算法

就是先标记出需要回收的内存对象快,然后在清理掉;

在这里不介绍标记和清理的具体策略,只介绍 GC过程是怎么调度的以及stw相关

这个算法,会导致 stw (stop the world)的问题,中断用户逻辑

触发GC机制

  1. 在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍,若是则触发(主GC线程为当前M)

  2. 监控线程发现上次GC的时间已经超过两分钟了,触发;将一个G任务放到全局G队列中去。(主GC线程为执行这个G任务的M)

每当触发的时候,在主GC线程中就会走如下的GC流程:

  1. stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止

  2. 标记:分配gc标记任务,唤醒 gcproc个 M(就是第一步休眠的那些),分别做这个,直到所有的M都做完,才结束;并且所有M再次进入休眠

  3. 清理:有一个单独的goroutine去清理已经标记的内存对象快

  4. start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数)

对于上面的三个步骤,分别解释:

stop the world:

  1. 设置gcwaiting=1,这个在每一个G任务之前会检查一次这个状态,如是,则会将当前M休眠;

  2. 如果这个M里面正在运行一个长时间的G任务,咋办呢,难道会等待这个G任务自己切换吗?这样的话可要等10ms啊,不能等!坚决不能等!
    所以会主动发出抢占标记(类似于上一篇),让当前G任务中断,再运行下一个G任务的时候,就会走到第1步

  3. 一直等待所有的M进入休眠,此时所有的业务逻辑代码都停止

标记:

  1. 根据gcproc的个数,分配成gcproc任务段;唤醒gcproc-1个M来执行(当前M也算一个)

  2. 对于一个M,唤醒前设置它的helpgc标记,唤醒之后这个M会立马判断这个标记,如是,则开始做分配给自己的标记任务,如果先做完了,就会从别的M里面找一些来做

  3. 等每一个M都做完,会再次进入休眠

清理:

  1. 通过设置参数,可以以一个单独goroutine  运行,这个功能是在1.3版本之后增加的,这样的话就直接到下一步了,清理过程不是stw的

  2. 也可以串行的在主GC线程执行;这样的话则清理过程也是stw的,

start the world:

  1. 设置gcwaiting=0

  2. 唤醒P个M来继续做G任务(此时没有helpgc标记),业务逻辑代码开始

综上:

是基于1.4版本的,GC过程在标记过程是(STW)的

在1.5版本里面对GC做了很大的优化;采用三色标记,将标记过程细化成三段,只有前后的两段是stw的;极大地缩短了gc的stw时间

其中重点解释下goparkunlock的用法,很多说gopark是将线程休眠,其实gopark是将gocouniue这个协程从队列中排除,然后重新调度,此刻该gocoutinue无法执行所以认为是休眠状态,他的用法就是需要有地方将gocontinue存下来,无论是网络模型还是垃圾回收的

func backgroundgc() {
       bggc.g = getg()
       for {
              gc(gcBackgroundMode)
              lock(&bggc.lock)
              bggc.working = 0
              goparkunlock(&bggc.lock, "Concurrent GC wait", traceEvGoBlock, 1)
       }
}
均为如此,bggc,g将gocontinue存下来,然后从队列中摘掉调度,后续
else if bggc.working == 0 {
       bggc.working = 1
       readied = true
       ready(bggc.g, 0)
}

利用ready唤醒

parforsetup(work.markfor, useOneP, uint32(_RootCount+local_allglen), false, markroot)
parfordo(work.markfor)

gc的垃圾回收函数里会同时启动几个线程进行标记,记住这个用法,setup是进行分配,分配后,parfordo是需要明确写明才会触发分配,

也就是说setup分配了线程休眠,但是这批线程等待唤醒,唤醒后需要手动执行parfordo

首先:

func stopm() {
       _g_ := getg()
       if _g_.m.locks != 0 {
              throw("stopm holding locks")
       }
       if _g_.m.p != 0 {
              throw("stopm holding p")
       }
       if _g_.m.spinning {
              _g_.m.spinning = false
              xadd(&sched.nmspinning, -1)
       }
retry:
       lock(&sched.lock)
       mput(_g_.m)
       unlock(&sched.lock)
       notesleep(&_g_.m.park)
       noteclear(&_g_.m.park)
       if _g_.m.helpgc != 0 {
              gchelper()
              _g_.m.helpgc = 0
              _g_.m.mcache = nil
              _g_.m.p = 0
              goto retry
       }
       acquirep(_g_.m.nextp.ptr())
       _g_.m.nextp = 0
}

线程休眠,但是如果线程被唤醒检查m.helpgc标志,是需要标记用的进入gcheper函数

其中函数里就会调用,所以是需要工作的线程显实调用

parfordo(work.markfor)
parforsetup(work.markfor, work.nproc, uint32(_RootCount+allglen), false, markroot)
if work.nproc > 1 {
      noteclear(&work.alldone)
      helpgc(int32(work.nproc))
}

而上面是进行parf的初始化,通过helpgc标记哪些线程需要进行标记。

关于线程休眠notewakeup与notesleep唤起linux用的以下函数,当然window平台可能没有该函数futex函数

func futexsleep(addr *uint32, val uint32, ns int64) {
       var ts timespec
       // Some Linux kernels have a bug where futex of
       // FUTEX_WAIT returns an internal error code
       // as an errno.  Libpthread ignores the return value
       // here, and so can we: as it says a few lines up,
       // spurious wakeups are allowed.
       if ns < 0 {
              futex(unsafe.Pointer(addr), _FUTEX_WAIT, val, nil, nil, 0)
              return
       }
       // It's difficult to live within the no-split stack limits here.
       // On ARM and 386, a 64-bit divide invokes a general software routine
       // that needs more stack than we can afford. So we use timediv instead.
       // But on real 64-bit systems, where words are larger but the stack limit
       // is not, even timediv is too heavy, and we really need to use just an
       // ordinary machine instruction.
       if ptrSize == 8 {
              ts.set_sec(ns / 1000000000)
              ts.set_nsec(int32(ns % 1000000000))
       } else {
              ts.tv_nsec = 0
              ts.set_sec(int64(timediv(ns, 1000000000, (*int32)(unsafe.Pointer(&ts.tv_nsec)))))
       }
       futex(unsafe.Pointer(addr), _FUTEX_WAIT, val, unsafe.Pointer(&ts), nil, 0)
}
// If any procs are sleeping on addr, wake up at most cnt.
//go:nosplit
func futexwakeup(addr *uint32, cnt uint32) {
       ret := futex(unsafe.Pointer(addr), _FUTEX_WAKE, cnt, nil, nil, 0)
       if ret >= 0 {
              return
       }
       // I don't know that futex wakeup can return
       // EAGAIN or EINTR, but if it does, it would be
       // safe to loop and call futex again.
       systemstack(func() {
              print("futexwakeup addr=", addr, " returned ", ret, "\n")
       })
       *(*int32)(unsafe.Pointer(uintptr(0x1006))) = 0x1006
}

抢占函数preemptone,当进行go continue进行堆栈newstack分配的时候(go里面的每个函数原则上都会检查,但是go里面的每个函数都会共用go开始申请的堆栈),

抢占后go被放入全局队列,然后重新schedule

handoffp(releasep())  系统调用会触发,releasep就是将p与m解开,m继续执行比如系统调用,hadoffp就是p有任务就重新进行分配线程执行,如果p没有任务可以执行,就放到空闲队列中

go的调度无非离不开上面几个函数

最近再看go的垃圾回收,然后以下几篇文章对我感触很大:

go语言内幕

// Create object file, write header.

其中Writeobjdirect会生成.o格式的目标文件,并且以“”go object“”为前缀,后续读取根据这个前缀判断是否是可执行文件。

然后可以根据

loadlib  ->  objfile  -> ldobj  ->ldobjfile 的链路来加载可执行文件,然后通过
symtab() 将符号表读出,加载到
ype _funcstruct{
entry  uintptr// start pc
nameoffint32  // function name
args  int32// in/out args size
frameint32// legacy frame size; use pcsp if possible
pcsp      int32
pcfile    int32
pcln      int32
npcdata  int32
nfuncdataint32
}

结构中,这样通过firstmoudule的全局变量,可以获取所有的符号表信息。

来源:Go语言社区

猜你喜欢

转载自blog.csdn.net/qun_y/article/details/89279861
今日推荐