当前位置: 首页 > news >正文

< Linux > 进程概念(1)

目录

1、进程的概念

2、进程控制块 - PCB

        task_ struct内容分类

3、查看进程

        通过ps命令查看进程

        通过proc查看

4、通过系统调用获取进程标示符

5、通过系统调用创建进程-fork初识

6、进程状态

        操作系统进程状态

        Linux进程状态

        僵尸进程

        僵尸进程的危害

7、进程状态总结

8、孤儿进程

9、进程优先级

        基本概念

        查看系统进程

        PRI and NI

        查看进程优先级信息

        通过top命令修改进程优先级(ni)

        四个重要概念


1、进程的概念

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体。
  • 严格意义上的进程 = 可执行程序 + 该进程对应的内核数据结构

假设你在磁盘上写了一个名为test.c的文件代码,最终经过编译链接形成test.exe可执行程序。这个可执行程序是一个文件,当前在磁盘中,根据冯诺依曼体系得知,磁盘就是一个外设,你的程序最终要被CPU运行,要先从外设(磁盘)加载到内存中,而加载的过程就叫做此程序运行起来了,但是这不能说是进程,此程序只是搬到内存,依旧是个程序(就比如我去北大逛了一圈,就能说我是北大的吗?)


2、进程控制块 - PCB

操作系统里面可能同时存在大量的进程 ,操作系统要将所有的进程管理起来,而对进程的管理,本质就是对进程数据的管理,依旧是先描述,再组织。下面分开讨论:

先描述:

  • 所以当一个程序加载到内存时,除了加载了代码和相关数据,操作系统还要为了管理该进程创建了对应的数据结构。而Linux操作系统描述进程是用一个叫task_struct的结构体进程的所有属性数据全部放在里头(task_struct),当我们有多个进程的时候,也相应的要匹配多个task_struct结构体。此时描述好后还需要再组织起来:

再组织:

  • 这里我们把所有的task_struct用双链表链接起来即可,此时对进程的管理就变成了对内核数据结构的管理。

而在操作系统里我们把描述进程的结构体叫做PCB进程控制块),而Linux下的进程控制块叫做struct task_struct,以此来描述进程结构体。

综上所谓的进程就是把描述进程的结构体(task_struct)和可执行程序(代码、数据)结合起来称之为进程


task_ struct内容分类

task_struct主要包含以下内容:

  • 标示符:描述本进程的唯一标示符,用来区别其他进程。
  • 状态:任务状态,退出代码,退出信号等。
  • 优先级:相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/ O状态信息:包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

3、查看进程

假设我现在生成了一个可执行程序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查看

我们都清楚根目录下有很多的路径:

注意上面的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)结构体中!!


4、通过系统调用获取进程标示符

这里我们可以通过使用系统调用函数getpidgetppid来分别获取进程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进程的子进程! 


5、通过系统调用创建进程-fork初识

fork函数是用来创建子进程的,它有两个返回值:

  • 返回成功的话,把子进程的pid返回给父进程,给子进程返回0,
  • 失败的话,-1返回给父进程。不创建子进程

因为其有两个返回值,这导致我一个printf语句运行后,会输出两个,并且返回的值一个为子进程的pid给父进程,一个0给子进程:

根据上面得知,既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。示例:

  • 由上图得知,子进程为16021,父进程为16020,父进程的父进程为14123,此外,我们还可以发现这里的if 和 else语句竟然可以同时执行。

总结: 

  1. fork之后,父进程和子进程会共享代码,一般都会执行后续的代码,这就是为什么printf会打印两次的原因。
  2. fork之后,父进程和子进程返回值不同,可以通过不同的返回值,判断,让父子执行不同的代码块。

补充1:fork()为什么给父进程返回子进程的pid,给子进程返回0?

  • 父进程必须有标识子进程的方案,这个方案就是fork之后给父进程返回子进程的pid!
  • 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本非常低,只需要getppid()即可,所以只需要给子进程返回0即可。

补充2:为什么fork会返回两次?

  • fork是用来创建子进程,则会导致系统多了一个进程(task_struct+子进程的代码和数据),子进程的task_struct对象内部的数据大部分从父进程继承下来的。子进程和父进程执行同样的代码,fork之后,父子进程代码共享,而数据要各自独立虽然代码共享,但是可以让不同的返回值执行不同的代码
  • 调用一个函数,当这个函数准备return的时候,这个函数的核心功能已经完成,子进程已经被创建了,并将子进程放入运行队列,现在父进程和子进程既然都存在,并且代码共享,那么当然return要返回两次。自然就有两个返回值

6、进程状态

进程状态本质上就是一个整数,在进程的task_struct。

  • 进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。

下面我将从操作系统层面上的进程状态以及细化到Linux层面上的进程状态来综合讲解:


操作系统进程状态

来看这样一幅图:

下面我将针对运行、终止、堵塞、挂起这四个状态来进行讲解,之后我们就能对这幅图有个清晰的认知。

1、进程运行: 

运行态指的是进程只要在运行队列中就叫做运行状态,它表明进程要么是在运行中要么在运行队列
里。代表我已经准备好了,随时可以调度。

2、进程终止:

  • 这个进程还在,只不过永远不运行了,随时等待被释放!

进程都终止了,为什么部立马释放对应的资源,而要维护一个终止状态?

  • 因为即使你进程终止了,但是你操作系统并不能立马就来释放你,就好比如你在一家餐厅的某个位置吃饭,当你吃饭走人后,这个位置能再次被别人使用吗,当然不能,因为服务员还没来得及收拾你的残渣。所以既然你已经终止了,那么就需要一直维持着终止状态,以此告诉操作系统等你不忙了赶紧来把我释放掉,这就是随时等待被释放。

3、进程阻塞:

  • 一个进程使用资源的时候,不仅仅是在申请CPU资源,进程可能申请更多的其它资源(磁盘、网卡、显卡、显示器资源、声卡……)
  • 如果我们申请CPU资源,暂时无法得到满足,因为资源永远是少数的,CPU只有一个,你在申请这块资源,可是其它进程可能也在申请这块资源,如果给了A,那么B和C该怎么办呢?所以我们在申请的时候是需要排队的,我们把此队列称为运行队列。如果我们申请其它慢设备资源呢也是需要排队的。
  • 进程为了管理所有的软硬件,必须先描述再组织,所有的软件在系统里都存在对应的数据结构来维护描述它,假设现在有一个资源正在被cpu调度,并且要读1个G的数据到内存,cpu就要执行它读数据的代码,可是当前磁盘正忙着呢,但是这个在cpu内部的进程不能干等着啊,不能影响cpu快速的访问其它进程啊,所以操作系统把此进程由cpu那放到了磁盘那,并让cpu继续执行其它进程,类似的,如果cpu需要读取网卡,可是网卡正在被别人读取,那么就把此进程放到网卡这个结构体那里,让cpu继续往后读取。

总结:

  1. 当进程访问某些资源(磁盘、网卡……)时,如果该资源暂时没有准备好,或者正在给其它进程提供服务,此时:1、当前进程要从runqueue移除。2、将当前进程放入对应设备的描述结构体中的等待队列!!!而这件工作是由操作系统完成的。
  2. 当我们的进程在等待外部资源的时候,该进程的代码,就不会被执行啦!!此时我们作为用户看到的现象就是我的进程卡住了(类似于你下载大型软件,看视频会卡一样),我们把这样的状态就称之为进程堵塞!!!
  3. 通俗说就是进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,进程所处的状态就叫堵塞!!!

4、进程挂起:

  • 挂起和阻塞有一点类似,挂起最终也是卡住了,不过挂起和阻塞在操作系统的定义上还是有点区别的。一个进程挂起,它不会去申请cpu资源,也就意味着该进程此时处于一个非运行状态。在上层看来此进程也就是被卡住了,如下是我们先前对进程理解的抽象图:

上图面临一个问题:如果进程过多而导致内存不足了怎么办?

  • 此时操作系统就要帮我们进行辗转腾挪。我的cpu只有一个,能被运行的进程也只有那一点点,并且都在运行队列里,那么其它进程就在阻塞队列里,比如在等待磁盘网卡之类的,可是磁盘可能已经相当忙了,排队已经排到了30个进程之外,此时磁盘短期时间内根本就不会就绪,而你又在磁盘上等待,但是你又占着PCB,代码和数据依旧在内存里放着。
  • 所以操作系统会把短期内不会被调度(你等的资源,短期内不会就绪)的进程,且它的代码和数据依旧在内存中!就是白白的浪费空间OS就会把该进程的代码和数据置换到磁盘上,如图所示:

  • 总结:如上,当把该进程的代码和数据置换到磁盘中后,释放掉这块空间,此时内存就多出来了这块空间的容量,可以短期内让其它进程使用,因此操作系统通过这样的方式可以短暂的让进程只残留PCB,剩下的代码和数据全部置换到磁盘上(swap分区),此时这样的进程就叫做进程挂起!!!

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操作系统中,我们可以输入下面的指令来查看进程状态:

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杀掉了,那么这个锅到底谁来背呢?

  1. 对于操作系统:OS本身的设定就是如果内存不够,OS有权利去杀掉任何进程,这是OS的权利,所以不赖OS。
  2. 对于被kill的进程:此进程把数据给磁盘就是应该做的,因为需求在那,进程也确实需要等待磁盘给我反馈结果,可是等待的过程中莫名其妙被OS杀掉了,想想就离谱,所以不赖进程。
  3. 对于磁盘:磁盘在这里扮演的角色就是一个跑腿的,进程需要把数据写到磁盘上,那我磁盘听从指令慢慢写入,这就需要进程在那里等待我磁盘的反馈结果,到底是读取成功与否。所以不赖磁盘。

