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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM