记一次第三方库的PR
背景
线上很多场景可能会用到pstree,比如查看容器的所有子进程,回滚任务需要杀死正在运行中的子进程。
一个轻量级的库
我们选用的是一个轻量级的pstree。github.com/sbinet/pstree
这个库比较简单,首先遍历/proc/ 获取所有PID
files, err := filepath.Glob("/proc/[0-9]*")
...
procs := make(map[int]Process, len(files))
for _, dir := range files {
proc, err := scan(dir)
if err != nil {
return nil, fmt.Errorf("could not scan %s: %w", dir, err)
}
if proc.Stat.Pid == 0 {
// process vanished since Glob.
continue
}
procs[proc.Stat.Pid] = proc
}
然后,scan()是读取/proc/[pid]/stat,获取pid和相应的ppid、name等信息
...
f, err := os.Open(filepath.Join(dir, "stat"))
if err != nil {
// process vanished since Glob.
return Process{}, nil
}
defer f.Close()
var proc Process
_, err = fmt.Fscanf(
f, statfmt,
&proc.Stat.Pid, &proc.Stat.Comm, &proc.Stat.State,
&proc.Stat.Ppid, &proc.Stat.Pgrp, &proc.Stat.Session,
&proc.Stat.Tty, &proc.Stat.Tpgid, &proc.Stat.Flags,
&proc.Stat.Minflt, &proc.Stat.Cminflt, &proc.Stat.Majflt, &proc.Stat.Cmajflt,
&proc.Stat.Utime, &proc.Stat.Stime,
&proc.Stat.Cutime, &proc.Stat.Cstime,
&proc.Stat.Priority,
&proc.Stat.Nice,
&proc.Stat.Nthreads,
&proc.Stat.Itrealval, &proc.Stat.Starttime,
&proc.Stat.Vsize, &proc.Stat.Rss,
)
...
正常情况下,tree建好之后,给定任意pid就可以查询这个pid的所有子孙进程了。
但是因为scan中用到了fmt.Fscanf
,这个函数是按照空格进行分割的,所以像c语言scanf %d (%s) %d
这样的尝试是无效的。当遇到包含空格的进程名,整个tree的建立就失败了。
比如,这个进程的stat文件
### cat /proc/1793371/stat
1793371 (PM2 v3.2.9: God) S 1785922 1793371 1793371 0 -1 4202752 702709694 1179 162 1 5202620 1577439 0 0 20 0 10 0 1726644485 1739554816 12966 18446744073709551615 4194304 33175580 140731369726544 140731369709608 140087382372809 0 0 4096 84487 18446744073709551615 0 0 17 24 0 0 40 0 0 35272736 35380656 51728384 140731369728149 140731369728224 140731369728224 140731369729996 0
fmt.Fscanf时会报错,
error: expected space in input to match format
按照/proc/[pid]/stat的格式http://man7.org/linux/man-pages/man5/proc.5.html
要求,comm字段是包含在圆括号中的。
(2) comm %s
The filename of the executable, in parentheses.
This is visible whether or not the executable is
swapped out.
这样可以将stat的内容按照圆括号进行split,comm字段直接赋值给name ,然后再分别处理pid 和comm后的字段。为了少处理一次error, 将comm之前的pid字段和之后的字段拼接起来,再用fmt.Sscanf
处理,最终实现如下
stat := filepath.Join(dir, "stat")
data, err := ioutil.ReadFile(stat)
if err != nil {
// process vanished since Glob.
return Process{}, nil
}
// extracting the name of the process, enclosed in matching parentheses.
info := strings.FieldsFunc(string(data), func(r rune) bool {
return r == '(' || r == ')'
})
if len(info) != 3 {
return Process{}, fmt.Errorf("%s: file format invalid", stat)
}
var proc Process
proc.Stat.Comm = info[1]
_, err = fmt.Sscanf(
info[0]+info[2], statfmt,
&proc.Stat.Pid, &proc.Stat.State,
&proc.Stat.Ppid, &proc.Stat.Pgrp, &proc.Stat.Session,
&proc.Stat.Tty, &proc.Stat.Tpgid, &proc.Stat.Flags,
&proc.Stat.Minflt, &proc.Stat.Cminflt, &proc.Stat.Majflt, &proc.Stat.Cmajflt,
&proc.Stat.Utime, &proc.Stat.Stime,
&proc.Stat.Cutime, &proc.Stat.Cstime,
&proc.Stat.Priority,
&proc.Stat.Nice,
&proc.Stat.Nthreads,
&proc.Stat.Itrealval, &proc.Stat.Starttime,
&proc.Stat.Vsize, &proc.Stat.Rss,
)
if err != nil {
return proc, fmt.Errorf("could not parse file %s: %w", stat, err)
}
PR https://github.com/sbinet/pstree/pull/3
已被作者merge