综上,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<stdio.h>  
#include<stdlib.h>  
#include<unistd.h>  
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中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 最终会导致内存泄漏

7、进程状态总结

  • 进程运行在操作系统上叫运行,在Linux内核叫运行状态R
  • 进程终止在操作系统上叫终止,在Linux内核叫僵尸状态Z和死亡状态X
  • 进程阻塞在操作系统上叫阻塞,在Linux内核叫浅度睡眠状态S和深度睡眠状态D
  • 进程挂起在操作系统上叫挂起,在Linux内核叫SDT

至此,值得关注的进程状态全部讲解完成,下面来认识另一种进程(孤儿进程)


8、孤儿进程

我们针对上面的代码进行修改:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
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进程


9、进程优先级

基本概念

什么是优先级 vs 权限(是什么?)

  • 优先级是进程获取资源的先后顺序,好比如中午下课,一堆学生蜂拥食堂打饭,这就需要排队,假设你在队尾,虽然是队尾,但是你也能打到饭,只不过是先后的问题。
  • 权限是能和不能的问题,好比如你看爱奇艺电影,vip电影只有vip用户才能看,普通用户不能看,因为你没有这个权限。

为什么会存在优先级?

  • 就好比如我去医院挂号,这就是最典型的排队,而排队的本质叫做确认优先级,而排队的原因在于资源有限,你挂号来晚了就没你的号了
  • 系统里面永远是进程占大多数,而资源是少数,cpu只有一个,可是进程有n个,这就导致竞争资源是常态,所以一定要确认前后,因此必须存在优先级来确定谁先谁后,

总结:

  1. cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  2. 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
  3. 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

查看系统进程

在linux或者unix系统中,我们运行一个无限循环的process文件,并用ps –l命令则会类似输出以下几个内容:

ps -l

我们很容易注意到其中的几个重要信息,如下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值

PRI and NI

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


通过top命令修改进程优先级(ni)

修改进程优先级就是在修改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开始+。


四个重要概念

  1. 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  2. 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
  3. 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
  4. 并发:多个进程在一个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)
  • 当进程恢复的时候:需要将曾经保存的上下文数据恢复到寄存器中

相关文章:

  • Nacos介绍以及使用
  • 数字孪生在工业制造中的应用领域及技术体系构建
  • 9.4-学习ing
  • Docker 学习笔记总结(二)
  • 标准地图获取及使用
  • Kubernetes控制平面组件:Controller-Manager控制器管理
  • 数据备份必备知识与策略设计方法
  • Checkerboard Artifacts(棋盘伪影)的发生以及解决方案:
  • 【算法题解】拓扑序计数+树形DP
  • 优雅编码之——传统项目中,使用openfeign替换掉项目中的httpclient
  • 【ENVI精讲】处理专题五:基于像元二分模型的植被覆盖度反演
  • Docker 官方镜像Tomcat 无法访问 解决方案
  • Qt使用OpenCv
  • 【JavaEE进阶系列 | 从小白到工程师】泛型的详细介绍使用与类型通配符,直接上手
  • java毕业设计论坛管理系统mybatis+源码+调试部署+系统+数据库+lw
  • 《Java编程思想》读书笔记-对象导论
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • ES学习笔记(10)--ES6中的函数和数组补漏
  • LeetCode541. Reverse String II -- 按步长反转字符串
  • mongo索引构建
  • Puppeteer:浏览器控制器
  • Python学习之路13-记分
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • ViewService——一种保证客户端与服务端同步的方法
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • vue2.0项目引入element-ui
  • vue-cli在webpack的配置文件探究
  • 半理解系列--Promise的进化史
  • 从tcpdump抓包看TCP/IP协议
  • 订阅Forge Viewer所有的事件
  • 解决jsp引用其他项目时出现的 cannot be resolved to a type错误
  • 聊聊hikari连接池的leakDetectionThreshold
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 体验javascript之美-第五课 匿名函数自执行和闭包是一回事儿吗?
  • 验证码识别技术——15分钟带你突破各种复杂不定长验证码
  • 用Node EJS写一个爬虫脚本每天定时给心爱的她发一封暖心邮件
  • ​configparser --- 配置文件解析器​
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • $.ajax()参数及用法
  • (007)XHTML文档之标题——h1~h6
  • (03)光刻——半导体电路的绘制
  • (2)Java 简介
  • (c语言)strcpy函数用法
  • (PHP)设置修改 Apache 文件根目录 (Document Root)(转帖)
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (企业 / 公司项目)前端使用pingyin-pro将汉字转成拼音
  • (区间dp) (经典例题) 石子合并
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (一)RocketMQ初步认识