《Linux是怎么样工作的》读书笔记(二)

简介: 《Linux是怎么样工作的》读书笔记

《Linux是怎么样工作的》读书笔记(一)https://developer.aliyun.com/article/1394971


Linux内存管理

简单介绍

下面我们就来简单介绍Linux内存管理的,在Linux中内存管理可以大致理解为三个部分:

  • 内核使用的内存
  • 进程使用的内存
  • 可用内存(空闲内存)

其中除开内核使用的内存维持系统正常运行不能被释放之外,其他均可以由操作系统自由支配。在Linux中拥有free命令来专门查看内存的使用情况,执行的效果类似如下:


/opt/app/tdev1$free
             total       used       free     shared    buffers     cached
Mem:       8175320    6159248    2016072          0     310208    5243680
-/+ buffers/cache:     605360    7569960
Swap:      6881272      16196    6865076

各个列的含义如下:

  • total:系统搭载物理内存总量,比如上面为8G。
  • free:表面可用内存。
  • buff/cache:缓冲区缓存和页面缓存,在计算机存储层次简析中提到了当内存不够可以使用释放缓存腾出空间给内存使用。
  • availiable:实际可以使用的内存,计算公式很简单即内核之外的可用总内存 - (free + buff/cache 最大可以释放的内存)

除了列数据之外还有一个swap的行,这个参数的含义将在后文进行介绍。

Linux除了free命令之外,还有sar -r命令,可以通过这个参数指定采集周期,比如-r 1就是1秒采集一次。

个人目前使用的电脑为Mac,虽然是类Unix系统但是没有free相关的命令,为此可以使用下面的命令进行简单的替代,但是不如free强大。

在Mac中使用top -l 1 | head -n 10查看整体系统运行情况。


MacBook-Pro ~ % top -l 1 | head -n 10
Processes: 604 total, 2 running, 602 sleeping, 3387 threads 
2022/04/15 17:29:57
Load Avg: 2.84, 3.27, 5.68 
CPU usage: 6.8% user, 14.18% sys, 79.72% idle 
SharedLibs: 491M resident, 96M data, 48M linkedit.
MemRegions: 168374 total, 5515M resident, 235M private, 2390M shared.
PhysMem: 15G used (1852M wired), 246M unused.
VM: 221T vsize, 3823M framework vsize, 0(0) swapins, 0(0) swapouts.
Networks: packets: 312659/297M in, 230345/153M out.
Disks: 788193/14G read, 161767/3167M written.

除此之外,可以在Mac中使用使用diskutil list


