源碼剖析Golang如何fork一個進程
創(chuàng)建一個新進程分為兩個步驟,一個是fork系統(tǒng)調(diào)用,一個是execve 系統(tǒng)調(diào)用,fork調(diào)用會復用父進程的堆棧,而execve直接覆蓋當前進程的堆棧,并且將下一條執(zhí)行指令指向新的可執(zhí)行文件。
在分析源碼之前,我們先來看看golang fork一個子進程該如何寫。(嚴格的講是先fork再execve創(chuàng)建一個子進程)
cmd := exec.Command("/bin/sh")
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()上述代碼將fork一個子進程,然后子進程將會調(diào)用execve系統(tǒng)調(diào)用,使用新的可執(zhí)行文件/bin/sh代替當前子進程的程序。并且當前的標準輸入輸出也傳遞給了子進程。
我們將著重看下golang是如何創(chuàng)建和將父進程的文件描述符傳遞給子進程的。
cmd.Run() 會調(diào)用到cmd.Start 方法,里面有一段邏輯和標準輸入輸出流的傳遞相關,我們來看看。
// /usr/local/go/src/os/exec/exec.go:625
func (c *Cmd) Start() error {
......
childFiles := make([]*os.File, 0, 3+len(c.ExtraFiles))
// 創(chuàng)建子進程的stdin 標準輸入
stdin, err := c.childStdin()
if err != nil {
return err
}
childFiles = append(childFiles, stdin)
// 創(chuàng)建子進程的stdout 標準輸出
stdout, err := c.childStdout()
if err != nil {
return err
}
childFiles = append(childFiles, stdout)
// 創(chuàng)建子進程的stderr 標準錯誤輸出
stderr, err := c.childStderr(stdout)
if err != nil {
return err
}
// 此時childFiles 已經(jīng)包含了上述3個標準輸入輸出流
childFiles = append(childFiles, stderr)
childFiles = append(childFiles, c.ExtraFiles...)
env, err := c.environ()
if err != nil {
return err
}
// os.StartProcess 將會啟動一個子進程并從childFiles繼承父進程的放入其中的文件描述符
c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{
Dir: c.Dir,
Files: childFiles,
Env: env,
Sys: c.SysProcAttr,
})
.....
}
如上所述,cmd.Start 會分別調(diào)用childStdin,childStdout,childStderr創(chuàng)建用于子進程的標準輸入輸出。來看看其中一個childStdin實現(xiàn)原理,其余childStdout,childStderr 實現(xiàn)原理也是和它類似的。
// /usr/local/go/src/os/exec/exec.go:489
func (c *Cmd) childStdin() (*os.File, error) {
.....
pr, pw, err := os.Pipe()
if err != nil {
return nil, err
}
c.childIOFiles = append(c.childIOFiles, pr)
c.parentIOPipes = append(c.parentIOPipes, pw)
// pw 寫入的數(shù)據(jù) 來源于 c.Stdin 父進程會啟動一個協(xié)程復制c.Stdin 到 pw
c.goroutine = append(c.goroutine, func() error {
_, err := io.Copy(pw, c.Stdin)
if skipStdinCopyError(err) {
err = nil
}
if err1 := pw.Close(); err == nil {
err = err1
}
return err
})
....
return pr, nil
}
childStdin 實際上是創(chuàng)建了一個管道,管道有返回值 pw,pr , 由pw寫入的數(shù)據(jù)可以由pr進行讀取,w 寫入的數(shù)據(jù) 來源于 c.Stdin 父進程會啟動一個協(xié)程復制c.Stdin 到 pw ,而c.Stdin 在我們最開的演示代碼那里賦值為了標準輸入。
cmd := exec.Command("/bin/sh")
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()而pr 則返回由父進程通過os.StartProcess的childFiles 傳遞給了子進程,并作為子進程的標準輸入,當子進程啟動后將會從pr中獲取標準輸入終端的數(shù)據(jù)。
看到這里,你應該能明白了,子進程是如何獲取獲取父進程的終端信息的了,通過建立了一個管道,然后將管道的一端傳遞給了子進程便能讓父子進程進行通信了。
讓我們再回到創(chuàng)建進程的主流程上,剛剛僅僅是分析出了,父進程將會為子進程創(chuàng)建它自己的標準輸入輸出流,雖然是通過管道包裝的,但還沒詳細分析出os.StartProcess 方法究竟通過了哪些手段來讓父進程的文件描述符傳遞給子進程。
注意下,golang中 fork 和execve 創(chuàng)建子進程 的過程 被封裝成了一個統(tǒng)一的方法forkExec,它能夠控制子進程,只繼承特定的文件描述符,而對其他文件描述符則進行關閉。而內(nèi)核fork系統(tǒng)調(diào)用則是會對父進程的所有文件描述符進行復制,那么golang又是如何做到只繼承特定的文件描述符的呢?這個也是接下來分析的重點
接下來,讓我們深入os.StartProcess 方法,看看golang是如何辦到只繼承父進程通過childFiles傳遞過來的文件描述符進行fork和execve調(diào)用的。
os.StartProcess 底層會調(diào)用到 forkAndExecInChild1 方法,由于代碼比較長,我這里只列出了關鍵步驟,并對其進行了注釋。
func forkAndExecInChild1(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr *ProcAttr, sys *SysProcAttr, pipe int) (r1 uintptr, err1 Errno, p [2]int, locked bool) {
...
// fork 調(diào)用前 會將attr.Files 里的數(shù)據(jù)復制到fd數(shù)組,我們傳遞給子進程的是childFiles,當代碼執(zhí)行到這里的時候,childFiles已經(jīng)轉(zhuǎn)化成了文件描述符存到attr.Files了。nextfd是為了后續(xù)再進行復制文件描述符時,不會對子進程要用到的文件描述符進行覆蓋,會在接下來步驟1進行詳細說明
nextfd = len(attr.Files)
for i, ufd := range attr.Files {
if nextfd < int(ufd) {
nextfd = int(ufd)
}
fd[i] = int(ufd)
}
nextfd++
.....
// 這里便進行了fork調(diào)用創(chuàng)建新進程了,不過可以看到這里用的是clone系統(tǒng)調(diào)用,其實它和fork類似,不過區(qū)別在于clone系統(tǒng)調(diào)用可以通過flags指定新進程 對于 父進程的哪些屬性需要繼承,哪些屬性不需要繼承,比如子進程需要新的網(wǎng)絡命名空間,則需要指定flags為syscall.CLONE_NEWNS
r1, err1 = rawVforkSyscall(SYS_CLONE, flags, 0)
....
// 步驟1: 總之經(jīng)過上面clone系統(tǒng)調(diào)用,已經(jīng)產(chǎn)生了子進程了,下面兩個步驟都是子進程才會進行的步驟,父進程在上述clone系統(tǒng)調(diào)用后,通過判斷err1 != 0 || r1 != 0 便返回了。
// 這里將fd[i] < i 的文件描述符 通過dup 系統(tǒng)調(diào)用復制到了一個新的文件描述符,因為后續(xù)步驟2里我們需要將復制 fd[i] 到第i個文件描述符 ,如果fd[i] < i ,那么將會導致復制的fd[i] 是子進程已經(jīng)產(chǎn)生復制行為的文件描述符,而不是父進程真正傳遞過來的文件描述符,所以要通過nextfd將這樣的文件描述符復制到fd數(shù)組外,并且設置O_CLOEXEC,這樣在后續(xù)的execve系統(tǒng)調(diào)用后,將會對它進行自動關閉。
for i = 0; i < len(fd); i++ {
if fd[i] >= 0 && fd[i] < i {
....
_, _, err1 = RawSyscall(SYS_DUP3, uintptr(fd[i]), uintptr(nextfd), O_CLOEXEC)
if err1 != 0 {
goto childerror
}
fd[i] = nextfd
nextfd++
}
}
....
// 步驟2 : 遍歷fd 讓 子進程fd[i] 個文件描述符復制給第i個文件描述符 ,注意這里就沒有設置O_CLOEXEC了,因為這里的文件描述符我們希望execve后還存在
for i = 0; i < len(fd); i++ {
....
_, _, err1 = RawSyscall(SYS_DUP3, uintptr(fd[i]), uintptr(i), 0)
if err1 != 0 {
goto childerror
}
}
....
// 進行execve 系統(tǒng)調(diào)用
_, _, err1 = RawSyscall(SYS_EXECVE,
uintptr(unsafe.Pointer(argv0)),
uintptr(unsafe.Pointer(&argv[0])),
uintptr(unsafe.Pointer(&envv[0])))
}
可以看出,golang在execve前, 通過dup系統(tǒng)調(diào)用達到了繼承父進程文件描述符的目的,最終達到的效果是繼承attr.Files 參數(shù)里的文件描述符,期間由于dup的使用 產(chǎn)生的多余的文件描述符也標記為了O_CLOEXEC,在SYS_EXECVE 系統(tǒng)調(diào)用時,便會關閉掉。
但是僅僅看到這里,并不能說明golang會對attr.Files外的文件描述符也進行關閉,因為fork系統(tǒng)調(diào)用時,子進程會自動繼承父進程的所有文件描述符,這些繼承的文件描述符會在execve后自動關閉嗎? 答案是默認是會的。
golang的 os.open 函數(shù)底層會調(diào)用下面的代碼對文件進行打開操作,可以看到打開時固定設置了syscall.O_CLOEXEC flag,所以,子進程進行execve時變會自動對這些文件描述符進行關閉了。
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
setSticky := false
if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {
if _, err := Stat(name); IsNotExist(err) {
setSticky = true
}
}
var r int
for {
var e error
r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e == nil {監(jiān)聽的socket文件也是默認開啟了syscall.SOCK_NONBLOCK參數(shù)
// descriptor as nonblocking and close-on-exec.
func sysSocket(family, sotype, proto int) (int, error) {
s, err := socketFunc(family, sotype|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, proto)
if err != nil {
return -1, os.NewSyscallError("socket", err)
}
return s, nil
}到此這篇關于源碼剖析Golang如何fork一個進程的文章就介紹到這了,更多相關Golang fork進程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Go語言使用漏桶算法和令牌桶算法來實現(xiàn)API限流
為防止服務器被過多的請求壓垮,限流是一個至關重要的技術(shù)手段,下面我們就來看看如何使用漏桶算法和令牌桶算法來實現(xiàn) API 的限流吧2024-11-11
基于Golang實現(xiàn)Redis協(xié)議解析器
這篇文章主要為大家詳細介紹了如何通過GO語言編寫簡單的Redis協(xié)議解析器,文中的示例代碼講解詳細,對我們深入了解Go語言有一定的幫助,需要的可以參考一下2023-03-03
go語言goto語句跳轉(zhuǎn)到指定的標簽實現(xiàn)方法
這篇文章主要介紹了go語言goto語句跳轉(zhuǎn)到指定的標簽實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-05-05
go?goroutine實現(xiàn)素數(shù)統(tǒng)計的示例
這篇文章主要介紹了go?goroutine實現(xiàn)素數(shù)統(tǒng)計,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07

