Go 每日一庫之 gopsutil

darjun發表於2020-04-06

簡介

gopsutil是 Python 工具庫psutil 的 Golang 移植版,可以幫助我們方便地獲取各種系統和硬體資訊。gopsutil為我們遮蔽了各個系統之間的差異,具有非常強悍的可移植性。有了gopsutil,我們不再需要針對不同的系統使用syscall呼叫對應的系統方法。更棒的是gopsutil的實現中沒有任何cgo的程式碼,使得交叉編譯成為可能。

快速使用

先安裝:

$ go get github.com/shirou/gopsutil
複製程式碼

由於gopsutil庫用到了golang.org/x/sys,後者在牆外,如果有類似下面的報錯:

cannot find package "golang.org/x/sys/windows"
複製程式碼

可使用下面的命令下載golang.org/x/sys在 GitHub 上的映象:

$ git clone git@github.com:golang/sys.git $GOPATH/src/golang.org/x/sys
複製程式碼

使用:

package main

import (
  "fmt"

  "github.com/shirou/gopsutil/mem"
)

func main() {
  v, _ := mem.VirtualMemory()

  fmt.Printf("Total: %v, Available: %v, UsedPercent:%f%%\n", v.Total, v.Available, v.UsedPercent)

  fmt.Println(v)
}
複製程式碼

gopsutil將不同的功能劃分到不同的子包中:

  • cpu:CPU 相關;
  • disk:磁碟相關;
  • docker:docker 相關;
  • host:主機相關;
  • mem:記憶體相關;
  • net:網路相關;
  • process:程式相關;
  • winservices:Windows 服務相關。

想要使用對應的功能,要匯入對應的子包。例如,上面程式碼中,我們要獲取記憶體資訊,匯入的是mem子包。mem.VirtualMemory()方法返回記憶體資訊結構mem.VirtualMemoryStat,該結構有豐富的欄位,我們最常使用的無外乎Total(總記憶體)、Available(可用記憶體)、Used(已使用記憶體)和UsedPercent(記憶體使用百分比)。mem.VirtualMemoryStat還實現了fmt.Stringer介面,以 JSON 格式返回記憶體資訊。語句fmt.Println(v)會自動呼叫v.String(),將返回資訊輸出。程式輸出:

Total: 8526921728, Available: 3768975360, UsedPercent:55.000000%
{"total":8526921728,"available":3768975360,"used":4757946368,"usedPercent":55,"free":0,"active":0,"inactive":0,"wired":0,"laundry":0,"buffers":0,"cached":0,"writeback":0,"dirty":0,"writebacktmp":0,"shared":0,"slab":0,"sreclaimable":0,"sunreclaim":0,"pagetables":0,"swapcached":0,"commitlimit":0,"committedas":0,"hightotal":0,"highfree":0,"lowtotal":0,"lowfree":0,"swaptotal":0,"swapfree":0,"mapped":0,"vmalloctotal":0,"vmallocused":0,"vmallocchunk":0,"hugepagestotal":0,"hugepagesfree":0,"hugepagesize":0}
複製程式碼

單位為位元組,我的電腦記憶體 8GB,當前使用百分比為 55%,可用記憶體 3768975360B(即 3.51GB)。

CPU

我們知道 CPU 的核數有兩種,一種是物理核數,一種是邏輯核數。物理核數就是主機板上實際有多少個 CPU,一個物理 CPU 上可以有多個核心,這些核心被稱為邏輯核。gopsutil中 CPU 相關功能在cpu子包中,cpu子包提供了獲取物理和邏輯核數、CPU 使用率的介面:

  • Counts(logical bool):傳入false,返回物理核數,傳入true,返回邏輯核數;
  • Percent(interval time.Duration, percpu bool):表示獲取interval時間間隔內的 CPU 使用率,percpufalse時,獲取總的 CPU 使用率,percputrue時,分別獲取每個 CPU 的使用率,返回一個[]float64型別的值。

例如:

func main() {
  physicalCnt, _ := cpu.Counts(false)
  logicalCnt, _ := cpu.Counts(true)
  fmt.Printf("physical count:%d logical count:%d\n", physicalCnt, logicalCnt)

  totalPercent, _ := cpu.Percent(3*time.Second, false)
  perPercents, _ := cpu.Percent(3*time.Second, true)
  fmt.Printf("total percent:%v per percents:%v", totalPercent, perPercents)
}
複製程式碼

上面程式碼獲取物理核數和邏輯核數,並獲取 3s 內的總 CPU 使用率和每個 CPU 各自的使用率,程式輸出(注意每次執行輸出可能都不相同):

physical count:4 logical count:8
total percent:[30.729166666666668] per percents:[32.64248704663213 26.94300518134715 44.559585492227974 23.958333333333336 36.787564766839374 20.3125 38.54166666666667 28.125]
複製程式碼

詳細資訊

呼叫cpu.Info()可獲取 CPU 的詳細資訊,返回[]cpu.InfoStat

func main() {
  infos, _ := cpu.Info()
  for _, info := range infos {
    data, _ := json.MarshalIndent(info, "", " ")
    fmt.Print(string(data))
  }
}
複製程式碼

為了方便檢視,我使用 JSON 輸出結果:

{
 "cpu": 0,
 "vendorId": "GenuineIntel",
 "family": "198",
 "model": "",
 "stepping": 0,
 "physicalId": "BFEBFBFF000906E9",
 "coreId": "",
 "cores": 8,
 "modelName": "Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz",
 "mhz": 3601,
 "cacheSize": 0,
 "flags": [],
 "microcode": ""
}
複製程式碼

由結果可以看出,CPU 是 Intel 的 i7-7700 系列,頻率 3.60GHz。上面是我在 Windows 上執行的返回結果,內部使用了github.com/StackExchange/wmi庫。在 Linux 下每個邏輯 CPU 都會返回一個InfoStat結構。

時間佔用

呼叫cpu.Times(percpu bool)可以獲取從開機算起,總 CPU 和 每個單獨的 CPU 時間佔用情況。傳入percpu=false返回總的,傳入percpu=true返回單個的。每個 CPU 時間佔用情況是一個TimeStat結構:

// src/github.com/shirou/gopsutil/cpu/cpu.go
type TimesStat struct {
  CPU       string  `json:"cpu"`
  User      float64 `json:"user"`
  System    float64 `json:"system"`
  Idle      float64 `json:"idle"`
  Nice      float64 `json:"nice"`
  Iowait    float64 `json:"iowait"`
  Irq       float64 `json:"irq"`
  Softirq   float64 `json:"softirq"`
  Steal     float64 `json:"steal"`
  Guest     float64 `json:"guest"`
  GuestNice float64 `json:"guestNice"`
}
複製程式碼
  • CPU:CPU 標識,如果是總的,該欄位為cpu-total,否則為cpu0cpu1...;
  • User:使用者時間佔用(使用者態);
  • System:系統時間佔用(核心態);
  • Idle:空閒時間;
  • ...

例如:

func main() {
  infos, _ := cpu.Times(true)
  for _, info := range infos {
    data, _ := json.MarshalIndent(info, "", " ")
    fmt.Print(string(data))
  }
}
複製程式碼

為了方便檢視,我用 JSON 輸出結果,下面是其中一個輸出:

{
 "cpu": "cpu0",
 "user": 674.46875,
 "system": 1184.984375,
 "idle": 7497.1875,
 "nice": 0,
 "iowait": 0,
 "irq": 75.578125,
 "softirq": 0,
 "steal": 0,
 "guest": 0,
 "guestNice": 0
}
複製程式碼

磁碟

子包disk用於獲取磁碟資訊。disk可獲取 IO 統計、分割槽和使用率資訊。下面依次介紹。

IO 統計

呼叫disk.IOCounters()函式,返回的 IO 統計資訊用map[string]IOCountersStat型別表示。每個分割槽一個結構,鍵為分割槽名,值為統計資訊。這裡摘取統計結構的部分欄位,主要有讀寫的次數、位元組數和時間:

// src/github.com/shirou/gopsutil/disk/disk.go
type IOCountersStat struct {
  ReadCount        uint64 `json:"readCount"`
  MergedReadCount  uint64 `json:"mergedReadCount"`
  WriteCount       uint64 `json:"writeCount"`
  MergedWriteCount uint64 `json:"mergedWriteCount"`
  ReadBytes        uint64 `json:"readBytes"`
  WriteBytes       uint64 `json:"writeBytes"`
  ReadTime         uint64 `json:"readTime"`
  WriteTime        uint64 `json:"writeTime"`
  // ...
}
複製程式碼

例如:

func main() {
  mapStat, _ := disk.IOCounters()
  for name, stat := range mapStat {
    fmt.Println(name)
    data, _ := json.MarshalIndent(stat, "", "  ")
    fmt.Println(string(data))
  }
}
複製程式碼

輸出包括所有分割槽,我這裡只展示一個:

C:
{
  "readCount": 184372,
  "mergedReadCount": 0,
  "writeCount": 42252,
  "mergedWriteCount": 0,
  "readBytes": 5205152768,
  "writeBytes": 701583872,
  "readTime": 333,
  "writeTime": 27,
  "iopsInProgress": 0,
  "ioTime": 0,
  "weightedIO": 0,
  "name": "C:",
  "serialNumber": "",
  "label": ""
}
複製程式碼

注意,disk.IOCounters()可傳入可變數量的字串引數用於標識分割槽,此引數在 Windows 上無效

分割槽

呼叫disk.PartitionStat(all bool)函式,返回分割槽資訊。如果all = false,只返回實際的物理分割槽(包括硬碟、CD-ROM、USB),忽略其它的虛擬分割槽。如果all = true則返回所有的分割槽。返回型別為[]PartitionStat,每個分割槽對應一個PartitionStat結構:

// src/github.com/shirou/gopsutil/disk/
type PartitionStat struct {
  Device     string `json:"device"`
  Mountpoint string `json:"mountpoint"`
  Fstype     string `json:"fstype"`
  Opts       string `json:"opts"`
}
複製程式碼
  • Device:分割槽標識,在 Windows 上即為C:這類格式;
  • Mountpoint:掛載點,即該分割槽的檔案路徑起始位置;
  • Fstype:檔案系統型別,Windows 常用的有 FAT、NTFS 等,Linux 有 ext、ext2、ext3等;
  • Opts:選項,與系統相關。

例如:

func main() {
  infos, _ := disk.Partitions(false)
  for _, info := range infos {
    data, _ := json.MarshalIndent(info, "", "  ")
    fmt.Println(string(data))
  }
}
複製程式碼

我的 Windows 機器輸出(只展示第一個分割槽):

{
  "device": "C:",
  "mountpoint": "C:",
  "fstype": "NTFS",
  "opts": "rw.compress"
}
複製程式碼

由上面的輸出可知,我的第一個分割槽為C:,檔案系統型別為NTFS

使用率

呼叫disk.Usage(path string)即可獲得路徑path所在磁碟的使用情況,返回一個UsageStat結構:

// src/github.com/shirou/gopsutil/disk.go
type UsageStat struct {
  Path              string  `json:"path"`
  Fstype            string  `json:"fstype"`
  Total             uint64  `json:"total"`
  Free              uint64  `json:"free"`
  Used              uint64  `json:"used"`
  UsedPercent       float64 `json:"usedPercent"`
  InodesTotal       uint64  `json:"inodesTotal"`
  InodesUsed        uint64  `json:"inodesUsed"`
  InodesFree        uint64  `json:"inodesFree"`
  InodesUsedPercent float64 `json:"inodesUsedPercent"`
}
複製程式碼
  • Path:路徑,傳入的引數;
  • Fstype:檔案系統型別;
  • Total:該分割槽總容量;
  • Free:空閒容量;
  • Used:已使用的容量;
  • UsedPercent:使用百分比。

例如:

func main() {
  info, _ := disk.Usage("D:/code/golang")
  data, _ := json.MarshalIndent(info, "", "  ")
  fmt.Println(string(data))
}
複製程式碼

