目录
1、进程的概念
2、进程控制块 - PCB
task_ struct内容分类
3、查看进程
通过ps命令查看进程
通过proc查看
4、通过系统调用获取进程标示符
5、通过系统调用创建进程-fork初识
6、进程状态
操作系统进程状态
Linux进程状态
僵尸进程
僵尸进程的危害
7、进程状态总结
8、孤儿进程
9、进程优先级
基本概念
查看系统进程
PRI and NI
查看进程优先级信息
通过top命令修改进程优先级(ni)
四个重要概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
- 严格意义上的进程 = 可执行程序 + 该进程对应的内核数据结构
假设你在磁盘上写了一个名为test.c的文件代码,最终经过编译链接形成test.exe可执行程序。这个可执行程序是一个文件,当前在磁盘中,根据冯诺依曼体系得知,磁盘就是一个外设,你的程序最终要被CPU运行,要先从外设(磁盘)加载到内存中,而加载的过程就叫做此程序运行起来了,但是这不能说是进程,此程序只是搬到内存,依旧是个程序(就比如我去北大逛了一圈,就能说我是北大的吗?)
操作系统里面可能同时存在大量的进程 ,操作系统要将所有的进程管理起来,而对进程的管理,本质就是对进程数据的管理,依旧是先描述,再组织。下面分开讨论:
先描述:
- 所以当一个程序加载到内存时,除了加载了代码和相关数据,操作系统还要为了管理该进程创建了对应的数据结构。而Linux操作系统描述进程是用一个叫task_struct的结构体,进程的所有属性数据全部放在里头(task_struct),当我们有多个进程的时候,也相应的要匹配多个task_struct结构体。此时描述好后还需要再组织起来:
再组织:
- 这里我们把所有的task_struct用双链表链接起来即可,此时对进程的管理就变成了对内核数据结构的管理。
而在操作系统里我们把描述进程的结构体叫做PCB(进程控制块),而Linux下的进程控制块叫做struct task_struct,以此来描述进程结构体。
综上所谓的进程就是把描述进程的结构体(task_struct)和可执行程序(代码、数据)结合起来称之为进程。
task_struct主要包含以下内容:
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/ O状态信息:包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
假设我现在生成了一个可执行程序mytest,它现在位于磁盘上,现在我们将程序运行起来,此时这个程序就变成了一个进程。
我们该如何查看现在的进程呢,主要有下列几种办法:
- 通过ps命令查看进程
- 通过proc查看
下面逐个分析:
通过ps命令查看进程
ps ajx | grep 'mytest'
ps ajx此命令会把你系统中所有的进程显示出来,随后我们跟了的几个命令为的是把名为mytest的进程显示出来,如下:
明明要的是mytest进程,为什么会出现grep的进程呢?
- 我们自己写的代码,编译成为可执行程序,启动之后就是一个进程,同样别人写的代码,启动之后也是一个进程。例如先前学到的ls、pwd、touch、grep、chgrp……指令,这些指令就是别人写的,存在目录/usr/bin/里头, 这些指令在执行后也会成为进程,这也就是为什么上面会把grep显示出来。
如何只显示mytest进程呢?
- 输入下面的命令:
ps ajx | grep 'mytest' | grep -v grep
此命令就是把带有grep字符的进程屏蔽掉,此时展现出来的就是我mytest进程。
我们都清楚根目录下有很多的路径:
注意上面的proc目录,它是一个内存文件系统,里面放的是当前系统实时的进程信息。我们进入此目录看看:
每一个进程在系统中,都会存在一个唯一的标识符,就如同每个学生都有学号一样,而这个唯一标识符在linux称为pid(全称process id),而上面蓝色的数字即为进程的pid。
我们可以查看你显示出来的所有的title列名称,输入下面的指令:
ps ajx | head -1
这一行显示的就是所有的属性名,提取好了属性,下面继续往后显示mytest的进程,直接使用&&逻辑与操作:
ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v grep
前面又得知proc存放的是当前系统实时的进程信息,现如今我们已然得知进程mytest的pid(11009),下面就在proc目录里面查询一下:
而如果我们关掉mytest的运行,根据proc是实时显示进程的性质,它此时就不会再显示进程mytest了:
而当我们再次运行起mytest程序时,再使用刚才的命令,结果如下:
此时会发现,先前的pid失效了,由此得知,重新启动程序就叫做一个新的进程,会给你重新分配一个pid。
补充1:如何理解当前路径?
下面输入这条指令:并从中截取这块内容。
ls /proc/11067 -al
- cwd表示进程当前的工作路径。
- exe表示进程对应的可执行程序的磁盘文件。
先前我们学习文件操作时知道,如果我用fopen创建一个文件,它默认是在当前路径,而当前路径指的就是当前进程所在路径,进程自己会维护。
补充2:pid,当前路径,这些都是进程的内部属性。都在进程的进程控制块PCB(task_struct)结构体中!!
这里我们可以通过使用系统调用函数getpid和getppid来分别获取进程pid和父进程ppid。
运行该程序,即可获得该进程的pid和ppid:
我们下面可以通过ps命令查看该进程的相关信息,
补充1:如何杀掉进程?
- 1、按下ctrl + c
- 2、使用命令:kill -9 pid
补充2:父进程ppid是不会变的
前面我们得知,我们每重启一个进程,其pid是会不断变化的,可是父进程ppid是不会变的:
我们可以输入下面的指令来查看这个父进程到底是什么:
ps ajx | head -1 && ps axj | grep 13009
图中可以看出父进程13009就是一个bash。几乎我们在命令行上所执行的所有的指令,都是bash进程的子进程!
fork函数是用来创建子进程的,它有两个返回值:
- 返回成功的话,把子进程的pid返回给父进程,给子进程返回0,
- 失败的话,-1返回给父进程。不创建子进程
因为其有两个返回值,这导致我一个printf语句运行后,会输出两个,并且返回的值一个为子进程的pid给父进程,一个0给子进程:
根据上面得知,既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。示例:
- 由上图得知,子进程为16021,父进程为16020,父进程的父进程为14123,此外,我们还可以发现这里的if 和 else语句竟然可以同时执行。
总结:
- fork之后,父进程和子进程会共享代码,一般都会执行后续的代码,这就是为什么printf会打印两次的原因。
- fork之后,父进程和子进程返回值不同,可以通过不同的返回值,判断,让父子执行不同的代码块。
补充1:fork()为什么给父进程返回子进程的pid,给子进程返回0?
- 父进程必须有标识子进程的方案,这个方案就是fork之后给父进程返回子进程的pid!
- 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本非常低,只需要getppid()即可,所以只需要给子进程返回0即可。
补充2:为什么fork会返回两次?
- fork是用来创建子进程,则会导致系统多了一个进程(task_struct+子进程的代码和数据),子进程的task_struct对象内部的数据大部分从父进程继承下来的。子进程和父进程执行同样的代码,fork之后,父子进程代码共享,而数据要各自独立。虽然代码共享,但是可以让不同的返回值执行不同的代码。
- 调用一个函数,当这个函数准备return的时候,这个函数的核心功能已经完成,子进程已经被创建了,并将子进程放入运行队列,现在父进程和子进程既然都存在,并且代码共享,那么当然return要返回两次。自然就有两个返回值。
进程状态本质上就是一个整数,在进程的task_struct。
- 进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
下面我将从操作系统层面上的进程状态以及细化到Linux层面上的进程状态来综合讲解:
来看这样一幅图:
下面我将针对运行、终止、堵塞、挂起这四个状态来进行讲解,之后我们就能对这幅图有个清晰的认知。
1、进程运行:
运行态指的是进程只要在运行队列中就叫做运行状态,它表明进程要么是在运行中要么在运行队列
里。代表我已经准备好了,随时可以调度。2、进程终止:
- 这个进程还在,只不过永远不运行了,随时等待被释放!
进程都终止了,为什么部立马释放对应的资源,而要维护一个终止状态?
- 因为即使你进程终止了,但是你操作系统并不能立马就来释放你,就好比如你在一家餐厅的某个位置吃饭,当你吃饭走人后,这个位置能再次被别人使用吗,当然不能,因为服务员还没来得及收拾你的残渣。所以既然你已经终止了,那么就需要一直维持着终止状态,以此告诉操作系统等你不忙了赶紧来把我释放掉,这就是随时等待被释放。
3、进程阻塞:
- 一个进程使用资源的时候,不仅仅是在申请CPU资源,进程可能申请更多的其它资源(磁盘、网卡、显卡、显示器资源、声卡……)
- 如果我们申请CPU资源,暂时无法得到满足,因为资源永远是少数的,CPU只有一个,你在申请这块资源,可是其它进程可能也在申请这块资源,如果给了A,那么B和C该怎么办呢?所以我们在申请的时候是需要排队的,我们把此队列称为运行队列。如果我们申请其它慢设备资源呢也是需要排队的。
- 进程为了管理所有的软硬件,必须先描述再组织,所有的软件在系统里都存在对应的数据结构来维护描述它,假设现在有一个资源正在被cpu调度,并且要读1个G的数据到内存,cpu就要执行它读数据的代码,可是当前磁盘正忙着呢,但是这个在cpu内部的进程不能干等着啊,不能影响cpu快速的访问其它进程啊,所以操作系统把此进程由cpu那放到了磁盘那,并让cpu继续执行其它进程,类似的,如果cpu需要读取网卡,可是网卡正在被别人读取,那么就把此进程放到网卡这个结构体那里,让cpu继续往后读取。
总结:
- 当进程访问某些资源(磁盘、网卡……)时,如果该资源暂时没有准备好,或者正在给其它进程提供服务,此时:1、当前进程要从runqueue移除。2、将当前进程放入对应设备的描述结构体中的等待队列!!!而这件工作是由操作系统完成的。
- 当我们的进程在等待外部资源的时候,该进程的代码,就不会被执行啦!!此时我们作为用户看到的现象就是我的进程卡住了(类似于你下载大型软件,看视频会卡一样),我们把这样的状态就称之为进程堵塞!!!
- 通俗说就是进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,进程所处的状态就叫堵塞!!!
4、进程挂起:
- 挂起和阻塞有一点类似,挂起最终也是卡住了,不过挂起和阻塞在操作系统的定义上还是有点区别的。一个进程挂起,它不会去申请cpu资源,也就意味着该进程此时处于一个非运行状态。在上层看来此进程也就是被卡住了,如下是我们先前对进程理解的抽象图:
上图面临一个问题:如果进程过多而导致内存不足了怎么办?
- 此时操作系统就要帮我们进行辗转腾挪。我的cpu只有一个,能被运行的进程也只有那一点点,并且都在运行队列里,那么其它进程就在阻塞队列里,比如在等待磁盘网卡之类的,可是磁盘可能已经相当忙了,排队已经排到了30个进程之外,此时磁盘短期时间内根本就不会就绪,而你又在磁盘上等待,但是你又占着PCB,代码和数据依旧在内存里放着。
- 所以操作系统会把短期内不会被调度(你等的资源,短期内不会就绪)的进程,且它的代码和数据依旧在内存中!就是白白的浪费空间,OS就会把该进程的代码和数据置换到磁盘上,如图所示:
- 总结:如上,当把该进程的代码和数据置换到磁盘中后,释放掉这块空间,此时内存就多出来了这块空间的容量,可以短期内让其它进程使用,因此操作系统通过这样的方式可以短暂的让进程只残留PCB,剩下的代码和数据全部置换到磁盘上(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操作系统中,我们可以输入下面的指令来查看进程状态:
ps aux / ps axj
下面具体展开介绍各个进程状态:
1、运行状态 - R
- 一个进程处于运行状态(running),并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
示例:
当我们编译运行此程序后,进入死循环,此程序没有访问其它外设资源(读文件、读磁盘……)此进程一直在cpu的队列里,下面来查看此进程的状态:
- 综上,只要你的代码不访问外设,只访问cpu资源,只要能抛弃来,那么就是R状态。
2、浅度睡眠状态 - S
示例:
我们运行此代码,并查看此进程的状态:
我明明已经运行了此进程,可为什么显示的会是S睡眠状态呢?
- 这里执行了printf语句,想要打印信息,这就需要访问外设(显示器),但是想占用显示器的不止你一个,其它人可能正在使用,操作系统就会把你的进程PCB放到显示器的等待队列里头,当轮到你访问的时候,操作系统再把你唤醒,此时你就可以打印了,这就是S睡眠状态,对应的就是上文操作系统层面上的阻塞状态。
S睡眠状态又叫做浅度睡眠和可中断睡眠。
- 浅度睡眠:当进程处于S状态,它可以随时被操作系统唤醒。
- 可中断睡眠:当进程处于S状态,它可以被你随时kill杀掉:
3、深度睡眠状态 - D
- D状态也是一种阻塞状态,它也是要让进程去等待某种资源,资源不就绪,那么此进程就处于某种等待状态。一般而言,Linux中,如果我们等待的是磁盘资源,我们进程阻塞所处的状态就是D。
下面对D状态进行深度剖析:
- 假设我内存上一个500MB大小的进程,要写到磁盘上,我们假设磁盘写入这500MB的数据耗费时间很长(实际非常快),此时该进程只能处在内存里默默等待磁盘完工,此时的进程就是S状态,不会被运行,等待磁盘资源就绪,可是在进程等待的过程中,内存中的进程越来越多,操作系统也越来越忙,假设操作系统在扫描所有进程的时候路过此S状态的进程,见此进程啥事不干,于是把它kill杀掉(一般服务器压力过大,OS是会终止用户进程的!!),可是当磁盘写完数据回来(无论写入成功与否)呼叫此进程的时候发现进程竟然不见了。
我们假设上述磁盘读取的500MB数据很重要,可是无论磁盘读取成功与否,此数据都失效了,因为对应的进程被OS杀掉了,那么这个锅到底谁来背呢?
- 对于操作系统:OS本身的设定就是如果内存不够,OS有权利去杀掉任何进程,这是OS的权利,所以不赖OS。
- 对于被kill的进程:此进程把数据给磁盘就是应该做的,因为需求在那,进程也确实需要等待磁盘给我反馈结果,可是等待的过程中莫名其妙被OS杀掉了,想想就离谱,所以不赖进程。
- 对于磁盘:磁盘在这里扮演的角色就是一个跑腿的,进程需要把数据写到磁盘上,那我磁盘听从指令慢慢写入,这就需要进程在那里等待我磁盘的反馈结果,到底是读取成功与否。所以不赖磁盘。
综上,OS、进程、磁盘这三者好像都没有错误,所以设计操作系统的人就在想能否不让OS杀掉这个在等待的进程呢,因此设计出了D状态,深度睡眠,也就是不可被中断睡眠。此时进程在等待磁盘的过程中,就由浅度睡眠S演化为深度睡眠D,具有了不可被中断属性,自然就不会被OS误杀了。而要彻底解决掉D状态,唯有关机重启和拔电源能够解决。
4、死亡状态 - X
这个状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,你不会在任务列表里看到这个状态。
5、僵尸状态 - Z
- 假设小王在外出跑步时,看到前面原先跑的好好的小刘突然倒下了,此时小王叫来了医生和警察,首先医生检查了一下,判断小刘已经“无了”,此时医生的任务已经完成并回去了,接着警察派出法医去检测小刘的死因,当法医检测好死因后,此时警察的事也完成了,往后就是查案,在走之前,通知了小刘的家属,告知其具体情况并让他们来认领“小刘”,
- 从小刘的倒下一直到被抬走,整个过程小刘已无生命迹象,但是并未有立刻抬走,因为警察要确定死因,在警察所排查的过程中,小刘所处的状态就是僵尸状态。
- 总结:当一个Linux中的进程退出的时候,一般不会直接进入X死亡状态(资源可以立马被回收),而是进入Z状态。
为什么进程要进入Z状态呢?
- 进程被创建出来,一定是因为有任务让这个进程执行,当进程退出的时候,一般需要把进程的执行结果告知给父进程或OS以此让我们得知任务的完成结果。
- 进程进入Z状态,就是为了维护退出信息,可以让父进程或os读取的。最后才能进入X状态。
6、暂停状态 - T/t
- 在Linux中,我们可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
暂停的意思是你本来是运行的,后来让你暂停了,假设现在有一个正在运行且无线循环的进程:
如果想对此进程暂停,输入以下命令:
kill -l
我们对此进程发送SIGSTOP也就是第19个信号,该进程就进入到了暂停状态。
kill -19 pid
我们再对该进程发送SIGCONT也就是第18个信号,该进程就继续运行了。
补充:
假设现在对我的一串代码进行调试,并打了一个断点,仔细观察我进程的状态:
- 小t也是暂停,不过它代表的是进程被调试的时候,遇到断定所处的状态,追踪状态。
前面说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程。再来回顾下:
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
下面来创建一个维持30秒的僵死进程示例:
#include
#include #include int main() { pid_t id = fork(); if (id == 0) { //child int cnt = 5; while (cnt) { printf("我是子进程,我还剩下:%d S\n", cnt--); sleep(1); } printf("我是子进程,我已经僵尸了,等待被检测\n"); exit(0); } else { //father while (1) { sleep(1); } } return 0; } 运行该代码后,我们可以通过以下监控脚本,每隔一秒对该进程的信息进行检测。
while :; do ps ajx | head -1 && ps ajx | grep process | grep -v grep; sleep 1; echo "—————————————————————————————————————————————————————————————————"; done
通过对上述pid和ppid的观察,我们得知,26900是父进程,26901是子进程,此时运行了5秒钟,26901退出了,状态变成了Z状态。此时该进程就是僵尸状态。
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 最终会导致内存泄漏
- 进程运行在操作系统上叫运行,在Linux内核叫运行状态R。
- 进程终止在操作系统上叫终止,在Linux内核叫僵尸状态Z和死亡状态X。
- 进程阻塞在操作系统上叫阻塞,在Linux内核叫浅度睡眠状态S和深度睡眠状态D。
- 进程挂起在操作系统上叫挂起,在Linux内核叫S、D、T。
至此,值得关注的进程状态全部讲解完成,下面来认识另一种进程(孤儿进程)
我们针对上面的代码进行修改:
#include
#include #include int main() { pid_t id = fork(); if (id == 0) { //child int cnt = 5; while (1) { printf("我是子进程,我还剩下:%d S\n", cnt--); sleep(1); } printf("我是子进程,我已经僵尸了,等待被检测\n"); exit(0); } else { //father int cnt = 3; while (cnt) { printf("我是父进程,我:%d\n", cnt--); sleep(1); } exit(0); } return 0; } 我们运行此程序并查看其进程的相关信息:
图中我们可以看出子进程是7968,父进程是7967,当父进程7067退出的时候,父进程为什么没有变成僵尸状态Z呢?
- 因为父进程的父进程是bash,bash会自动回收它的子进程,也就是这里的父进程,换言之这里没有看到僵尸是因为父进程被它的父进程bash回收了。
先前我们说过,子进程退出的时候,父进程要通过某种方式回收子进程,但是这里很明显父进程先走了,子进程还没退出,可如果我子进程退出了,那谁来回收我呢?
- 总结:这里操作系统就扮演了干爹的角色,也就是如果父进程提前退出,子进程就会被1号进程(就是操作系统)领养,我们把这种被领养的进程称为孤儿进程。
补充:
- 这里注意当父进程退出后,由S+变成S,带加号的是前台进程,不带加号的是后台进程,并且这里使用ctrl+c并不能结束进程,需要手动kill进程:
什么是优先级 vs 权限(是什么?)
- 优先级是进程获取资源的先后顺序,好比如中午下课,一堆学生蜂拥食堂打饭,这就需要排队,假设你在队尾,虽然是队尾,但是你也能打到饭,只不过是先后的问题。
- 权限是能和不能的问题,好比如你看爱奇艺电影,vip电影只有vip用户才能看,普通用户不能看,因为你没有这个权限。
为什么会存在优先级?
- 就好比如我去医院挂号,这就是最典型的排队,而排队的本质叫做确认优先级,而排队的原因在于资源有限,你挂号来晚了就没你的号了
- 系统里面永远是进程占大多数,而资源是少数,cpu只有一个,可是进程有n个,这就导致竞争资源是常态,所以一定要确认前后,因此必须存在优先级来确定谁先谁后,
总结:
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
在linux或者unix系统中,我们运行一个无限循环的process文件,并用ps –l命令则会类似输出以下几个内容:
ps -l
我们很容易注意到其中的几个重要信息,如下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
Linux下用PRI(priority)和NI(nice)来确认优先级,
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高,越大代表进程优先级越低。
- Linux下默认进程的优先级是80
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,只能通过修改ni值来更改优先级
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice,注意每次设置优先级,这个old优先级都会被恢复成80。
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,所以pri的取值范围是60至99。分别40个级别
当我们创建一个进程后,我们可以使用ps -al命令查看该进程优先级的信息。
ps -al
修改进程优先级就是在修改ni值,这里有两种方法:
- 1、通过top命令更改进程的nice值
- 2、通过renice命令更改进程的nice值
- 注意优先级不能随便修改,它会打破调度器平衡的,如果你非得修改,就要用超级用户修改。
这里我们只讨论第一种方法:
- 通过top命令更改进程的nice值
输入sudo top命令:
按下‘ r ’键:此时会要求你输入待调整nice值的进程的PID。
输入后按下回车,此时会要求你输入调整后的nice值。
修改号后,再ps -la查看是否已经修改:
Linux不允许无节制的修改优先级,根据先前的性质不难得知,尽管我ni的值设置为-100,可是ni最低是-20,因此这里ni值为-20,而PRI = PRI(old)+nice = 80 - 20 = 60。假设我后续把ni的值设置为10,最后ni的值为10,PRI的值为80 + 10 = 90,因为每次设置优先级,这个old优先级都会被恢复成80,所以是从80开始+。
- 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
下面具体解释并发:
- 多个进程在你的系统中运行 != 多个进程在你的系统中同时运行
- 进程不是占用了cpu,就会一直执行到结束然后释放cpu资源,我们遇到的大部分操作系统都是分时的!所谓的分时就是操作系统会给每一个进程,在以此调度周期中,赋予一个时间片的概念。
- 如图:假设进程1进入cpu运行,假设操作系统给它分配10ms的时间,如果10ms到了,无论结果如何都不会再让你进程1继续运行了,操作系统会把你进程1从cpu上剥离下来,然后再调度进程2……假设往后每个进程都是分配10ms,1s = 1000ms,那么在1s内,这5个进程平均每个都要调度20次。
总结:在一个时间段内,多个进程都会通过切换交叉的方式,让多个进程代码,在一段时间内都得到推进,这种现象,我们叫并发。
补充1:抢占式内核
- 我们的操作系统内部支持抢占式内核,正在运行的低优先级进程,但如果来个优先级更高的进程,我们的调度器会直接把进程从cpu上玻璃,放上优先级更高的进程,这就是进程抢占。
补充2:进程的 优先级 | 队列
- 操作系统内是允许不同的优先级的存在的,且相同优先级的进程,是可以存在多个的。但是进程是一个先进先出的队列,如果在原有稳定的进程基础上,突然来了一个优先级更高的进程,那它就会随便插队吗,这不就不符合队列的性质了。这里就可以借用指针数组、hash来解决:
总结:linux内核是根据不同的优先级,将特定的进程放入不同的队列中,而cpu就很自然的从数组的优先级最高的地方开始寻找进程。
补充3:进程间是如何进行切换的。
- cpu内部存在各种各样的寄存器,可以临时的保存数据。而寄存器又分可见寄存器和不可见寄存器。当进程在被执行的时候,一定会存在大量的临时数据,会暂存在cpu内的寄存器中。当你要把下一个进程放上来的时候,除了要把上一个进程拿走,还要把你的历史数据拿走,示例:
- 假如小王刚上大一,还没开学,随即填报了大学生征兵入伍,为了保留学籍,小王向学校申请了保留学籍,此时学校要留存小王的数据(个人信息,学籍……)。小王两年义务兵退伍后,随即又向学校申请恢复学籍,此时小王就可以从先前被切走的地方(大一)继续运行(上学)。此时学校扮演的角色就是cpu,小王在学校产生的所有临时数据就是上下文数据,小王就是一个进程,小王当兵被招走就相等于被操作系统切换下去,前提是在cpu内把临时数据保存好,为了的是后续的恢复。
总结:我们把进程在运行中产生的各种寄存器数据,我们叫做进程的硬件上下文,
- 当进程被剥离:需要保存上下文数据(task_struct)。
- 当进程恢复的时候:需要将曾经保存的上下文数据恢复到寄存器中。