【Linux】进程概念
本篇博客是有关进程状态
的,好久没有写Linux的博客了,一起来看看吧!
实验系统:
CentOS 7.6
1.系统进程的运行状态
当我们想到进程的时候,一定要首先想到task_struct
结构体。该结构体内部有一个state状态码,用于标识当前进程处于什么状态
1.1 运行态
CPU会有一个进程队列(双链表),队列的每一个成员都是一个task_struct
结构体,用来维护即将运行的进程。当轮到某个进程运行的时候,CPU就会将这个进程的数据和代码放入内存和自己的寄存器,并开始运行
只要进入了运行队列的进程,就是运行态
的进程
所以运行态并不是正在运行的进程
为什么我们对这件事的感知不大呢?那是因为现代的CPU的运行速度非常快,这些运行队列的轮转周期很短
1.2 终止态
终止态:进程还在,但是永远不会运行,在队列中等待被释放
为什么进程都终止了,不立马释放对应的资源,而需要维护一个终止态?
- 这是因为当前CPU/操作系统正在忙着干其他的事情,没时间过来释放你。所以会将不运行的进程放入终止态的队列(将该进程
task_struct
结构体插入该队列) - 当操作系统空闲的时候,便会取终止态队列里面的进程进行释放
1.3 阻塞态
一个进程使用资源的时候,不仅仅会申请CPU的计算资源,还有可能申请其他更多的资源,比如网络/硬盘/网卡/显卡
等等
如果申请这些资源的时候得不到满足,就需要排队
- CPU资源:运行队列
- 其他资源:也需要进行排队
下面用伪代码的方式来描述一下进程为何会阻塞
1 | struct cpuinfo |
当我们的进程访问某种资源,特别是外存(磁盘)这种慢设备资源的时候,如果磁盘暂时还没有准备好,操作系统就会把当前进程从运行队列剥离,插入到对应需要访问的设备下的等待队列中
DDR4内存
的读写大约是40GB/S,即便是现在市面上较快的pcie4.0固态硬盘
其读写速度也只有7GB/S左右,这个差距还是很大的。所以才说磁盘是“慢设备”资源
操作系统移动task_struct
到对应的队列下,就是起了它管理进程的作用。同时将进程剥离运行队列,也能让等待慢设备资源的进程不至于把整个系统卡死。当我们电脑上运行的进程很多的时候,就有可能遇到当前的进程在等待过程中出现了阻塞,此时进程的代码不会运行。最直观的反应便是当前进程卡住不动了。
1.4 进程挂起
进程挂起和进程阻塞很类似,但也有不同。
进程挂起和阻塞不同的是,阻塞只是单纯地在等待慢资源。而挂起则是该进程的数据被放入回了磁盘,进程本身依旧在排队等待。操作系统会有一个专门的swap
分区,用来存放挂起进程
的代码和数据。
操作系统这么管理,是为了不让内存在多进程运行的时候不够用了。这也是为什么,当我们内存不够用的时候,往往伴随着磁盘的频繁读取。
当然,内存不够也有可能是某一个进程需要一次性加载的代码数据已经超过了内存的大小😂
1.5 进程状态转换图
下图中左下角的部分便演示了操作系统在处理阻塞和挂起态的操作循环
下面这个图是一个简洁的版本。
2.Linux下的进程状态描述
在linux下进程的状态是存放在一个数组里面的
2.1 S和R状态的说明
其中R对应的是运行态,S对应的就是阻塞态(linux下为休眠)
我们可以运行一个程序看看它处于什么状态
1 |
|
运行发现,左侧打印出了当前进程PID
,而右侧当我们使用ps
命令查询该进程的时候,发现该进程的状态是S+
也就是休眠状态
1 | [muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test |
这是为什么?程序不是一直都在运行吗?
首先需要知道的是,printf
需要将数据打印输出到屏幕上,屏幕作为外设,同样属于慢资源。所以我们的进程绝大部分时间都是处于sleep(1)
以及等待屏幕刷新的过程中。而CPU只需要执行printf
一个操作,这个操作几乎是瞬间就进行,当然也看不到该进程处于运行态了
即便我们把sleep(1)
去掉,进程也是需要等待屏幕刷新,同样处于S+
状态
那要怎样才能让进程处于运行态呢?很简单,我们写一个不和外设交互的死循环即可
1 | #include <stdio.h> |
可以看到现在进程的状态是R+
,处于运行态!
1 | [muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test |
2.2 D状态disk sleep
除了基本的S状态,linux下还有一个专门的disk sleep
状态。如同它的名字一样,这个状态是专门为访问硬盘的进程设计的
假设有下面这样一个场景
1 | 1.进程A需要访问磁盘资源,写入1GB的数据 |
出现这种数据丢失,谁都不想的嘛。所以Linux就设置了一个D
状态,
- S 浅度睡眠
- D 深度睡眠
处于D状态的进程不能被操作系统kill
掉。要想杀掉一个D状态的进程,只有下面三种办法
- 等硬盘读写完毕,给进程返回结果之后,进程从D状态变成其他状态,操作系统进行处理
- 关机重启
- 拔掉电脑的电源
linux下可以用
DD
命令直接对硬盘进操作
3.僵尸进程Z
僵尸进程对应的状态码是Z
,而X
是1.2
提到的终止态
3.1 为什么会存在僵尸进程?
当Linux中的一个进程退出的时候,一般不会进入X状态(终止态,可以回收资源),而是进入Z状态
- 进程运行了一定的程序,可以理解为这个进程的任务
- 当该进程退出的时候,需要知道这个进程是如何结束的(可以理解为终止的原因)
- 一般是将执行结果交还给操作系统或者父进程
维护一个状态Z,就是为了维护进程的退出信息,可以让父进程/操作系统
读取
父进程/操作系统
是通过进程等待来读取进程的退出信息的
在task_struct
里面就有一个专门的成员来维护退出信息
3.2 如何复现僵尸状态?
我们创建子进程的时候,只要父进程不搭理子进程,一直运行父进程,提前终止子进程,就可以观察到子进程进入僵尸状态
1 |
|
1 | [muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test1 | grep -v grep |
英语小课堂:defunct
一般我们都会要求父进程回收子进程,不过这个得后续才能学到了!
ps循环监控脚本
我们可以使用一个监控脚本来更方便的监控结果
1 | while :; do ps jax | head -1 && ps jax | grep test | grep -v grep;sleep 1; echo "########################"; done |
上面这个语句的作用是,每一秒执行一次ps jax | head -1 && ps jax | grep test | grep -v grep
命令,直到我们使用ctrl+c
终止进程
需要注意分隔符,while
后面的是:;
不要写成双冒号!
3.3 长时间僵尸状态的弊端
如果一个僵尸进程长时间不被处理,就容易出现内存泄漏!
子进程的状态是用数据维护的,如果父进程一直不回收子进程,该子进程的task_struct
就一直留存在内存中,这就是一定的内存泄漏。
3.4 孤儿进程
当一个进程的父进程先退出的时候,子进程就会变成孤儿进程
为什么这里我们没有看到父进程进入Z或者X状态呢?那是因为这里父进程的父进程是bash
,命令行回收了我们的父进程。
可子进程为何还在这里呢?
- 父进程退出之后,子进程并不会不见,而是会被
1号
进程(操作系统)领养。 - 这时候我们可以把子进程称为
孤儿进程
注:1号进程又称init进程
而操作系统领养之后的子进程,即便你使用ctrl+c
也干不掉这个进程
仔细观察,可以看到子进程被操作系统领养后,运行状态上的+
不见了
1 | PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
前台和后台进程
状态码上带有+
号,代表进程是一个前台进程
- 能被
CTRL+C
终止的都是前台进程 - 后台进程一直在运行,会影响我们的命令行输入
我们可以使用kill -9
干掉该进程,干掉进程之后,就可以使用CTRL+C
恢复正常的命令行了
3.5 守护进程和精灵进程
守护进程&精灵进程:这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响
4.进程暂停T
暂停一共有两种状态,一种是stopped
,第二种是tarcing stop
一般linux系统是用大T指代stopped
,用小t指代tarcing stop
4.1 T-stopped
kill发信号
在之前的操作中,我们已经学过使用kill -9 pid
来干掉一个进程,实际上kill命令能干的事情远不止这一个
1 | kill -l |
使用这个命令可以查看到kill命令支持什么操作
其中我们要用到的是第19和第18,分别用于暂停/恢复
一个进程
1 |
|
写一个啥事不干的死循环用于测试,可以看到该进程处于R+
状态
1 | [muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ kill -19 9503 |
执行kill -19
命令之后,我们可以看到该进程被终止,状态码变为大T
进程暂停并不代表进程结束,这就好比我们看视频的时候暂停一样。你暂停了播放,但是播放器这个进程并不会直接终止!
要想让这个进程重新运行,执行kill -18
即可,进程恢复为R状态。此时也没有了+
,代表这是一个后台进程
1 | PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
使用kill -9
干掉该后台进程即可
4.2 t-tarcing stop
tarcing
一词意为追踪,最简单的情况便是我们使用的gdb调试打断点
1 | [muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test |
这时候test_g
进程就是一个处于小t状态的进程
5. 进程优先级
- 权限:能还是不能的问题,决定进程能不能访问某种特定的资源
- 优先级:进程可以访问该资源,但有先后顺序(运行队列)
进程在排队获取资源的本质就是在确认优先级。这是因为系统的某些慢资源不够多个进程同时使用,这时候就需要让进程进入排队来先后访问。
而优先级越高的进程,操作系统执行它的响应就会更快。其会把它插入到优先级低于它的进程之前,先运行这个“vip进程”,再运行那些“普通进程”
5.1 linux进程优先级
可以用下面的ps -la
命令查看当前bash下的进程
1 | [muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps -l |
其中的PRI
和NI
就是我们进程优先级的数据
- linux的进程优先级=
priority_old+nice
- linux下进程的默认优先级是80,
PRI
值越低,优先级越高 NI
值是进程优先级的修正数据,我们修改进程优先级,修改的是NI
值而不是PRI
这两个值允许的范围如下,Linux系统并不支持用户无节制的修改优先级
1 | -20 <= NI <= 19 |
5.2 使用top命令进行修改优先级
linux下修改优先级的操作如下,运行test1
程序后,先查看它的优先级信息
1 | [muxue@bt-7274:~]$ ps -la |
在使用sudo top
后,进入界面按r
,输入需要设置的进程pid
后,再输入需要调整的nice值
1 | [muxue@bt-7274:~]$ ps -la |
这里可以看到,test1进程的优先级已经被我们改成了70
再来尝试第二次,这次nice设置为20看看
1 | [muxue@bt-7274:~]$ ps -la |
诶,pid设置成20之后,为啥NI
值变成了19,而PRI
变成了99呢?
依据我们以往的惯性思维,既然进程优先级=
priority_old+nice
,那么修改了之后不应该是原本的70+20=90吗?为什么是99呢?
这是因为每一次设置的时候,priority_old
都会被重置成80。所以可以直接记住,Linux下进程的优先级=80+Ni值
The End
感谢你看到最后,如果本篇博客有啥问题,欢迎在评论区提出!