由於返回的是磁碟的使用情況,所以路徑D:/code/golangD:返回同樣的結果,只是結構中的Path欄位不同而已。程式輸出:

{
  "path": "D:/code/golang",
  "fstype": "",
  "total": 475779821568,
  "free": 385225650176,
  "used": 90554171392,
  "usedPercent": 19.032789388496106,
  "inodesTotal": 0,
  "inodesUsed": 0,
  "inodesFree": 0,
  "inodesUsedPercent": 0
}
複製程式碼

主機

子包host可以獲取主機相關資訊,如開機時間、核心版本號、平臺資訊等等。

開機時間

host.BootTime()返回主機開機時間的時間戳:

func main() {
  timestamp, _ := host.BootTime()
  t := time.Unix(int64(timestamp), 0)
  fmt.Println(t.Local().Format("2006-01-02 15:04:05"))
}
複製程式碼

上面先獲取開機時間,然後通過time.Unix()將其轉為time.Time型別,最後輸出2006-01-02 15:04:05格式的時間:

2020-04-06 20:25:32
複製程式碼

核心版本和平臺資訊

func main() {
  version, _ := host.KernelVersion()
  fmt.Println(version)

  platform, family, version, _ := host.PlatformInformation()
  fmt.Println("platform:", platform)
  fmt.Println("family:", family,
  fmt.Println("version:", version)
}
複製程式碼

在我的 Win10 上執行輸出:

10.0.18362 Build 18362
platform: Microsoft Windows 10 Pro
family: Standalone Workstation
version: 10.0.18362 Build 18362
複製程式碼

終端使用者

host.Users()返回終端連線上來的使用者資訊,每個使用者一個UserStat結構:

// src/github.com/shirou/gopsutil/host/host.go
type UserStat struct {
  User     string `json:"user"`
  Terminal string `json:"terminal"`
  Host     string `json:"host"`
  Started  int    `json:"started"`
}
複製程式碼

欄位一目瞭然,看示例:

func main() {
  users, _ := host.Users()
  for _, user := range users {
    data, _ := json.MarshalIndent(user, "", " ")
    fmt.Println(string(data))
  }
}
複製程式碼

記憶體

快速開始中,我們演示瞭如何使用mem.VirtualMemory()來獲取記憶體資訊。該函式返回的只是實體記憶體資訊。我們還可以使用mem.SwapMemory()獲取交換記憶體的資訊,資訊儲存在結構SwapMemoryStat中:

// src/github.com/shirou/gopsutil/mem/
type SwapMemoryStat struct {
  Total       uint64  `json:"total"`
  Used        uint64  `json:"used"`
  Free        uint64  `json:"free"`
  UsedPercent float64 `json:"usedPercent"`
  Sin         uint64  `json:"sin"`
  Sout        uint64  `json:"sout"`
  PgIn        uint64  `json:"pgin"`
  PgOut       uint64  `json:"pgout"`
  PgFault     uint64  `json:"pgfault"`
}
複製程式碼

欄位含義很容易理解,PgIn/PgOut/PgFault這三個欄位我們重點介紹一下。交換記憶體是以為單位的,如果出現缺頁錯誤(page fault),作業系統會將磁碟中的某些頁載入記憶體,同時會根據特定的機制淘汰一些記憶體中的頁。PgIn表徵載入頁數,PgOut淘汰頁數,PgFault缺頁錯誤數。

例如:

func main() {
  swapMemory, _ := mem.SwapMemory()
  data, _ := json.MarshalIndent(swapMemory, "", " ")
  fmt.Println(string(data))
}
複製程式碼

程式

process可用於獲取系統當前執行的程式資訊,建立新程式,對程式進行一些操作等。

func main() {
  var rootProcess *process.Process
  processes, _ := process.Processes()
  for _, p := range processes {
    if p.Pid == 0 {
      rootProcess = p
      break
    }
  }

  fmt.Println(rootProcess)

  fmt.Println("children:")
  children, _ := rootProcess.Children()
  for _, p := range children {
    fmt.Println(p)
  }
}
複製程式碼

先呼叫process.Processes()獲取當前系統中執行的所有程式,然後找到Pid為 0 的程式,即作業系統的第一個程式,最後呼叫Children()返回其子程式。還有很多方法可獲取程式資訊,感興趣可檢視文件瞭解~

Windows 服務

winservices子包可以獲取 Windows 系統中的服務資訊,內部使用了golang.org/x/sys包。在winservices中,一個服務對應一個Service結構:

// src/github.com/shirou/gopsutil/winservices/winservices.go
type Service struct {
  Name   string
  Config mgr.Config
  Status ServiceStatus
  // contains filtered or unexported fields
}
複製程式碼

mgr.Config為包golang.org/x/sys中的結構,該結構詳細記錄了服務型別、啟動型別(自動/手動)、二進位制檔案路徑等資訊:

// src/golang.org/x/sys/windows/svc/mgr/config.go
type Config struct {
  ServiceType      uint32
  StartType        uint32
  ErrorControl     uint32
  BinaryPathName   string
  LoadOrderGroup   string
  TagId            uint32
  Dependencies     []string
  ServiceStartName string
  DisplayName      string
  Password         string
  Description      string
  SidType          uint32
  DelayedAutoStart bool
}
複製程式碼

ServiceStatus結構記錄了服務的狀態:

// src/github.com/shirou/gopsutil/winservices/winservices.go
type ServiceStatus struct {
  State         svc.State
  Accepts       svc.Accepted
  Pid           uint32
  Win32ExitCode uint32
}
複製程式碼
  • State:為服務狀態,有已停止、執行、暫停等;
  • Accepts:表示服務接收哪些操作,有暫停、繼續、會話切換等;
  • Pid:程式 ID;
  • Win32ExitCode:應用程式退出狀態碼。

下面程式中,我將系統中所有服務的名稱、二進位制檔案路徑和狀態輸出到控制檯:

func main() {
  services, _ := winservices.ListServices()

  for _, service := range services {
    newservice, _ := winservices.NewService(service.Name)
    newservice.GetServiceDetail()
    fmt.Println("Name:", newservice.Name, "Binary Path:", newservice.Config.BinaryPathName, "State: ", newservice.Status.State)
  }
}
複製程式碼

注意,呼叫winservices.ListServices()返回的Service物件資訊是不全的,我們通過NewService()以該服務名稱建立一個服務,然後呼叫GetServiceDetail()方法獲取該服務的詳細資訊。不能直接通過service.GetServiceDetail()來呼叫,因為ListService()返回的物件缺少必要的系統資源控制程式碼(為了節約資源),呼叫GetServiceDetail()方法會panic!!!

錯誤和超時

由於大部分函式都涉及到底層的系統呼叫,所以發生錯誤和超時是在所難免的。幾乎所有的介面都有兩個返回值,第二個作為錯誤。在前面的例子中,我們為了簡化程式碼都忽略了錯誤,在實際使用中,建議對錯誤進行處理。

另外,大部分介面都是一對,一個不帶context.Context型別的引數,另一個帶有該型別引數,用於做上下文控制。在內部呼叫發生錯誤或超時後能及時處理,避免長時間等待返回。實際上,不帶context.Context引數的函式內部都是以context.Background()為引數呼叫帶有context.Context的函式的:

// src/github.com/shirou/gopsutil/cpu_windows.go
func Times(percpu bool) ([]TimesStat, error) {
  return TimesWithContext(context.Background(), percpu)
}

func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) {
  // ...
}
複製程式碼

總結

gopsutil庫方便了我們獲取本機的資訊,且很好地處理了各個系統間的相容問題,提供了一致的介面。還有幾個子包例如net/docker限於篇幅沒有介紹,感興趣的童鞋可自行探索。

大家如果發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue?

參考

  1. gopsutil GitHub:github.com/shirou/gops…
  2. Go 每日一庫 GitHub:github.com/darjun/go-d…

我的部落格:darjun.github.io

歡迎關注我的微信公眾號【GoUpUp】,共同學習,一起進步~

Go 每日一庫之 gopsutil

相關文章