专栏首页优雅R「笔记」理解Linux进程

「笔记」理解Linux进程

原文项目:https://github.com/tobegit3hub/understand_linux_process[1]原文阅读地址:https://tobegit3hub1.gitbooks.io/understanding-linux-processes/content/[2]

进程(Process)是计算机中已运行程序的实体。用户下达运行程序的命令后,就会产生进程。每个CPU核心任何时间内仅能运行一项进程。

PID 和 PPID

PID全称Process ID,是标识和区分进程的ID,它是一个全局唯一的正整数。每个进程除了一定有PID还会有PPID,也就是父进程ID,通过PPID可以找到父进程的信息。

跟人类起源问题一样,父进程的父进程的父进程又是什么呢?实际上有一个PID为1的进程是由内核创建的init进程,其他子进程都是由它衍生出来,所以前面的描述并不准确,进程号为1的进程并没有PPID。因为所有进程都来自于一个进程,所以Linux的进程模型也叫做进程树。

Golang 代码:

package main

import (
 "fmt"
 "os"
)

func main() {
 fmt.Println("PID: ", os.Getpid())
 fmt.Println("PPID", os.Getppid())
}

结果:

$ go run "/Users/wsx/Documents/GitHub/self-study/go/prac/pid.go"
PID:  94788
PPID 94771

查看和使用 PID

使用 top 命令后,第一列显示的就是 PID。

ps 命令也可以查到 PID 信息:

一般我们想要杀死某个程序时会用到 PID,通过kill命令来结束进程,也可以通过 kill -9或其他数字向进程发送不同的信号

进程名字

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

package main

import (
 "fmt"
 "os"
)

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

查看输出:

$ go run "/Users/wsx/Documents/GitHub/self-study/go/prac/pname.go"
/var/folders/bj/nw1w4g1j37ddpgb6zmh3sfh80000gn/T/go-build836639686/b001/exe/pname

进程参数

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

进程参数一般可分为两类,一类是程序名,一类是Argument,也就是作为进程运行的实体参数。例如,cp config.yml config.yml.bakcp 是程序名,后 2 个是传入程序的实体参数。

package main

import (
 "fmt"
 "os"
)

func main() {
 argsWithProg := os.Args
 argsWithoutProg := os.Args[1:]

 arg := os.Args[3]
 fmt.Println(argsWithProg)
 fmt.Println(argsWithoutProg)
 fmt.Println(arg)
}

查看输出:

$ go run ./cla.go a b c d e
[/var/folders/bj/nw1w4g1j37ddpgb6zmh3sfh80000gn/T/go-build409118155/b001/exe/cla a b c d e]
[a b c d e]
c

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

进程输入输出

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

标准输入

来自管道(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))
}

运行:

$ echo 123 | go run ./stdin.go 
123

标准输出

上面输出的 123 就是标准输出。

错误输出

程序的错误输出与标准输出类似,一般是程序打印的错误信息会输出到错误输出中。

并发与并行

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

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

进程状态

我们知道进程是代码运行的实体,而进程有可能是正在运行的,也可能是已经停止的,这就是进程的状态。可以查看 Linux 源码中定义的进程状态:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
  "R (running)",        /*   0 */
  "S (sleeping)",        /*   1 */
  "D (disk sleep)",    /*   2 */
  "T (stopped)",        /*   4 */
  "t (tracing stop)",    /*   8 */
  "X (dead)",        /*  16 */
  "Z (zombie)",        /*  32 */
};

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

退出码

任何进程退出时,都会留下退出码,操作系统根据退出码可以知道进程是否正常运行。退出码是0到255的整数,通常0表示正常退出,其他数字表示不同的错误

$ cat yes
cat: yes: No such file or directory                                                                                                                    ⍉
$ echo $? # 获取上一条命令退出码
1

image.png

进程文件

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

[email protected]:/go/src# ls /proc/1/
attr        cmdline          cwd      fdinfo   loginuid   mounts      numa_maps      pagemap      sessionid  status   wchan
auxv        comm             environ  gid_map  maps       mountstats  oom_adj        personality  smaps      syscall
cgroup      coredump_filter  exe      io       mem        net         oom_score      projid_map   stat       task
clear_refs  cpuset           fd       limits   mountinfo  ns          oom_score_adj  root         statm      uid_map

我们可以看一下它的运行状态,通过cat /proc/1/status即可。

[email protected]:/go/src# cat /proc/1/status
Name:   bash
State:  S (sleeping)
Tgid:   1
Ngid:   0
Pid:    1
PPid:   0
TracerPid:      0
Uid:    0       0       0       0
Gid:    0       0       0       0
FDSize: 256
Groups:
VmPeak:    20300 kB
VmSize:    20300 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:      3228 kB
VmRSS:      3228 kB
VmData:      408 kB
VmStk:       136 kB
VmExe:       968 kB
VmLib:      2292 kB
VmPTE:        60 kB
VmSwap:        0 kB
Threads:        1
SigQ:   0/3947
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000010000
SigIgn: 0000000000380004
SigCgt: 000000004b817efb
CapInh: 00000000a80425fb
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
Seccomp:        0
Cpus_allowed:   1
Cpus_allowed_list:      0
Mems_allowed:   00000000,00000001
Mems_allowed_list:      0
voluntary_ctxt_switches:        684
nonvoluntary_ctxt_switches:     597

死锁

死锁(Deadlock)就是一个进程拿着资源A请求资源B,另一个进程拿着资源B请求资源A,双方都不释放自己的资源,导致两个进程都进行不下去。我们可以写代码模拟进程死锁的例子。

package main
func main() {
  ch := make(chan int)
  <-ch
}
$ go run "/Users/wsx/Documents/GitHub/self-study/go/prac/deadlock.go"
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
 /Users/wsx/Documents/GitHub/self-study/go/prac/deadlock.go:5 +0x4d
exit status 2

这里Go虚拟机已经替我们检测出死锁的情况,因为所有Goroutine都阻塞住没有运行。

活锁

进入活锁的进程是没有阻塞的,会继续使用CPU,但外界看到整个进程都没有前进。举个很简单的例子,两个人相向过独木桥,他们同时向一边谦让,这样两个人都过不去,然后二者同时又移到另一边,这样两个人又过不去了。如果不受其他因素干扰,两个人一直同步在移动,但外界看来两个人都没有前进,这就是活锁。活锁会导致CPU耗尽的,解决办法是引入随机变量、增加重试次数等。

POSIX

POSIX(Portable Operation System Interface)听起来好高端,就是一种操作系统的接口标准,至于谁遵循这个标准呢?就是大名鼎鼎的Unix和Linux了,有人问Mac OS是否兼容POSIX呢,答案是Yes苹果的操作系统也是Unix-based的。有了这个规范,你就可以调用通用的API了,Linux提供的POSIX系统调用在Unix上也能执行,因此学习Linux的底层接口最好就是理解POSIX标准。我们运行Hello World程序时,操作系统通过POSIX定义的forkexec接口创建起一个POSIX进程,这个进程就可以使用通用的IPC、信号等机制。POSIX也定义了线程的标准,包括创建和控制线程的API。

nohup

Nohup的原理很简单,终端关闭后会给此终端下的每一个进程发送SIGHUP信号,而使用nohup运行的进程则会忽略这个信号,因此终端关闭后进程也不会退出。

Go 进程编程

Go 能够执行任意 Go 或者非 Go 程序,并且等待放回结果,外部进程结束后继续执行 Go 程序。

衍生进程

如果你的程序需要执行外部命令,可以直接使用exec.Command()来Spawn(衍生)进程,并且根据需要获得外部程序的返回值。

package main

import (
 "fmt"
 "io/ioutil"
 "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 grep\ngoodbye 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))
}

执行外部程序

与Spawn不同,执行外部程序并不会返回到原进程中,也就是让外部程序完全取代本进程。

package main

import "syscall"
import "os"
import "os/exec"

func main() {
    binary, lookErr := exec.LookPath("ls")
    if lookErr != nil {
        panic(lookErr)
    }
    args := []string{"ls", "-a", "-l", "-h"}
    env := os.Environ()
    execErr := syscall.Exec(binary, args, env)
    if execErr != nil {
        panic(execErr)
    }
}

进程进阶

进程锁

