《现代操作系统》第10章——实例研究1:UNIX、Linux和Android

news/2024/9/19 17:14:31/文章来源:https://www.cnblogs.com/tianshihao/p/18233342

《现代操作系统》第 10 章——实例研究 1:UNIX、Linux 和 Android

10.1 UNIX 与 Linux 的历史

第一次使 UNIX 的两种流派一致的严肃尝试来源于 IEEE(它是一个得到高度尊重的中立组织)标准委员会的赞助。有上百名来自业界、学界以及政府的人员参加了此项工作。他们共同决定将这个项目命名为 POSIX。前三个字母代表可移植操作系统(Portable Operating System),后缀 IX 用来使这个名字与 UNIX 的构词相似。

10.2 Linux 简介

10.2.1 Linux 的设计目标

10.2.2 到 Linux 的接口

Linux系统中的层次结构

程序通过把参数放入寄存器(有时是栈)来调用系统调用,并发出陷入指令从用户模式切换到内核模式。由于不能用 C 语言写一条陷入指令,因此系统提供了一个库,每个函数对应一个系统调用。这些函数使用汇编语言写的,不过可以从 C 中调用。每一个函数首先将参数放到合适的地方,然后执行陷阱命令。因此,为了执行 read 系统调用,一个 C 程序需要调用 read 库函数。

10.2.3 shell

shell 命令行界面使用起来更快速,功能更强大,扩展性更好,并且让用户不会遭受由于必须一直使用鼠标而引起的肢体重复性劳损(RSI)。

10.2.4 Linux 应用程序

10.2.5 内核结构

内核坐落在硬件之上,负责实现与 I/O 设备和存储管理单元的交互。暴露给上层应用的是系统调用,这些系统调用是内核的接口。最底层是中断处理程序,它们是与设备交互的最主要的方式。在两者中间的部分是内核的子系统,可以分为三个部分:I/O 部件、内存管理部件和进程管理部件。

I/O 部件包含所有负责与设备交互以及实现联网和存储 I/O 功能的内核部件。在最高层,这些 I/O 功能全部整合在一个虚拟文件系统层中。也就是说,从顶层来看,对一个文件进行读操作,不论是在内存还是磁盘中,都和从终端输入中读取一个字符是一样的。从底层看,所有的 I/O 操作都要通过某一个设备驱动器来完成。所有的 Linux 驱动程序又可以分为字符驱动程序和块驱动程序,两者的区别是,块设备允许查找和随机访问,而字符设备不允许。从技术上讲,网络设备实际上是一种字符设备,不过它们的处理和其它字符设备不太一样,因此将网络设备单独分类。

内存管理部件负责维护虚拟内存到物理内存的映射,维护最近被访问页面的缓存以及实现一个好的页面置换算法,并且根据需要把需要的数据和代码页读入到内存中。

进程管理部件最主要的任务是负责进程的创建和终止。它还包括一个进程调度器,负责选择下一个运行哪个进程或者线程。信号管理程序也属于进程管理部件。

10.3 Linux 中的进程

10.3.1 基本概念

每一个进程都有一个独立的程序计数器,用这个计数器可以追踪下一条将要被执行的指令。

系统启动的时候 shell 脚本会开启守护进程(daemon)。计划任务(cron daemon)是一个典型的守护进程。它每分钟运行一次来检查是否有工作需要它完成。

Linux 中的进程可以通过一种消息传递的方式进行通信。在两个进程之间可以建立一个管道,一个进程向这个通道中写入字节流,另一个进程从这个通道中读取字节流。这些通道就被称为管道(pipe)。管道是同步的,因为如果一个进程试图从一个空的管道中读取数据,这个进程就会被挂起直到管道中有可用的数据为止。

进程还可以通过另一种方式通信:软中断。一个进程可以给另一个进程发信号(signal)。进程可以告诉操作系统当信号到来时它们希望发生什么事情。如果一个进程希望获取所有发送给它的信号,它就必须指定一个信号处理函数。当信号到达时,控制立即切换到信号处理函数。当信号处理函数结束并返回之后,控制像硬件 I/O 中断一样返回到陷入点处。

10.3.2 Linux 中进程管理相关的系统调用

*10.3.3 Linux 中进程与线程的实现

每一个进程都有一个运行在用户程序的用户模式。但是当它的某一个线程调用系统调用之后,进程会陷入内核模式并且运行在内核上下文中,它将使用不同的内存映射并且拥有对所有机器资源的访问权。它还是同一个线程,但是现在拥有更高的权限,同时拥有自己的内核堆栈以及内核程序计数器。

在 Linux 系统内核中,进程通过数据结构task_struct被表示成任务(task),Linux 系统用任务的数据结构来表示所有的执行上下文。所以,一个单线程的只有一个任何数据结构,而一个多线程的进程将为每一个用户级线程分配一个任务数据结构。最后,Linux 的内核是多线程的,并且它所拥有的是与任何用户进程无关的内核级线程,这些内核级线程执行内核代码。

对于每一个进程,一个类型为task_struct的进程描述符是始终存在与内存当中的。它包含了全部进程所需的重要信息,如调度参数、已打开的文件描述符列表等。进程描述符从进程被创建开始就一直存在于内核堆栈之中。

Linux 通过进程标识符(PID)来区分进程。内核将所有进程的任务数据结构组织成一个双向链表。不需要遍历这个链表来访问进程描述符,PID 可以直接被映射成进程的任务数据结构所在的地址,从而立即访问进程的信息。

任务数据结构包含非常多的分量。并不是所有的分量都会被加载进内存,不需要它们的时候,这些段可以被交换出去或重新分页以达到不浪费内存的目的。

进程描述符的信息可以打只归纳为以下几个大类:

  1. 调度参数。进程优先级,最近消耗的 CPU 时间,最近睡眠的时间。上面几项结合到一起决定下一个要运行的进程是哪一个。
  2. 内存映射。指向代码、数据、堆栈段或页表的指针。如果代码是共享的,代码指针指向共享代码表。当进程不再内存当中时,关于如何在磁盘上找到这些数据的信息也被保存在这里。
  3. 信号。掩码显示了哪些信号被忽略、哪些信号需要被捕捉、哪些信号被暂时阻塞以及哪些信号在传递当中。
  4. 机器寄存器。当内核陷阱发生时,机器寄存器的内容(也包括被使用了的浮点寄存器的内容)会被保存。
  5. 系统调用状态。关于当前系统调用的信息,包括参数和返回值。
  6. 文件描述符。当一个与文件描述符有关的系统调用被调用的时候,文件描述符作为索引在文件描述符表中定位相关文件的 i 节点数据结构。
  7. 统计数据。指向记录用户、进程占用系统 CPU 时间的表的指针。一些系统还保存一个进程最多可以占用 CPU 的时间、进程可以拥有的最大堆栈空间、进程可以消耗的页面数等。
  8. 内核堆栈。进程的内核部分可以使用的固定堆栈。
  9. 其他。当前进程状态。如果有的话,包括正在等待的时间、距离警报时钟超时的时间、PID、父进程的 PID 以及其他用户标识符、组标识符等。

当系统调用fork执行的时候,调用fork函数的进程陷入内核并且创建一个任务数据结构和其他相关的进程结构,如内核堆栈和thread_info结构。

进程描述符的主要内容根据父进程的进程描述符的值来填充。Linux 系统寻找一个可用的 PID,且该 PID 此刻未被任何进程远程使用。更新进程标识符散列表的表项使之指向新的任务数据结构即可。以防散列表发生冲突,相同键值的进程描述符会被组装成链表。它会把task_struct结构中的一些分量设置为指向任务数组中相应进程的前一/后一进程的指针。

现在就应该为子进程的数据段、堆栈段分配内存,并且对父进程的段进行复制,因为fork函数意味着父、子进程之间不再共享内存。如果代码段是只读的,那么可以共享也可以复制。然后子进程就可以运行了。

