Golang 系统调用Syscall + RawSyscall

go源码中关于系统调用的定义如下:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

其中Syscall和RawSyscall区别在于Syscall开始和结束,分别调用了 runtime 中的进入系统调用和退出系统调用的函数,说明Syscall函数受调度器控制,不会造成系统堵塞,而RawSyscall函数没有调用runtime,因此可能会造成堵塞,一般我们使用Syscall就可以了,RawSyscall最好用在不会堵塞的情况下。

 

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

Syscall 的定义位于 src/syscall/asm_linux_amd64.s, 是用汇编写成的,封装了对linux底层的调用。接收4个参数,其中trap为中断信号,a1,a2,a3为底层调用函数对应的参数

举例说明:Go调用底层ioctl函数

trap中断类型传入syscall.SYS_IOCTL,SYS_IOCTL中断号表示调用linux底层ioctl函数
Syscall函数中剩下三个参数a1,a2,a3分别对应ioctl的三个参数。可以man命令查看linux ioctl函数参数,如下

int ioctl(int d, int request, ...);

第一个参数d指定一个由open/socket创建的文件描述符,即socket套接字
第二个参数request指定操作的类型,即对该文件描述符执行何种操作,设备相关的请求的代码
第三个参数为一块内存区域,通常依赖于request指定的操作类型

具体过程如下:
1 通过socket创建套接字
2 初始化struct ifconf与/或struct ifreq结构
3 调用ioctl函数,执行相应类型的SIO操作
4 获取返回至truct ifconf与/或struct ifreq结构中的相关信息

调用底层socket函数创建socket套接字,linux下用man命令查看socket函数用法

 

int socket(int domain, int type, int protocol);

其中domain为协议类型,type为套接字类型,protocol指定某个协议类型常值
domain的值有:

AF_INET IPv4协议
AF_INET6 Ipv6协议
AF_ROUTE 路由套接字
...

type的值有:

SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_RAW 原始套接字
...

protocol的值有:

IPPROTO_IP IP传输协议
IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
...

 

因此linux下调用socket生成套接字写法:

fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);

综上,转换成go语言中系统调用写法

fd, _, err := syscall.RawSyscall(syscall.SYS_SOCKET, syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP)

此时即生成了的socket套接字fd
我们传给int ioctl(int d, int request, …);函数作为第一个参数,第二个参数request操作的类型我们传入SIOCETHTOOL,获取ethtool信息
SIOCETHTOOL 在源码中宏定义为

#define SIOCETHTOOL     0x8946

第三个参数为struct ifreq结构内存地址
Struct ifreq结构如下:

Struct ifreq{
Char ifr_name[IFNAMSIZ];
Union{
    Struct  sockaddr  ifru_addr;
    Struct  sockaddr  ifru_dstaddr;
    Struct  sockaddr  ifru_broadaddr;
    Struct  sockaddr  ifru_netmask;
    Struct  sockaddr  ifru_hwaddr;
    Short  ifru_flags;
    Int     ifru_metric;
    Caddr_t ifru_data;
}ifr_ifru;
};
#define ifr_addr        ifr_ifru.ifru_addr
#define ifr_broadaddr   ifr_ifru.ifru_broadadd
#define ifr_hwaddr      ifr_ifru_hwaddr

 

 

综上,linux调用ioctl函数如下:

fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
ioctl(fd, SIOCETHTOOL, &ifreq);

go语言:

fd, _, err := syscall.RawSyscall(syscall.SYS_SOCKET, syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP)
if err != 0 {
        return syscall.Errno(err)
    }

_, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), SIOCETHTOOL, uintptr(unsafe.Pointer(&ifreq)))
if ep != 0 {
        return syscall.Errno(ep)
    }

 

 

和调度的交互

 

这里只列出Syscall和RawSyscall的源码:

//Syscall
TEXT ·Syscall(SB),NOSPLIT,$0-56
    CALL    runtime·entersyscall(SB)
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX    // syscall entry
    SYSCALL
    CMPQ    AX, $0xfffffffffffff001
    JLS    ok
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX
    MOVQ    AX, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET
ok:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET

 

//RawSyscall
TEXT ·RawSyscall(SB),NOSPLIT,$0-56
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX    // syscall entry
    SYSCALL
    CMPQ    AX, $0xfffffffffffff001
    JLS    ok1
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX
    MOVQ    AX, err+48(FP)
    RET
ok1:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    RET

 


Syscall和RawSyscall的实现比较典型,可以看到这两个实现最主要的区别在于:
Syscall在进入系统调用的时候,调用了runtime·entersyscall(SB)函数,在结束系统调用的时候调用了runtime·exitsyscall(SB)。做到进入和退出syscall的时候通知runtime。

