【探求Linux的奥秘】第五章:认识历程

[复制链接]
发表于 2025-8-31 11:57:46 | 显示全部楼层 |阅读模式



前言

   本专题将先容关于Linux操纵系统的种种,前几章我们先容了Linux下常用的一些根本开发工具,本章将讲解Linux中系统的概念——历程。(本章节默认利用的环境是centos 7.8)
  1. 冯·诺依曼体系结构

冯·诺依曼体系结构一种盘算机设计架构模子,至今是绝大多数盘算机系统的基础架构,例如我们现在的各类电脑、笔记本、服务器等大部分都遵守冯诺依曼体系。

截⾄⽬前,我们所认识的盘算机,都是由⼀个个的硬件组件构成:


  • 输入设备:例如键盘、鼠标、摄像头、磁盘等,从外界吸收数据和指令。
  • 中心处置处罚器(CPU):包括运算器和控制器,负责实验指令和处置处罚数据。
  • 存储器:用来存储步伐指令和数据,通常是随机存取存储器(RAM)。
  • 输出设备:例如表现器,打印机等,将盘算结果输出给用户或其他系统。
这里的存储器指的是内存,而我们寻常口头上所说的电脑和手机的内存指的是磁盘(硬盘驱动器(HDD)、固态硬盘(SSD)、光盘、U盘等长期性存储设备),我们称之为外存。而我们的步伐想要运行,第一步就是要先加载到内存中,也就是上图的存储器中。那么我们的步伐在加载到内存之前则以文件的形式存储在磁盘中。
那么为什么我们的软件想要运行需要加载到内存上呢?这是由冯诺依曼体系结构决定的:它规定命据的流动只能以上图中红色的箭头进行流动,我们知道软件的运行其实就是CPU去访问我们的数据,实验我们的代码,也就是说CPU只能从内存中获取对应的代码和数据。
那么我们就能知道其实数据的流动就是从一个设备"拷贝"到另一个设备,以是


  • 体系结构的服从就是由设备的"拷贝"服从决定
也就是说在数据层面,我们的CPU只和内存打交道,外设(输入设备和输出设备)只和内存打交道。
那么为什么不让CPU直接和外设打交道呢?反而要借助内存来当作中间值呢?


  • 最主要的缘故因由是要提高服从:
    CPU的处置处罚速度通常远高于外设。因此,假如CPU直接与外设进行数据交换,可能会造成CPU空闲等待,从而浪费处置处罚能力。通过存储器作为缓冲区,CPU可以在读取或写入外设数据时继续实验其他使命,提高了团体服从。
  • 同时也是为了简化设计:
    通过将数据和指令存储在统一的存储器中,可以避免复杂的硬件结构,使得盘算机的设计和实现变得更加简单和同等。
   那么可能又有人会问为什么不把磁盘的读取速度增加呢?这里简单的表明一下:使磁盘的速度增加同时还陪同的是其成本的增加:

  可以说今世的盘算机是性价比的产物。与之相关的则是芯片技术和摩尔定律:
  

  • 摩尔定律:集成电路上可容纳的晶体管数量约莫每两年会翻一番。
  • 芯片技术:更小的晶体管能在更高的频率下工作。
  两者结合也代表着盘算机的发展。因此由于盘算机硬件的快速迭代以及高速磁盘的成本造就了我们今世的盘算机结构。
  数据流动

对冯诺依曼体系结构的理解,不能停留在概念上,要深⼊到对软件数据流理解上,下面让我们用一个例子来深入相识一下:从你打开QQ,开始给朋友发送消息,到他得到消息之后的数据流动过程。假如是在qq上发送⽂件呢?
这个过程涉及到网络,不外我们先不关心,只是看数据的流动:我们和朋友可以看作是两台冯诺依曼体系结构,我们从键盘上输入消息,然后消息流入到内存,QQ这个软件从内存上获取消息,颠末加工将其放回内存,然后消息流入到输出设备,这里我们的消息其实是"拷贝"到了网卡上,然后通过网络流入到朋友的设备的网卡上,这里的网卡所代表的是输入设备,然后在颠末相同的操纵,消息流入到朋友的输出设备上,也就是表现器上,发送文件也是一样的原理,只是输入设备由键盘变为了磁盘,输出设备由表现器变为了磁盘。干讲其实是不太好理解的,下面我们通过一张图片来深入理解:

2. 操纵系统(Operating System)

2.1 概念

我们的Linux就是一款操纵系统,那么操纵系统是什么呢?
任何盘算机系统都包含⼀个根本的步伐集合,称为操纵系统(OS)。笼统的理解,操纵系统包括:


  • 内核(历程管理,内存管理,文件管理,驱动管理)
  • 其他步伐(例如函数库,shell步伐等)

操纵系统是管理盘算机硬件和软件资源的系统软件,是介于用户和硬件之间的管理步伐。它的主要使命是协调和控制盘算机的各种资源(如CPU、内存、磁盘、输入输出设备等),为用户和应用步伐提供一个方便、高效和安全的运行环境。
目前我们只需要知道操纵系统是一款对进行软硬件进行管理的软件即可。
2.2 设计OS的目的

前面我们说了OS是用来管理软硬件的软件,那么我们设计它的目的其实就是:


  • 对上,为应用步伐提供一个精良的实验环境(目的)

  • 对下,与硬件交互,管理全部的软硬件资源(本领)
在整个盘算机软硬件架构中,操纵系统的定位是:⼀款纯正的“搞管理”的软件。

  • 由上图我们可以知道盘算机的软硬件体系结构是层状结构。
  • 假如要访问操纵系统,就必须利用系统调用——其实就是函数,只不外是系统提供的函数。
  • 假如我们的步伐访问了硬件,那么它必须贯穿整个软硬件体系结构!
  • 我们利用的各类库很多都在底层封装了系统调用。
2.3 如何理解“管理”

我们该如何理解操纵系统所谓的管理呢?
我们以学校为例,在学校中有三个身份,分别是学生,辅导员,校长。在这里边学生是被管理者,校长是管理者,而导员是中间层。
我们做一件事情,总是先进行商议,也就是决定之后才会去实验。以是我们的校长也就是管理者对应着决定权,而导员也就是中间层对应着实验权。好比一个学生犯下了事情,那么通常是由导员上报给校长,校长颠末决定后假如决定开除该学生,那么将会是导员去通知该学生并帮其完成各种手续。
同时,导员也会收集每个同学的信息,进行汇总,然后交给校长,校长通过对这些数据的管理来管理学生。
   因此,我们可以看出,当管理者要管理被管理者时它们并不需要见面,因为管理者可以通过管理被管理者的“数据”来对被管理者进行管理,那么如何得到这些数据呢?由中间层进行获取。
  我们的操纵系统其实就是这个中间层
还是上面的例子,当校长要对学生的数据进行管理时,应该如何管理才能简单又方便呢?其实很简单,我们可以定义一个结构体,里面存放各类信息,我们可以通过差别的需求,将这些结构体以差别的形式链接起来,例如我们将其以链表的形式连接起来,那么校长管理学生的工作就可以转换为对链表的增删查改:

在这个过程中,校长管理学生的工作其实就是建模的过程,它的过程就是先组织,在描述


  • 描述起来,⽤struct结构体
  • 组织起来,⽤链表或其他⾼效的数据结构
凭借这六个字,我们便可以对任何“管理”场景进行建模!
现在我们还没有学习历程,那么操纵系统是怎么进行历程管理的呢?很简单,先把历程描述起来,再把历程组织起来!
2.4 系统调用和库函数概念

操纵系统要向上提供对应的服务,但是操纵系统不信赖任何用户(人),因此便有了系统调用,系统调用就是函数,用户在系统调用中通过输出参数给操纵系统,然后操纵系统给用户返回值,操纵系统和用户之间进行某种数据交互,这就叫做系统调用。
系统调⽤在使⽤上,功能⽐较基础,对⽤⼾的要求相对也⽐较⾼,以是,有⼼的开发者可以对部分系统调⽤进⾏适度封装,从⽽形成库,有了库,就很有利于更上层⽤⼾或者开发者进⾏⼆次开发。
也就是说库函数和系统调用其实是上下层的关系。
3. 历程

3.1 根本概念

   内核观点:继承分配系统资源(CPU时间,内存)的实体。
  我们运行的每一个步伐都是一个历程,在你的盘算机上,打开一个文本编辑器、浏览器或音乐播放器。每一个都是一个独立的历程,正在实验它们各自的使命。

有了前面的基础,我们知道操纵系统管理历程也是先对历程进行描述,再组织。那么操纵系统是如何去描述历程的呢?
历程信息被放在⼀个叫做**历程控制块(PCB,process control block)**的数据结构中,可以理解为历程属性的集合。 PCB是一个统称,在Linux下,PCB叫做task_struct。
task_struct是Linux内核的⼀种数据结构,它包含着历程的信息,是一个在Linux中描述历程的结构体。
上面我们说每一个运行的步伐都是一个历程,其实历程并不单单只有运行的步伐,在Linux中:
   历程 = 内核数据结构对象(task_struct) + 步伐的代码和数据
  那么下面让我们来认识一下task_struct:
task_struct中存放的都是历程的各种属性,例如:


  • 标识符:描述本历程的唯⼀标⽰符,⽤来区别其他历程
  • 状态: 使命状态,退出代码,退出信号等
  • 优先级: 相对于其他历程的优先级
  • 步伐计数器: 步伐中即将被执⾏的下⼀条指令的地点
  • 内存指针: 包括步伐代码和历程相关数据的指针,另有和其他历程共享的内存块的指针
  • 上下⽂数据: 历程执⾏时处置处罚器的寄存器中的数据
  • I∕O状态信息: 包括显⽰的I/O请求,分配给历程的I∕O设备和被历程使⽤的⽂件列表
  • 等等…
历程的全部属性我们都可以直接或间接在相应的task_struct中获取。
在Linux的内核源码中我们可以找到task_struct的定义:

而且每一个task_struct都是由双向链表来组织起来的,在内核源代码里我们也可以看到:

全部运⾏在系统⾥的历程都以task_struct链表的形式存在内核⾥。
我们编写一个步伐,在没有运行前都是存放在磁盘中的二进制文件,由冯诺依曼体系结构我们知道,我们假如想要实验这个步伐,那么我们的可实验步伐就会被加载到内存上,此时在加载内存上的是我们可实验步伐的代码和数据,我们知道操纵系统也是一个软件,以是在我们启动电脑加载等待的那一段时间里,操纵系统就被加载到了内存。操纵系统必然要对多个被加载到内存的步伐进行管理,但是对于加载到内存中的代码和数据操纵系统并不认识,也没有办法去对它们进行调度等操纵,因此,就有了PCB,也就是,操纵系统会给每一个加载到内存的代码和数据分配一个task_struct,它里面存储了各类属性,使得操纵系统可以对相应的代码和数据进行正确的操纵,而对于一个task_struct结构体和它对应的代码和数据,我们称之为历程。而历程是以链表的形式链接在一起的,以是操纵系统对历程的管理,就酿成了对链表的增删查改。

操纵系统管理历程的方式也再一次体系了先描述,后组织的思想。
3.1.1 查察历程

我们汗青上实验的全部的指令、工具、自己编写的步伐等运行起来都是历程!每一个历程都有属于自己的pid,相当于历程的名字,不会有相同的pid同时存在,pid作为历程的一种属性,它存在于历程的task_struct中,那我们能不能去查察某一个历程的pid呢?答案是可以的:
在Linux中提供了一个名为getpid()的系统调用,它可以资助我们把历程的pid从task_struct中拷贝出来,它的返回值pid_t是系统提供的一种数据范例,由于Linux的内核是用C语言写的,以是这里的pid_t其实就是int整数,也就是我们当前历程的pid。下面让我们运行一个步伐,看看它成为历程后的pid:

该步伐是一个死循环,这样方便我们一会去对该历程的pid进行验证。代码也很简单,每过一秒打印一次该历程的pid,运行该步伐:

从运行结果我们可以看到该历程的pid是7095,在Linux中⼤多数历程信息可以使⽤top和ps这些⽤⼾级⼯具来获取 :


  • ps axj:
    对于ps的选项这里我们不外多表明,各人只需要知道我们可以通过该指令来查察历程的信息,不外该命令只能查察我们实验该命令时的历程:

    我们可以看到历程之多,很难去找到目标历程,因此我们可以通过行文本过滤器grep来快速找到我们的目标历程:

    我们先来分析一下我们输入的命令:
    1. ps axj | head -1 && ps axj | grep proc
    复制代码
    &&是用于连接我们要实验的两条命令,ps axj | head -1是表现ps axj的第一行,也就是历程的属性名,ps axj | grep proc是表现我们运行的历程,因为我们的步伐名是proc。同时我们也可以用分号;来取代&&。
    可以看到我们的pid与我们查到的pid是相同的:

    那么上图中第二个历程是什么呢?其着实上面的命令中的末了我们用grep来帮我们过滤信息,我们知道其实我们的每一个命令实验时都是一个历程,以是在我们用ps axj查察历程时grep也在运行,以是这第二个历程就是grep。
  • top:利用top命令时,可以查察当前系统中正在运行的历程及其资源利用情况,它是及时变化的,需要我们通过q来退出。同时利用top命令后我们可以改变历程的优先级以及杀死一个历程,这些我们到后面再说,下面先让我们来看一看top的效果:

那么像我们上面写的死循环步伐我们在运行时该怎么退出呢?也就是说我们该如何竣事或者说杀死正在运行的历程呢?目前有两种简单的办法:


  • ctrl+c:之前我们在利用某些命令卡住时我们都会利用ctrl+c来退出,其实ctrl+c的作用就是杀死当前历程:

  • kill -9 [pid]:此外,我们还可以通过命令kill -9 [pid]来杀死某一个历程,这里涉及到信号的概念,我们不外多表明,只需要知道该命令即可:

在Linux当中我们也可以ls命令查察目录结构去查察历程,proc也就是process(历程)的简写,也就是说我们可以通过文件的方式去查察历程。操纵系统不仅仅可以把磁盘上的文件让我们用ls这样的命令让我们查到,它把内存的相关数据也以文件的方式呈现出来,让我们可以动态看到内存相关的数据,/porc目录就是内存级别的文件系统,例如下图中以数字为名称的一个个目录就是一个个历程,这些数字就是每一个历程的pid:

   这也符合Linux上齐备皆文件的概念
  例如我们在启动一个历程,我们可以在该目录中查到对应pid的目录:

而当我们杀掉该历程后,再在/proc目录中探求就找不到对应的目录:

也就是说当我们的历程退出后系统便会主动打扫其所对的目录,而一个新历程出现时,系统也会主动创建相应的目录。
   这里我们增补两个小知识点:
这些历程目录中存放着历程的各种信息,此中我们需要注意两个:cwd、exe,固然它们涉及到了软硬链接,不外我们先不思量,只看它们的作用:
  

  这里我们不卖关子,直接先容:
  

  • cwd:它的值是我们步伐所处的目录,它的作用是当我们在进行文件操纵时,假如要打开一些文件时我们没有输入绝对路径,而是输入的相对路径,那么它的相对路径就是相对于cwd的,例如我们要以只写的方式打开文件a.log:
    1. fopen("a.log", "w");
    复制代码
    可以看到这里我们给的是相对路径,也就是cwd所代表的路径下,假如该路径下没有文件a.log,那么就会在该路径下创建该文件。也就是历程会记载下来自己的当前文件。
  • exe:历程对应的可实验文件。
  Linux中的全部历程都是由其父历程创建的,一个父历程可以创建多个子历程,以是Linux中全部历程的结构也是多叉树结构。上面我们可以看到getppid()也是一个系统调用,它的作用是得到父历程的pid,