但是复制内存的代价想当高昂,所以现代 Linux 系统都采用了写时复制的技术。即赋予子进程属于它的页表,但是这些页表都指向父进程的页面,同时把这些页面标记为只读。当进程(可以是子进程可以是父进程)试图向某一页面写入数据的时候,它会收到写保护错误。内核发现进程的写入行为之后会为进程分配一个该页面的新副本,并将这个副本标记为可读、可写。通过这种方式,使得只有需要写入数据的页面才会被复制。这样做不需要在内存中维护同一个程序的两个副本从而节省了 RAM。

子进程开始运行之后运行代码(以 shell 的副本作为例子)调用系统调用exec,将命令名(ls)作为exec函数的参数。内核找到并验证相应的可执行文件,把参数和环境变量复制到内核,释放旧的地址空间和页表。

现在必须要建立并填充新的地址空间了。首先,新的页表会被创建,并指出所需的页面不在内存中。当新进程开始运行的时候,它会立刻收到一个缺页中断,这会使得的第一个含有代码的页面从可执行文件调入内存。通过这个方式,不需要预先加载任何东西,所以程序可以快速地开始运行,只有在所需页面不在内存中时才会发生页面错误。最后,参数和环境变量被复制到新的堆栈中,信号被重置,寄存器全部被清零。从这里开始,新的命令就可以运行了。

shell执行命令ls的步骤

Linux 中的线程

从历史观点上说,进程是资源容器,而线程是执行单元。一个进程包含一个或多个线程,线程之间共享地址空间、已打开的文件、信号处理函数、警报信息和其他。

2000 年的时候,Linux 系统引入了一个新的系统调用clone,模糊了进程和线程的区别。clone可是设置在传统观念上在线程和新线程间共享的资源是进程特有还是线程特有的。它的调用方式如下:

  pid = clone(function, stack_ptr, sharing_flags, arg);

如果通过sharing_flags指定地址空间是不共享的,新线程会获得地址空间的完整副本,但是新线程对这个副本进行的修改对于旧的线程来说是不可见的(事实上定义了一个新进程)。

参数sharing_flags是一个位图,这个位图允许比传统的 UNIX 系统更加细粒度的共享。

标志 值位时的含义 清楚时的含义
CLONE_VM 创建一个新线程 创建一个新进程
CLONE_FS 共享 umask、根目录和工作目录 不共享
CLONE_FILES 共享文件描述符 复制该文件描述符
CLONE_SIGHAND 共享信号句柄表 复制该表
CLONE_PID 新线程获得旧的 PID 新线程获得自己的 PID
CLONE_PTRACE 新线程与调用者有相同的父亲 新线程的父亲时调用者

Linux 对进程标识符(PID)和任务标识符(TID,线程 ID)进行了区分。这两个分量都存储在任务数据结构中。当调用clone函数创建一个新进程而不需要和旧进程共享任何信息时,PID 被设置成一个新值;否则,任务得到一个新的任务标识符,但是 PID 不变。

10.3.4 Linux 中进程调度

Linux 系统的线程是内核线程,所以 Linux 系统的调度是基于线程的。

Linux 把线程分了三类:1. 实时先入先出;2. 实时轮转;3. 分时。实时先入先出和实时轮转本质是具有更高优先级的线程,其执行的最后期限无法确定,并不是真正的“实时”。实时线程的优先级从 0 到 99,0 是实时线程的最高优先级,99 是实时线程的最低优先级。

实时先入先出线程不会被其他进程抢占,除非是一个拥有更高优先级的实时先入先出线程。

实时轮转和实时先入先出基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后可以被抢占。运行足够时间的实时轮转线程会被插入到实时轮转线程列表的末尾。

传统的非实时线程形成单独的类并有单独的算法进行调度,这样可以使非实时线程不与实时线程竞争资源。这些线程的优先级从 100 到 139(也就是说,Linux 系统包含 140 个不同的优先级)。Linux 系统根据非实时线程的要求以及它们的优先级分配 CPU 时间片。

Linux 系统给每个线程分配一个 nice 值(即优先级调节值)。默认值是 0,但是可以通过调用系统调用nice(value)来修改,修改值的范围从-20 到 19。这个值决定了线程的静态优先级。一个在后台大量计算的用户可以在他的程序里面调用这个系统调用为其他用户让出更多的计算机资源。而系统管理员可以调节的 nice 值范围从-20 到-1,即只有系统管理员可以要求比普通服务更好的服务。

Linux O(1)调度器(O(1) scheduler)是历史上一个流行的 Linux 系统调度程序,它可以在常数时间内执行任务调度。该调度器的调度队列被组织成两个数组,一个是正在活动的数组,一个是任务过期失效的数组。每个数组都包含了 140 个链表头(对应 140 个不同的优先级),链表头指向给定优先级的双向进程链表。其基本操作为:

  1. 调度器从正在活动数组里面选择一个优先级最高的任务。
    1. 若该任务的时间片过期失效了,就将它移动到过期失效数组。
    2. 若该任务阻塞了,比如它正在等待 I/O 事件,那么在它的时间片过期失效之前,一旦所等待的事件发生,这个任务就可以继续运行(赋予其更高的优先级),它将最终被放回到之前正在活动的数组中,时间片根据它所消耗的 CPU 时间相应地减少。一旦它的时间片消耗殆尽,它也会被放到过期失效的数组中。
  2. 当正在活动数组中没有其他的任务时,调度器交换指针,使得正在活动数组变为过期失效数组,过期失效数组变为正在活动数组,然后继续调度。

这种方法的好处是可以保证低优先级的任务不会被饿死。

不同优先级的任务被赋予不同的时间片长度,高优先级的进程拥有较长的时间片。

另外,由于 Linux 系统事先不知道一个任务究竟是 I/O 密集型,还是 CPU 密集型,它只是依赖于连续保持的交互启发式方法。通过这种方法,Linux 系统区分静态优先级和动态优先级。线程的动态优先级被不断地重新计算,其目的在于:1. 奖励互动进程,2. 惩罚占有 CPU 的进程(占用够多了,应该让给其它进程了)。

如何维护动态优先级呢?调度器给每一个任务维护一个名为sleep_avg的变量。每当任务被唤醒是,这个变量会增加;当任务被抢占或时间量过期时,这个变量会相应的减少。减少的值用来动态生成优先级奖励。当一个线程从正在活动数组移动到过期失效数组中时,调度器会重新计算它的优先级。

该调度器有显著的缺点,例如,利用启发式方法来确定一个任务的交互性,会使该任务的优先级复杂且不完善,从而导致在处理交互任务时性能很糟糕。

完全公平调度器(Completely Fair Scheduler,CFS)可以更好地解决这个问题。CFS 的主要思想是使用一颗红黑树作为调度队列的数据结构。根据任务在 CPU 上运行的时间长短而将其有序地排列在树中,这种事件称为虚拟运行时间(vruntime)。CFS 采用 ns 级的粒度来说明任务的运行时间。树中的每个内部节点对应于一个任务。左侧的子节点对应于在 CPU 上运行时间更少的任务,因此左侧的任务会更早地被调度,右侧的子节点是那些迄今消耗 CPU 时间较多的任务,叶子节点在调度器中不起任何作用。

CFS 调度算法可以总结如下:

  1. 该算法总是优先调度那些使用 CPU 时间最少的任务,通常是在树中最左边节点上的任务。
  2. CFS 会周期性地根据任务已经运行的时间,递增它的虚拟运行时间值,并将这个值与树中当前最左节点的值进行比较,如果正在运行的任务仍具有较小的虚拟运行时间值,那么它将继续运行,否则,它将被插入红黑树的适当位置,并且 CPU 将执行新的最左边节点上的任务。
  3. CFS 会改变任务的虚拟运行时间的有效速率,对于优先级较低的任务,时间流逝更快,它的虚拟运行时间也将增加的更快,相较于优先级更快的任务,优先级较低的任务将更快地重新插入树中。因此,CFS 可避免使用不同的调度队列结构来放置不同优先级的任务。

虚拟运行时间相当于中间的那一层,用来调速。

总之,选择一个树中的节点来运行的操作可以在常数时间内完成,然而在调度队列中插入一个任务需要 O(log(N))的时间,其中 N 是系统中的任务数。

10.3.5 启动 Linux 系统