这两个函数runtime·entersyscall和runtime·exitsyscall的实现在proc.go文件里面。其实在runtime·entersyscall函数里面,通知系统调用时候,是会将g的M的P解绑,P可以去继续获取M执行其余的g,这样提升效率。

所以如果用户代码使用了 RawSyscall 来做一些阻塞的系统调用,是有可能阻塞其它的 g 的。RawSyscall 只是为了在执行那些一定不会阻塞的系统调用时,能节省两次对 runtime 的函数调用消耗

runtime·entersyscall和runtime·exitsyscall这两个函数也是与scheduler交互的地方,后面会对源码进行分析

 

 

运行时支持

我们之前讲了很多次,Go语言runtime为了实现较高的并发度,对OS系统调用做了一些优化,主要就体现在runtime·entersyscall和入runtime·exitsyscall这两个函数上,它们的实现代码在src/pkg/runtime/proc.c之中,之前我们已经多次讨论过这个文件了。

在分析实现代前,我们先来看看函数的声明,位置在src/pkg/runtime/runtime.h中:

  1.  
    void runtime·entersyscall(void);
  2.  
    void runtime·entersyscallblock(void);
  3.  
    void runtime·exitsyscall(void);
  4.  
     

这里声明了3个函数,多了一个void runtime·entersyscallblock(void),在后面会分析它的功能和使用情况。

好了,现在来看实现代码。首先,我们很容易找到了void runtime·exitsyscall(void) 的实现,而另外两个却找不到,只是找到了两个与之向接近的函数定义,分别是:

  1.  
    void ·entersyscall(int dummy... }
  2.  
    void ·entersyscallblock(int dummy... }
  3.  
     

通过反汇编分析,我发现代码中所有对runtime·entersyscallruntime·entersyscallblock的调用最后都分别映射到了·entersyscall 和·entersyscallblock,也就是说前面两个函数分别是后面两个函数的别名。至于为什么这样实现,我没有找到相关的文档说明,但感觉应该主要是由于前后两组函数参数不同的关系 —— 函数调用本身是不需要传入参数的,而函数实现时,无中生有了一个dummy参数,其目的就是为了通过该参数指针(地址)方便定位调用者的PC和SP值。

runtime·entersyscall

