在Go开发中,通过os/exec启动外部进程是常见操作,要测量这些进程的内存使用量,需要结合操作系统的进程管理接口实现,不同系统的实现方式存在差异。

核心实现思路
os/exec启动外部进程后会返回*exec.Cmd对象,我们可以通过该对象的Process字段获取进程ID,再基于进程ID调用系统接口查询内存信息。Linux系统可以通过读取/proc文件系统获取进程内存数据,Windows系统可以调用系统API获取进程内存统计信息。
Linux系统下的实现
Linux系统中每个进程都会在/proc目录下对应一个以进程ID命名的文件夹,其中的status文件包含了进程的内存使用信息,我们只需要读取该文件并解析对应字段即可。
实现步骤
- 通过
cmd.Process.Pid获取外部进程的ID - 拼接/proc/[pid]/status文件路径
- 读取文件内容,找到VmRSS字段,该字段表示进程实际使用的物理内存大小
代码示例
package main
import (
"fmt"
"io/ioutil"
"os/exec"
"strings"
)
// 获取Linux下进程的物理内存使用量,单位KB
func getLinuxProcessMemory(pid int) (int, error) {
path := fmt.Sprintf("/proc/%d/status", pid)
content, err := ioutil.ReadFile(path)
if err != nil {
return 0, err
}
lines := strings.Split(string(content), "n")
for _, line := range lines {
if strings.HasPrefix(line, "VmRSS:") {
// 格式为 VmRSS: 12345 kB
fields := strings.Fields(line)
if len(fields) >= 2 {
var mem int
fmt.Sscanf(fields[1], "%d", &mem)
return mem, nil
}
}
}
return 0, fmt.Errorf("未找到VmRSS字段")
}
func main() {
// 启动一个外部进程,这里以sleep命令为例
cmd := exec.Command("sleep", "100")
err := cmd.Start()
if err != nil {
fmt.Println("启动进程失败:", err)
return
}
defer cmd.Process.Kill()
// 获取进程内存使用量
mem, err := getLinuxProcessMemory(cmd.Process.Pid)
if err != nil {
fmt.Println("获取内存失败:", err)
return
}
fmt.Printf("外部进程PID: %d, 物理内存使用量: %d KBn", cmd.Process.Pid, mem)
}
Windows系统下的实现
Windows系统没有/proc文件系统,需要调用kernel32.dll中的GetProcessMemoryInfo函数来获取进程内存信息,Go中可以通过syscall包调用系统API实现。
代码示例
package main
import (
"fmt"
"os/exec"
"syscall"
"unsafe"
)
// 定义Windows内存信息结构体
type processMemoryCounters struct {
Cb uint32
PageFaultCount uint32
PeakWorkingSetSize uint64
WorkingSetSize uint64
QuotaPeakPagedPoolUsage uint64
QuotaPagedPoolUsage uint64
QuotaPeakNonPagedPoolUsage uint64
QuotaNonPagedPoolUsage uint64
PagefileUsage uint64
PeakPagefileUsage uint64
}
// 获取Windows下进程的物理内存使用量,单位字节
func getWindowsProcessMemory(pid int) (uint64, error) {
// 打开进程获取句柄
handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION|syscall.PROCESS_VM_READ, false, uint32(pid))
if err != nil {
return 0, err
}
defer syscall.CloseHandle(handle)
// 加载kernel32.dll
kernel32 := syscall.NewLazyDLL("kernel32.dll")
getProcessMemoryInfo := kernel32.NewProc("GetProcessMemoryInfo")
var memCounters processMemoryCounters
memCounters.Cb = uint32(unsafe.Sizeof(memCounters))
// 调用API获取内存信息
ret, _, err := getProcessMemoryInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&memCounters)), uintptr(memCounters.Cb))
if ret == 0 {
return 0, err
}
// WorkingSetSize为工作集大小,即实际物理内存使用量
return memCounters.WorkingSetSize, nil
}
func main() {
// 启动一个外部进程,这里以notepad为例
cmd := exec.Command("notepad.exe")
err := cmd.Start()
if err != nil {
fmt.Println("启动进程失败:", err)
return
}
defer cmd.Process.Kill()
mem, err := getWindowsProcessMemory(cmd.Process.Pid)
if err != nil {
fmt.Println("获取内存失败:", err)
return
}
fmt.Printf("外部进程PID: %d, 物理内存使用量: %d 字节n", cmd.Process.Pid, mem)
}
注意事项
- 进程启动后需要等待一小段时间再获取内存信息,避免进程还未完成初始化导致数据不准确
- 不同系统的内存单位不同,Linux下VmRSS默认单位是KB,Windows下WorkingSetSize默认单位是字节,使用时需要做统一转换
- 如果外部进程已经退出,获取进程信息会返回错误,需要做好异常处理
- 测量内存时建议多次采样取平均值,减少瞬时波动带来的误差
跨平台兼容方案
如果需要在不同系统下运行,可以通过runtime.GOOS判断当前系统类型,分别调用对应的实现方法:
package main
import (
"fmt"
"os/exec"
"runtime"
)
func getProcessMemory(pid int) (uint64, error) {
switch runtime.GOOS {
case "linux":
memKB, err := getLinuxProcessMemory(pid)
if err != nil {
return 0, err
}
// 转换为字节
return uint64(memKB) * 1024, nil
case "windows":
return getWindowsProcessMemory(pid)
default:
return 0, fmt.Errorf("不支持的操作系统: %s", runtime.GOOS)
}
}
func main() {
cmd := exec.Command("sleep", "100")
err := cmd.Start()
if err != nil {
fmt.Println("启动进程失败:", err)
return
}
defer cmd.Process.Kill()
mem, err := getProcessMemory(cmd.Process.Pid)
if err != nil {
fmt.Println("获取内存失败:", err)
return
}
fmt.Printf("外部进程内存使用量: %d 字节n", mem)
}