当计算机启动时,BIOS 加电自检(POST),并对硬件进行预测和初始化(操作系统的启动依赖于磁盘访问、I/O 设备等硬件)。接下来,启动磁盘的第一个扇区,即主引导记录(MBR),被读到一个固定的内存区域并且执行。这个分区中有一个很小的程序,它从磁盘中启动,调入一个名为boot的独立程序。boot 程序将自身复制到高地址的内存当中从而为操作系统释放低地址的内存。

复制完成之后,boot 程序读取启动设备的根目录,由引导程序 GRUB(多系统启动管理器)让 boot 程序理解文件系统和目录格式。

之后,boot 程序读入操作系统内核,并把控制交给操作系统内核。之后,boot 程序就不再运行了,系统内核开始运行。

内核的启动是用汇编语言写成的,具有较高的机器依赖性。主要的工作包括创建内核堆栈、识别 CPU 类型、计算可用内存、禁用中断、启用内存管理单元,最后调用 C 语言写成的 main 函数开始执行操作系统的主要部分。

C 语言代码开始的时候会分配一个消息缓冲区来帮助调试启动出现的问题。随着初始化工作的进行,信息被写入消息缓冲区,这些消息与正在发生的事情相关,所以,如果出现启动失败的情况,这些信息可以通过一个特殊的诊断程序调出来。(我们可以把它当作是操作系统的“飞行信息记录器”,黑盒子)

接下来,内核数据结构得到分配。大部分内核数据结构的大小是固定的,但是一小部分,如页面缓存和特殊的页表结构,依赖与可用内存的大小。

之后,系统进行自动配置。这个过程包括识别设备、分配设备驱动程序、建立设备驱动程序的数据结构、建立文件系统、建立进程表、建立内存表、建立进程调度表、建立系统调用表、建立信号表、建立文件表、建立网络表、建立其他表。

一旦所有的硬件都配置好了,接下来它要进行的工作就是细心地手动运行进程 0,建立它的堆栈,运行它。进程 0 继续进行初始化,做如下的工作:配置实时时钟,挂在根文件系统,创建 init 进程(进程 1)和页面守护进程(进程 2)。

init 进程检测它的标志以确定它应该为单用户还是多用户服务。如果是为单用户服务,它调用 fork 函数创建一个 shell 进程,并且等待这个进程结束。如果是为多用户服务,它调用 fork 函数创建一个运行系统初始化 shell 脚本(即/etc/rc)的进程,这个进程可以进行文件系统一致性检测、挂载附加文件系统、开启守护进程等。然后这个进程从/etc/ttys 中读取数据,其中/etc/ttys 列出了所有的终端和它们的属性。对于每一个启用的终端,这个进程调用 fork 函数创建一个自身的副本,进行内部处理并运行一个名为 getty 的程序。

getty 程序为每条连线(原文是 session?)设置传输速率和其他属性(比如,有一些可能是调制解调器),然后在终端屏幕上输出:

login:

等待用户从键盘键入用户名。当用户键入用户名之后,getty 程序就结束了,登录程序/bin/login 开始运行。login 程序要求输入密码,给密码加密,并于保存在密码文件/etc/passwd 中的加密密码进行对比。如果密码正确,login 程序就以用户 shell 程序替换自身,等待第一个命令;否则,login 程序要求输入另一个用户名。

登录成功之后,用户就可以运行自己想运行的程序。

10.4 Linux 中的内存管理

10.4.1 基本概念

每个 Linux 进程都有一个地址空间,逻辑上有三段组成:代码、数据和堆栈段。

Todo:这里插入一张图片。

代码段(Code Segment)包含了形成程序可执行代码的机器指令。它是由编译器和汇编器把 C、C++或者其他程序源码转换成机器代码产生的。通常,代码段是只读的,自修改程序大约在 1950 年就不再时兴了。因此代码段既不增长也不较少。

数据段(Data Segment)包含了所有程序变量、字符串、数字和其他数据的存储。它有两部分,初始化数据和未初始化数据(BSS,符号起始块)。数据段的初始化部分包括编译器常量和那些在程序启动时就需要一个初始值的变量。所有 BSS 部分中的变量在加载后被初始化为 0。

例如,在 C 语言中可以在声明一个字符串的同时初始化它。当程序启动时,它需要一个初始值。为了实现这种构造,编译器在地址空间给字符串分配一个位置,同时保证在程序启动的时候,该位置包含了合适的字符串。从操作系统的角度来看,初始化数据和程序代码并没有什么不同。

未初始化数据,或者 BSS 的存在实际上只是一个优化。如果一个全局变量未显式初始化,那么 C 鱼眼的语义说明它的初始值为 0。而实际上,大部分全局变量并没有显式初始化,因此都是 0。这些可以通过简单设置可执行文件的一个段来实现,这个段的大小刚好等于数据所需的字节数。

但这个段是可以被优化的。为了节省可以执行文件(由代码段和数据段组成)的空间,操作系统只是记录了 BSS 段的大小,而不是实际的数据。没有必要让全是 0 的 BSS 段占用可执行文件的空间、磁盘空间。只需要将初始化数据段后面的 BSS 段替换为一个很短的头部,来告诉系统,在运行时,初始化数据后面有多少空间需要被初始化为 0。这样可以节约磁盘空间。

且为了避免一个全是 0 的物理页框,在初始化的时候,Linux 就分配了一个静态零页面,只一个全 0 的写保护页面。当加载程序时,未初始化数据区域被设置为指向该零页面。当一个进程真正要写这个区域时,写时复制的机制就开始起作用,一个实际的页框被分配给该进程。

即,所有的静态零页面在内存中只存在一个物理页框,多个进程的未初始化数据区域(如 BSS 段)都指向这个静态零页面。因为它是只读的,所以不需要为每个进程单独分配物理页框。当某个进程试图写这个页面的时候,会触发写保护错误,此时,内核会为该进程分配一个新的物理页框,并将原来的数据(即全 0 的数据)复制到新页框中。然后,这个进程可以在新的页框中进行写操作。

第三段是栈段(Stack Segment)。在大多数机器里,它从虚拟地址空间的顶部或者附近开始,并且向低地址空间延伸。当一个程序启动时,它的栈并不是空的。它包含了所有的环境变量以及为了调用它而向 shell 输入的命令行。

第四段是堆段(Heap Segment),其大小是可变的。有一个系统调用brk,允许程序设置其堆段的大小。C 库函数malloc通常被用来分配内存,它就大量使用这个系统调用。

10.4.2 Linux 中的内存管理系统调用

10.4.3 Linux 中的内存管理的实现

2. 内存分配机制

页面分配器和伙伴算法

分配物理内存页框的主要机制是页面分配器,它使用了著名的伙伴算法

img

管理一块内存的基本思想如下。

  1. 刚开始,内存有一块连续的片段组成,即(a)中的 64 个页面。
  2. 刚一个内存请求到达时,比如 7 个页面,首先上舍入到 2 的幂,比如 8 个页面。然后整个内存被分割成两半,如(b)。
  3. 因为这些片段还是太大了,较低的片段被再次二分(c),然后再二分(d)。
  4. 现在我们有了一块大小合适的内存,因此把它分配给请求者,如(d)。
  5. 现在假定 8 个页面的第二个请求到达了。这个请求被(e)直接满足了。
  6. 此时需要 4 个页面的第三个请求到达了。最小可用的块被分割(f),然后其一半被分配(g)。
  7. 接下来,8 页面的第二个块被释放(h)
  8. 最后,8 页面的另一个块也被释放。因为刚刚释放的两个邻接的 8 页面块来自同一个 16 页面块,它们合并起来得到一个 16 页面的块(i)。

伙伴算法有一些附加特性。

它有个数组,其中第一种元素是大小为 1 个单位的内存块列表的头部,第二个元素是大小为 2 个单位的内存块列表的头部,下一个是大小为 4 个单位的内存块列表的头部,以此类推。用这种方法,任何 2 的幂次大小的块都可以快速找到。

slab 分配器和对象缓存

但是,伙伴算法会导致大量的内部碎片。例如,请求一个 65 个页面的块,会导致一个 128 个页面的块被分配,剩下的 63 个页面就浪费了。这个问题可以通过slab 分配器来解决。slab 分配器使用伙伴算法获得内存块,但是之后从其中切出 slab(更小的单元)并且分别进行管理。

