- Linux进程阅读
- 0.疑惑
- 1.Docker环境进行
- 2.进程基础
- a.进程输入与输出
- 3.并发与并行
- 举个例子: Nginx
- 4.进程状态
- 5.查看状态
- 6.进程文件
- 7.POSIX
- 8.NuHup
- 9.衍生(Spawn)新进程
- 10.复制进程
- 11.进程进阶
- a.进程锁
- b.孤儿进程
- c.僵尸进程
- d.守护进程
- e.一些概念
- f.系统调用
- g.Epoll
- h.写时复制
- i.创建目录权限
- 12.源文地址
- 13.END
僵尸进程、还有COW(Copy On Write)、Flock(File Lock)、Epoll和Namespace的概念又是否了解过呢?
1.Docker环境进行docker run -i -t tobegit3hub/understand_linux_process_examp2.进程基础
PPID 就是父进程ID 第一个P是Parent
为什么进程都会有父进程ID呢?因为进程都是由父进程衍生出来的,后面会详细介绍几种衍生的方法。那么跟人类起源问题一样,父进程的父进程的父进程又是什么呢?实际上有一个PID为1的进程是由内核创建的init进程,其他子进程都是由它衍生出来,所以前面的描述并不准确,进程号为1的进程并没有PPID。
因为所有进程都来自于一个进程,所以Linux的进程模型也叫做进程树。
要想获得进程的PPID,可以通过以下Getppid()这个函数来获得
有趣的事情发生了,有没有发现每次运行的父进程ID都不一样,这不符合我们的预期啊,原来我们通过go run每次都会启动一个新的Go虚拟机来执行进程。
拿到PID后,我们就可以通过kill命令来结束进程了,也可以通过kill -9或其他数字向进程发送不同的信号。
信号是个很重要的概念,我们后面会详细介绍,那么有了进程ID,我们也可以看看进程名字。
每个进程都一定有进程名字,例如我们运行top,进程名就是“top”,如果是自定义的程序呢?
其实进程名一般都是进程参数的第一个字符串,在Go中可以这样获得进程名。
获取进程参数
获得进程Flag
使用Flag可以更容易得将命令行参数转化成我们需要的数据类型,其中flag.go代码如下
a.进程输入与输出每个进程操作系统都会分配三个文件资源,分别是标准输入(STDIN)、标准输出(STDOUT)和错误输出(STDERR)。通过这些输入流,我们能够轻易得从键盘获得数据,然后在显示器输出数据。
来自管道(Pipe)的数据也是标准输入的一种,我们写了以下的实例来输出标注输入的数据。
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
3.并发与并行
并发(Concurrently)和并行(Parallel)是两个不同的概念。借用Go创始人Rob Pike的说法,并发不是并行,并发更好。并发是一共要处理(deal with)很多事情,并行是一次可以做(do)多少事情。
举个简单的例子,华罗庚泡茶,必须有烧水、洗杯子、拿茶叶等步骤。现在我们想尽快做完这件事,也就是“一共要处理很多事情”,有很多方法可以实现并发,例如请多个人同时做,这就是并行。并行是实现并发的一种方式,但不是唯一的方式。我们一个人也可以实现并发,例如先烧水、然后不用等水烧开就去洗杯子,所以通过调整程序运行方式也可以实现并发。
前面提到多进程的并行可以提高并发度,那么进程是越多越好?一般遇到这种问题都回答不是,事实上,很多大型项目都不会同时开太多进程。
下面以支持100K并发量的Nginx服务器为例。
举个例子: NginxNginx是一个高性能、高并发的Web服务器,也就是说它可以同时处理超过10万个HTTP请求,而它建议的启动的进程数不要超过CPU个数,为什么呢?
我们首先要知道Nginx是Master-worker模型,Master进程只负责管理Worker进程,而Worker进程是负责处理真实的请求。每个Worker进程能够处理的请求数跟内存有关,因为在Linux上Nginx使用了epoll这种多路复用的IO接口,所以不需要多线程做并行也能实现并发。
而多进程有一个坏处就是带来了CPU上下文切换时间,所以一味提高进程个数反而使系统系能下降。当然如果当前进程小于CPU个数,就没有充分利用多核的资源,所以Nginx建议Worker数应该等于CPU个数。
4.进程状态
通过ps aux可以看到进程的状态。
O:进程正在处理器运行,这个状态从来没有见过.
S:休眠状态(sleeping)
R:等待运行(runable)R Running or runnable (on run queue) 进程处于运行或就绪状态
I:空闲状态(idle)
Z:僵尸状态(zombie)
T:跟踪状态(Traced)
B:进程正在等待更多的内存页
D: 不可中断的深度睡眠,一般由IO引起,同步IO在做读或写操作时,cpu不能做其它事情,只能等待,这时进程处于这种状态,如果程序采用异步IO,这种状态应该就很少见到了
其中就绪状态表示进程已经分配到除CPU以外的资源,等CPU调度它时就可以马上执行了。运行状态就是正在运行了,获得包括CPU在内的所有资源。等待状态表示因等待某个事件而没有被执行,这时候不耗CPU时间,而这个时间有可能是等待IO、申请不到足够的缓冲区或者在等待信号。
6.进程文件 7.POSIX
8.NuHupPOSIX(Portable Operation System Interface)听起来好高端,就是一种操作系统的接口标准,至于谁遵循这个标准呢?就是大名鼎鼎的Unix和Linux了,有人问Mac OS是否兼容POSIX呢,答案是Yes苹果的操作系统也是Unix-based的。
有了这个规范,你就可以调用通用的API了,Linux提供的POSIX系统调用在Unix上也能执行,因此学习Linux的底层接口最好就是理解POSIX标准。
补充一句,目前很多编程语言(Go、Java、Python、Ruby等)都是天生跨平台的,因此我们很少注意系统调用的兼容性。实际上POSIX提供了这些语言上跨平台的语义,而且这是源码级别的保证。
9.衍生(Spawn)新进程每个开发者都会躺过这个坑,在命令行跑一个后台程序,关闭终端后发现进程也退出了,网上搜一下发现要用nohup,究竟什么原因呢?
原来普通进程运行时默认会绑定TTY(虚拟终端),关闭终端后系统会给上面所有进程发送TERM信号,这时普通进程也就退出了。当然还有些进程不会退出,这就是后面将会提到的守护进程。
Nohup的原理也很简单,终端关闭后会给此终端下的每一个进程发送SIGHUP信号,而使用nohup运行的进程则会忽略这个信号,因此终端关闭后进程也不会退出。
它能够执行任意Go或者非Go程序,并且等待放回结果,外部进程结束后继续执行本程序。
package main
import "fmt"
import "io/ioutil"
import "os/exec"
func main() {
dateCmd := exec.Command("date")
dateOut, err := dateCmd.Output()
if err != nil {
panic(err)
}
fmt.Println("> date")
fmt.Println(string(dateOut))
grepCmd := exec.Command("grep", "hello")
grepIn, _ := grepCmd.StdinPipe()
grepOut, _ := grepCmd.StdoutPipe()
grepCmd.Start()
grepIn.Write([]byte("hello grepngoodbye grep"))
grepIn.Close()
grepBytes, _ := ioutil.ReadAll(grepOut)
grepCmd.Wait()
fmt.Println("> grep hello")
fmt.Println(string(grepBytes))
lsCmd := exec.Command("bash", "-c", "ls -a -l -h")
lsOut, err := lsCmd.Output()
if err != nil {
panic(err)
}
fmt.Println("> ls -a -l -h")
fmt.Println(string(lsOut))
}
10.复制进程
如果我们仅仅想复制父进程的堆栈空间呢,很遗憾Go没有提供这样的接口,因为使用Spawn、Exec和Goroutine已经能覆盖绝大部分的使用案例了。
事实上无论是Spawn还是Exec都是通过实现Fork系统调用来实现的,后面将会详细介绍它的实现原理。
11.进程进阶 a.进程锁这里的进程锁与线程锁、互斥量、读写锁和自旋锁不同,它是通过记录一个PID文件,避免两个进程同时运行的文件锁。
使用PID文件锁还有一个好处,方便进程向自己发停止或者重启信号。Nginx编译时可指定参数--pid-path=/var/run/nginx.pid,进程起来后就会把当前的PID写入这个文件,当然如果这个文件已经存在了,也就是前一个进程还没有退出,那么Nginx就不会重新启动。进程管理工具Supervisord也是通过记录进程的PID来停止或者拉起它监控的进程的。
进程锁在特定场景是非常适用的,而操作系统默认不会为每个程序创建进程锁,那我们该如何使用呢?
其实要实现一个进程锁很简单,通过文件就可以实现了。例如程序开始运行时去检查一个PID文件,如果文件存在就直接退出,如果文件不存在就创建一个,并把当前进程的PID写入文件中。这样我们很容易可以实和读锁,但是所有流程都需要自己控制。
当然根据DRY(Don’t Repeat Yourself)原则,Linux已经为我们提供了flock接口。
b.孤儿进程c.僵尸进程我们经常听别人说到孤儿进程(Orphan Process),究竟是什么呢,现在我们一次理解透。
根据维基百科的解释,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。
孤儿进程与僵尸进程是完全不同的,后面会详细介绍僵尸进程。而孤儿进程借用了现实中孤儿的概念,也就是父进程不在了,子进程还在运行,这时我们就把子进程的PPID设为1。前面讲PID提到,操作系统会创建进程号为1的init进程,它没有父进程也不会退出,可以收养系统的孤儿进程。
在现实中用户可能刻意使进程成为孤儿进程,这样就可以让它与父进程会话脱钩,成为后面会介绍的守护进程。
d.守护进程 e.一些概念当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
理解了孤儿进程和僵尸进程,我们临时加了守护进程这一小节,守护进程就是后台进程吗?没那么简单。
- 进程间通信
IPC全称Interprocess Communication,指进程间协作的各种方法,当然包括共享内存,信号量或Socket等
- 管道
管道是进程间通信最简单的方式,任何进程的标准输出都可以作为其他进程的输入。
f.系统调用g.Epoll我们要想启动一个进程,需要操作系统的调用(system call)。实际上操作系统和普通进程是运行在不同空间上的,操作系统进程运行在内核态(todo: kernel space),开发者运行得进程运行在用户态(todo: user space),这样有效规避了用户程序破坏系统的可能。
如果用户态进程想执行内核态的操作,只能通过系统调用了。Linux提供了超多系统调用函数,我们关注与进程相关的系统调用后面也会详细讲解。
Epoll是poll的改进版,更加高效,能同时处理大量文件描述符,跟高并发有关,Nginx就是充分利用了epoll的特性。讲这些没用,我们先了解poll是什么。
- poll
Poll本质上是Linux系统调用,其接口为int poll(struct pollfd *fds,nfds_t nfds, int timeout),作用是监控资源是否可用。
举个例子,一个Web服务器建了多个socket连接,它需要知道里面哪些连接传输发了请求需要处理,功能与select系统调用类似,不过poll不会清空文件描述符集合,因此检测大量socket时更加高效。
h.写时复制一般我们运行程序都是Fork一个进程后马上执行Exec加载程序,而Fork的是否实际上用的是父进程的堆栈空间,Linux通过Copy On Write技术极大地减少了Fork的开销。
Copy On Write的含义是只有真正写的时候才把数据写到子进程的数据,Fork时只会把页表复制到子进程,这样父子进程都指向同一个物理内存页,只有再写子进程的时候才会把内存页的内容重新复制一份。
i.创建目录权限package main
import (
"fmt"
"os"
"syscall"
)
func main() {
mask := syscall.Umask(0)
defer syscall.Umask(mask)
err := os.MkdirAll("/tmp/gotest/", 0777)
if err != nil {
panic(err)
}
fmt.Println("Mkdir /tmp/gotest/")
}
这并不是Go的Bug,包括Linux系统调用都是这样的,创建目录除了给定的权限还要加上系统的Umask,Go也是如实遵循这种约定。
如果你想达到你的预期权限,知道Umask及其用法是必须的。
12.源文地址https://tobegit3hub1.gitbooks.io/understanding-linux-processes/content/process_advanced/file_lock.html
13.END