它是通过记录一个PID文件,避免两个进程同时运行的文件锁。其实要实现一个进程锁很简单,通过文件就可以实现了。例如程序开始运行时去检查一个PID文件,如果文件存在就直接退出,如果文件不存在就创建一个,并把当前进程的PID写入文件中。这样我们很容易可以实和读锁,但是所有流程都需要自己控制。当然根据DRY(Don't Repeat Yourself)原则,Linux已经为我们提供了flock接口。

孤儿进程

孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。也就是父进程不在了,子进程还在运行。在现实中用户可能刻意使进程成为孤儿进程,这样就可以让它与父进程会话脱钩,成为后面会介绍的守护进程

僵尸进程

当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

守护进程

我们可以认为守护进程就是后台服务进程,因为它会有一个很长的生命周期提供服务,关闭终端不会影响服务,也就是说可以忽略某些信号。

进程间通信

  • 管道:管道是进程间通信最简单的方式,任何进程的标准输出都可以作为其他进程的输入。
  • 信号。
  • 消息队列。
  • 信号量:本质上是一个整型计数器,调用wait时计数减一,减到零开始阻塞进程,从而达到进程、线程间协作的作用。
  • 套接字:通过网络来通信,这也是最通用的IPC,不要求进程在同一台服务器上。

信号

信号只是告诉进程发生了什么事件,而不会传递任何数据。Linux中定义了很多信号,不同的Unix-like系统也不一样,我们可以通过下面的命令来查当前系统支持的种类。

$ kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2

其中1至31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),32到63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。在命令行中止一个程序我们一般摁Ctrl+C,这就是发送SIGINT信号,而使用kill命令呢?默认是SIGTERM,加上-9参数才是SIGKILL。

系统调用

我们要想启动一个进程,需要操作系统的调用(system call)。实际上操作系统和普通进程是运行在不同空间上的,操作系统进程运行在内核态(kernel space),开发者运行的进程运行在用户态(user space),这样有效规避了用户程序破坏系统的可能。如果用户态进程想执行内核态的操作,只能通过系统调用。

文件描述符

Linux很重要的设计思想就是一切皆文件,网络是文件,键盘等外设也是文件。内核给每个访问的文件分配了文件描述符(File Descriptor),它本质是一个非负整数,在打开或新建文件时返回,以后读写文件都要通过这个文件描述符了。

POSIX已经定义了STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO三个常量,也就是0、1、2。这三个文件描述符是每个进程都有的,这也解释了为什么每个进程都有编号为0、1、2的文件而不会与其他进程冲突。

文件描述符帮助应用找到这个文件,而文件的打开模式等上下文信息存储在文件对象中,这个对象直接与文件描述符关联。

注意了,每个系统对文件描述符个数都有限制。我们网上看到配置ulimit也是为了调大系统的打开文件个数,因为一般服务器都要同时处理成千上万个起请求,记住socket连接也是文件哦,使用系统默认值会出现莫名奇怪的问题。

Epoll

Epoll是poll的改进版,更加高效,能同时处理大量文件描述符,跟高并发有关,Nginx就是充分利用了epoll的特性。Poll本质上是Linux系统调用,其接口为int poll(struct pollfd *fds,nfds_t nfds, int timeout),作用是监控资源是否可用。举个例子,一个Web服务器建了多个socket连接,它需要知道里面哪些连接传输发了请求需要处理。poll会轮询整个文件描述符集合,而epoll可以做到只查询被内核IO事件唤醒的集合,当然它还提供边沿触发(Edge Triggered)等特性。简单来说epoll是基于文件描述符的callback函数来实现的,只有发生IO时间的socket会调用callback函数,然后加入epoll的Ready队列。

共享内存

不同进程之间内存空间是独立的,也就是说进程不能访问也不会干扰其他进程的内存。如果两个进程希望通过共享内存的方式通信呢?可以通过mmap()系统调用实现。

Copy on write

一般我们运行程序都是Fork一个进程后马上执行Exec加载程序。Copy On Write的含义是只有真正写的时候才把数据写到子进程的数据,Fork时只会把页表复制到子进程,这样父子进程都指向同一个物理内存页,只有在写子进程的时候才会把内存页的内容重新复制一份。

Cgroups

Cgroups全称Control Groups,是Linux内核用于资源隔离的技术。目前Cgroups可以控制CPU、内存、磁盘访问。我们首先在文件系统创建Cgroups组,然后修改这个组的属性,启动进程时指定加入的Cgroups组,这样进程相当于在一个受限的资源内运行了。Cgroups是Docker容器技术的基础,另一项技术是大名鼎鼎的Namespaces。

Namespace

Namespaces是容器技术的基础,因为有了命名空间的隔离,才能限制容器之间的进程通信,像虚拟内存对于物理内存那样,开发者无需针对容器修改已有的代码。Linux内核提供了clone系统调用,创建进程时使用clone取代fork即刻创建同一命名空间下的进程。

参考资料

[1]

https://github.com/tobegit3hub/understand_linux_process: https://github.com/tobegit3hub/understand_linux_process