而在实际情况中,内核会频繁地创建和撤销某些类型的对象,例如 task_struct,因此系统使用了对象缓存来优化这个过程。这些缓存由指向一个或多个 slab 的指针组成,而 slab 可以存储大量相同类型的对象。每个 slab 要么是满的,要么是部分满的,要么是空的。

例如,当内核需要分配一个新的进程描述符(即一个新的 task_struct)时,它就会在 task_struct 的对象缓存中寻找,首先试图找一个部分满的 slab 并且在那里分配一个新的 task_struct 对象。如果没有这样的 slab 可用,就在空闲 slab 列表中查找。最后,如果有必要,它会分配一个新的 slab,把新的 task_struct 放在那里,同时把该 slab 连接到对象缓存当中。

补充,在内核地址空间分配连续内存区域的 kmalloc 内核服务,实际就是建立在 slab 和对象缓存接口之上的。

vmalloc 分配器

vmalloc 内存分配器仅适用于那些需要连续虚拟内存地址空间的请求,在物理内存中它们并不适用。什么意思?

3. 虚拟地址空间表示

10.4.4 Linux 中的分页

Linux 中的分页是一部分由内核实现,而一部分由一个新的进程——页面守护进程实现的。页面守护进程是进程 2(进程 0 是 idle 进程,传统上成为交换器,而进程 2 是 init)。页面守护进程周期性运行。

Linux 是一个请求换页系统。代码段和映射文件换页到它们各自在磁盘上的文件中。所有其他的都被换页到分页分区或者一个长度固定的分页文件,叫作交换区。其中,分页文件可以被动态添加或者删除,并且都有一个优先级。换页到一个独立分区并且像一个原始设备那样访问的这种方式比换页到一个文件的方式更加高效:1. 这样做不需要文件块和磁盘块的映射;2. 物理写可以是任意大小的,不仅仅是文件块的大小;3. 一个页总是被连续地写到磁盘上,而不是分散地写到文件中,避免了文件系统的碎片化,提高了效率。

1. 页面置换算法

Linux 试图保留一些空闲页面,这样就可以在需要的时候分配它们。当然这个页面池需要不断地补充。PFRA(页框回收算法)展示了它是如何发生的。

Linux 区分 4 种不同的页面:不可回收的(unreclaimable)可交换的(swappable)可同步的(syncable)可丢弃的(discardable)

  1. 不可回收的原棉包括页面保留或者锁定页面、内核态栈等,不会被换出。
  2. 可交换页必须在回收之前写回到交换区或者分页磁盘分区。
  3. 可同步页面如果被标记为 dirty 就必须要写回到磁盘。
  4. 可丢弃页面可以被立即回收。

在启动的时候,init 开启一个页面守护进程 kswapd(每个内存节点都有一个),配置其周期性运行。每次 kswapd 被唤醒,它通过比较每个内存区域的高低水位和当前内存的使用来检查是否有足够的空闲页面可用。有一个阈值确定可被回收的页面的数量,用以控制 I/O 的压力(由 PFRA 操作导致的磁盘写的次数)。另外,回收页面和扫描页面的总数目都是可以配置的。

PFRA 用一个类似时钟的算法来选择旧页面换出。PFRA 为每个页面维护两个标记:活动/非活动、是否被引用。这两个标记构成 4 种状态,理所当然,处在非活动列表,且自上次检查以来未被引用过的页面,是移出的最佳选项。如果需要,处于其它状态的页面也可能会被回收。

另外,守护进程 pdflush 周期性醒来,将脏页面写回到磁盘。

10.5 Linux 中的 I/O 系统

10.6 Linux 中的文件系统

10.7 Linux 的安全性

10.8 Android

10.9 小结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/799963.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux使用yum命令报错Cannot find a valid baseurl for repo: base/7/x86_64

【问题】 在VMware上安装搭建centOS 7虚拟机,配置好网络后,尝试通过yum命令进行安装docker容器。执行命令报错: 已加载插件:fastestmirror, langpacks Loading mirror speeds from cached hostfile  Could not retrieve mirrorlist http://mirrorlist.centos.org/?relea…