运行该步伐,进行观察:

我们可以发现在我们不断启动杀死历程,每一次实验该步伐的历程的pid都差别,这是因为每一次我们启动历程的时间都是向系统里重新加载,以是每一次都差别。但是我们可以发现每一次实验我们的父历程是稳定的,那么这个父历程是谁呢?我们可以去查一查:

可以看到我们查出来的历程是bash,那么bash是什么呢?前面我们讲过,bash就是我们当前利用的命令行表明器,以是命令行表明器的本质也是一个历程。
   os会给每一个登录用户分配一个bash(前面带-体现长途登录)
  当前我们有两个登录,以是应该有两个bash,下面让我们来验证一下:

以是命令行表明器就是一个历程,它先打印对应的字符串,然后等待我们输入命令,等我们输入完成后再去进行相应的操纵,跟我们在C语言利用scanf是一样的。以是我们在命令行上实验的全部命令的历程的父历程都是bash。
3.1.2 创建历程

那么bash是如何创建历程的呢?也就是说我们该如何去创建一个子历程呢?我们利用代码创建子历程的方式也是利用一个系统调用——fork:

这里我们需要先知道fork有两个返回值:

可以看到当我们利用fork成功创建子历程后,它会给父历程返回子历程的pid,给子历程返回0,那么一个函数为什么会有两个返回值呢?这是因为当我们的父历程创建子历程时,操纵系统会给子历程分配一个task_struct,但是由于没有步伐新的加载,以是子历程没有自己的代码和数据,因此它和父历程利用同一份代码和数据。
因此我们可以让父子实验差别的逻辑:


信赖到这里各人有很多题目:


  • 为什么fork给父子返回差别的返回值?
  • 为什么一个函数会返回两次?
  • 为什么一个变量既即是0,又大于0,导致if else语句同时创建?
下面让我们一个个的来解决这些题目,要解决这些题目,我们需要先简单相识一下fork():
在调用 fork() 时,操纵系统会创建一个新的历程(子历程),并将父历程(当前历程)的实验上下文(包括寄存器、内存和步伐计数器等)复制到新的子历程中。这意味着在子历程中,最紧张的一个点是:
   fork() 调用成功后,子历程会从 fork() 调用的下一行代码开始实验,这时,父历程和子历程的 pid(fork() 的返回值)的值是差别的。
  

  • 父历程调用 fork():
    假如 fork() 成功,父历程的 pid 变量会被设置为新子历程的历程ID(一个正整数)。
  • 子历程从 fork() 调用的下一行开始实验:
    在这个情况下,在子历程中,pid 的值会是 0。因此,可以通过检查 pid 的值来区分父历程和子历程。
通过这种设计,父历程可以知道它的子历程的PID,而子历程可以知道自己是新创建的历程。从而使它们可以进行独立的操纵,同时也确保父历程可以追踪和管理它所创建的子历程。这也就是fork()会给父子进行返回差别的返回值的缘故因由,
因为子历程是被我们创建的,以是它没有独立的代码和数据,以是子历程会默认复制父历程的代码和数据。由于历程是通过PCB中的指针去指向内存中对应的代码和数据,以是这里我们可以看成是子历程进行了浅拷贝,指向和父历程相同的代码和数据,只是当我们的子历程或者父历程假如想要修改此中的某项值时,会发生写时拷贝,操纵系统会在内存中重新开辟一块空间,把修改的数据拷贝一份,让目标历程去修改这个拷贝。这里边详细的细节我们后面再进行讲解。下面让我们用一张图来简单的理解一下:

以是我们可以得出一个结论:历程具有独立性
因此一个变量既可以即是0,也可以大于0,其实这只是我们肉眼看到的好像是一个变量既可以即是0,也可以大于0,实际上这是父子历程两个历程在并发实验,固然父子历程共享同一份代码,但是它们各自运行自己的,不会相互影响。
举个例子,就像两个人写同一篇作文时,固然两个人写的是同一个题材,但他们有各自的思路与想法,因此写出来的作文固然题材相同,但是内容差别,这里作文题材就是我们的代码,两个人就是父子两个历程。

那么一个函数为什么会返回两次呢?其实也很简单,在我们调用fork()函数时,在它实验完之前我们的子历程就已经被创建出来了,也就是说在fork()函数return之前子历程就已经存在了,以是父子历程各有一个返回值。

以上就是我们对历程的一个开端的认识,接下来让我们再来深入的相识历程。
3.2 历程状态

3.2.1 简单先容