好了,我们回到函数实现分析上来,看看进入系统调用前,runtime究竟都做了那些特别处理。下面将这个函数分成3段进行分析:

  • 首先,函数通过“pragma”将该函数声明为“NOSPLIT”,令其中的函数调用不触发栈扩展检查。

    刚进入函数,先禁止抢占,然后通过dummy参数获得调用者的SP和PC值(通过save函数保存到g->sched.spg->sched.pc),将其分别保存到groutine的syscallspsyscallpc字段,同时记录的字段还有syscallstacksyscallguard。这些字段的功能主要是使得垃圾收集器明确栈分析的边界 —— 对于正在进行系统调用的任务,只对其进入系统调用前的栈进行“标记-清除”。(实际上,Go语言的cgo机制也利用了entersyscall,因而cgo运行的代码不受垃圾收集机制管理。)

    然后,Goroutine的状态切换到Gsyscall状态。

  1.  
    #pragma textflag NOSPLIT
  2.  
    void
  3.  
    ·entersyscall(int32 dummy)
  4.  
    {
  5.  
    // Disable preemption because during this function g is in Gsyscall status,
  6.  
    // but can have inconsistent g->sched, do not let GC observe it.
  7.  
    m->locks++;
  8.  
     
  9.  
    // Leave SP around for GC and traceback.
  10.  
    save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
  11.  
    g->syscallsp g->sched.sp;
  12.  
    g->syscallpc g->sched.pc;
  13.  
    g->syscallstack g->stackbase;
  14.  
    g->syscallguard g->stackguard;
  15.  
    g->status Gsyscall;
  16.  
    if(g->syscallsp g->syscallguard-StackGuard || g->syscallstack g->syscallsp{
  17.  
    // runtime·printf("entersyscall inconsistent %p [%p,%p]\n",
  18.  
    // g->syscallsp, g->syscallguard-StackGuard, g->syscallstack);
  19.  
    runtime·throw("entersyscall");
  20.  
    }
  21.  
     
  • 下面的代码是唤醒runtime的后台监控线程sysmon,在之前讲调度器的时候说过,sysmon会监控所有执行syscall的线程M,一旦超过某个时间阈值,就将该M与对应的P解耦。
  1.  
    if(runtime·atomicload(&runtime·sched.sysmonwait)) // TODO: fast atomic
  2.  
    runtime·lock(&runtime·sched);
  3.  
    if(runtime·atomicload(&runtime·sched.sysmonwait)) {
  4.  
    runtime·atomicstore(&runtime·sched.sysmonwait0);
  5.  
    runtime·notewakeup(&runtime·sched.sysmonnote);
  6.  
    }
  7.  
    runtime·unlock(&runtime·sched);
  8.  
    save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
  9.  
    }
  10.  
     
  • 将M的mcache字段置空,并将P的m字段置空,将P的状态切换到Psyscall(注意,与G类似,P也存在若干状态的切换,Psyscall 和 Pgcstop都是其中的状态)。

    检查系统此刻是否需要进行“垃圾收集”,注意,syscall和gc是可以并行执行的。

    由于处于syscall状态的任务是不能进行栈分裂的,因此通过g->stackguard0 = StackPreempt使得后续操作时,一旦出现意外调用了栈分裂操作,都会进入 runtime的morestack函数并捕获到错误。最后别忘记重新使能任务抢占。

  1.  
    m->mcache nil;
  2.  
    m->p->nil;
  3.  
    runtime·atomicstore(&m->p->statusPsyscall);
  4.  
    if(runtime·sched.gcwaiting{
  5.  
    runtime·lock(&runtime·sched);
  6.  
    if (runtime·sched.stopwait && runtime·cas(&m->p->statusPsyscallPgcstop)) {
  7.  
    if(--runtime·sched.stopwait == 0)
  8.  
    runtime·notewakeup(&runtime·sched.stopnote);
  9.  
    }
  10.  
    runtime·unlock(&runtime·sched);
  11.  
    save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
  12.  
    }
  13.  
     
  14.  
    // Goroutines must not split stacks in Gsyscall status (it would corrupt g->sched).
  15.  
    // We set stackguard to StackPreempt so that first split stack check calls morestack.
  16.  
    // Morestack detects this case and throws.
  17.  
    g->stackguard0 StackPreempt;
  18.  
    m->locks--;
  19.  
    }
  20.  
     

这里提一个问题:为什么每次调用runtime·lock(&runtime.sched)runtime·unlock(&runtime·sched)后,都要重新调用save保存SP和PC值呢?

runtime·entersyscallblock

与 ·entersyscall函数不同,·entersyscallblock在一开始就认为当前执行的syscall 会执行一个相对比较长的时间,因此在进入该函数后,就进行了M和P的解耦操作,无需等待sysmon处理。

  • 该函数第一部分与·entersyscall函数类似:
  1.  
    #pragma textflag NOSPLIT
  2.  
    void
  3.  
    ·entersyscallblock(int32 dummy)
  4.  
    {
  5.  
    *p;
  6.  
     
  7.  
    m->locks++// see comment in entersyscall
  8.  
     
  9.  
    // Leave SP around for GC and traceback.
  10.  
    save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
  11.  
    g->syscallsp g->sched.sp;
  12.  
    g->syscallpc g->sched.pc;
  13.  
    g->syscallstack g->stackbase;
  14.  
    g->syscallguard g->stackguard;
  15.  
    g->status Gsyscall;
  16.  
    if(g->syscallsp g->syscallguard-StackGuard || g->syscallstack g->syscallsp{
  17.  
    // runtime·printf("entersyscall inconsistent %p [%p,%p]\n",
  18.  
    // g->syscallsp, g->syscallguard-StackGuard, g->syscallstack);
  19.  
    runtime·throw("entersyscallblock");
  20.  
    }
  21.  
     
  • 后面的部分就不太一样了,基本上就是直接将当前M与P解耦,P重新回到Pidle状态。
  1.  
    releasep();
  2.  
    handoffp(p);
  3.  
    if(g->isbackground// do not consider blocked scavenger for deadlock detection
  4.  
    incidlelocked(1);
  5.  
     
  6.  
    // Resave for traceback during blocked call.
  7.  
    save(runtime·getcallerpc(&dummy), runtime·getcallersp(&dummy));
  8.  
     
  9.  
    g->stackguard0 StackPreempt// see comment in entersyscall
  10.  
    m->locks--;
  11.  
    }
  12.  
     

前面说过,所有syscall包中的系统调用封装都只调用了runtime·entersyscall,那么runtime·entersyscallblock的使用场景是什么呢?

通过查找,发现Go1.2中,仅有的一处对runtime·entersyscallblock的使用来自bool runtime.notetsleepg(Note *n, int64 ns)中(当然,针对不同的OS平台有Futex和Sema两种不同的实现)。Note类型在Go中主要提供一种“通知-唤醒”机制,有点类似PThread中的“条件变量”。 为了实现高并发度,Go不但实现了线程级的阻塞,还提供了Goroutine级阻塞,使得一个运行的Goroutine也可以阻塞在一个Note上 —— 对应的P会解耦释放,因此系统整体并发性不会收到影响。

上述机制在runtime中多有使用,比如在“定时器”模块中 —— 后面有机会会详细介绍。

runtime·exitsyscall

该函数主要的功能是从syscall状态恢复,其结构比较清晰,主要分为两个步骤:

  • 尝试调用exitsyscallfast函数,假设对应的M与P没有完全解耦,那么该操作会重新将M与P绑定;否则尝试获取另一个空闲的P并与当前M绑定。如果绑定成功,返回true,否则返回false,留待runtime·exitsyscall做后续处理。 代码如下:
  1.  
    // The goroutine g exited its system call.
  2.  
    // Arrange for it to run on a cpu again.
  3.  
    // This is called only from the go syscall library, not
  4.  
    // from the low-level system calls used by the runtime.
  5.  
    #pragma textflag NOSPLIT
  6.  
    void
  7.  
    runtime·exitsyscall(void)
  8.  
    {
  9.  
    m->locks++// see comment in entersyscall
  10.  
     
  11.  
    if(g->isbackground// do not consider blocked scavenger for deadlock detection
  12.  
    incidlelocked(-1);
  13.  
     
  14.  
    if(exitsyscallfast()) {
  15.  
    // There's a cpu for us, so we can run.
  16.  
    m->p->syscalltick++;
  17.  
    g->status Grunning;
  18.  
    // Garbage collector isn't running (since we are),
  19.  
    // so okay to clear gcstack and gcsp.
  20.  
    g->syscallstack (uintptr)nil;
  21.  
    g->syscallsp (uintptr)nil;
  22.  
    m->locks--;
  23.  
    if(g->preempt{
  24.  
    // restore the preemption request in case we've cleared it in newstack
  25.  
    g->stackguard0 StackPreempt;
  26.  
    else {
  27.  
    // otherwise restore the real stackguard, we've spoiled it in entersyscall/entersyscallblock
  28.  
    g->stackguard0 g->stackguard;
  29.  
    }
  30.  
    return;
  31.  
    }
  32.  
     
  33.  
    m->locks--;
  34.  
     
  • 如果exitsyscallfast函数失败,则需要将当前的groutine放回到任务队列中等待被其他“M&P”调度执行,通过上一讲我们知道,类似的操作必须在g0的栈上执行,因此需要使用runtime.mcall来完成,代码如下:
  1.  
    // Call the scheduler.
  2.  
    runtime·mcall(exitsyscall0);
  3.  
     
  4.  
    // Scheduler returned, so we're allowed to run now.
  5.  
    // Delete the gcstack information that we left for
  6.  
    // the garbage collector during the system call.
  7.  
    // Must wait until now because until gosched returns
  8.  
    // we don't know for sure that the garbage collector
  9.  
    // is not running.
  10.  
    g->syscallstack (uintptr)nil;
  11.  
    g->syscallsp (uintptr)nil;
  12.  
    m->p->syscalltick++;
  13.  
    }
  14.  
     
  • 我们再仔细看看exitsyscall0的实现,和runtime的其他部分类似,M对于放弃执行总是有点不太情愿,所以首先还是会先看看有没有空闲的P,如果还是没有,只好将groutine放回全局任务队列中,如果当前M与G是绑定的,那M必须阻塞直到有空闲P可用才能被唤醒执行;如果M没有与G绑定,则M线程结束。 最后,当这个goroutine被再次调度执行时,会返回到runtime.mcall调用后的代码处,做一些后续的清理工作 —— 将syscallstacksyscallsp字段清楚以保证GC的正确执行;对P的syscalltick字段增1。

一点说明

Go语言之所以设计了M及P这两个概念,并对执行syscall的线程进行特别处理,适当进行M和P的解耦,主要是为了提高并发度,降低频繁、长时间的阻塞syscall带来的问题。但是必须意识到,这种机制本身也存在一定的开销,比如任务迁移可能影响CACHE、TLB的性能。

所以在实现中,并非所有的系统调用之前都会先调用·entersyscall

对于runtime中的一些底层syscall,比如所有的底层锁操作 —— 在Linux中使用的是Futex机制 —— 相应的Lock/Unlock操作都使用了底层系统调用,此时线程会直接调用syscall而不需要其他的操作,这样主要是保证底层代码的高效执行。

一些不容易造成执行线程阻塞的系统调用,在Go的syscall包中,通过RawSyscall进行封装,也不会调用runtime·entersyscallruntime·exitsyscall

 

posted on 2021-01-20 10:43  tycoon3  阅读(4755)  评论(0编辑  收藏  举报

导航