研发工程师的「第一性原理」思维

回顾复盘五年来的研发经历,愈发认同身边同事强调的“第一性原理”思维,仅做浅浅记录和分享一、定义与理论介绍第一性原理(First Principles),又称基本原理,是指从最基本的假设和定义出发,通过逻辑推理和演绎得出结论的一种思维方法。它强调对事物的本质和根源进行深入的…

Meta Llama3 论文研读

一、 引言概述(Intro & Overview) Llama3是一系列基于Transformer结构的大型多语言模型,通过优化数据质量、训练规模和模型架构,旨在提升模型在各种语言理解任务中的表现。通过引入更优质的数据和更高效的训练方法,Llama3展示了在自然语言处理领域的巨大潜力。其创新点…

9月26日云技术研讨会 | SOA整车EE架构开发流程及工具实施方案

本次研讨会经纬恒润将结合业务团队多年来在SOA架构开发和工具实施领域的项目实践经验,分享探讨SOA趋势下先进的整车EE架构开发模式,聚焦在SOA开发难点分析、开发阶段划分、开发工具链的适配与应用等内容。9月26日,我们在直播间期待您的参与! 面向服务的架构(Service…

服务的UUID

1. UUID 广播数据中,一般会包含一个UUID列表,用以展示自己支持的服务。但是GAP和GATT服务的UUID不能加到广播中。广播包中可以根据自身情况包含一部分服务的UUID或者包含所有服务的UUID。部分服务的UUID列表和完整的UUID列表只能包含一个。 什么情况下包含部分UUID?广播数据…

Common PyPI?

Skip to main contentTwo factor authentication is available, enable it now for your account.Search PyPISearch macnote Common questions BasicsWhats a package, project, or release? How do I install a file (package) from PyPI? How do I package and publish my…

Hackademic.RTB1 打靶记录

第一次打靶机,思路看的红队笔记https://www.vulnhub.com/entry/hackademic-rtb1,17/环境:kali Linux - 192.168.75.131,靶机 - 192.168.75.132 主机发现和端口扫描扫描整个网络有哪台机子在线,不进行端口扫描 nmap -sP 192.168.75.0/24 Starting Nmap 7.93 ( https://nmap.…

安装网站时出现“连接数据库出现数据库服务器或登录密码无效,无法连接数据库,请重新设定”解决方法

当你在安装网站时遇到“连接数据库出现数据库服务器或登录密码无效,无法连接数据库,请重新设定”的错误时,可以按照以下步骤来排查和解决这个问题。 排查步骤确认数据库连接信息:确认数据库服务器地址、用户名、密码、数据库名称等信息是否正确。 确认数据库服务器是否处于…

易优eyoucms网站二次验证密码忘记解决办法

当你忘记了易优CMS(EyouCMS)后台的二次验证密码时,可以通过修改数据库中的相关设置来绕过或重置这个验证。根据提供的记录信息,可以按照以下步骤来进行操作: 步骤登录数据库 修改相关设置 清除缓存详细步骤 1. 登录数据库 首先,你需要登录到你的MySQL数据库。你可以使用命…

mysql事务隔离级别和spring事务传播机制

一、事务并发会出现的三个问题 数据库事务具有ACID4个特性: A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行; C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100; I:Isolation…

电商系统的简单设计

订单模块 作为电商系统,首入眼帘的就是订单模块,也是电商基础的模块之一。订单流程包含了订单从下单到完成的整个流程,订单的状态如下:这里迎来了第一个问题,可以看到订单状态有非常多种,如果用if else去做判断,逻辑会非常多,这时候就需要用到状态机模式了,状态机如何使…

限流器的实践

背景 我们有一个业务场景是给学生发布考试,发布的过程不复杂,就是一个老师传递一些考试相关的参数过来,服务器自动给所有学生生成一份任务,但是在学生上交的时候会有个问题,就是成百上千的学生一起上交,会有并发流量的问题。 这里由于我们的考试可能会设计多个班级的联考…