```shell
~ > diskutil list
/dev/disk0 (internal):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                         500.3 GB   disk0
   1:             Apple_APFS_ISC ⁨⁩                        524.3 MB   disk0s1
   2:                 Apple_APFS ⁨Container disk3⁩         494.4 GB   disk0s2
   3:        Apple_APFS_Recovery ⁨⁩                        5.4 GB     disk0s3
/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +494.4 GB   disk3
                                 Physical Store disk0s2
   1:                APFS Volume ⁨mysystem⁩                15.2 GB    disk3s1
   2:              APFS Snapshot ⁨com.apple.os.update-...⁩ 15.2 GB    disk3s1s1
   3:                APFS Volume ⁨Preboot⁩                 529.6 MB   disk3s2
   4:                APFS Volume ⁨Recovery⁩                798.6 MB   disk3s3
   5:                APFS Volume ⁨Data⁩                    455.3 GB   disk3s5
   6:                APFS Volume ⁨VM⁩                      24.6 KB    disk3s6
/dev/disk6 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *512.1 GB   disk6
   1:       Microsoft Basic Data ⁨⁩                        512.1 GB   disk6s1
/dev/disk7 (external, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *1.0 TB     disk7
   1:       Microsoft Basic Data ⁨Extreme SSD⁩             1.0 TB     disk7s1

下面是freesar这两个命令的输出结果对应关系:

  • total : 无对应
  • free :kbememfree
  • buff/cache :kbbufferrs + kbcached
  • available:无对应

如果内存使用过多,系统为了空出内存可能出现强制 kill 某个进程的操作,此操作是随机的并且无法被监控,商用机器上执行这种操作是十分危险的,所以有部分的商用机器会开启一旦OOM直接把整个系统强制关闭的操作。

内存分配方式及问题

内核分配内存的时机大致有下面两种:

  1. 创建进程。
  2. 创建进程之后进行动态内存分配。

在进程创建之后如果进程还需要内核提供更多的内存,则可以向内核发出内存的请求申请,内核收到指令之后,则划分可用内存并且把起始结束的地址给进程进行使用。

但是这种要一点给一点的方式有下面几个常见的问题:

  • 难以执行多个任务。
  • 访问其他用途的内存区域。
  • 内存的碎片化。

注意⚠️:内存不仅仅需要和CPU通信还需要和其他的控制器和硬件打交道,分配内存给进程只是诸多任务的项目之一。

难以执行多任务

可以理解为进程频繁的需要申请内存的情况,这时候内核需要不断的操作分配内存给进程,整个任务相当于被单个进程给拖累了。

另外如果多个任务出现分配内存的区域刚好相同,此时需要要完成内存分配给那个进程任务,则另一个进程等待也是可以理解的。

内存碎片化

原因是进程每次获取内存都需要了解这部分内容要涵盖多少区域否则就不能获取这些内存。

内存碎片化的另一个重大问题是明明有很多富裕的内存但是却拿不出一块完整连续的空间给进程使用,导致不断的回收和分配操作。

访问其他用途的内存区域

这种情况进程访问被叫做缺页访问中断,在后续的内容会进行介绍。

虚拟地址和物理地址

为了解决上面的问题,操作系统使用了虚拟内容和物理内存的方式进行内存管理。

我们需要了解三个概念:地址空间、虚拟地址、物理地址

地址空间:指的是可以通过地址访问的范围都统称为地址空间。

虚拟地址:虚拟地址指的是进程无法直接访问到真实的物理内存地址,而是访问和实际内存地址映射的虚拟内存地址,目的是为了保护系统硬件安全。

物理地址:也就是我们实际内存对应的实际的物理地址。

这里举一个简单的例子:如果内核给进程分配100地址的虚拟内存地址,那么这个虚拟内存地址实上可能会指向实际的600物理地址。

页表

完成虚拟地址到物理地址的映射依靠的是页表,在虚拟内存当中所有的内存都被划分为页,一个页对应的数据条目叫做页表项,页表项记录物理地址到虚拟地址的映射关系。

在x86-64的架构中一个页的大小为4KB,进程在内存是有固定的起止地址的,那么如果出现超出地址的页访问,也就是访问了没有虚拟地址和物理地址映射的空间会出现什么情况呢?

如果出现越界访问,那么此时CPU会出现缺页中断,并且终止在缺页中进行操作的进程指令,同时启动内核的中断处理机构处理。

注意⚠️:对应访问其他用途的内存区域这个问题。

虚拟内存分配操作

虚拟内存的分配操作步骤我们可以理解为几个核心的步骤:

  • 内核寻找物理地址并且把需要的物理地址空间计算。
  • 创建进程的页表把物理地址映射到虚拟地址。
  • 如果进程需要动态内存管理,内核会分配新页表以及新的可用内存给进程使用,当然同时提供对应的物理内存空间。

物理分页使用的是请求分页的方式进行处理,这个分配的操作十分复杂。

内存的上层分配

在C语言中分配内存的函数是malloc函数,而Linux操作系统中用于分配内存的函数是mmap函数,这两者最大区别是mmap函数使用的是按页的方式分配,而malloc是按照字节的方式分配。

glibc通过系统调用mmap申请大量的内存空间作为内存池,程序则调用malloc内存池请求分配出具体的内存供进程使用,如果进程需要再次获取内存则需要再次通过mmap获取内存并且再次进行分配操作。

在上层编程语言也是使用了类似的操作,首先通过glibc向内核申请内存执行虚拟内存的分配操作,然后malloc函数再去请求划分具体的内存使用,只不过更上层的语言使用了解析器和脚本进行掩盖而已,实际上通过层层翻译最终的操作依然是上面提到的操作。

虚拟内存是如何解决简单分配的问题的?

这里我们再次把上面三个问题搬出来,再解释虚拟内存是如何处理问题的:

  • 难以执行多个任务:每个进程有独立的虚拟地址空间,所以可以编写专用地址空间程序防止多个任务阻塞等待的情况。
  • 访问其他用途的内存区域:虚拟地址空间是进程独有,页表也是进程独有。页表的另一个作用是限制可以防止当前的进程访问到其他线程的页表和地址空间。
  • 内存的碎片化:内存碎片化使用页表的方式进行分配,因为页表记录了物理地址到虚拟地址的映射,这样就可以很好的知道未使用的空间都干了啥。

虚拟内存的其他作用:

  • 文件映射
  • 请求分页
  • 利用写时复制的方式快速创建进程
  • 多级页表
  • 标准大页

小结

这一部分简要阐述Linux内存管理的入门理解部分,这一部分主要介绍了简要的内存分配方式,以及Linux对此通过页表的方式实现物理地址和虚拟地址的分配,最后阐述了操作系统和编程语言也就是进程之间是如何分配内存的,具体的分配步骤和交互逻辑介绍。

Linux内存管理优化

文件映射

经过之前的内容我们了解到文件映射通过映射虚拟内存的方式实现,进程访问内存对时候实际是文件对应的副本虚拟内存地址,既然访问虚拟内存位置可以完成文件的修改映射,那么直接访问物理内存也就是实际内存修改内容也是可行的。

如果知道文件的具体地址,甚至可以直接定位到内存地址对于内容进行覆盖,在书中有一个C语言写的验证程序比较有意思。

请求分页

进程向内核申请内存的通过请求分页的方式完成,之前提到过通过mmap的方式申请内存的方式虽然很方便但是是有问题的:

通常的内存分配方式有下面两种:

  • 物理内存的直接申请和分配,高效。
  • 句柄分配的方式,也就是页表对于虚拟内存和实际内存映射之后再给进程。

这两种分配方式都存在两个比较明显的问题,那就是分配的时候如果申请了却没有使用会大量浪费,另外一次glibc访问需要超过进程的内存,但是进程此时很可能不会使用甚至可能根本不使用,此时很可能出现很大的进程管理大量被申请未使用内存

请求分页就是用来解决上面提到的问题的。

请求分页理念

为了更好理解请求分页需要先理解分页的三种状态

  • 未分配页表和物理内存给进程。
  • 已分配页表但是未分配物理内存。
  • 已分配页表和物理内存。

为了解决分配浪费的问题,分配进程的内存仅使用一次分配方式,请求分页的核心是利用内核缺页中断的机制,当进程初次访问到已分配但是没有没有分配物理内存的空间,对于此时内核会进行缺页中断处理,同时给进程真正申请物理内存进行分配动作,这样可以保证每次分配内存的动作都是有效的。

这种方式也类似懒加载的方式,即可以保证分配动作运行,进程无感知缺页中断的情况,依然可以正常运行。

如果使用C语言按照请求分页的特点进行实验,可以发现当内存没有使用的时候即使显示已经分配内存,但是实际可用物理内存没有变动

另外分配内存失败分为虚拟内存分配失败,物理内存分配失败,这是因为“懒加载”的设计导致的,另外虚拟内存不足不一定会导致物理内存不足,因为只要可用物理在分配空间小于虚拟内存那就是没法分配并且会分配失败。

写时复制

写时复制是利用fork的函数提高虚拟内存分配效率。

在文件系统的体现是update或者delete操作不会动原数据,而是用副本完成操作,当操作完成再更新引用,如果中间宕机断电,则用日志恢复状态即可。

Linux 系统的内存管理中调用 fork 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是首先与父进程共用相同的页表,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 写时复制 机制。

写时复制的流程如下:

  • 在父进程调用fork的时候,并不是把所有内存复制给子进程而是递交自己的页表给子进程,如果子父进程只进行只读操作双方都会共享页表,但是一旦一方要改变数据,就会立马解除共享
  • 在解除共享的时候会有如下操作
  • 由于写入失去权限,所以会出发缺页中断,此时会切断用户态,保存当前进程状态并且进行内核态。
  • 切换至内核态,内核干预,执行缺页中断。
  • 写入方的数据复制到另一处,并且把写入方的页表全部更新为新复制的内存并且赋予写入方写入权限,同时把之前共享的页表更新,并且把另一方刷新之后的页表重新连接即可。(关键)
  • 最后父子进程彻底写入权限和页表独立。但是之前解除共享的页表依然可以自由读写。

注意⚠️:注意解除共享并不是所有的共享都解除,而是解除共享需要独立的部分,这种处理是。

另外还需要强调进行fork调用的时候并不会复制页表和内容,而是真正写入的时候会触发复制动作,这也是写时复制名字由来。

写时复制中如果只进行只读操作双方都会共享页表,但是一旦一方要改变数据,就会立马解除共享。

交换内存

交换内存是Linux内核一种OOM情况下的补偿机制,作用也是为了缓解内存溢出和不足的问题,交换内存的实现依靠的是虚拟内存的机制

简单理解就是在物理内存虽然不够但是虚拟内存可以借用外部存储器也就是硬盘的一部分空间来充当物理内存使用,这一块分区叫做交换区,由于是借物理存储空间,这个操作也叫做换出。

另外如果借用的空间被释放则会立即归还,这种操作叫做换入,由于交换内存以页为单位,部分资料也叫页面调入和调出都是一个意思。

交换内存很容易认为是扩充物理内存的美好方式,但是这里有一个本质的问题,那就是硬盘的访问速度和内存相比差的次方级别的差距,另外如果长期内存不足很容易导致交换内存不断的换入换出出现明显的性能抖动。

硬性页缺失和软性页缺失

另外这类需要外部存储器的缺页中断在术语中被称之为硬性页缺失,相对的不需要外部存储器的页缺失是软性页缺失,虽然本质都是内核在触发和完整操作,但是硬性的缺失总归比软性缺失后果严重很多。

这里也要吐槽一下M1的各种偷硬盘缓存来提高性能的操作.....这也是为什么要把硬盘和CPU集成的原因之一。

多级页表

多级页表的设计核心是:避免把全部页表一直保存在内存中是多级页表的关键所在,特别是那些不需要的页表就不应该保留。

在X86-64架构当中,虚拟地址的空间大小约为128T,一个页的大小为4KB,页表的项目大小为8个字节。

所以一个进程的页表至少需要256GB内存!(8 * 128T / 4KB),但是我们都知道现在的电脑一般都是16GB内存为主。

那么系统应该如何维护页表?这就引入了多层页表来进行管理,多级页表可以从最简单的角度当作一个多级的指针看待。

首先我们可以思考,一个进程是否需要整个页表来管理内存?很显然是不需要的,这是引入多级页表的理由之一。

假设一个进程需要12M空间,顶端需要4M,数据部分占用4M,底部又是一些堆栈内容和记录信息,在数据顶端上方和堆栈之间是大量根本没有使用的空闲区。

多级页表实际上就是大目录套小目录,和我们的一本书一样,小目录负责小的进程,而遇到比较大的进程就放到空闲页比较大的目录中完成分配操作,多级页表既可以高效的利用内存的同时,可以最大限度的减少页表本身的数据结构在内存的占用,同时上面的例子也可以发现绝大多数的进程其实根本不需要太大的页表进行维护和管理。

最后从网上的资料翻阅中发现一张下面的图,对于多级页表的理解有一定帮助:

image.png

最后X86_64 使用了4层的页表结构,直当理解就是四级指针。

标准大页

随着进程虚拟内存和页表的使用,进程使用的物理内存也会增加。

根据请求分页和写时复制的概念,调用fork函数的时候会对于父子进程共享的页表进行拷贝,虽然这个拷贝动作不会占用物理内存,但是需要拷贝一份完整的页表,当页表很大的时候也会造成性能浪费。

为了解决这个问题,Linux提供了标准大页的机制,和他名字一样就是比普通的页表更大的页,利用这种页表可以减小进程页表所需的内存使用量,通过多级页表和标准大表可以有效的减少整个页表项的数量。

用法

C语言中使用mmap函数参数赋予 MAP_HUGETLB 标志,表示可以获取大页,但是更加常用的方式是使用程序允许使用使用标准大页而不是这种手动切换的方式。

标准大页对于虚拟机和数据库等需要使用大量内存的应用程序是很有必要的,根据实际情况决定是否使用标准大页,通过这种设置可以减少这一类软件内存占用,还能提高fork效率。

透明大页

透明大页是随着标准大页带来的附带特性,主要的作用是在连续的4KB页面如果符合指定条件就可以通过透明大页的机制转为一个标准大页,以及在不满足标准大页条件的时候拆分为多个4KB页面。

小结

这部分从文件映射的内容引申了Linux两个重要的机制:请求分页写时复制,目的本质上都是尽量减少进程对于内存的浪费,但是需要注意的是这两种方式都是使用了内核模式的系统中断机制来进行处理的,所以对于内核的性能以及稳定性要求非常高。

在之后的内容介绍了交换内存以及多级页表和标准大页几个内容,其中多级页表内部的细节非常的复杂,通常需要对于操作系统底层有比较熟悉的认知才能完全的了解这个页表的细节。

进程调度器

针对Linux进程调度有下面的思考:

  • 每一个CPU同一时间只能调度一个进程。
  • 每一个进程有*近乎相等的执行时间。
  • 对于逻辑CPU而言进程调度使用轮询的方式执行,当轮询完成则回到第一个进程反复。
  • 进程数量消耗时间和进程量成正比。

对于进程调度来说不能保证一个程序是连续完成的,由于CPU调度和进程切换,上下文也会出现切换情况。

进程状态

对于大部分进程来说,当我们不使用的时候多数处于睡眠状态。

除了睡眠状态之外,进程还有下面几种状态:

  • 运行状态:获得CPU调度,执行进程任务。
  • 僵死状态:进程等待结束,等待父进程回收。
  • 就绪状态:具备运行条件,等待CPU分配。
  • 睡眠状态:进程不准备运行除非某种条件触发才会获得CPU调度分配。

处在睡眠态的进程触发运行态的条件如下:

  • 外部存储器访问。
  • 用户键入或者鼠标操作触发事件。
  • 等待指定时间。
  • 等待指定时间。

通过ps ax命令可以查看当前的进程状态,下面的案例以个人的Mac电脑为例:


MacBook-Pro ~ % ps ax
  PID   TT  STAT      TIME COMMAND
32615   ??  Ss     0:00.11 /usr/libexec/nearbyd
32632   ??  Ss     0:00.51 /System/Library/CoreServices/Screen Time.app/Content
32634   ??  Ss     0:00.02 /System/Library/PrivateFrameworks/Categories.framewo
32635   ??  S      0:00.12 /System/Library/CoreServices/iconservicesagent
32636   ??  Ss     0:00.05 /System/Library/CoreServices/iconservicesd
32671   ??  S      0:02.44 /Applications/Microsoft Edge.app/Contents/Frameworks
32673   ??  S      0:02.86 /Applications/Microsoft Edge.app/Contents/Frameworks
32678   ??  Ss     0:00.17 /System/Library/PrivateFrameworks/UIFoundation.frame
32726   ??  S      0:00.07 /System/Library/Frameworks/CoreServices.framework/Fr
32736   ??  S      0:00.08 /System/Library/Frameworks/CoreServices.framework/Fr
32738   ??  S      0:00.75 /System/Applications/Utilities/Terminal.app/Contents
32739   ??  Ss     0:00.02 /System/Library/PrivateFrameworks/Categories.framewo
32746   ??  Ss     0:00.03 /System/Library/Frameworks/Metal.framework/Versions/
32740 s000  Ss     0:00.02 login -pf xxxxxx
32741 s000  S      0:00.03 -zsh
32750 s000  R+     0:00.01 ps ax

s表示sleepd表示此时可能在等待磁盘IO,但是如果长时间处于d状态则可能是磁盘IO等待超时或者内核可能发生故障。

下面是根据上面的介绍绘制的进程状态流转图

image.png

如果只执行一个进程同时在进程中间休眠过一次,那么此时休眠的进程在干什么?

进程会进入空进程的模式轮询,但是空进程不是没有事做,而是需要调用一些维持系统运行的线程,为了保证系统正常稳定运行。

因为CPU和空闲进程,所以同样会不断的切换睡眠态和运动态,运动态获取用户输入操作完成动作,睡眠态则执行一些轻量操作。

针对睡眠态的进程会有如下特点:

  • 遵循同一时间CPU只能完成一个进程操作
  • 睡眠态不占用CPU时间。

吞吐量和延迟

  • 吞吐量:处理完成的进程数量 / 耗费时间
  • 延迟:结束处理时间 - 开始处理时间

通过这两点可以总结几点规则:

吞吐量的上限是进程的数量多过逻辑CPU的数量,则再增加进程无法增加吞吐量,另外进程中的延迟总是平均的,也就是说多个进程执行会获得近似平均的延迟,最后进程越多延迟越高。

但是现实系统没有那么多理想情况,多数情况是下面几种:

  • 空闲进程:此时吞吐量很低,因为很多逻辑CPU都在睡眠状态。
  • 进程运行态:此时没有就绪:这种状态比较理想,CPU可以安排到下一次处理,虽然会延后开始执行提高吞吐量,但是可能会出现CPU空闲的情况。
  • 进程运行态,同时就绪:此时就好像赛跑,但是只有一个跑道,每一个进程轮流处理一会儿,所以此时延迟变长。

另外由于很多程序编写都是单线程程序,一核运行,多核围观或许在过去更普遍。

优化吞吐量和延迟的方式是使用 sar 命令找到运行时间和开销最大进程,同时把一些死进程kill掉。

多CPU调度情况

分片时间每一个进程用一个CPU工作,那么分配和调度CPU安排工作又是如何的?

主要有两种方式,第一种是通过轮询负载均衡,另一种是全局分配,把任务分配给空闲进程的逻辑CPU。

负载均衡是CPU遇到进程任务依次安排工作,当最后一个CPU安排完成之后,则再回到第一个CPU进行分配,同时都是对于进程执行一定的时间,也就是说出现CPU-A处理一部分,另一部分可能是CPU-B完成。

全局分配的方式比较简单,就是把任务分配给处于空闲进程的逻辑CPU完成工作。

查看系统逻辑CPU的命令如下:

grep -c processor /proc/cpuinfo

多核cpu通常只有在同时运行多个进程的时候才会发挥作用,但是并不是说有多少核心就有多少倍性能,因为大部分时候进程很少很多CPU都在睡眠态度

如果进程超过逻辑CPU数量,无论怎么增加进程都不会提高处理速度

最后处于睡眠状态的进程其实可以指定睡眠时间,通过sleep函数调用完成进程休眠的操作。

小结

进程调度器的内容远没有上面介绍的简单,但是作为理解进程的切入点是不错的。

计算机程序概览

放到最后是因为个人认为算是这本书相对没有什么价值的部分。

概览

狭义上的计算机结构是:CPU,内存,外部设备,其中外部设备包含输入输出和外部外存储器以及网络适配器。

而从广义上来看,计算机可以用抽象化的概念进行解释,则可以简单讲计算机分为三部分:

  • 第一部分是应用程序,这些程序依托于环境和载体。
  • 第二部分就是运行程序的载体,负责管理系统调用,进程切换和设备驱动这些工作,同时担任重要的硬件抽象的角色,为应用程序掩盖掉复杂的底层维护工作。
  • 最后一部分是硬件,这部分就是我们狭义的理解计算机的部分,这里不多介绍。

在硬件设备重复执行以下步骤:

  • 输入设备或者网络适配器发起请求
  • 读取内存命令,CPU上执行,把结果写入负责保存到内存区域
  • 内存的数据写入HDD、SDD等存储器

而程序大致则分为下面几种:

  • 应用程序:让用户直接使用,为用户提供帮助程序,例如计算机等办公软件,计算机上的办公软件,智能手机和其他应用。
  • 中间件:辅助程序运行等软件,比如WEB服务器和数据库
  • OS:控制硬件,为应用程序和中间件提供运行环境,Linux 叫做OS。

用户模式和内核模式

下面的内容可以认为是把之前的内容回顾一遍:

用户模式

用户进程访问的时候都是用户模式,用户模式是受到保护或者说权限受限的,只能够使用内核分配的内存和CPU进行操作,失去使用权则会处于等待的情况。

用户进程的一大特点是用户的空间只能用户进程使用,所以一旦有用户进程崩溃了,内核可以去把它给清理掉。因此增强系统的鲁棒性。

内核模式

此模式下运行的代码对 CPU 和内存具有无限的使用权限,这个强大的权限使得内核可以轻易腐化和崩溃整个系统,所以内核使用的空间是只能被内核访问的,其他任何用户都无权访问。

特点对比

内核级线程特点 用户级线程的特点
1.进程中的一个线程被阻塞,内核能调度同一进程的其他线程(就绪态)占有处理器运行。 2.多处理器环境中,内核能同时调度同一进程的多线程,将这些线程映射到不同的处理器核心上,提高进程的执行效率。 3.应用程序线程在用户态运行,线程调度和管理在内核实现。线程调度时,控制权从一个线程改变到另一线程,需要模式切换,系统开销较大 1.线程切换不需要内核模式,能节省模式切换开销和内核资源。 2.允许进程按照特定的需要选择不同的调度算法来调度线程。调度算法需要自己实现。 3.由于其不需要内核进行支持,所以可以跨OS运行。 4.不能利用多核处理器优点,OS调度进程,每个进程仅有一个ULT能执行 5.一个ULT阻塞,将导致整个进程的阻塞。

对于用户线程阻塞在后续技术的发展出现了一种叫做Jacketing技术

使用Jacketing技术把阻塞式系统调用改造成非阻塞式的。当线程陷入系统调用时,执行jacketing程序,由jacketing程序来检查资源使用情况,以决定是否执行进程切换或传递控制权给另一个线程。

也就是说Jacketing技术实现了当用户线程阻塞的时候更加灵活的进行切换,防止出现一个线程的阻塞导致多线程阻塞这种情况。

用户模式切换到内核模式

一般是发生了中断或者无法处理的系统异常情况下出现。

此时内核会剥夺用户进程的控制权进行处理, 并且执行一些内核的修复操作,比如缺页中断申请内存并且分配新的页表给进程,这种机制为请求分页的处理方式。

注意⚠️:更多细节可以回顾[进程调度器]查看有关进程调度的内容。

用户模式切换到内核模式一般会经历下面的步骤:

  • CPU模式切换
  • 保存当前的进程状态到核心栈。
  • 中断异常程序调度处理

内核模式切换到用户模式

当中断异常处理调度程序完成之后,内核模式会逐渐转为用户模式运行,此时用户线程回从核心栈找回当前到进程状态,并且CPU运行模式也会执行为用户模式。

模式切换的优劣对比

其实这两个模式用最抽象的概念就是应用程序和系统程序之间的差别,因为对于用户来说模式切换是透明的,基本使用过电脑用过电脑程序,基本都经历过应用程序崩溃引发系统崩溃的情况,此时就是一种用户模式到内核模式的切换。

用户模式的好处在于访问和空间占用都是受到内核管控的,但是有一个很大的问题是一旦出现中断异常就会发生用户模式和内核模式的切换,在通常情况下看起来没什么问题,但是随着进程需要的内存越来越大,每一次切换对于系统带来影响和开销都是非常严重的。

在很多大型进程中,因为模式切换带来的问题也并不罕见,更多情况下是用户编写的低质量或者问题代码出现资源浪费导致的问题。

简单对比Windows

下面我们类比Windows系统的内核模式以及用户模式的切换,这里主要看看微软的官方文档是如何介绍的:

用户模式:进程享受专用的虚拟地址空间,和Linux类似的 在用户模式下运行的处理器无法访问为操作系统保留的虚拟地址。

内核模式:在内核模式下运行的所有代码都共享单个虚拟地址空间

image.png

其实简单对比一下就会发现实现机制都是类似的,只不过内部实现代码不同和细节不同而已,基本的特性都是相似的。

C标准库

C标准库在很早就被国际设立为标准规范,哪怕过去这么多年依然通用。

而C标准库中较为核心的组件是glibc,之前介绍过glibc是用户进程像内核申请内存的关键实现函数,使用glibc申请内存再使用mmap函数申请具体的内存,这部分内容可以阅读[内存管理]进行了解。

glibc通过系统调用向mmap申请大量的内存空间作为内存池,程序则调用malloc根据内存池分配出具体的内存使用,如果进程需要再次获取内存则需要再次通过mmap获取内存并且再次进行分配操作。

另外高级的编程语言同样依赖或者基于C标准库,比如Python的ppidloop就是如此,除此之外还有C++对于C标准库的进一步扩展等。

OS提供的程序

下面列举一些OS提供的程序:

  • 初始化系统:init
  • 变更系统运作方式:sysctl、nice、sync
  • 文件操作:touch、mkdir
  • 文本数据处理:grep、sort、uniq
  • 性能测试:sar、iostat
  • 编译:gcc
  • 脚本语言运行环境:perl、python、ruby
  • shell:bash
  • 视窗系统:x

另外系统调用通常也可以分为下面几种:

  • 进程控制:[[010106 - 进程调度器]]
  • 内存管理:[[010105 - Linux内存管理]]
  • 进程通信:[[010106 - 进程调度器]]
  • 网络管理(本书未涉及)
  • 文件系统操作:[[010103 - Linux文件系统设计]]
  • 文件操作:[[010101 - Linux与外部结构介绍]]

写在最后

还是挺不错的一本书,虽然没有那些板砖书那么系统,但是对于初学者来说是个不错的切入书,这本书买实体书不是很划算,因为配图讲解工作原理的内容比较多,甚至让我觉得作者应该多一点文字描述......

附录

附录部分

LBA(Logical Block Addressing)逻辑块寻址模式

HDD常见寻址方式

CHS寻址 CHS寻址也被称为NORMAL 普通模式,此寻址模式是最早的 IDE 方式。

在硬盘访问时,BIOS 和 IDE 控制器对参数不做任何转换。该模式支持的最大柱面数为 1024,最大磁头数为 16,最大扇区数为 63,每扇区字节数为 512,因此支持最大硬盘的容量为:

512x63x16x1024=528MB

⚠️注意:最早期的计算机仅仅只需要500多MB就够用,和现在确实是天差地别。

LBA寻址

LBA的寻址特点是地址不再和物理磁盘的位置一一对应,前面CHS寻址使用了三个关键参数:磁头位置,存储柱面位置,扇区位置三个参数利用三维的参数来计算容量,而LBA寻址则使用了一个参数进行寻址,同时由IDE控制计算柱面、磁头、扇区的参数等组成的逻辑地址转为物理地址。 LBA可以设置最大的磁头数为255,而其他参数和CHS寻址的模式类似,所以我们对应上面的结算公式只需要把16改为255,最终结果如下:

512x63x255x1024=8.4G。

另外在早期的LBA寻址中主板上大多数是28位的LBA寻址,而前面讨论了LBA的三维参数和物理地址不是一一对应的,而是通过IDE计算逻辑地址寻找最终的物理地址。

根据计算机的位操作我们可以计算出逻辑块为2的28次方= 137,438,953,472个字节也就是137G,意味着最早期的磁盘寻址极限为137G,当然这也是因为当时的主板技术跟不上硬盘技术导致的,并且当时的计算机使用人员用不到137G的容量。

当然要突破这个限制也非常简单,把28位的寻址提高就可以直接支持更大容量的硬盘,经过发展,目前使用的都是48位LBA寻址方式

而48位LBA寻址方式的理论容量极限是144,115,188,075,855,872字节=144,000,000 GB!上亿的磁盘容量基本够目前使用。

由于LARGE、LBA寻址模式采用了逻辑变换算法看上去比CHS复杂不少,但是不少的资料、磁盘工具类软件中采用的硬盘参数介绍和计算方法却还是按照相对而言比较简单的CHS寻址模式

而LBA寻址模式说白了也是在CHS寻址模式上的改进,也需要向前兼容,因此CHS寻址模式是硬盘寻址模式的基础,理解CHS寻址模式HDD硬盘使用和维护还是很有用的。

LARGE寻址

LARGE 大硬盘模式,在硬盘的柱面超过 1024 而又不为 LBA 支持时采用。LARGE 模式采用的方法非常简单粗暴,就是把直接把柱面数除以 2,把磁头数乘以 2,其结果总容量不变方式寻址。

⚠️注意:LBA寻址到现在依旧是主流。

容量和大小对比

  • CHS(或称为Normal)模式: 适应容量≤504MB的硬盘。
  • LARGE(或称LRG)模式: 适应504MB≤容量≤8.4GB的硬盘 。
  • LBA(Logical Block Addressing)模式: 适应容量≥504MB的硬盘,但BIOS需支持扩展INT13H,否则也只能适应≤8.4GB的硬盘

小结

LARGE寻址模式把柱面数除以整数倍、磁头数乘以整数倍而得到的逻辑磁头/柱面/扇区参数进行寻址,所以表示的已不是硬盘中的物理位置,而是逻辑位置,但是这种计算方式显然比较粗糙使用比较少

LBA寻址模式是直接以扇区为单位进行寻址的,不再用磁头/柱面/扇区三种单位来进行寻址。

但为了保持与之前的CHS模式的兼容,通过逻辑变换算法,可以转换为磁头/柱面/扇区三种参数来表示,但表示的也和LARGE寻址模式一样已不是硬盘中的物理位置而是逻辑位置了。

参考资料

  1. 磁盘I/O那些事
  2. 为什么硬盘转速是5400或者7200这么奇怪的数字?7200转的硬盘一定比5400快吗?
  3. ## 求硬盘的3种寻址模式 NORMAL LBA LARGE 详细介绍
  4. # 硬盘基本知识(磁头、磁道、扇区、柱面)
  5. linux内存管理---虚拟地址、逻辑地址、线性地址、物理地址的区别
  6. linux - glibc 中的 mmap 实现 - 带有符号 mmap 的动态库 - 堆栈溢出 (stackoverflow.com)
相关文章
|
6月前
|
存储 缓存 固态存储
《Linux是怎么样工作的》读书笔记(一)
《Linux是怎么样工作的》读书笔记
131 0
《Linux是怎么样工作的》读书笔记(一)
|
消息中间件 存储 网络协议
《硬核linux攻略》读书笔记更新中
《硬核linux攻略》读书笔记更新中
|
Shell Linux Perl
《Linux Shell脚本攻略》读书笔记
《Linux Shell脚本攻略》读书笔记
216 0
|
存储 Shell Linux
《Linux命令行与shell脚本编程大全》读书笔记————第一章 初识Linux shell
本章内容 1、什么是Linux 2、Linux内核的组成   1、1 什么是Linux Linux课划分为以下四部分 a)Linux内核 b)GNU工具 c)图形化桌面环境 d)应用软件   1.1.1 深入探究Linux内核 内核主要负责以下四种功能 a)系统内存管理 b)软件程序管理 c)硬件设备管理 d)文件系统管理   1、系统内存管理 内核不仅管理服务器上的可用内存,还可以创建和管理虚拟内存(即实际上不存在的内存)。
1285 0
|
Linux 机器学习/深度学习 安全
|
关系型数据库 Linux
《Linux就该这么学》读书笔记
cat /proc/cpuinfo lsmod 安装VNC服务 重置root密码: image.png RPM操作: image.png 日期: image.
1290 0
《Linux内核设计与实现》读书笔记 - 目录 (完结)
《Linux内核设计与实现》读书笔记 - 目录 (完结) 读完这本书回过头才发现, 第一篇笔记居然是 2012年8月发的, 将近一年半的时间才看完这本书(汗!!!).