本文目錄一覽:
- 1、(十一)golang 內存分析
- 2、golang多進程庫pagent
- 3、golang 通過cmd 調程序,怎麼不顯示命令行
- 4、golang 進程創建,fork,以及熱重啟(無縫升級)
- 5、Golang-基於TimeingWheel定時器
(十一)golang 內存分析
編寫過C語言程序的肯定知道通過malloc()方法動態申請內存,其中內存分配器使用的是glibc提供的ptmalloc2。 除了glibc,業界比較出名的內存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免內存碎片和性能上均比glic有比較大的優勢,在多線程環境中效果更明顯。
Golang中也實現了內存分配器,原理與tcmalloc類似,簡單的說就是維護一塊大的全局內存,每個線程(Golang中為P)維護一塊小的私有內存,私有內存不足再從全局申請。另外,內存分配與GC(垃圾回收)關係密切,所以了解GC前有必要了解內存分配的原理。
為了方便自主管理內存,做法便是先向系統申請一塊內存,然後將內存切割成小塊,通過一定的內存分配演算法管理內存。 以64位系統為例,Golang程序啟動時會向系統申請的內存如下圖所示:
預申請的內存劃分為spans、bitmap、arena三部分。其中arena即為所謂的堆區,應用中需要的內存從這裡分配。其中spans和bitmap是為了管理arena區而存在的。
arena的大小為512G,為了方便管理把arena區域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁;
spans區域存放span的指針,每個指針對應一個page,所以span區域的大小為(512GB/8KB)乘以指針大小8byte = 512M
bitmap區域大小也是通過arena計算出來,不過主要用於GC。
span是用於管理arena頁的關鍵數據結構,每個span中包含1個或多個連續頁,為了滿足小對象分配,span中的一頁會劃分更小的粒度,而對於大對象比如超過頁大小,則通過多頁實現。
根據對象大小,劃分了一系列class,每個class都代表一個固定大小的對象,以及每個span的大小。如下表所示:
上表中每列含義如下:
class: class ID,每個span結構中都有一個class ID, 表示該span可處理的對象類型
bytes/obj:該class代表對象的位元組數
bytes/span:每個span佔用堆的位元組數,也即頁數乘以頁大小
objects: 每個span可分配的對象個數,也即(bytes/spans)/(bytes/obj)waste
bytes: 每個span產生的內存碎片,也即(bytes/spans)%(bytes/obj)上表可見最大的對象是32K大小,超過32K大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。
span是內存管理的基本單位,每個span用於管理特定的class對象, 跟據對象大小,span將一個或多個頁拆分成多個塊進行管理。src/runtime/mheap.go:mspan定義了其數據結構:
以class 10為例,span和管理的內存如下圖所示:
spanclass為10,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址。allocBits指向一個點陣圖,每位代表一個塊是否被分配,本例中有兩個塊已經被分配,其allocCount也為2。next和prev用於將多個span鏈接起來,這有利於管理多個span,接下來會進行說明。
有了管理內存的基本單位span,還要有個數據結構來管理span,這個數據結構叫mcentral,各線程需要內存時從mcentral管理的span中申請內存,為了避免多線程申請內存時不斷的加鎖,Golang為每個線程分配了span的緩存,這個緩存即是cache。src/runtime/mcache.go:mcache定義了cache的數據結構
alloc為mspan的指針數組,數組大小為class總數的2倍。數組中每個元素代表了一種class類型的span列表,每種class類型都有兩組span列表,第一組列表中所表示的對象中包含了指針,第二組列表中所表示的對象不含有指針,這麼做是為了提高GC掃描性能,對於不包含指針的span列表,沒必要去掃描。根據對象是否包含指針,將對象分為noscan和scan兩類,其中noscan代表沒有指針,而scan則代表有指針,需要GC進行掃描。mcache和span的對應關係如下圖所示:
mchache在初始化時是沒有任何span的,在使用過程中會動態的從central中獲取並緩存下來,跟據使用情況,每種class的span個數也不相同。上圖所示,class 0的span數比class1的要多,說明本線程中分配的小對象要多一些。
cache作為線程的私有資源為單個線程服務,而central則是全局資源,為多個線程服務,當某個線程內存不足時會向central申請,當某個線程釋放內存時又會回收進central。src/runtime/mcentral.go:mcentral定義了central數據結構:
lock: 線程間互斥鎖,防止多線程讀寫衝突
spanclass : 每個mcentral管理著一組有相同class的span列表
nonempty: 指還有內存可用的span列表
empty: 指沒有內存可用的span列表
nmalloc: 指累計分配的對象個數線程從central獲取span步驟如下:
將span歸還步驟如下:
從mcentral數據結構可見,每個mcentral對象只管理特定的class規格的span。事實上每種class都會對應一個mcentral,這個mcentral的集合存放於mheap數據結構中。src/runtime/mheap.go:mheap定義了heap的數據結構:
lock: 互斥鎖
spans: 指向spans區域,用於映射span和page的關係
bitmap:bitmap的起始地址
arena_start: arena區域首地址
arena_used: 當前arena已使用區域的最大地址
central: 每種class對應的兩個mcentral
從數據結構可見,mheap管理著全部的內存,事實上Golang就是通過一個mheap類型的全局變數進行內存管理的。mheap內存管理示意圖如下:
系統預分配的內存分為spans、bitmap、arean三個區域,通過mheap管理起來。接下來看內存分配過程。
針對待分配對象的大小不同有不同的分配邏輯:
(0, 16B) 且不包含指針的對象: Tiny分配
(0, 16B) 包含指針的對象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大對象分配其中Tiny分配和大對象分配都屬於內存管理的優化範疇,這裡暫時僅關注一般的分配方法。
以申請size為n的內存為例,分配步驟如下:
Golang內存分配是個相當複雜的過程,其中還摻雜了GC的處理,這裡僅僅對其關鍵數據結構進行了說明,了解其原理而又不至於深陷實現細節。1、Golang程序啟動時申請一大塊內存並劃分成spans、bitmap、arena區域
2、arena區域按頁劃分成一個個小塊。
3、span管理一個或多個頁。
4、mcentral管理多個span供線程申請使用
5、mcache作為線程私有資源,資源來源於mcentral。
golang多進程庫pagent
地址:
pagent是一個多進程模型的golang庫,具有以下特點:
簡單: 父子進程只通過stdin和stdout來交互
安全: 多進程很安全,子進程掛掉一個不影響其他子進程
解耦:子進程交互和業務分離
例子:
package main
import (
“fmt”
“time”
“github.com/adwpc/pagent”
)
type MyBiz struct {
pagent.Master
}
func NewBiz() *MyBiz {
return MyBiz{}
}
func (a *MyBiz) BizRunning(id, str string) error {
fmt.Println(“[MyBiz BizRunning] str=” + str)
return nil
}
func (a *MyBiz) BizFinish(id string, err error) error {
fmt.Println(“[MyBiz BizFinish] id=” + id)
return err
}
func main() {
a := NewBiz()
fmt.Println(“worker1————————-“)
a.GetWorker(“worker1”).Start(“bash”, a.BizRunning, a.BizFinish)
a.GetWorker(“worker1”).Input(“ls”)
time.Sleep(1 * time.Second)
a.DelWorker(“worker1”)
fmt.Println(“worker2————————-“)
a.GetWorker(“worker2”).Start(“ifconfig”, nil, a.BizFinish)
time.Sleep(1 * time.Second)
a.DelWorker(“worker2”)
fmt.Printf(“end!—————————-“)
}
golang 通過cmd 調程序,怎麼不顯示命令行
1.首先,運行,輸入cmd進入dos命令面盤。選好自己要打開的文件,並知道其路徑。
2.以打開ps為例。首先找到ps的路徑E:\my software\photoshop cs6\Photoshop CS6\Photoshop.exe
3.先在cmd命令中輸入E: 如果你的文件在其他盤就輸入其他的盤。現在進入E盤了。再輸入cd my software\photoshop cs6\Photoshop CS6。注意,cd後有一個空格,文件夾的大小寫也要注意。這個步驟也可以分開打,如cd my software回車,cd photoshop cs6回車,cd Photoshop CS6回車。
4.最後輸入start Photoshop.exe回車文件就打開了。
Windows 命令提示符(cmd.exe)是 Windows NT 下的一個用於運行 Windows 控制面板程序或某些 DOS 程序的shell程序;或在 Windows CE 下只用於運行控制面板程序的外殼程序。
golang 進程創建,fork,以及熱重啟(無縫升級)
一般來說,進程的操作使用的是一些系統的命令,所以go內部使用os包,進行一些運行系統命令的操作
os 包及其子包 os/exec 提供了創建進程的方法。
一般的,應該優先使用 os/exec 包。因為 os/exec 包依賴 os 包中關鍵創建進程的 API,為了便於理解,我們先探討 os 包中和進程相關的部分。
Unix :fork創建一個進程,(及其一些變種,如 vfork、clone)。
Go:Linux 下創建進程使用的系統調用是 clone。
允許一進程(父進程)創建一新進程(子進程)。具體做法是,新的子進程幾近於對父進程的翻版:子進程獲得父進程的棧、數據段、堆和執行文本段的拷貝。可將此視為把父進程一分為二。
終止一進程,將進程佔用的所有資源(內存、文件描述符等)歸還內核,交其進行再次分配。參數 status 為一整型變數,表示進程的退出狀態。父進程可使用系統調用 wait() 來獲取該狀態。
目的有二:其一,如果子進程尚未調用 exit() 終止,那麼 wait 會掛起父進程直至子進程終止;其二,子進程的終止狀態通過 wait 的 status 參數返回。
載入一個新程序(路徑名為 pathname,參數列表為 argv,環境變數列表為 envp)到當前進程的內存。這將丟棄現存的程序文本段,並為新程序重新創建棧、數據段以及堆。通常將這一動作稱為執行一個新程序。
沒有直接提供 fork 系統調用的封裝,而是將 fork 和 execve 合二為一,提供了 syscall.ForkExec。如果想只調用 fork,得自己通過 syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) 實現。
os.Process 存儲了通過 StartProcess 創建的進程的相關信息。
一般通過 StartProcess 創建 Process 的實例,函數聲明如下:
它使用提供的程序名、命令行參數、屬性開始一個新進程。StartProcess 是一個低級別的介面。os/exec 包提供了高級別的介面,一般應該盡量使用 os/exec 包。如果出錯,錯誤的類型會是 *PathError。
屬性定義如下:
FindProcess 可以通過 pid 查找一個運行中的進程。該函數返回的 Process 對象可以用於獲取關於底層操作系統進程的信息。在 Unix 系統中,此函數總是成功,即使 pid 對應的進程不存在。
Process 提供了四個方法:Kill、Signal、Wait 和 Release。其中 Kill 和 Signal 跟信號相關,而 Kill 實際上就是調用 Signal,發送了 SIGKILL 信號,強制進程退出,關於信號,後續章節會專門講解。
Release 方法用於釋放 Process 對象相關的資源,以便將來可以被再使用。該方法只有在確定沒有調用 Wait 時才需要調用。Unix 中,該方法的內部實現只是將 Process 的 pid 置為 -1。
通過 os 包可以做到運行外部命令,如前面的例子。不過,Go 標準庫為我們封裝了更好用的包: os/exec,運行外部命令,應該優先使用它,它包裝了 os.StartProcess 函數以便更容易的重定向標準輸入和輸出,使用管道連接 I/O,以及作其它的一些調整。
exec.LookPath 函數在 PATH 指定目錄中搜索可執行程序,如 file 中有 /,則只在當前目錄搜索。該函數返回完整路徑或相對於當前路徑的一個相對路徑。
func LookPath(file string) (string, error)
如果在 PATH 中沒有找到可執行文件,則返回 exec.ErrNotFound。
Cmd 結構代表一個正在準備或者在執行中的外部命令,調用了 Run、Output 或 CombinedOutput 後,Cmd 實例不能被重用。
一般的,應該通過 exec.Command 函數產生 Cmd 實例:
用法
得到 * Cmd 實例後,接下來一般有兩種寫法:
前面講到,通過 Cmd 實例後,有兩種方式運行命令。有時候,我們不只是簡單的運行命令,還希望能控制命令的輸入和輸出。通過上面的 API 介紹,控制輸入輸出有幾種方法:
參考資料:
Golang-基於TimeingWheel定時器
在linux下實現定時器主要有如下方式
在這當中 基於時間輪方式實現的定時器 時間複雜度最小,效率最高,然而我們可以通過 優先隊列 實現時間輪定時器。
優先隊列的實現可以使用最大堆和最小堆,因此在隊列中所有的數據都可以定義排序規則自動排序。我們直接通過隊列中 pop 函數獲取數據,就是我們按照自定義排序規則想要的數據。
在 Golang 中實現一個優先隊列異常簡單,在 container/head 包中已經幫我們封裝了,實現的細節,我們只需要實現特定的介面就可以。
下面是官方提供的例子
因為優先隊列底層數據結構是由二叉樹構建的,所以我們可以通過數組來保存二叉樹上的每一個節點。
改數組需要實現 Go 預先定義的介面 Len , Less , Swap , Push , Pop 和 update 。
timerType結構是定時任務抽象結構
首先的 start 函數,當創建一個 TimeingWheel 時,通過一個 goroutine 來執行 start ,在start中for循環和select來監控不同的channel的狀態
通過for循環從隊列中取數據,直到該隊列為空或者是遇見第一個當前時間比任務開始時間大的任務, append 到 expired 中。因為優先隊列中是根據 expiration 來排序的,
所以當取到第一個定時任務未到的任務時,表示該定時任務以後的任務都未到時間。
當 getExpired 函數取出隊列中要執行的任務時,當有的定時任務需要不斷執行,所以就需要判斷是否該定時任務需要重新放回優先隊列中。 isRepeat 是通過判斷任務中 interval 是否大於 0 判斷,
如果大於0 則,表示永久就生效。
防止外部濫用,阻塞定時器協程,框架又一次封裝了timer這個包,名為 timer_wapper 這個包,它提供了兩種調用方式。
參數和上面的參數一樣,只是在第三個參數中使用了任務池,將定時任務放入了任務池中。定時任務的本身執行就是一個 put 操作。
至於put以後,那就是 workers 這個包管理的了。在 worker 包中, 也就是維護了一個任務池,任務池中的任務會有序的執行,方便管理。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/180104.html