一个CPU在同一时刻只能实验一个历程,但是在内存中却有着很多历程存在,这时有的历程正在CPU中被实验,而优点CPU还在内存中等待,以是历程状态指的是操纵系统中某个历程在某一时刻所处的运行状态。差别的状态反映了历程当前的运动情况以及它与CPU和资源的关系。下图是操纵系统中通用的体现历程状态的图,我们相识一下即可,接下来将结合Linux中详细的历程状态进行讲解,可以与该图进行一下对比:

3.2.2 运行&&壅闭&&挂起



  • 运行:每一个CPU都会维护一个运行队列(runqueue),它里面存放的是历程的PCB(task_struct)。在今世大部分的盘算机中,正在运行或者已经在运行队列中等待运行的历程都处于运行状态
  • 壅闭:当我们在利用scanf或者cin时,其实不是等待用户输入,而是在等待键盘硬件就绪,也就是等待键盘上有按键被按下了,当我们没有按下键盘时,称为键盘文件不就绪,这时scanf这个历程就没有办法读到数据,它就需要等待。以是壅闭状态就是等待某种设备或资源就绪
           OS要管理系统中的各种硬件资源也是通过先描述,在组织的方法进行管理,以是对于这些硬件也都有一个结构体对其进行描述,这些结构也是以链表的形式在操纵系统中,以是操纵系统对硬件的管理就酿成了对该链表的管理,与OS对历程的管理是一样的。
        因此在操纵系统中不仅有运行队列,另有设备队列。
        在我们每一个设备的结构体中都有一个范例为struct tast struct*的等待队列,当我们CPU在实验某一个历程时,假如要实验scanf从键盘读数据,但是OS查询键盘发现其处于不活泼状态,也就是没有按键被按下,那么该历程的PCB就会被从CPU中拿出来,将其链接到键盘对应的结构体中的等待队列中,此时该历程就处于壅闭状态。
        一旦键盘上有按键被按下,操纵系统便会从查察对应的等待队列,将处于队首的PCB重新链入运行队列中。
  • 挂起:我们的内存的大小是有限的,当我们的内存空间严肃不敷时,有一些处于壅闭状态或者其他一些暂时不会被实验的历程的代码和数据便会被唤出到磁盘中特定的空间(swap交换分区),这些历程的状态就处于挂起状态。当这些历程需要实验时OS便会从磁盘中将其对应的代码和数据重新唤入到内存中。
历程状态的变化的体现形式之一就是要在差别的队列中流动,本质都是数据结构的增删查改。
3.2.3 理解内核链表

在Linux中的双向链表与我们之前定义的双向链表差别,我们定义的双向链表的每一个节点中包含了数据以及前后指针,而在Linux内核中的双向链表的结点中只有前后指针:

以是在我们的task_struct结构体中就会存放该链表的指针,只不外这时我们链表的结点中的next不再指向下一个task_struct的开头,而是下一个task_struct中的next:

以是我们就可以通过这个链表去遍历每一个task_struct,但是我们通过链表去遍历我们只能得到该链表中task_struct的链表节点的地点,但是我们假如要遍历task_struct肯定是要得到它的各种属性,那么我们该如何通过链表的节点去找到整个task_struct的数据呢?其实很简单,我们知道,我们结构体的地点其实就是它第一个成员的地点,而且成员地点是依次变大的,以是我们就可以通过
  1. next - &((struct task_struct*)0->links)
复制代码
得到我们task_struct中起始地点到links成员的偏移量(C语言中有专门的宏offset去求偏移量),以是只要我们知道当前task_struct的links(也就是next)的地点减去偏移量,就可以得到task_struct的起始地点了,这样我们就可以随意的访问task_struct中的每一个成员了。
以是我们可以使一个task_struct既属于运行队列,也属于全局链表。可以存在于多种数据结构中

   其着实内核数据结构中很多都是网状的!
  3.2.4 Linux的历程状态

⼀个历程可以有⼏个状态(在Linux内核⾥,历程有时间也叫做使命)。历程状态就是task_struct内的一个整数:

在Linux内核软代码里这样定义历程状态:
  1. /*
  2. *The task state array is a strange "bitmap" of
  3. *reasons to sleep. Thus "running" is zero, and
  4. *you can test for combinations of others with
  5. *simple bit tests.
  6. */
  7. static const char *const task_state_array[] = {
  8.         "R (running)", /*0 */
  9.         "S (sleeping)", /*1 */
  10.         "D (disk sleep)", /*2 */
  11.         "T (stopped)", /*4 */
  12.         "t (tracing stop)", /*8 */
  13.         "X (dead)", /*16 */
  14.         "Z (zombie)", /*32 */
  15. }
