进程概念
本节目标
1. 进程概念
1.1 进程的概念
1.2 描述进程—PCB
2.进程的基本操作
2.1 查看进程
2.2 结束进程
2.3 查看进程的另一种方式(了解)
2.4 进程的系统调用(getpid)
2.5 常见进程调用(父进程、子进程)
2.6 通过系统调用创建进程-fork初识
3. 进程状态
3.1 普遍的操作系统层面
3.2 具体的Linux操作系统层面
4. 两种特殊的进程
4.1 僵尸进程
4.2 孤儿进程
5. 进程优先级(了解范畴)
6. 进程的其他概念
7. 进程切换
本节目标
1. 进程概念
2. 进程的基本操作
3. 进程状态
4. 特殊进程
5. 进程优先级
6. 进程切换
那在还没有学习进程之前,就问大家,操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!
1. 进程概念
1.1 进程的概念
对于我们的了解来说,什么是进程呢? 有的资料是这么说:一个运行起来的程序(加载到内存)叫做进程;在内存中的程序叫做进程。也就是说,进程和程序相比具有动态属性。
对于之前我们通过C写出的进度条程序来说,其本质就是一个文件并且存放在磁盘中。但是其并没有真正的运行,当我们运行程序的时候,文件就会从磁盘加载到内存,但是磁盘中那么多的文件全部加载到内存中明显是不现实的并且我们也不需要其他文件加载到内存,这时候就需要操作系统对文件进行管理从而只让我们想要执行的程序加载到内存,那操作系统是如何管理的呢?
即上篇提到的:先描述,再组织
1.2描述进程—PCB
通过上述的概念,我们了解的并不多,那么接下来就来分析一下:如果有很多这样的进程加载到内存中,操作系统要如何进行管理呢? 即利用先描述再组织的思想。
而所谓的先描述,这里引进了一个新的概念:PCB :进程控制块 struct task_struct{}
1.那么什么是进程控制块呢?
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,称之为PCB(process control block)在Linux中描述进程的结构体叫做task_struct。
在磁盘中的程序中,并没有进程控制块以及内部属性信息的存在,而是加载到内存之后通过操作系统的一系列的管理才出现的。
//进程控制块 struct task_struct { //该进程的所有属性 //该进程对应的代码和属性地址 struct task_struct *next; };
2.进程控制块如何对进程进行管理的呢?
- 磁盘中的可执行程序在将要运行时,所有的进程中的数据(并不是程序本身)加载到内存中,此时操作系统会建立起一个PCB来保存每一个程序的信息
这个时候PCB就会对每一个进程都建立起相应的结构体(即进程控制块)将对应的进程的属性、代码等匹配的传到这个结构体中:(这就是先描述)
- 此时,操作系统就会将每一个进程控制块都连接起来,形成链表结构,并返回头结点。这样便通过数据结构的形式将进程的信息组织起来。
通过这样的先描述再组织的思想,当我们处理优先级高的进程时,我们就可以通过遍历头结点,找到优先级最高的那个节点的信息,并将这个进程的代码执行。
因此通过上面的描述,我们就可以回答1.2开始时所问的问题:操作系统要如何进行管理呢?
答:所谓的对进程进行管理,会变成对进程对应的PCB进行相关的管理,也就是说:对进程的管理变成了对链表的增删查!
最终,通过上述的解释,进程究竟是什么我们也就知晓了:进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码
3.为什么要有进程控制块(PCB)呢?
通过上一篇介绍的软硬件体系结构以及刚才的描述,对于为什么要有PCB进行了解答:
对进程管理的核心是对数据进行管理,因此当我们加载程序到内存之前,我们必须拿到所有程序的数据,由于拿到的数据杂乱无章并且未进行分类,这时候就需要PCB将其归类,将对应的数据放到相应的进程控制块里!
即加载进程的时候,操作系统为了方便管理会new一个struct task_struct也就是进程控制块的结构体,然后一点点的将上面加载的数据填充到这里的内部属性(状态,标记,追踪等),因此这里再一次强调了:进程不是程序加载到内存,而是在内存中new了task_struct结构体!
2.进程的基本操作
2.1 查看进程
这里演示过程:
创建文件(Makefile、myproc.c、myproc)
执行,并通过打开复制ssh渠道,输入指令
ps ajx | head -1 && ps ajx | grep "myproc"
(//&&的左右是两个指令,通过&&可以将两个指令一起执行。左面是通过管道将进程的第一行显示出来,右面是将myproc相关的进程信息打印出来,不利用管道将会出现其他没必要的信息)
动图:(在运行中,进程就具有动态属性)
2.2结束进程
1.通过指令结束进程
kill -9 PID
这样就可以结束掉我们的进程了。
2.通过ctrl + c
快捷键
通过快捷键ctrl + c
也可以结束进程。
2.3查看进程的另一种方式(了解)
通过上面的描述,查看进程和结束进程我们都已经了解,在这里还要引入一个新的关于查看进程的知识。
仍是对于这个程序,我们让其运行(实际上进程在调度运行的时候,就有了动态属性)。
通过指令:ls /proc/5058
我们就可以看到这个进程中的信息。
这也可以说明,进程实际上也是一个Linux中能够保存的文件。我们进入到5058:
我们发现,其中生成了一个这样的.exe文件,这实际上就是我们正在运行的程序。
那如果我们把这个文件删除了,程序还会不会运行呢?我们接下来试一试:
我们发现,左面的颜色变红提示了已经删掉,但是右下角的程序仍然在进行,这也恰恰说明了加载到内存的数据不会受到磁盘文件的影响!
2.4进程的系统调用(getpid)
我们发现,上面的执行过程中我们如果想要结束进程,就需要kill 9 PID
,而这个PID的值我们该如何获取呢?我们可以通过getpid函数获取。
我们打开手册 man getpid:
我们发现,getpid()函数的返回类型是pid_t,这是我们在C/C++所不曾见到的类型,而getpid()的功能就是:返回这个进程的pid。
因此我们就可以根据这个信息编写代码:
我们发现,确实是一样的。也就是说,当我们想知道一个进程的PID,就可以通过getpid获取。
2.5 常见进程调用(父进程、子进程)
1.父进程与子进程
我们将上述myproc.c增加一个父进程的打印:
当我们结束时,发现父进程的PID并没有发生变化,事实上父进程的PID就是bash即命令行的PID,父进程本身因此在下一次登陆之前,父进程的PID不会发生变化。
2. 子bug父进程的变化
为了解释这个,我们将myproc.c内部增加一个bug:
我们看看结果:
这说明这个程序执行了,并且报错了,但是仍然可以通过命令提示错误,因此我们可以看出,程序执行是以子进程在执行,其出错并不会导致父进程错误,父进程也就是命令行的进程,因为提示的错误就是父进程在提示!因此父进程并没有错误!
2.6 通过系统调用创建进程-fork初识
1. fork创建子进程
我们通过man fork
了解到,fork是创建子进程的函数,但是当这个函数执行前,只有自己本身这个进程和他的父进程,执行之后,这个自己本身的进程就会变成子进程的父进程,而相应的这个进程的父进程也就变成了父进程的父进程。
我们发现,这就相当于bash是爷爷进程,而程序本身是父进程,fork创建出的的是子进程。
2. fork返回值
通过man查看fork手册/return val
,我们发现fork有两个返回值,那具体是什么含义呢?接下来我们实验一下:
结果:
我们发现,同一个变量id,在后续不会被修改的情况下,竟然有不同的内容!但是这里我们还不知道为什么也没办法解释,因此在这里我们打不过,就加入他,利用这个规则,我们来看看下面的程序:(注意sleep这里的细节)
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> int main() { // 创建子进程 -- fork是一个函数 -- 函数执行前:只有一个父进程 -- 函数执行后:父进程+子进程 pid_t id = fork(); if(id == 0) { //子进程 while(1) { printf("子进程,pid:%d, ppid:%d,id:%d\n",getpid(), getppid(), id); sleep(1); } } else if(id > 0) { // parent父进程 while(1) { printf("父进程,pid:%d, ppid:%d, id: %d\n",getpid(),getppid(),id); sleep(2); } } else { } printf("我是一个进程,pid:%d, ppid:%d id:%d\n", getpid(), getppid(), id); return 0; }
结果(动图):
我们发现,两个进程同时执行,即if可以执行,elseif的也一起执行,没错,if和elseif竟然同时执行了,这就是引入fork的缘故(多进程)。
即fork()之后,因为pid既可以是0也可以是1,会有父进程+子进程两个进程在执行后续代码(即两条路线去执行if和else if两个分支),fork()的后续代码,被父子进程共享,让父子进程执行后续共享代码的一部分,这就叫做并发式的编程!
3.进程状态
对于运行状态,有以下几种:
3.1普遍的操作系统层面
进程状态的概念: 通过上面的学习我们知道,当一个程序执行加载到内存时,操作系统就会创建对应的PCB(进程控制块:struct),而在这个PCB结构体中,有一个位置存放着整型的成员变量,而这个整形变量的不同数值,就代表不同的进程状态。
进程状态一共有九种:运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡 ;其中运行、阻塞、挂起是最为重要的进程状态。下面就讲解这三个最重要的状态:
1.运行状态R
对于运行状态来说,并不是在CPU中正在运行才是运行状态,只要是进程在CPU的等待队列中,那么就可以称之为进程的运行状态(R)
2.阻塞状态(T)
对于阻塞状态来说,进程在磁盘中的阻塞队列中,就称之为进程的阻塞状态。
通过上面的描述,事实上等于没说什么,什么是运行队列?什么是阻塞队列?为什么会有运行队列和阻塞队列?二者之间存在着什么关系? 事实上这才是运行状态和阻塞状态的切入点,那我们带着疑问一起了解:
当一个进程开始执行时,会将二进制代码从磁盘加载到CPU中去执行,但是我们知道,进程的数量远远多于CPU的数量,这个时候操作系统就会将这些进程进行管理,让一些进程去等待,另一些进程去运行。假设我们只有一个CPU。当一个进程正在执行是,那么其他进程就需要去排队等待CPU资源(CPU会维护一个运行队列让这些要执行的进程去排队,这个运行队列是内核为CPU准备的,一个CPU,一个运行队列)(注:让进程入队列的本质就是将该进程的PCB结构体对象放入运行队列中,而不是让进程(程序)自己去排队)而等待的这些进程都在运行队列中,那么他们就都处于运行状态!
我们知道,冯诺依曼体系结构中的CPU很快,但是(外设)磁盘相对较慢,但是进程除了访问CPU,也避免不了访问磁盘(外设),比如fwrite就是将进程与磁盘之间相联系(而对于scanf、cin这些函数也会访问显示器,一些代码也会通过网络访问网卡,这些都是与外设想交互)但是磁盘很慢,不像CPU一样,所以当A用着,其他的进程就需要等待,因此这样看来进程也可能在外设上等待,也就是说,不要只意味着进程只会占用CPU资源,进程同样也会占用外设资源,因此外设也有自己的描述结构体,也可以维护自己的等待队列! 进程在外设的等待队列中就是进程的阻塞状态!
由于外设的等待队列过慢,CPU因为会执行代码但被外设的速度限制,这时CPU就会对外设说,不好意思,你太慢了,我不想等你了!因此这个时候CPU就会将对应的代码的进程从CPU的等待队列(也就是运行队列)中放到外设的等待队列中,从而去执行CPU运行队列中的下一个进程。
这就好比什么呢?就相当于当你去银行办理业务,当到你到指定柜台去填表时,由于你填的太慢,这时候工作人员为了不让你占用过多时间,就会让你离开窗口,去旁边的桌子上继续填表,为的就是不耽误你后面人的时间让他们继续办理业务。因此这就将这个进程从CPU的运行队列里强行拉到了外设的等待队列中。这个进程也就从运行状态切换到了阻塞状态!
而当你填完表之后,你通过其他工作人员的告知,就会直接进入窗口处理业务,也就是从阻塞状态直接变成运行状态,这个操作就是CPU自动调度而不是操作系统处理的,因为操作系统之前已经处理完你,也就是把你从窗口移到了附近的桌子,就不会继续管你了!
需要注意的是:我们所提到的这些拖拽,排队,不是进程本身的行为,而是他们PCB的行为!
因此所谓的进程的不同状态,本质就是进程在不同的队列中,等待某种资源!
对于新建和就绪状态其实是同一种状态,很好理解,就是这个进程刚刚被创建好,也就是我们所谓的make,因此我们不需要去描述。
3.挂起状态
将内存中进程的数据和代码转移到磁盘的状态被称为挂起状态。
为了解释挂起状态,我们建立这样一个场景:如果阻塞的进程过多,那么他们是不会被立即调度的,也就是说不会将这些进程从阻塞状态同时转换成运行状态,因为这些阻塞的进程本身也需要彼此之间进程排队。那么我们知道,在进程阻塞时,其对应的数据和代码还在内存中,万一内存不够了,怎么办?
由于过多的进程站着茅坑不拉屎的不良行为,这时操作系统就会体现他的义务将这些有不良行为的阻塞进程体验社会的毒打,因此为了不让阻塞的进程占用内存,操作系统就会将阻塞状态的进程的数据和代码转移到磁盘上储存,这样这个进程就从阻塞状态变成了挂起状态!
此时内存中仍有挂起状态进程的PCB,只是其中的代码和数据转移到了磁盘,减少了内存占用的空间。将内存的相关数据加载或保存到磁盘,叫做内存数据的换入换出。
就好比几年前用过的安卓手机,比如王者荣耀,如果手机用的时间久或者内存小的话,每一次后台运行之后点进去,就有可能重新开始而不是接着上次的界面,这就是因为占用内存过多,内存不够,于是操作系统(安卓系统)将这个进程挂起了。
3.2 具体的Linux操作系统层面
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/* * 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 */ };
其中,这个结构体中的各种状态就一一对应着我们在普遍的操作系统层面上的状态,分别是:运行状态,睡眠状态,深度睡眠状态,停止状态,停止追踪,死亡状态,僵尸状态。下面先从定义上描述一下一些状态,之后再具体分析。
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
1. R运行状态
先来看看执行的代码:
那当我们执行,看生成的可执行程序是什么状态:(STAT就代表这个进程的状态)
通过./myprocess,执行后这个程序便处于运行状态(R+)。
2. S睡眠状态
我们将上面的代码加上一行printf输出的形式:
有了printf,就需要动图展示一下具体的状态了:(动图)
我们发现,在打印的过程中,myprocess的状态不是运行状态,而是S+休眠状态。事实上,这是由于我们通过printf访问的是外设,而我们知道外设很慢,因此进程等待显示器就绪需要花费很长时间,于是就会把该进程的PCB转移到外设的PCB上排队,这也就对应了普遍操作系统层面上的阻塞状态。
当然,这也不是绝对的,只是对大多数进程而言(99%),因为大多数都是在等待显示器就绪,如果我们想访问变成R状态,可以sleep一个很大的秒数,这样就有机会看到是R状态了
3. D深度睡眠状态
我们之前用的printf,scanf事实上都算是浅度睡眠,即S即为浅度睡眠状态,浅度睡眠状态是可以终止的,也就是ctrl c就可以直接终止。而深度睡眠状态是不可以终止的,即无法被操作系统杀死,只能通过断电解决。
对于这个状态,我们通过想象这样的一个场景去理解他:
当一个用户给操作系统、进程、磁盘三个人制定任务时,其中进程和磁盘说这是1万条用户数据,你帮我拷贝一下,磁盘答应了,但是磁盘有可能拷贝成功,也有可能失败。然而这个时候由于进程过多,内存不够了,此时操作系统就会站出来将一些不干活的进程杀掉,但内存仍然不够,于是操作系统秉着他的责任,将所有的进程全部杀掉,自己累的够呛也挂了。但这时磁盘处理数据失败了,就回去找进程,让他重新搞来一份数据,但是此时进程没有回应他,喊了很多次仍无人回应,最后也就这样草草了事。
当用户知道这个任务没有完成,就找到他们三个追究责任,磁盘率先站出来说:这不关我的事,我本来就有可能成功有可能失败,当我失败的时候去找进程发现他不见了,和我没有关系;这时候进程也出来说话了:这和我也没有关系,我是被操作系统杀掉了,怎么能回应你呢?最后操作系统发现他们两个把矛头指向了自己,就气愤的说:我有我的职责,内存不够了,我必须杀掉进程防止内存不够,这就是我的任务,并不是只针对你这一个进程,所有进程在我眼里都是一样的。用户听了他们说的话,却都觉得没什么问题,于是就对操作系统说:下次对于这样的情况,不要杀掉指定的进程,这就相当于给了这个进程一个免死金牌,操作系统也是清楚了具体的规则,同意了,进程和磁盘也表示没问题。这个时候,这个给予免死金牌的进程就处于所谓的深度睡眠状态,不可被操作系统杀死,只能断电处理!
需要注意的是:深度睡眠一般只会在高IO的情况发生下,且如果操作系统中存在多个深度睡眠状态的程序,那么说明该操作系统也即将崩溃了。
4. T停止状态
将printf注释掉:
当我们查看kill 手册,找到对应的19号选项后:
我们发现,状态就从R状态变成了T状态,变成了之后呢,T状态代表什么呢?事实上,T状态也是阻塞状态中的一种,因为其代码不被CPU执行,但是其属不属于挂起状态呢?这个我们无从得知,因为这是操作系统所做的事情。
既然有暂停,那么就一定有继续。我们看上面的kill选项中,18号就是继续的选项,那我们来看看:
我们发现,又变回了R状态,只不过有个细微的差别,和最初的R+相比,+不见了。
如果我们将程序加上printf,让其变成S+状态,当我们再对应的显示器上输入除了ctrl c的其他命令行,我们会发现其并不会执行,而显示器上照常打印,这就是所谓的前台进程。如果我们将其T掉,再kill 18对应的进程让其继续,我们会发现状态变成了S,出现了和运行状态时一样的情况,而这时当我们再输入命令行时,会发现可以显示对应的结果,并且可以继续打印,这就是所谓的后台进程,但是对于后台进程,不能用ctrl c结束,只能通过kil -9 PID的形式结束进程。因此我们也就知道了+的意义,有+的是前台进程,没有+的是后台进程。
5. t追踪暂停状态
对于追踪暂停状态,其实是一种特殊的停止状态,即进程在调试时就处于追踪暂停状态:(gdb)
6. X死亡状态
死亡状态代表着一个进程结束运行,该进程对应的PCB以及代码和数据全部被操作系统回收。
7. Z僵尸状态
- 僵尸状态是什么?
我们知道,进程被创建出来是为了完成任务的,而完成的结果也是需要被关注的,即进程完成的结果会被其父进程或者操作系统接收,因此在进程退出的时候,不能释放该进程对应的资源,而是保存一段时间,让父进程或者操作系统来进行读取。因此在这个进程退出后到被回收(OS、父进程)前的状态就是僵尸状态!
- 僵尸状态的危害
对于僵尸状态的进程,事实上不是数据存在在内存,而是其对应的PCB在内存中占用空间,因此如果这种进程过多导致PCB占用内存过大,并且父进程和操作系统没有及时回收这种资源,那么就极易发生内存泄漏。由此可见。除了malloc或者new,系统层面上也是有可能发生内存泄漏的。
- 如何解决僵尸状态?
既然有僵尸状态的危害,就一定有解决的方法,解决的方法将在后续讲解,在此只需要知道僵尸状态是什么,有什么危害就是我们这一节的目标。
既然有僵尸状态的危害,就一定有解决的方法,解决的方法将在后续讲解,在此只需要知道僵尸状态是什么,有什么危害就是我们这一节的目标。
总结: 具体的Linux操作系统下的进程状态和普遍的操作系统上进程的状态的分类是不同的,Linux操作系统和普通的进程状态相比没有阻塞和挂起状态,普通OS的阻塞状态在LinuxOS中通过睡眠、深度睡眠、暂停、追踪暂停等状态表现出来,而进程处于这些状态时是否会被调整为挂起状态,用户是不可得知的,因为操作系统没必要将挂起状态暴露给用户,用户也不关心一个进程是否会处于挂起状态。
具体的Linux操作系统层面
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/* * 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 */ };
其中,这个结构体中的各种状态就一一对应着我们在普遍的操作系统层面上的状态,分别是:运行状态,睡眠状态,深度睡眠状态,停止状态,停止追踪,死亡状态,僵尸状态。下面先从定义上描述一下一些状态,之后再具体分析。
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。