一、进程状态
1.1 操作系统学科(运行、阻塞、挂起)
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态
大多数操作系统都遵循以下原则
1.1.1 运行状态
因为有一个调度器需要确保CPU的资源被合理使用,所以需要维护一个运行队列,他将进程的task_struct结构体连接起来,而被链接起来的进程就会按顺序被调度器调度,此时处于运行队列的这些进程就处于运行态,这说明运行态并不指的是正常运行的进程,而是处在运行队列中并且随时可以被调度的进程!
1.1.2 并发执行和进程切换
调度器将进程放到CPU上去运行,并不代表必须要将进程全部运行完才会被放下来!!因为
(1)进程当中可能会存在一些死循环的进程 (2)调度器要尽量保证公平性,不能让一个进程占用CPU太长时间。 ——>那么什么时候应该把这个进程放下来,就要取决于时间片,当一个进程在超过时间片还没有结束的时候,就要把他放下去然后重新放在新的运行队列的位置。
CPU运行速度是很快的,所以其实我们人所能感受到的,所以在一个时间段内必然所有的进程都会被执行,称之为并发执行。 而大量地把进程从CPU上拿上来在放下去的这个过程,称之为进程切换!
1.1.3 阻塞状态
操作系统管理硬件的过程也是需要先描述再组织,因此不同的硬件设备都需要维护一个阻塞队列,当该硬件没有准备好的时间,该进程只能在阻塞队列中等待。
比如说scanf函数从键盘获取数据,但是如果我们一直不输入的话,这个进程就会被阻塞!!
1.1.4 挂起状态
当操作系统的内部资源严重不足的时候,需要在保证正常运行的前提下想办法把一些内存里的资源置换到磁盘中来节省空间,我们将被置换的进程所处的状态叫做挂起状态。
(1)一般来说,导致内存资源不足的原因是因为存在大量处在阻塞队列的进程 ,所以我们要办法将一些资源置换到磁盘中,但是为了不影响阻塞队列的管理,所以大多数情况下并不止直接将task_struct结构体置换过去,而是将该进程的数据和代码先置换过去,而当执行到该进程的时候,再通过某种方式将其数据和代码置换回来。
(2)其实在我们的电脑中存在一个交换分区,该分区就是专门用来置换一些导致内存溢出的资源
1、挂起状态就是PCB在排队,但是他对应的代码和数据被暂时移到外设中,节省内存空间 。
2、一个进程是否被挂起并不需要让你知道,就跟你把钱存银行里一样,你并不知道自己的钱是被干什么用了,银行并不会告诉你,只是你想要的时候他能及时给到你就好!!
扩展知识:我们的电脑现在大多数使用的都是SSD固态硬盘,磁盘一般只有大公司的后端在使用,虽然比较慢但是便宜且容量更大。
1.2 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 */
};
1.2.1 进程状态查看
ps aux / ps axj 命令
1.2.2 R状态
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列中。
1.2.3 S状态
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。 ——>其实就相当于是阻塞状态(因为需要跟硬件保持联系)
为了更好地观察阻塞状态,我们来举个例子:
第一段代码是只有while循环
第二段代码有printf+while循环
问题1:为什么第一种情况是R状态,而第二种情况是S状态呢???
因为printf需要一直和显示器建立联系,所以有很大概率一直处在等待状态,因为需要等显示器准备好(CPU太快了 所以显示器缓不过来) ,如果不跟硬件建立联系那么该进程的执行速度是非常快的!!
问题2:键盘会因为用户不输入而卡着,那为什么显示器也会卡着呢??
因为cpu的速度比外设快太多了,所以大多数的进程都在等待,另一方面我们当前的机器是云服务器,用xshell进行链接,所以还会涉及到网络的概念,自然快不了。
同理,其实我们的bash进程也是S状态,因为他在等待你输入指令,你不输入他就会卡住
1.2.4 D状态
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
区分:S是浅度睡眠(可以被唤醒)、D是深度睡眠 (不相应任何需求)为了能够更好地理解他们的区别,以下会讲述一个故事
比方说我们现在编译了一段代码,需要将1GB的文件写入磁盘中,内存需要跟磁盘建立联系,磁盘在被写入之前需要判断该行为是否可以被执行,比方说现在磁盘中的空间不足1GB,那么这个请求就应该被驳回,这个过程中我们的内存需要先对磁盘说:“我打算写入1GB的内容,你看看可不可以” 磁盘回复:“那你稍等一下,我看看自己的空间是否足够” 当该信息确认后,然后磁盘回复进程:“我当前空间足够,可以执行” 。然后进程才会通过其对应的代码和数据来将1GB写入磁盘。 所以这个过程有发起也有返回,内存像磁盘申请,磁盘完成后将结果返回给内存,但是这个过程是需要等待的!!
假设当前有大量的进程处于阻塞队列,此时内存不够了,因此操作系统需要杀死一部分进程来保证运行 。当系统压力很大时,依靠内存的辗转腾挪解决不了时,操作系统只能想办法杀死他认为不太重要的进程!!
内存在向磁盘发出请求的时候,在磁盘还没回复是否可行的时候该进程就被操作系统杀死了,所以磁盘想要回复的时候发现该进程不在了,所以就懵圈了。当磁盘想要回应的时候却发现那个等待自己的进程没有了,那么现在写入失败了怎么办?我是应该继续尝试呢,还是丢掉呢??此时不同的操作系统有不同的做法。
比如是在银行,某些数据丢失导致损失了几个亿!!这个时候法官 叫来了 操作系统、进程、磁盘 三个人,来这个过程应该是谁的错,第一嫌疑人是操作系统,因为操作系统杀进程了,操作系统说:“请问我是否履行了自己的职责,我是否是在比较极端的情况下去杀进程的,我能做的最大努力就是保证操作系统不挂掉!!如果我有错,那我下次再遇到这种情况??我还做不做了?就算我不杀进程,导致操作系统挂了,他数据该丢还是会丢,还会影响其他进程,这个责任又该谁负责呢??” 法官觉得操作系统说得有道理,于是又把矛头转向了第二嫌疑人磁盘,因为磁盘在写入失败的时候擅自把数据丢失了。磁盘说:“这不怪我,我就是个跑腿的,我在写入的时候就告诉他可能会失败了,所以我让他在那里等我的结果,可是他人不见了,而是丢失是因为我还有其他工作得做,如果我有错的话,那我们是不是得把磁盘所有逻辑都改了???”法官觉得磁盘说的也有代理,于是又把矛头转向了进程,此时进程扑通一声跪了下来说:“我是被杀的,我怎么能有错呢?”所以凡是存在争议很大的地方,大部分都是因为制度设置的不合理。所以法官说,你们都回去吧,我把操作系统改一改——>让一些进行在磁盘写入完毕期间,这个进程不能被任何人杀掉,其实就是不接受任何响应,但是D状态不多见因为如果有很多说明系统已经临近崩溃了!!
1.2.5 T状态
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
指令:kill -l
其中9是杀进程,19是暂停进程,18是重启进程
T状态存在的意义:可能是需要等待某种资源,或者是我们单纯不想让该进程运行!!
应用场景就是gdb,当程序运行起来的时候遇到了我打的一个断点,然后就停下来了,这是这个过程就可以被应用于gdb这个进程在控制被调试的这个进程!!
1.2.6 X状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
1.3 僵尸状态
比如你正在公园跑步,突然看见一个老人走了两步就倒地上了,这时候你叫了120,120发现人已经没救了,于是走了。然后你又叫了110,但是110并不会立马清理现场,因为本质查明真相的原则,可能会需要先带着法医对尸体进行检测然后再确认结果,比如说异常死亡或者是正常死亡(因为家人需要了解情况),然后才会去清理现场。 其实这段已经死亡一直到清理现场之前的这段时间,就相当于是僵尸状态。
回到进程的角度,一个进程在退出的时候并不是立即把所有的资源全部释放,而是要把当前进程的退出信息维持一段时间!!——>因为父进程需要关心子进程!!
为了观察这个现象,我们需要想办法让子进程在中途退出,所以需要exit函数
情况1:当子进程先退出 父进程还在
1 #include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 int main()5 {6 pid_t id =fork();7 if(id==0)8 {9 int cnt=5;10 while(cnt)11 {12 printf("child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt); 13 --cnt;14 sleep(1);15 }16 exit(0);17 }18 else19 {20 while(1)21 {22 printf("parent,pid:%d,ppid:%d\n",getpid(),getppid());23 sleep(1);24 }25 }26 return 0;27 }
所以只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程保持Z状态,相关的资源尤其是task_struct结构体不能被释放!资源会一直被占用!(defunct是废弃的意思,表示当前进程已经死亡了)
问题1:为什么要存在僵尸状态??
——>因为他要告诉关心他的进程(父进程),你交代给我的事情我办得怎么样了,所以他一定要等到父进程读取之后才能完全死亡
问题2::那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费(甚至是内存泄漏)?
——>维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护!!需要占用内存。
情况2:父进程先退出 子进程还在
#include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 int main()5 {6 pid_t id =fork();7 if(id!=0)8 {9 int cnt=5;10 while(cnt)11 {12 printf("parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);13 --cnt;14 sleep(1);15 }16 exit(0);17 }18 else19 {20 while(1)21 {22 printf("children,pid:%d,ppid:%d\n",getpid(),getppid());23 sleep(1);24 }25 } 26 return 0;27 }
说明如果父进程比子进程先一步消亡,那么子进程会变成孤儿进程,他的PPID会变成1 也就是被系统进程给收养。
问题1:为什么要被领养呢??
——>因为孤儿进程未来也会消亡,也会被释放!!
问题2:ctrl+c为什么无法中止异常进程,他的底层原理是什么??
——>本质上是在一瞬间父进程会被bash进程回收掉!!所以子进程也在父进程退出的一瞬间被收回掉了!! 所以由于子进程的PPID不是bash进程而是系统进程,所以无法中止
问题3:子进程是bash进程的孙子进程,为什么父进程消亡后不由bash进程回收而是交由系统进程回收???
——>因为bash做不到,因为孙子进程不是他去创建的!! 他没有这个权限,而系统进程可以做到,因为要将孤儿进程托孤给系统进程 当然不同的操作系统具体的实现方法可能也不同!!
二、Linux具体是怎么维护进程的
你可能会有这样的疑问:Linux内部维护进程主要是采用双链表的形式管理,但是由于其可能有不同的应用场景需求,所以有些时候我们也要把它放到队列、二叉树……中管理,所以为了方便这样的操作,我们的task_struct结构体里面必然需要维护各种各样类型的指针!!那么具体应该如何实现呢??
首先并不是整个task_struct结构体链接在一起,而是通过单独创建一个node结构体来进行链接,所以其实节点都是指向该结构体中间的位置而不是头部
既然链表链接的并不是头部,那么我们通过节点的链接找到了下一个节点的某个位置,要如何去找到头部呢??
1、将0强转成task_struct结构体的类型,其实就是假设在0位置都有一个task_struct结构体大小的内存,然后找到他的node节点并取他的地址,由于低地址是0,所以找到node节点的地址就其实就相当于知道了node在task_struct的偏移量 ——> &(task_struct*)0—>node
2、当前找到的位置然后用当前指向的位置(比如start) 减去这个偏移量,就可以找到该结构体的头部。——>start-
3、最后将这个头部的地址强转成task_struct* 就可以拿到整个PCB结构体了 ,就能访问里面的其他数据
总结: ( task_struct*)(start-&(task_struct*)0—>node)——>other
三、进程优先级
3.1 如何理解优先级
问题1:优先级vs权限
——>权限的意义是这件事我能不能做,而优先级的意义就是对于资源的访问谁先谁后(cpu资源分配的先后顺序,就是指进程的优先权)
问题2:为什么需要有优先级
——>因为资源是有限的,而进程是多个的,所以进程之间存在竞争关系,优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可能会改善系统性能。
问题3:操作系统是如何做的
——>操作系统会根据自己的一套规则来尽可能地保证进程的良性竞争,如果进程长时间得不到CPU资源,那么该进程的代码就长时间无法得到推进(进程的饥饿问题),具体需要去给进程维护运行队列,让进程按顺序去执行,但为了防止某些进程运行时间过长,还会有时间片的限制(调度器在工作)
问题4:人为可以调整优先级吗?
——>优先级是可以被人为调整的,我或许可以通过调整优先级让自己的某一个进程可以在同一时间内一直被调度,但是其实Linux并不希望我们有过高的权限,所以他的调整也不是无规则地调整,是带有一定限制的!!
3.2 查看和调整优先级方法
ps –l命令则会类似输出以下几个内容:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
3.2.1 PRI和NI
PRI(priotity)即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
NI(nice)其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。
3.2.2 top更改nice值
进入top后按“r”–>输入进程PID–>输入nice值
3.2.3 nice和renice改变优先级
Linux nice和renice命令:改变进程优先级 -扫盲篇_nice设置优先级为什么正数设置不了-CSDN博客
其实这方面的知识并不需要了解很深,因为大多数场景下我们并不会人为地去修改优先级
四、Linux内核的调度算法
1、需要维护两个队列让他们按顺序排队运行
问题1:为什么会有0-99的进程出现呢??
——>因为这类进程可能是非常重要的!!也就是说无论你当前在运行什么进程,这类进程都会优先被调度(比方说电脑出故障了,发出警报,这个就是无论如何都要优先被警告)
问题2:为什么需要维护两个队列??
——>因为需要一个队列去运行,但是在运行过程中,后来准备好的进程就会被放到另一个等待队列中,因为这样才能确保按顺序,不然比如我当前运行到后面的时候突然前面又插队了几个进程进来,我又得回头去访问!!
问题3:同等优先级的怎么办?
——>因为维护的是一个指针数组,所以比如说我当前所处的优先级是100,那么下一个优先级100的进来之后就会被链接在后面。本质上是一个开散列!!
2、需要维护一个位图,来确认位置
问题1:如何确定位置呢?
——>因为优先级有各种各样的,比方说在100的位置有2个进程,在133的位置有2个进程,但是我们并不能马上知道这几个地方有进程,而是只能通过遍历数组的方式来一个个查看。最后我们最后当数组遍历到结尾的时候才能确定队列位空
问题2:用位图大O(1) 调度算法优化
——>只有100-139一共40个级别,我只需要用5个字节一共40个比特位来标记是否存在进程即可,这样我们就可以通过位运算的方法快速找到 队列中存在进程的位置。 最后当位图位0的时候,就说明队列位空了!!
3、需要维护两个指针
因为当运行队列运行完之后就要让等待队列进场,所以最好的方法就是维护两个指针分别指向两个队列,然后当运行队列为空的时候再交换一下指针的指向,让等待队列变成新的运行队列
五、进程重要名词概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行(少见,大多数配置的电脑都是只有一个cpu)
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发(常见)
六、Linux内核进程切换的方法
关于进程切换,我们需要提到并发,并发是基于进程切换基于时间片轮转的调度算法
那么进程具体是如何切换的呢??
6.1 CPU知识铺垫
问题引入
1、我们的函数返回值是局部变量,但是为什么可以被外部拿到呢??
——>因为将数据存储在CPU寄存器中带了出去!! 比如return a会转化成move eax 10的指令
2、系统如何得知我们的进程执行到了哪句代码??
——>因为有程序计数器pc(在PCB内部)以及eip(栈帧寄存器),他们的作用就是记录当前进程正在执行的下一行指令的地址 (其实我们选择语句、循环……的跳转都跟pc有关)
3、寄存器有很多,具体有哪些种类呢???
通用寄存器:eax、ebx、ecx、edx……(需要什么就做什么的寄存器)
栈帧寄存器:ebp、esp、eip……(ebp和esp是维护栈顶和栈底的,而eip是存储程序计数器的值,表示着进程的下一条指令应该从哪里执行)
状态寄存器:status(记录当前CPU的一些状态)
4、寄存器扮演什么角色呢??
——>寄存器存储的空间并不大,但是速度快,所以他的作用就是 提高效率,将进程的高频数据放入寄存器中!!
5、如果返回的是内置类型,寄存器可以做到,但如果返回的是一个特别大的对象呢??
——>因为内置类型很小,所以默认可以被翻译成mov eax ,但如果是一个对象的话,其实本质上来说就是进行了一个拷贝构造,如果是C++11的话可能还会涉及到移动语义,所以本质上来说也是由一个个语句组成的,所以就允许使用多个寄存器来工作。 相当于就是多次mov。
6、寄存器总结
——>(1)CPU寄存器里面保存的是进程的临时(高频)数据——>进程的上下文信息
(2)由于进程保存的是临时的数据,所以新的数据进入的时候可能会需要覆盖老的数据,因此进程的一些更重要的数据应该被放在离CPU更近的地方!!因为那样更安全更不易丢失!!(比如说重要数据肯定不适合放在通用寄存器上)
6.2 进程切换的本质
下面讲两个故事:
1、你步入大学,但是你有一个参军梦,于是你报名了并且成功被选上了,你很开心于是你就直接去军营了,但是你走之前床铺没有收拾、也没有告诉学校。 于是当你在军营的时候,其实学校并不知道有你去军营了,所以他会给你安排考试,安排宿舍……结果过了一段时间当你回校后,你发现你的宿舍早就换人了,你床铺的东西也都被丢了,然后你本来应该是大一的,却显示大三,挂了三四十门课,即将被勒令退学…… 这个时候你找到了学校说:“我又不是干坏事,而是去当兵,为什么要让我退学??” 学校:“这不怪我啊,你没有打招呼,我根本就不知道你当兵去了”……
2、还是刚刚的你,但是这次你把自己的床铺收拾好了打包带走,然后你走之前去跟导员报告,将自己的入伍证明交予他查看,然后像学校请求保留学籍,其实就相当于把你的档案给封存了,这个时候学校知道你去当兵了,所以就不会给你安排考试,不会安排宿舍,你的学籍被保留了,你是大二上学期当的兵,你回来时候也是大二上学期……
通过上面两个故事,我们明白了在我们离开之前一定要做好收尾,这样才能更好地回来,所以对于进程来说,进程从CPU离开的时候,也要将自己的上下文数据保存好然后带走,保存的目的是为了更好地回来(恢复)
问题引入:
问题1:进程在被切换的时候要做什么
——> (1) 保存上下文 (2)恢复上下文
问题2:pc帮助我们记录下一条指令要运行的位置,那么要保存的数据究竟是存放在哪里呢?
——>存储在PCB中,PCB内部应该有相关的结构体,在寄存器要去执行其他进程之前将相关的数据先存在内部,然后寄存器就可以离开了,当后面寄存器回来的时候就可以帮助进程恢复之前的数据。