# Linux 学习
# Kernel空间和用户空间的概念
# 内核空间和用户空间
32 位操作系统,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程32 位操作系统,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G(也就是Windows XP在不安装补丁的时候只能使用4G内存)。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的。
操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。
具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
对上面这段内容我们可以这样理解:
「每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。」
# 内存地址映射过程
x86 CPU采用了段页式地址映射模型。进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存。段页式机制如下图。
页虚拟内存
页式虚拟内存,是将程序的内存空间分成一个个的页,每个页的大小相等(如1kB、2kB、4kB等)。而对应的物理内存空间同样的按照同样大小的页来组织起来。进程页可以直接映射到物理内存页上。
页式内存地址表示: 虚拟内存地址(VA) = 页面基地址(BA)+ 页内偏移(S)= 页号(P) * 页大小(pSize) + 页内偏移(S)
# Linux内核高端内存
Linux将内核地址空间划分为三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高端内存HIGH_MEM地址空间范围为0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?
当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。如下图。
例如内核想访问2G开始的一段大小为1MB的物理内存,即物理地址范围为0x80000000 ~ 0x800FFFFF。访问之前先找到一段1MB大小的空闲地址空间,假设找到的空闲地址空间为0xF8700000 ~ 0xF87FFFFF,用这1MB的逻辑地址空间映射到物理地址空间0x80000000 ~ 0x800FFFFF的内存。映射关系如下:
逻辑地址 | 物理内存地址 |
---|---|
0xF8700000 | 0x80000000 |
0xF8700001 | 0x80000001 |
0xF8700002 | 0x80000002 |
… | … |
0xF87FFFFF | 0x800FFFFF |
当内核访问完0x80000000 ~ 0x800FFFFF物理内存后,就将0xF8700000 ~ 0xF87FFFFF内核线性空间释放。这样其他进程或代码也可以使用0xF8700000 ~ 0xF87FFFFF这段地址访问其他物理内存。
从上面的描述,我们可以知道高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。
看到这里,不禁有人会问:万一有内核进程或模块一直占用某段逻辑地址空间不释放,怎么办?若真的出现的这种情况,则内核的高端内存地址空间越来越紧张,若都被占用不释放,则没有建立映射到物理内存都无法访问了。
在香港尖沙咀有些写字楼,洗手间很少且有门锁的。客户要去洗手间的话,可以向前台拿钥匙,方便完后,把钥匙归还到前台。这样虽然只有一个洗手间,但可以满足所有客户去洗手间的需求。要是某个客户一直占用洗手间、钥匙不归还,那么其他客户都无法上洗手间了。Linux内核高端内存管理的思想类似。
Linux内核高端内存的划分 内核将高端内存划分为3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
对于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行(为什么?想想 MMU 是如何访问物理内存的),也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。
对应高端内存的3部分,高端内存映射有三种方式: 映射到”内核动态映射空间”(noncontiguous memory allocation) 这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间”中。
持久内核映射(permanent kernel mapping) 如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间? 内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。
临时映射(temporary kernel mapping) 内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。
这块空间具有如下特点: (1)每个 CPU 占用一块空间 (2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。
当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现临时映射。
Linux高端内存的由来
当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,如逻辑地址0xc0000003对应的物理地址为0x3,0xc0000004对应的物理地址为0x4,… …,逻辑地址与物理地址对应的关系为
逻辑地址 | 物理内存地址 |
---|---|
0xc0000000 | 0x0 |
0xc0000001 | 0x1 |
0xc0000002 | 0x2 |
0xc0000003 | 0x3 |
… | … |
0xe0000000 | 0x20000000 |
… | … |
0xffffffff | 0x40000000 ?? |
假设按照上述简单的地址映射关系,那么内核逻辑地址空间访问为0xc0000000 ~ 0xffffffff,那么对应的物理内存范围就为0x0 ~ 0x40000000,即只能访问1G物理内存。若机器中安装8G物理内存,那么内核就只能访问前1G物理内存,后面7G物理内存将会无法访问,因为内核的地址空间已经全部映射到物理内存地址范围0x0 ~ 0x40000000。即使安装了8G物理内存,那么物理地址为0x40000001的内存,内核该怎么去访问呢?代码中必须要有内存逻辑地址的,0xc0000000 ~ 0xffffffff的地址空间已经被用完了,所以无法访问物理地址0x40000000以后的内存。
显然不能将内核地址空间0xc0000000 ~ 0xfffffff全部用来简单的地址映射。因此x86架构中将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存,这就是内存高端内存概念的由来。
在x86结构中,三种类型的区域如下:
ZONE_DMA 内存开始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~ 结束
物理地址 = 逻辑地址 – 0xC0000000
# 为什么需要区分内核空间与用户空间
在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。
比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。
当进程运行在 Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态。
# 内核态与用户态
内核态、用户态:「当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。」
在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。
对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。
所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。
如何从用户空间到内核空间
在操作系统的所有资源操作都是在内核空间中完成的,用户无法直接进行操作,但是可以通过内核提供的接口完成这样的操作。
有三种方式从用户态进入到内核态:系统调用、软中断和硬件中断
。
Linux 系统的结构。它大体可以分为三个部分,从下往上依次为:硬件 -> 内核空间 -> 用户空间
在硬件之上,内核空间中的代码控制了硬件资源的使用权,用户空间中的代码只有通过内核暴露的系统调用接口(System Call Interface)才能使用到系统中的硬件资源。其实,不光是 Linux,Windows 操作系统的设计也是大同小异。
实际上我们可以将每个处理器在任何指定时间点上的活动概括为下列三者之一:
- 运行于用户空间,执行用户进程。
- 运行于内核空间,处于进程上下文,代表某个特定的进程执行。
- 运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。
以上三点几乎包括所有的情况,比如当 CPU 空闲时,内核就运行一个空进程,处于进程上下文,但运行在内核空间。
说明:Linux 系统的中断服务程序不在进程的上下文中执行,它们在一个与所有进程都无关的、专门的中断上下文中执行。
之所以存在一个专门的执行环境,就是为了保证中断服务程序能够在第一时间响应和处理中断请求,然后快速地退出。
[^15]:
# 进程、线程、协程的概念
# 进程
进程是资源分配的基本单位。
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
进程通常由程序, 数据集和进程控制块三部分组成. ^4
- 程序: 描述进程需要完成的功能以及如何去完成
- 数据集: 程序执行过程中需要使用的资源(包括IO资源和基本数据)
- 进程控制块: 记录进程的外部特征以及描述其执行过程. 操作系统正是通过它来控制和管理进程
# 线程
线程是独立调度的基本单位。 一个进程中可以有多个线程,它们共享进程资源
- 超线程 在每一个CPU内部将重要的缓存器(register)分成两群,而让程序分别使用这两群缓存器,也就是说,可以有两个程序【同时竞争CPU的运算单元】,而不是让操作系统切换(鸟哥的Linux-P13)
# 协程
协程则是由编程语言所支持的在用户空间内的多任务,简单的说就是同一个线程中,系统维护一个程序列表,某个程序阻塞了,如果有其它程序等待执行,则不切线程而直接恢复那个程序的执行上下文。但各语言实现协程的机制是不同的,go中的协程其实就是轻量化的线程,go只是尽量在一个线程中复用协程,但如果某个线程阻塞了,go会在其它线程中调度其它协程来执行。而python中的协程就是在主线程中复用的,也就是说,如果python中的某协程如果卡了,那么其它协程全部挂。^5
# 异同
- 拥有资源 进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
- 调度 线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
- 通信方面 线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
# 芯片指令集概念
RISC-V(读作“RISC-FIVE”)是一个基于精简指令集(RISC)原则的开源指令集架构(ISA)
CISC Reduced Instruction Set Computer,中文是精简指令集。特点是所有指令的格式都是一致的,所有指令的指令周期也是相同的,并且采用流水线技术。
CPU执行计算任务时都需要遵从一定的规范,程序在被执行前都需要先翻译为CPU可以理解的语言。这种规范或语言就是指令集(ISA,Instruction Set Architecture)。程序被按照某种指令集的规范翻译为CPU可识别的底层代码的过程叫做编译(compile)。 x86、ARM v8、MIPS都是指令集的代号。指令集可以被扩展,如x86增加64位支持就有了x86-64。厂商开发兼容某种指令集的CPU需要指令集专利持有者授权,典型例子如Intel授权AMD,使后者可以开发兼容x86指令集的CPU。微架构的概念
CPU的基本组成单元即为核心(core)。多个核心可以同时执行多件计算任务,前提是这些任务没有先后顺序。
核心的实现方式被称为微架构(microarchitecture)。微架构的设计影响核心可以达到的最高频率、核心在一定频率下能执行的运算量、一定工艺水平下核心的能耗水平等等。
- 多媒体微指令集 MMX,SSE(单指令多数据流扩展),SSE2,SSE3,SSE4,AMD-3DNOW
- 虚拟化微指令集 Intel-VT AMD-SVM
- 省电功能 Intel-SpeedStep,AMD-PowerNow
- 64/32位兼容技术 AMD-AMD64,Intel-EM64T
# 各种Linux发布版本
在 1994 年终于完成的 Linux 的核心正式版! version 1.0 。 这一版同时还加入 了 X Window System 的支持呢!更于 1996 年完成了 2.0 版。[^9]POSIX规范
Linux 的核心原型是 1991 年由托瓦兹 (Linus Torvalds) 写出来的 1960: 年代初期麻省理工学院 (MIT) 发展了所谓的: 『兼容分时系统( Compatible Time-Sharing System, CTSS )』 1969: Thompson 的这个档案系统有两个重要的概念,分别是: o 所有的程序或系统装置都是档案 o 不管建构编辑器还是附属档案,所写的程序只有一个目的,且要有效的完成目标。 1973: unix正式版 1977 : 重要的 Unix 分支: BSD 的诞生 1979: 一个措手不及的版权宣告 1984 年之一: x86 架构的 Minix 诞生 1984 年之二: GNU 与 FSF 计划的成立 1988 年: 图形接口 XFree86 计划
Linux发行版的地址 • Red Hat: http://www.redhat.com • Fedora: http://fedora.redhat.com • Mandriva: http://www.mandriva.com • Novell SuSE: http://www.novell.com/linux/suse/ • Debian: http://www.debian.org/ • Slackware: http://www.slackware.com/ • Linpus: http://www.linpus.com.tw/ • Gentoo: http://www.gentoo.org/ • Ubuntu: http://www.ubuntulinux.org/ • CentOS: http://www.centos.org/
# 宏内核与微内核的区别
# 宏内核
宏内核是将操作系统功能作为一个紧密结合的整体放到内核。
由于各模块共享信息,因此有很高的性能。
# 微内核
由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。
因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。
Linux采用的是宏内核,但是它也区分内核空间和用户空间
# 中断分类
# 外中断
由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
# 异常
由CPU执行指令的内部事件引起,如非法操作码,地址越界,算术溢出
# 陷入
在用户程序中使用系统调用
# CI/CD
尝试了使用Jenkins
持续集成(Continuous Integration ,CI)
在传统软件开发过程中,集成通常发生在每个人都完成了各自的工作之后。在项目尾声阶段,通常集成还要痛苦的花费数周或者数月的时间来完成。持续集成是一个将集成提前至开发周期的早期阶段的实践方式,让构建、测试和集成代码更经常反复地发生。
开发人员通常使用一种叫做CI Server 的工具来做构建和集成。持续集成要求史蒂夫和安妮能够自测代码。分别测试各自代码来保证它能够正常工作,这些测试通常被称为单元测试(Unit tests)。
代码集成以后,当所有的单元测试通过,史蒂夫和安妮就得到了一个绿色构建(green build)。这表明他们已经成功地集成在一起,代码正按照测试预期地在工作。然而,尽管集成代码能够成功地一起工作了,它仍未为生产做好准备,因为它没有在类似生产的环境中测试和工作。在下面持续交付部分你可以了解到持续集成后面发生了什么。
持续交付(Continuous Delivery)
Continuous Delivery (CD) 持续交付是持续集成的延伸,将集成后的代码部署到类生产环境,确保可以以可持续的方式快速向客户发布新的更改。如果代码没有问题,可以继续手工部署到生产环境中
。
持续部署(Continuous Deploy)
如果真的想获得持续交付的好处,应该尽早部署到生产环境,以确保可以小批次发布,在发生问题时可以轻松排除故障。于是有了持续部署。
我们通常将这个在不同环境发布和测试的过程叫做部署流水线
持续部署是在持续交付的基础上,把部署到生产环境的过程自动化。
2
# DevOps
DevOps是一种思想、一组最佳实践、以及一种文化
。DevOps落地实施,从组织架构、设计人员、流程、人员分工、人员技能到工具,变化很大,要求很高,完全颠覆了现有的开发运维模式,建设风险很高。
DevOps是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。通过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。具体来说,就是在软件交付和部署过程中提高沟通与协作的效率,旨在更快、更可靠的的发布更高质量的产品。
也就是说DevOps是一组过程和方法的统称,并不指代某一特定的软件工具或软件工具组合。各种工具软件或软件组合可以实现DevOps的概念方法。其本质是一整套的方法论,而不是指某种或某些工具集合,与软件开发中设计到的OOP
、AOP
、IOC
(或DI)等类似,是一种理论或过程或方法的抽象或代称。
# CI、CD、DevOps关系
概念性的内容,每个人的理解都有所不同。就好比CGI
这个词,即可以理解成CGI这种协议,也可以理解成实现了CGI协议的软件工具,都没有问题,咬文嚼字过犹不及。留下一图:
# 分布式部署
# 参考链接
[^9]: 可移植操作系统接口 POSIX
[^15]: DevOps与CICD的区别 及 docker、k8s的CICD思路
[^18]: 存储管理之页式、段式、段页式存储 以及 优缺点