關於如何使用go語言實現新進程的創建和進程間通信,我在網上找了不少的資料,但是始終未能發現讓自己滿意的答案,因此我打算自己來分析這部分源代碼,然后善加利用,並且分享給大家,期望大家能從中獲得啟發。
首先我們來看一段代碼
proc, _ := os.StartProcess(name, args, attr)
if err != nil {
fmt.Println(err)
}
_, err = proc.Wait()
if err != nil {
fmt.Println(err)
}
我們來看看這個os.StartProcess里面到底做了什么東西? 而 proc.Wait()又做了什么?跟我一起深入進去吧。
// StartProcess starts a new process with the program, arguments and attributes
// specified by name, argv and attr.
//
// StartProcess is a low-level interface. The os/exec package provides
// higher-level interfaces.
//
// If there is an error, it will be of type *PathError.
func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) {
return startProcess(name, argv, attr)
}
注釋是說,這個函數依照提供三個參數來實現開啟新進程的操作,它是一個低級接口,而os/exec包裝提供高級接口。如果這里出現報錯,應該會是一個指針型路徑錯誤。
下一步我們探究startProcess是什么?
func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) {
sysattr := &syscall.ProcAttr{
Dir: attr.Dir,
Env: attr.Env,
Sys: attr.Sys,
}
for _, f := range attr.Files {
sysattr.Files = append(sysattr.Files, f.Fd())
}
pid, h, e := syscall.StartProcess(name, argv, sysattr)
if e != nil {
return nil, &PathError{"fork/exec", name, e}
}
return newProcess(pid, h), nil
}
首先我們看到sysattr被賦予一個 &syscall.ProcAttr指針,這個syscall里面的ProcAttr是什么結構呢,要先理解它,這樣有助於理解我們利用它來啟動后面的syscall
// ProcAttr holds the attributes that will be applied to a new process
// started by StartProcess.
type ProcAttr struct {
// If Dir is non-empty, the child changes into the directory before
// creating the process.
Dir string
// If Env is non-nil, it gives the environment variables for the
// new process in the form returned by Environ.
// If it is nil, the result of Environ will be used.
Env []string
// Files specifies the open files inherited by the new process. The
// first three entries correspond to standard input, standard output, and
// standard error. An implementation may support additional entries,
// depending on the underlying operating system. A nil entry corresponds
// to that file being closed when the process starts.
Files []*File
// Operating system-specific process creation attributes.
// Note that setting this field means that your program
// may not execute properly or even compile on some
// operating systems.
Sys *syscall.SysProcAttr
}
第一句簡單明了,說明了ProcAttr結構中包含了我們啟動進程過程中使用的多項屬性值,
1)Dir是目錄的意思,相當於新進程的工作目錄,如果配置了就會跳轉目錄。
2)Env是指新的進程的環境變量列表。
3)Files前三項對應標准輸入,標准輸出和標准錯誤輸出。每個實現可以支持其他條目,如果傳入的條目是nil,該進程啟動時,file就是關閉的。
4)最后一個*syscall.SysProcAttr就是系統屬性,不過作者也提醒道有些參數在跨平台過程中有可能不起作用。
下面我們看下*syscall.SysProcAttr結構。
type SysProcAttr struct {
Chroot string // Chroot.
Credential *Credential // Credential.
Ptrace bool // Enable tracing.
Setsid bool // Create session.
Setpgid bool // Set process group ID to new pid (SYSV setpgrp)
Setctty bool // Set controlling terminal to fd 0
Noctty bool // Detach fd 0 from controlling terminal
}
// Credential holds user and group identities to be assumed
// by a child process started by StartProcess.
type Credential struct {
Uid uint32 // User ID.
Gid uint32 // Group ID.
Groups []uint32 // Supplementary group IDs.
}
可以看到這里面所涉及到的屬性。(部分屬性跨平台不起作用)
1)Chroot
2) Credential包括uid\gid\groups設定
3)一些bool屬性,參與設定新進程的使用過程。
Ptrace 是否允許tracing
Setsid 是否開啟sid
Setpgid 是否設定組id給新進程
Setctty 是否可以使用終端訪問
Noctty 將終端和fd0 進行分離。
OK,現在我們了解了這么多之后,還是誰去看看前面的代碼吧。如下:
func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) {
sysattr := &syscall.ProcAttr{
Dir: attr.Dir,
Env: attr.Env,
Sys: attr.Sys,
}
for _, f := range attr.Files {
sysattr.Files = append(sysattr.Files, f.Fd())
}
pid, h, e := syscall.StartProcess(name, argv, sysattr)
if e != nil {
return nil, &PathError{"fork/exec", name, e}
}
return newProcess(pid, h), nil
}
繼續看startProcess
sysattr := &syscall.ProcAttr{
Dir: attr.Dir,
Env: attr.Env,
Sys: attr.Sys,
}
Dir工作目錄,Env環境變量、Sys 內容被賦予了sysattr 。
for _, f := range attr.Files {
sysattr.Files = append(sysattr.Files, f.Fd())
}
文件Files屬性被安排加入到sysattr中,這樣我們就把attr *ProcAttr參數的整體內容都賦予了sysattr ,下面看如何利用這個sysattr
pid, h, e := syscall.StartProcess(name, argv, sysattr) sysattr作為第三項參數傳入了新的
syscall.StartProcess(name, argv, sysattr)
注意:這里我們注意到一個問題,看看我們期初的代碼
proc, _ := os.StartProcess(name, args, attr)
if err != nil {
fmt.Println(err)
}
這一行代碼和我們的期初的代碼是多么相像啊,於是我們明白調用os的StartProcess就是調用syscall.StartProcess,因此我們明白,syscall.StartProcess屬於底層調用。os.StartProcess是上層調用。os.StartProces只是在syscall.StartProcess外面包裝了一層而已,因此,我們明白,當我們想新創建一個進程的時候,只要參數都已經輸入完畢,我們既可以使用os.StartProcess來實現,也可以使用syscall.StartProcess來實現。只不過需要注意的是,兩者返回的對象不相同。
怎么個不相同呢?
我們看到了os.StartProcess 返回的是return newProcess(pid, h), nil, 而
syscall.StartProcess返回的是pid, h, e
也就是說os.StartProcess 返回的是syscall.StartProcess返回值對pid和h的包裝的結果。
// Process stores the information about a process created by StartProcess.
type Process struct {
Pid int
handle uintptr
isdone uint32 // process has been successfully waited on, non zero if true
}
func newProcess(pid int, handle uintptr) *Process {
p := &Process{Pid: pid, handle: handle}
runtime.SetFinalizer(p, (*Process).Release)
return p
}
通過觀察這個包裝的過程我們明白,之所以返回這個結果的目的是為了處理一些程序在進行時過程中的問題。下面我們就得了解下程序運行時的概念。
runtime.SetFinalizer(p, (*Process).Release)這一行在做什么呢?
這部分就是難點了,如果理解了這部分就會了解程序為什么包裝了這一層,它的目的何在。
下面則是一大段英文。我門來試着理解一下。該段英文引用自 malloc.go.
// SetFinalizer sets the finalizer associated with x to f.
// When the garbage collector finds an unreachable block
// with an associated finalizer, it clears the association and runs
// f(x) in a separate goroutine. This makes x reachable again, but
// now without an associated finalizer. Assuming that SetFinalizer
// is not called again, the next time the garbage collector sees
// that x is unreachable, it will free x.
//
// SetFinalizer(x, nil) clears any finalizer associated with x.
//
// The argument x must be a pointer to an object allocated by
// calling new or by taking the address of a composite literal.
// The argument f must be a function that takes a single argument
// to which x's type can be assigned, and can have arbitrary ignored return
// values. If either of these is not true, SetFinalizer aborts the
// program.
//
// Finalizers are run in dependency order: if A points at B, both have
// finalizers, and they are otherwise unreachable, only the finalizer
// for A runs; once A is freed, the finalizer for B can run.
// If a cyclic structure includes a block with a finalizer, that
// cycle is not guaranteed to be garbage collected and the finalizer
// is not guaranteed to run, because there is no ordering that
// respects the dependencies.
//
// The finalizer for x is scheduled to run at some arbitrary time after
// x becomes unreachable.
// There is no guarantee that finalizers will run before a program exits,
// so typically they are useful only for releasing non-memory resources
// associated with an object during a long-running program.
// For example, an os.File object could use a finalizer to close the
// associated operating system file descriptor when a program discards
// an os.File without calling Close, but it would be a mistake
// to depend on a finalizer to flush an in-memory I/O buffer such as a
// bufio.Writer, because the buffer would not be flushed at program exit.
//
// It is not guaranteed that a finalizer will run if the size of *x is
// zero bytes.
//
// It is not guaranteed that a finalizer will run for objects allocated
// in initializers for package-level variables. Such objects may be
// linker-allocated, not heap-allocated.
//
// A single goroutine runs all finalizers for a program, sequentially.
// If a finalizer must run for a long time, it should do so by starting
// a new goroutine.
我這里不是想照抄英文,只是為了文章的完整性,我們來看看它說了什么吧。下面是以上大段英文的簡單理解。在垃圾回收機制中,有兩種回收方式,其中一種是自動方式,另外一種是手動方式,自動方式是將長期未使用的數據自動回收掉,還有一種是必須手工強制回收的方式,而runtime.SetFinalizer屬於后者。
這個方法是有兩個參數,前面那個是一個變量,后面緊跟着一個這個變量的釋放函數。
因此我們明白runtime.SetFinalizer(p, (*Process).Release)就是當進程運行完畢之后,將這個進程釋放掉的意思。其中
(*Process).Release指的是這個函數
func (p *Process) Release() error {
return p.release()
}
func (p *Process) release() error {
// NOOP for Plan 9.
p.Pid = -1
// no need for a finalizer anymore
runtime.SetFinalizer(p, nil)
return nil
}
把該進程的進程號設定為-1 然后見p釋放掉。
這就不難理解為什么從os下面要包裝住syscall的原因了。我可以這么理解,os屬於系統級別的,因此包含了對垃圾處理的過程,而syscall屬於簡單系統調用未能實現這部分。
好了我們繼續理解,syscall的startProcess過程吧。
pid, h, e := syscall.StartProcess(name, argv, sysattr)
這個過程調用是調用了startProcess內部函數。
// StartProcess wraps ForkExec for package os.
func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle uintptr, err error) {
pid, err = startProcess(argv0, argv, attr)
return pid, 0, err
}
這就是startProcess內部函數。
func startProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) {
type forkRet struct {
pid int
err error
}
forkc := make(chan forkRet, 1)
go func() {
runtime.LockOSThread()
var ret forkRet
ret.pid, ret.err = forkExec(argv0, argv, attr)
// If fork fails there is nothing to wait for.
if ret.err != nil || ret.pid == 0 {
forkc <- ret
return
}
waitc := make(chan *waitErr, 1)
// Mark that the process is running.
procs.Lock()
if procs.waits == nil {
procs.waits = make(map[int]chan *waitErr)
}
procs.waits[ret.pid] = waitc
procs.Unlock()
forkc <- ret
var w waitErr
for w.err == nil && w.Pid != ret.pid {
w.err = Await(&w.Waitmsg)
}
waitc <- &w
close(waitc)
}()
ret := <-forkc
return ret.pid, ret.err
}
這個函數我們看一下。先定義了一個供返回的結構forkRet。然后創建了一個channel用於存放返回結構體。然后啟動一個攜程走fork的過程。這個過程如下:
1)先鎖住程序runtime.LockOSThread
2)執行 fork過程 ret.pid, ret.err = forkExec(argv0, argv, attr)
3)通過
var procs struct {
sync.Mutex
waits map[int]chan *waitErr
}
這樣一個結構加鎖,然后將獲得的pid作為key存放到一個waits 的map里面,然后解鎖。
4)將結果傳到go協程之外。 forkc <- ret
5)單獨處理waitErr流程,await是用來等待進程執行完畢之后關閉進程用的,相關的代碼在開始時就存在,以下是一開始時的代碼,也是通過系統調用syscall來實現的。雖然這部分也有很多底層代碼,但是我覺得還是需要讀者按照以上分析思路自己去探究一下,這里就不做分析了。
_, err = proc.Wait()
if err != nil {
fmt.Println(err)
}
至此,如何啟動一個新的進程的部分已經分析完畢,根據我的總結,我們發現在創建進程和銷毀進程使用進程的過程中,os包起到了包裝底層調用的作用。因此我們日常中無需刻意分析這么多源代碼,只需要明白os 進程部分提供給我們的api就可以了。鑒於此,下一篇文章則開始介紹如何啟動和使用進程,進行詳細描述。以便讓初學者完成這一部分的操作。