进程基础

雨天,闲来无事用go语言总结一哈进程;

进程的定义

进程是计算机中已运行程序的实体.用户下达运行程序的命令后,就会产生进程.进程需要一些资源才能完成工作,如CPU使用时间、存储器、文件以及I/O设备,且为依序逐一进行,也就是每个CPU核心任何时间内仅能运行一项进程。
简而言之:进程就是代码运行的实体.这里补充一点,进程不一定都是正在运行的,也可能在等待调度或者停止.

PID

PID全称Process ID,是标识和区分进程的ID,它是一个全局唯一的正整数.

原来Hello World进程运行时也有一个PID,只是它运行结束后PID也释放,通过Getpid()函数可以获得当前进程的PID。

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"os"
)

func main() {
fmt.Println(os.Getpid())
}

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

PPID

每个进程除了一定有PID还会有PPID,也就是父进程ID,通过PPID可以找到父进程的信息.
为什么进程都会有父进程ID?
进程都是由父进程衍生出来的,父进程的父进程的父进程又是什么呢?实际上有一个PID为1的进程是由内核创建的init进程,其他子进程都是由它衍生出来,所以前面的描述并不准确,进程号为1的进程并没有PPID。

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

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

1
2
3
4
5
6
7
8
9
10
package main

import (
"fmt"
"os"
)

func main() {
fmt.Println(os.Getppid())
}

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

进程参数

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

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

1
cp suisuisui.txt sui.txt

进程输入与输出

每个进程操作系统都会分配三个文件资源,分别是标准输入(STDIN)、标准输出(STDOUT)和错误输出(STDERR)。通过这些输入流,我们能够轻易得从键盘获得数据,然后在显示器输出数据。

标准输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"io/ioutil"
"os"
)

func main() {
bytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}

fmt.Println(string(bytes))
}

标准输出

通过fmt.Println()把数据输出到屏幕上,这就是标准输出了

并发与并行

并发与并行

并发(Concurrently)和并行(Parallel)是两个不同的概念。借用Go创始人Rob Pike的说法,并发不是并行,并发更好。并发是一共要处理(deal with)很多事情,并行是一次可以做(do)多少事情。

举个简单的例子,随某人泡茶,必须有烧水、洗杯子、拿茶叶等步骤。现在我们想尽快做完这件事,也就是“一共要处理很多事情”,有很多方法可以实现并发,例如请多个人同时做,这就是并行。并行是实现并发的一种方式,但不是唯一的方式。我们一个人也可以实现并发,例如先烧水、然后不用等水烧开就去洗杯子,所以通过调整程序运行方式也可以实现并发

并行是实现并发的一种方式,在多核CPU的时代,并行是我们设计高效程序所要考虑的

进程越多越好?

Nginx是一个高性能、高并发的Web服务器,也就是说它可以同时处理超过10万个HTTP请求,而它建议的启动的进程数不要超过CPU个数,为什么呢?

我们首先要知道Nginx是Master-worker模型,Master进程只负责管理Worker进程,而Worker进程是负责处理真实的请求。每个Worker进程能够处理的请求数跟内存有关,因为在Linux上Nginx使用了epoll这种多路复用的IO接口,所以不需要多线程做并行也能实现并发。

而多进程有一个坏处就是带来了CPU上下文切换时间,所以一味提高进程个数反而使系统系能下降。当然如果当前进程小于CPU个数,就没有充分利用多核的资源,所以Nginx建议Worker数应该等于CPU个数。

进程状态

通过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、申请不到足够的缓冲区或者在等待信号。

进程文件

在Linux中“一切皆文件”,进程的一切运行信息(占用CPU、内存等)都可以在文件系统找到,例如看一下PID为1的进程信息。

1
cat /proc/1/status

ps命令获得的数据也是在这个文件系统获得的

死锁

死锁(Deadlock)就是一个进程拿着资源A请求资源B,另一个进程拿着资源B请求资源A,双方都不释放自己的资源,导致两个进程都进行不下去。

活锁

如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求……T2可能永远等待。

这显然是饿死(Starvation)的定义,进入活锁的进程是没有阻塞的,会继续使用CPU,但外界看到整个进程都没有前进。

活锁实例

举个很简单的例子,两个人相向过独木桥,他们同时向一边谦让,这样两个人都过不去,然后二者同时又移到另一边,这样两个人又过不去了。如果不受其他因素干扰,两个人一直同步在移动,但外界看来两个人都没有前进,这就是活锁。

活锁会导致CPU耗尽的,解决办法是引入随机变量、增加重试次数等。

所以活锁也是程序设计上可能存在的问题,导致进程都没办法运行下去了,还耗CPU。

POSIX**

一种操作系统的接口标准,至于谁遵循这个标准呢?就是大名鼎鼎的Unix和Linux了

有了这个规范,你就可以调用通用的API了,Linux提供的POSIX系统调用在Unix上也能执行,因此学习Linux的底层接口最好就是理解POSIX标准。

POSIX进程

我们运行Hello World程序时,操作系统通过POSIX定义的fork和exec接口创建起一个POSIX进程,这个进程就可以使用通用的IPC、信号等机制。

POSIX线程

POSIX也定义了线程的标准,包括创建和控制线程的API,在Pthreads库中实现

纵有疾风起,人生不言弃!