[2]

https://tobegit3hub1.gitbooks.io/understanding-linux-processes/content/: https://tobegit3hub1.gitbooks.io/understanding-linux-processes/content/

本文分享自微信公众号 - 优雅R(elegant-r),作者:王诗翔

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-04-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 《理解 Unix 进程》笔记-1

    Unix 系统是由用户空间(userland)和内核组成。Unix 内核位于计算机硬件之上,是与硬件交互的中介。这些交互包括通过问卷系统进程读/写、在网络上发送...

    goodspeed
  • Linux笔记(10)| 进程概述

    父进程返回正整数,子进程返回0,在执行fork函数之前,操作系统只有一个进程,fork函数之前的,代码只会被执行一次,在执行fork函数之后,操作系统有两个几乎...

    飞哥
  • 多线程笔记(一)程序,进程,线程分别如何理解

    可以理解为多个路,如果现在有10辆车,要从A点到B点,如果只有一条路,那么10辆车是需要排队的,但是如果现在有10条路,那么同一时间,10辆车就同时到达了,不需...

    一天不写程序难受
  • Android 进阶解密笔记-Android 系统进程

    僵尸进程:在Linux中,父进程使用fork创建子进程,子进程终止后,但父进程不知道子进程终止,虽然子进程已经退出,但系统还未它保留一定的信息(比如进程号,退出...

    Yif
  • 【Linux笔记】make工程管理工具(二)

    上一篇笔记分享了使用make工具编译C程序的方法(【Linux笔记】make工程管理工具(一)),但是还未分享make工具是什么,本篇笔记就来看一下make工具...

    正念君
  • 【Linux笔记】make工程管理工具(一)

    上一篇笔记写了如何使用gcc的编译命令编译:【Linux笔记】Linux下编译C程序。当源文件较少时,使用gcc编译命令编译就比较方便,在gcc编译命令中依次列...

    正念君
  • Linux笔记(2)| 进阶命令

    当你知道你要找的文件名,但是你忘记了它被放在哪个目录下,要找到该文件时,用find。

    飞哥
  • Linux之守护进程理解(2)

    1、屏蔽一些有关控制终端操作的信号 防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起。 2、脱离控制终端,登录会话和进程组 登录会话可以包含多...

    chain
  • Linux进程管理

    每个用户均可同时运行多个程序。为了区分每一个运行的程序,Linux给每个进程都做了标识,称为进程号(process ID),每个进程的进程号是唯一的。

    Java3y
  • Linux进程管理

    本文包括: 查看进程命令 ps、查看进程树命令 pstree、实时显示进程命令 top、查看后台任务命令 jobs、后台任务调至前台命令 fg、终止进程命令 k...

    Theo Tsao
  • Linux 进程管理

    进程是 UNIX/Linux 用来表示正在运行的程序的一种抽象概念,所有系统上面运行的的数据都会以进程的形态存在。

    用户1679793
  • Linux进程管理

    ps命令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵...

    职场亮哥
  • linux进程管理

    查看进行使用的指令是 ps ,一般来说使用的参数是 ps -aux,ps -ef,正常与grep连用

    小小咸鱼YwY
  • Linux笔记(16)| 进程同步机制——管道和IPC

    今天要分享的是Linux进程的同步机制,包括管道和IPC。之前学习的信号也有控制进程同步的作用,但是信号仅仅传输很少的信息,而且系统开销大,所以这里再介绍几种其...

    飞哥
  • Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)

    exit是c语言的库函数,他最终调用_exit。在此之前,先清洗标准输出的缓存,调用用atexit注册的函数等, 在c语言的main函数中调用return就等价...

    233333
  • Python 学习笔记 - 多进程和进程

    前面学习了多线程,接下来学习多进程的创建和使用。多进程更适合计算密集型的操作,他的语法和多线程非常相像,唯一需要注意的是,多线程之间是可以直接共享内存数据的;但...

    py3study
  • Linux进程详解

    程序是指储存在外部存储(如硬盘)的一个可执行文件, 而进程是指处于执行期间的程序, 进程包括 代码段(text section) 和 数据段(data sect...

    用户7686797
  • linux之进程管理

    用户 进程Id 占用cpu 占用内存 虚拟内存 物理内存 使用的终端 当前状态 启动时间 占用cpu总计时 ...

    西西嘛呦
  • 【Linux】学习笔记(八) Linux 磁盘管理

    韩旭051

扫码关注云+社区

领取腾讯云代金券