GO中的defer會在當(dāng)前函數(shù)返回前執(zhí)行傳入的函數(shù),常用于關(guān)閉文件描述符,關(guān)閉鏈接及解鎖等操作。
成都創(chuàng)新互聯(lián)-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比淇濱網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式淇濱網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋淇濱地區(qū)。費(fèi)用合理售后完善,十年實(shí)體公司更值得信賴。
Go語言中使用defer時(shí)會遇到兩個(gè)常見問題:
接下來我們來詳細(xì)處理這兩個(gè)問題。
官方有段對defer的解釋:
這里我們先來一道經(jīng)典的面試題
你覺得這個(gè)會打印什么?
輸出結(jié)果:
這里是遵循先入后出的原則,同時(shí)保留當(dāng)前變量的值。
把這道題簡化一下:
輸出結(jié)果
上述代碼輸出似乎不符合預(yù)期,這個(gè)現(xiàn)象出現(xiàn)的原因是什么呢?經(jīng)過分析,我們發(fā)現(xiàn)調(diào)用defer關(guān)鍵字會立即拷貝函數(shù)中引用的外部參數(shù),所以fmt.Println(i)的這個(gè)i是在調(diào)用defer的時(shí)候就已經(jīng)賦值了,所以會直接打印1。
想要解決這個(gè)問題也很簡單,只需要向defer關(guān)鍵字傳入匿名函數(shù)
這里把一些垃圾回收使用的字段忽略了。
中間代碼生成階段cmd/compile/internal/gc/ssa.go會處理程序中的defer,該函數(shù)會根據(jù)條件不同,使用三種機(jī)制來處理該關(guān)鍵字
開放編碼、堆分配和棧分配是defer關(guān)鍵字的三種方法,而Go1.14加入的開放編碼,使得關(guān)鍵字開銷可以忽略不計(jì)。
call方法會為所有函數(shù)和方法調(diào)用生成中間代碼,工作內(nèi)容:
defer關(guān)鍵字在運(yùn)行時(shí)會調(diào)用deferproc,這個(gè)函數(shù)實(shí)現(xiàn)在src/runtime/panic.go里,接受兩個(gè)參數(shù):參數(shù)的大小和閉包所在的地址。
編譯器不僅將defer關(guān)鍵字轉(zhuǎn)成deferproc函數(shù),還會通過以下三種方式為所有調(diào)用defer的函數(shù)末尾插入deferreturn的函數(shù)調(diào)用
1、在cmd/compile/internal/gc/walk.go的walkstmt函數(shù)中,在遇到ODEFFER節(jié)點(diǎn)時(shí)會執(zhí)行Curfn.Func.SetHasDefer(true),設(shè)置當(dāng)前函數(shù)的hasdefer屬性
2、在ssa.go的buildssa會執(zhí)行s.hasdefer = fn.Func.HasDefer()更新hasdefer
3、在exit中會根據(jù)hasdefer在函數(shù)返回前插入deferreturn的函數(shù)調(diào)用
runtime.deferproc為defer創(chuàng)建了一個(gè)runtime._defer結(jié)構(gòu)體、設(shè)置它的函數(shù)指針fn、程序計(jì)數(shù)器pc和棧指針sp并將相關(guān)參數(shù)拷貝到相鄰的內(nèi)存空間中
最后調(diào)用的return0是唯一一個(gè)不會觸發(fā)延遲調(diào)用的函數(shù),可以避免deferreturn的遞歸調(diào)用。
newdefer的分配方式是從pool緩存池中獲取:
這三種方式取到的結(jié)構(gòu)體_defer,都會被添加到鏈表的隊(duì)頭,這也是為什么defer按照后進(jìn)先出的順序執(zhí)行。
deferreturn就是從鏈表的隊(duì)頭取出并調(diào)用jmpdefer傳入需要執(zhí)行的函數(shù)和參數(shù)。
該函數(shù)只有在所有延遲函數(shù)都執(zhí)行后才會返回。
如果我們能夠?qū)⒉糠纸Y(jié)構(gòu)體分配到棧上就可以節(jié)約內(nèi)存分配帶來的額外開銷。
在call函數(shù)中有在棧上分配
在運(yùn)行期間deferprocStack只需要設(shè)置一些未在編譯期間初始化的字段,就可以將棧上的_defer追加到函數(shù)的鏈表上。
除了分配的位置和堆的不同,其他的大致相同。
Go語言在1.14中通過開放編碼實(shí)現(xiàn)defer關(guān)鍵字,使用代碼內(nèi)聯(lián)優(yōu)化defer關(guān)鍵的額外開銷并引入函數(shù)數(shù)據(jù)funcdata管理panic的調(diào)用,該優(yōu)化可以將 defer 的調(diào)用開銷從 1.13 版本的 ~35ns 降低至 ~6ns 左右。
然而開放編碼作為一種優(yōu)化 defer 關(guān)鍵字的方法,它不是在所有的場景下都會開啟的,開放編碼只會在滿足以下的條件時(shí)啟用:
如果函數(shù)中defer關(guān)鍵字的數(shù)量多于8個(gè)或者defer處于循環(huán)中,那么就會禁用開放編碼優(yōu)化。
可以看到這里,判斷編譯參數(shù)不用-N,返回語句的數(shù)量和defer數(shù)量的乘積小于15,會啟用開放編碼優(yōu)化。
延遲比特deferBitsTemp和延遲記錄是使用開放編碼實(shí)現(xiàn)defer的兩個(gè)最重要的結(jié)構(gòu),一旦使用開放編碼,buildssa會在棧上初始化大小為8個(gè)比特的deferBits
延遲比特中的每一個(gè)比特位都表示該位對應(yīng)的defer關(guān)鍵字是否需要被執(zhí)行。延遲比特的作用就是標(biāo)記哪些defer關(guān)鍵字在函數(shù)中被執(zhí)行,這樣就能在函數(shù)返回時(shí)根據(jù)對應(yīng)的deferBits確定要執(zhí)行的函數(shù)。
而deferBits的大小為8比特,所以該優(yōu)化的條件就是defer的數(shù)量小于8.
而執(zhí)行延遲調(diào)用的時(shí)候仍在deferreturn
這里做了特殊的優(yōu)化,在runOpenDeferFrame執(zhí)行開放編碼延遲函數(shù)
1、從結(jié)構(gòu)體_defer讀取deferBits,執(zhí)行函數(shù)等信息
2、在循環(huán)中依次讀取執(zhí)行函數(shù)的地址和參數(shù)信息,并通過deferBits判斷是否要執(zhí)行
3、調(diào)用reflectcallSave執(zhí)行函數(shù)
1、新加入的defer放入隊(duì)頭,執(zhí)行defer時(shí)是從隊(duì)頭取函數(shù)調(diào)用,所以是后進(jìn)先出
2、通過判斷defer關(guān)鍵字、return數(shù)量來判斷是否開啟開放編碼優(yōu)化
3、調(diào)用deferproc函數(shù)創(chuàng)建新的延遲調(diào)用函數(shù)時(shí),會立即拷貝函數(shù)的參數(shù),函數(shù)的參數(shù)不會等到真正執(zhí)行時(shí)計(jì)算
再簡單不過了,給一個(gè)路徑給它,返回文件描述符,如果出現(xiàn)錯(cuò)誤就會返回一個(gè) *PathError。
這是一個(gè)只讀打開模式,實(shí)際上就是 os.OpenFile() 的快捷操作,它的原型如下:
libcontainer 是Docker中用于容器管理的包,它基于Go語言實(shí)現(xiàn),通過管理namespaces、cgroups、capabilities以及文件系統(tǒng)來進(jìn)行容器控制。你可以使用libcontainer創(chuàng)建容器,并對容器進(jìn)行生命周期管理。
容器是一個(gè)可管理的執(zhí)行環(huán)境,與主機(jī)系統(tǒng)共享內(nèi)核,可與系統(tǒng)中的其他容器進(jìn)行隔離。
在2013年Docker剛發(fā)布的時(shí)候,它是一款基于LXC的開源容器管理引擎。把LXC復(fù)雜的容器創(chuàng)建與使用方式簡化為Docker自己的一套命令體系。隨著Docker的不斷發(fā)展,它開始有了更為遠(yuǎn)大的目標(biāo),那就是反向定義容器的實(shí)現(xiàn)標(biāo)準(zhǔn),將底層實(shí)現(xiàn)都抽象化到libcontainer的接口。這就意味著,底層容器的實(shí)現(xiàn)方式變成了一種可變的方案,無論是使用namespace、cgroups技術(shù)抑或是使用systemd等其他方案,只要實(shí)現(xiàn)了libcontainer定義的一組接口,Docker都可以運(yùn)行。這也為Docker實(shí)現(xiàn)全面的跨平臺帶來了可能。
1.libcontainer 特性
目前版本的libcontainer,功能實(shí)現(xiàn)上涵蓋了包括namespaces使用、cgroups管理、Rootfs的配置啟動、默認(rèn)的Linux capability權(quán)限集、以及進(jìn)程運(yùn)行的環(huán)境變量配置。內(nèi)核版本最低要求為2.6,最好是3.8,這與內(nèi)核對namespace的支持有關(guān)。 目前除user namespace不完全支持以外,其他五個(gè)namespace都是默認(rèn)開啟的,通過clone系統(tǒng)調(diào)用進(jìn)行創(chuàng)建。
1.1 建立文件系統(tǒng)
文件系統(tǒng)方面,容器運(yùn)行需要rootfs。所有容器中要執(zhí)行的指令,都需要包含在rootfs(在Docker中指令包含在其上疊加的鏡像層也可以執(zhí)行)所有掛載在容器銷毀時(shí)都會被卸載,因?yàn)閙ount namespace會在容器銷毀時(shí)一同消失。為了容器可以正常執(zhí)行命令,以下文件系統(tǒng)必須在容器運(yùn)行時(shí)掛載到rootfs中。
當(dāng)容器的文件系統(tǒng)剛掛載完畢時(shí),/dev文件系統(tǒng)會被一系列設(shè)備節(jié)點(diǎn)所填充,所以rootfs不應(yīng)該管理/dev文件系統(tǒng)下的設(shè)備節(jié)點(diǎn),libcontainer會負(fù)責(zé)處理并正確啟動這些設(shè)備。設(shè)備及其權(quán)限模式如下。
容器支持偽終端TTY,當(dāng)用戶使用時(shí),就會建立/dev/console設(shè)備。其他終端支持設(shè)備,如/dev/ptmx則是宿主機(jī)的/dev/ptmx 鏈接。容器中指向宿主機(jī) /dev/null的IO也會被重定向到容器內(nèi)的 /dev/null設(shè)備。當(dāng)/proc掛載完成后,/dev/中與IO相關(guān)的鏈接也會建立,如下表。
pivot_root 則用于改變進(jìn)程的根目錄,這樣可以有效的將進(jìn)程控制在我們建立的rootfs中。如果rootfs是基于ramfs的(不支持pivot_root),那么會在mount時(shí)使用MS_MOVE標(biāo)志位加上chroot來頂替。 當(dāng)文件系統(tǒng)創(chuàng)建完畢后,umask權(quán)限被重新設(shè)置回0022。
1.2 資源管理
在《Docker背后的內(nèi)核知識:cgroups資源隔離》一文中已經(jīng)提到,Docker使用cgroups進(jìn)行資源管理與限制,包括設(shè)備、內(nèi)存、CPU、輸入輸出等。 目前除網(wǎng)絡(luò)外所有內(nèi)核支持的子系統(tǒng)都被加入到libcontainer的管理中,所以libcontainer使用cgroups原生支持的統(tǒng)計(jì)信息作為資源管理的監(jiān)控展示。 容器中運(yùn)行的第一個(gè)進(jìn)程init,必須在初始化開始前放置到指定的cgroup目錄中,這樣就能防止初始化完成后運(yùn)行的其他用戶指令逃逸出cgroups的控制。父子進(jìn)程的同步則通過管道來完成,在隨后的運(yùn)行時(shí)初始化中會進(jìn)行展開描述。
1.3 可配置的容器安全
容器安全一直是被廣泛探討的話題,使用namespace對進(jìn)程進(jìn)行隔離是容器安全的基礎(chǔ),遺憾的是,usernamespace由于設(shè)計(jì)上的復(fù)雜性,還沒有被libcontainer完全支持。 libcontainer目前可通過配置capabilities、SELinux、apparmor 以及seccomp進(jìn)行一定的安全防范,目前除seccomp以外都有一份默認(rèn)的配置項(xiàng)提供給用戶作為參考。 在本系列的后續(xù)文章中,我們將對容器安全進(jìn)行更深入的探討,敬請期待。
1.4 運(yùn)行時(shí)與初始化進(jìn)程
在容器創(chuàng)建過程中,父進(jìn)程需要與容器的init進(jìn)程進(jìn)行同步通信,通信的方式則通過向容器中傳入管道來實(shí)現(xiàn)。當(dāng)init啟動時(shí),他會等待管道內(nèi)傳入EOF信息,這就給父進(jìn)程完成初始化,建立uid/gid映射,并把新進(jìn)程放進(jìn)新建的cgroup一定的時(shí)間。 在libcontainer中運(yùn)行的應(yīng)用(進(jìn)程),應(yīng)該是事先靜態(tài)編譯完成的。libcontainer在容器中并不提供任何類似Unix init這樣的守護(hù)進(jìn)程,用戶提供的參數(shù)也是通過exec系統(tǒng)調(diào)用提供給用戶進(jìn)程。通常情況下容器中也沒有長進(jìn)程存在。 如果容器打開了偽終端,就會通過dup2把console作為容器的輸入輸出(STDIN, STDOUT, STDERR)對象。 除此之外,以下4個(gè)文件也會在容器運(yùn)行時(shí)自動生成。 * /etc/hosts * /etc/resolv.conf * /etc/hostname * /etc/localtime
1.5 在運(yùn)行著的容器中執(zhí)行新進(jìn)程
用戶也可以在運(yùn)行著的容器中執(zhí)行一條新的指令,就是我們熟悉的docker exec功能。同樣,執(zhí)行指令的二進(jìn)制文件需要包含在容器的rootfs之內(nèi)。 通過這種方式運(yùn)行起來的進(jìn)程會隨容器的狀態(tài)變化,如容器被暫停,進(jìn)程也隨之暫停,恢復(fù)也隨之恢復(fù)。當(dāng)容器進(jìn)程不存在時(shí),進(jìn)程就會被銷毀,重啟也不會恢復(fù)。
1.6 容器熱遷移(Checkpoint Restore)
目前l(fā)ibcontainer已經(jīng)集成了CRIU作為容器檢查點(diǎn)保存與恢復(fù)(通常也稱為熱遷移)的解決方案,應(yīng)該在不久之后就會被Docker使用。也就是說,通過libcontainer你已經(jīng)可以把一個(gè)正在運(yùn)行的進(jìn)程狀態(tài)保存到磁盤上,然后在本地或其他機(jī)器中重新恢復(fù)當(dāng)前的運(yùn)行狀態(tài)。這個(gè)功能主要帶來如下幾個(gè)好處。
服務(wù)器需要維護(hù)(如系統(tǒng)升級、重啟等)時(shí),通過熱遷移技術(shù)把容器轉(zhuǎn)移到別的服務(wù)器繼續(xù)運(yùn)行,應(yīng)用服務(wù)信息不會丟失。
對于初始化時(shí)間極長的應(yīng)用程序來說,容器熱遷移可以加快啟動時(shí)間,當(dāng)應(yīng)用啟動完成后就保存它的檢查點(diǎn)狀態(tài),下次要重啟時(shí)直接通過檢查點(diǎn)啟動即可。
在高性能計(jì)算的場景中,容器熱遷移可以保證運(yùn)行了許多天的計(jì)算結(jié)果不會丟失,只要周期性的進(jìn)行檢查點(diǎn)快照保存就可以了。
要使用這個(gè)功能,需要保證機(jī)器上已經(jīng)安裝了1.5.2或更高版本的criu工具。不同Linux發(fā)行版都有criu的安裝包,你也可以在CRIU官網(wǎng)上找到從源碼安裝的方法。我們將會在nsinit的使用中介紹容器熱遷移的使用方法。 CRIU(Checkpoint/Restore In Userspace)由OpenVZ項(xiàng)目于2005年發(fā)起,因?yàn)槠渖婕暗膬?nèi)核系統(tǒng)繁多、代碼多達(dá)數(shù)萬行,其復(fù)雜性與向后兼容性都阻礙著它進(jìn)入內(nèi)核主線,幾經(jīng)周折之后決定在用戶空間實(shí)現(xiàn),并在2012年被Linus加并入內(nèi)核主線,其后得以快速發(fā)展。 你可以在CRIU官網(wǎng)查看其原理,簡單描述起來可以分為兩部分,一是檢查點(diǎn)的保存,其中分為3步。
收集進(jìn)程與其子進(jìn)程構(gòu)成的樹,并凍結(jié)所有進(jìn)程。
收集任務(wù)(包括進(jìn)程和線程)使用的所有資源,并保存。
清理我們收集資源的相關(guān)寄生代碼,并與進(jìn)程分離。
第二部分自然是恢復(fù),分為4步。
讀取快照文件并解析出共享的資源,對多個(gè)進(jìn)程共享的資源優(yōu)先恢復(fù),其他資源則隨后需要時(shí)恢復(fù)。
使用fork恢復(fù)整個(gè)進(jìn)程樹,注意此時(shí)并不恢復(fù)線程,在第4步恢復(fù)。
恢復(fù)所有基礎(chǔ)任務(wù)(包括進(jìn)程和線程)資源,除了內(nèi)存映射、計(jì)時(shí)器、證書和線程。這一步主要打開文件、準(zhǔn)備namespace、創(chuàng)建socket連接等。
恢復(fù)進(jìn)程運(yùn)行的上下文環(huán)境,恢復(fù)剩下的其他資源,繼續(xù)運(yùn)行進(jìn)程。
至此,libcontainer的基本特性已經(jīng)預(yù)覽完畢,下面我們將從使用開始,一步步深入libcontainer的原理。
2. nsinit與libcontainer的使用
俗話說,了解一個(gè)工具最好的入門方式就是去使用它,nsinit就是一個(gè)為了方便不通過Docker就可以直接使用libcontainer而開發(fā)的命令行工具。它可以用于啟動一個(gè)容器或者在已有的容器中執(zhí)行命令。使用nsinit需要有 rootfs 以及相應(yīng)的配置文件。
2.1 nsinit的構(gòu)建
使用nsinit需要rootfs,最簡單最常用的是使用Docker busybox,相關(guān)配置文件則可以參考sample_configs目錄,主要配置的參數(shù)及其作用將在配置參數(shù)一節(jié)中介紹。拷貝一份命名為container.json文件到你rootfs所在目錄中,這份文件就包含了你對容器做的特定配置,包括運(yùn)行環(huán)境、網(wǎng)絡(luò)以及不同的權(quán)限。這份配置對容器中的所有進(jìn)程都會產(chǎn)生效果。 具體的構(gòu)建步驟在官方的README文檔中已經(jīng)給出,在此為了節(jié)省篇幅不再贅述。 最終編譯完成后生成nsinit二進(jìn)制文件,將這個(gè)指令加入到系統(tǒng)的環(huán)境變量,在busybox目錄下執(zhí)行如下命令,即可使用,需要root權(quán)限。 nsinit exec --tty --config container.json /bin/bash 執(zhí)行完成后會生成一個(gè)以容器ID命名的文件夾,上述命令沒有指定容器ID,默認(rèn)名為”nsinit”,在“nsinit”文件夾下會生成一個(gè)state.json文件,表示容器的狀態(tài),其中的內(nèi)容與配置參數(shù)中的內(nèi)容類似,展示容器的狀態(tài)。
2.2 nsinit的使用
目前nsinit定義了9個(gè)指令,使用nsinit -h就可以看到,對于每個(gè)單獨(dú)的指令使用--help就能獲得更詳細(xì)的使用參數(shù),如nsinit config --help。 nsinit這個(gè)命令行工具是通過cli.go實(shí)現(xiàn)的,cli.go封裝了命令行工具需要做的一些細(xì)節(jié),包括參數(shù)解析、命令執(zhí)行函數(shù)構(gòu)建等等,這就使得nsinit本身的代碼非常簡潔明了。具體的命令功能如下。
config:使用內(nèi)置的默認(rèn)參數(shù)加上執(zhí)行命令時(shí)用戶添加的部分參數(shù),生成一份容器可用的標(biāo)準(zhǔn)配置文件。
exec:啟動容器并執(zhí)行命令。除了一些共有的參數(shù)外,還有如下一些獨(dú)有的參數(shù)。
--tty,-t:為容器分配一個(gè)終端顯示輸出內(nèi)容。
--config:使用配置文件,后跟文件路徑。
--id:指定容器ID,默認(rèn)為nsinit。
--user,-u:指定用戶,默認(rèn)為“root”.
--cwd:指定當(dāng)前工作目錄。
--env:為進(jìn)程設(shè)置環(huán)境變量。
init:這是一個(gè)內(nèi)置的參數(shù),用戶并不能直接使用。這個(gè)命令是在容器內(nèi)部執(zhí)行,為容器進(jìn)行namespace初始化,并在完成初始化后執(zhí)行用戶指令。所以在代碼中,運(yùn)行nsinit exec后,傳入到容器中運(yùn)行的實(shí)際上是nsinit init,把用戶指令作為配置項(xiàng)傳入。
oom:展示容器的內(nèi)存超限通知。
pause/unpause:暫停/恢復(fù)容器中的進(jìn)程。
stats:顯示容器中的統(tǒng)計(jì)信息,主要包括cgroup和網(wǎng)絡(luò)。
state:展示容器狀態(tài),就是讀取state.json文件。
checkpoint:保存容器的檢查點(diǎn)快照并結(jié)束容器進(jìn)程。需要填--image-path參數(shù),后面是檢查點(diǎn)保存的快照文件路徑。完整的命令示例如下。 nsinit checkpoint --image-path=/tmp/criu
restore:從容器檢查點(diǎn)快照恢復(fù)容器進(jìn)程的運(yùn)行。參數(shù)同上。
總結(jié)起來,nsinit與Docker execdriver進(jìn)行的工作基本相同,所以在Docker的源碼中并不會涉及到nsinit包的調(diào)用,但是nsinit為libcontainer自身的調(diào)試和使用帶來了極大的便利。
3. 配置參數(shù)解析
no_pivot_root :這個(gè)參數(shù)表示用rootfs作為文件系統(tǒng)掛載點(diǎn),不單獨(dú)設(shè)置pivot_root。
parent_death_signal: 這個(gè)參數(shù)表示當(dāng)容器父進(jìn)程銷毀時(shí)發(fā)送給容器進(jìn)程的信號。
pivot_dir:在容器root目錄中指定一個(gè)目錄作為容器文件系統(tǒng)掛載點(diǎn)目錄。
rootfs:容器根目錄位置。
readonlyfs:設(shè)定容器根目錄為只讀。
mounts:設(shè)定額外的掛載,填充的信息包括原路徑,容器內(nèi)目的路徑,文件系統(tǒng)類型,掛載標(biāo)識位,掛載的數(shù)據(jù)大小和權(quán)限,最后設(shè)定共享掛載還是非共享掛載(獨(dú)立于mount_label的設(shè)定起作用)。
devices:設(shè)定在容器啟動時(shí)要創(chuàng)建的設(shè)備,填充的信息包括設(shè)備類型、容器內(nèi)設(shè)備路徑、設(shè)備塊號(major,minor)、cgroup文件權(quán)限、用戶編號、用戶組編號。
mount_label:設(shè)定共享掛載還是非共享掛載。
hostname:設(shè)定主機(jī)名。
namespaces:設(shè)定要加入的namespace,每個(gè)不同種類的namespace都可以指定,默認(rèn)與父進(jìn)程在同一個(gè)namespace中。
capabilities:設(shè)定在容器內(nèi)的進(jìn)程擁有的capabilities權(quán)限,所有沒加入此配置項(xiàng)的capabilities會被移除,即容器內(nèi)進(jìn)程失去該權(quán)限。
networks:初始化容器的網(wǎng)絡(luò)配置,包括類型(loopback、veth)、名稱、網(wǎng)橋、物理地址、IPV4地址及網(wǎng)關(guān)、IPV6地址及網(wǎng)關(guān)、Mtu大小、傳輸緩沖長度txqueuelen、Hairpin Mode設(shè)置以及宿主機(jī)設(shè)備名稱。
routes:配置路由表。
cgroups:配置cgroups資源限制參數(shù),使用的參數(shù)不多,主要包括允許的設(shè)備列表、內(nèi)存、交換區(qū)用量、CPU用量、塊設(shè)備訪問優(yōu)先級、應(yīng)用啟停等。
apparmor_profile:配置用于SELinux的apparmor文件。
process_label:同樣用于selinux的配置。
rlimits:最大文件打開數(shù)量,默認(rèn)與父進(jìn)程相同。
additional_groups:設(shè)定gid,添加同一用戶下的其他組。
uid_mappings:用于User namespace的uid映射。
gid_mappings:用戶User namespace的gid映射。
readonly_paths:在容器內(nèi)設(shè)定只讀部分的文件路徑。
MaskPaths:配置不使用的設(shè)備,通過綁定/dev/null進(jìn)行路徑掩蓋。
4. libcontainer實(shí)現(xiàn)原理
在Docker中,對容器管理的模塊為execdriver,目前Docker支持的容器管理方式有兩種,一種就是最初支持的LXC方式,另一種稱為native,即使用libcontainer進(jìn)行容器管理。在孫宏亮的《Docker源碼分析系列》中,Docker Deamon啟動過程中就會對execdriver進(jìn)行初始化,會根據(jù)驅(qū)動的名稱選擇使用的容器管理方式。 雖然在execdriver中只有LXC和native兩種選擇,但是native(即libcontainer)通過接口的方式定義了一系列容器管理的操作,包括處理容器的創(chuàng)建(Factory)、容器生命周期管理(Container)、進(jìn)程生命周期管理(Process)等一系列接口,相信如果Docker的熱潮一直像如今這般洶涌,那么不久的將來,Docker必將實(shí)現(xiàn)其全平臺通用的宏偉藍(lán)圖。本節(jié)也將從libcontainer的這些抽象對象開始講解,與你一同解開Docker容器管理之謎。在介紹抽象對象的具體實(shí)現(xiàn)過程中會與Docker execdriver聯(lián)系起來,讓你充分了解整個(gè)過程。
4.1 Factory 對象
Factory對象為容器創(chuàng)建和初始化工作提供了一組抽象接口,目前已經(jīng)具體實(shí)現(xiàn)的是Linux系統(tǒng)上的Factory對象。Factory抽象對象包含如下四個(gè)方法,我們將主要描述這四個(gè)方法的工作過程,涉及到具體實(shí)現(xiàn)方法則以LinuxFactory為例進(jìn)行講解。
Create():通過一個(gè)id和一份配置參數(shù)創(chuàng)建容器,返回一個(gè)運(yùn)行的進(jìn)程。容器的id由字母、數(shù)字和下劃線構(gòu)成,長度范圍為1~1024。容器ID為每個(gè)容器獨(dú)有,不能沖突。創(chuàng)建的最終返回一個(gè)Container類,包含這個(gè)id、狀態(tài)目錄(在root目錄下創(chuàng)建的以id命名的文件夾,存state.json容器狀態(tài)文件)、容器配置參數(shù)、初始化路徑和參數(shù),以及管理cgroup的方式(包含直接通過文件操作管理和systemd管理兩個(gè)選擇,默認(rèn)選cgroup文件系統(tǒng)管理)。
Load():當(dāng)創(chuàng)建的id已經(jīng)存在時(shí),即已經(jīng)Create過,存在id文件目錄,就會從id目錄下直接讀取state.json來載入容器。其中的參數(shù)在配置參數(shù)部分有詳細(xì)解釋。
Type():返回容器管理的類型,目前可能返回的有l(wèi)ibcontainer和lxc,為未來支持更多容器接口做準(zhǔn)備。
StartInitialization():容器內(nèi)初始化函數(shù)。
這部分代碼是在容器內(nèi)部執(zhí)行的,當(dāng)容器創(chuàng)建時(shí),如果New不加任何參數(shù),默認(rèn)在容器進(jìn)程中運(yùn)行的第一條命令就是nsinit init。在execdriver的初始化中,會向reexec注冊初始化器,命名為native,然后在創(chuàng)建libcontainer以后把native作為執(zhí)行參數(shù)傳遞到容器中執(zhí)行,這個(gè)初始化器創(chuàng)建的libcontainer就是沒有參數(shù)的。
傳入的參數(shù)是一個(gè)管道文件描述符,為了保證在初始化過程中,父子進(jìn)程間狀態(tài)同步和配置信息傳遞而建立。
不管是純粹新建的容器還是已經(jīng)創(chuàng)建的容器執(zhí)行新的命令,都是從這個(gè)入口做初始化。
第一步,通過管道獲取配置信息。
第二步,從配置信息中獲取環(huán)境變量并設(shè)置為容器內(nèi)環(huán)境變量。
若是已經(jīng)存在的容器執(zhí)行新命令,則只需要配置cgroup、namespace的Capabilities以及AppArmor等信息,最后執(zhí)行命令。
若是純粹新建的容器,則還需要初始化網(wǎng)絡(luò)、路由、namespace、主機(jī)名、配置只讀路徑等等,最后執(zhí)行命令。
至此,容器就已經(jīng)創(chuàng)建和初始化完畢了。
4.2 Container 對象
Container對象主要包含了容器配置、控制、狀態(tài)顯示等功能,是對不同平臺容器功能的抽象。目前已經(jīng)具體實(shí)現(xiàn)的是Linux平臺下的Container對象。每一個(gè)Container進(jìn)程內(nèi)部都是線程安全的。因?yàn)镃ontainer有可能被外部的進(jìn)程銷毀,所以每個(gè)方法都會對容器是否存在進(jìn)行檢測。
ID():顯示Container的ID,在Factor對象中已經(jīng)說過,ID很重要,具有唯一性。
Status():返回容器內(nèi)進(jìn)程是運(yùn)行狀態(tài)還是停止?fàn)顟B(tài)。通過執(zhí)行“SIG=0”的KILL命令對進(jìn)程是否存在進(jìn)行檢測。
State():返回容器的狀態(tài),包括容器ID、配置信息、初始進(jìn)程ID、進(jìn)程啟動時(shí)間、cgroup文件路徑、namespace路徑。通過調(diào)用Status()判斷進(jìn)程是否存在。
Config():返回容器的配置信息,可在“配置參數(shù)解析”部分查看有哪些方面的配置信息。
Processes():返回cgroup文件cgroup.procs中的值,在Docker背后的內(nèi)核知識:cgroups資源限制部分的講解中我們已經(jīng)提過,cgroup.procs文件會羅列所有在該cgroup中的線程組ID(即若有線程創(chuàng)建了子線程,則子線程的PID不包含在內(nèi))。由于容器不斷在運(yùn)行,所以返回的結(jié)果并不能保證完全存活,除非容器處于“PAUSED”狀態(tài)。
Stats():返回容器的統(tǒng)計(jì)信息,包括容器的cgroups中的統(tǒng)計(jì)以及網(wǎng)卡設(shè)備的統(tǒng)計(jì)信息。Cgroups中主要統(tǒng)計(jì)了cpu、memory和blkio這三個(gè)子系統(tǒng)的統(tǒng)計(jì)內(nèi)容,具體了解可以通過閱讀“cgroups資源限制”部分對于這三個(gè)子系統(tǒng)統(tǒng)計(jì)內(nèi)容的介紹來了解。網(wǎng)卡設(shè)備的統(tǒng)計(jì)則通過讀取系統(tǒng)中,網(wǎng)絡(luò)網(wǎng)卡文件的統(tǒng)計(jì)信息文件/sys/class/net/EthInterface/statistics來實(shí)現(xiàn)。
Set():設(shè)置容器cgroup各子系統(tǒng)的文件路徑。因?yàn)閏groups的配置是進(jìn)程運(yùn)行時(shí)也會生效的,所以我們可以通過這個(gè)方法在容器運(yùn)行時(shí)改變cgroups文件從而改變資源分配。
Start():構(gòu)建ParentProcess對象,用于處理啟動容器進(jìn)程的所有初始化工作,并作為父進(jìn)程與新創(chuàng)建的子進(jìn)程(容器)進(jìn)行初始化通信。傳入的Process對象可以幫助我們追蹤進(jìn)程的生命周期,Process對象將在后文詳細(xì)介紹。
啟動的過程首先會調(diào)用Status()方法的具體實(shí)現(xiàn)得知進(jìn)程是否存活。
創(chuàng)建一個(gè)管道(詳見Docker初始化通信——管道)為后期父子進(jìn)程通信做準(zhǔn)備。
配置子進(jìn)程cmd命令模板,配置參數(shù)的值就是從factory.Create()傳入進(jìn)來的,包括命令執(zhí)行的工作目錄、命令參數(shù)、輸入輸出、根目錄、子進(jìn)程管道以及KILL信號的值。
根據(jù)容器進(jìn)程是否存在確定是在已有容器中執(zhí)行命令還是創(chuàng)建新的容器執(zhí)行命令。若存在,則把配置的命令構(gòu)建成一個(gè)exec.Cmd對象、cgroup路徑、父子進(jìn)程管道及配置保留到ParentProcess對象中;若不存在,則創(chuàng)建容器進(jìn)程及相應(yīng)namespace,目前對user namespace有了一定的支持,若配置時(shí)加入user namespace,會針對配置項(xiàng)進(jìn)行映射,默認(rèn)映射到宿主機(jī)的root用戶,最后同樣構(gòu)建出相應(yīng)的配置內(nèi)容保留到ParentProcess對象中。通過在cmd.Env寫入環(huán)境變量_libcontainer_INITTYPE來告訴容器進(jìn)程采用的哪種方式啟動。
執(zhí)行ParentProcess中構(gòu)建的exec.Cmd內(nèi)容,即執(zhí)行ParentProcess.start(),具體的執(zhí)行過程在Process部分介紹。
最后如果是新建的容器進(jìn)程,還會執(zhí)行狀態(tài)更新函數(shù),把state.json的內(nèi)容刷新。
Destroy():首先使用cgroup的freezer子系統(tǒng)暫停所有運(yùn)行的進(jìn)程,然后給所有進(jìn)程發(fā)送SIGKIL信號(如果沒有使用pid namespace就不對進(jìn)程處理)。最后把cgroup及其子系統(tǒng)卸載,刪除cgroup文件夾。
Pause():使用cgroup的freezer子系統(tǒng)暫停所有運(yùn)行的進(jìn)程。
Resume():使用cgroup的freezer子系統(tǒng)恢復(fù)所有運(yùn)行的進(jìn)程。
NotifyOOM():為容器內(nèi)存使用超界提供只讀的通道,通過向cgroup.event_control寫入eventfd(用作線程間通信的消息隊(duì)列)和cgroup.oom_control(用于決定內(nèi)存使用超限后的處理方式)來實(shí)現(xiàn)。
Checkpoint():保存容器進(jìn)程檢查點(diǎn)快照,為容器熱遷移做準(zhǔn)備。通過使用CRIU的SWRK模式來實(shí)現(xiàn),這種模式是CRIU另外兩種模式CLI和RPC的結(jié)合體,允許用戶需要的時(shí)候像使用命令行工具一樣運(yùn)行CRIU,并接受用戶遠(yuǎn)程調(diào)用的請求,即傳入的熱遷移檢查點(diǎn)保存請求,傳入文件形式以Google的protobuf協(xié)議保存。
Restore():恢復(fù)檢查點(diǎn)快照并運(yùn)行,完成容器熱遷移。同樣通過CRIU的SWRK模式實(shí)現(xiàn),恢復(fù)的時(shí)候可以傳入配置文件設(shè)置恢復(fù)掛載點(diǎn)、網(wǎng)絡(luò)等配置信息。
至此,Container對象中的所有函數(shù)及相關(guān)功能都已經(jīng)介紹完畢,包含了容器生命周期的全部過程。
TIPs: Docker初始化通信——管道
libcontainer創(chuàng)建容器進(jìn)程時(shí)需要做初始化工作,此時(shí)就涉及到使用了namespace隔離后的兩個(gè)進(jìn)程間的通信。我們把負(fù)責(zé)創(chuàng)建容器的進(jìn)程稱為父進(jìn)程,容器進(jìn)程稱為子進(jìn)程。父進(jìn)程clone出子進(jìn)程以后,依舊是共享內(nèi)存的。但是如何讓子進(jìn)程知道內(nèi)存中寫入了新數(shù)據(jù)依舊是一個(gè)問題,一般有四種方法。
發(fā)送信號通知(signal)
對內(nèi)存輪詢訪問(poll memory)
sockets通信(sockets)
文件和文件描述符(files and file-descriptors)
對于Signal而言,本身包含的信息有限,需要額外記錄,namespace帶來的上下文變化使其不易理解,并不是最佳選擇。顯然通過輪詢內(nèi)存的方式來溝通是一個(gè)非常低效的做法。另外,因?yàn)镈ocker會加入network namespace,實(shí)際上初始時(shí)網(wǎng)絡(luò)棧也是完全隔離的,所以socket方式并不可行。 Docker最終選擇的方式就是打開的可讀可寫文件描述符——管道。 Linux中,通過pipe(int fd[2])系統(tǒng)調(diào)用就可以創(chuàng)建管道,參數(shù)是一個(gè)包含兩個(gè)整型的數(shù)組。調(diào)用完成后,在fd[1]端寫入的數(shù)據(jù),就可以從fd[0]端讀取。
// 需要加入頭文件: #include // 全局變量: int fd[2]; // 在父進(jìn)程中進(jìn)行初始化: pipe(fd); // 關(guān)閉管道文件描述符 close(checkpoint[1]);
調(diào)用pipe函數(shù)后,創(chuàng)建的子進(jìn)程會內(nèi)嵌這個(gè)打開的文件描述符,對fd[1]寫入數(shù)據(jù)后可以在fd[0]端讀取。通過管道,父子進(jìn)程之間就可以通信。通信完畢的奧秘就在于EOF信號的傳遞。大家都知道,當(dāng)打開的文件描述符都關(guān)閉時(shí),才能讀到EOF信號,所以libcontainer中父進(jìn)程先關(guān)閉自己這一端的管道,然后等待子進(jìn)程關(guān)閉另一端的管道文件描述符,傳來EOF表示子進(jìn)程已經(jīng)完成了初始化的過程。
4.3 Process 對象
Process 主要分為兩類,一類在源碼中就叫Process,用于容器內(nèi)進(jìn)程的配置和IO的管理;另一類在源碼中叫ParentProcess,負(fù)責(zé)處理容器啟動工作,與Container對象直接進(jìn)行接觸,啟動完成后作為Process的一部分,執(zhí)行等待、發(fā)信號、獲得pid等管理工作。 ParentProcess對象,主要包含以下六個(gè)函數(shù),而根據(jù)”需要新建容器”和“在已經(jīng)存在的容器中執(zhí)行”的不同方式,具體的實(shí)現(xiàn)也有所不同。
已有容器中執(zhí)行命令
pid(): 啟動容器進(jìn)程后通過管道從容器進(jìn)程中獲得,因?yàn)槿萜饕呀?jīng)存在,與Docker Deamon在不同的pid namespace中,從進(jìn)程所在的namespace獲得的進(jìn)程號才有意義。
start(): 初始化容器中的執(zhí)行進(jìn)程。在已有容器中執(zhí)行命令一般由docker exec調(diào)用,在execdriver包中,執(zhí)行exec時(shí)會引入nsenter包,從而調(diào)用其中的C語言代碼,執(zhí)行nsexec()函數(shù),該函數(shù)會讀取配置文件,使用setns()加入到相應(yīng)的namespace,然后通過clone()在該namespace中生成一個(gè)子進(jìn)程,并把子進(jìn)程通過管道傳遞出去,使用setns()以后并沒有進(jìn)入pid namespace,所以還需要通過加上clone()系統(tǒng)調(diào)用。
開始執(zhí)行進(jìn)程,首先會運(yùn)行C代碼,通過管道獲得進(jìn)程pid,最后等待C代碼執(zhí)行完畢。
通過獲得的pid把cmd中的Process替換成新生成的子進(jìn)程。
把子進(jìn)程加入cgroup中。
通過管道傳配置文件給子進(jìn)程。
等待初始化完成或出錯(cuò)返回,結(jié)束。
新建容器執(zhí)行命令
pid():啟動容器進(jìn)程后通過exec.Cmd自帶的pid()函數(shù)即可獲得。
start():初始化及執(zhí)行容器命令。
開始運(yùn)行進(jìn)程。
把進(jìn)程pid加入到cgroup中管理。
初始化容器網(wǎng)絡(luò)。(本部分內(nèi)容豐富,將從本系列的后續(xù)文章中深入講解)
通過管道發(fā)送配置文件給子進(jìn)程。
等待初始化完成或出錯(cuò)返回,結(jié)束。
實(shí)現(xiàn)方式類似的一些函數(shù)
**terminate() **:發(fā)送SIGKILL信號結(jié)束進(jìn)程。
**startTime() **:獲取進(jìn)程的啟動時(shí)間。
signal():發(fā)送信號給進(jìn)程。
wait():等待程序執(zhí)行結(jié)束,返回結(jié)束的程序狀態(tài)。
Process對象,主要描述了容器內(nèi)進(jìn)程的配置以及IO。包括參數(shù)Args,環(huán)境變量Env,用戶User(由于uid、gid映射),工作目錄Cwd,標(biāo)準(zhǔn)輸入輸出及錯(cuò)誤輸入,控制終端路徑consolePath,容器權(quán)限Capabilities以及上述提到的ParentProcess對象ops(擁有上面的一些操作函數(shù),可以直接管理進(jìn)程)。
5. 總結(jié)
本文主要介紹了Docker容器管理的方式libcontainer,從libcontainer的使用到源碼實(shí)現(xiàn)方式。我們深入到容器進(jìn)程內(nèi)部,感受到了libcontainer較為全面的設(shè)計(jì)。總體而言,libcontainer本身主要分為三大塊工作內(nèi)容,一是容器的創(chuàng)建及初始化,二是容器生命周期管理,三則是進(jìn)程管理,調(diào)用方為Docker的execdriver。容器的監(jiān)控主要通過cgroups的狀態(tài)統(tǒng)計(jì)信息,未來會加入進(jìn)程追蹤等更豐富的功能。另一方面,libcontainer在安全支持方面也為用戶盡可能多的提供了支持和選擇。遺憾的是,容器安全的配置需要用戶對系統(tǒng)安全本身有足夠高的理解,user namespace也尚未支持,可見libcontainer依舊有很多工作要完善。但是Docker社區(qū)的火熱也自然帶動了大家對libcontainer的關(guān)注,相信在不久的將來,libcontainer就會變得更安全、更易用。
有個(gè)通過代理進(jìn)來的tcp連接,通過Conn.RemoteAddr獲取到的是代理點(diǎn)的ip地址,為了獲取實(shí)際客戶端的ip,找到了syscall.Getpeername的方法,而這個(gè)方法需要的是連接的fd。
I/O 操作也叫輸入輸出操作。其中 I 是指 Input,O 是指 Output,用于讀或者寫數(shù)據(jù)的,有些語言中也叫流操作,是指數(shù)據(jù)通信的通道。
Golang 標(biāo)準(zhǔn)庫對 IO 的抽象非常精巧,各個(gè)組件可以隨意組合,可以作為接口設(shè)計(jì)的典范。
io 包中提供 I/O 原始操作的一系列接口。它主要包裝了一些已有的實(shí)現(xiàn),如 os 包中的那些,并將這些抽象成為實(shí)用性的功能和一些其他相關(guān)的接口。
在 io 包中最重要的是兩個(gè)接口:Reader 和 Writer 接口,首先來介紹這讀的操作。
Reader 接口的定義,Read() 方法用于讀取數(shù)據(jù)。
Read 將 len(p) 個(gè)字節(jié)讀取到 p 中。它返回讀取的字節(jié)數(shù) n(0 = n = len(p))以及任何遇到的錯(cuò)誤。即使 Read 返回的 n len(p),它也會在調(diào)用過程
中使用 p 的全部作為暫存空間。若一些數(shù)據(jù)可用但不到 len(p) 個(gè)字節(jié),Read 會照例返回可用的東西,而不是等待更多。
當(dāng) Read 在成功讀取 n 0 個(gè)字節(jié)后遇到一個(gè)錯(cuò)誤或 EOF 情況,它就會返回讀取的字節(jié)數(shù),這種一般情況的一個(gè)例子就是 Reader 在輸入流結(jié)束時(shí)會返回一個(gè)非零的字節(jié)數(shù),可能的返回不是 err == EOF 就是 err == nil。無論如何,下一個(gè) Read 都應(yīng)當(dāng)返回 0、EOF。
調(diào)用者應(yīng)當(dāng)總在考慮到錯(cuò)誤 err 前處理 n 0 的字節(jié)。這樣做可以在讀取一些字節(jié),以及允許的 EOF 行為后正確地處理 I/O 錯(cuò)誤。
Read 的實(shí)現(xiàn)會阻止返回零字節(jié)的計(jì)數(shù)和一個(gè) nil 錯(cuò)誤,調(diào)用者應(yīng)將這種情況視作空操作。
ReaderFrom接口的定義,封裝了基本的 ReadFrom 方法。
ReadFrom 從 r 中讀取數(shù)據(jù)到對象的數(shù)據(jù)流中,直到 r 返回 EOF 或 r 出現(xiàn)讀取錯(cuò)誤為止,返回值 n 是讀取的字節(jié)數(shù),返回值 err 就是 r 的返回值 err。
定義ReaderAt接口,ReaderAt 接口封裝了基本的 ReadAt 方法
ReadAt 從對象數(shù)據(jù)流的 off 處讀出數(shù)據(jù)到 p 中,忽略數(shù)據(jù)的讀寫指針,從數(shù)據(jù)的起始位置偏移 off 處開始讀取,如果對象的數(shù)據(jù)流只有部分可用,不足以填滿 p,則 ReadAt 將等待所有數(shù)據(jù)可用之后,繼續(xù)向 p 中寫入,直到將 p 填滿后再返回。
在這點(diǎn)上 ReadAt 要比 Read 更嚴(yán)格,返回讀取的字節(jié)數(shù) n 和讀取時(shí)遇到的錯(cuò)誤,如果 n len(p),則需要返回一個(gè) err 值來說明,為什么沒有將 p 填滿(比如 EOF),如果 n = len(p),而且對象的數(shù)據(jù)沒有全部讀完,則 err 將返回 nil,如果 n = len(p),而且對象的數(shù)據(jù)剛好全部讀完,則 err 將返回 EOF 或者 nil(不確定)
file 類是在 os 包中的,封裝了底層的文件描述符和相關(guān)信息,同時(shí)封裝了 Read 和 Write 的實(shí)現(xiàn)。
讀取文件中的數(shù)據(jù):
Writer 接口的定義,Write() 方法用于寫出數(shù)據(jù)。
Write 將 len(p) 個(gè)字節(jié)從 p 中寫入到基本數(shù)據(jù)流中。它返回從 p 中被寫入的字節(jié)數(shù) n(0 = n = len(p))以及任何遇到的引起寫入提前停止的錯(cuò)誤。若 Write 返回的 n len(p),它就必須返回一個(gè)非 nil 的錯(cuò)誤。Write 不能修改此切片的數(shù)據(jù),即便它是臨時(shí)的。
Seeker接口的定義,封裝了基本的 Seek 方法。
Seeker 用來移動數(shù)據(jù)的讀寫指針,Seek 設(shè)置下一次讀寫操作的指針位置,每次的讀寫操作都是從指針位置開始的。
whence 的含義:
如果 whence 為 0:表示從數(shù)據(jù)的開頭開始移動指針
如果 whence 為 1:表示從數(shù)據(jù)的當(dāng)前指針位置開始移動指針
如果 whence 為 2:表示從數(shù)據(jù)的尾部開始移動指針
offset 是指針移動的偏移量
返回移動后的指針位置和移動過程中遇到的任何錯(cuò)誤
WriterTo接口的定義,封裝了基本的 WriteTo 方法。
WriterTo 將對象的數(shù)據(jù)流寫入到 w 中,直到對象的數(shù)據(jù)流全部寫入完畢或遇到寫入錯(cuò)誤為止。返回值 n 是寫入的字節(jié)數(shù),返回值 err 就是 w 的返回值 err。
定義WriterAt接口,WriterAt 接口封裝了基本的 WriteAt 方法
WriteAt 將 p 中的數(shù)據(jù)寫入到對象數(shù)據(jù)流的 off 處,忽略數(shù)據(jù)的讀寫指針,從數(shù)據(jù)的起始位置偏移 off 處開始寫入,返回寫入的字節(jié)數(shù)和寫入時(shí)遇到的錯(cuò)誤。如果 n len(p),則必須返回一個(gè) err 值來說明為什么沒有將 p 完全寫入
file 類是在 os 包中的,封裝了底層的文件描述符和相關(guān)信息,同時(shí)封裝了 Read 和 Write 的實(shí)現(xiàn)。
寫出數(shù)據(jù)到本地文件:
一般來說,進(jìn)程的操作使用的是一些系統(tǒng)的命令,所以go內(nèi)部使用os包,進(jìn)行一些運(yùn)行系統(tǒng)命令的操作
os 包及其子包 os/exec 提供了創(chuàng)建進(jìn)程的方法。
一般的,應(yīng)該優(yōu)先使用 os/exec 包。因?yàn)?os/exec 包依賴 os 包中關(guān)鍵創(chuàng)建進(jìn)程的 API,為了便于理解,我們先探討 os 包中和進(jìn)程相關(guān)的部分。
Unix :fork創(chuàng)建一個(gè)進(jìn)程,(及其一些變種,如 vfork、clone)。
Go:Linux 下創(chuàng)建進(jìn)程使用的系統(tǒng)調(diào)用是 clone。
允許一進(jìn)程(父進(jìn)程)創(chuàng)建一新進(jìn)程(子進(jìn)程)。具體做法是,新的子進(jìn)程幾近于對父進(jìn)程的翻版:子進(jìn)程獲得父進(jìn)程的棧、數(shù)據(jù)段、堆和執(zhí)行文本段的拷貝。可將此視為把父進(jìn)程一分為二。
終止一進(jìn)程,將進(jìn)程占用的所有資源(內(nèi)存、文件描述符等)歸還內(nèi)核,交其進(jìn)行再次分配。參數(shù) status 為一整型變量,表示進(jìn)程的退出狀態(tài)。父進(jìn)程可使用系統(tǒng)調(diào)用 wait() 來獲取該狀態(tài)。
目的有二:其一,如果子進(jìn)程尚未調(diào)用 exit() 終止,那么 wait 會掛起父進(jìn)程直至子進(jìn)程終止;其二,子進(jìn)程的終止?fàn)顟B(tài)通過 wait 的 status 參數(shù)返回。
加載一個(gè)新程序(路徑名為 pathname,參數(shù)列表為 argv,環(huán)境變量列表為 envp)到當(dāng)前進(jìn)程的內(nèi)存。這將丟棄現(xiàn)存的程序文本段,并為新程序重新創(chuàng)建棧、數(shù)據(jù)段以及堆。通常將這一動作稱為執(zhí)行一個(gè)新程序。
沒有直接提供 fork 系統(tǒng)調(diào)用的封裝,而是將 fork 和 execve 合二為一,提供了 syscall.ForkExec。如果想只調(diào)用 fork,得自己通過 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) 實(shí)現(xiàn)。
os.Process 存儲了通過 StartProcess 創(chuàng)建的進(jìn)程的相關(guān)信息。
一般通過 StartProcess 創(chuàng)建 Process 的實(shí)例,函數(shù)聲明如下:
它使用提供的程序名、命令行參數(shù)、屬性開始一個(gè)新進(jìn)程。StartProcess 是一個(gè)低級別的接口。os/exec 包提供了高級別的接口,一般應(yīng)該盡量使用 os/exec 包。如果出錯(cuò),錯(cuò)誤的類型會是 *PathError。
屬性定義如下:
FindProcess 可以通過 pid 查找一個(gè)運(yùn)行中的進(jìn)程。該函數(shù)返回的 Process 對象可以用于獲取關(guān)于底層操作系統(tǒng)進(jìn)程的信息。在 Unix 系統(tǒng)中,此函數(shù)總是成功,即使 pid 對應(yīng)的進(jìn)程不存在。
Process 提供了四個(gè)方法:Kill、Signal、Wait 和 Release。其中 Kill 和 Signal 跟信號相關(guān),而 Kill 實(shí)際上就是調(diào)用 Signal,發(fā)送了 SIGKILL 信號,強(qiáng)制進(jìn)程退出,關(guān)于信號,后續(xù)章節(jié)會專門講解。
Release 方法用于釋放 Process 對象相關(guān)的資源,以便將來可以被再使用。該方法只有在確定沒有調(diào)用 Wait 時(shí)才需要調(diào)用。Unix 中,該方法的內(nèi)部實(shí)現(xiàn)只是將 Process 的 pid 置為 -1。
通過 os 包可以做到運(yùn)行外部命令,如前面的例子。不過,Go 標(biāo)準(zhǔn)庫為我們封裝了更好用的包: os/exec,運(yùn)行外部命令,應(yīng)該優(yōu)先使用它,它包裝了 os.StartProcess 函數(shù)以便更容易的重定向標(biāo)準(zhǔn)輸入和輸出,使用管道連接 I/O,以及作其它的一些調(diào)整。
exec.LookPath 函數(shù)在 PATH 指定目錄中搜索可執(zhí)行程序,如 file 中有 /,則只在當(dāng)前目錄搜索。該函數(shù)返回完整路徑或相對于當(dāng)前路徑的一個(gè)相對路徑。
func LookPath(file string) (string, error)
如果在 PATH 中沒有找到可執(zhí)行文件,則返回 exec.ErrNotFound。
Cmd 結(jié)構(gòu)代表一個(gè)正在準(zhǔn)備或者在執(zhí)行中的外部命令,調(diào)用了 Run、Output 或 CombinedOutput 后,Cmd 實(shí)例不能被重用。
一般的,應(yīng)該通過 exec.Command 函數(shù)產(chǎn)生 Cmd 實(shí)例:
用法
得到 * Cmd 實(shí)例后,接下來一般有兩種寫法:
前面講到,通過 Cmd 實(shí)例后,有兩種方式運(yùn)行命令。有時(shí)候,我們不只是簡單的運(yùn)行命令,還希望能控制命令的輸入和輸出。通過上面的 API 介紹,控制輸入輸出有幾種方法:
參考資料: