第二版:展开在第四个约定的操作系统
操作系统部分的展开思路主要是作为程序员,该如何避免操作系统的系统资源代价,尽量避开因操作系统而产生的多余性能消耗,当然,这也是串联操作系统的过程中进行的。
一切的开始:一段编程完成的C++代码
从IDE中编写完成的代码,实质上还只是一串字符串,当然也可以看成一个CPP后缀的文件,里面放着字符串。
这段代码,是需要根据约定去编写的,这种约定便是语言的规则(编写规则),各种约定会伴随着这段程序的诞生到销毁。
值得一提的是,在多文件编写的时候(这在C++中我认为是无法避免的),一开始也只有main文件参与最初的编译,但是会提前经过预处理来将使用的头文件包含在一个文件中。
注:也可能不是main文件,这个可以自己在IDE中自己决定,但是主程序入口唯一是确定的(命名可以改名,但是入口一定是唯一的)。
第一个约定:编程语法
我们在C++中编写的程序,需要遵循他自己的一套语法(编写规则),这种语法一般体现了语言的特点,诸如是否对缩减敏感(PYTHON),垃圾回收(JAVA),一个语句结束的标志是什么(C++是“;”),关键字,语言是否简洁、紧凑,使用方便、灵活,是否拥有丰富的运算符等等。
一个编程语言通常需要具备以下六个特征和特点:
词法:语言中的单词或符号必须遵守一定的规则,称为词法。例如,一个程序中的关键字、标识符、运算符、分隔符等,都需要遵守语言的词法规则。不同语言的词法规则可能不同。
语法:语言中的单词或符号的组合必须遵守一定的规则,称为语法。例如,一个程序中的语句、表达式、函数定义等,都需要遵守语言的语法规则。不同语言的语法规则可能不同。
语义:语言中的单词或符号的含义必须遵守一定的规则,称为语义。例如,一个程序中的变量、函数、对象等,都需要遵守语言的语义规则。不同语言的语义规则可能不同。
数据类型:语言中需要定义各种数据类型,例如整数、浮点数、字符、字符串、数组、结构体、类等。不同语言的数据类型可能不同。
控制结构:语言中需要提供各种控制结构,例如条件语句、循环语句、跳转语句等。不同语言的控制结构可能不同。
函数和模块:语言中需要提供函数和模块等组织程序的机制。不同语言的函数和模块机制可能不同。
不同的编程语言之间,上述特征和特点可能存在差异。例如,C++语言中的语法和语义与JAVA语言有所不同,PYTHON语言的语法和数据类型与C++语言也有所不同。因此,我们可以通过了解不同编程语言的语法和特点,来区分它们之间的差异。
这些不同的差异便是不同的语言约定,不同的约定体现了语言的特点,也使得语言可以拥有百花齐放的特点,毕竟语法,词法,语义的创造是极其主观且自由的。
我们甚至可以把C++的关键词全部改为 a,b,c,d,e,f,g这种来替换int,float,class,double,long,char等,来创建一个新语言(哪怕极其抽象)。
第二个约定:编译器的分析(词法/语法/语义)
在介绍第二个约定之前,我们需要先了解一下编译器的分析:
词法分析
词法分析是编译器的第一个阶段,也被称为扫描。其主要任务是将源代码分解为一个个单词,例如变量名、常量、操作符、分隔符等。在这个阶段,编译器会跳过空格、注释和其他无意义的字符。词法分析器会将代码中的每个单词转换为一种对应的标记或词法单元,这些词法单元会被传递给下一个阶段进行分析。
作用:去除多余字符,将源代码转换为词法单元序列,为后续的语法分析和语义分析提供输入。
语法分析
语法分析是编译器的第二个阶段,也被称为解析。其主要任务是根据编程语言的语法规则检查词法单元的序列,并将其组合成可以被编译器理解的语法结构,例如表达式、语句和程序块。语法分析器会根据语法规则来检查代码的正确性,并生成一棵语法树或语法图,这个结构描述了代码的结构和含义。
作用:根据编程语言的语法规则检查词法单元的序列,并将其组合成可以被编译器理解的语法结构。根据语法规则来检查代码的正确性,并生成一棵语法树描述代码的结构和含义。
语义分析
语义分析是编译器的第三个阶段,其主要任务是检查代码的含义,以确保代码在语法上正确并且具有合理的含义。在这个阶段,编译器会检查变量和函数的使用是否符合语言规范,类型是否匹配,以及其他语义错误。例如,语义分析器会检查变量是否已经声明,函数是否被正确地调用,并检查类型转换是否正确。如果发现错误,编译器会产生一个错误信息并停止编译过程。
作用:检查代码的含义,以确保代码在语义上正确并且具有合理的含义。语义分析器会在语法分析的基础上进一步检查代码的正确性,包括变量和函数的使用是否符合语言规范,类型是否匹配等等。
值得一提的是,在我们在IDE中编写代码的过程中,其纠错与提示便是基于词法/语法/语义分析进行的。(也就是说,词法/语法/语义分析并不是只在编译期才存在,因情况而决定)
在我们完成与语言编写规则的约定后,语言也会和我们的编译器来形成约定,来告诉编译器该如何将这些语言转换为语法树,进而转换为中间码。
每一个语言的转换步骤都不尽相同,因为对一个语句而言,可以有很多种的解释方法,也就是很多种的分析方法,也就是每种语言都拥有自己独特的语法树。
但是我们有了语言的约定了,我们便会知晓其语法,进而可以得到语法树。
得到语法树后,我们便可以利用其生成中间码(见下一节)。
注:在编译期间C++会将内联函数,模板实例化,宏定义,常量表达式计算等完成处理
第三个约定:CPU与中间码
指令集与约定
编译器在完成了与语言的约定后,便会进行到这个步骤,这个步骤是因为市面上会存在很多的CPU类型(主要是指令集的不同和位数的不同:64/32):
x86 指令集:是 Intel 公司为其第一块 16 位 CPU(i8086)专门开发的指令集。后来,IBM 公司推出的第一台个人电脑中的 CPU —— i8088(i8086 简化版)也使用了 x86 指令集,成为了个人电脑的标准平台。大多数个人电脑都使用 x86 指令集。
ARM 指令集:是一种 RISC(Reduced Instruction Set Computing,精简指令集)结构的 CPU 指令集,由 ARM 公司开发。它的特点是指令数量较少、指令长度固定、寄存器使用灵活,适合用于嵌入式系统和移动设备。
MIPS 指令集:也是一种 RISC 结构的 CPU 指令集,由 MIPS Technologies 公司开发。它的特点是指令数量少、指令格式规整、执行速度快,广泛应用于网络设备、数字家电等领域。
PowerPC 指令集:是 IBM 公司和 Motorola 公司联合开发的 RISC 结构的 CPU 指令集,适用于高性能计算机和服务器等领域。
SPARC 指令集:是一种 RISC 结构的 CPU 指令集,由 Sun 公司开发。它的特点是指令数量少、指令长度固定、寄存器使用灵活,适合用于高性能计算机和服务器等领域。
AVR 指令集:是一种 RISC 结构的 CPU 指令集,由 Atmel 公司开发,适用于嵌入式系统和控制领域。
PIC 指令集:是一种 RISC 结构的 CPU 指令集,由 Microchip Technology 公司开发,适用于嵌入式系统和控制领域。
Intel 8051 指令集:是一种 8 位 CPU 指令集,由英特尔公司开发,适用于嵌入式系统和控制领域。
Xtensa 指令集:是一种可配置的 RISC 结构的 CPU 指令集,由 Cadence Design Systems 公司开发,适用于数字信号处理、音频、视频等领域。
RISC-V 指令集:是一种开放式的 RISC 结构的 CPU 指令集,由加州大学伯克利分校开发,适用于嵌入式系统、物联网、云计算等领域。
这些 CPU 指令集都有各自的特点和优势,所以往往会需要根据具体需求来选择适合的指令集,以获得更好的性能和效率。(这便造成了CPU的多种多样,而且程序也不一定是在CPU上,也可能是一个特制的计算芯片,也就是说,程序的运行平台十分繁杂庞大)
这么多的指令集,且支持的指令大不相同,我们不可能一一去做适配,这个工作量太大了,所以我们需要一个约定,一个CPU指令集与中间码之间的约定。
编译器需要将中间码根据不同的CPU来进行特定的转换,从而得到最终的汇编语言/机器码。
这种约定体现在最终产生汇编语言之上:
汇编语言是用人类看得懂的语言来描述指令集,不同的CPU支持的指令大不相同,所以不同CPU的汇编语言便会产出不同,无法统一进行翻译。
经过约定,我们的中间码可以根据不同的CPU产生不同的汇编语言。
编译的后端:机器码的诞生
现在我们的字符串程序已经根据不同的CPU的不同来翻译为特定的汇编语言了,也就是说,我们得到了目标代码,不过这种代码不一定是可运行的,比如C++,这时候便需要进行链接,将库与依赖导入,这种导入可以将库与依赖的目标代码导入已有的目标代码中。
链接有动态链接与静态链接的区别:
动态链接是指将库文件中的代码在程序运行时载入内存,并链接到程序中。在动态链接的过程中,编译器仅将程序中用到的库函数的引用存储在可执行文件中,当程序运行时,操作系统会根据需要从库文件中载入所需的代码,然后链接到程序中,从而生成一个可执行文件。这个过程会使得可执行文件变得较大,但也能够使程序的启动速度更快,因为所有代码都在一个文件中,不需要再进行额外的加载和链接。
静态链接是指将目标代码文件和库文件直接合并成一个可执行文件。在静态链接的过程中,编译器将源代码编译生成目标代码,然后将目标代码中需要用到的库函数的代码复制到可执行文件中,从而生成一个包含所有代码的可执行文件。这个过程会使得可执行文件变得较大,但也能够使程序的启动速度更快,因为所有代码都在一个文件中,不需要再进行额外的加载和链接。
在编译的后端(经过分析阶段),会经过汇编器和链接器来对目标代码进行处理,它们都属于将源代码转换为可执行代码的重要组成部分。汇编器将汇编语言代码转换为机器语言代码,并生成一个可重定位的目标文件,而链接器则将多个可重定位目标文件合并为一个可执行的二进制程序或库文件。两者协同工作,将编译器生成的中间代码转换为可执行的目标代码。
我们先了解一下汇编器与链接器:
汇编器是将汇编语言代码转换为机器语言代码的程序,也是编译器的一部分。汇编器的主要任务是将汇编语言源代码转换为二进制目标文件,以供链接器进行链接,最终生成可执行文件。汇编器的处理过程通常包括以下几个步骤:
词法分析:汇编器会将汇编语言源代码分解为一个个单词,例如指令、操作数、标签等。
语法分析:汇编器会根据汇编语言的语法规则检查词法单元的序列,并将其组合成可以被汇编器理解的汇编结构,例如指令、数据等。
代码生成:汇编器会将汇编结构转换为机器语言代码,并生成二进制目标文件。
汇编器的处理过程与编译器的处理过程类似,都是将源代码转换为目标代码的过程。汇编器的主要作用是将汇编语言代码转换为二进制目标文件,以供链接器进行链接。在这个过程中,汇编器会将汇编语言指令转换为机器语言指令,并生成符号表、重定位表等数据结构,以便链接器进行链接。汇编器的输出通常是二进制目标文件。
链接器是将多个可重定位目标文件合并为一个可执行文件或库文件的程序。在编译器生成可重定位目标文件之后,链接器将会把多个目标文件进行链接,生成一个可执行的二进制程序或库文件。链接器的主要作用是将多个可重定位目标文件合并为一个可执行文件或库文件,并对目标文件进行符号解析和重定位等处理。主要有以下几个任务:
符号解析:将每个符号的定义和引用联系起来。链接器通过符号表来查找符号的定义和引用,以确定每个符号的地址。
重定位:将每个符号定义与存储器中的一个具体位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器的位置,从而重定位这些节。
符号合并:将多个目标文件中相同的符号合并为一个,从而避免重复定义的问题。
生成可执行文件或库文件:最终将目标文件链接成可执行文件或库文件。
在链接器处理目标文件时,会根据不同的操作系统和编译器的规范进行处理。例如,在Windows操作系统上,常用的链接器是Microsoft Visual C++的link.exe,而在Linux和UNIX操作系统上,常用的链接器是GNU的ld。不同的链接器可能会有不同的处理过程和规范,但总的来说,它们都需要完成上述几个任务。
总的来说,C++的链接期是将编译器编译生成的目标文件链接成最终的可执行文件或库文件的过程。链接器需要完成符号解析、重定位、符号合并和生成可执行文件或库文件等任务。不同的操作系统和编译器可能会有不同的链接器,但它们都需要遵循一定的规范进行处理。
汇编器和链接器是编译器的两个重要组成部分,汇编器和链接器都是编译器的后端阶段,它们都属于将源代码转换为可执行代码的重要组成部分。汇编器将汇编语言代码转换为机器语言代码,并生成一个可重定位的目标文件,而链接器则将多个可重定位目标文件合并为一个可执行的二进制程序或库文件。两者协同工作,将编译器生成的中间代码转换为可执行的目标代码。
第四个约定:程序与操作系统
这里,我们的程序在准备开始运行时的前提会产生分歧:
一种是在小型的,嵌入式的CPU上执行的程序,小而精,没有操作系统。
一种是在个人主机(此处不考虑分布式,超大型主机)上运行程序,拥有操作系统。
有无操作系统,程序在执行过程中的很多操作都会有不同:
内存管理:在操作系统下,程序需要经过内存管理单元的管理,才能使用内存资源。在没有操作系统的情况下,程序需要自己管理内存,手动分配和释放内存空间。
外设访问:在操作系统下,程序需要通过操作系统提供的API接口来访问外设资源,如磁盘、网络等。在没有操作系统的情况下,程序需要自己编写底层驱动程序,直接访问硬件资源。
进程管理:在操作系统下,程序运行在进程的上下文中,由操作系统进行调度和管理。在没有操作系统的情况下,程序需要自己管理进程,包括创建、调度、销毁等操作。
其他还有多线程(没有操作系统,便不会有上下文切换的代价),安全性等问题。
如此,程序在执行过程中,也会有不同的,需要遵循的约定,主要在操作系统上:
上下文切换(用户态与内核态之间的互相转换)
进程的阻塞(单核的,简单的CPU不会产生这种问题)
进程管理(时间片轮转,先来先服务,最短作业优先,优先级)
CPU调度(先来先服务,最高响应比优先,时间片轮转)
进程管理主要涉及进程的创建、撤销、阻塞、唤醒、同步和通信等方面。
CPU调度是对CPU资源的管理过程,主要涉及对进程调度和切换的控制和管理。
两者十分相似,区别在于,进程管理是对进程的管理和控制,而CPU调度是对CPU资源的管理和调度。进程管理是CPU调度的基础,CPU调度是进程管理的延伸。二者之间的关系是相互依存的。
操作系统-系统的启动
BIOS自检阶段
BIOS自检阶段是计算机开机后的第一个阶段,其作用是检查计算机硬件是否正常:
加电自检:计算机开机后,BIOS会进行加电自检,检查计算机的硬件是否正常。检查的内容包括CPU、内存、硬盘、显卡、键盘、鼠标等硬件设备。
初始化设置:对设备的一些参数进行初始化设置,例如显示器分辨率、声音设置、网络连接等。
加电自检可以确保设备在启动时能够正常运行,并及时发现可能存在的问题,减少系统故障的发生。
初始化CMOS是一种存储器,用于存储计算机的基本配置信息。BIOS会读取CMOS存储器中的信息,以确定计算机的配置参数。
显示设备检查:BIOS会检查计算机的显示设备,包括显卡和显示器。检查显示设备的目的是确保显示设备能够正常工作。
启动设备检查:BIOS会检查计算机的启动设备,确定启动设备的顺序。启动设备可以是硬盘、光盘、USB设备等。
硬件设备初始化:BIOS会对硬件设备进行初始化和配置,包括PCI、USB、SATA等接口设备。设备初始化的目的是确保设备能够正常工作。
POST报告:在完成自检后,BIOS会将自检结果报告给用户。如果检查到硬件故障,BIOS会发出警报声或显示错误信息。
BIOS自检阶段是计算机开机后的第一个阶段,其作用是检查计算机硬件是否正常。BIOS会进行加电自检、初始化CMOS、显示设备检查、启动设备检查、硬件设备初始化和POST报告等操作。这些操作的完成,保证了计算机硬件的正常工作,为操作系统的启动奠定了基础。
启动加载器阶段
启动加载器阶段是操作系统启动过程的第二个阶段,其主要作用是加载操作系统内核。启动加载器(bootloader)是一个小程序,通常存储在硬盘的引导扇区中。
计算机启动时,BIOS会读取硬盘的引导扇区,将启动加载器加载到内存中。
启动加载器通常位于硬盘的第一个扇区,大小通常为512字节。由于这个扇区的位置和大小是固定的,因此启动加载器需要非常精简和高效。(具体需要看主板上的BIOS,且其一般为ROM,无法修改)
启动加载器会执行一些简单的初始化操作,例如建立中断向量表、设置堆栈等。
启动加载器会读取操作系统内核的映像文件,将其加载到内存中。操作系统内核通常存储在硬盘的特定位置,例如Linux内核通常存储在/boot目录下。
启动加载器会将控制权转交给操作系统内核,将系统的控制权转移给操作系统。
启动加载器阶段是操作系统启动过程的第二个阶段,其主要作用是加载操作系统内核。启动加载器通常存储在硬盘的引导扇区中,大小非常精简和高效,可以将操作系统内核加载到内存中,并将系统的控制权转移给操作系统。
操作系统内核启动阶段
操作系统内核启动阶段是操作系统启动过程的第三个阶段,其主要作用是对计算机进行初始化和配置,建立内核数据结构,启动各种系统服务和设备驱动程序,为用户提供各种服务:
系统初始化:操作系统内核会对计算机进行初始化和配置,包括建立内核数据结构、初始化硬件设备、分配内存空间等。内核数据结构包括进程表、内存管理表、设备驱动表等,用于管理系统资源。
设备驱动程序初始化:操作系统内核会启动各种系统服务和设备驱动程序,包括网络服务、文件系统服务、用户界面服务等。设备驱动程序用于控制硬件设备,如显卡、声卡、网卡等。
内存管理初始化:操作系统内核会初始化内存管理系统,包括建立内存分页表、虚拟内存系统等。内存管理系统负责管理系统内存资源,包括内存分配、内存回收等操作。
进程管理初始化:操作系统内核会初始化进程管理系统,包括建立进程表、进程调度算法、进程通信机制等。进程管理系统负责管理系统中的各个进程,控制进程的创建、撤销和调度等操作。
文件系统初始化:操作系统内核会初始化文件系统,包括建立文件目录、文件索引表等。文件系统负责管理系统中的各种文件,包括文件的存储、读取和删除等操作。
用户界面初始化:操作系统内核会初始化用户界面系统,包括建立窗口管理器、图形用户界面等。用户界面系统负责为用户提供友好的界面和操作方式。
操作系统内核启动阶段是操作系统启动过程的第三个阶段,其主要作用是对计算机进行初始化和配置,建立内核数据结构,启动各种系统服务和设备驱动程序,为用户提供各种服务。内核启动阶段的完成,为用户提供了基础的计算机资源管理和服务支持。
用户登录阶段
用户登录阶段是操作系统启动过程的第四个阶段,其主要作用是验证用户身份,加载用户的配置文件和环境变量等信息:
显示登录界面:操作系统会在显示器上显示登录界面,包括用户名和密码输入框、登录按钮等。
用户输入账号和密码:用户需要在登录界面中输入正确的用户名和密码,以验证身份。
用户身份验证:操作系统会对用户输入的账号和密码进行验证。如果验证成功,系统会加载用户的配置文件和环境变量等信息。
用户配置文件加载:用户的配置文件包括用户的主目录、桌面图标、用户偏好设置等信息。操作系统会加载用户的配置文件,以提供个性化的用户环境。
环境变量加载:环境变量是一组键值对,用于存储系统的配置信息和用户的个性化设置。操作系统会加载用户的环境变量,以提供系统服务和应用程序的运行。
用户进程创建:用户登录成功后,操作系统会为其创建一个独立的用户进程,加载用户环境和配置文件,并启动用户应用程序和服务进程。用户可以在操作系统提供的环境中进行各种操作和应用程序开发。
用户登录阶段是操作系统启动过程的第四个阶段,其主要作用是验证用户身份,加载用户的配置文件和环境变量等信息。用户登录成功后,操作系统会为其创建一个独立的用户进程,加载用户环境和配置文件,并启动用户应用程序和服务进程。
用户环境初始化阶段
户环境初始化阶段是操作系统启动过程的第五个阶段,其主要作用是为用户提供一个友好、高效的工作环境,以方便用户进行各种操作和应用程序开发:
加载用户环境:操作系统会加载用户的环境,包括用户的配置文件、环境变量、桌面图标等。这些信息用于提供用户个性化的工作环境。
启动用户应用程序:操作系统会启动用户的应用程序,例如文本编辑器、浏览器、计算器等。这些应用程序用于用户进行各种操作和应用程序开发。
启动系统服务:操作系统会启动各种系统服务,例如网络服务、打印服务、安全服务等。这些服务为用户提供各种系统资源和服务支持。
启动用户服务进程:操作系统会启动用户服务进程,例如数据库服务、Web服务、消息队列服务等。这些服务进程用于支持用户的应用程序开发和数据管理。
提供用户交互界面:操作系统会提供各种用户交互界面,例如图形用户界面、命令行界面等。这些界面用于用户与操作系统进行交互和操作。
用户环境初始化阶段是操作系统启动过程的第五个阶段,其主要作用是为用户提供一个友好、高效的工作环境,以方便用户进行各种操作和应用程序开发。操作系统会加载用户环境、启动用户应用程序和服务进程,并提供各种用户交互界面。
启动完成-小结
操作系统启动过程是计算机开机后操作系统从无到有的一个过程,主要包括BIOS自检、启动加载器、操作系统内核启动、用户登录和用户环境初始化等阶段。在此过程中,BIOS负责计算机硬件的自检和启动加载器的加载,启动加载器负责加载操作系统内核,并将系统的控制权转交给内核,操作系统内核负责对计算机进行初始化和配置,建立内核数据结构,启动各种系统服务和设备驱动程序,为用户提供各种服务,用户登录和用户环境初始化则是为用户提供一个友好、高效的工作环境。操作系统启动过程是计算机启动的重要部分,其完成为操作系统和计算机的正常运行提供了基础。
系统的启动到此便结束,接下来便是操作系统的部分了,毕竟我们的程序便是由操作系统控制的,我们需要知道我们的程序是如何被控制的,在这些控制过程中产生了哪些操作,那些操作是可以避免的,是可以避免的系统资源消耗。
操作系统-内存管理
操作系统内存管理是操作系统的重要功能之一。其主要任务是对计算机系统中的内存进行有效的管理,包括内存分配、内存回收、内存保护等。内存管理的重点、特点和算法如下:
1.重点:
内存管理的重点是有效地利用计算机系统中的内存资源,尽可能地提高内存利用率,同时保证系统的安全和稳定。
2.特点:
(1)虚拟内存技术:操作系统通过虚拟内存技术将磁盘空间虚拟化为内存,从而扩大了系统内存的容量。
(2)内存保护:操作系统通过内存保护技术,保护系统的关键数据和程序不被非法访问和修改。
(3)内存分配:操作系统需要对内存进行分配和回收,保证内存的有效利用。
3.算法:
(1)分页式存储管理:将物理内存划分为若干个固定大小的物理块,将程序和数据也划分为若干个大小相等的逻辑块,称为页面。
(2)分段式存储管理:将程序和数据划分为若干个不同长度的逻辑块,称为段,每个段可以有不同的访问权限。
(3)段页式存储管理:将物理内存划分为若干个固定大小的物理块,每个物理块可以划分为若干个大小相等的逻辑块,即页,而程序和数据则划分为若干个不同长度的逻辑块,即段。该算法综合了分段式存储管理和分页式存储管理的优点。
总之,内存管理是操作系统的重要功能之一,其重点在于有效地利用内存资源,特点在于虚拟内存技术和内存保护技术,算法包括分页式存储管理、分段式存储管理和段页式存储管理等。(上述的算法其实都可以用链表实现,简单易懂,就是效率不算高)
现在我们来说一下现代的操作系统
现代操作系统实现内存管理主要包括物理内存管理和虚拟内存管理两个方面。物理内存管理主要涉及程序的装入、交换技术、连续分配管理和非连续分配管理,包括分页、分段和段页式等方式。而虚拟内存管理主要包括虚拟内存概念、请求分页管理方式、页面置换算法、页面分配策略、工作集和抖动等。
虚拟内存的实现是将虚拟地址空间分解成页,并将每一页映射到物理内存的某个页框或者解除映射。转换检测缓冲区(TLB)提供中间由虚拟地址转换为物理地址时的缓存,可以直接将虚拟地址映射到物理地址,加速分页过程。常用的页面置换算法包括FIFO、LRU、LFU、NUR和随机算法等,其中LRU算法是最常用的一种,基于页面的历史访问情况进行页面置换。
除此之外,操作系统还会将虚拟地址划分为内核空间和用户空间,用户进程只能访问用户空间的虚拟地址,只有通过系统调用、外设中断或异常才能访问内核空间[2]。同时,操作系统也会对内存节点进行分区,将节点分为DMA、Normal和High Memory内存区,以及采用多级页表和倒排页表等方式处理巨大的虚拟地址空间。
总的来说,现代操作系统采用多种策略和算法来实现内存管理,以提高系统的性能和稳定性。
操作系统-内存管理-分段与分页
值得注意的一点是,现代的操作系统总体来说,使用的分段+分页,但是巧妙的分段内存设计,相当于把分段的概念给屏蔽了:
某个程序(WINDOWS)的中断上下文段寄存器
cs: 001b
ds: 0023
ss: 0023
es: 0023
只有0x001b和0x0023两个值,这不是一个地址,而是一个段选择子,按照段选择子的格式展开来看这两个值指向的是哪个段描述符:
十六进制:001b
二进制:0000000000011 0 11 - 段序号:3 - 表类型:GDT - 特权级:Ring3
十六进制:0023
二进制:0000000000100 0 11 - 段序号:4 - 表类型:GDT - 特权级:Ring3
也就是说,cs段指向的是GDT中的第3个表项,其他三个寄存器指向的是GDT中的第4个表项。
接下来,我们来看一下这个神秘的GDT里面的内容到底是什么?很多人学了内存管理,可能还从来没看过真实的GDT里面到底是什么数据吧。
GDT是位于操作系统内核地址空间中的,在Windows上有两种查看方式,一种是通过Windbg,一种是通过一些ARK工具,我这里选择使用PChunter这个神器进行查看。
前面提到过,GDT中的表项是段描述符,这是一个比较复杂的数据格式,好在,这个神器对段描述符进行了解析,使用表格字段的方式进行了展示,让我们看起来轻松多了。
第3个表项和第4个表项,它们的基地址都是0x00000000。
再看它们的界限值,都是0x000FFFFF,界限的单位不是字节而是Page/页,把这个值乘以页面的大小4KB,就是0xFFFFF000。也就说这个段的上限到了0xFFFFF000这个页面,再把这一个页面的大小加进去,就是0xFFFFFFFF了!
所以,重点来了!看到了吗,GDT中的第3个和第4个表项所描述的这两个段,它们的基地址都是0x00000000,整个段的大小都是0xFFFFFFFF,这意味着什么?这意味着整个进程的地址空间实际上就是一个段!
也就是说:进程的代码段、数据段、栈段、扩展段这四个段全部重合了,而且是整个进程地址空间共计4GB成了一个段。
说起来是分段,实际上等于没分了,再加上段的基地址全部是0,那进行地址翻译的时候,有没有段都没什么区别了。
总结:操作系统实际上把段给架空了。
注:
不过不是所有的64 位所有段偏移都会被当做 0 对待, FS 和 GS 是例外。事实上 Linux x64 GCC 就经常用 FS 加上一个立即数偏移来访问内存中的 canary 值。
这里关于分段/分页的内容,来自:
知乎-现代操作系统内存管理到底是分段还是分页,段寄存器还有用吗?
https://zhuanlan.zhihu.com/p/409754117
内容相当具有深度。
操作系统-内存管理的代价
内存管理的代价较多:
内存空间:操作系统需要占用一定的内存空间来管理系统内存,包括内存分配、回收和页表管理等。这些内存空间会减少可供应用程序使用的内存空间,从而影响系统的性能和稳定性。
CPU 时间:操作系统需要消耗一定的CPU时间来进行内存管理。例如,当系统需要分配内存时,操作系统需要扫描内存空闲块列表来查找可用内存块,这会消耗一定的CPU时间。如果内存空闲块列表很长,这个操作就会变得非常耗时。
I/O 带宽:操作系统需要频繁地进行内存读写操作,例如将数据从内存写入硬盘或从硬盘读取到内存。这些操作会占用系统的I/O带宽,从而影响系统的整体性能。
系统稳定性:内存管理是操作系统的核心功能之一,如果操作系统在内存管理上出现问题,可能会导致系统崩溃或运行不稳定。因此,操作系统需要投入大量的资源来保证内存管理的正确性和稳定性。
但是其中我们可以进行优化的部分:
减少内存分配和释放次数:频繁的内存分配和释放会导致系统频繁调度内存,从而增加系统资源的代价。可以通过预先分配一定大小的内存空间,避免频繁的内存分配和释放。
使用内存池:内存池是一种预先分配一定大小的内存空间的技术,可以避免频繁的内存分配和释放。程序员可以使用内存池来管理程序中的内存分配和释放。
避免内存泄漏:内存泄漏是指程序中申请的内存没有被正确释放,导致系统中的内存资源被占用,最终导致系统崩溃。程序员需要注意内存泄漏问题,并及时释放申请的内存空间。
操作系统-进程间通信
现代操作系统提供了多种进程间通信的方式,下面介绍一些常见的进程间通信方法:
管道:管道是一种半双工的通信方式,数据只能单向流动。管道分为匿名管道和命名管道两种。匿名管道只能在具有亲缘关系的进程间使用,而命名管道可以在不同进程之间进行通信。管道的缺点是只能传递简单的字节流,不能传递复杂的数据类型和结构体等。
消息队列:消息队列是一种消息传递机制,可以实现异步通信,不同进程间通过消息队列发送消息,接收方从队列中获取消息并进行处理。消息队列是一种可靠的进程间通信方式,能够保证消息的可靠传输,但是消息队列需要操作系统提供支持。
共享内存:共享内存是一种将内存映射到多个进程的机制,多个进程可以直接访问同一块物理内存,从而实现高速数据交换。共享内存通常需要配合信号量或其他同步机制使用,以确保多个进程对共享内存的访问不会发生冲突。
信号量:信号量是一种计数器,可以用来同步多个进程对共享资源的访问。当一个进程需要访问共享资源时,先对信号量进行操作,若计数器大于0,则允许进程访问并将计数器减1,否则进程等待。信号量常用于进程同步和进程互斥等场景。
套接字:套接字是一种通用的进程间通信机制,可以在不同计算机之间进行通信。套接字通常用于网络通信,但也可以用于本地进程间通信。
信号(UNIX和LINUX):响应某些条件而产生的一个事件,接收到该信号的进程会相应地采取一些行动。通常信号是由一个错误产生的。但它们还可以作为进程间通信或修改行为的一种方式,明确地由一个进程发送给另一个进程。一个信号的产生叫生成,接收到一个信号叫捕获。
事件(WINDOWS):是一种同步对象,主要用于在进程间通信与线程同步,事件分为自动重置事件和手动重置事件两种类型,事件对象还有两种状态:未信号状态和信号状态。
自动重置事件:当一个线程或进程等待一个自动重置事件时,如果该事件处于未信号状态,那么该线程或进程会立即阻塞等待。当事件被信号后,操作系统会自动将事件重置为未信号状态,并唤醒等待该事件的线程或进程中的一个。此时,如果有多个线程或进程等待该事件,只有一个线程或进程会被唤醒。
手动重置事件:当一个线程或进程等待一个手动重置事件时,如果该事件处于未信号状态,那么该线程或进程会立即阻塞等待。当事件被信号后,操作系统不会自动将事件重置为未信号状态,需要手动调用函数将事件重置为未信号状态。此时,如果有多个线程或进程等待该事件,所有等待的线程或进程都会被唤醒。
未信号状态表示事件还没有被信号,等待线程或进程需要等待事件被信号后才能继续执行。信号状态表示事件已经被信号,等待线程或进程可以继续执行。
特点:
可以在同一台计算机或网络中的不同计算机上进行(部分)。
可以实现不同进程之间的数据共享和协作,提高计算机系统的效率和性能。
可以实现异步通信,提高系统的可靠性和稳定性。
进程间通信可以解决数据共享,进程间协作还有系统资源资源共享的问题,同时也产生一些新问题:
进程间同步问题:不同的进程执行速度可能不同,需要通过同步机制来保证数据一致性和任务协作的正确性。
进程间通信的效率问题:进程间通信需要进行数据拷贝和消息传递等操作,会占用系统资源和降低系统性能。
进程间安全问题:进程间通信可能会受到黑客攻击、数据窃取等安全问题的威胁,需要加强进程间通信的安全性和防护措施。
这些问题都是需要我们去权衡的,实际的问题需要根据情景去具体应用不同的进程间通信的方法。
操作系统-进程管理
操作系统实现进程管理的方式主要包括进程调度、进程同步和进程通信等方面。
进程调度:
进程调度是指操作系统在多个进程之间分配CPU资源的过程。操作系统会根据不同的调度算法,从就绪队列中选择一个合适的进程来运行。常见的调度算法包括先来先服务、最短作业优先、时间片轮转等。其中,时间片轮转是最常用的调度算法之一,它可以平衡不同进程之间的优先级,避免进程饥饿问题。
进程同步:
进程同步是指操作系统在多个进程之间协调和控制资源的访问,避免竞争条件和死锁等问题。常见的进程同步机制包括信号量、互斥锁、条件变量等。其中,信号量是最常用的进程同步机制之一,它可以保证多个进程之间对共享资源的访问是有序的,并避免了竞争条件和死锁等问题。
进程通信:
进程通信是指操作系统在多个进程之间传递信息和数据的过程。常见的进程通信机制包括管道、消息队列、共享内存、信号量等。其中,共享内存是最快的进程通信方式之一,它可以将内存映射到多个进程中,从而实现高速数据交换。
特点:
1. 进程管理可以实现多个进程的并发执行,提高系统的效率和性能。
2. 进程管理可以控制和管理进程的资源分配和使用,避免资源竞争和浪费。
3. 进程管理可以实现进程之间的通信和协作,提高系统的可靠性和稳定性。
算法:
1. 进程调度算法:常见的进程调度算法包括先来先服务、最短作业优先、时间片轮转等。其中,时间片轮转是最常用的调度算法之一。
2. 进程同步算法:常见的进程同步算法包括信号量、互斥锁、条件变量等。其中,信号量是最常用的进程同步机制之一。
3. 进程通信算法:常见的进程通信算法包括管道、消息队列、共享内存、信号量等。其中,共享内存是最快的进程通信方式之一。
总之,进程管理是现代操作系统中的重要组成部分,它可以实现多个进程之间的并发执行、资源分配和管理、进程通信和协作等功能。在实现进程管理时,需要考虑多种因素,包括调度算法、同步算法和通信算法等,从而达到优化系统性能和提高系统可靠性的目的。
操作系统-进程管理的代价-上下文切换
进程的调度势必会产生上下文的切换,上下文切换需要涉及大量的系统资源和时间,包括保存和恢复进程状态所需的CPU时间、内存空间和I/O操作。降低系统的性能和效率,因此需要尽可能地避免上下文切换。
其中,上下文切换会牵扯到内核态与用户态的转换(因为中间需要执行操作系统的调度算法,这是内核的部分):
用户态:指进程在执行普通用户代码时所处的状态。
内核态:指进程在执行操作系统内核代码时所处的状态。
我们可以很容易的看出上下文的切换会消耗宝贵的CPU存储资源,所有我们需要避免这种上下文切换的代价,那么问题来了,我们无法控制操作系统的调度算法,如何减小这种代价呢?
这里我们不能使用线程来避免,因为这也是操作系统创建,控制切换的,也就是说,线程也会产生上下文切换的代价,我们需要使用更轻量的线程——协程来避免内核级的上下文切换。
协程是一种用户级别的轻量级线程,不同于操作系统内核级别的线程。
协程具有以下的特点:
协程是由程序员自己进行管理和调度的,程序员需要手动控制协程的创建、切换和销毁。
协程是以协作式的方式进行调度,一个协程需要主动让出CPU资源,才能切换到下一个协程。
协程之间共享同一块栈空间,减少了内存的使用,同时也方便了数据共享和传递。
协程的上下文切换由程序员进行管理,只需要保存和恢复少量的上下文信息,成本比较低。
协程的上下文切换是基于用户态的,不会有内核态的中间转变,可以减少程序的资源开支,协程之间共享同一块栈空间,也减少了内存的使用,对于提高系统的效率而言是很好的。
这里写出线程与协程的代价对比:
上下文切换的开销:线程的上下文切换是由操作系统进行管理的,需要保存和恢复寄存器、程序计数器等多个上下文信息,成本比较高。而协程的上下文切换由程序员进行管理,只需要保存和恢复少量的上下文信息,成本比较低。
切换的频率:由于线程是由操作系统进行管理的,线程的切换频率比较高,因为操作系统需要根据调度算法进行线程的切换。而协程是由程序员进行管理的,协程的切换频率相对较低,因为程序员可以手动控制协程的切换。
内存的开销:由于线程是由操作系统进行管理的,每个线程都有自己的栈空间和堆空间,因此线程的内存开销比较大。而协程之间共享同一块栈空间,减少了内存的使用,降低了内存的开销。
综上所述,协程的上下文切换代价比线程低,因为协程的上下文切换只需要保存和恢复少量的上下文信息,而线程的上下文切换需要保存和恢复大量的上下文信息。协程的切换频率相对较低,因为程序员可以手动控制协程的切换。由于协程之间共享同一块栈空间,减少了内存的使用,因此协程的内存开销也比线程低。
除了协程以外,我们也有其他的方法来避免上下文切换,协程是通过不使用内核态来解决这个问题,那么我们也可以减少上下文切换来减少系统资源的消耗——避免阻塞操作。
阻塞操作会导致进程或线程的休眠,等待某个事件的发生。当事件发生时,操作系统会将进程或线程唤醒,从而引发上下文切换。因此,需要尽可能地避免阻塞操作,采用异步操作或非阻塞式操作来实现任务的执行。
除了上下文的切换代价以外,我们还有:
内存开销:每个进程需要占用一定的内存空间,包括代码段、数据段、堆栈段等。随着进程数量的增加,系统的内存使用量也会相应增加。
调度算法开销:进程调度算法需要占用一定的系统资源和时间,从就绪队列中选择一个合适的进程来运行。不同的调度算法会产生不同的代价和效果。
进程同步开销:多个进程之间的同步和通信需要使用一定的同步机制和通信机制,例如信号量、管道、共享内存等。这些机制会产生一定的系统资源代价和性能开销。
其中,可以由我们去控制的代价不多:
减少进程数量:减少进程的数量可以降低系统的内存开销、上下文切换开销和调度算法开销等。程序员可以通过合理地设计和实现进程通信和协作机制,将多个功能相似的进程合并为一个进程,从而减少进程数量,降低系统的资源代价。
使用合适的调度算法和同步机制:调度算法和同步机制会影响系统的性能和效率。程序员可以根据具体的应用场景选择合适的调度算法和同步机制,从而减少系统的资源代价。
合理地使用文件和数据:文件和数据的访问会占用一定的系统资源和存储空间。程序员可以通过缓存、压缩、分区等方式,合理地使用文件和数据,降低系统的资源代价。
文件系统开销:进程需要访问文件和数据,需要使用文件系统来进行管理和存储。文件系统会占用一定的系统资源和存储空间。
注:上下文的部分已经详细介绍,不再赘述。
我们不能盲目的跟随系统的进程调度,需要自己主动的去减少一些不必要的资源消耗。
操作系统-文件系统
件系统是通过将磁盘分成一个个固定大小的块,每个块作为最小的数据单位进行管理和存储。文件系统将文件抽象为一个个逻辑块,再将逻辑块映射为磁盘块,从而实现了文件的存储、读取和管理。下面重点讲述文件系统的特点和算法。
文件系统的特点:
抽象性:将物理存储设备抽象为逻辑块,将文件抽象为一组逻辑块,用户可以通过文件名等方式来访问文件。
透明性:屏蔽了磁盘物理结构的细节,用户不需要关心具体存储位置和物理块大小等信息,只需要通过文件名等方式即可访问文件。
可靠性:提供了对文件的备份和恢复功能,可以防止数据丢失和损坏。
安全性:提供了文件的访问控制和权限管理等功能,可以保护文件的安全性。
算法:
文件分配算法:文件系统需要决定如何分配磁盘空间给文件,常用的算法有连续分配、链式分配、索引分配等。
文件管理算法:文件系统需要管理文件的元数据信息,如文件名、大小、创建时间等,常用的算法有目录结构、文件控制块等。
磁盘调度算法:文件系统需要管理磁盘的读写操作,常用的算法有先来先服务、最短寻道时间优先、扫描算法等。
总之,文件系统是操作系统的重要组成部分,实现了对文件的存储、读取和管理,具有抽象性、透明性、可靠性和安全性等特点,需要使用合适的算法来实现文件的分配、管理和调度等操作。
操作系统-文件系统的代价
文件系统的实现需要消耗系统的各种资源,包括以下几个方面:
磁盘空间:文件系统需要使用磁盘空间来存储文件和元数据信息,磁盘空间的大小取决于文件的数量、大小和文件系统的设计。
内存空间:文件系统需要使用内存空间来缓存文件数据和元数据信息,以提高文件的读取和写入效率。内存空间的大小取决于文件的访问模式和文件系统的设计。
CPU 时间:文件系统需要使用CPU时间来执行各种操作,如文件的分配、读取和写入等。CPU时间的消耗取决于文件系统的实现方式和算法。
网络带宽:文件系统需要使用网络带宽来支持远程访问和文件共享,网络带宽的消耗取决于文件的传输量和网络的负载情况。
这些都是会产生系统资源的消耗,而我们可以采取以下几种方式来避免文件系统的代价:
合理使用缓存:程序员可以在代码中使用缓存技术,将常用的文件数据和元数据信息缓存到内存中,以减少对磁盘和网络的访问。但是,缓存的使用需要注意缓存大小和缓存策略等问题,以避免缓存过大或者缓存策略不当导致性能下降的问题。
优化文件访问方式:程序员可以通过优化文件的访问方式来减少文件系统的代价。例如,可以采用批量读取和写入数据的方式来减少磁盘访问的次数,以提高磁盘的读写效率。
避免频繁的文件操作:程序员可以避免频繁地打开、关闭和删除文件等操作,以减少文件系统的负载。例如,可以将多个操作合并为一个操作,以减少磁盘访问的次数和时间。
优化算法和数据结构:程序员可以通过优化算法和数据结构来减少文件系统的代价。例如,可以采用高效的文件分配算法和磁盘调度算法,以提高文件系统的性能和效率。
避免过度使用网络带宽:如果文件系统需要支持远程访问和文件共享,程序员可以避免过度使用网络带宽,例如,可以采用压缩和数据加密技术来减少网络传输的数据量和安全风险。
综上所述,我们可以通过合理使用缓存、优化文件访问方式、避免频繁的文件操作、优化算法和数据结构、避免过度使用网络带宽等方式来避免文件系统的代价,以提高程序的性能和效率。
操作系统-网络系统
实现网络系统的主要方式是通过网络协议栈的实现,网络协议栈是操作系统内核中用于实现网络通信的重要组成部分。网络协议栈的主要功能包括数据传输、错误检测、数据分段、数据重组等。下面重点讲述操作系统实现网络系统的特点、重点和算法。
特点:
多用户支持:操作系统必须支持多用户和多任务,并能够控制用户和任务的访问权限,保证网络系统的安全性。
可靠性:网络系统需要保证数据传输的可靠性,操作系统需要实现各种错误检测和纠正机制,例如CRC校验、冗余传输等。
性能:网络系统需要保证高效的数据传输和响应速度,操作系统需要实现高效的数据缓存、调度算法等。
可扩展性:网络系统需要支持不同的网络协议和传输协议,操作系统需要支持多种网络协议栈的实现,并支持动态加载和卸载。
重点:
网络协议栈的实现:网络协议栈是操作系统中实现网络通信的核心组件,包括物理层、数据链路层、网络层、传输层和应用层等。操作系统需要实现不同的网络协议栈,例如TCP/IP协议栈、IPX/SPX协议栈等。
网络缓存管理:操作系统需要实现高效的网络缓存管理,包括数据缓存和路由缓存等。数据缓存用于缓存网络数据,路由缓存用于缓存网络路由信息,提高网络传输效率。
网络调度算法:操作系统需要实现高效的网络调度算法,包括数据传输调度和路由选择算法等。数据传输调度算法用于调度网络数据传输,路由选择算法用于选择最优的网络路由。
算法:
TCP/IP协议栈实现算法:TCP/IP协议栈是目前最广泛使用的网络协议栈之一,其实现算法包括数据分段算法、拥塞控制算法、重传机制等。
数据传输调度算法:数据传输调度算法包括最短路径算法、最小带宽路径算法、最大流量算法等,用于调度网络数据传输。
路由选择算法:路由选择算法包括最短路径算法、贪心算法、遗传算法等,用于选择最优的网络路由。
综上所述,操作系统实现网络系统需要考虑多个方面,包括多用户支持、可靠性、性能和可扩展性等。重点是实现网络协议栈、网络缓存管理和网络调度算法等。算法方面包括TCP/IP协议栈实现算法、数据传输调度算法和路由选择算法等。
操作系统-网络系统的代价
网络系统的实现需要消耗系统的各种资源,包括以下几个方面:
CPU资源:网络通信过程中,需要进行数据的封装、解封、加密、解密等操作,这些操作都需要消耗CPU资源。在高并发的情况下,CPU资源的消耗会更加明显。
内存资源:网络数据在传输过程中需要进行缓存,因此,操作系统的网络系统需要占用一定的内存资源。在高并发的情况下,网络数据的缓存会占用更多的内存资源。
带宽资源:网络通信需要使用带宽资源,带宽资源的消耗与数据传输的速率和数据量有关。在高并发的情况下,网络通信会占用更多的带宽资源。
磁盘资源:在使用网络文件系统时,需要进行文件的读写操作,这些操作会占用磁盘资源。在高并发的情况下,文件读写操作会更加频繁,磁盘资源的消耗也会更加明显。
在其中,我们可以优化:
CPU资源:可以通过编写高效的代码和使用优化算法来减少CPU资源的使用,例如使用多线程、异步IO等技术来提高CPU利用率。
内存资源:可以通过优化内存管理和垃圾回收机制来减少内存资源的使用,例如使用对象池、延迟初始化等技术来降低内存占用。
网络带宽:可以通过优化网络传输算法和数据压缩算法来减少网络带宽的使用,例如使用数据压缩、分块传输等技术来降低数据传输量。
磁盘空间:可以通过优化数据存储算法和数据压缩算法来减少磁盘空间的使用,例如使用压缩存储、数据分块等技术来降低数据占用空间。
可以使用的方法:
代码优化:编写高效的代码,避免浪费系统资源,例如避免重复计算、避免频繁创建和销毁对象等。
算法优化:优化算法,减少资源占用,例如选择合适的数据结构、使用缓存等。
异步编程:使用异步编程技术,减少等待时间,提高系统资源利用率。
数据压缩:使用数据压缩算法,减少数据传输量,降低网络带宽和磁盘空间的使用。
缓存技术:使用缓存技术,减少数据访问次数,提高系统性能和效率。
操作系统-结语
我们可以看到操作系统在实现整体中,有很多的系统资源使用代价,但是这些代价不是必须的,是我们可以避免的,在这个技术日新月异的时代,可以避免的技术肯定也不止这些,但是我水平也就这样(目前),所以我们不能停止对系统的探索,不能停止对程序的优化。
毕竟C++的对底层资源的开放,代表了C++强调“底层控制”的思想。C++的设计初衷是为了提供一种高级的编程语言,同时也具有直接操作底层资源的能力,使程序员可以更加灵活、高效地开发软件系统,那么我们便不能辜负语言对我们的信赖。
注:关于文件系统与网络系统的部分写的比较省略,了解不深入,不敢写的太深,只能介绍一些大概。
计算机:约定组成的网络
计算机是依靠一系列的约定和规则,使得软硬件能够协同工作,完成各种复杂的任务。这些约定和规则是不断发展和完善的,它们不是死板的,而是随着技术的进步和应用场景的变化而不断演化。进程管理和CPU调度是操作系统的两个重要组成部分,它们共同协作,以实现程序运行的效率最大化。
C++作为一门高级编程语言,屏蔽了汇编语言的复杂性,让开发人员能够更加专注于业务逻辑的实现。因此,了解计算机体系结构和编程规则,能够帮助我们更好地理解软硬件协同工作的原理,为编写高质量的程序提供帮助。
注:如有错误,请指正,谢谢。