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的概念又是否了解过呢?