复制代码
  我们也可以利用ps axj命令去查察历程的状态,这里我们简单先容一些ps的选项:
  

  • a:显⽰⼀个终端全部的历程,包括其他⽤⼾的历程。
  • x:显⽰没有控制终端的历程,例如后台运⾏的守卫历程。
  • j:显⽰历程归属的历程组ID、会话ID、⽗历程ID,以及与作业控制相关的信息
  • u:以⽤⼾为中⼼的格式显⽰历程信息,提供历程的详细信息,如⽤⼾、CPU和内存使⽤情况等
  我们先来认识一下这些状态:


  • R 运⾏状态(running): 并不意味着历程⼀定在运⾏中,它表明历程要么是在运⾏中要么在运⾏队列⾥。
    我们实验一个步伐
  • S 睡眠状态(sleeping): 历程在等待某个事件(如IO操纵完成),可以被信号停止。也就是壅闭状态。也叫做可停止睡眠(interruptible sleep))。
  • D 磁盘休眠状态(Disk sleep):历程等待IO等不可停止资源,不能被信号打断(通常用于等待硬件)。也叫不可停止睡眠状态(uninterruptible sleep)。假如我们的历程要往磁盘中写入数据时,那么该历程就会处于D状态,这是为了避免在写入时突然被停止导致的数据丢失。
  • T 停⽌状态(stopped): 历程被暂停,通常是因为吸收到停止信号(如SIGSTOP、SIGTSTP)。T状态是由系统决定的。t (tracing stop)经常在我们调试步伐时打断点使步伐实验一部分代码后停在指定的代码时,该历程就处于t停止状态,是由我们用户自己决定的。
  • X 死亡状态(dead):这个状态只是⼀个返回状态,我们不会在使命列表⾥看到这个状态。
  • Z 僵尸状态(zombie):下面详细先容。
其实我们利用的kill命令就是给指定的历程相应的信号,同时我们的ctrl+c和ctrl+z这些快捷键其实也是信号。关于信号的详细内容我们后面会进行详细讲解。

3.2.5 僵⼫历程 (zombie)

僵尸状态(Zombies)是⼀个⽐较特别的状态。它是当子历程退出而且⽗历程没有读取到⼦历程退出的返回代码时就会产⽣(至于父历程如何得到子历程退出时的返回代码后面会将)。
存在该状态的缘故因由:


  • 对于父历程来说,它不会无缘无故的创建一个子历程,当子历程被父历程创建后,那么它肯定要完成某种事情,前面我们说过历程之间具有独立性,那么父历程该怎么知道子历程是否完成了相应的使命呢?以是便有了僵尸历程,当子历程完成退出后,便会处于僵尸状态,等待父历程去读取。假如父历程不进行读取,那么子历程就会不停处于Z状态。
历程的状态以及父历程需要读取子历程的信息都属于子历程的各类属性,以是它们都生存在task_struct中, 也就是说假如历程不停处于Z状态,那么我们就要不停维护 task_struct。
我们可以通过下面的代码进行验证:
  1. #include<stdio.h>
  2. #include<unistd.h>
  3. #include<sys/types.h>
  4. int main()
  5. {
  6.      int id = fork();
  7.      if(id == 0)
  8.      {
  9.          printf("我是子进程,我的id=%d, 我的父进程id=%d\n", getpid(), getppid());
  10.      }      
  11.      else   
  12.      {
  13.       
  14.          while(1)
  15.          {
  16.              ;
  17.          }
  18.      
  19.      }
  20.      return 0;
  21. }  
复制代码
我们让子历程打印一行字符串后就竣事,而父历程不停运行,让我们来看看子历程实验竣事后的状态:

可以看到当我们的子历程竣事后没有被父历程接纳时,子历程就会处于僵尸状态。
那么当一个父历程创建了很多子历程,但就是不接纳,就会造成内存资源的浪费,因为数据结构对象自己就要占用内存。当父历程不停不接纳子历程,那么就会产生内存泄漏的题目。
   当我们历程退出之后,那么在历程实验时在堆上动态开辟的空间假如没有释放,那么系统会主动接纳,以是当历程退出后就不会存在内存泄漏的题目。那么什么样的历程才存在内存泄漏的题目呢?其实很简单,就是那些不容易退出的历程,也就是常驻内存的历程是最怕内存泄漏题目的。操纵系统就是一个常驻内存的历程,我们的操纵系统也是一个软件,以是它也是一个历程,而且OS伴随着我们从开机不停到关机。
  因此当子历程进入僵尸状态后,父历程一定要接纳子历程(详细做法后面讲)。
3.2.6 孤儿历程

上面是子历程先退出会有僵尸历程,那么假如是父历程先退出呢?
在操纵系统中,某个历程的父历程已经竣事,而该历程仍然在运行的状态,那么该历程就叫做孤儿历程。此时孤儿历程会被操纵系统的init历程(PID为1)收养。init会定期检查这些孤儿历程并将其变为其子历程,init历程成为它们的父历程。
为什么要有历程去领养孤儿历程呢?这是因为假如一个历程处于孤儿历程后,那么当该历程竣事后便会处于僵尸状态,而且没有父历程能进行接纳,因为它的父历程已经竣事,会造成资源泄漏乃至内存泄漏。为了避免这种情况,操纵系统便会给孤儿历程重新找一个父历程,确保孤儿历程可以大概被有用管理和清算。
下面让我们通过一段代码演示一下:
  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.         perror("fork");
  9.         return 1;
  10.     }
  11.     else if(id == 0){
  12.         //child
  13.         while(1)
  14.         {
  15.             ;
  16.         }
  17.     }
  18.     else{
  19.         //parent
  20.         while(1)
  21.         {
  22.             ;
  23.         }
  24.     }
  25.    
  26.     return 0;
  27. }
复制代码
让我们来看看结果,这是步伐刚开始实验时,父子历程都在运行:

此时我们杀掉父历程:

可以看到此时父历程已经竣事,子历程的父历程pid变为1,也就是说子历程被init历程领养,此时固然子历程表现的状态还是运行状态,但是我们可以看到在终端上并没有表现,这是因为此时子历程在后台运行。
一个历程酿成孤儿历程后会酿成后台历程,也就是在后台运行。在Linux中,假如你想要一个历程在后台运行,那么只需要在实验该命令时在后面加上&即可,后台历程运行的体现形式是在历程状态后没有+号,前台历程的状态后有+号。

3.3 历程优先级

3.3.1 简单先容

历程优先级是历程得到CPU资源的先后次序,就是指历程的优先权(priority) 。历程优先级越高,操纵系统越可能在调度中选择它,从而使其优先获得CPU的实验时机。
那么为什么历程要有优先级呢?这是因为在单核处置处罚器中(也就是只有一个CPU),同一时刻只能实验一个历程,因此我们需要通过优先级来确认哪个历程被先实验。


  • 通过将高优先级的历程首先调度,使得CPU可以优先实验那些更紧张或更需要快速响应的使命,从而提高系统的团体服从。
  • 通过得当的优先级调度算法,操纵系统可以大概在多个历程之间公平分配资源,避免某些低优先级历程的自我壅闭。
   这里我们要区分一下优先级和权限:
  

  • 优先级是某些历程一定可以大概能到资源,只是谁先谁后的题目
  • 权限则是是否能得到资源
  历程的优先级也是历程的属性,存放在task_struct中,是一个整数。这个整数的值越低,体现优先级越高,反之优先级越低。基于时间片的分时操纵系统,思量到公平性,优先级的变化幅度不能太大。
3.3.2 查察优先级

在Linux中我们可以通过命令ps -al来查察我们历程的优先级:

这里的PRI就是我们历程的优先级,该值越小历程的优先级越高。NI是nice值,体现历程优先级的修正数值。
历程真实的优先级(也就是上图中表现的PRI)= PRI的默认值(80)+NI
因此在Linux中,我们通过修改NI的值来改变历程的优先级。我们可以通过top工具来更改NI值:

   进⼊top后按“r”‒>输⼊历程PID‒>输⼊nice值
  更改后再让我们来查察一下历程的优先级:

可以发现我们将NI值改为10后,历程的优先级变为80(PRI的默认值)+10(NI),那么我们再将改历程的NI值改为5看一看结果:

可以看到,当我们修改NI值为5后,PRI的值变为85,这是因为历程的优先级只即是PRI(默认)+ NI,跟之前的优先级无关。
3.3.3 修改优先级

我们除了可以通过top命令来修改历程的优先级,还可以通过nice和renice两个命令来进行:


  • nice命令用于以特定的优先级启动一个新的历程(通过改变NI值)。默认情况下,会将NI值变为10,使优先级增加从而让其他紧张历程获得更多的CPU时间。
    语法结构:
    1. nice -n num <command>
    复制代码
    演示:

  • renice命令用于动态调整已经运行中的历程的优先级。这允许用户在历程实验时根据需要改变其优先级。
    语法结构:
    1. renice num -p pid
    复制代码
    演示:

可以看到,这些命令都是通过修改NI的值来改变历程的优先级。以是,调整历程优先级,在Linux下,就是调整历程nice(NI)值
除此之外,我们还可以在代码中利用一些系统调用修改优先级:

相识一下即可。
3.3.4 优先级的极值

Linux历程的优先级是存在极值的,范围是[60,99],一共40个优先级。以是在我们更改NI值时NI值也存在极值,它的范围是[-20,19],当我们修改NI时假如输入的值凌驾了这个范围,那么NI便会被更改为离范围内迩来的值:
例如我们把NI值改为100:

把NI值改为-100:

在Linux中之以是要为优先级设定范围,是为了防止优先级设定的不公道,导致优先级很低的历程长时间得不到CPU资源,进而导致历程饥饿
   竞争vs独立 && 并行vs并发
  

  • 竞争性:系统历程数⽬众多,⽽CPU资源只有少量,甚⾄1个,以是历程之间是具有竞争属性的。为了⾼效完成使命,更公道竞争相关资源,便具有了优先级
  • 独⽴性: 多历程运⾏,需要独享各种资源,多历程运⾏期间互不⼲扰
  • 并⾏: 多个历程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
  • 并发: 多个历程在⼀个CPU下采⽤历程切换的⽅式,在⼀段时间之内,让多个历程都得以推进,称之为并发。并发涉及到很短的时间片切换,这些时间片的单位来到了微秒级乃至纳秒级,使得多个历程看起来在同时运行。
  3.4 历程切换

我们先来相识一下时间片的概念:


  • 时间⽚:今世盘算机都是分时操纵系统,每个历程都有它合适的时间⽚(其实就是⼀个计数器)。时间⽚到达,历程就被操纵系统从CPU中剥离下来。
