前言
在上一章我们知道了什么是进程,并简单了解了PCB。
本文我们将继续深入学习进程概念相关知识点:
- 学习进程状态,学会创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害
- 了解进程调度,Linux进程优先级,理解进程竞争性与独立性,理解并行与并发
一、查看进程
查看进程一般有如下两种方式:
- 使用ps指令查看进程
- 通过/proc系统目录查看
0. 准备工作
- 编写一个简单的程序,如下:
#include<stdio.h>
#include<unistd.h>int main()
{while(1){printf("我是一个进程!\n");sleep(1);}return 0;
}
- 编写Makefile
myprocess:myprocess.cgcc $^ -o $@
.PHONY:clean
clean:rm -rf myprocess
- 执行make命令,生成可执行程序myprocess
- 运行myprocess,这个时候myprocess就是一个进程
1. 使用ps指令查看进程
查看所有进程,指令如下:
ps ajx
我们一般不单独使用ps ajx,因为它会打印所有的进程。我们一般会使用grep筛选出我们想查看的进程,指令如下:
# 筛选包含myprocess关键字的所有进程 ps ajx | grep myprocess
问题1:为什么会出现grep的进程?——系统指令也是一个可执行程序,执行时当然也是一个进程!
问题2:如何只显示myprocess进程,指令如下:ps ajx | grep myprocess | grep -v grep
问题3:这些列是什么意思,我们筛选进程之前先打印一下它的第一行列名称,指令如下:ps ajx | head -1 && ps ajx | grep myprocess
- 想要执行多条指令,可以使用&&或分号;来连接执行(注意:1. 必须都执行成功,不能只执行成功一条指令;2. 执行顺序从左到右。)
- PID:每一个进程在系统中要被管理起来,必须要有进程的id,即进程的唯一标识符(进程id并不是不变的,因为每重新启动一次程序,重新分配进程id)
- PPID:父进程id
- COMMAND:这个进程运行的时执行的指令
tip:我们可以通过进程的PID,结束进程,指令如下:(ctrl+c也可以结束进程)kill -9 PID
2. 通过/proc目录查看进程
在Linux系统中,/proc目录是一个非常特殊的目录,它是一个虚拟文件系统,主要用于提供系统运行时的进程信息和内核参数。/proc目录的内容并不是存储在磁盘上的,而是由内核动态生成的,反映了系统当前的运行状态。
如下图,我们ls查看/proc目录:
如图我们可以看到再proc中有很多文件,现在我们只关注蓝色的文件。这些蓝色的文件都是目录,并且我们发现这些蓝色文件的名称基本都是数字!
这些数字是什么呢——这些数字就是当前进程的pid。
在proc目录中默认给进程创建一个以它pid为名称的目录,这个目录中保存该进程的大部分属性!
在这个目录中保存了很多该进程的消息,如图我们现在需要关注其中两个消息:
- exe:exe链接文件指向当前进程的可执行文件路径
- cwd:current work directory——当前进程的工作目录。例如我们touch text,为什么就在当前目录下创建了?——因为touch执行的时候也是一个进程,在进程中有cwd属性,创建文件的时候会把cwd拼接到text前面,即cwd/text,所以touch创建的文件就在当前目录下
注:当myprocess进程停止后,我们就不能再通过ll /proc/pid 查到它的进程
小结:
- 这里我们只需要记住proc目录动态保存了系统中所有进程的信息。
- touch创建文件的时候,默认在当前目录下创建,是因为进程中有一个cwd属性(当前进程的工作目录)
二、通过系统调用获取进程标识符(进程id)
- 进程id(PID)
- 父进程id(PPID)
- ps ajx指令打印所有进程的相关常见属性,其本质是遍历task_struct双向循环链表,将task_struct的相关属性拿出来格式化打印
- 问题:我们怎样可以自己拿到自己的进程id?
首先我们知道进程id存在于task_struct,而task_struct存在于操作系统中,其次我们知道操作系统不可以直接访问,必须通过系统调用接口。所以我们必须通过系统调用接口才能访问进程id!
1. 通过系统调用接口获取进程id
- getpid():系统调用接口,获取进程id
- getppid():系统调用接口,获取父进程id
- 我们可以通过man手册查看getpid的使用说明:
man getpid
getpid的使用:
- 代码示例:
#include<stdio.h> #include<unistd.h> #include <sys/types.h>int main() {while(1){printf("I am a process, my id is: %d, parent: %d\n", getpid(), getppid());sleep(1);}return 0; }
- 监测脚本,每隔1s打印一次进程属性,指令如下:
while : ; do ps ajx | head -1 ; ps ajx | grep myprocess | grep -v grep; echo "--------"; sleep 1; done
- 我们运行发现:每重新启动一次程序,都要重新分配进程id,但是它的父进程id一直不变!(tip:每一次重新登录xshell的时候,我们的系统会单独再创建一个bash进程)
问题:这个父进程是什么?查看指令如下:ps ajx | head -1 && ps ajx | grep 28363
查看发现这个父进程就是bash命令行解释器,命令行解释器它的核心是获取用户的输入,然后帮助用户进行命令行解释。
在命令行运行的所有指令都是进程,他们的父进程都是bash!
总结:
- 在我们登录xshell的时候,系统会为我们创建一个bash进程,即帮我们创建好一个命令行解释器的进程,帮我们在显示器中打印对话框终端
- 我们在命令行中输入的所有指令,最后都是bash进程的子进程
- bash进程只负责命令行解释,具体执行出问题只会影响它的子进程,这就是为什么在终端中我们运行的进程他们的父进程id一直不变的原因。
2. 小结
- 进程关系重点维护父子进程,所以没有母亲进程,爷爷进程
- getpid获取自己进程的id。getppid获取父进程id
- 系统调用接口的使用和使用C接口一样,直接调用即可
- task_struct属性标识符(进程id):描述本进程的唯一标识符,用来区别其他进程
- 我们可以根据进程id杀掉一个进程(kill -9 pid)
- 在终端重新启动程序,进程id会变化,但它的父进程id一般不变
- 在终端执行的所有指令他们的父进程都是bash!
三、通过系统调用创建进程-fork
- 问题:创建进程的方式有哪些?
- bash创建进程:我们已经知道了在命令行执行指令,系统会为我们创建该指令的进程
- 自己手动创建进程:使用系统调用接口fork,自己手动创建进程
1. 自己手动创建进程-fork
使用man手册,查阅fork,指令如下:
man fork
- 库函数:#include<unistd.h>
- 函数声明:pid_t fork(void);
- 功能:以调用进程为模版创建一个新进程
- 返回值:如果成功,则在父进程返回子进程的pid,并在子进程中返回0;如果失败,则在父进程中返回-1,没有子进程创建。
fork的使用示例:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int main()
{printf("begin: 我是一个进程:pid: %d, ppid: %d\n",getpid(), getppid());//创建一个子进程pid_t id = fork();//如果创建成功,则在父进程中返回子进程的pid,并在子进程中返回0if(0 == id){//子进程while(1){printf("我是子进程,pid:%d, ppid: %d\n",getpid(), getppid());sleep(1);}}else if(id > 0){//父进程while(1){//父进程printf("我是父进程, pid: %d, ppid: %d\n",getpid(), getppid());sleep(1);}}else{//error}return 0;
}
运行结果:
问题:按照我们之前所学的知识,代码从上往下执行,如上代码运行不应该一直打印id为0的代码块吗,为什么实际运行结果是交叉打印子进程和父进程的代码块?
正常情况下我们的执行流是从上往下执行的,但调用fork之后,它会帮我们创建子进程,它会给子进程返回0,父进程中返回子进程id,所以fork之后代码就变成了两个执行流,一个执行流执行子进程的代码,一个执行流执行父进程的代码。
tip:fork以当前调用进程为模版创建一个子进程!
理解fork:我们需要解决以下几个问题
问题1:fork函数,究竟在干什么?
- fork以父进程为模版创建一个子进程
- fork之后,父子进程代码共享,数据不共享,采用写时拷贝。
问题2:为什么fork要给子进程返回0,给父进程返回子进程pid?
- 返回值为什么不同:因为fork之后的代码父子共享,所以fork返回不同的返回值,是为了区分,让不同的执行流,执行不同的代码块。
- 为什么子进程返回0,父进程返回子进程pid:
- 父进程可以有多个子进程,而子进程只有一个父进程。例如:在现实生活中,一般一个父亲可以有多个子女,而一个子女只有一个父亲。
- 父进程将来要对子进程做控制,为了区分不同的子进程,所以在父进程中必须返回子进程的pid,用来标定子进程的唯一性。
- 而子进程不一样,它的父进程只有一个通过getppid就可以获取,所以只需要返回0知道子进程创建成功即可。
问题3:一个函数是如何做到返回两次的?
- 一个函数在返回之前,它的核心工作一定都做完了,所以fork的return语句属于父子进程共享的,父进程返回一次,子进程返回一次
问题4:一个变量怎么会有不同的内容?
- 因为一、fork之后,父子进程代码共享;二、fork返回的时候,父子进程返回值不一样,发生写时拷贝。所以一个变量会有不同的内容!
补充:如果父子进程被创建好,fork之后,谁先运行?
- 谁先运行,由调度器决定,是不确定的
- 调度器让每个进程公平的被调度
2. 小结
- fork创建子进程
- 父子进程代码共享,数据不共享写时拷贝
- fork之后通常要用if进行分流,让父子进程做不同的事情
- bash通过fork创建子进程,fork之后,父进程负责命令行解释,子进程负责解释指令
四、进程状态
1. 操作系统的进程状态
如下我们百度获取的两张进程状态图:
进程状态:进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。
- 在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。
- 在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
- 下面我们只重点学习运行、阻塞、挂起这三个状态
1、运行态
- 对于操作系统,我们的任何进程运行时,都需要消耗CPU资源
- 所有运行在系统里的进程都以task_struct双向链表的形式存在操作系统里
- 在操作系统中想要运行的进程是非常多的,而CPU资源是少的,所以进程需要去竞争CPU的资源,但调度器需要让每一个进程都被合理的使用,所以每个CPU都需要维护一个运行队列struct runqueue
- 只要链入运行队列的进程,所处的状态就叫做运行态(R态)。 理解:只要处于运行队列的进程,都表示我已经准备好了,随时可以被调度!
- 问题:一个进程只要把自己放到CPU上开始运行了,是不是就一直要执行完毕,才能自己放下来?
不是,因为每一个进程都有一个叫做时间片的概念,例如一个进程最多在CPU上运行10ms,时间到了,CPU就会把进程放下来!——并发执行:在一个时间段内,所有的进程都会被执行!- 进程切换:在CPU上有大量的把进程拿上去、放下来的动作
2、阻塞态
- 进程的阻塞状态是指进程因等待某些资源或事件而暂时无法继续执行的状态。例如:当进程执行I/O操作时,如果I/O设备忙,进程会进入阻塞状态,等待I/O操作完成。
- 阻塞态的特点:
- 暂停执行:处于阻塞状态的进程会暂停执行,直到所等待的事件或资源变得可用。
- 队列管理:通常,处于阻塞状态的进程会被排成一个队列,有的系统会根据阻塞原因的不同将进程排成多个队列。
- 每一个外设都有一个等待队列,在等待队列中的进程状态我们叫做阻塞状态
- 在阻塞状态下,进程无法运行
3、挂起态
- 这是一种极端情况,通常发生在内存资源严重不足时,操作系统会将不常用的进程的代码和数据移动到磁盘的Swap分区,仅保留PCB在内存中。
- 挂起态的换出和换入:
- 换出:将进程的代码和数据移动到磁盘的swap分区
- 换入:将进程的代码和数据从swap中移动到操作系统
- 挂起态在保证进程的正常调度的情况下,节省了内存资源
- 一般现在的挂起都是阻塞挂起(也有其他挂起),一般挂起了都是阻塞的,但阻塞不一定是挂起的
- 系统中是否存在该进程,只取决于进程的PCB是否在内存中(理解:一个人是否是学生,不取决你是否在校,取决于你是否存在学校的学生系统中)
2. Linux操作系统的进程状态
不同的进程状态就决定了该进程当下要做什么事情。例如:
- 当前我是R状态,我接下来就是要被调度运行了;
- 当前我是阻塞态,我接下来就要等条件就绪等设备准备好,设备准备好之后我要把我的状态改为运行态投递到运行队列中,然后被CPU调度运行;
- 当前我是挂起态,我首先要做的就是将swap盘中的代码和数据换入到内存
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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状态变化图:
2.1 R运行态
- R运行状态(running):R状态就是Linux中的运行状态,运行状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- 注意:不要用你的感受去于CPU的速度做对比,因为CPU的速度太快了。
- 例如:我们写一个C程序死循环打印,将其运行之后,猜测它是什么状态
#include<stdio.h>int main()
{while(1){printf("hello\n");}return 0;
}
运行该程序后,我们查看该进程状态:
以我们的感受,我们看到该进程一直在打印运行,但是查看该进程的状态却是S状态,不是R状态!这是为什么呢?
修改代码,我们不再打印,仅仅是一个空语句死循环。此时我们再运行程序,查看进程状态就是R状态了。
需要一直打印的进程状态是S,不需要打印的进程状态是R,这是为什么——不要用我们的感受去揣测CPU的速度,因为打印是需要访问显示器设备的,当需要一直打印的时候他就需要频繁的去访问显示器,显示器可能并不是可以直接写入的状态,所以进程有很大的概率都是在等待显示器就绪,所以进程状态大概率是阻塞态S
tip:
- 我们看到R+后面有一个+,这表示我们当前进程在前台运行。(前台:我们此时运行了的程序,我们的bash解释器就不再运行了,输指令没有反应了)
- 运行程序的时候在后面空格加&,就代表进程在后台运行。注意:处于后台运行的进程【ctrl + C】就不能结束进程了,只能通过kill -9 pid结束进程。
2.2 S睡眠状态&D磁盘休眠状态
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
因为CPU的速度很快,所以大多数进程状态都处于等待事件完成,即S态。
在Linux操作系统中的S态对应的就是操作系统中的阻塞状态。
在Linux操作系统中阻塞状态不仅仅有S这样的浅度睡眠状态,还有D这样的深度睡眠状态!
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
浅度睡眠:可以被唤醒!——理解:随时可以响应外部的变化。例如可以被kill -9 pid杀掉
深度睡眠:磁盘休眠,让进程在等待磁盘写入完毕期间,要保证这个进程不能被任何人杀掉!例如:现在有一个进程,假设他要向磁盘写1GB数据,此时磁盘需要花时间将这1GB数据写入,在这一段时间里进程一直在等待磁盘写入完毕返回响应,响应写入成功或写入失败。问题:假设此时OS压力很大,内存严重不足,OS会杀掉一些它认为不重要的进程,这会OS发现有一个进程一直在等待磁盘写入,自己一点事不干,就把它杀掉了。但是这会出问题了磁盘写入数据失败,返回响应的时候进程又被杀掉了,数据就被丢失了。那数据丢失是谁的责任,找谁负责——1、找OS?OS有管理软硬件资源权力,并且OS是在内存严重不足的情况下杀掉的进程,所以数据丢失与OS无关,OS的职责就是保证系统不挂,并不是保证数据不丢失!2、找磁盘?磁盘就是一个跑腿的,磁盘的工作模式就是如此,磁盘写数据的时候,就是需要进程等待磁盘返回响应。所以数据丢失不关磁盘,磁盘的工作模式就是如此。3、找进程?进程是被OS杀掉的,不是进程自己跑路了,不在哪里等待的,所以丢失数据也不关进程的事。
浅度睡眠,可以接收外部请求可以被杀死;深度睡眠,不接收任何外部请求不可以被杀死!
当用户都可以查看到D状态时,说明操作系统的压力很大了,快要崩溃了!
2.3 停止/暂停状态-T/t
T: 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
进程中有一个概念叫做信号,在之前我们就使用过9号信号杀掉进程,我们可以通过以下指令查看进程信号有哪些:kill -l
我们可以使用19号信号暂停进程,如果想重新将暂停的进程运行起来可以使用18号信号。
暂停进程及恢复进程,进程会变成后台进程。
S休眠状态和T暂停状态有区别吗——有,S一定是在等待某种资源,但T可能在等待某种资源也可能并没有等待某种资源只是单纯的控制进程暂停
gdb控制进程暂停——gdb调试
2.4 X死亡状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。因为当进程结束时,CPU会回收其资源,包括进程控制块(PCB)和代码数据等
Linux中的X死亡态对应操作系统中的终止态。
2.5 Z僵尸状态
2.5.1 僵尸进程
一个进程在死亡之后,并不会直接进入X死亡状态,而是进入Z僵尸状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
理解:在电影之中我们看到发生命案的时候,都是首先警察来查明死因,是正常死亡还是非正常死亡,确认了死因之后才能通知家属来把人带走。
查明死因这个时间段所处的状态就是僵尸状态,确定死因进入回收所处的状态就是死亡状态。
僵死进程会以Z僵尸状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
僵尸进程代码示例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main()
{pid_t id = fork();//子进程结束后,父进程没有主动回收子进程信息if(id == 0){int cnt = 5;while(cnt--){printf("i am child, pid: %d, ppid: %d, cnt: %d\n",getpid(), getppid(), cnt);sleep(1);}exit(0);}else{while(1){printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}return 0;
}
监测进程状态:
while : ; do ps ajx | head -1 ; ps ajx | grep myprocess | grep -v grep; echo "--------"; sleep 1; done
进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,进程的相关资源尤其是task_struct结构体不能被释放!
2.5.2 僵尸进程的危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护!
- 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 总结:如果进程一直处于僵尸状态,那这个进程的资源会被一直占用,此时就会造成内存泄漏!
3. 孤儿进程
子进程处于僵尸状态,杀掉父进程时,我们发现父子进程都查不到了。
问题1:父进程不僵尸状态吗?
- 不是,他被bash回收了,任何的父进程只对它的子进程负责。
问题2:父进程如果提前退出,那么子进程后退出,进入Z之后,子进程如何处理呢?#include<stdio.h> #include<unistd.h> #include<stdlib.h>int main() {pid_t id = fork();//父进程提前结束,子进程后结束if(id == 0){int cnt = 100;while(cnt--){printf("我是子进程,pid:%d, ppid: %d\n", getpid(), getppid());sleep(1);}exit(0);}else{int cnt = 5;while(cnt--){printf("我是父进程,pid:%d, ppid: %d\n", getpid(), getppid());sleep(1);}}return 0; }
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收喽。(1号进程就是我们操作系统本身)
父子进程,如果父进程先退出,子进程的父进程就会被改成1号进程(操作系统),父进程是1号进程——孤儿进程!该进程被系统领养!
问题:为什么要被领养?
因为孤儿进程未来也会退出,也要被释放
4. 补充:task_struct的组织
进程具有父子关系,一个父进程可以有多个子进程,所以Linux中进程PCB结构是一颗多叉树。
Linux中对于PCB结构体的组织有多种,是怎么做的呢——在PCB结构体中添加链接属性字段,例如双向循环链表的组织
五、进程优先级
1. 基本概念
问题1:优先级是什么?
- 权限决定能不能,优先级是已经能了决定谁先谁后
- 优先级:对于资源的访问,谁先访问,谁后访问
问题2:为什么要存在优先级?
- 因为资源是有限,进程是多个的,所以注定了进程之间是竞争关系!——竞争性
- 因为操作系统必须保证大家良性竞争,所以要确认优先级
- 如果我们进程长时间得不到CPU资源,那该进程的代码长时间无法得到推进——进程的饥饿问题
总结:
- CPU资源分配的先后顺序,就是指进程的优先权(priority)
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的Linux很有用,可能改善系统性能(注:不要随便改变进程的优先级,因为只有调度器可以最公平的帮你调度你的进程)
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
问题3:Linux优先级是怎么做的?
- 在运行队列中定义了两个类型为task_struct*的指针数组
- 这个指针数组的下标对应着进程的优先级,我们根据进程的优先级把进程的PCB链入到不同的子队列中
- 这两个数组一个为正在被调度的运行队列,一个是空闲队列。已经被调度的进程和增加的新进程就链入到空闲队列中,运行队列调度完了就交换调度空闲队列。
- Linux内核的O(1)调度算法:通过位图,位图中的每一个比特位代表一个下标,所以我们只需判断该比特位是否为01来判断对应子队列是否为空,该时间复杂度为O(1)
- 所有在运行队列的进程都是R状态,他们最终会根据优先级打散到我们数组的不同下标处,所以我们就可以使用数组下标的不同从上向下访问遍历出来的PCB就是根据优先级调度的进程,所以我们就能做到根据不同优先级先调度那个进程,所以调整优先级的本质就是把进程的PCB链入到运行队列的对应下标处的子队列
2. 查看进程优先级
- 查看进程优先级
//查看当前终端下的进程优先级,不能查看其他终端的进程 ps -l //携带-a选项才能查看用户启用的所有进程,这就不仅能查看当前终端,还能查看其他终端了 ps -al //过滤其他干扰进程 ps -al | head -1 && ps -al | grep myproc
- 我们查出的进程属性有很多,如下:
- UID:代表执行者的身份(ls携带-n选项可以看到文件拥有者的UID)
- PID:代表这个进程的代号
- PPID:代表这个进程是由哪个进程发展衍生而来的,即父进程的代号
- PRI(priority):代表这个进程可被执行的优先级,其值越小越早被执行
- NI:代表这个进程的nice值,是进程优先级的修正数据
vim批量化注释:
- 命令模式下【ctrl+v】,左下角出现V-BLOCK
- 【h左j下k上l右】选中区域
- 【shift+i】进入插入模式,输入//注释
- 最后【ESC】
vim取消批量化注释:
- 命令模式下【ctrl+v】,左下角出现V-BLOCK
- 【h左j下k上l右】选中区域
- 最后【d】
3. PRI 和 NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
小结:
- 进程的优先级可以被调整
- 虽然优先级可以被调整,但是为了每一个进程都被公平的调度,Linux不想过多的让用户参与优先级的调整,所以优先级的调整范围nice为[-20,19]
- 进程默认的优先级为80,所以进程优先级范围为[60,99]
注意:
- 普通用户不能调整优先级
- 当我们的输入nice修正值大于边界值时取边界值
- 每一次调整优先级,默认的PRI(old)都是80
4. 操作系统是如何根据优先级,开展的调度?
- 位图就像位运算,其中的每一个比特位都有特定的含义,用01表示不同状态
- 一般的位图就是一串的01序列,用01表示不同的含义
- 位图可能有很多比特位,所以位图结构一般都是结构体里面套数组
//定义具有800比特位的位图 struct bitmap {char bits[100]; } int main() {//找第N个比特位int i = N / (sizeof(char) * 8);int pos = N % (sizeof(char) * 8);bitmap b;b.bits[i] & pos; }
- 在前面我们知道了每一个CPU都要维护一个自己运行队列,但并没有讲解运行队列怎么根据不同的优先级调度进程?
- 在这个运行队列中我们定义两个类型为task_struct*的指针数组,这个指针数组的大小为140,下标[0,99]是给其他种类的进程用的,有特殊用途我们不考虑,而下标[100,139]对应我们上面学习的优先级范围[66,99]
- 即这个指针数组不同的下标就对应进程不同的优先级,这个指针数组,我们只考虑下标[100,139]的区域
- 所有在运行队列的进程都是R状态,他们最终会根据优先级打散到我们数组的不同下标处,所以我们就可以使用数组下标的不同从上向下访问遍历出来的PCB就是根据优先级调度的进程,所以我们就能做到根据不同优先级先调度那个进程,所以调整优先级的本质就是把进程的PCB链入到运行队列的对应下标处的子队列
- 这两个指针数组,一个为正在调度的运行队列,一个为空闲队列。
- 为什么要有一个空闲队列——因为要解决在调度运行队列的时候,已经调度过的进程和不断新增的进程链入在哪里的问题——将调度过的进程和新到的进程链入到空闲的队列中
- 一个运行队列调度结束后再调度另一个空闲队列——通过交换实现
- Linux内核的O(1)调度算法:通过位图,位图中的每一个比特位代表一个下标,所以我们只需判断该比特位是否为01来判断对应子队列是否为空,该时间复杂度为O(1)
//运行队列 struct runqueue {//定义一个指针数组,task_struct*指向一个存放进程PCB的地址//下标[0,99]是给其他种类的进程用的,有特殊用途我们不考虑//刚才上面我们学习了优先级的范围是[66,99]是40个,而下标[100,139]也刚好是40,即优先级的范围和下标一一对应[66,99] --> [100,139]//这个指针数组的下标就是进程的优先级//这个指针数组,我们只考虑下标[100,139]的区域task_struct *running[140];//问题:我们在调度运行队列的时候,不仅要将已经调度过的队列排到后面又会不断地有新进程链入,该怎么解决呢?//1、添加一个镜像指针数组,将调度过的进程个新到的进程链入到空闲的队列中task_struct *waiting[140];//2、一个运行队列调度结束和再调度另一个运行队列——通过交换实现//指向running指针数组task_struct **run;//指向waiting镜像指针数组task_struct **wait;//交换两个指针,可以认为run一直指向运行队列,wait一直指向空闲队列swap(&run,&wait);//问题:判断队列是否为空,我们只能遍历数组,但时间复杂度太高了,该怎么解决?//通过位图,位图中的每一个比特位代表一个下标,所以我们只需判断该比特位是否为01来判断对应子队列是否为空,该时间复杂度为O(1)——Linux内核的O(1)调度算法!//小结:所有在运行队列的进程都是R状态,他们最终会根据优先级打散到我们数组的不同下标处,所以我们就可以使用数组下标的不同从上向下访问遍历出来的PCB就是根据优先级调度的进程,所以我们就能做到根据不同优先级先调度那个进程,所以调整优先级的本质就是把进程的PCB链入到运行队列的对应下标处的子队列
六、其他概念
竞争性:系统进程数目众多,而CPU资源只有少量,甚至只有1个,所以进程之间是具有竞争属性的。为了高效完成任务,更加合理竞争资源,便有了优先级。
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行:多个进程在多个CPU下分别,同时运行,这称为并行
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内, 让多个进程都得以推进,称之为并发
- 进程切换为什么我们没有感觉到?——》不要用我们人为的感知来衡量CPU的速度,如果你都能感知到进程切换了,那CPU也太差了!
- 一个进程被调度后,如果代码一直没有跑完会不会一直占用CPU资源?——》不会,因为操作系统不允许,所以每一个进程都有一个时间片的概念。
- 并发 = 进程切换 + 时间片,即并发是基于进程切换基于时间片轮转的调度算法。
- 进程是如何切换的?
- CPU中包含各种寄存器,寄存器具有对数据临时保存的能力。例如:函数的返回值是局部变量,函数销毁的时候也会随之销毁,外部能拿到函数的返回值就是通过CPU的寄存器!
- 程序运行时,它怎么知道当前运行到了哪一行?系统如何得知我们的进程当前执行到哪一行代码了?——》通过程序计数器(PC指针/eip指令指针):记录当前进程正在执行指令的下一行指令的地址!
- CPU中寄存器有很多:通用寄存器(eax,ebx,ecx,edx)、栈帧寄存器(ebp,esp,eip)、状态寄存器(status)……
- 为什么CPU中有这么多寄存器,他在其中扮演什么角色?——》提高效率,将进程高频数据放到寄存器中——》CPU内寄存器保存进程相关数据!
- 即CPU寄存器保存进程的临时数据——进程的上下文
- 进程在从CPU上离开的时候,要将自己的上下文数据保存好,甚至带走!(保存的目的,就是为了未来的恢复!)
- 进程切换其实有两个阶段:一、保存上下文;二、恢复上下文。