目录
一、什么进程
1.进程概念
由上篇博客我们已经知道,任何程序在运行时都需先加载到内存,CPU只与内存进行交互。假如我们现在写了一个程序,而程序的本质就是一个存放在磁盘上的文件。所以当我们要运行程序时,程序首先需要加载到内存中。
操作系统管理的理念是什么? — 先描述,再组织
所以操作系统首先把所有的进程都描述起来,而为了描述每一个进程,就产生了PCB
2.描述进程-PCB
每个进程都有其属性,比如:加载的紧急程度、需要调用什么资源、占用多少空间…所以在管理进程时,操作系统会根据每个进程的需求去给其添加相应的属性,然后将其打包起来变成一个个”进程控制块“,又叫PCB。
在windows操作系统上,这些进程控制块直接称为PCB
而在linux操作系统中,这些进程控制块被命名为:task_struct
task_struct内容分类
3.组织进程
描述完之后,操作系统便利用数据结构对其进行组织。比如通过链表,将每个PCB链接起来
所以到最后,所谓的进程管理,变成了对进程对应的PCB进行相关的管理,再通过数据结构转化为对链表的增删查改。
综上所述,准确来说
进程 = 内核数据结构(task_struct)+进程对应的磁盘代码
二、进程的基本操作
1.查看进程
✏️借助管道使用 ps axj 指令配合 grep 指令来查看指定进程
我们可以在Linux中写一段死循环的程序,便于我们查看进程
2.获取进程标识符
- 进程id:pid
- 父进程id:ppid
- 获取进程id:getpid( )
- 获取父进程id:getppid( )
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("我是一个进程,我的id是:%d,我的父进程id是%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
❓当我们中止进程再重新加载,id又会是什么呢?
由图我们可以发现,当我们重新运行程序时,此程序进程的id会与之前不同。因为重新运行也即是重新从磁盘中加载到内存,所以进程的id便会改变。但是我们发现它的父进程id是不变的?那到底是谁来产生的子进程呢?
通过具体查看进程,我们发现,test程序的父进程是bash
,bash
也即是shell外壳。这也证明了在执行指令时,shell为了防止程序有误奔溃,导致自身奔溃,所以安全起见,在运行程序时,shell不会自己去执行指令,而是不断地去派生子进程去执行。所以这也是每个程序进程在不断重新运行时,其父进程保持不变的原因。
3.初识创建子进程
认识完子进程父进程,我们是否能够自己手动去创建子进程呢?系统调用接口提供了 fork 让我们实现这个目的
fork 在进程管理中能够起到许多作用,而我们目前作为初步认识进程管理,也不对fork进行过多展开,初识fork即可。
在Linux中我们可以通过指令 man 了解fork的用法
综上所述
- fork( )之后,会有父进程+子进程两个进程在执行后续代码
- fork后续的代码,会被父子进程所共享
- fork之后通常要用if进行分流,通过返回值不同,让父子进程执行后续共享代码的不同部分
三、操作系统的进程状态
1.什么是进程状态
计算机的日常使用中,会同时运行很多程序,也就意味着会有许多进程加载到内存需要CPU等系统硬件去运行。那么操作系统这位管理大师为了管理好这些进程,则需要根据各种具体情况去做出相对于的管理。于是乎在task_struct(PCB)
便定义了许多进程状态信息,可以理解为是操作系统对进程的一种“标记”,便于操作系统进行管理。
在普适的操作系统上,进程状态有:运行、挂起、阻塞、新建、就绪、等待、死亡;不同书籍对于进程状态的命名可能没有完全一致,但进程状态的原理是相同的。而一些操作系统除了这些基础的进程状态以外,可能还会有其他一些属于自己的定义。
2.普遍操作系统层面
在普遍操作系统层面上,进程状态大致有:运行、挂起、阻塞、新建、就绪、等待、死亡。这些进程状态本质上是为了满足不同的运行场景。今天我们具体介绍其中较难的运行、阻塞、挂起✏️
2.1运行状态 & 阻塞状态
一般计算机只有一个CPU,但是进程却有很多,所以为了便于管理,操作系统给CPU安排了一个“运行队列”,需要占用CPU的进程就按顺序在运行队列中排队。虽然CPU只有一个,但是CPU又是出了名的快!CPU运行每个进程时都很快,所以那些需要占用CPU资源的进程就得随时准备着,进入CPU的运行队列准备CPU的处理。
举个例子🌰
假如现在你投了简历给腾讯,想要参加面试。而投简历的人非常多,腾讯那边需要筛选以及按顺序安排一面,而你投了简历之后也不知道多久才能收到通知,也不确定简历是否能够通过,所以便开始放松自我享受生活。而有一天腾讯打电话给你说:小伙子你的简历很不错,接下来这几天你等面试官打电话给你然后进行一面!而收到通知的你便开始复习,随时准备迎接面试的到来。
所以,不只是在CPU中正在运行的进程属于运行状态,在运行队列中随时准备着的进程,也属于处在运行状态。除此之外,像你参加面试,不是你本人
跑去公司排队,那样腾讯大楼得被挤爆,而是能够代表你的“你的简历
"在排队。同理,在排队的不是一个个可执行文件,而是带有那些程序属性信息的PBC
在排队。让进程入队列的本质是将该进程的task_struct结构体对象放入运行队列中。
各个进程除了会占用CPU,也可能随时随地会占用外设的资源,但是各个外设的速度却远远比不上CPU的速度。
所以有可能当CPU在运行进程的过程中,进程同时也需要去调用磁盘的资源,但是磁盘等外设对于进程来说也是狼多肉少,并且这些外设的速度对于CPU来说慢得多,所以进程去访问磁盘也需要排队!难不成当在CPU中运行一半的进程需要排队访问磁盘时,CPU会等他?那别人怎么办?
就好比众人在银行排队处理业务,有人业务办一半发现身份证忘记拿了,需要回家去取身份证,难道整条队伍的人还有银行的工作人员全部停下来等那个人去拿身份证吗?
因此,处于运行状态的进程由于发生某些事件而暂时无法继续执行,将此进程的PCB从CPU的运行队列中剥离出来,放入其他等待队列中的情况,便称此进程处于阻塞状态。
2.2挂起状态
我们知道,进程的产生本质上为了对加载到内存的程序的管理,而处于阻塞状态的进程由于需要等待某种资源,所以其对应的代码和数据在短期内并不会被执行,但是其仍然已加载到内存中📚而内存的空间相比于磁盘空间来说可谓是小巫见大巫,所以当内存空间不足时,操作系统就会将这些处于阻塞状态的进程对应的代码和数据拷贝到磁盘中,然后释放内存中的那一份,从而节省内存空间。
上述这种由于内存空间不足,操作系统将那些正在等待资源,短期内不会运行的进程对应的代码数据放到磁盘以节省内存空间的状态被称为挂起状态
对进程挂起,移动的是进程对应的代码数据,进程的PCB仍处在等待队列中,当进程结束等待转变为其他状态时,操作系统会将其对应的代码数据重新加载到内存中继续运行,其本质上是对内存数据的唤入和唤出。
🎈阻塞不一定挂起,挂起也不一定阻塞。是否挂起取决于内存空间状况,可能进程一新建就被挂起 — 新建挂起、就绪挂起、运行挂起…
3.Linux操作系统的进程状态
上面我们分享的是普遍操作系统中进程的状态,而个别操作系统会有其特别的状态定义,下面我们来学习Linux操作系统下的进程状态。
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 */
};
看完Linux中对各个进程状态的定义,可以看到Linux中进程一共有七种状态:运行、睡眠、深度睡眠、暂停、追踪暂停、死亡、僵尸📌
3.1运行状态(R)
运行状态也就是进程的PCB处于CPU的运行队列中,我们可以编写一个死循环程序,查看一下此程序的进程状态是什么
STAT表示“状态”,由图可见test程序的进程处于运行状态
3.2睡眠状态(S)
当我们在死循环的程序中加入了打印指令,我们发现程序的进程变为睡眠状态
而Linux中的睡眠状态(S)其实就是我们上文提到的阻塞状态💤
可是为什么左边的窗口程序一直运行着,进程状态却还是睡眠状态呢?
3.3深度睡眠状态(D)
在上文提到,当内存空间不足时,操作系统会将一部分进程挂起来节省内存空间;但是如果挂起了许多进程后,内存空间仍然严重不足以供正在运行的进程使用时,操作系统会主动杀掉某些没有在运行的进程。
因此为了避免这种情况的发生,Linux设计出了深度睡眠(D)状态,就相当于给某个进程一张免死金牌,让操作系统在不得不杀掉一些进程时不能杀掉这些处于深度睡眠状态的进程。处在深度睡眠状态的进程既不能被用户给杀掉,也不能被操作系统杀掉,只能通过断电或等待进程自己醒来!
注:一般情况下只有在高IO时才会导致进程进入深度睡眠状态
3.4暂停状态(T)
暂停状态,顾名思义就是进程运行到一半暂停了,也属于阻塞状态的一种。我们可以使用kill指令来让一个进程从运行状态变为暂停状态。
kill指令有许多选项,我们目前只需了解部分选项即可
kill -9 -----> KILL (杀掉进程)可用于我们手动将某个进程杀掉
kill -19 ------> STOP(暂停进程)可用于我们手动将某个进程暂停
kill -18 -------> CONT(continue 继续进程)可用于我们手动将某个暂停的进程继续
细心的兄弟可能会注意到:进程一开始处于运行状态是R+,在暂停或继续之后,进程状态的+号便消失了
📌进程状态后面的+号表示此进程是一个前台进程,若没有+号则表示此进程是后台进程
前台进程我们可以通过Ctrl+c将其终止,也可以用 kill -9 指令将其杀死;
而对于后台进程我们只能通过kill -9指令将其杀死。
3.5追踪暂停状态(t)
🌈追踪暂停状态是一种特殊的暂停状态,顾名思义就是此进程正在被追踪,比如我们使用gdb调试进程
3.6死亡状态(X)
🌈死亡状态即使该进程已经死亡,代表该进程已经不再运行,对应的PCB以及代码数据将会全部被操作系统回收
3.7僵尸状态(Z)
进程也是如此,进程也会死亡,可能是正常运行结束后死亡,也可能是出现bug奔溃…运行进程的目的便是帮使用者完成某些任务,而任务是否完成结果如何总得让人知道吧?
所以,一个进程在死亡后,不能立刻释放其全部资源❌对应的代码和数据可以释放,但是代表该进程属性状态的PCB应该继续保留,等待调用其进程的父进程或操作系统来读取。
进程在死亡后等待父进程或者操作系统来检测或回收PCB的状态,便是僵尸状态
4.两种特殊的进程
4.1僵尸进程
处于僵尸状态的进程便是僵尸进程
我们创建了父子进程后,使用kill指令将子进程杀掉后,由于父进程没有对子进程的退出状态进行检测回收,所以子进程进入了Z状态;子进程不再运行的同时,子进程后面提示了(失效的、不再使用的),此时如果父进程一直不对子进程进行检测回收,那么子进程就会一直处于一种僵尸进程的状态。
僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
- 一个父进程若创建了很多子进程,就是不回收,便会造成内存资源的浪费!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间
- 过多的僵尸进程会导致占用空间(没人收尸)也可能会导致内存泄漏
4.2孤儿进程
- 父进程如果提前退出,子进程后退出,进入Z后该如何处理?
- 父进程先退出的话,子进程就被称为“孤儿进程”
我们发现,当子进程变为孤儿进程后,它的父进程id变为1,1便是操作系统🎈所以当进程变为孤儿进程时操作系统就会领养它
此外,进程变为孤儿进程后,也会变成后台进程,只能通过kill指令才能令其进程结束运行
5.进程优先级
5.1基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
5.2查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中几个重要信息:
- UID:代表执行者的身份
- PID:代表这个进程的代号
- PPID:代表父进程的代号
- PRI:代表这个进程可被执行的优先级,其值越小越早执行
- NI:代表这个进程的nice值
5.3PRI and NI
- PRI还是比较好理解的,即进程的优先级,或者通俗点说就是进程被CPU执行的先后顺序,此值越小进程的优先级别就越高
- NI即是nice的缩写,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是
-20至19
,一共40个级别,而PRI初始默认为80,所以进程的优先级取值范围为60至99
5.4查看修改进程优先级
用top命令查看及其更改已存在进程的nice:
- top
- 进入top后按“r” —> 输入进程PID —> 输入nice值
6.其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
级别,而PRI初始默认为80,所以进程的优先级取值范围为60~99