时间片是为历程分配的最大实验时间长度,通常以毫秒为单位。当一个历程利用完当时间片后,系统会强制进行历程切换,将CPU控制权转移到下一个准备好的历程。在这种策略中,CPU为每个历程分配一个固定长度的时间段,以确保多个历程可以大概公平地共享CPU资源。
以是当一个历程占据CPU,并不会一次性就把自己的代码实验完(不思量极度情况),当它的时间片到了之后,它就要为其他历程让地方了。那么当这个历程下一次占据CPU后如何恢复自己之前的数据呢?这与寄存器有关。
寄存器就是CPU内部的临时空间。在CPU中存在着非常多的寄存器,用于存储CPU在实验指令时所需的临时数据和指令。以是当我们的历程在需要切换时,历程会生存当前历程的上下文数据,也就是CPU内寄存器中的各种数据,包括当前代码运行到的位置以及各种临时数据,这些都被生存到历程的task_struct中的TSS结构体中(感兴趣的可以查察Linux源码),当历程再次占用CPU时,就会通过该结构体中的数据覆盖寄存器的内容。

以是历程切换最焦点的就是生存和恢复当前历程的硬件上下文的数据,也就是CPU中寄存器的内容。
3.5 Linux2.6内核历程O(1)调度算法

在Linux 2.6内核中,O(1)调度算法是为了提高历程调度的服从而设计的。该调度算法的主要特点是可以大概在常数时间内(O(1))做出调度决定,无论系统中有多少历程。这种设计使得Linux内核在多使命环境下可以大概有用地管理并调度大量的历程。
一个CPU有一个运行队列(runqueue)。

上图是Linux2.6内核中历程运行队列的数据结构,
下面我们先来看这里面的queue[140],它其实是是一个指针数组,范例是struct task_struct* 。其着实Linux中一共是有140个优先级,只是前100个,也就是[0,99]是及时优先级,只是我们并不思量这一部分,因为它涉及到的是及时操纵系统,而我们所处的互联网领域上利用的都是分时操纵系统。
   及时操纵系统:
  及时操纵系统广泛应用于需要高度可靠性和及时响应的领域,如工业主动化、航空航天、医疗设备、汽车电子、通讯系统、消耗电子等。
  及时操纵系统是针对需要即时或准时反应的系统设计的,具备严酷的时间束缚,以确保关键使命的及时完成。通过专门的功能和优化,RTOS可以大概有用地管理资源,以满足各类及时应用的需求。
  以是我们只关心[100,139]这四十个优先级,与我们前面说的优先级范围是同等的。以是我们便可以用我们的优先级映射到queue[140]的下标中。在这个指针数组中每一个指针的范例都是task_struct*,以是我们就可以把优先级相同的历程链入到相应下标的队列中。这样我们在调度历程时便可以顺着queue[140]去探求依次调度历程,在局部上也就是优先级相同的历程我们采用FIFO(先进先出)进行列队调度。
   它的本质是一个开散列的哈希算法。
  这样进行调度还需要遍历queue[140],服从还是不高,那么调度器该如何快速地挑选一个历程呢?这就要靠我们的另一个成员bitmap[5]了,也就是位图。它的范例是unsigned int,就是无符号整数,一个无符号整数对应32个比特位,那么5个就对应了160个比特位,它刚好对应了我们queue[140]中的140个下标一一对应,多出来的不管,这是无法避免的消耗。以是每一个比特位的内容就代表了queue[140]中对应的队列是否为空,0体现为空,1体现不为空。因此,我们便可以使遍历queue[140]的操纵转换为查找位图的操纵,大大提高了服从。
而nr_active体现的是整个队列当中一共有多少个历程,以是在我们调度历程时先查nr_active,它大于0再查位图,找到第一个不为0的比特位对应的下标,然后在queue[140]中取对应的队列的队首。
我们观察上图中可以发现有两个就绪队列:也就是*active和*expired指向的两块内容。这两个队列的成员是一样的,这是为什么呢?举一个简单的例子:现在我们有一个优先级为60的历程和一个优先级为99的历程,而且优先级为60的历程是一个死循环。当60历程被调度,时间片竣事后,因为还要在被调度,以是又把它链回到运行队列里,那么下一次调度器再次调度时由于它的优先级高,以是还回调度这个60历程,这样我们的90历程显然就不停不会被调度,就会导致历程饥饿,这显然不是我们想看到的。
以是在运行队列中存在两个就绪队列,一个叫做active,一个叫做expired,当一个历程从cpu中被切换出来后,我们把它链入到expired队列中,然后直到active队列中的历程都被调度事后,也就是active队列中的历程为空了,此时我们交换active和expired两个指针的内容,使expired成为新的active。

一个新的历程被链入到运行队列中时在没有其他因素干扰下是将其链入到此中的expired中。
简单总结一下:


  • 过期队列和运动队列结构⼀模⼀样。
  • 过期队列上放置的历程,都是时间⽚耗尽的历程。
  • active指针永远指向运动队列。
  • expired指针永远指向过期队列。
  • 运动队列上的历程会越来越少,过期队列上的历程会越来越多,当运动队列中没有历程时,交换active指针和expired指针的内容,就相当于有具有了⼀批新的运动历程!

在系统当中查找⼀个最合适调度的历程的时间复杂度是⼀个常数,不随着历程增多⽽导致时间成本增加,我们称之为历程调度O(1)算法!
尾声

   本章讲解就到此竣事了,若有纰漏或不敷之处欢迎各人在批评区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表