PID 和 PPID

PID

首先我们来学习PID这个概念,PID全称Process ID,是标识和区分进程的ID,它是一个全局唯一的正整数。

Hello World进程运行时也有一个PID,只是它运行结束后PID也释放了,我们可以通过print_pid.go程序显示当前进程的PID。

示例程序

程序print_pid.go的源码如下,通过Getpid()函数可以获得当前进程的PID。

package main
import (
  "fmt"
  "os"
)
func main() {
    fmt.Println(os.Getpid())
}

运行结果

[email protected]:/go/src# go run print_pid.go
2922
[email protected]:/go/src# go run print_pid.go
2932

可以看出,进程运行时PID是由操作系统随机分配的,同一个程序运行两次会产生两个进程,当然也就有两个不同的PID。

PPID

每个进程除了一定有PID还会有PPID,也就是父进程ID,通过PPID可以找到父进程的信息。

为什么进程都会有父进程ID呢?因为进程都是由父进程衍生出来的,后面会详细介绍几种衍生的方法。那么跟人类起源问题一样,父进程的父进程的父进程又是什么呢?实际上有一个PID为1的进程是由内核创建的init进程,其他子进程都是由它衍生出来,所以前面的描述并不准确,进程号为1的进程并没有PPID

因为所有进程都来自于一个进程,所以Linux的进程模型也叫做进程树。

示例程序

要想获得进程的PPID,可以通过以下Getppid()这个函数来获得,print_ppid.go程序的代码如下。

package main
import (
  "fmt"
  "os"
  )
func main() {
    fmt.Println(os.Getppid())
}

运行结果

[email protected]:/go/src# go run print_ppid.go
2892
[email protected]:/go/src# go run print_ppid.go
2902

有趣的事情发生了,有没有发现每次运行的父进程ID都不一样,这不符合我们的预期啊,原来我们通过go run每次都会启动一个新的Go虚拟机来执行进程。

编译后运行

如果我们先生成二进制文件再执行结果会怎样呢?

[email protected]:/go/src# ./print_ppid
1
[email protected]:/go/src# ./print_ppid
1
[email protected]:/go/src# ps aux |grep "1" |grep -v "ps" |grep -v "grep"
root         1  0.0  0.3  20228  3184 ?        Ss   07:25   0:00 /bin/bash

这次我们发现父进程ID都是一样的了,而且通过ps命令可以看到父进程就是bash,说明通过终端执行命令其实是从bash这个进程衍生出各种子进程。

为了执行这个程序要查找包依赖、编译、打包、链接(和go build做一样的东西)然后执行,这是全新的进程。

查看PID

首先我们想知道进程的PID,可以通过top或者ps命令来查看。

Top

在命令行执行top后,得到类似下面的输出,可以看到目前有三个进程,PID分别是1、8和9。

top - 12:45:18 up 1 min,  0 users,  load average: 0.86, 0.51, 0.20
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:   2056748 total,   301984 used,  1754764 free,    20984 buffers
KiB Swap:  1427664 total,        0 used,  1427664 free.   231376 cached Mem
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
1 root      20   0    4312    692    612 S   0.0  0.0   0:00.23 sh
8 root      20   0   20232   3048   2756 S   0.0  0.1   0:00.03 bash
9 root      20   0   21904   2384   2060 R   0.0  0.1   0:00.00 top

PS

执行ps aux后输出如下,其中aux参数让ps命令显示更详细的参数信息。前面PID为9的top进程已经退出了,取而代之的是PID为11的ps进程。

[email protected]:/go/src# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.0   4312   692 ?        Ss   12:45   0:00 /bin/sh -c /bin/bash
root         8  0.0  0.1  20232  3224 ?        S    12:45   0:00 /bin/bash
root        11  0.0  0.0  17484  2000 ?        R+   12:46   0:00 ps aux

使用PID

拿到PID后,我们就可以通过kill命令来结束进程了,也可以通过kill -9或其他数字向进程发送不同的信号。

信号是个很重要的概念,我们后面会详细介绍,那么有了进程ID,我们也可以看看进程名字。

进程名字

每个进程都一定有进程名字,例如我们运行top,进程名就是“top”,如果是自定义的程序呢?其实进程名一般都是进程参数的第一个字符串,在Go中可以这样获得进程名。

package main
import (
  "fmt"
  "os"
)
func main() {
    processName := os.Args[0]
    fmt.Println(processName)
}

进程的输出结果如下。

[email protected]:/go/src# go run process_name.go
/tmp/go-build650749614/command-line-arguments/_obj/exe/process_name
[email protected]:/go/src# go build process_name.go
[email protected]:/go/src# ./process_name
./process_name

是否稍稍有些意外,因为go run会启动进程重新编译、链接和运行程序,因此每次运行的进程名都不相同,而编译出来的程序有明确的名字,所以它的进程的名字都是一样的。

进程参数

任何进程启动时都可以赋予一个字符串数组作为参数,一般名为ARGV或ARGS。

通过解析这些参数可以让你的程序更加通用,例如cp命令通过给定两个参数就可以复制任意的文件,当然如果需要的参数太多最好还是使用配置文件。

获得进程Argument

进程参数一般可分为两类,一是Argument,也就是作为进程运行的实体参数。例如cp config.yml config.yml.bak的这两个参数。

设计Go程序时可以轻易地获得这些参数,argument.go代码如下,代码来自https://gobyexample.com/command-line-arguments

package main
import "os"
import "fmt"
func main() {
  argsWithProg := os.Args
  argsWithoutProg := os.Args[1:]
  arg := os.Args[3]
  fmt.Println(argsWithProg)
  fmt.Println(argsWithoutProg)
  fmt.Println(arg)
}

运行效果如下。

$ go build command-line-arguments.go
$ ./command-line-arguments a b c d
[./command-line-arguments a b c d]
[a b c d]
c

可以看出通过os.Args,不管是不是实体参数都可以获得,但是对于类似开关的辅助参数,Go提供了另一种更好的方法。

获得进程Flag

使用Flag可以更容易得将命令行参数转化成我们需要的数据类型,其中flag.go代码如下,代码来自https://gobyexample.com/command-line-flags

package main
import "flag"
import "fmt"
func main() {
  wordPtr := flag.String("word", "foo", "a string")
  numbPtr := flag.Int("numb", 42, "an int")
  boolPtr := flag.Bool("fork", false, "a bool")
  var svar string
  flag.StringVar(&svar, "svar", "bar", "a string var")
  flag.Parse()
  fmt.Println("word:", *wordPtr)
  fmt.Println("numb:", *numbPtr)
  fmt.Println("fork:", *boolPtr)
  fmt.Println("svar:", svar)
  fmt.Println("tail:", flag.Args())
}

运行结果如下,相比直接使用os.Args代码也简洁了不少。

[email protected]:/go/src# ./flag -word=opt -numb=7 -fork -svar=flag
word: opt
numb: 7
fork: true
svar: flag
tail: []
[email protected]:/go/src# ./flag -h
Usage of ./flag:
  -fork=false: a bool
  -numb=42: an int
  -svar="bar": a string var
  -word="foo": a string

使用Go获取进程参数是很简单的,不过一旦参数太多,最佳实践还是使用配置文件。

进程参数只有在启动进程时才能赋值,如果需要在程序运行时进行交互,就需要了解进程的输入与输出了。

下一节:进程的概念大家都很熟悉,但你是否能准确说出僵尸进程的含义呢?还有COW(Copy On Write)、Flock(File Lock)、Epoll和Namespace的概念又是否了解过呢?