you are better than you think

记一次第三方库的PR

· by thur · Read in about 2 min · (329 Words)
golang || pstree

背景

线上很多场景可能会用到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

Comments