操作系统(1)

操作系统

一、概述

(一)定义与角色

操作系统是管理计算机硬件与软件资源、控制程序执行、改善人机交互界面,并为计算机系统中的其他软件和用户提供服务的系统软件。它充当了硬件与用户以及应用程序之间的桥梁,使得计算机系统能够高效、有序且安全地运行。例如,用户打开软件、读写文件、使用外部设备等操作,都需要操作系统在背后协调硬件资源来实现。

(二)目标
  1. 方便性:把复杂的硬件操作封装起来,提供简单易懂的操作界面,像图形化界面中通过鼠标点击就能完成各种任务,无需用户了解磁盘读写、内存寻址等底层细节。
  2. 有效性:合理分配和调度计算机的各种资源(如 CPU、内存、I/O 设备等),提高资源利用率,让多个程序可以并发执行,避免资源闲置浪费。
  3. 可扩充性:能够方便地接纳新的硬件设备和软件功能,只需安装相应驱动程序或更新软件接口就能整合新元素,保障系统不断发展。
(三)特征
  1. 并发(Concurrency):指多个程序或事件在同一时间段内同时发生。在单 CPU 系统中,通过时间片轮转等调度方式,让多个进程看似同时运行;在多 CPU 系统中,多个进程可真正在不同 CPU 核心上同时执行,例如一边听歌一边编辑文档,两个进程并发执行。
  2. 共享(Sharing):系统中的资源(如内存、文件、设备等)可以被多个进程共同使用。有互斥共享(如打印机,同一时刻只能被一个进程使用)和同时共享(如磁盘文件,多个进程可同时读取)两种形式。
  3. 虚拟(Virtualization):通过某种技术手段,将物理实体转变为逻辑上的对应物,使用户感觉拥有比实际物理资源更多的资源。比如虚拟内存,利用外存扩充内存空间,让用户觉得内存很大。
  4. 异步(Asynchrony):进程的执行顺序和执行时间是不确定的,以不可预知的速度向前推进。尽管进程有不同状态及调度规则,但由于各种因素(如 I/O 操作时长不确定),执行情况难以准确预判。
(四)操作系统的发展历程

经历了从无操作系统的手工操作阶段(人工手动输入输出,资源利用率极低),到批处理系统(先是单道批处理,后有多道批处理提高资源利用率但缺乏交互性),再到分时操作系统(多用户分时使用,交互性强)、实时操作系统(响应时间严格要求)、网络操作系统(具备网络相关功能)以及分布式操作系统(管理多台联网计算机,协同工作)等不同阶段,功能不断完善和拓展。

二、进程管理

(一)进程概念

进程是程序在一个数据集合上运行的过程,是操作系统进行资源分配和调度的基本单位。它包含程序段(存放指令代码)、数据段(存放运行相关数据)以及进程控制块(PCB,记录进程标识符、状态、优先级、程序计数器、寄存器内容等关键信息)。例如,打开一个浏览器应用程序,操作系统就为其创建一个进程,该进程有自己的代码执行逻辑、网页相关数据以及管理自身状态的 PCB。

(二)进程状态及转换
  1. 基本状态
    • 就绪态(Ready):进程已获取除 CPU 之外的所有必要资源,只要分配到 CPU 就能运行,多个就绪进程会排成就绪队列等待调度。
    • 运行态(Running):进程正在占用 CPU 执行指令,在单 CPU 系统中同一时刻只有一个进程处于此状态。
    • 阻塞态(Blocked):又称等待态,进程因等待某一事件(如等待 I/O 操作完成、等待信号量释放等)而暂停执行,即便 CPU 空闲也不能运行,需等事件结束后转换为就绪态。
  2. 状态转换
    • 就绪态→运行态:调度程序按调度算法(如先来先服务、优先级调度等)从就绪队列选进程分配 CPU 时发生转换。
    • 运行态→就绪态:常见于时间片用完(时间片轮转算法下)或有更高优先级进程进入就绪队列(优先级调度且允许抢占时)。
    • 运行态→阻塞态:当进程执行中需等待事件发生(如进行磁盘读写操作)主动放弃 CPU 时转换。
    • 阻塞态→就绪态:等待的事件完成(如磁盘读操作结束产生中断通知操作系统)后,操作系统将其转换回就绪态。
(三)进程控制

通过特定的原语(原子操作,不可中断)来实现,包括:

  1. 创建进程:如在 UNIX 系统中通过 fork 系统调用创建子进程,操作系统为新进程分配资源、初始化 PCB 并加入就绪队列。
  2. 终止进程:进程完成任务或因错误、收到外部终止信号等结束运行,操作系统回收其占用资源(内存、文件等)并撤销 PCB。
  3. 阻塞与唤醒进程:进程可主动通过系统调用使自己进入阻塞态等待事件,事件完成后由相关机制(如中断处理程序)唤醒,使其从阻塞态变为就绪态。
(四)进程同步与互斥
  1. 临界区问题:多个进程并发访问共享临界资源(如打印机、共享变量等)时,临界区(访问临界资源的代码段)需保证同一时刻只有一个进程能进入,否则会出现数据不一致等问题。
  2. 互斥:对临界资源访问的排他性要求,实现方法有软件方法(如 Peterson 算法,但复杂且效率低)和硬件方法(如利用中断屏蔽指令、TestAndSet 指令、Swap 指令等,执行速度快但依赖硬件支持)。
  3. 同步:多个进程相互协作时协调执行顺序,常见通过信号量机制实现,信号量是个整型变量,有 P(等待,使信号量减 1,若小于 0 则阻塞)和 V(释放,使信号量加 1,若小于等于 0 则唤醒阻塞进程)操作,还可以用管程(将共享变量及相关操作封装,保证同步互斥且更易维护)等机制。
(五)进程通信
  1. 共享存储:多个进程共享一块内存区域来交换信息,需通过同步互斥机制确保数据一致性,比如共享缓冲区用于生产者和消费者进程传递数据。
  2. 消息传递:进程间通过发送和接收消息来通信,分为直接通信(明确指定接收方和发送方)和间接通信(通过中间实体如消息队列,发送方将消息放入队列,接收方从队列取消息)。
  3. 管道通信:常用于父子进程间通信,是一种半双工通信方式,一端写入数据,另一端读出数据,数据按写入顺序读出,有一定的同步机制保证读写有序。
(六)线程

线程是 CPU 调度和分派的基本单位,基本不拥有系统资源,与同属一个进程的其他线程共享进程资源(如内存空间、文件资源等)。分为用户级线程(由应用程序管理,对操作系统透明,切换快但一个线程阻塞会导致所属进程所有线程阻塞)和内核级线程(由操作系统管理,一个线程阻塞不影响其他线程,切换开销大些但能更好利用多核 CPU)。

三、存储管理

(一)内存管理功能
  1. 内存分配与回收:根据进程需求分配内存空间,进程结束后回收其占用内存,分配方式有静态分配(编译时确定内存大小)和动态分配(运行时按需申请)。
  2. 内存保护:防止不同进程的内存空间相互干扰,通过硬件(如基址寄存器、限长寄存器)和软件配合,监控进程访问内存操作,禁止越界访问。
  3. 内存映射:将程序中的逻辑地址转换为物理内存地址,便于 CPU 正确访问内存数据,例如分页存储管理、分段存储管理等都涉及地址映射机制。
  4. 内存扩充:借助虚拟存储技术,利用外存空间扩充内存容量,基于局部性原理(时间局部性、空间局部性),把暂时不用的程序和数据放外存,需要时调入内存,让用户感觉内存变大了。
(二)分区存储管理方案
  1. 固定分区分配:事先将内存划分为若干大小固定的分区,每个分区装入一个作业(进程),分区大小可以相同也可以不同,优点是简单易行,缺点是容易产生内部碎片(分区内未被利用的空间),且分区大小限制了可装入作业的大小。
  2. 动态分区分配:根据进程大小动态地划分内存分区,分配时找合适空闲分区分配给进程,会产生外部碎片(内存中存在很多分散的小空闲分区无法利用),解决外部碎片可采用紧凑技术(移动进程,使空闲分区合并),常用的分配算法有首次适应算法(从空闲分区链首开始找能满足大小的分区)、最佳适应算法(选最接近进程大小的空闲分区)、最坏适应算法(选最大空闲分区)等。
  3. 可重定位分区分配:在动态分区基础上,为解决外部碎片问题,通过动态重定位技术(借助重定位寄存器),移动进程在内存中的位置,合并空闲分区,提高内存利用率。
(三)页式存储管理方案
  1. 基本原理:将内存空间和程序空间都划分为固定大小的页(如每页 4KB),程序的逻辑地址分为页号和页内偏移量,通过页表(记录页号对应的物理块号等信息)实现逻辑地址到物理地址的转换,CPU 访问内存时先查页表找到物理块号,再结合页内偏移量确定物理地址。
  2. 页表结构及优化:页表可能占用较大内存空间,可采用多级页表(如二级页表、三级页表等,将页表再分页管理)、快表(TLB,存放近期常用页表项,是一种高速缓存,加快地址转换速度)等方式优化。
(四)段式存储管理方案
  1. 基本原理:按程序的逻辑结构(如代码段、数据段、堆栈段等)划分成不同的段,每段有自己的段名和长度,逻辑地址由段号和段内偏移量组成,通过段表(记录段号对应的内存起始地址、段长等信息)实现地址转换,便于程序和数据的模块化组织、共享和保护。
  2. 优缺点:优点是符合程序逻辑结构,便于程序段的动态增长、共享和保护;缺点是段的长度不一,内存分配管理相对复杂,容易产生外部碎片。
(五)段页式存储管理方案

结合了段式和页式的优点,先按段式划分程序,每个段再按页式划分,逻辑地址由段号、段内页号、页内偏移量组成,访问内存时先通过段表找到对应段的页表,再通过页表找到物理块,进行地址转换,这种方式管理复杂但综合性能较好。

(六)虚拟存储管理
  1. 基本概念:基于局部性原理,在程序运行时,不必将整个程序和数据都放入内存,只把当前需要的部分调入内存,其余放在外存(如磁盘),当需要访问外存部分时再调入内存,从用户角度感觉内存空间得到扩充。
  2. 页面置换算法:当内存空间已满,需要调入新页面时,决定置换出哪个已在内存的页面,常见算法有:
    • 先进先出(FIFO):按页面进入内存的先后顺序置换,容易实现,但可能出现 Belady 异常(增加物理内存页面数,缺页次数反而增加),性能不佳。
    • 最近最久未使用(LRU):置换最近最长时间未被使用的页面,符合程序局部性规律,缺页率相对较低,但实现成本较高,需记录页面使用时间顺序信息。
    • 最佳置换算法(OPT):理论上选择以后永远不会使用或在最长时间内不会再使用的页面进行置换,缺页率最低,但无法预知未来页面使用情况,实际中难以实现,常作性能对比参照。
  3. 页面分配策略:决定为每个进程分配多少物理页面,有固定分配局部置换(给进程固定页面数,缺页时只在分配的页面内置换)、可变分配全局置换(根据系统空闲页面情况给进程分配页面,缺页时可从全局空闲页面中置换)、可变分配局部置换(初始分配一定页面数,缺页时可适当增加分配页面数,且只在自身页面内置换)等策略。

四、文件管理

(一)文件和文件系统
  1. 文件概念:文件是具有符号名的、在逻辑上具有完整意义的一组相关信息项的有序序列,是计算机存储信息的基本单位,如文档、程序、图像等都以文件形式存在。
  2. 文件系统:是操作系统中负责管理和存储文件的软件机构,包括文件存储空间管理、目录管理、文件的读写管理、文件保护等功能,为用户和应用程序提供方便的文件操作接口,使得用户能方便地创建、删除、修改、访问文件。
(二)文件的逻辑结构
  1. 无结构文件(流式文件):文件内部没有明显的结构,是一串字符流,像文本文件,对其读写操作通常按顺序进行,基本单位是字节或字符,适合存储文本、图像等数据,操作简单灵活。
  2. 有结构文件(记录式文件):由若干记录组成,每个记录有固定格式和长度(定长记录)或不同格式和长度(变长记录),常用于存储数据库等有结构的数据,便于按记录进行检索、修改等操作,常见类型有顺序文件(记录按顺序存储,适合顺序访问)、索引文件(为每个记录建立索引,便于随机访问)、索引顺序文件(结合顺序文件和索引文件特点,先按顺序分组,再对组建立索引,提高查找效率)等。
(三)文件的目录结构
  1. 单级目录结构:最简单的目录结构,所有文件都在同一个目录下,优点是简单,缺点是文件数量多时查找困难,且文件名不能重复,不便于文件管理和共享。
  2. 两级目录结构:分为主目录和用户目录,不同用户有自己的子目录,可在一定程度上解决文件名冲突问题,方便用户管理自己的文件,但仍不够灵活,不能很好地反映文件的层次关系。
  3. 树形目录结构:最常用的目录结构,类似倒立的树,根目录下有多个子目录,子目录下又可细分,层次分明,便于文件分类管理、查找和共享,用户通过完整路径名(如 “C:\Program Files\Microsoft Office\Word.exe”)定位文件,操作系统依据目录项(包含文件名、属性、存储位置等信息)管理文件。
  4. 图形目录结构:以图形化方式展示目录和文件关系,更直观,便于用户操作,常用于图形用户界面操作系统中,用户通过鼠标点击等操作浏览、管理文件。
(四)文件存储空间管理
  1. 空闲表法:用一张表格记录磁盘上各个空闲块的起始地址和长度等信息,分配空间时查找合适空闲块,回收时更新表格内容,适用于连续分配方式,管理简单但表格维护开销较大。
  2. 空闲链表法:将磁盘空闲块通过链表形式连接,每个空闲块包含指向下一个空闲块的指针,分配时顺着链表查找合适空闲块,回收时将空闲块插入链表合适位置,灵活性高但查找效率相对较低。
  3. 位示图法:用每一位对应磁盘上的一个物理块,0 表示空闲,1 表示已占用,通过位运算进行空间分配和回收操作,占用空间小且便于快速查找空闲块,但需要额外的位运算操作。
(五)文件共享与文件保护
  1. 文件共享:允许多个用户或进程共享同一个文件,可通过硬链接(多个文件名指向同一个文件的索引节点,删除一个文件名不影响文件本身,除非所有硬链接都删除)和软链接(也叫符号链接,是一个特殊文件,指向另一个文件的路径,访问时通过路径查找目标文件)等方式实现,节省存储空间,提高资源利用率。
  2. 文件保护:防止文件被非法访问、篡改或破坏,常用方法有:
    • 设置访问权限:如规定用户对文件的读、写、执行权限,通过文件属性或访问控制列表(ACL)来体现,不同用户或用户组有不同权限。
    • 加密:对文件内容进行加密处理,只有拥有正确密钥的用户才能解密并访问文件,安全性高,但加密和解密会增加系统开销。

五、设备管理

(一)I/O 系统
  1. 组成部分:由 CPU 与控制器的接口、I/O 逻辑、控制器和设备之间的接口等构成,负责在 CPU、内存与各类外部设备(输入输出设备,如键盘、鼠标、打印机、磁盘等)之间进行数据传输和协调控制。
  2. 设备分类
    • 按使用特性分:存储设备(如磁盘、磁带等,用于长期存储数据)、输入设备(如键盘、鼠标、扫描仪等,用于向计算机输入数据)、输出设备(如显示器、打印机等,用于输出计算机处理结果)。
    • 按传输速率分:低速设备(如键盘,数据传输速度慢)、中速设备(如打印机)、高速设备(如磁盘,数据传输速度快)。
    • 按信息交换单位分:字符设备(以字符为单位进行数据传输,如终端设备)、块设备(以块为单位进行数据传输,如磁盘,每次读写一块数据)。
(二)I/O 控制方式
  1. 程序查询方式:最原始的控制方式,CPU 不断循环查询设备的状态寄存器,判断设备是否准备好进行数据传输。若设备未准备好,CPU 就一直等待;若准备好了,CPU 则进行数据传输操作。这种方式 CPU 利用率极低,因为在等待设备准备期间,CPU 一直处于空闲状态,只能用于简单的、对实时性要求不高且设备传输速度较慢的场景,例如早期简单的单片机系统与外部慢速设备的数据交互。
  2. 程序中断方式:当设备完成数据准备或数据传输结束等情况时,会向 CPU 发送中断请求信号,CPU 响应中断后暂停当前正在执行的程序,转而去执行相应的中断处理程序来完成数据传输等操作,之后再返回原程序继续执行。这种方式使得 CPU 在设备进行数据准备期间可以执行其他任务,提高了 CPU 的利用率,适用于像打印机这类慢速的、需要不定期与 CPU 交互数据的设备,能让 CPU 在等待打印机打印的过程中去处理其他事务。
  3. DMA(直接存储器访问)方式:在内存和设备之间开辟了一条直接的数据传输通路,设备可以不经过 CPU 直接与内存进行批量的数据传输,只需在传输开始和结束时向 CPU 发送请求和通知。它适用于高速的外部设备,如磁盘等,极大地减轻了 CPU 在数据传输过程中的负担,提高了系统的数据传输效率,因为磁盘读写数据量往往较大,如果通过 CPU 频繁中转会严重影响性能。
  4. 通道方式:通道是一种具有特殊功能的处理器,它可以独立执行通道程序来控制设备与内存之间的数据传输,进一步减轻 CPU 的负担,且能同时管理多台同类型或不同类型的设备,实现设备的并行操作。通道方式常用于大型计算机系统中管理众多复杂的 I/O 设备,例如大型服务器连接多个磁盘阵列、磁带机等设备时,通过通道进行高效的管控和数据传输。
(三)缓冲管理
  1. 缓冲的作用:由于 CPU 和 I/O 设备在处理速度上存在很大差异,设置缓冲区可以缓和这种速度不匹配的矛盾,提高 CPU 和 I/O 设备的并行性,同时减少数据传输过程中的中断次数等。例如,在网络数据接收时,网络接口的数据传输速度可能不稳定且相对较慢,而 CPU 处理数据速度很快,通过缓冲区先暂存网络传来的数据,让 CPU 可以在合适时间批量处理,避免频繁等待。
  2. 缓冲的类型
    • 单缓冲:在设备和 CPU 之间设置一个缓冲区,设备先将数据写入缓冲区,然后 CPU 再从缓冲区读取数据进行处理。它结构简单,但对于连续快速的数据传输场景,效率提升有限,常用于简单的、数据量不大且对传输速度要求不高的情况,比如键盘输入数据的临时存储。
    • 双缓冲:设置两个缓冲区,设备向一个缓冲区写入数据的同时,CPU 可以从另一个缓冲区读取数据,两个缓冲区交替使用,使得设备和 CPU 能更好地并行工作,提高了数据传输效率,常用于图像显示等需要一定并行处理能力的场景,一边可以接收新的图像数据到一个缓冲区,另一边 CPU 可以对另一个缓冲区中的图像数据进行渲染等操作。
    • 循环缓冲:由多个缓冲区组成一个循环队列的形式,适用于高速设备的数据传输,比如磁盘读写操作,多个缓冲区可以连续不断地接收和处理数据,保证数据的流畅传输,防止因设备和 CPU 速度差异导致的数据丢失或处理延迟等问题。
(四)设备分配
  1. 设备分配原则:根据设备的类型、数量以及各进程的需求,按照一定的策略来合理分配设备资源。对于独占设备(如打印机,同一时间只能被一个进程使用),通常采用独占分配方式,确保使用的独占性;对于共享设备(如磁盘,多个进程可同时访问不同区域),可以多个进程同时使用,通过合适的调度算法来协调访问顺序,避免冲突。同时还要考虑设备的使用效率、公平性以及避免死锁等问题。
  2. 设备分配算法:常见的有先来先服务算法(按照进程申请设备的先后顺序进行分配,简单公平,但可能对急需设备的重要进程不利)、优先级高的先分配算法(根据进程的优先级高低来分配设备,优先满足重要的、优先级高的进程需求,但要注意避免低优先级进程长期得不到设备的 “饥饿” 现象)等。
(五)I/O 软件系统层次模型
  1. 用户层软件:这是最靠近用户的一层,通常由应用程序中的 I/O 相关函数等组成,例如在 C 语言中调用 printf 函数进行输出操作,它通过系统调用接口向下面的层次发起 I/O 请求,为用户提供了方便的操作接口来实现简单的 I/O 功能,并且可以根据应用程序的需求对数据进行一定的格式处理等。
  2. 设备独立性软件:也叫与设备无关的 I/O 软件,它主要负责实现逻辑设备名到物理设备名的转换,为用户层软件提供统一的 I/O 操作接口,而不管底层具体使用的是哪种实际设备。比如,应用程序只需用通用的 “打印机” 这个逻辑设备名来发起打印请求,设备独立性软件会根据系统配置等情况找到对应的实际物理打印机设备来执行操作,同时还处理设备的分配、缓冲管理、出错处理等功能,提高了 I/O 系统的通用性和可移植性。
  3. 设备驱动程序:它是直接与硬件设备打交道的软件,负责将操作系统上层的 I/O 指令转化为设备能够理解和执行的特定控制信号,驱动设备完成相应的操作,如控制磁盘读写头的移动、打印机的打印头动作等,并且将设备的状态信息(如设备是否故障、是否忙碌等)反馈给上层软件,不同类型的设备都有对应的专门驱动程序,需要根据设备的硬件特性进行编写和适配。
  4. 中断处理程序:当设备完成一次 I/O 操作或者出现异常情况时,会通过中断机制向 CPU 发送中断信号,中断处理程序会立即响应中断,暂停当前正在执行的程序,去处理与该中断相关的事宜,比如完成数据的后续传输、对设备故障进行相应处理等,处理完后再恢复原程序的执行,确保设备和 CPU 之间能高效协调工作。
  5. 硬件:就是实际的 I/O 设备本身以及与之相关的硬件电路、接口等,是整个 I/O 系统的物理基础,如磁盘的盘片、磁头、电机等部件,以及与计算机主板连接的接口线路等,它们执行具体的物理数据读写、信号转换等操作。

六、运行机制与体系结构

(一)运行机制
  1. 核心态与用户态
    • 核心态(也叫管态、内核态):操作系统内核程序运行在此状态下,具有最高的权限,可以访问计算机系统的所有硬件资源和全部内存空间,能执行所有的指令,例如进行设备驱动、内存管理等关键操作,主要是为了保证系统的安全性和稳定性,防止普通用户程序随意篡改系统关键数据或干扰硬件的正常运行。
    • 用户态(也叫目态):用户应用程序运行在此状态下,权限受到严格限制,只能访问分配给自己的内存区域,执行部分非特权指令,不能直接操作硬件设备或访问系统关键资源,这样可以避免因用户程序错误等原因破坏整个系统的正常运行秩序。
  2. 特权指令与非特权指令
    • 特权指令:只能在核心态下执行的指令,如启动设备、设置时钟、内存清零等指令,这些指令如果能被用户程序随意执行,可能会导致系统混乱,例如用户程序随意修改时钟,会使整个系统的时间管理出现问题,影响依赖时间的各种系统功能和应用程序的正常运行。
    • 非特权指令:在用户态下可以正常执行的指令,比如简单的算术运算、逻辑运算、数据传送等指令,这些指令不会对系统的关键资源和整体运行造成直接的危害,可用于用户程序实现自身的业务逻辑功能。
  3. 中断与异常
    • 中断:是由外部设备(如键盘按键按下、磁盘数据传输完成等)或其他外部事件引发的,向 CPU 发送中断请求信号,使 CPU 暂停当前执行的程序,转而去处理相应的中断事件,处理完后再返回原程序继续执行,它是实现设备管理、多任务并发等功能的重要机制,能让 CPU 及时响应外部设备的需求,提高系统的整体效率和响应能力。
    • 异常:是 CPU 在执行指令过程中自身检测到的一些不正常情况,比如除法运算中除数为零、执行了非法指令等,此时 CPU 也会暂停当前程序,转而去执行相应的异常处理程序,处理完异常后再决定是否继续执行原程序或者采取其他措施(如终止程序等),以保证系统在出现意外情况时能尽量维持稳定运行。
  4. 系统调用:是操作系统提供给应用程序的接口,应用程序通过系统调用可以请求操作系统内核提供特定的服务,比如打开文件、创建进程、申请内存等操作,这些操作需要操作系统内核的权限和资源才能完成,所以应用程序要从用户态陷入到核心态,通过系统调用的方式向内核发起请求,内核执行相应的服务后再将结果返回给应用程序,系统调用是用户程序与操作系统内核交互的重要途径,也是构建操作系统功能应用的关键环节。
(二)体系结构
  1. 整体式结构:将整个操作系统的功能都编写在一个大的模块中,各个功能模块之间的联系紧密,调用关系复杂,优点是效率较高,代码执行速度快,因为各部分之间的交互相对直接;缺点是可维护性和可扩展性差,一旦某个部分出现问题或者需要更新功能,可能会牵一发而动全身,影响到整个操作系统的稳定运行,早期的一些操作系统(如早期的 DOS 系统部分采用这种结构)多是这种形式,随着操作系统功能的日益复杂,这种结构逐渐不太适用了。
  2. 层次式结构:把操作系统按照功能的不同划分成多个层次,每一层都建立在下层的基础之上,为上层提供服务,同时又调用下层的功能,层次之间有清晰的接口,例如最底层是硬件相关的驱动等功能,往上依次是内存管理、进程管理、文件管理等层次,这种结构的优点是便于理解和维护,各层功能相对独立,修改某一层的功能对其他层的影响相对较小,提高了系统的可扩展性;缺点是由于分层导致系统调用的开销可能会有所增加,执行效率可能略受影响,像 MULTICS 操作系统就是典型的采用层次式结构的操作系统。
  3. 微内核结构:将操作系统的核心功能尽量精简,只保留最基本的如进程管理、内存管理、中断处理等功能在内核中(也就是微内核部分),其他的很多服务(如文件系统、设备驱动等)都作为独立的服务器进程运行在用户态,通过消息传递机制与微内核进行通信和交互来完成相应功能,这种结构的优点是具有良好的可扩展性、可移植性和安全性,因为各个服务相对独立,便于更新和替换,而且即使某个服务出现问题也不容易影响到内核的稳定运行;缺点是由于频繁的消息传递,系统性能可能在一定程度上受影响,不过随着硬件性能的提升以及优化技术的发展,这种结构在很多现代操作系统(如 Windows NT 系列、部分 Linux 发行版等)中都有应用,尤其适合分布式、嵌入式等复杂多变的应用场景。

首先你要搞明白你学习操作系统的目的是什么?操作系统的重要性如何?学习操作系统会给我带来什么?下面我会从这几个方面为你回答下。
操作系统也是一种软件,但是操作系统是一种非常复杂的软件。操作系统提供了几种抽象模型

  • 文件:对 I/O 设备的抽象
  • 虚拟内存:对程序存储器的抽象
  • 进程:对一个正在运行程序的抽象
  • 虚拟机:对整个操作系统的抽象

认识操作系统

在了解操作系统前,你需要先知道一下什么是计算机系统:现代计算机系统由一个或多个处理器、主存、打印机、键盘、鼠标、显示器、网络接口以及各种输入 / 输出设备构成的系统。这些都属于硬件的范畴。我们程序员不会直接和这些硬件打交道,并且每位程序员不可能会掌握所有计算机系统的细节。
所以计算机科学家在硬件的基础之上,安装了一层软件,这层软件能够根据用户输入的指令达到控制硬件的效果,从而满足用户的需求,这样的软件称为 操作系统,它的任务就是为用户程序提供一个更好、更简单、更清晰的计算机模型。也就是说,操作系统相当于是一个中间层,为用户层和硬件提供各自的借口,屏蔽了不同应用和硬件之间的差异,达到统一标准的作用。

上面一个操作系统的简化图,最底层是硬件,硬件包括芯片、电路板、磁盘、键盘、显示器等我们上面提到的设备,在硬件之上是软件。大部分计算机有两种运行模式:内核态用户态,软件中最基础的部分是操作系统,它运行在 内核态 中。操作系统具有硬件的访问权,可以执行机器能够运行的任何指令。软件的其余部分运行在 用户态 下。
在大概了解到操作系统之后,我们先来认识一下硬件都有哪些

计算机硬件

计算机硬件是计算机的重要组成部分,其中包含了 5 个重要的组成部分:运算器、控制器、存储器、输入设备、输出设备

  • 运算器:运算器最主要的功能是对数据和信息进行加工和运算。它是计算机中执行算数和各种逻辑运算的部件。运算器的基本运算包括加、减、乘、除、移位等操作,这些是由 算术逻辑单元(Arithmetic&logical Unit) 实现的。而运算器主要由算数逻辑单元和寄存器构成。
  • 控制器:指按照指定顺序改变主电路或控制电路的部件,它主要起到了控制命令执行的作用,完成协调和指挥整个计算机系统的操作。控制器是由程序计数器、指令寄存器、解码译码器等构成。

运算器和控制器共同组成了 CPU

  • 存储器:存储器就是计算机的记忆设备,顾名思义,存储器可以保存信息。存储器分为两种,一种是主存,也就是内存,它是 CPU 主要交互对象,还有一种是外存,比如硬盘软盘等。下面是现代计算机系统的存储架构
  • 输入设备:输入设备是给计算机获取外部信息的设备,它主要包括键盘和鼠标。
  • 输出设备:输出设备是给用户呈现根据输入设备获取的信息经过一系列的计算后得到显示的设备,它主要包括显示器、打印机等。
    这五部分也是冯诺伊曼的体系结构,它认为计算机必须具有如下功能:
    把需要的程序和数据送至计算机中。必须具有长期记忆程序、数据、中间结果及最终运算结果的能力。能够完成各种算术、逻辑运算和数据传送等数据加工处理的能力。能够根据需要控制程序走向,并能根据指令控制机器的各部件协调操作。能够按照要求将处理结果输出给用户。

    下面是一张 intel 家族产品图,是一个详细的计算机硬件分类,我们在根据图中涉及到硬件进行介绍
  • 总线(Buses):在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。通常总线被设计成传送定长的字节块,也就是 字(word)。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。现在大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。
  • I/O 设备(I/O Devices):Input/Output 设备是系统和外部世界的连接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。
    每个 I/O 设备连接 I/O 总线都被称为控制器(controller) 或者是 适配器(Adapter)。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备本身或者系统的主印制板电路(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论组织形式如何,它们的最终目的都是彼此交换信息。
  • 主存(Main Memory),主存是一个临时存储设备,而不是永久性存储,磁盘是 永久性存储 的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列 DRAM(dynamic random access memory) 动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它唯一的地址编号,从 0 开始。一般来说,组成程序的每条机器指令都由不同数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。比如,在 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 需要 4 个字节,而 long 和 double 需要 8 个字节。
  • 处理器(Processor)CPU(central processing unit) 或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。
    从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操作。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操作,然后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(比如 jmp 指令就不会顺序读取)
    下面是 CPU 可能执行简单操作的几个步骤
  • 加载(Load):从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容
  • 存储(Store):将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容
  • 操作(Operate):把两个寄存器的内容复制到 ALU(Arithmetic logic unit) 。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。

算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。

  • 跳转(jump):从指令中抽取一个字,把这个字复制到程序计数器(PC) 中,覆盖原来的值

进程和线程

关于进程和线程,你需要理解下面这张脑图中的重点

进程
操作系统中最核心的概念就是 进程,进程是对正在运行中的程序的一个抽象。操作系统的其他所有内容都是围绕着进程展开的。
在多道程序处理的系统中,CPU 会在进程间快速切换,使每个程序运行几十或者几百毫秒。然而,严格意义来说,在某一个瞬间,CPU 只能运行一个进程,然而我们如果把时间定位为 1 秒内的话,它可能运行多个进程。这样就会让我们产生并行的错觉。因为 CPU 执行速度很快,进程间的换进换出也非常迅速,因此我们很难对多个并行进程进行跟踪。所以,操作系统的设计者开发了用于描述并行的一种概念模型(顺序进程),使得并行更加容易理解和分析。

进程模型

一个进程就是一个正在执行的程序的实例,进程也包括程序计数器、寄存器和变量的当前值。从概念上来说,每个进程都有各自的虚拟 CPU,但是实际情况是 CPU 会在各个进程之间进行来回切换。

如上图所示,这是一个具有 4 个程序的多道处理程序,在进程不断切换的过程中,程序计数器也在不同的变化。

在上图中,这 4 道程序被抽象为 4 个拥有各自控制流程(即每个自己的程序计数器)的进程,并且每个程序都独立的运行。当然,实际上只有一个物理程序计数器,每个程序要运行时,其逻辑程序计数器会装载到物理程序计数器中。当程序运行结束后,其物理程序计数器就会是真正的程序计数器,然后再把它放回进程的逻辑计数器中。
从下图我们可以看到,在观察足够长的一段时间后,所有的进程都运行了,但在任何一个给定的瞬间仅有一个进程真正运行

因此,当我们说一个 CPU 只能真正一次运行一个进程的时候,即使有 2 个核(或 CPU),每一个核也只能一次运行一个线程
由于 CPU 会在各个进程之间来回快速切换,所以每个进程在 CPU 中的运行时间是无法确定的。并且当同一个进程再次在 CPU 中运行时,其在 CPU 内部的运行时间往往也是不固定的。
这里的关键思想是认识到一个进程所需的条件,进程是某一类特定活动的总和,它有程序、输入输出以及状态。

进程的创建

操作系统需要一些方式来创建进程。下面是一些创建进程的方式

  • 系统初始化(init):启动操作系统时,通常会创建若干个进程。
  • 正在运行的程序执行了创建进程的系统调用(比如 fork)
  • 用户请求创建一个新进程:在许多交互式系统中,输入一个命令或者双击图标就可以启动程序,以上任意一种操作都可以选择开启一个新的进程,在基本的 UNIX 系统中运行 X,新进程将接管启动它的窗口。
  • 初始化一个批处理工作
    从技术上讲,在所有这些情况下,让现有流程执行流程是通过创建系统调用来创建新流程的。该进程可能是正在运行的用户进程,是从键盘或鼠标调用的系统进程或批处理程序。这些就是系统调用创建新进程的过程。该系统调用告诉操作系统创建一个新进程,并直接或间接指示在其中运行哪个程序。
    在 UNIX 中,仅有一个系统调用来创建一个新的进程,这个系统调用就是 fork。这个调用会创建一个与调用进程相关的副本。在 fork 后,一个父进程和子进程会有相同的内存映像,相同的环境字符串和相同的打开文件。
    在 Windows 中,情况正相反,一个简单的 Win32 功能调用 CreateProcess,会处理流程创建并将正确的程序加载到新的进程中。这个调用会有 10 个参数,包括了需要执行的程序、输入给程序的命令行参数、各种安全属性、有关打开的文件是否继承控制位、优先级信息、进程所需要创建的窗口规格以及指向一个结构的指针,在该结构中新创建进程的信息被返回给调用者。在 Windows 中,从一开始父进程的地址空间和子进程的地址空间就是不同的

进程的终止

进程在创建之后,它就开始运行并做完成任务。然而,没有什么事儿是永不停歇的,包括进程也一样。进程早晚会发生终止,但是通常是由于以下情况触发的

  • 正常退出(自愿的) : 多数进程是由于完成了工作而终止。当编译器完成了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作。这个调用在 UNIX 中是 exit ,在 Windows 中是 ExitProcess
  • 错误退出(自愿的):比如执行一条不存在的命令,于是编译器就会提醒并退出。
  • 严重错误(非自愿的)
  • 被其他进程杀死(非自愿的) : 某个进程执行系统调用告诉操作系统杀死某个进程。在 UNIX 中,这个系统调用是 kill。在 Win32 中对应的函数是 TerminateProcess(注意不是系统调用)。

进程的层次结构

在一些系统中,当一个进程创建了其他进程后,父进程和子进程就会以某种方式进行关联。子进程它自己就会创建更多进程,从而形成一个进程层次结构。

UNIX 进程体系

在 UNIX 中,进程和它的所有子进程以及子进程的子进程共同组成一个进程组。当用户从键盘中发出一个信号后,该信号被发送给当前与键盘相关的进程组中的所有成员(它们通常是在当前窗口创建的所有活动进程)。每个进程可以分别捕获该信号、忽略该信号或采取默认的动作,即被信号 kill 掉。整个操作系统中所有的进程都隶属于一个单个以 init 为根的进程树。

Windows 进程体系

相反,Windows 中没有进程层次的概念,Windows 中所有进程都是平等的,唯一类似于层次结构的是在创建进程的时候,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程。然而,这个令牌可能也会移交给别的操作系统,这样就不存在层次结构了。而在 UNIX 中,进程不能剥夺其子进程的 进程权。(这样看来,还是 Windows 比较)。

进程状态

尽管每个进程是一个独立的实体,有其自己的程序计数器和内部状态,但是,进程之间仍然需要相互帮助。当一个进程开始运行时,它可能会经历下面这几种状态

图中会涉及三种状态

  1. 运行态,运行态指的就是进程实际占用 CPU 时间片运行时
  2. 就绪态,就绪态指的是可运行,但因为其他进程正在运行而处于就绪状态
  3. 阻塞态,除非某种外部事件发生,否则进程不能运行

进程的实现

操作系统为了执行进程间的切换,会维护着一张表,这张表就是 进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息。
下面展示了一个典型系统中的关键字段

第一列内容与进程管理有关,第二列内容与 存储管理有关,第三列内容与文件管理有关。
现在我们应该对进程表有个大致的了解了,就可以在对单个 CPU 上如何运行多个顺序进程的错觉做更多的解释。与每一 I/O 类相关联的是一个称作 中断向量(interrupt vector) 的位置(靠近内存底部的固定区域)。它包含中断服务程序的入口地址。假设当一个磁盘中断发生时,用户进程 3 正在运行,则中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址。这就是硬件所做的事情。然后软件就随即接管一切剩余的工作。
当中断结束后,操作系统会调用一个 C 程序来处理中断剩下的工作。在完成剩下的工作后,会使某些进程就绪,接着调用调度程序,决定随后运行哪个进程。然后将控制权转移给一段汇编语言代码,为当前的进程装入寄存器值以及内存映射并启动该进程运行,下面显示了中断处理和调度的过程。

  1. 硬件压入堆栈程序计数器等
  2. 硬件从中断向量装入新的程序计数器
  3. 汇编语言过程保存寄存器的值
  4. 汇编语言过程设置新的堆栈
  5. C 中断服务器运行(典型的读和缓存写入)
  6. 调度器决定下面哪个程序先运行
  7. C 过程返回至汇编代码
  8. 汇编语言过程开始运行新的当前进程
    一个进程在执行过程中可能被中断数千次,但关键每次中断后,被中断的进程都返回到与中断发生前完全相同的状态。
    线程
    在传统的操作系统中,每个进程都有一个地址空间和一个控制线程。事实上,这是大部分进程的定义。不过,在许多情况下,经常存在同一地址空间中运行多个控制线程的情形,这些线程就像是分离的进程。下面我们就着重探讨一下什么是线程

线程的使用

或许这个疑问也是你的疑问,为什么要在进程的基础上再创建一个线程的概念,准确的说,这其实是进程模型和线程模型的讨论,回答这个问题,可能需要分三步来回答

  • 多线程之间会共享同一块地址空间和所有可用数据的能力,这是进程所不具备的
  • 线程要比进程更轻量级,由于线程更轻,所以它比进程更容易创建,也更容易撤销。在许多系统中,创建一个线程要比创建一个进程快 10 - 100 倍。
  • 第三个原因可能是性能方面的探讨,如果多个线程都是 CPU 密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的 I/O 处理,拥有多个线程能在这些活动中彼此重叠进行,从而会加快应用程序的执行速度

经典的线程模型

进程中拥有一个执行的线程,通常简写为 线程(thread)。线程会有程序计数器,用来记录接着要执行哪一条指令;线程实际上 CPU 上调度执行的实体。
下图我们可以看到三个传统的进程,每个进程有自己的地址空间和单个控制线程。每个线程都在不同的地址空间中运行

下图中,我们可以看到有一个进程三个线程的情况。每个线程都在相同的地址空间中运行。

线程不像是进程那样具备较强的独立性。同一个进程中的所有线程都会有完全一样的地址空间,这意味着它们也共享同样的全局变量。由于每个线程都可以访问进程地址空间内每个内存地址,因此一个线程可以读取、写入甚至擦除另一个线程的堆栈。线程之间除了共享同一内存空间外,还具有如下不同的内容

上图左边的是同一个进程中每个线程共享的内容,上图右边是每个线程中的内容。也就是说左边的列表是进程的属性,右边的列表是线程的属性。
线程之间的状态转换和进程之间的状态转换是一样的
每个线程都会有自己的堆栈,如下图所示

线程系统调用

进程通常会从当前的某个单线程开始,然后这个线程通过调用一个库函数(比如 thread_create )创建新的线程。线程创建的函数会要求指定新创建线程的名称。创建的线程通常都返回一个线程标识符,该标识符就是新线程的名字。
当一个线程完成工作后,可以通过调用一个函数(比如 thread_exit)来退出。紧接着线程消失,状态变为终止,不能再进行调度。在某些线程的运行过程中,可以通过调用函数例如 thread_join ,表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止。
另一个常见的线程是调用 thread_yield,它允许线程自动放弃 CPU 从而让另一个线程运行。这样一个调用还是很重要的,因为不同于进程,线程是无法利用时钟中断强制让线程让出 CPU 的。

POSIX 线程

POSIX 线程 通常称为 pthreads是一种独立于语言而存在的执行模型,以及并行执行模型。

它允许程序控制时间上重叠的多个不同的工作流程。每个工作流程都称为一个线程,可以通过调用 POSIX Threads API 来实现对这些流程的创建和控制。可以把它理解为线程的标准。

POSIX Threads 的实现在许多类似且符合 POSIX 的操作系统上可用,例如 FreeBSD、NetBSD、OpenBSD、Linux、macOS、Android、Solaris,它在现有 Windows API 之上实现了 pthread

IEEE 是世界上最大的技术专业组织,致力于为人类的利益而发展技术。

线程调用描述
pthread_create创建一个新线程
pthread_exit结束调用的线程
pthread_join等待一个特定的线程退出
pthread_yield释放 CPU 来运行另外一个线程
pthread_attr_init创建并初始化一个线程的属性结构
pthread_attr_destory删除一个线程的属性结构
所有的 Pthreads 都有特定的属性,每一个都含有标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这个属性包括堆栈大小、调度参数以及其他线程需要的项目。

线程实现

主要有三种实现方式

  • 在用户空间中实现线程;
  • 在内核空间中实现线程;
  • 在用户和内核空间中混合实现线程。
    下面我们分开讨论一下
在用户空间中实现线程

第一种方法是把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。所有的这类实现都有同样的通用结构

线程在运行时系统之上运行,运行时系统是管理线程过程的集合,包括前面提到的四个过程: pthread_create, pthread_exit, pthread_join 和 pthread_yield。

在内核中实现线程

当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。

内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同,但是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。
所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。但是在用户实现中,运行时系统始终运行自己的线程,直到内核剥夺它的 CPU 时间片(或者没有可运行的线程存在了)为止。

混合实现

结合用户空间和内核空间的优点,设计人员采用了一种内核级线程的方式,然后将用户级线程与某些或者全部内核线程多路复用起来

在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

进程间通信

进程是需要频繁的和其他进程进行交流的。下面我们会一起讨论有关 进程间通信(Inter Process Communication, IPC) 的问题。大致来说,进程间的通信机制可以分为 6 种

下面我们分别对其进行概述

信号 signal

信号是 UNIX 系统最先开始使用的进程间通信机制,因为 Linux 是继承于 UNIX 的,所以 Linux 也支持信号机制,通过向一个或多个进程发送异步事件信号来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。
你可以在 Linux 系统上输入 kill -l 来列出系统使用的信号,下面是我提供的一些信号

进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:SIGSTOPSIGKILL 信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。如果选择交给内核进行处理,那么就执行默认处理。
操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。

管道 pipe

Linux 系统中的进程可以通过建立管道 pipe 进行通信

在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的管线 pipelines 就是用管道实现的,当 shell 发现输出

sort <f | head

它会创建两个进程,一个是 sort,一个是 head,sort,会在这两个应用程序之间建立一个管道使得 sort 进程的标准输出作为 head 程序的标准输入。sort 进程产生的输出就不用写到文件中了,如果管道满了系统会停止 sort 以等待 head 读出数据

管道实际上就是 |,两个应用程序不知道有管道的存在,一切都是由 shell 管理和控制的。

共享内存 shared memory

两个进程之间还可以通过共享内存进行进程间通信,其中两个或者多个进程可以访问公共内存空间。两个进程的共享工作是通过共享内存完成的,一个进程所作的修改可以对另一个进程可见 (很像线程间的通信)。

在使用共享内存前,需要经过一系列的调用流程,流程如下

  • 创建共享内存段或者使用已创建的共享内存段(shmget())
  • 将进程附加到已经创建的内存段中(shmat())
  • 从已连接的共享内存段分离进程(shmdt())
  • 对共享内存段执行控制操作(shmctl())

先入先出队列 FIFO

先入先出队列 FIFO 通常被称为 命名管道(Named Pipes),命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件和独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为

写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。

消息队列 Message Queue

一听到消息队列这个名词你可能不知道是什么意思,消息队列是用来描述内核寻址空间内的内部链接列表。可以按几种不同的方式将消息按顺序发送到队列并从队列中检索消息。每个消息队列由 IPC 标识符唯一标识。消息队列有两种模式,一种是严格模式, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 非严格模式,消息的顺序性不是非常重要。

套接字 Socket

还有一种管理两个进程间通信的是使用 socket,socket 提供端到端的双相通信。一个套接字可以与一个或多个进程关联。就像管道有命令管道和未命名管道一样,套接字也有两种模式,套接字一般用于两个进程之间的网络通信,网络套接字需要来自诸如TCP(传输控制协议)或较低级别UDP(用户数据报协议)等基础协议的支持。
套接字有以下几种分类

  • 顺序包套接字(Sequential Packet Socket): 此类套接字为最大长度固定的数据报提供可靠的连接。此连接是双向的并且是顺序的。
  • 数据报套接字(Datagram Socket):数据包套接字支持双向数据流。数据包套接字接受消息的顺序与发送者可能不同。
  • 流式套接字(Stream Socket):流套接字的工作方式类似于电话对话,提供双向可靠的数据流。
  • 原始套接字(Raw Socket): 可以使用原始套接字访问基础通信协议。

调度

当一个计算机是多道程序设计系统时,会频繁的有很多进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程 / 线程处于就绪状态时,就会发生这种情况。如果只有一个 CPU 可用,那么必须选择接下来哪个进程 / 线程可以运行。操作系统中有一个叫做 调度程序(scheduler) 的角色存在,它就是做这件事儿的,该程序使用的算法叫做 调度算法(scheduling algorithm)

调度算法的分类

毫无疑问,不同的环境下需要不同的调度算法。之所以出现这种情况,是因为不同的应用程序和不同的操作系统有不同的目标。也就是说,在不同的系统中,调度程序的优化也是不同的。这里有必要划分出三种环境

  • 批处理(Batch) : 商业领域
  • 交互式(Interactive) : 交互式用户环境
  • 实时(Real time)

批处理中的调度

现在让我们把目光从一般性的调度转换为特定的调度算法。下面我们会探讨在批处理中的调度。

先来先服务

最简单的非抢占式调度算法的设计就是 先来先服务(first-come,first-serverd)。当第一个任务从外部进入系统时,将会立即启动并允许运行任意长的时间。它不会因为运行时间太长而中断。当其他作业进入时,它们排到就绪队列尾部。当正在运行的进程阻塞,处于等待队列的第一个进程就开始运行。当一个阻塞的进程重新处于就绪态时,它会像一个新到达的任务,会排在队列的末尾,即排在所有进程最后。

这个算法的强大之处在于易于理解和编程,在这个算法中,一个单链表记录了所有就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程即可;要添加一个新的作业或者阻塞一个进程,只要把这个作业或进程附加在队列的末尾即可。这是很简单的一种实现。

最短作业优先

批处理中,第二种调度算法是 最短作业优先(Shortest Job First),我们假设运行时间已知。例如,一家保险公司,因为每天要做类似的工作,所以人们可以相当精确地预测处理 1000 个索赔的一批作业需要多长时间。当输入队列中有若干个同等重要的作业被启动时,调度程序应使用最短优先作业算法

需要注意的是,在所有的进程都可以运行的情况下,最短作业优先的算法才是最优的。

最短剩余时间优先

最短作业优先的抢占式版本被称作为 最短剩余时间优先(Shortest Remaining Time Next) 算法。使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。

交互式系统中的调度

交互式系统中在个人计算机、服务器和其他系统中都是很常用的,所以有必要来探讨一下交互式调度

轮询调度

一种最古老、最简单、最公平并且最广泛使用的算法就是 轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾,就像下图的 b。

优先级调度

轮询调度假设了所有的进程是同等重要的。但事实情况可能不是这样。例如,在一所大学中的等级制度,首先是院长,然后是教授、秘书、后勤人员,最后是学生。这种将外部情况考虑在内就实现了优先级调度(priority scheduling)

它的基本思想很明确,每个进程都被赋予一个优先级,优先级高的进程优先运行。

多级队列

最早使用优先级调度的系统是 CTSS(Compatible TimeSharing System)。CTSS 在每次切换前都需要将当前进程换出到磁盘,并从磁盘上读入一个新进程。为 CPU 密集型进程设置较长的时间片比频繁地分给他们很短的时间要更有效(减少交换次数)。另一方面,如前所述,长时间片的进程又会影响到响应时间,解决办法是设置优先级类。属于最高优先级的进程运行一个时间片,次高优先级进程运行 2 个时间片,再下面一级运行 4 个时间片,以此类推。当一个进程用完分配的时间片后,它被移到下一类。

最短进程优先

最短进程优先是根据进程过去的行为进行推测,并执行估计运行时间最短的那一个。假设每个终端上每条命令的预估运行时间为 T0,现在假设测量到其下一次运行时间为 T1,可以用两个值的加权来改进估计时间,即aT0+ (1- 1)T1。通过选择 a 的值,可以决定是尽快忘掉老的运行时间,还是在一段长时间内始终记住它们。当 a = 1/2 时,可以得到下面这个序列
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
可以看到,在三轮过后,T0 在新的估计值中所占比重下降至 1/8。

保证调度

一种完全不同的调度方法是对用户做出明确的性能保证。一种实际而且容易实现的保证是:若用户工作时有 n 个用户登录,则每个用户将获得 CPU 处理能力的 1/n。类似地,在一个有 n 个进程运行的单用户系统中,若所有的进程都等价,则每个进程将获得 1/n 的 CPU 时间。

彩票调度

对用户进行承诺并在随后兑现承诺是一件好事,不过很难实现。但是存在着一种简单的方式,有一种既可以给出预测结果而又有一种比较简单的实现方式的算法,就是 彩票调度(lottery scheduling)算法。
其基本思想是为进程提供各种系统资源(例如 CPU 时间)的彩票。当做出一个调度决策的时候,就随机抽出一张彩票,拥有彩票的进程将获得该资源。在应用到 CPU 调度时,系统可以每秒持有 50 次抽奖,每个中奖者将获得比如 20 毫秒的 CPU 时间作为奖励。

公平分享调度

到目前为止,我们假设被调度的都是各个进程自身,而不用考虑该进程的拥有者是谁。结果是,如果用户 1 启动了 9 个进程,而用户 2 启动了一个进程,使用轮转或相同优先级调度算法,那么用户 1 将得到 90 % 的 CPU 时间,而用户 2 将之得到 10 % 的 CPU 时间。
为了阻止这种情况的出现,一些系统在调度前会把进程的拥有者考虑在内。在这种模型下,每个用户都会分配一些 CPU 时间,而调度程序会选择进程并强制执行。因此如果两个用户每个都会有 50% 的 CPU 时间片保证,那么无论一个用户有多少个进程,都将获得相同的 CPU 份额。

实时系统中的调度

实时系统(real-time) 是一个时间扮演了重要作用的系统。实时系统可以分为两类,硬实时(hard real time)软实时(soft real time) 系统,前者意味着必须要满足绝对的截止时间;后者的含义是虽然不希望偶尔错失截止时间,但是可以容忍。
实时系统中的事件可以按照响应方式进一步分类为周期性(以规则的时间间隔发生)事件或 非周期性(发生时间不可预知)事件。一个系统可能要响应多个周期性事件流,根据每个事件处理所需的时间,可能甚至无法处理所有事件。例如,如果有 m 个周期事件,事件 i 以周期 Pi 发生,并需要 Ci 秒 CPU 时间处理一个事件,那么可以处理负载的条件是

只有满足这个条件的实时系统称为可调度的,这意味着它实际上能够被实现。一个不满足此检验标准的进程不能被调度,因为这些进程共同需要的 CPU 时间总和大于 CPU 能提供的时间。
下面我们来了解一下内存管理,你需要知道的知识点如下

地址空间

如果要使多个应用程序同时运行在内存中,必须要解决两个问题:保护重定位。第一种解决方式是用保护密钥标记内存块,并将执行过程的密钥与提取的每个存储字的密钥进行比较。这种方式只能解决第一种问题(破坏操作系统),但是不能解决多进程在内存中同时运行的问题。
还有一种更好的方式是创造一个存储器抽象:地址空间(the address space)。就像进程的概念创建了一种抽象的 CPU 来运行程序,地址空间也创建了一种抽象内存供程序使用。

基址寄存器和变址寄存器

最简单的办法是使用动态重定位(dynamic relocation)技术,它就是通过一种简单的方式将每个进程的地址空间映射到物理内存的不同区域。还有一种方式是使用基址寄存器和变址寄存器。

  • 基址寄存器:存储数据内存的起始位置
  • 变址寄存器:存储应用程序的长度。
    每当进程引用内存以获取指令或读取、写入数据时,CPU 都会自动将基址值添加到进程生成的地址中,然后再将其发送到内存总线上。同时,它检查程序提供的地址是否大于或等于变址寄存器 中的值。如果程序提供的地址要超过变址寄存器的范围,那么会产生错误并中止访问。

交换技术

在程序运行过程中,经常会出现内存不足的问题。
针对上面内存不足的问题,提出了两种处理方式:最简单的一种方式就是交换(swapping)技术,即把一个进程完整的调入内存,然后再内存中运行一段时间,再把它放回磁盘。空闲进程会存储在磁盘中,所以这些进程在没有运行时不会占用太多内存。另外一种策略叫做虚拟内存(virtual memory),虚拟内存技术能够允许应用程序部分的运行在内存中。下面我们首先先探讨一下交换

交换过程

下面是一个交换过程

刚开始的时候,只有进程 A 在内存中,然后从创建进程 B 和进程 C 或者从磁盘中把它们换入内存,然后在图 d 中,A 被换出内存到磁盘中,最后 A 重新进来。因为图 g 中的进程 A 现在到了不同的位置,所以在装载过程中需要被重新定位,或者在交换程序时通过软件来执行;或者在程序执行期间通过硬件来重定位。基址寄存器和变址寄存器就适用于这种情况。

交换在内存创建了多个 空闲区(hole),内存会把所有的空闲区尽可能向下移动合并成为一个大的空闲区。这项技术称为内存紧缩(memory compaction)。但是这项技术通常不会使用,因为这项技术会消耗很多 CPU 时间。

空闲内存管理

在进行内存动态分配时,操作系统必须对其进行管理。大致上说,有两种监控内存使用的方式

  • 位图(bitmap)
  • 空闲列表(free lists)
使用位图的存储管理

使用位图方法时,内存可能被划分为小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0 表示空闲, 1 表示占用(或者相反)。一块内存区域和其对应的位图如下

位图提供了一种简单的方法在固定大小的内存中跟踪内存的使用情况,因为位图的大小取决于内存和分配单元的大小。这种方法有一个问题是,当决定为把具有 k 个分配单元的进程放入内存时,内容管理器(memory manager) 必须搜索位图,在位图中找出能够运行 k 个连续 0 位的串。在位图中找出制定长度的连续 0 串是一个很耗时的操作,这是位图的缺点。(可以简单理解为在杂乱无章的数组中,找出具有一大长串空闲的数组单元)

使用链表进行管理

另一种记录内存使用情况的方法是,维护一个记录已分配内存段和空闲内存段的链表,段会包含进程或者是两个进程的空闲区域。可用上面的图 c 来表示内存的使用情况。链表中的每一项都可以代表一个 空闲区(H) 或者是进程(P)的起始标志,长度和下一个链表项的位置。

当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以为创建的进程(或者从磁盘中换入的进程)分配内存。我们先假设内存管理器知道应该分配多少内存,最简单的算法是使用 首次适配(first fit)。内存管理器会沿着段列表进行扫描,直到找个一个足够大的空闲区为止。 除非空闲区大小和要分配的空间大小一样,否则将空闲区分为两部分,一部分供进程使用;一部分生成新的空闲区。首次适配算法是一种速度很快的算法,因为它会尽可能的搜索链表。
首次适配的一个小的变体是 下次适配(next fit)。它和首次匹配的工作方式相同,只有一个不同之处那就是下次适配在每次找到合适的空闲区时就会记录当时的位置,以便下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次匹配算法那样每次都会从头开始搜索。
另外一个著名的并且广泛使用的算法是 最佳适配(best fit)。最佳适配会从头到尾寻找整个链表,找出能够容纳进程的最小空闲区。
虚拟内存
尽管基址寄存器和变址寄存器用来创建地址空间的抽象,但是这有一个其他的问题需要解决:管理软件的不断增大(managing bloatware)。虚拟内存的基本思想是,每个程序都有自己的地址空间,这个地址空间被划分为多个称为页面(page)的块。每一页都是连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,硬件会立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。

分页

大部分使用虚拟内存的系统中都会使用一种 分页(paging) 技术。在任何一台计算机上,程序会引用使用一组内存地址。当程序执行

MOV REG,1000

这条指令时,它会把内存地址为 1000 的内存单元的内容复制到 REG 中(或者相反,这取决于计算机)。地址可以通过索引、基址寄存器、段寄存器或其他方式产生。
这些程序生成的地址被称为 虚拟地址(virtual addresses) 并形成虚拟地址空间(virtual address space),在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存中线上,读写操作都使用同样地址的物理内存。在使用虚拟内存时,虚拟地址不会直接发送到内存总线上。相反,会使用 MMU(Memory Management Unit) 内存管理单元把虚拟地址映射为物理内存地址,像下图这样

下面这幅图展示了这种映射是如何工作的

页表给出虚拟地址与物理内存地址之间的映射关系。每一页起始于 4096 的倍数位置,结束于 4095 的位置,所以 4K 到 8K 实际为 4096 - 8191 ,8K - 12K 就是 8192 - 12287
在这个例子中,我们可能有一个 16 位地址的计算机,地址从 0 - 64 K - 1,这些是虚拟地址。然而只有 32 KB 的物理地址。所以虽然可以编写 64 KB 的程序,但是程序无法全部调入内存运行,在磁盘上必须有一个最多 64 KB 的程序核心映像的完整副本,以保证程序片段在需要时被调入内存。

页表

虚拟页号可作为页表的索引用来找到虚拟页中的内容。由页表项可以找到页框号(如果有的话)。然后把页框号拼接到偏移量的高位端,以替换掉虚拟页号,形成物理地址。

因此,页表的目的是把虚拟页映射到页框中。从数学上说,页表是一个函数,它的参数是虚拟页号,结果是物理页框号。

通过这个函数可以把虚拟地址中的虚拟页转换为页框,从而形成物理地址。

页表项的结构

下面我们探讨一下页表项的具体结构,上面你知道了页表项的大致构成,是由页框号和在 / 不在位构成的,现在我们来具体探讨一下页表项的构成

页表项的结构是与机器相关的,但是不同机器上的页表项大致相同。上面是一个页表项的构成,不同计算机的页表项可能不同,但是一般来说都是 32 位的。页表项中最重要的字段就是页框号(Page frame number)。毕竟,页表到页框最重要的一步操作就是要把此值映射过去。下一个比较重要的就是在/不在位,如果此位上的值是 1,那么页表项是有效的并且能够被使用。如果此值是 0 的话,则表示该页表项对应的虚拟页面不在内存中,访问该页面会引起一个缺页异常(page fault)
保护位(Protection) 告诉我们哪一种访问是允许的,啥意思呢?最简单的表示形式是这个域只有一位,0 表示可读可写,1 表示的是只读
修改位(Modified)访问位(Referenced) 会跟踪页面的使用情况。当一个页面被写入时,硬件会自动的设置修改位。修改位在页面重新分配页框时很有用。如果一个页面已经被修改过(即它是 的),则必须把它写回磁盘。如果一个页面没有被修改过(即它是 干净的),那么重新分配时这个页框会被直接丢弃,因为磁盘上的副本仍然是有效的。这个位有时也叫做 脏位(dirty bit),因为它反映了页面的状态。
访问位(Referenced) 在页面被访问时被设置,不管是读还是写。这个值能够帮助操作系统在发生缺页中断时选择要淘汰的页。不再使用的页要比正在使用的页更适合被淘汰。这个位在后面要讨论的页面置换算法中作用很大。
最后一位用于禁止该页面被高速缓存,这个功能对于映射到设备寄存器还是内存中起到了关键作用。通过这一位可以禁用高速缓存。具有独立的 I/O 空间而不是用内存映射 I/O 的机器来说,并不需要这一位。
页面置换算法
下面我们就来探讨一下有哪些页面置换算法。

最优页面置换算法

最优的页面置换算法的工作流程如下:在缺页中断发生时,这些页面之一将在下一条指令(包含该指令的页面)上被引用。其他页面则可能要到 10、100 或者 1000 条指令后才会被访问。每个页面都可以用在该页首次被访问前所要执行的指令数作为标记。
最优化的页面算法表明应该标记最大的页面。如果一个页面在 800 万条指令内不会被使用,另外一个页面在 600 万条指令内不会被使用,则置换前一个页面,从而把需要调入这个页面而发生的缺页中断推迟。计算机也像人类一样,会把不愿意做的事情尽可能的往后拖。
这个算法最大的问题时无法实现。当缺页中断发生时,操作系统无法知道各个页面的下一次将在什么时候被访问。这种算法在实际过程中根本不会使用。

最近未使用页面置换算法

为了能够让操作系统收集页面使用信息,大部分使用虚拟地址的计算机都有两个状态位,R 和 M,来和每个页面进行关联。每当引用页面(读入或写入)时都设置 R,写入(即修改)页面时设置 M,这些位包含在每个页表项中,就像下面所示

因为每次访问时都会更新这些位,因此由硬件来设置它们非常重要。一旦某个位被设置为 1,就会一直保持 1 直到操作系统下次来修改此位。
如果硬件没有这些位,那么可以使用操作系统的缺页中断时钟中断机制来进行模拟。当启动一个进程时,将其所有的页面都标记为不在内存;一旦访问任何一个页面就会引发一次缺页中断,此时操作系统就可以设置 R 位(在它的内部表中),修改页表项使其指向正确的页面,并设置为 READ ONLY 模式,然后重新启动引起缺页中断的指令。如果页面随后被修改,就会发生另一个缺页异常。从而允许操作系统设置 M 位并把页面的模式设置为 READ/WRITE
可以用 R 位和 M 位来构造一个简单的页面置换算法:当启动一个进程时,操作系统将其所有页面的两个位都设置为 0。R 位定期的被清零(在每个时钟中断)。用来将最近未引用的页面和已引用的页面分开。
当出现缺页中断后,操作系统会检查所有的页面,并根据它们的 R 位和 M 位将当前值分为四类:

  • 第 0 类:没有引用 R,没有修改 M
  • 第 1 类:没有引用 R,已修改 M
  • 第 2 类:引用 R ,没有修改 M
  • 第 3 类:已被访问 R,已被修改 M
    尽管看起来好像无法实现第一类页面,但是当第三类页面的 R 位被时钟中断清除时,它们就会发生。时钟中断不会清除 M 位,因为需要这个信息才能知道是否写回磁盘中。清除 R 但不清除 M 会导致出现一类页面。
    NRU(Not Recently Used) 算法从编号最小的非空类中随机删除一个页面。此算法隐含的思想是,在一个时钟内(约 20 ms)淘汰一个已修改但是没有被访问的页面要比一个大量引用的未修改页面好,NRU 的主要优点是易于理解并且能够有效的实现

先进先出页面置换算法

另一种开销较小的方式是使用 FIFO(First-In,First-Out) 算法,这种类型的数据结构也适用在页面置换算法中。由操作系统维护一个所有在当前内存中的页面的链表,最早进入的放在表头,最新进入的页面放在表尾。在发生缺页异常时,会把头部的页移除并且把新的页添加到表尾。

第二次机会页面置换算法

我们上面学到的 FIFO 链表页面有个缺陷,那就是出链和入链并不会进行 check 检查,这样就会容易把经常使用的页面置换出去,为了避免这一问题,我们对该算法做一个简单的修改:我们检查最老页面的 R 位,如果是 0 ,那么这个页面就是最老的而且没有被使用,那么这个页面就会被立刻换出。如果 R 位是 1,那么就清除此位,此页面会被放在链表的尾部,修改它的装入时间就像刚放进来的一样。然后继续搜索。
这种算法叫做 第二次机会(second chance)算法,就像下面这样,我们看到页面 A 到 H 保留在链表中,并按到达内存的时间排序。

a)按照先进先出的方法排列的页面;b)在时刻 20 处发生缺页异常中断并且 A 的 R 位已经设置时的页面链表。
假设缺页异常发生在时刻 20 处,这时最老的页面是 A ,它是在 0 时刻到达的。如果 A 的 R 位是 0,那么它将被淘汰出内存,或者把它写回磁盘(如果它已经被修改过),或者只是简单的放弃(如果它是未被修改过)。另一方面,如果它的 R 位已经设置了,则将 A 放到链表的尾部并且重新设置装入时间为当前时刻(20 处),然后清除 R 位。然后从 B 页面开始继续搜索合适的页面。
寻找第二次机会的是在最近的时钟间隔中未被访问过的页面。如果所有的页面都被访问过,该算法就会被简化为单纯的 FIFO 算法。具体来说,假设图 a 中所有页面都设置了 R 位。操作系统将页面依次移到链表末尾,每次都在添加到末尾时清除 R 位。最后,算法又会回到页面 A,此时的 R 位已经被清除,那么页面 A 就会被执行出链处理,因此算法能够正常结束。

时钟页面置换算法

一种比较好的方式是把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面。如下图所示

当缺页错误出现时,算法首先检查表针指向的页面,如果它的 R 位是 0 就淘汰该页面,并把新的页面插入到这个位置,然后把表针向前移动一位;如果 R 位是 1 就清除 R 位并把表针前移一个位置。重复这个过程直到找到了一个 R 位为 0 的页面位置。了解这个算法的工作方式,就明白为什么它被称为 时钟(clokc)算法了。

最近最少使用页面置换算法

在前面几条指令中频繁使用的页面和可能在后面的几条指令中被使用。反过来说,已经很久没有使用的页面有可能在未来一段时间内仍不会被使用。这个思想揭示了一个可以实现的算法:在缺页中断时,置换未使用时间最长的页面。这个策略称为 LRU(Least Recently Used) ,最近最少使用页面置换算法。
虽然 LRU 在理论上是可以实现的,但是从长远看来代价比较高。为了完全实现 LRU,会在内存中维护一个所有页面的链表,最频繁使用的页位于表头,最近最少使用的页位于表尾。困难的是在每次内存引用时更新整个链表。在链表中找到一个页面,删除它,然后把它移动到表头是一个非常耗时的操作,即使使用硬件来实现也是一样的费时。

用软件模拟 LRU

尽管上面的 LRU 算法在原则上是可以实现的,但是很少有机器能够拥有那些特殊的硬件。上面是硬件的实现方式,那么现在考虑要用软件来实现 LRU 。一种可以实现的方案是 NFU(Not Frequently Used,最不常用)算法。它需要一个软件计数器来和每个页面关联,初始化的时候是 0 。在每个时钟中断时,操作系统会浏览内存中的所有页,会将每个页面的 R 位(0 或 1)加到它的计数器上。这个计数器大体上跟踪了各个页面访问的频繁程度。当缺页异常出现时,则置换计数器值最小的页面。
只需要对 NFU 做一个简单的修改就可以让它模拟 LRU,这个修改有两个步骤

  • 首先,在 R 位被添加进来之前先把计数器右移一位;
  • 第二步,R 位被添加到最左边的位而不是最右边的位。
    修改以后的算法称为 老化(aging) 算法,下图解释了老化算法是如何工作的。

    我们假设在第一个时钟周期内页面 0 - 5 的 R 位依次是 1,0,1,0,1,1,(也就是页面 0 是 1,页面 1 是 0,页面 2 是 1 这样类推)。也就是说,在 0 个时钟周期到 1 个时钟周期之间,0,2,4,5 都被引用了,从而把它们的 R 位设置为 1,剩下的设置为 0 。在相关的六个计数器被右移之后 R 位被添加到 左侧 ,就像上图中的 a。剩下的四列显示了接下来的四个时钟周期内的六个计数器变化。

CPU 正在以某个频率前进,该频率的周期称为时钟滴答时钟周期。一个 100Mhz 的处理器每秒将接收 100,000,000 个时钟滴答。
当缺页异常出现时,将置换(就是移除)计数器值最小的页面。如果一个页面在前面 4 个时钟周期内都没有被访问过,那么它的计数器应该会有四个连续的 0 ,因此它的值肯定要比前面 3 个时钟周期内都没有被访问过的页面的计数器小。
这个算法与 LRU 算法有两个重要的区别:看一下上图中的 e,第三列和第五列

工作集时钟页面置换算法

当缺页异常发生后,需要扫描整个页表才能确定被淘汰的页面,因此基本工作集算法还是比较浪费时间的。一个对基本工作集算法的提升是基于时钟算法但是却使用工作集的信息,这种算法称为WSClock(工作集时钟)。由于它的实现简单并且具有高性能,因此在实践中被广泛应用。
与时钟算法一样,所需的数据结构是一个以页框为元素的循环列表,就像下面这样

​ 工作集时钟页面置换算法的操作:a) 和 b) 给出 R = 1 时所发生的情形;c) 和 d) 给出 R = 0 的例子
最初的时候,该表是空的。当装入第一个页面后,把它加载到该表中。随着更多的页面的加入,它们形成一个环形结构。每个表项包含来自基本工作集算法的上次使用时间,以及 R 位(已标明)和 M 位(未标明)。
与时钟算法一样,在每个缺页异常时,首先检查指针指向的页面。如果 R 位被是设置为 1,该页面在当前时钟周期内就被使用过,那么该页面就不适合被淘汰。然后把该页面的 R 位置为 0,指针指向下一个页面,并重复该算法。该事件序列化后的状态参见图 b。
现在考虑指针指向的页面 R = 0 时会发生什么,参见图 c,如果页面的使用期限大于 t 并且页面为被访问过,那么这个页面就不会在工作集中,并且在磁盘上会有一个此页面的副本。申请重新调入一个新的页面,并把新的页面放在其中,如图 d 所示。另一方面,如果页面被修改过,就不能重新申请页面,因为这个页面在磁盘上没有有效的副本。为了避免由于调度写磁盘操作引起的进程切换,指针继续向前走,算法继续对下一个页面进行操作。毕竟,有可能存在一个老的,没有被修改过的页面可以立即使用。
原则上来说,所有的页面都有可能因为磁盘I/O 在某个时钟周期内被调度。为了降低磁盘阻塞,需要设置一个限制,即最大只允许写回 n 个页面。一旦达到该限制,就不允许调度新的写操作。
那么就有个问题,指针会绕一圈回到原点的,如果回到原点,它的起始点会发生什么?这里有两种情况:

  • 至少调度了一次写操作
  • 没有调度过写操作
    在第一种情况中,指针仅仅是不停的移动,寻找一个未被修改过的页面。由于已经调度了一个或者多个写操作,最终会有某个写操作完成,它的页面会被标记为未修改。置换遇到的第一个未被修改过的页面,这个页面不一定是第一个被调度写操作的页面,因为硬盘驱动程序为了优化性能可能会把写操作重排序。
    对于第二种情况,所有的页面都在工作集中,否则将至少调度了一个写操作。由于缺乏额外的信息,最简单的方法就是置换一个未被修改的页面来使用,扫描中需要记录未被修改的页面的位置,如果不存在未被修改的页面,就选定当前页面并把它写回磁盘。

页面置换算法小结

我们到现在已经研究了各种页面置换算法,现在我们来一个简单的总结,算法的总结归纳如下

算法注释
最优算法不可实现,但可以用作基准
NRU(最近未使用) 算法和 LRU 算法很相似
FIFO(先进先出) 算法有可能会抛弃重要的页面
第二次机会算法比 FIFO 有较大的改善
时钟算法实际使用
LRU(最近最少) 算法比较优秀,但是很难实现
NFU(最不经常食用) 算法和 LRU 很类似
老化算法近似 LRU 的高效算法
工作集算法实施起来开销很大
工作集时钟算法比较有效的算法
* `最优算法`在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,`因此实际上该算法不能使用`。然而,它可以作为衡量其他算法的标准。 * `NRU` 算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。 * `FIFO` 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。 * `第二次机会`算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。 * `时钟` 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。 * `LRU` 算法是一个非常优秀的算法,但是没有`特殊的硬件(TLB)`很难实现。如果没有硬件,就不能使用 LRU 算法。 * `NFU` 算法是一种近似于 LRU 的算法,它的性能不是非常好。 * `老化` 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择 * 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。`WSClock` 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。 总之,**最好的算法是老化算法和 WSClock 算法**。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。 下面来聊一聊文件系统,你需要知道下面这些知识点 ![](https://i-blog.csdnimg.cn/img_convert/35f4dc7005e1ca6e287ad3820e7c7d51.png) # 文件 ### 文件命名 文件是一种抽象机制,它提供了一种方式用来存储信息以及在后面进行读取。可能任何一种机制最重要的特性就是管理对象的命名方式。在创建一个文件后,它会给文件一个命名。当进程终止时,文件会继续存在,并且其他进程可以使用`名称访问该文件`。 文件命名规则对于不同的操作系统来说是不一样的,但是所有现代操作系统都允许使用 1 - 8 个字母的字符串作为合法文件名。 某些文件区分大小写字母,而大多数则不区分。`UNIX` 属于第一类;历史悠久的 `MS-DOS` 属于第二类(顺便说一句,尽管 MS-DOS 历史悠久,但 MS-DOS 仍在嵌入式系统中非常广泛地使用,因此它绝不是过时的);因此,UNIX 系统会有三种不同的命名文件:`maria`、`Maria`、`MARIA` 。在 MS-DOS ,所有这些命名都属于相同的文件。 ![](https://i-blog.csdnimg.cn/img_convert/33a0adeeffca25e42957a355916d406b.png) 许多操作系统支持两部分的文件名,它们之间用 `.` 分隔开,比如文件名 `prog.c`。原点后面的文件称为 `文件扩展名(file extension)` ,文件扩展名通常表示文件的一些信息。一些常用的文件扩展名以及含义如下图所示
扩展名含义
bak备份文件
cc 源程序文件
gif符合图形交换格式的图像文件
hlp帮助文件
htmlWWW 超文本标记语言文档
jpg符合 JPEG 编码标准的静态图片
mp3符合 MP3 音频编码格式的音乐文件
mpg符合 MPEG 编码标准的电影
o目标文件(编译器输出格式,尚未链接)
pdfpdf 格式的文件
psPostScript 文件
tex为 TEX 格式化程序准备的输入文件
txt文本文件
zip压缩文件
在 UNIX 系统中,文件扩展名只是一种约定,操作系统并不强制采用。

文件结构

文件的构造有多种方式。下图列出了常用的三种构造方式

​ 三种不同的文件。 a) 字节序列 。b) 记录序列。c) 树
上图中的 a 是一种无结构的字节序列,操作系统不关心序列的内容是什么,操作系统能看到的就是字节(bytes)。其文件内容的任何含义只在用户程序中进行解释。UNIX 和 Windows 都采用这种办法。
图 b 表示在文件结构上的第一部改进。在这个模型中,文件是具有固定长度记录的序列,每个记录都有其内部结构。 把文件作为记录序列的核心思想是:读操作返回一个记录,而写操作重写或者追加一个记录。第三种文件结构如上图 c 所示。在这种组织结构中,文件由一颗记录树构成,记录树的长度不一定相同,每个记录树都在记录中的固定位置包含一个key 字段。这棵树按 key 进行排序,从而可以对特定的 key 进行快速查找。

文件类型

很多操作系统支持多种文件类型。例如,UNIX(同样包括 OS X)和 Windows 都具有常规的文件和目录。除此之外,UNIX 还具有字符特殊文件(character special file)块特殊文件(block special file)常规文件(Regular files) 是包含有用户信息的文件。用户一般使用的文件大都是常规文件,常规文件一般包括 可执行文件、文本文件、图像文件,从常规文件读取数据或将数据写入时,内核会根据文件系统的规则执行操作,是写入可能被延迟,记录日志或者接受其他操作。

文件访问

早期的操作系统只有一种访问方式:序列访问(sequential access)。在这些系统中,进程可以按照顺序读取所有的字节或文件中的记录,但是不能跳过并乱序执行它们。顺序访问文件是可以返回到起点的,需要时可以多次读取该文件。当存储介质是磁带而不是磁盘时,顺序访问文件很方便。
在使用磁盘来存储文件时,可以不按照顺序读取文件中的字节或者记录,或者按照关键字而不是位置来访问记录。这种能够以任意次序进行读取的称为随机访问文件(random access file)。许多应用程序都需要这种方式。
随机访问文件对许多应用程序来说都必不可少,例如,数据库系统。如果乘客打电话预定某航班机票,订票程序必须能够直接访问航班记录,而不必先读取其他航班的成千上万条记录。
有两种方法可以指示从何处开始读取文件。第一种方法是直接使用 read 从头开始读取。另一种是用一个特殊的 seek 操作设置当前位置,在 seek 操作后,从这个当前位置顺序地开始读文件。UNIX 和 Windows 使用的是后面一种方式。

文件属性

文件包括文件名和数据。除此之外,所有的操作系统还会保存其他与文件相关的信息,如文件创建的日期和时间、文件大小。我们可以称这些为文件的属性(attributes)。有些人也喜欢把它们称作 元数据(metadata)。文件的属性在不同的系统中差别很大。文件的属性只有两种状态:设置(set)清除(clear)

文件操作

使用文件的目的是用来存储信息并方便以后的检索。对于存储和检索,不同的系统提供了不同的操作。以下是与文件有关的最常用的一些系统调用:

  1. Create,创建不包含任何数据的文件。调用的目的是表示文件即将建立,并对文件设置一些属性。
  2. Delete,当文件不再需要,必须删除它以释放内存空间。为此总会有一个系统调用来删除文件。
  3. Open,在使用文件之前,必须先打开文件。这个调用的目的是允许系统将属性和磁盘地址列表保存到主存中,用来以后的快速访问。
  4. Close,当所有进程完成时,属性和磁盘地址不再需要,因此应关闭文件以释放表空间。很多系统限制进程打开文件的个数,以此达到鼓励用户关闭不再使用的文件。磁盘以块为单位写入,关闭文件时会强制写入最后一,即使这个块空间内部还不满。
  5. Read,数据从文件中读取。通常情况下,读取的数据来自文件的当前位置。调用者必须指定需要读取多少数据,并且提供存放这些数据的缓冲区。
  6. Write,向文件写数据,写操作一般也是从文件的当前位置开始进行。如果当前位置是文件的末尾,则会直接追加进行写入。如果当前位置在文件中,则现有数据被覆盖,并且永远消失。
  7. append,使用 append 只能向文件末尾添加数据。
  8. seek,对于随机访问的文件,要指定从何处开始获取数据。通常的方法是用 seek 系统调用把当前位置指针指向文件中的特定位置。seek 调用结束后,就可以从指定位置开始读写数据了。
  9. get attributes,进程运行时通常需要读取文件属性。
  10. set attributes,用户可以自己设置一些文件属性,甚至是在文件创建之后,实现该功能的是 set attributes 系统调用。
  11. rename,用户可以自己更改已有文件的名字,rename 系统调用用于这一目的。

目录

文件系统通常提供目录(directories) 或者 文件夹(folders) 用于记录文件的位置,在很多系统中目录本身也是文件,下面我们会讨论关于文件,他们的组织形式、属性和可以对文件进行的操作。

一级目录系统

目录系统最简单的形式是有一个能够包含所有文件的目录。这种目录被称为根目录(root directory),由于根目录的唯一性,所以其名称并不重要。在最早期的个人计算机中,这种系统很常见,部分原因是因为只有一个用户。下面是一个单层目录系统的例子

​ 含有四个文件的单层目录系统
该目录中有四个文件。这种设计的优点在于简单,并且能够快速定位文件,毕竟只有一个地方可以检索。这种目录组织形式现在一般用于简单的嵌入式设备(如数码相机和某些便携式音乐播放器)上使用。

层次目录系统

对于简单的应用而言,一般都用单层目录方式,但是这种组织形式并不适合于现代计算机,因为现代计算机含有成千上万个文件和文件夹。如果都放在根目录下,查找起来会非常困难。为了解决这一问题,出现了层次目录系统(Hierarchical Directory Systems),也称为目录树。通过这种方式,可以用很多目录把文件进行分组。进而,如果多个用户共享同一个文件服务器,比如公司的网络系统,每个用户可以为自己的目录树拥有自己的私人根目录。这种方式的组织结构如下

根目录含有目录 A、B 和 C ,分别属于不同的用户,其中两个用户个字创建了子目录。用户可以创建任意数量的子目录,现代文件系统都是按照这种方式组织的。

路径名

当目录树组织文件系统时,需要有某种方法指明文件名。常用的方法有两种,第一种方式是每个文件都会用一个绝对路径名(absolute path name),它由根目录到文件的路径组成。
另外一种指定文件名的方法是 相对路径名(relative path name)。它常常和 工作目录(working directory) (也称作 当前目录(current directory))一起使用。用户可以指定一个目录作为当前工作目录。例如,如果当前目录是 /usr/ast,那么绝对路径 /usr/ast/mailbox可以直接使用 mailbox 来引用。

目录操作

不同文件中管理目录的系统调用的差别比管理文件的系统调用差别大。为了了解这些系统调用有哪些以及它们怎样工作,下面给出一个例子(取自 UNIX)。

  1. Create,创建目录,除了目录项 ... 外,目录内容为空。
  2. Delete,删除目录,只有空目录可以删除。只包含 ... 的目录被认为是空目录,这两个目录项通常不能删除
  3. opendir,目录内容可被读取。例如,未列出目录中的全部文件,程序必须先打开该目录,然后读其中全部文件的文件名。与打开和读文件相同,在读目录前,必须先打开文件。
  4. closedir,读目录结束后,应该关闭目录用于释放内部表空间。
  5. readdir,系统调用 readdir 返回打开目录的下一个目录项。以前也采用 read 系统调用来读取目录,但是这种方法有一个缺点:程序员必须了解和处理目录的内部结构。相反,不论采用哪一种目录结构,readdir 总是以标准格式返回一个目录项。
  6. rename,在很多方面目录和文件都相似。文件可以更换名称,目录也可以。
  7. link,链接技术允许在多个目录中出现同一个文件。这个系统调用指定一个存在的文件和一个路径名,并建立从该文件到路径所指名字的链接。这样,可以在多个目录中出现同一个文件。有时也被称为硬链接(hard link)
  8. unlink,删除目录项。如果被解除链接的文件只出现在一个目录中,则将它从文件中删除。如果它出现在多个目录中,则只删除指定路径名的链接,依然保留其他路径名的链接。在 UNIX 中,用于删除文件的系统调用就是 unlink。
    文件系统的实现

文件系统布局

文件系统存储在磁盘中。大部分的磁盘能够划分出一到多个分区,叫做磁盘分区(disk partitioning) 或者是磁盘分片(disk slicing)。每个分区都有独立的文件系统,每块分区的文件系统可以不同。磁盘的 0 号分区称为 主引导记录(Master Boot Record, MBR),用来引导(boot) 计算机。在 MBR 的结尾是分区表(partition table)。每个分区表给出每个分区由开始到结束的地址。
当计算机开始引 boot 时,BIOS 读入并执行 MBR。

引导块

MBR 做的第一件事就是确定活动分区,读入它的第一个块,称为引导块(boot block) 并执行。引导块中的程序将加载分区中的操作系统。为了一致性,每个分区都会从引导块开始,即使引导块不包含操作系统。引导块占据文件系统的前 4096 个字节,从磁盘上的字节偏移量 0 开始。引导块可用于启动操作系统。
除了从引导块开始之外,磁盘分区的布局是随着文件系统的不同而变化的。通常文件系统会包含一些属性,如下

​ 文件系统布局

超级块

紧跟在引导块后面的是 超级块(Superblock),超级块 的大小为 4096 字节,从磁盘上的字节偏移 4096 开始。超级块包含文件系统的所有关键参数

  • 文件系统的大小
  • 文件系统中的数据块数
  • 指示文件系统状态的标志
  • 分配组大小
    在计算机启动或者文件系统首次使用时,超级块会被读入内存。
空闲空间块

接着是文件系统中空闲块的信息,例如,可以用位图或者指针列表的形式给出。
BitMap 位图或者 Bit vector 位向量
位图或位向量是一系列位或位的集合,其中每个位对应一个磁盘块,该位可以采用两个值:0 和 1,0 表示已分配该块,而 1 表示一个空闲块。下图中的磁盘上给定的磁盘块实例(分配了绿色块)可以用 16 位的位图表示为:0000111000000110。

使用链表进行管理
在这种方法中,空闲磁盘块链接在一起,即一个空闲块包含指向下一个空闲块的指针。第一个磁盘块的块号存储在磁盘上的单独位置,也缓存在内存中。

碎片

这里不得不提一个叫做碎片(fragment)的概念,也称为片段。一般零散的单个数据通常称为片段。 磁盘块可以进一步分为固定大小的分配单元,片段只是在驱动器上彼此不相邻的文件片段。

inode

然后在后面是一个 inode(index node),也称作索引节点。它是一个数组的结构,每个文件有一个 inode,inode 非常重要,它说明了文件的方方面面。每个索引节点都存储对象数据的属性和磁盘块位置
有一种简单的方法可以找到它们 ls -lai 命令。让我们看一下根文件系统:

inode 节点主要包括了以下信息

  • 模式 / 权限(保护)
  • 所有者 ID
  • 组 ID
  • 文件大小
  • 文件的硬链接数
  • 上次访问时间
  • 最后修改时间
  • inode 上次修改时间
    文件分为两部分,索引节点和块。一旦创建后,每种类型的块数是固定的。你不能增加分区上 inode 的数量,也不能增加磁盘块的数量。
    紧跟在 inode 后面的是根目录,它存放的是文件系统目录树的根部。最后,磁盘的其他部分存放了其他所有的目录和文件。

文件的实现

最重要的问题是记录各个文件分别用到了哪些磁盘块。不同的系统采用了不同的方法。下面我们会探讨一下这些方式。分配背后的主要思想是有效利用文件空间快速访问文件 ,主要有三种分配方案

  • 连续分配
  • 链表分配
  • 索引分配
连续分配

最简单的分配方案是把每个文件作为一连串连续数据块存储在磁盘上。因此,在具有 1KB 块的磁盘上,将为 50 KB 文件分配 50 个连续块。

​ 使用连续空间存储文件
上面展示了 40 个连续的内存块。从最左侧的 0 块开始。初始状态下,还没有装载文件,因此磁盘是空的。接着,从磁盘开始处(块 0 )处开始写入占用 4 块长度的内存 A 。然后是一个占用 6 块长度的内存 B,会直接在 A 的末尾开始写。
注意每个文件都会在新的文件块开始写,所以如果文件 A 只占用了 3 又 1/2 个块,那么最后一个块的部分内存会被浪费。在上面这幅图中,总共展示了 7 个文件,每个文件都会从上个文件的末尾块开始写新的文件块。
连续的磁盘空间分配有两个优点。

  • 第一,连续文件存储实现起来比较简单,只需要记住两个数字就可以:一个是第一个块的文件地址和文件的块数量。给定第一个块的编号,可以通过简单的加法找到任何其他块的编号。
  • 第二点是读取性能比较强,可以通过一次操作从文件中读取整个文件。只需要一次寻找第一个块。后面就不再需要寻道时间和旋转延迟,所以数据会以全带宽进入磁盘。
    因此,连续的空间分配具有实现简单高性能的特点。
    不幸的是,连续空间分配也有很明显的不足。随着时间的推移,磁盘会变得很零碎。下图解释了这种现象

    这里有两个文件 D 和 F 被删除了。当删除一个文件时,此文件所占用的块也随之释放,就会在磁盘空间中留下一些空闲块。磁盘并不会在这个位置挤压掉空闲块,因为这会复制空闲块之后的所有文件,可能会有上百万的块,这个量级就太大了。
链表分配

第二种存储文件的方式是为每个文件构造磁盘块链表,每个文件都是磁盘块的链接列表,就像下面所示

​ 以磁盘块的链表形式存储文件
每个块的第一个字作为指向下一块的指针,块的其他部分存放数据。如果上面这张图你看的不是很清楚的话,可以看看整个的链表分配方案

与连续分配方案不同,这一方法可以充分利用每个磁盘块。除了最后一个磁盘块外,不会因为磁盘碎片而浪费存储空间。同样,在目录项中,只要存储了第一个文件块,那么其他文件块也能够被找到。
另一方面,在链表的分配方案中,尽管顺序读取非常方便,但是随机访问却很困难(这也是数组和链表数据结构的一大区别)。
还有一个问题是,由于指针会占用一些字节,每个磁盘块实际存储数据的字节数并不再是 2 的整数次幂。虽然这个问题并不会很严重,但是这种方式降低了程序运行效率。许多程序都是以长度为 2 的整数次幂来读写磁盘,由于每个块的前几个字节被指针所使用,所以要读出一个完成的块大小信息,就需要当前块的信息和下一块的信息拼凑而成,因此就引发了查找和拼接的开销。

使用内存表进行链表分配

由于连续分配和链表分配都有其不可忽视的缺点。所以提出了使用内存中的表来解决分配问题。取出每个磁盘块的指针字,把它们放在内存的一个表中,就可以解决上述链表的两个不足之处。下面是一个例子

上图表示了链表形成的磁盘块的内容。这两个图中都有两个文件,文件 A 依次使用了磁盘块地址 4、7、 2、 10、 12,文件 B 使用了 6、3、11 和 14。也就是说,文件 A 从地址 4 处开始,顺着链表走就能找到文件 A 的全部磁盘块。同样,从第 6 块开始,顺着链走到最后,也能够找到文件 B 的全部磁盘块。你会发现,这两个链表都以不属于有效磁盘编号的特殊标记(-1)结束。内存中的这种表格称为 文件分配表(File Application Table,FAT)

目录的实现

文件只有打开后才能够被读取。在文件打开后,操作系统会使用用户提供的路径名来定位磁盘中的目录。目录项提供了查找文件磁盘块所需要的信息。根据系统的不同,提供的信息也不同,可能提供的信息是整个文件的磁盘地址,或者是第一个块的数量(两个链表方案)或 inode 的数量。不过不管用那种情况,目录系统的主要功能就是 将文件的 ASCII 码的名称映射到定位数据所需的信息上

共享文件

当多个用户在同一个项目中工作时,他们通常需要共享文件。如果这个共享文件同时出现在多个用户目录下,那么他们协同工作起来就很方便。下面的这张图我们在上面提到过,但是有一个更改的地方,就是 C 的一个文件也出现在了 B 的目录下

如果按照如上图的这种组织方式而言,那么 B 的目录与该共享文件的联系称为 链接(link)。那么文件系统现在就是一个 有向无环图(Directed Acyclic Graph, 简称 DAG),而不是一棵树了。

日志结构文件系统

技术的改变会给当前的文件系统带来压力。这种情况下,CPU 会变得越来越快,磁盘会变得越来越大并且越来越便宜(但不会越来越快)。内存容量也是以指数级增长。但是磁盘的寻道时间(除了固态盘,因为固态盘没有寻道时间)并没有获得提高。
为此,Berkeley 设计了一种全新的文件系统,试图缓解这个问题,这个文件系统就是 日志结构文件系统(Log-structured File System, LFS)。旨在解决以下问题。

  • 不断增长的系统内存
  • 顺序 I/O 性能胜过随机 I/O 性能
  • 现有低效率的文件系统
  • 文件系统不支持 RAID(虚拟化)
    另一方面,当时的文件系统不论是 UNIX 还是 FFS,都有大量的随机读写(在 FFS 中创建一个新文件至少需要 5 次随机写),因此成为整个系统的性能瓶颈。同时因为 Page cache 的存在,作者认为随机读不是主要问题:随着越来越大的内存,大部分的读操作都能被 cache,因此 LFS 主要要解决的是减少对硬盘的随机写操作。
    在这种设计中,inode 甚至具有与 UNIX 中相同的结构,但是现在它们分散在整个日志中,而不是位于磁盘上的固定位置。所以,inode 很定位。为了能够找到 inode ,维护了一个由 inode 索引的 inode map(inode 映射)。表项 i 指向磁盘中的第 i 个 inode 。这个映射保存在磁盘中,但是也保存在缓存中,因此,使用最频繁的部分大部分时间都在内存中。

    到目前为止,所有写入最初都缓存在内存中,并且追加在日志末尾,所有缓存的写入都定期在单个段中写入磁盘。所以,现在打开文件也就意味着用映射定位文件的索引节点。一旦 inode 被定位后,磁盘块的地址就能够被找到。所有这些块本身都将位于日志中某处的分段中。
    真实情况下的磁盘容量是有限的,所以最终日志会占满整个磁盘空间,这种情况下就会出现没有新的磁盘块被写入到日志中。幸运的是,许多现有段可能具有不再需要的块。例如,如果一个文件被覆盖了,那么它的 inode 将被指向新的块,但是旧的磁盘块仍在先前写入的段中占据着空间。
    为了处理这个问题,LFS 有一个清理(clean)线程,它会循环扫描日志并对日志进行压缩。首先,通过查看日志中第一部分的信息来查看其中存在哪些索引节点和文件。它会检查当前 inode 的映射来查看 inode 否在在当前块中,是否仍在被使用。如果不是,该信息将被丢弃。如果仍然在使用,那么 inode 和块就会进入内存等待写回到下一个段中。然后原来的段被标记为空闲,以便日志可以用来存放新的数据。用这种方法,清理线程遍历日志,从后面移走旧的段,然后将有效的数据放入内存等待写到下一个段中。由此一来整个磁盘会形成一个大的环形缓冲区,写线程将新的段写在前面,而清理线程则清理后面的段。

日志文件系统

虽然日志结构系统的设计很优雅,但是由于它们和现有的文件系统不相匹配,因此还没有广泛使用。不过,从日志文件结构系统衍生出来一种新的日志系统,叫做日志文件系统,它会记录系统下一步将要做什么的日志。微软的 NTFS 文件系统、Linux 的 ext3 就使用了此日志。 OS X 将日志系统作为可供选项。为了看清它是如何工作的,我们下面讨论一个例子,比如 移除文件 ,这个操作在 UNIX 中需要三个步骤完成:

  • 在目录中删除文件
  • 释放 inode 到空闲 inode 池
  • 将所有磁盘块归还给空闲磁盘池。

虚拟文件系统

UNIX 操作系统使用一种 虚拟文件系统(Virtual File System, VFS) 来尝试将多种文件系统构成一个有序的结构。关键的思想是抽象出所有文件系统都共有的部分,并将这部分代码放在一层,这一层再调用具体文件系统来管理数据。下面是一个 VFS 的系统结构

还是那句经典的话,在计算机世界中,任何解决不了的问题都可以加个代理来解决。所有和文件相关的系统调用在最初的处理上都指向虚拟文件系统。这些来自用户进程的调用,都是标准的 POSIX 系统调用,比如 open、read、write 和 seek 等。VFS 对用户进程有一个 上层 接口,这个接口就是著名的 POSIX 接口。
文件系统的管理和优化----
能够使文件系统工作是一回事,能够使文件系统高效、稳定的工作是另一回事,下面我们就来探讨一下文件系统的管理和优化。

磁盘空间管理

文件通常存在磁盘中,所以如何管理磁盘空间是一个操作系统的设计者需要考虑的问题。在文件上进行存有两种策略:分配 n 个字节的连续磁盘空间;或者把文件拆分成多个并不一定连续的块。在存储管理系统中,主要有分段管理分页管理 两种方式。
正如我们所看到的,按连续字节序列存储文件有一个明显的问题,当文件扩大时,有可能需要在磁盘上移动文件。内存中分段也有同样的问题。不同的是,相对于把文件从磁盘的一个位置移动到另一个位置,内存中段的移动操作要快很多。因此,几乎所有的文件系统都把文件分割成固定大小的块来存储。

块大小

一旦把文件分为固定大小的块来存储,就会出现问题,块的大小是多少?按照磁盘组织方式,扇区、磁道和柱面显然都可以作为分配单位。在分页系统中,分页大小也是主要因素。
拥有大的块尺寸意味着每个文件,甚至 1 字节文件,都要占用一个柱面空间,也就是说小文件浪费了大量的磁盘空间。另一方面,小块意味着大部分文件将会跨越多个块,因此需要多次搜索和旋转延迟才能读取它们,从而降低了性能。因此,如果分配的块太大会浪费空间;分配的块太小会浪费时间

记录空闲块

一旦指定了块大小,下一个问题就是怎样跟踪空闲块。有两种方法被广泛采用,如下图所示

第一种方法是采用磁盘块链表,链表的每个块中包含极可能多的空闲磁盘块号。对于 1 KB 的块和 32 位的磁盘块号,空闲表中每个块包含有 255 个空闲的块号。考虑 1 TB 的硬盘,拥有大概十亿个磁盘块。为了存储全部地址块号,如果每块可以保存 255 个块号,则需要将近 400 万个块。通常,空闲块用于保存空闲列表,因此存储基本上是空闲的。
另一种空闲空间管理的技术是位图(bitmap),n 个块的磁盘需要 n 位位图。在位图中,空闲块用 1 表示,已分配的块用 0 表示。对于 1 TB 硬盘的例子,需要 10 亿位表示,即需要大约 130 000 个 1 KB 块存储。很明显,和 32 位链表模型相比,位图需要的空间更少,因为每个块使用 1 位。只有当磁盘快满的时候,链表需要的块才会比位图少。

磁盘配额

为了防止一些用户占用太多的磁盘空间,多用户操作通常提供一种磁盘配额(enforcing disk quotas)的机制。系统管理员为每个用户分配最大的文件和块分配,并且操作系统确保用户不会超过其配额。我们下面会谈到这一机制。
在用户打开一个文件时,操作系统会找到文件属性磁盘地址,并把它们送入内存中的打开文件表。其中一个属性告诉文件所有者是谁。任何有关文件的增加都会记到所有者的配额中。

​ 配额表中记录了每个用户的配额
第二张表包含了每个用户当前打开文件的配额记录,即使是其他人打开该文件也一样。如上图所示,该表的内容是从被打开文件的所有者的磁盘配额文件中提取出来的。当所有文件关闭时,该记录被写回配额文件。
当在打开文件表中建立一新表项时,会产生一个指向所有者配额记录的指针。每次向文件中添加一个块时,文件所有者所用数据块的总数也随之增加,并会同时增加硬限制软限制的检查。可以超出软限制,但硬限制不可以超出。当已达到硬限制时,再往文件中添加内容将引发错误。同样,对文件数目也存在类似的检查。

文件系统备份

做文件备份很耗费时间而且也很浪费空间,这会引起下面几个问题。首先,是要备份整个文件还是仅备份一部分呢?一般来说,只是备份特定目录及其下的全部文件,而不是备份整个文件系统。
其次,对上次未修改过的文件再进行备份是一种浪费,因而产生了一种增量转储(incremental dumps) 的思想。最简单的增量转储的形式就是周期性的做全面的备份,而每天只对增量转储完成后发生变化的文件做单个备份。
稍微好一点的方式是只备份最近一次转储以来更改过的文件。当然,这种做法极大的缩减了转储时间,但恢复起来却更复杂,因为最近的全面转储先要全部恢复,随后按逆序进行增量转储。为了方便恢复,人们往往使用更复杂的转储模式。
第三,既然待转储的往往是海量数据,那么在将其写入磁带之前对文件进行压缩就很有必要。但是,如果在备份过程中出现了文件损坏的情况,就会导致破坏压缩算法,从而使整个磁带无法读取。所以在备份前是否进行文件压缩需慎重考虑。
第四,对正在使用的文件系统做备份是很难的。如果在转储过程中要添加,删除和修改文件和目录,则转储结果可能不一致。因此,因为转储过程中需要花费数个小时的时间,所以有必要在晚上将系统脱机进行备份,然而这种方式的接受程度并不高。所以,人们修改了转储算法,记下文件系统的瞬时快照,即复制关键的数据结构,然后需要把将来对文件和目录所做的修改复制到块中,而不是到处更新他们。
磁盘转储到备份磁盘上有两种方案:物理转储和逻辑转储物理转储(physical dump) 是从磁盘的 0 块开始,依次将所有磁盘块按照顺序写入到输出磁盘,并在复制最后一个磁盘时停止。这种程序的万无一失性是其他程序所不具备的。
第二个需要考虑的是坏块的转储。制造大型磁盘而没有瑕疵是不可能的,所以也会存在一些坏块(bad blocks)。有时进行低级格式化后,坏块会被检测出来并进行标记,这种情况的解决办法是用磁盘末尾的一些空闲块所替换。
然而,一些块在格式化后会变坏,在这种情况下操作系统可以检测到它们。通常情况下,它可以通过创建一个由所有坏块组成的文件来解决问题,确保它们不会出现在空闲池中并且永远不会被分配。那么此文件是完全不可读的。如果磁盘控制器将所有的坏块重新映射,物理转储还是能够正常工作的。
Windows 系统有分页文件(paging files)休眠文件(hibernation files) 。它们在文件还原时不发挥作用,同时也不应该在第一时间进行备份。

文件系统的一致性

影响可靠性的一个因素是文件系统的一致性。许多文件系统读取磁盘块、修改磁盘块、再把它们写回磁盘。如果系统在所有块写入之前崩溃,文件系统就会处于一种不一致(inconsistent)的状态。如果某些尚未写回的块是索引节点块,目录块或包含空闲列表的块,则此问题是很严重的。
为了处理文件系统一致性问题,大部分计算机都会有应用程序来检查文件系统的一致性。例如,UNIX 有 fsck;Windows 有 sfc,每当引导系统时(尤其是在崩溃后),都可以运行该程序。
可以进行两种一致性检查:块的一致性检查和文件的一致性检查。为了检查块的一致性,应用程序会建立两张表,每个包含一个计数器的块,最初设置为 0 。第一个表中的计数器跟踪该块在文件中出现的次数,第二张表中的计数器记录每个块在空闲列表、空闲位图中出现的频率。

文件系统性能

访问磁盘的效率要比内存满的多,是时候又祭出这张图了

从内存读一个 32 位字大概是 10ns,从硬盘上读的速率大概是 100MB/S,对每个 32 位字来说,效率会慢了四倍,另外,还要加上 5 - 10 ms 的寻道时间等其他损耗,如果只访问一个字,内存要比磁盘快百万数量级。所以磁盘优化是很有必要的,下面我们会讨论几种优化方式

高速缓存

最常用的减少磁盘访问次数的技术是使用 块高速缓存(block cache) 或者 缓冲区高速缓存(buffer cache)。高速缓存指的是一系列的块,它们在逻辑上属于磁盘,但实际上基于性能的考虑被保存在内存中。
管理高速缓存有不同的算法,常用的算法是:检查全部的读请求,查看在高速缓存中是否有所需要的块。如果存在,可执行读操作而无须访问磁盘。如果检查块不再高速缓存中,那么首先把它读入高速缓存,再复制到所需的地方。之后,对同一个块的请求都通过高速缓存来完成。
高速缓存的操作如下图所示

由于在高速缓存中有许多块,所以需要某种方法快速确定所需的块是否存在。常用方法是将设备和磁盘地址进行散列操作,然后,在散列表中查找结果。具有相同散列值的块在一个链表中连接在一起(这个数据结构是不是很像 HashMap?),这样就可以沿着冲突链查找其他块。
如果高速缓存已满,此时需要调入新的块,则要把原来的某一块调出高速缓存,如果要调出的块在上次调入后已经被修改过,则需要把它写回磁盘。

块提前读

第二个明显提高文件系统的性能是,在需要用到块之前,试图提前将其写入高速缓存,从而提高命中率。许多文件都是顺序读取。如果请求文件系统在某个文件中生成块 k,文件系统执行相关操作并且在完成之后,会检查高速缓存,以便确定块 k + 1 是否已经在高速缓存。如果不在,文件系统会为 k + 1 安排一个预读取,因为文件希望在用到该块的时候能够直接从高速缓存中读取。
当然,块提前读取策略只适用于实际顺序读取的文件。对随机访问的文件,提前读丝毫不起作用。甚至还会造成阻碍。

减少磁盘臂运动

高速缓存和块提前读并不是提高文件系统性能的唯一方法。另一种重要的技术是把有可能顺序访问的块放在一起,当然最好是在同一个柱面上,从而减少磁盘臂的移动次数。当写一个输出文件时,文件系统就必须按照要求一次一次地分配磁盘块。如果用位图来记录空闲块,并且整个位图在内存中,那么选择与前一块最近的空闲块是很容易的。如果用空闲表,并且链表的一部分存在磁盘上,要分配紧邻的空闲块就会困难很多。

磁盘碎片整理

在初始安装操作系统后,文件就会被不断的创建和清除,于是磁盘会产生很多的碎片,在创建一个文件时,它使用的块会散布在整个磁盘上,降低性能。删除文件后,回收磁盘块,可能会造成空穴。
磁盘性能可以通过如下方式恢复:移动文件使它们相互挨着,并把所有的至少是大部分的空闲空间放在一个或多个大的连续区域内。Windows 有一个程序 defrag 就是做这个事儿的。Windows 用户会经常使用它,SSD 除外。
磁盘碎片整理程序会在让文件系统上很好地运行。Linux 文件系统(特别是 ext2 和 ext3)由于其选择磁盘块的方式,在磁盘碎片整理上一般不会像 Windows 一样困难,因此很少需要手动的磁盘碎片整理。而且,固态硬盘并不受磁盘碎片的影响,事实上,在固态硬盘上做磁盘碎片整理反倒是多此一举,不仅没有提高性能,反而磨损了固态硬盘。所以碎片整理只会缩短固态硬盘的寿命。
下面我们来探讨一下 I/O 流程问题。

I/O 设备

什么是 I/O 设备?I/O 设备又叫做输入 / 输出设备,它是人类用来和计算机进行通信的外部硬件。输入 / 输出设备能够向计算机发送数据(输出)并从计算机接收数据(输入)
I/O 设备(I/O devices)可以分成两种:块设备(block devices)字符设备(character devices)

块设备

块设备是一个能存储固定大小块信息的设备,它支持以固定大小的块,扇区或群集读取和(可选)写入数据。每个块都有自己的物理地址。通常块的大小在 512 - 65536 之间。所有传输的信息都会以连续的块为单位。块设备的基本特征是每个块都较为对立,能够独立的进行读写。常见的块设备有 硬盘、蓝光光盘、USB 盘
与字符设备相比,块设备通常需要较少的引脚。

块设备的缺点

基于给定固态存储器的块设备比基于相同类型的存储器的字节寻址要慢一些,因为必须在块的开头开始读取或写入。所以,要读取该块的任何部分,必须寻找到该块的开始,读取整个块,如果不使用该块,则将其丢弃。要写入块的一部分,必须寻找到块的开始,将整个块读入内存,修改数据,再次寻找到块的开头处,然后将整个块写回设备。

字符设备

另一类 I/O 设备是字符设备。字符设备以字符为单位发送或接收一个字符流,而不考虑任何块结构。字符设备是不可寻址的,也没有任何寻道操作。常见的字符设备有 打印机、网络设备、鼠标、以及大多数与磁盘不同的设备

设备控制器

设备控制器是处理 CPU 传入和传出信号的系统。设备通过插头和插座连接到计算机,并且插座连接到设备控制器。设备控制器从连接的设备处接收数据,并将其存储在控制器内部的一些特殊目的寄存器(special purpose registers) 也就是本地缓冲区中。
每个设备控制器都会有一个应用程序与之对应,设备控制器通过应用程序的接口通过中断与操作系统进行通信。设备控制器是硬件,而设备驱动程序是软件。

内存映射 I/O

每个控制器都会有几个寄存器用来和 CPU 进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据,接收数据、开启或者关闭设备等。通过从这些寄存器中读取信息,操作系统能够知道设备的状态,是否准备接受一个新命令等。
为了控制寄存器,许多设备都会有数据缓冲区(data buffer),来供系统进行读写。
那么问题来了,CPU 如何与设备寄存器和设备数据缓冲区进行通信呢?存在两个可选的方式。第一种方法是,每个控制寄存器都被分配一个 I/O 端口(I/O port)号,这是一个 8 位或 16 位的整数。所有 I/O 端口的集合形成了受保护的 I/O 端口空间,以便普通用户程序无法访问它(只有操作系统可以访问)。使用特殊的 I/O 指令像是

IN REG,PORT

CPU 可以读取控制寄存器 PORT 的内容并将结果放在 CPU 寄存器 REG 中。类似的,使用

OUT PORT,REG

CPU 可以将 REG 的内容写到控制寄存器中。大多数早期计算机,包括几乎所有大型主机,如 IBM 360 及其所有后续机型,都是以这种方式工作的。
第二个方法是 PDP-11 引入的,它将所有控制寄存器映射到内存空间中。

直接内存访问

无论一个 CPU 是否具有内存映射 I/O,它都需要寻址设备控制器以便与它们交换数据。CPU 可以从 I/O 控制器每次请求一个字节的数据,但是这么做会浪费 CPU 时间,所以经常会用到一种称为直接内存访问(Direct Memory Access) 的方案。为了简化,我们假设 CPU 通过单一的系统总线访问所有的设备和内存,该总线连接 CPU 、内存和 I/O 设备,如下图所示

​ DMA 传送操作

现代操作系统实际更为复杂,但是原理是相同的。如果硬件有 DMA 控制器,那么操作系统只能使用 DMA。有时这个控制器会集成到磁盘控制器和其他控制器中,但这种设计需要在每个设备上都装有一个分离的 DMA 控制器。单个的 DMA 控制器可用于向多个设备传输,这种传输往往同时进行。

DMA 工作原理

首先 CPU 通过设置 DMA 控制器的寄存器对它进行编程,所以 DMA 控制器知道将什么数据传送到什么地方。DMA 控制器还要向磁盘控制器发出一个命令,通知它从磁盘读数据到其内部的缓冲区并检验校验和。当有效数据位于磁盘控制器的缓冲区中时,DMA 就可以开始了。
DMA 控制器通过在总线上发出一个读请求到磁盘控制器而发起 DMA 传送,这是第二步。这个读请求就像其他读请求一样,磁盘控制器并不知道或者并不关心它是来自 CPU 还是来自 DMA 控制器。通常情况下,要写的内存地址在总线的地址线上,所以当磁盘控制器去匹配下一个字时,它知道将该字写到什么地方。写到内存就是另外一个总线循环了,这是第三步。当写操作完成时,磁盘控制器在总线上发出一个应答信号到 DMA 控制器,这是第四步。
然后,DMA 控制器会增加内存地址并减少字节数量。如果字节数量仍然大于 0 ,就会循环步骤 2 - 步骤 4 ,直到字节计数变为 0 。此时,DMA 控制器会打断 CPU 并告诉它传输已经完成了。

重温中断

在一台个人计算机体系结构中,中断结构会如下所示

​ 中断是怎样发生的
当一个 I/O 设备完成它的工作后,它就会产生一个中断(默认操作系统已经开启中断),它通过在总线上声明已分配的信号来实现此目的。主板上的中断控制器芯片会检测到这个信号,然后执行中断操作。

精确中断和不精确中断

使机器处于良好状态的中断称为精确中断(precise interrupt)。这样的中断具有四个属性:

  • PC (程序计数器)保存在一个已知的地方
  • PC 所指向的指令之前所有的指令已经完全执行
  • PC 所指向的指令之后所有的指令都没有执行
  • PC 所指向的指令的执行状态是已知的
    不满足以上要求的中断称为 不精确中断(imprecise interrupt),不精确中断让人很头疼。上图描述了不精确中断的现象。指令的执行时序和完成度具有不确定性,而且恢复起来也非常麻烦。

IO 软件原理

I/O 软件目标

设备独立性

I/O 软件设计一个很重要的目标就是设备独立性(device independence)。这意味着我们能够编写访问任何设备的应用程序,而不用事先指定特定的设备

错误处理

除了设备独立性外,I/O 软件实现的第二个重要的目标就是错误处理(error handling)。通常情况下来说,错误应该交给硬件层面去处理。如果设备控制器发现了读错误的话,它会尽可能的去修复这个错误。如果设备控制器处理不了这个问题,那么设备驱动程序应该进行处理,设备驱动程序会再次尝试读取操作,很多错误都是偶然性的,如果设备驱动程序无法处理这个错误,才会把错误向上抛到硬件层面(上层)进行处理,很多时候,上层并不需要知道下层是如何解决错误的。

同步和异步传输

I/O 软件实现的第三个目标就是 同步(synchronous)异步(asynchronous,即中断驱动)传输。这里先说一下同步和异步是怎么回事吧。
同步传输中数据通常以块或帧的形式发送。发送方和接收方在数据传输之前应该具有同步时钟。而在异步传输中,数据通常以字节或者字符的形式发送,异步传输则不需要同步时钟,但是会在传输之前向数据添加奇偶校验位。大部分物理IO(physical I/O) 是异步的。物理 I/O 中的 CPU 是很聪明的,CPU 传输完成后会转而做其他事情,它和中断心灵相通,等到中断发生后,CPU 才会回到传输这件事情上来。

缓冲

I/O 软件的最后一个问题是缓冲(buffering)。通常情况下,从一个设备发出的数据不会直接到达最后的设备。其间会经过一系列的校验、检查、缓冲等操作才能到达。

共享和独占

I/O 软件引起的最后一个问题就是共享设备和独占设备的问题。有些 I/O 设备能够被许多用户共同使用。一些设备比如磁盘,让多个用户使用一般不会产生什么问题,但是某些设备必须具有独占性,即只允许单个用户使用完成后才能让其他用户使用。
一共有三种控制 I/O 设备的方法

  • 使用程序控制 I/O
  • 使用中断驱动 I/O
  • 使用 DMA 驱动 I/O
    I/O 层次结构–
    I/O 软件通常组织成四个层次,它们的大致结构如下图所示

    下面我们具体的来探讨一下上面的层次结构

中断处理程序

在计算机系统中,中断就像女人的脾气一样无时无刻都在产生,中断的出现往往是让人很不爽的。中断处理程序又被称为中断服务程序 或者是 ISR(Interrupt Service Routines),它是最靠近硬件的一层。中断处理程序由硬件中断、软件中断或者是软件异常启动产生的中断,用于实现设备驱动程序或受保护的操作模式(例如系统调用)之间的转换。
中断处理程序负责处理中断发生时的所有操作,操作完成后阻塞,然后启动中断驱动程序来解决阻塞。通常会有三种通知方式,依赖于不同的具体实现

  • 信号量实现中:在信号量上使用 up 进行通知;
  • 管程实现:对管程中的条件变量执行 signal 操作
  • 还有一些情况是发送一些消息

设备驱动程序

每个连接到计算机的 I/O 设备都需要有某些特定设备的代码对其进行控制。这些提供 I/O 设备到设备控制器转换的过程的代码称为 设备驱动程序(Device driver)
设备控制器的主要功能有下面这些

  • 接收和识别命令:设备控制器可以接受来自 CPU 的指令,并进行识别。设备控制器内部也会有寄存器,用来存放指令和参数
  • 进行数据交换:CPU、控制器和设备之间会进行数据的交换,CPU 通过总线把指令发送给控制器,或从控制器中并行地读出数据;控制器将数据写入指定设备。
  • 地址识别:每个硬件设备都有自己的地址,设备控制器能够识别这些不同的地址,来达到控制硬件的目的,此外,为使 CPU 能向寄存器中写入或者读取数据,这些寄存器都应具有唯一的地址。
  • 差错检测:设备控制器还具有对设备传递过来的数据进行检测的功能。
    在这种情况下,设备控制器会阻塞,直到中断来解除阻塞状态。还有一种情况是操作是可以无延迟的完成,所以驱动程序不需要阻塞。在第一种情况下,操作系统可能被中断唤醒;第二种情况下操作系统不会被休眠。
    设备驱动程序必须是可重入的,因为设备驱动程序会阻塞和唤醒然后再次阻塞。驱动程序不允许进行系统调用,但是它们通常需要与内核的其余部分进行交互。

与设备无关的 I/O 软件

I/O 软件有两种,一种是我们上面介绍过的基于特定设备的,还有一种是设备无关性的,设备无关性也就是不需要特定的设备。设备驱动程序与设备无关的软件之间的界限取决于具体的系统。下面显示的功能由设备无关的软件实现

与设备无关的软件的基本功能是对所有设备执行公共的 I/O 功能,并且向用户层软件提供一个统一的接口。

缓冲

无论是对于块设备还是字符设备来说,缓冲都是一个非常重要的考量标准。缓冲技术应用广泛,但它也有缺点。如果数据被缓冲次数太多,会影响性能。

错误处理

在 I/O 中,出错是一种再正常不过的情况了。当出错发生时,操作系统必须尽可能处理这些错误。有一些错误是只有特定的设备才能处理,有一些是由框架进行处理,这些错误和特定的设备无关。
I/O 错误的一类是程序员编程错误,比如还没有打开文件前就读流,或者不关闭流导致内存溢出等等。这类问题由程序员处理;另外一类是实际的 I/O 错误,例如向一个磁盘坏块写入数据,无论怎么写都写入不了。这类问题由驱动程序处理,驱动程序处理不了交给硬件处理,这个我们上面也说过。

设备驱动程序统一接口

我们在操作系统概述中说到,操作系统一个非常重要的功能就是屏蔽了硬件和软件的差异性,为硬件和软件提供了统一的标准,这个标准还体现在为设备驱动程序提供统一的接口,因为不同的硬件和厂商编写的设备驱动程序不同,所以如果为每个驱动程序都单独提供接口的话,这样没法搞,所以必须统一。

分配和释放

一些设备例如打印机,它只能由一个进程来使用,这就需要操作系统根据实际情况判断是否能够对设备的请求进行检查,判断是否能够接受其他请求,一种比较简单直接的方式是在特殊文件上执行 open操作。如果设备不可用,那么直接 open 会导致失败。还有一种方式是不直接导致失败,而是让其阻塞,等到另外一个进程释放资源后,在进行 open 打开操作。这种方式就把选择权交给了用户,由用户判断是否应该等待。

设备无关的块

不同的磁盘会具有不同的扇区大小,但是软件不会关心扇区大小,只管存储就是了。一些字符设备可以一次一个字节的交付数据,而其他的设备则以较大的单位交付数据,这些差异也可以隐藏起来。

用户空间的 I/O 软件

虽然大部分 I/O 软件都在内核结构中,但是还有一些在用户空间实现的 I/O 软件,凡事没有绝对。一些 I/O 软件和库过程在用户空间存在,然后以提供系统调用的方式实现。

盘可以说是硬件里面比较简单的构造了,同时也是最重要的。下面我们从盘谈起,聊聊它的物理构造

盘硬件

盘会有很多种类型。其中最简单的构造就是磁盘(magnetic hard disks), 也被称为 hard disk,HDD等。磁盘通常与安装在磁臂上的磁头配对,磁头可将数据读取或者将数据写入磁盘,因此磁盘的读写速度都同样快。在磁盘中,数据是随机访问的,这也就说明可以通过任意的顺序来存储检索单个数据块,所以你可以在任意位置放置磁盘来让磁头读取,磁盘是一种非易失性的设备,即使断电也能永久保留。

磁盘

为了组织和检索数据,会将磁盘组织成特定的结构,这些特定的结构就是磁道、扇区和柱面

磁盘被组织成柱面形式,每个盘用轴相连,每一个柱面包含若干磁道,每个磁道由若干扇区组成。软盘上大约每个磁道有 8 - 32 个扇区,硬盘上每条磁道上扇区的数量可达几百个,磁头大约是 1 - 16 个。
对于磁盘驱动程序来说,一个非常重要的特性就是控制器是否能够同时控制两个或者多个驱动器进行磁道寻址,这就是重叠寻道(overlapped seek)。对于控制器来说,它能够控制一个磁盘驱动程序完成寻道操作,同时让其他驱动程序等待寻道结束。控制器也可以在一个驱动程序上进行读写草哦做,与此同时让另外的驱动器进行寻道操作,但是软盘控制器不能在两个驱动器上进行读写操作。

RAID

RAID 称为 磁盘冗余阵列,简称 磁盘阵列。利用虚拟化技术把多个硬盘结合在一起,成为一个或多个磁盘阵列组,目的是提升性能或数据冗余。
RAID 有不同的级别

  • RAID 0 - 无容错的条带化磁盘阵列
  • RAID 1 - 镜像和双工
  • RAID 2 - 内存式纠错码
  • RAID 3 - 比特交错奇偶校验
  • RAID 4 - 块交错奇偶校验
  • RAID 5 - 块交错分布式奇偶校验
  • RAID 6 - P + Q 冗余
磁盘格式化

磁盘由一堆铝的、合金或玻璃的盘片组成,磁盘刚被创建出来后,没有任何信息。磁盘在使用前必须经过低级格式化(low-levvel format),下面是一个扇区的格式

前导码相当于是标示扇区的开始位置,通常以位模式开始,前导码还包括柱面号扇区号等一些其他信息。紧随前导码后面的是数据区,数据部分的大小由低级格式化程序来确定。大部分磁盘使用 512 字节的扇区。数据区后面是 ECC,ECC 的全称是 error correction code数据纠错码,它与普通的错误检测不同,ECC 还可以用于恢复读错误。ECC 阶段的大小由不同的磁盘制造商实现。ECC 大小的设计标准取决于设计者愿意牺牲多少磁盘空间来提高可靠性,以及程序可以处理的 ECC 的复杂程度。通常情况下 ECC 是 16 位,除此之外,硬盘一般具有一定数量的备用扇区,用于替换制造缺陷的扇区。

磁盘臂调度算法

下面我们来探讨一下关于影响磁盘读写的算法,一般情况下,影响磁盘快读写的时间由下面几个因素决定

  • 寻道时间 - 寻道时间指的就是将磁盘臂移动到需要读取磁盘块上的时间
  • 旋转延迟 - 等待合适的扇区旋转到磁头下所需的时间
  • 实际数据的读取或者写入时间
    这三种时间参数也是磁盘寻道的过程。一般情况下,寻道时间对总时间的影响最大,所以,有效的降低寻道时间能够提高磁盘的读取速度。
    如果磁盘驱动程序每次接收一个请求并按照接收顺序完成请求,这种处理方式也就是 先来先服务(First-Come, First-served, FCFS) ,这种方式很难优化寻道时间。因为每次都会按照顺序处理,不管顺序如何,有可能这次读完后需要等待一个磁盘旋转一周才能继续读取,而其他柱面能够马上进行读取,这种情况下每次请求也会排队。
    通常情况下,磁盘在进行寻道时,其他进程会产生其他的磁盘请求。磁盘驱动程序会维护一张表,表中会记录着柱面号当作索引,每个柱面未完成的请求会形成链表,链表头存放在表的相应表项中。
    一种对先来先服务的算法改良的方案是使用 最短路径优先(SSF) 算法,下面描述了这个算法。
    假如我们在对磁道 6 号进行寻址时,同时发生了对 11 , 2 , 4, 14, 8, 15, 3 的请求,如果采用先来先服务的原则,如下图所示

    我们可以计算一下磁盘臂所跨越的磁盘数量为 5 + 9 + 2 + 10 + 6 + 7 + 12 = 51,相当于是跨越了 51 次盘面,如果使用最短路径优先,我们来计算一下跨越的盘面

    跨越的磁盘数量为 4 + 1 + 1 + 4 + 3 + 3 + 1 = 17 ,相比 51 足足省了两倍的时间。
    但是,最短路径优先的算法也不是完美无缺的,这种算法照样存在问题,那就是优先级 问题,
    这里有一个原型可以参考就是我们日常生活中的电梯,电梯使用一种电梯算法(elevator algorithm) 来进行调度,从而满足协调效率和公平性这两个相互冲突的目标。电梯一般会保持向一个方向移动,直到在那个方向上没有请求为止,然后改变方向。
    电梯算法需要维护一个二进制位,也就是当前的方向位:UP(向上)或者是 DOWN(向下)。当一个请求处理完成后,磁盘或电梯的驱动程序会检查该位,如果此位是 UP 位,磁盘臂或者电梯仓移到下一个更高跌未完成的请求。如果高位没有未完成的请求,则取相反方向。当方向位是 DOWN 时,同时存在一个低位的请求,磁盘臂会转向该点。如果不存在的话,那么它只是停止并等待。
    我们举个例子来描述一下电梯算法,比如各个柱面得到服务的顺序是 4,7,10,14,9,6,3,1 ,那么它的流程图如下

    所以电梯算法需要跨越的盘面数量是 3 + 3 + 4 + 5 + 3 + 3 + 1 = 22
    电梯算法通常情况下不如 SSF 算法。

错误处理

一般坏块有两种处理办法,一种是在控制器中进行处理;一种是在操作系统层面进行处理。
这两种方法经常替换使用,比如一个具有 30 个数据扇区和两个备用扇区的磁盘,其中扇区 4 是有瑕疵的。

控制器能做的事情就是将备用扇区之一重新映射。

还有一种处理方式是将所有的扇区都向上移动一个扇区

上面这这两种情况下控制器都必须知道哪个扇区,可以通过内部的表来跟踪这一信息,或者通过重写前导码来给出重新映射的扇区号。如果是重写前导码,那么涉及移动的方式必须重写后面所有的前导码,但是最终会提供良好的性能。

稳定存储器

磁盘经常会出现错误,导致好的扇区会变成坏扇区,驱动程序也有可能挂掉。RAID 可以对扇区出错或者是驱动器崩溃提出保护,然而 RAID 却不能对坏数据中的写错误提供保护,也不能对写操作期间的崩溃提供保护,这样就会破坏原始数据。
我们期望磁盘能够准确无误的工作,但是事实情况是不可能的,但是我们能够知道的是,一个磁盘子系统具有如下特性:当一个写命令发给它时,磁盘要么正确地写数据,要么什么也不做,让现有的数据完整无误的保留。这样的系统称为 稳定存储器(stable storage)。 稳定存储器的目标就是不惜一切代价保证磁盘的一致性。
稳定存储器使用两个一对相同的磁盘,对应的块一同工作形成一个无差别的块。稳定存储器为了实现这个目的,定义了下面三种操作:

  • 稳定写(stable write)
  • 稳定读(stable read)
  • 崩溃恢复(crash recovery)

时钟

时钟(Clocks) 也被称为定时器(timers),时钟 / 定时器对任何程序系统来说都是必不可少的。时钟负责维护时间、防止一个进程长期占用 CPU 时间等其他功能。时钟软件(clock software) 也是一种设备驱动的方式。下面我们就来对时钟进行介绍,一般都是先讨论硬件再介绍软件,采用由下到上的方式,也是告诉你,底层是最重要的。

时钟硬件

在计算机中有两种类型的时钟,这些时钟与现实生活中使用的时钟完全不一样。

  • 比较简单的一种时钟被连接到 110 V 或 220 V 的电源线上,这样每个电压周期会产生一个中断,大概是 50 - 60 HZ。这些时钟过去一直占据支配地位。
  • 另外的一种时钟由晶体振荡器、计数器和寄存器组成,示意图如下所示

    这种时钟称为可编程时钟 ,可编程时钟有两种模式,一种是 一键式(one-shot mode),当时钟启动时,会把存储器中的值复制到计数器中,然后,每次晶体的振荡器的脉冲都会使计数器 -1。当计数器变为 0 时,会产生一个中断,并停止工作,直到软件再一次显示启动。还有一种模式时 方波(square-wave mode) 模式,在这种模式下,当计数器变为 0 并产生中断后,存储寄存器的值会自动复制到计数器中,这种周期性的中断称为一个时钟周期。

时钟软件

时钟硬件所做的工作只是根据已知的时间间隔产生中断,而其他的工作都是由时钟软件来完成,一般操作系统的不同,时钟软件的具体实现也不同,但是一般都会包括以下这几点

  • 维护一天的时间
  • 阻止进程运行的时间超过其指定时间
  • 统计 CPU 的使用情况
  • 处理用户进程的警告系统调用
  • 为系统各个部分提供看门狗定时器
  • 完成概要剖析,监视和信息收集

软定时器

时钟软件也被称为可编程时钟,可以设置它以程序需要的任何速率引发中断。时钟软件触发的中断是一种硬中断,但是某些应用程序对于硬中断来说是不可接受的。
这时候就需要一种软定时器(soft timer) 避免了中断,无论何时当内核因为某种原因呢在运行时,它返回用户态之前都会检查时钟来了解软定时器是否到期。如果软定时器到期,则执行被调度的事件也无需切换到内核态,因为本身已经处于内核态中。这种方式避免了频繁的内核态和用户态之前的切换,提高了程序运行效率。
软定时器因为不同的原因切换进入内核态的速率不同,原因主要有

  • 系统调用
  • TLB 未命中
  • 缺页异常
  • I/O 中断
  • CPU 变得空闲
    死锁问题也是操作系统非常重要的一类问题

    资源
    大部分的死锁都和资源有关,在进程对设备、文件具有独占性(排他性)时会产生死锁。我们把这类需要排他性使用的对象称为资源(resource)。资源主要分为 可抢占资源和不可抢占资源

可抢占资源和不可抢占资源

资源主要有可抢占资源和不可抢占资源。可抢占资源(preemptable resource) 可以从拥有它的进程中抢占而不会造成其他影响,内存就是一种可抢占性资源,任何进程都能够抢先获得内存的使用权。
不可抢占资源(nonpreemtable resource) 指的是除非引起错误或者异常,否则进程无法抢占指定资源,这种不可抢占的资源比如有光盘,在进程执行调度的过程中,其他进程是不能得到该资源的。
死锁
如果要对死锁进行一个定义的话,下面的定义比较贴切
如果一组进程中的每个进程都在等待一个事件,而这个事件只能由该组中的另一个进程触发,这种情况会导致死锁

资源死锁的条件

针对我们上面的描述,资源死锁可能出现的情况主要有

  • 互斥条件:每个资源都被分配给了一个进程或者资源是可用的
  • 保持和等待条件:已经获取资源的进程被认为能够获取新的资源
  • 不可抢占条件:分配给一个进程的资源不能强制的从其他进程抢占资源,它只能由占有它的进程显示释放
  • 循环等待:死锁发生时,系统中一定有两个或者两个以上的进程组成一个循环,循环中的每个进程都在等待下一个进程释放的资源。
    发生死锁时,上面的情况必须同时会发生。如果其中任意一个条件不会成立,死锁就不会发生。可以通过破坏其中任意一个条件来破坏死锁,下面这些破坏条件就是我们探讨的重点

死锁模型

Holt 在 1972 年提出对死锁进行建模,建模的标准如下:

  • 圆形表示进程
  • 方形表示资源
    从资源节点到进程节点表示资源已经被进程占用,如下图所示

    在上图中表示当前资源 R 正在被 A 进程所占用
    由进程节点到资源节点的有向图表示当前进程正在请求资源,并且该进程已经被阻塞,处于等待这个资源的状态

    在上图中,表示的含义是进程 B 正在请求资源 S 。Holt 认为,死锁的描述应该如下

    这是一个死锁的过程,进程 C 等待资源 T 的释放,资源 T 却已经被进程 D 占用,进程 D 等待请求占用资源 U ,资源 U 却已经被线程 C 占用,从而形成环。
    有四种处理死锁的策略:
  • 忽略死锁带来的影响(惊呆了)
  • 检测死锁并回复死锁,死锁发生时对其进行检测,一旦发生死锁后,采取行动解决问题
  • 通过仔细分配资源来避免死锁
  • 通过破坏死锁产生的四个条件之一来避免死锁
    下面我们分别介绍一下这四种方法
    鸵鸟算法
    最简单的解决办法就是使用鸵鸟算法(ostrich algorithm),把头埋在沙子里,假装问题根本没有发生。每个人看待这个问题的反应都不同。数学家认为死锁是不可接受的,必须通过有效的策略来防止死锁的产生。工程师想要知道问题发生的频次,系统因为其他原因崩溃的次数和死锁带来的严重后果。如果死锁发生的频次很低,而经常会由于硬件故障、编译器错误等其他操作系统问题导致系统崩溃,那么大多数工程师不会修复死锁。
    死锁检测和恢复
    第二种技术是死锁的检测和恢复。这种解决方式不会尝试去阻止死锁的出现。相反,这种解决方案会希望死锁尽可能的出现,在监测到死锁出现后,对其进行恢复。下面我们就来探讨一下死锁的检测和恢复的几种方式

每种类型一个资源的死锁检测方式

每种资源类型都有一个资源是什么意思?我们经常提到的打印机就是这样的,资源只有打印机,但是设备都不会超过一个。
可以通过构造一张资源分配表来检测这种错误,比如我们上面提到的

如果这张图包含了一个或一个以上的环,那么死锁就存在,处于这个环中任意一个进程都是死锁的进程。

每种类型多个资源的死锁检测方式

如果有多种相同的资源存在,就需要采用另一种方法来检测死锁。可以通过构造一个矩阵来检测从 P1 -> Pn 这 n 个进程中的死锁。
现在我们提供一种基于矩阵的算法来检测从 P1 到 Pn 这 n 个进程中的死锁。假设资源类型为 m,E1 代表资源类型 1,E2 表示资源类型 2 ,Ei 代表资源类型 i (1 <= i <= m)。E 表示的是 现有资源向量(existing resource vector),代表每种已存在的资源总数。
现在我们就需要构造两个数组:C 表示的是当前分配矩阵(current allocation matrix) ,R 表示的是 请求矩阵(request matrix)。Ci 表示的是 Pi 持有每一种类型资源的资源数。所以,Cij 表示 Pi 持有资源 j 的数量。Rij 表示 Pi 所需要获得的资源 j 的数量

一般来说,已分配资源 j 的数量加起来再和所有可供使用的资源数相加 = 该类资源的总数。
死锁的检测就是基于向量的比较。每个进程起初都是没有被标记过的,算法会开始对进程做标记,进程被标记后说明进程被执行了,不会进入死锁,当算法结束时,任何没有被标记过的进程都会被判定为死锁进程。
上面我们探讨了两种检测死锁的方式,那么现在你知道怎么检测后,你何时去做死锁检测呢?一般来说,有两个考量标准:

  • 每当有资源请求时就去检测,这种方式会占用昂贵的 CPU 时间。
  • 每隔 k 分钟检测一次,或者当 CPU 使用率降低到某个标准下去检测。考虑到 CPU 效率的原因,如果死锁进程达到一定数量,就没有多少进程可以运行,所以 CPU 会经常空闲。

从死锁中恢复

上面我们探讨了如何检测进程死锁,我们最终的目的肯定是想让程序能够正常的运行下去,所以针对检测出来的死锁,我们要对其进行恢复,下面我们会探讨几种死锁的恢复方式

通过抢占进行恢复

在某些情况下,可能会临时将某个资源从它的持有者转移到另一个进程。比如在不通知原进程的情况下,将某个资源从进程中强制取走给其他进程使用,使用完后又送回。这种恢复方式一般比较困难而且有些简单粗暴,并不可取。

通过回滚进行恢复

如果系统设计者和机器操作员知道有可能发生死锁,那么就可以定期检查流程。进程的检测点意味着进程的状态可以被写入到文件以便后面进行恢复。检测点不仅包含存储映像(memory image),还包含资源状态(resource state)。一种更有效的解决方式是不要覆盖原有的检测点,而是每出现一个检测点都要把它写入到文件中,这样当进程执行时,就会有一系列的检查点文件被累积起来。
为了进行恢复,要从上一个较早的检查点上开始,这样所需要资源的进程会回滚到上一个时间点,在这个时间点上,死锁进程还没有获取所需要的资源,可以在此时对其进行资源分配。

杀死进程恢复

最简单有效的解决方案是直接杀死一个死锁进程。但是杀死一个进程可能照样行不通,这时候就需要杀死别的资源进行恢复。
另外一种方式是选择一个环外的进程作为牺牲品来释放进程资源。
死锁避免
我们上面讨论的是如何检测出现死锁和如何恢复死锁,下面我们探讨几种规避死锁的方式

单个资源的银行家算法

银行家算法是 Dijkstra 在 1965 年提出的一种调度算法,它本身是一种死锁的调度算法。它的模型是基于一个城镇中的银行家,银行家向城镇中的客户承诺了一定数量的贷款额度。算法要做的就是判断请求是否会进入一种不安全的状态。如果是,就拒绝请求,如果请求后系统是安全的,就接受该请求。
类似的,还有多个资源的银行家算法,读者可以自行了解。
破坏死锁
死锁本质上是无法避免的,因为它需要获得未知的资源和请求,但是死锁是满足四个条件后才出现的,它们分别是

  • 互斥
  • 保持和等待
  • 不可抢占
  • 循环等待
    我们分别对这四个条件进行讨论,按理说破坏其中的任意一个条件就能够破坏死锁

破坏互斥条件

我们首先考虑的就是破坏互斥使用条件。如果资源不被一个进程独占,那么死锁肯定不会产生。如果两个打印机同时使用一个资源会造成混乱,打印机的解决方式是使用 假脱机打印机(spooling printer) ,这项技术可以允许多个进程同时产生输出,在这种模型中,实际请求打印机的唯一进程是打印机守护进程,也称为后台进程。后台进程不会请求其他资源。我们可以消除打印机的死锁。
后台进程通常被编写为能够输出完整的文件后才能打印,假如两个进程都占用了假脱机空间的一半,而这两个进程都没有完成全部的输出,就会导致死锁。
因此,尽量做到尽可能少的进程可以请求资源。

破坏保持等待的条件

第二种方式是如果我们能阻止持有资源的进程请求其他资源,我们就能够消除死锁。一种实现方式是让所有的进程开始执行前请求全部的资源。如果所需的资源可用,进程会完成资源的分配并运行到结束。如果有任何一个资源处于频繁分配的情况,那么没有分配到资源的进程就会等待。
很多进程无法在执行完成前就知道到底需要多少资源,如果知道的话,就可以使用银行家算法;还有一个问题是这样无法合理有效利用资源
还有一种方式是进程在请求其他资源时,先释放所占用的资源,然后再尝试一次获取全部的资源。

破坏不可抢占条件

破坏不可抢占条件也是可以的。可以通过虚拟化的方式来避免这种情况。

破坏循环等待条件

现在就剩最后一个条件了,循环等待条件可以通过多种方法来破坏。一种方式是制定一个标准,一个进程在任何时候只能使用一种资源。如果需要另外一种资源,必须释放当前资源。对于需要将大文件从磁带复制到打印机的过程,此限制是不可接受的。
另一种方式是将所有的资源统一编号,如下图所示

进程可以在任何时间提出请求,但是所有的请求都必须按照资源的顺序提出。如果按照此分配规则的话,那么资源分配之间不会出现环。

尽管通过这种方式来消除死锁,但是编号的顺序不可能让每个进程都会接受。
其他问题
下面我们来探讨一下其他问题,包括 通信死锁、活锁是什么、饥饿问题和两阶段加锁

两阶段加锁

虽然很多情况下死锁的避免和预防都能处理,但是效果并不好。随着时间的推移,提出了很多优秀的算法用来处理死锁。例如在数据库系统中,一个经常发生的操作是请求锁住一些记录,然后更新所有锁定的记录。当同时有多个进程运行时,就会有死锁的风险。
一种解决方式是使用 两阶段提交(two-phase locking)。顾名思义分为两个阶段,一阶段是进程尝试一次锁定它需要的所有记录。如果成功后,才会开始第二阶段,第二阶段是执行更新并释放锁。第一阶段并不做真正有意义的工作。
如果在第一阶段某个进程所需要的记录已经被加锁,那么该进程会释放所有锁定的记录并重新开始第一阶段。从某种意义上来说,这种方法类似于预先请求所有必需的资源或者是在进行一些不可逆的操作之前请求所有的资源。
不过在一般的应用场景中,两阶段加锁的策略并不通用。如果一个进程缺少资源就会半途中断并重新开始的方式是不可接受的。

通信死锁

我们上面一直讨论的是资源死锁,资源死锁是一种死锁类型,但并不是唯一类型,还有通信死锁,也就是两个或多个进程在发送消息时出现的死锁。进程 A 给进程 B 发了一条消息,然后进程 A 阻塞直到进程 B 返回响应。假设请求消息丢失了,那么进程 A 在一直等着回复,进程 B 也会阻塞等待请求消息到来,这时候就产生死锁
尽管会产生死锁,但是这并不是一个资源死锁,因为 A 并没有占据 B 的资源。事实上,通信死锁并没有完全可见的资源。根据死锁的定义来说:每个进程因为等待其他进程引起的事件而产生阻塞,这就是一种死锁。相较于最常见的通信死锁,我们把上面这种情况称为通信死锁(communication deadlock)
通信死锁不能通过调度的方式来避免,但是可以使用通信中一个非常重要的概念来避免:超时(timeout)。在通信过程中,只要一个信息被发出后,发送者就会启动一个定时器,定时器会记录消息的超时时间,如果超时时间到了但是消息还没有返回,就会认为消息已经丢失并重新发送,通过这种方式,可以避免通信死锁。
但是并非所有网络通信发生的死锁都是通信死锁,也存在资源死锁,下面就是一个典型的资源死锁。
当一个数据包从主机进入路由器时,会被放入一个缓冲区,然后再传输到另外一个路由器,再到另一个,以此类推直到目的地。缓冲区都是资源并且数量有限。如下图所示,每个路由器都有 10 个缓冲区(实际上有很多)。

假如路由器 A 的所有数据需要发送到 B ,B 的所有数据包需要发送到 D,然后 D 的所有数据包需要发送到 A 。没有数据包可以移动,因为在另一端没有缓冲区可用,这就是一个典型的资源死锁。

活锁

某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会尝试礼貌的释放已经获得的锁,然后等待非常短的时间再次尝试获取。可以想像一下这个场景:当两个人在狭路相逢的时候,都想给对方让路,相同的步调会导致双方都无法前进。
现在假想有一对并行的进程用到了两个资源。它们分别尝试获取另一个锁失败后,两个进程都会释放自己持有的锁,再次进行尝试,这个过程会一直进行重复。很明显,这个过程中没有进程阻塞,但是进程仍然不会向下执行,这种状况我们称之为 活锁(livelock)

饥饿

与死锁和活锁的一个非常相似的问题是 饥饿(starvvation)。想象一下你什么时候会饿?一段时间不吃东西是不是会饿?对于进程来讲,最重要的就是资源,如果一段时间没有获得资源,那么进程会产生饥饿,这些进程会永远得不到服务。
我们假设打印机的分配方案是每次都会分配给最小文件的进程,那么要打印大文件的进程会永远得不到服务,导致进程饥饿,进程会无限制的推后,虽然它没有阻塞。

linux 常用命令

1. 查看端口号

netstat -napt
ss -napt

2. 查看栈空间的大小

ulimit -s        //默认8192
       -a 也可以查看全部

3. 动态库和静态库的制作

g++ -fPIC -shared libxxx.so 
ar rsc  

4. 查看一个文件中次数出现最多的 IP

cat  //输出文件 
uniq -c // 在每一列旁边显示该列的重复次数(-c)  -u 显示仅出现一次的行
sort -n // 按数值(-n)排序 -r 从大到小排序
head -n 200 // 表示显示1-200行

5. 强制刷盘

sync
fsync 

6. GBD 命令

-b  [行数][函数名] 断点
-c  运行到断点
-s  进入函数
-n  下一行
-finish 结束函数
-bt 查看函数堆栈
-frame [spec] 0的话表示当前栈帧 最大的是main函数
-info frame 打印当前栈帧的信息(编号、地址、局部变量等)    
-until 跳出循环体
-b filename:line number 设置程序的断点在某个文件中
-d N 删除断点
-display/print/p: 查看局部变量
-x 查看内存:
命令:x/3uh 0x7ff320
表示,从内存地址0x7ff320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十进制显示无符号整型。

7. 程序崩了是什么情况?怎么处理?

  1. 访问不存在的内存地址
  2. 访问系统保护的内存地址
  3. 数组访问越界等。
    首先可以用ulimit -c unlimited来开启.core文件。
    然后用gdb 程序名 core进行调试

8. 解压和压缩

1. 解压tar.gz 文件: tar -zxf 
2. 解压zip 文件: unzip

9. 创建文件

touch filename.xxx

10. 排查死锁?

  1. 首先可以通过 ps-aux | grep 程序名称 来查看进程的状态。如果程序的 stat 为 Sl + 的话,那么这个多线程进程处于睡眠状态;
  2. 然后需要查看每个线程占用 CPU 的情况,因为主线程处理睡眠有可能是死锁,也有可能是死循环。死循环的话,线程占用的 CPU 资源会比较多。查看的指令是 top -Hp 进程号查看进程内部每个线程的运行情况;
  3. 如果线程占用 CPU 资源为 0,那么就是阻塞了而且死锁了
    那么,我们可以先把debug版本的程序运行起来:
  4. 先查一下进程号,ps- aux | grep 程序名
  5. 这时候可以通过gdb attach pid 调试正在运行的程序,当然这个程序是在debug版本下运行的;
  6. 然后使用thread apply all bt 打印所有线性的调用栈信息;
  7. 然后用 info threads 看一下所有的线程情况
  8. 切换到某一个thread [1|2|3]
  9. 再用where打印该线程的堆栈情况
  10. 从上到下检查有没有 cpp 文件对应的行数;
  11. 再去看对应的行就行了

11. 从一台 Linux 拷贝文件到另外一台主机?

scp 本地 Linux 系统文件路径 远程用户名 @IP 地址: 远程系统文件绝对路径名

12. Linux 查看一个文件大小的方式?

  1. ls - l filepath :显示字节数
  2. stat filepath : 显示字节数

13. Linux 查看一个进程打开的文件?

lsof -p + 进程号

14. Linux 文件权限?

​ 从左到右是三组 rwx,描述了文件所属者、文件所属组、其他用户的可读可写可执行状态。

15. Linux 查看进程的系统调用?

strace 

16. Linux 后台进程?

nohup xxx &

17. Top 命令的参数分别是什么意思?

VIRT:分配的虚拟内存的大小。
RES:实际使用内存的大小。
SHR:共享内存的大小。

18. Linux 查看磁盘的大小?

df -h + 目录

计算机系统基础

1. 程序编译过程

  • gcc HelloWorld.c -E -o HelloWorld.i 预处理:加入头文件,替换宏。
  • gcc HelloWorld.c -S -c -o HelloWorld.s 编译:包含预处理,将 C 程序转换成汇编程序。
  • gcc HelloWorld.c -c -o HelloWorld.o 汇编:包含预处理和编译,将汇编程序转换成可链接的二进制程序。
  • gcc HelloWorld.c -o HelloWorld 链接:包含以上所有操作,将可链接的二进制程序和其它别的库链接在一起,形成可执行的程序文件。

2. 内核结构与设计

计算机资源
  1. 总线,负责连接各种其它设备,是其它设备工作的基础。
  2. CPU,即中央处理器,负责执行程序和处理数据运算。
  3. 内存,负责储存运行时的代码和数据。
  4. 硬盘,负责长久储存用户文件数据。
  5. 网卡,负责计算机与计算机之间的通信。
  6. 显卡,负责显示工作。
  7. 各种 I/O 设备,如显示器,打印机,键盘,鼠标等。
内存管理计算机资源
  1. 管理 CPU,由于 CPU 是执行程序的,而内核把运行时的程序抽象成进程,所以又称为进程管理。
  2. 管理内存,由于程序和数据都要占用内存,内存是非常宝贵的资源,所以内核要非常小心地分配、释放内存。
  3. 管理硬盘,而硬盘主要存放用户数据,而内核把用户数据抽象成文件,即管理文件,文件需要合理地组织,方便用户查找和读写,所以形成了文件系统。
  4. 管理显卡,负责显示信息,而现在操作系统都是支持 GUI(图形用户接口)的,管理显卡自然而然地就成了内核中的图形系统。
  5. 管理网卡,网卡主要完成网络通信,网络通信需要各种通信协议,最后在内核中就形成了网络协议栈,又称网络组件。
  6. 管理各种 I/O 设备,我们经常把键盘、鼠标、打印机、显示器等统称为 I/O(输入输出)设备,在内核中抽象成 I/O 管理器。
    内核要想管理和控制这些硬件就要编写对应的代码,通常这样的代码我们称之为驱动程序
宏内核结构

宏内核就是把以上诸如管理进程的代码、管理内存的代码、管理各种 I/O 设备的代码、文件系统的代码、图形系统代码以及其它功能模块的代码,把这些所有的代码经过编译,最后链接在一起,形成一个大的可执行程序
这个大程序里有实现支持这些功能的所有代码,向用户应用软件提供一些接口,这些接口就是常说的系统 API 函数。而这个大程序会在处理器的特权模式下运行,这个模式通常被称为宏内核模式。

宏内核提供内存分配功能的服务过程,具体如下:

  1. 应用程序调用内存分配的 API(应用程序接口)函数。
  2. 处理器切换到特权模式,开始运行内核代码。
  3. 内核里的内存管理代码按照特定的算法,分配一块内存。
  4. 把分配的内存块的首地址,返回给内存分配的 API 函数。
  5. 内存分配的 API 函数返回,处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址,并且可以使用这块内存了。
    宏内核缺点:
  • 没有模块化,没有扩展性、没有移植性,高度耦合在一起,一旦其中一个组件有漏洞,内核中所有的组件可能都会出问题。
  • 开发一个新的功能也得重新编译、链接、安装内核。
    宏内核优点 :
  • 性能很好,因为在内核中,这些组件可以互相调用,性能极高。
微内核结构

微内核功能尽可能少:仅仅只有进程调度、处理中断、内存空间映射、进程间通信等功能。
实际的进程管理、内存管理、设备管理、文件管理等服务功能,做成一个个服务进程。和用户应用进程一样,只是它们很特殊,宏内核提供的功能,在微内核架构里由这些服务进程专门负责完成。
微内核定义了一种良好的进程间通信的机制——消息
应用程序要请求相关服务,就向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,接着服务进程会完成相关的服务。服务进程的编程模型就是循环处理来自其它进程的消息,完成相关的服务功能。

微内核提供内存分配功能的服务过程:

  1. 应用程序发送内存分配的消息,这个发送消息的函数是微内核提供的,相当于系统 API,微内核的 API(应用程序接口)相当少,极端情况下仅需要两个,一个接收消息的 API 和一个发送消息的 API。
  2. 处理器切换到特权模式,开始运行内核代码。
  3. 微内核代码让当前进程停止运行,并根据消息包中的数据,确定消息发送给谁,分配内存的消息当然是发送给内存管理服务进程。
  4. 内存管理服务进程收到消息,分配一块内存。
  5. 内存管理服务进程,也会通过消息的形式返回分配内存块的地址给内核,然后继续等待下一条消息。
  6. 微内核把包含内存块地址的消息返回给发送内存分配消息的应用程序。
  7. 处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址,并且可以使用这块内存了。
    微内核优点:
  • 系统结构相当清晰利于协作开发。
  • 系统有良好的移植性
  • 微内核有相当好的伸缩性、扩展性,因为那些系统功能只是一个进程
    微内核缺点:
  • 同样是分配内存,一来一去的消息带来了非常大的开销,当然各个服务进程的切换开销也不小。这样系统性能就大打折扣。
分离硬件的相关性
  • 分离硬件的相关性,就是要把操作硬件和处理硬件功能差异的代码抽离出来,形成一个独立的软件抽象层,对外提供相应的接口,方便上层开发。
  • 硬件平台相关的代码都抽离出来,放在一个独立硬件相关层中实现并且定义好相关的调用接口,再在这个层之上开发内核的其它功能代码,就会方便得多,结构也会清晰很多。
    操作系统的移植性也会大大增强,移植到不同的硬件平台时,就构造开发一个与之对应的硬件相关层。这就是分离硬件相关性的好处。

3.CPU 工作模式

  1. 实模式,单道程序能掌控计算机所有的资源,仅支持 16 位地址空间,分段的内存模型,对指令不加限制地运行,对内存没有保护隔离作用。
  2. 保护模式,保护模式包含特权级,对指令及其访问的资源进行控制,对内存段与段之间的访问进行严格检查,没有权限的绝不放行,对中断的响应也要进行严格的权限检查,扩展了 CPU 寄存器位宽,使之能够寻址 32 位的内存地址空间和处理 32 位的数据,从而 CPU 的性能大大提高。
  3. 长模式,又名 AMD64 模式,在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器,使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力。长模式弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU。

4. 虚拟地址与真实地址

虚拟地址到物理地址的转换

虚拟地址到物理地址的转换: 软硬件结合的方式实现,它就是 MMU(内存管理单元)。MMU 可以接受软件给出的地址对应关系数据,进行地址转换。

上图中展示了 MMU 通过地址关系转换表,将 0x80000~0x84000 的虚拟地址空间转换成 0x10000~0x14000 的物理地址空间,而地址关系转换表本身则是放物理内存中的
分页模型: 把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。根据软件配置不同,这个页的大小可以设置为 4KB、2MB、4MB、1GB

结合图片可以看出,一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放虚拟页地址对应的物理页地址就行了。

MMU

MMU 即内存管理单元,是用硬件电路逻辑实现的一个地址转换器件,它负责接受虚拟地址和地址关系转换表,以及输出物理地址。

上图中,程序代码中的虚拟地址,经过 CPU 的分段机制产生了线性地址,平坦模式和长模式下线性地址和虚拟地址是相等的。

MMU 页表

地址关系转换表——页表。它描述了虚拟地址到物理地址的转换关系,也可以说是虚拟页到物理页的映射关系,所以称为页表。
页表中并不存放虚拟地址和物理地址的对应关系,只存放物理页面的地址,MMU 以虚拟地址为索引去查表返回物理页面地址,而且页表是分级的,总体分为三个部分:一个顶级页目录,多个中级页目录,最后才是页表,逻辑结构图如下.

从上面可以看出,一个虚拟地址被分成从左至右四个位段:

  • 第一个位段索引顶级页目录中一个项,该项指向一个中级页目录,
  • 然后用第二个位段去索引中级页目录中的一个项,该项指向一个页目录,
  • 再用第三个位段去索引页目录中的项,该项指向一个物理页地址,
  • 最后用第四个位段作该物理页内的偏移去访问物理内存。这就是 MMU 的工作流程。

5.Cache 与内存

内存: 是计算机中用于存储数据和程序的硬件设备。它允许计算机快速读取和写入数据,以及执行指令。内存通常被描述为随机访问存储器(RAM),因为它可以随时存取任意地址的数据。
Cache(缓存):是一种高速缓存存储器,位于计算机内部的 CPU 和主内存之间。它用于暂时存储处理器频繁访问的数据和指令,以减少对主内存的访问次数,从而提高系统性能。

Cache 的工作流程如下:
  1. CPU 首先从 L1 cache 开始查询所需数据或指令。
  2. 如果在当前级别的 cache 中命中,则直接读取数据或指令;否则,查询下一个级别的 cache。
  3. 依次查询 L2 和 L3 cache,如果命中,则从相应的 cache 中读取数据或指令,并将其复制到更高一级的 cache 中供以后使用。
  4. 如果都未命中,则从主内存中读取数据或指令,并将其复制到所有级别的 cache 中。
  5. 写操作时,若数据存在于缓存中,则直接更新缓存中的数据,否则写入主内存。
  6. 当缓存满时,采用替换策略腾出空间,常见的策略包括 LRU、FIFO 和随机替换等。
Cache 的结构


这是一颗最简单的双核心 CPU,它有三级 Cache,第一级 Cache 是指令和数据分开的,第二级 Cache 是独立于 CPU 核心的,第三级 Cache 是所有 CPU 核心共享的。

Cache 缓存一致性问题
  1. 一个 CPU 核心中的指令 Cache 和数据 Cache 的一致性问题。
    对于程序代码运行而言,指令都是经过指令 Cache,而指令中涉及到的数据则会经过数据 Cache。
    修改了内存地址 A 这个位置的代码,这个时候通过储存的方式去写的地址 A,所以新的指令会进入数据 Cache。但是接下来去执行地址 A 处的指令的时候,指令 Cache 里面可能命中的是修改之前的指令。
    所以,这个时候软件需要把数据 Cache 中的数据写入到内存中,然后让指令 Cache 无效,重新加载内存中的数据。
  2. 多个 CPU 核心各自的 2 级 Cache 的一致性问题。
    为了解决这些问题,硬件工程师们开发了多种协议,典型的多核心 Cache 数据同步协议有 MESI 和 MOESI

6. 解决数据同步的四种方法

  1. 原子变量,在只有单个变量全局数据的情况下,这种变量非常实用,如全局计数器、状态标志变量等。我们利用了 CPU 的原子指令实现了一组操作原子变量的函数。
  2. 中断的控制。当要操作的数据很多的情况下,用原子变量就不适合了。但是我们发现在单核心的 CPU,同一时刻只有一个代码执行流,除了响应中断导致代码执行流切换,不会有其它条件会干扰全局数据的操作,所以我们只要在操作全局数据时关闭或者开启中断就行了,为此我们开发了控制中断的函数。
  3. 自旋锁。由于多核心的 CPU 出现,控制中断已经失效了,因为系统中同时有多个代码执行流,为了解决这个问题,我们开发了自旋锁,自旋锁要么一下子获取锁,要么循环等待最终获取锁。
  4. 信号量。如果长时间等待后才能获取数据,在这样的情况下,前面中断控制和自旋锁都不能很好地解决,于是我们开发了信号量。信号量由一套数据结构和函数组成,它能使获取数据的代码执行流进入睡眠,然后在相关条件满足时被唤醒,这样就能让 CPU 能有时间处理其它任务。所以信号量同时解决了三个问题:等待、互斥、唤醒。

7. linux 启动流程

8. 如何划分与组织内存?

内存分段和分页是常用的内存管理方式,每种方式都有其优缺点。
内存分段的优点包括:

  1. 允许多个进程共享物理内存;
  2. 可以按需加载数据,减少内存占用量;
  3. 避免了内部碎片问题;
  4. 段表可以动态增长,更加灵活。
    其缺点包括:
  5. 内存分段需要消耗额外的时间和内存来维护段表,增加了系统开销;
  6. 分段容易造成外部碎片,导致内存利用率下降。
    内存分页的优点包括:
  7. 可以实现虚拟内存扩展,允许程序使用比实际物理内存更多的内存;
  8. 分页保证了固定大小的块,使操作系统更高效地管理内存,减少内存碎片;
  9. 分页也可以实现内存保护,防止程序意外修改数据。
    其缺点包括:
  10. 分页需要消耗额外的时间和内存来维护页表,增加了系统开销;
  11. 分页可能会导致内部碎片问题,即一页的大小过大时,可能会浪费一部分内存空间;
  12. 分页不支持多个进程共享同一个页面。
    分段与分页的区别,发现段长度不一,容易产生内存碎片、不容易和硬盘换入换出数据,更不能实现扁平化的虚拟内存地址空间,其实现在所有的商用操作系统都使用了分页模式管理内存。

9. Linux

伙伴系统如何分配内存?(管理物理内存页)
伙伴系统(buddy system) :是一种常见的内存管理算法,它负责管理 Linux 中物理内存的分配和释放。

  • 这个算法通过将可用的内存按照 2 的幂次方进行划分,形成一棵二叉树,每个节点表示某个内存块的大小和状态(已分配或者未分配)。
  • 当需要分配一块内存时,伙伴系统会找到大小最接近并且大于等于所需内存大小的空闲内存块,并将其划分为两个伙伴块(即大小相同的两个子节点),其中一个被分配给请求方,另一个则重新加入伙伴系统的空闲链表中。
  • 当一个内存块被释放时,伙伴系统会检查该块的伙伴是否也为空闲状态,如果是,则将两个伙伴块合并成一个更大的块,继续加入空闲链表中。这种方式可以避免内存碎片问题,并提高内存利用率。
    在 Linux 物理内存页面管理中,连续且相同大小的 pages 就可以表示成伙伴

    上图中,首先最小的 page(0,1)是伙伴,page(2,3)是伙伴,page(4,5)是伙伴,page(6,7)是伙伴,然后 A 与 B 是伙伴,C 与 D 是伙伴,最后 E 与 F 是伙伴。有了图解,你是不是瞬间明白伙伴系统的伙伴了呢?
    Linux 下怎样分配物理内存页面首先要找到内存节点,接着找到内存区,然后合适的空闲链表,最后在其中找到页的 page 结构,完成物理内存页面的分配。
  • 内存节点(pglist_data):Linux 对 NUMA (Non-Uniform Memory Access,非一致性内存访问)进行了抽象,它可以将一整块连续物理内存的划分成几个内存节点,也可以把不是连续的物理内存当成真正的 NUMA。
  • 内存区(zone):因为硬件的限制,Linux 内核不能对所有的物理内存页统一对待,所以就把属性相同物理内存页面,归结到了一个区中。zone 中 free_area 结构的数组,这个数组就是用于实现伙伴系统。
  • free_area :实现伙伴系统 , 其中 MAX_ORDER 的值默认为 11,分别表示挂载地址, 连续的 page 结构数目为 (0 - 2^11) 1,2,4,8,16,32…… 最大为 1024。free_area 结构中又是一个 list_head 链表数组, 该数组将具有相同迁移类型的 page 结构尽可能地分组,有的页面可以迁移,有的不可以迁移,同一类型的所有相同 order 的 page 结构,就构成了一组 page 结构块。

    linux 分配内存页面的主要函数__alloc_pages_nodemask:
  1. 准备分配页面的参数;
  2. 进入快速分配路径;
  3. 若快速分配路径没有分配到页面,就进入慢速分配路径。
  • 快速路径分配:是指当有足够的可用内存时,Linux 伙伴系统将首选最接近所需大小的可用块进行分配。该过程非常快速,因为它只涉及到查找可用块的信息。
  • 慢速路径分配:是指当没有足够的可用内存时,Linux 伙伴系统必须通过一系列操作来拆分较大的块,直到找到适合所需大小的块。此过程可能需要遍历整个内存二叉树,因此速度较慢。虽然慢速路径分配比快速路径分配要慢,但它确保了内存的高效利用,因为它能够将较大的块拆分成更小的块以满足内存请求。

10. SLAB 如何分配内存?

(kmalloc() 函数使用)
伙伴系统的缺点

  • Buddy 提供了以 page 为单位的内存分配接口,这对内核来说颗粒度还太大了,所以需要一种新的机制,将 page 拆分为更小的单位来管理
  • 所以,引入 slab 分配器是为了弥补内存管理粒度太大的不足
    slab 能解决什么问题?
  • slab 分配需要解决的是内存的内部碎片问题。
    slab 分配例子:
  • 比如我需要一个 100 字节的连续物理内存,那么内核 slab 分配器会给我提供一个相应大小的连续物理内存单元,为 128 字节大小 (不会是整好 100 字节,而是这个档的一个对齐值,如 100 字节对应 128 字节,30 字节对应 32 字节,60 字节对应 64 字节)
    slab 分配器的基本思想
  • 先利用页面分配器分配出单个或者一组连续的物理页面,然后在此基础上将整块页面分割成多个相等的小内存单元,以满足小内存空间分配的需要。当然,为了有效的管理这些小的内存单元并保证极高的内存使用速度和效率。
    什么是 object?
  • object 是 slab 内存分配器对外提供的申请内存的基本单位。slab 内存分配器从 buddy system 申请了 buddy 之后,会将其拆分成一个个 object,并缓存在 kmem cache 实例的 cpu_cache 中,用户申请内存时,其实获取的就是一个个 object。
  • 一旦 object 缓存耗尽,就会重新从 buddy system 申请 slab,并将其拆分成 object,放入内存池。
    什么是 cache?
  • slab 内存分配器中的 cache 跟硬件 cache 无关,是一个纯软件的概念。
  • slab 内存分配器有两种 cache,一个是 slab 的 cache,一个是 object 的 cache。
  • slab 内存分配器从 buddy system 获取页面后,会将其加入 kmem cache 的 node 节点,这个就是 slab 的 cache;
  • 将 slab 拆分成多个 object,并将 object 加入 kmem cache 的 cpu_cache 内存池,这个就是 object 的 cache;
  • 可以看到这两种 cache 实际是对共同的物理页面的两种缓存形式。
    slab 内存结构

    SLAB 对象: 在 SLAB 分配器中,它把一个内存页面或者一组连续的内存页面,划分成大小相同的块,其中这一个小的内存块就是 SLAB 对象
    array_cache 结构 : 是每个 CPU 一个 array_cache 类型的变量,cpu_cache 是用于管理空闲对象的
    kmem_cache 结构:SLAB 管理头用 kmem_cache 结构来表示
    kmem_cache_node 结构 : 每个内存节点对应一个,它就是用来管理 kmem_cache 结构的。kmem_cache_node 中的三个链表,它们分别挂载的 pages,有一部分是空闲对象的 page、还有对象全部都已经分配的 page,以及全部都为空闲对象的 page。这是为了提高分配时查找 kmem_cache 的性能。
    SLAB 分配对象的过程
  1. 根据请求分配对象的大小,查找对应的 kmem_cache 结构,接着从这个结构中获取 arry_cache 结构,然后分配对象。
  2. 如果没有空闲对象了,就需要在 kmem_cache 对应的 kmem_cache_node 结构中查找有空闲对象的 kmem_cache。
  3. 如果还是没找到,最后就要分配内存页面新增 kmem_cache 结构了。

11. Linux 进程与进程调度


为什么要用红黑树来组织调度实体? 这是因为要维护虚拟时间的顺序,又要从中频繁的删除和插入调度实体,这种情况下红黑树这种结构是最好的

12. 虚拟文件系统管理文件

VFS(Virtual Filesystem Switch)虚拟文件系统 :是一个内核软件层,在具体的文件系统之上抽象的一层,表现为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统,同时也为不同文件系统的通信提供了媒介。

  • VFS 提供了一个抽象层,让不同的文件系统表现出一致的行为。对于用户空间和内核空间的其他部分,这些文件系统看起来都是一样的:文件都有目录,都支持建立、打开,读写、关闭和删除操作,不用关注不同文件系统的细节。
  • 操作具体文件时,VFS 会根据需要调用具体文件系统的函数。从此文件系统的细节就被 VFS 屏蔽了,应用程序只需要调用标准的接口就行了。
    VFS 的数据结构:
  • 超级块结构:表示文件系统 , 中包含了 VFS 规定的标准信息,也有具体文件系统的特有信息
  • 目录结构:表示文件路径的 ,目录也是文件,需要用 inode 索引结构来管理目录文件数据。目录文件数据:名称、类型(文件或者目录)、inode 号
  • 索引结点结构:inode 结构表示一个文件索引结点,它里面包含文件权限、文件所属用户、文件访问和修改时间、文件数据块号等一个文件的全部信息,一个 inode 结构就对应一个文件
  • 文件对象结构:进程打开的文件实例结构,存放已打开的文件和进程之间交互的信息,包含了我们非常熟悉的信息,如访问模式、当前读写偏移等

13. 从 URL 到网卡

如何全局观察网络数据流动?
输入 URL,从一个请求到响应都发生了什么事?

  1. 常规的网络交互过程是从客户端发起网络请求,用户态的应用程序(浏览器)会生成 HTTP 请求报文、并通过 DNS 协议查找到对应的远端 IP 地址。
  2. 在套接字生成之后进入内核态,浏览器会委托操作系统内核协议栈中的上半部分,也就是 TCP/UDP 协议发起连接请求。
  3. 然后经由协议栈下半部分的 IP 协议进行封装,使数据包具有远程定位能力。
  4. 经过 MAC 层处理,找到接收方的目标 MAC 地址。
  5. 最终数据包在经过网卡转化成电信号经过交换机、路由器发送到服务端,服务端经过处理拿到数据,再通过各种网络协议把数据响应给客户端。
  6. 客户端拿到数据进行渲染。
  7. 客户端和服务端之间反复交换数据,客户端的页面数据就会发生变化。
    网络为什么要分层呢?
    通过分层处理简化问题难度,分层也保证了网络的松耦合和相对的灵活,分层拆分后易于各层的实现和维护,也方便了各层的后续扩展。
    TCP/IP 层的体系结构中数据处理的过程和网络协议
    TCP/IP 协议栈是现在使用最广泛的网络协议栈,Internet 就是建立在 TCP/IP 协议栈基础上的。
    协议栈内部分为几部分,分别承担着不同的作用。
  • 协议栈的上半部分负责和应用层通过套接字(Socket)进行交互,它可以是 TCP 协议或 UDP 协议。应用层会委托协议栈的上部分完成收发数据的工作;
  • 而协议栈的下半部分则负责把数据发送给到指定方的 IP 协议,由 IP 协议连接下层的网卡驱动。
  1. 用户输入:在浏览器中输入 URL
    浏览器根输入内容校验 URL 的合法性,并补全 URL
    对 URL 进行解析,浏览器确定了服务器的主机名和请求路径
  2. 网络请求前:查看浏览器缓存
    浏览器首先会检查保存在本地计算机中的缓存,如果访问过当前的 URL,会先进入缓存中查询是否有要请求的文件。
    此时存在的缓存有路由器缓存、DNS 缓存、浏览器缓存、Service Worker、Memory Cache、Disk Cache、Push Cache、系统缓存等。
  3. 域名解析:DNS
    在发送消息之前,查找服务端的 IP 地址
    访问 DNS 服务器,维护了 IP 和域名的映射关系 。
    客户端只要请求到一个 DNS 服务器,就可以一层层递归和迭代查找到所有的 DNS 服务器了
  4. 可靠性传输:建立 TCP 连接
    浏览器通过 DNS 解析拿到 IP 地址后, 取出 URL 的端口。操作系统协议栈的上半部分创建新的套接字(Socket)向对应的 IP 发起 TCP 连接请求。
    建立 TCP 首先会先进行三次握手
  5. 目的地定位:IP 层
    IP 协议是 TCP/IP 协议栈的核心,IP 协议中规定了在 Internet 上进行通信时应遵循的规则,包括 IP 数据包应如何构成、数据包的路由等,而 IP 层实现了网络上的点对点通信。
    IP 层被设计成三个部分,分别是 IP 寻址、路由和分包组包
  6. 点对点传输:MAC(链路层)
    MAC 地址指的就是计算机网卡的物理地址(Physical Address),MAC 地址被固化到网卡中,是唯一且无重复的
    对 IP 包加上 MAC 头,这个 MAC 头包括发送方的 MAC 头和接收方的 MAC 头,用于两个物理地址点对点的传输
  7. 电信号的出口:网卡(物理层)
    网卡驱动程序会将 MAC 数据包写入网卡的缓冲区(网卡上的内存).
    网卡会在 MAC 数据包的起止位置加入起止帧和校验序列,最后网卡会将加入起止帧和校验序列的 MAC 数据包转化为电信号,发送出去。
  8. 客户端服务端的持续数据交换(应用层)
  • 数据从网卡出去,到达客户端,再重复刚才的过程拿到相应数据。
  • 客户端拿到对应的 HTML 资源,浏览器就可以开始解析渲染了,这步操作完成后,用户最终就能通过浏览器看到相应的页面。

14. 从内核到应用

网络数据在内核中如何流转
网络发收过程
发送过程

  1. 应用程序首先会准备好数据,调用用户态下的库函数。接着调用系统 API 接口函数,进入到内核态。
  2. 内核态对应的系统服务函数会复制应用程序的数据到内核的内存空间中,然后将数据移交给网络协议栈
  3. 在网络协议栈中将数据层层打包。包装好的数据会交给网卡驱动
  4. 网卡驱动程序负责将打包好的数据写入网卡并让其发送出去。

    接收过程
  5. 网卡接收到数据,通过 DMA 复制到指定的内存,接着发送中断,以便通知网卡驱动,
  6. 网卡驱动处理中断复制数据。
  7. 然后网络协议收到网卡驱动传过来的数据,层层解包,获取真正的有效数据。
  8. 数据会发送给用户态监听的应用进程。

15. 详解 socket

实现与网络编程接口
套接字是 UNIX 兼容系统的一大特色,从实现的角度来看,套接字是通信的抽象描述;从内核角度看,同时也是一个管理通信过程的对象——struct socket 结构。
在 Linux 操作系统中,替代传输层以上协议实体的标准接口,称为套接字,它负责实现传输层以上所有的功能,可以说套接字是 TCP/IP 协议栈对外的窗口

Linux 的网络体系结构可以支持多个协议栈和网络地址类型,通过地址族的值和协议交换表,Linux 的套接字实现了支持多协议栈这项功能。

  1. Linux 套接字体系结构独立于具体网络协议栈的套接字,可以同时支持多个网络协议栈的工作。
  2. 任何协议栈都可以在套接字通用体系结构的基础上,派生出具有协议族特点的套接字接口。

16. linux: 系统调用 API

Linux 内核也是一样,应用程序会调用库函数,在库函数中调用 API 入口函数,触发中断进入 Linux 内核执行系统调用,完成相应的功能服务。
在 Linux 内核之上,使用最广泛的 C 库是 glibc,其中包括 C 标准库的实现,也包括所有和系统 API 对应的库接口函数。几乎所有 C 程序都要调用 glibc 的库函数,所以 glibc 是 Linux 内核上 C 程序运行的基础。

在 Linux 中,应用程序使用系统调用的流程:

  1. 应用程序使用库函数,库函数中调用系统调用 API 的接口。 需要将参数存储在特定的寄存器或内存位置中,包括系统调用号。
  2. 系统调用指令 syscall ,将控制权转移到内核态。
  3. 内核接收到系统调用请求后,使用系统调用号来访问系统调用表,并获取对应系统调用的入口地址。
  4. 内核函数会根据应用程序提供的参数执行相应的操作。
  5. 当内核完成系统调用后,它将结果存储在一个特定的寄存器或内存位置中,使用系统返回指令 sysexit,返回用户态。

操作系统知识点总结

一、操作系统概述

1.1 操作系统的定义与目标

定义:操作系统是控制管理计算机系统的硬软件,分配调度资源的系统软件
目标:方便性,有效性(提高系统资源的利用率、提高系统的吞吐量),可扩充性,开放性。

1.2 操作系统的基本功能
  1. 统一管理计算机资源:处理器资源,IO 设备资源,存储器资源,文件资源;
  2. 实现了对计算机资源的抽象:IO 设备管理软件提供读写接口,文件管理软件提供操作文件接;
  3. 提供了用户与计算机之间的接口:GUI(图形用户界面),命令形式,系统调用形式。
1.3 操作系统的特征

最基本的特征,互为存在条件:并发,共享;
(1)并行:指两个或多个事件可以在同一个时刻发生,多核 CPU 可以实现并行,一个 cpu 同一时刻只有一个程序在运行;
(2)并发:指两个或多个事件可以在同一个时间间隔发生,用户看起来是每个程序都在运行,实际上是每个程序都交替执行

(3)共享性:操作系统的中资源可供多个并发的程序共同使用,这种形式称之为资源共享
互斥共享:当资源被程序占用时,其它想使用的程序只能等待。
同时访问:某种资源并发的被多个程序访问。
虚拟和异步特性前提是具有并发性
(4)虚拟性:表现为把一个物理实体转变为若干个逻辑实体。
时分复用技术:资源在时间上进行复用,不同程序并发使用,多道程序分时使用计算机的硬件资源,提高资源的利用率。
空分复用技术:用来实现虚拟磁盘(物理磁盘虚拟为逻辑磁盘,电脑上的 C 盘、D 盘等)、虚拟内存(在逻辑上扩大程序的存储容量)等,提高资源的利用率,提高编程效率。
(5)异步性:在多道程序环境下,允许多个进程并发执行,但由于资源等因素的限制,使进程的执行以 “停停走走” 的方式运行,而且每个进程执行的情况(运行、暂停、速度、完成)也是未知的

1.4 操作系统的中断处理

中断机制的作用:为了在多道批处理系统中让用户进行交互;
中断产生

  • 发生中断时,CPU 立马切换到管态,开展管理工作;(管态又叫特权态,系统态或核心态,是操作系统管理的程序执行时,机器所处的状态。)
  • 发生中断后,当前运行的进程回暂停运行,由操作系统内核对中断进行处理;
  • 对于不同的中断信号,会进行不同的处理。
    中断的分类
  1. 内中断(也叫 “异常”、“例外”、“陷入”)- 信号来源:CPU 内部,与当前执行指令有关;
  2. 外中断(中断) 信号来源:CPU 外部,与当前执行指令无关。
    外中断的处理过程
  3. 每执行完一个指令后,CPU 都需要检查当前是否有外部中断 信号;
  4. 如果检查到外部中断信号,则需要保护被中断进程的 CPU 环境(如程序状态字 PSW,程序计数器 PC、各种通用寄存器)把他们存储在 PCB(进程控制块中);
  5. 根据中断信号类型转入相应的中断处理程序;
  6. 恢复原进程的 CPU 环境并退出中断,返回原进程继续执行。

二、进程管理

2.1 进程管理之进程实体

为什么需要进程

  1. 进程是系统进行资源分配和调度的基本单位
  2. 进程作为程序独立运行的载体保障程序正常执行;
  3. 进程的存在使得操作系统资源的利用率大幅提升。+
    进程控制块(PCB):用于描述和控制进程运行的通用数据结构, 记录进程当前状态和控制进程运行的全部信息,是进程存在的唯一标识
    进程(Process)与线程(Thread)
    线程:操作系统进行 ** 运行调度的最小单位 **。
    进程:系统进行 ** 资源分配和调度的基本单位 **。
    区别与联系
  4. 一个进程可以有一个或多个线程
  5. 线程包含在进程之中,是进程中实际运行工作的单位
  6. 进程的线程共享进程资源
  7. 一个进程可以并发多个线程,每个线程执行不同的任务
2.2 进程管理之五状态模型

就绪状态:其它资源(进程控制块、内存、栈空间、堆空间等)都准备好、只差 CPU 的状态。
  执行状态:进程获得 CPU,其程序正在执行。
  阻塞状态:进程因某种原因放弃 CPU 的状态,阻塞进程以队列的形式放置。
  创建状态:创建进程时拥有 PCB 但其它资源尚未就绪。
  终止状态:进程结束由系统清理或者归还 PCB 的状态。

2.3 进程管理之进程同步

生产者 - 消费者问题:有一群生产者进程在生产产品,并将这些产品提供给消费者进程进行消费,生产者进程和消费者进程可以并发执行,在两者之间设置了一个具有 n 个缓冲区的缓冲池,生产者进程需要将所生产的产品放到缓冲区中(+1 操作),消费者进程可以从缓冲区取走产品消费(-1 操作)。


产生问题:当两者并发执行时可能出差错,导致预期的结果与真实的结果不相符:当执行生产者 + 1 和消费者 - 1 操作之后,缓冲区的值从 10 变为了 11

哲学家进餐问题:有 5 个哲学家,他们的生活方式是交替的思考和进餐,哲学家们共同使用一张圆桌,分别坐在 5 张椅子上,圆桌上有 5 只碗和 5 只筷子。平时哲学家们只进行思考,饥饿时则试图取靠近他们的左右两只筷子,只有两只筷子都被拿到的时候才能进餐,否则等待,进餐完毕后,放下左右筷子进行思考。

这会导致以下的问题,筷子就相当于临界资源:
临界资源指的是一些虽作为共享资源却又无法同时被多个线程共同访问的共享资源。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放该共享资源才可重新竞争使用共享资源。

进程同步的作用:对竞争资源在多进程间进行使用次序的协调,使得并发执行的多个进程之间可以有效使用资源和相互合作
进程间同步的四原则

  1. 空闲让进:资源无占用,允许使用;
  2. 忙则等待:资源被占用,请求进程等待;
  3. 有限等待:保证有限等待时间能够使用资源;
  4. 让权等待:等待时,进程需要让出 CPU。
2.3.1 进程同步的方法(重要)

  1. 使用 fork 系统调用创建进程:使用 fork 系统调用无参数,fork 会返回两次,分别返回子进程 id 和 0,返回子进程 id 的是父进程,返回 0 的是子进程。
  • fork 系统调用是用于创建进程的;
  • fork 创建的进程初始化状态与父进程一样;
  • 系统会为 fork 的进程分配新的资源
  1. 共享内存:在某种程度上,多进程是共同使用物理内存的,但是由于操作系统的进程管理,进程间的内存空间是独立的,因此进程默认是不能访问进程空间之外的内存空间的。
  • 共享存储允许不相关的进程访问同一片物理内存;
  • 共享内存是两个进程之间共享和传递数据最快的方式
  • 共享内存未提供同步机制,需要借助其他机制管理访问;

    3.Unix 域套接字
    域套接字是一种高级的进程间通信的方法,可以用于同一机器进程间通信。
    套接字(socket):为网络通信中使用的术语。
    Unix 系统提供的域套接字提供了网络套接字类似的功能,如 Nfinx、uWSGI 等。
    服务端和客户端分别使用 Unix 域套接字的过程:
2.3.2 线程同步的方法(重要)

线程同步的方法

  1. 互斥锁:互斥锁是最简单的线程同步的方法,也称为互斥量,处于两态之一的变量:解锁和加锁,两个状态可以保证资源访问的串行。 原子性:指一系列操作不可被中断的特性,要么全部执行完成,要么全部没有执行。

  2. 自旋锁:自旋锁是一种多线程同步的变量,使用自旋锁的线程会反复检查锁变量是否可用,自旋锁不会让出 CPU,是一种忙等待状态,即死循环等待锁被释放自旋锁的效率远高于互斥锁。特点:避免了进程或者线程上下文切换的开销,但是不适合在单核 CPU 使用

  3. 读写锁:是一种特殊的自旋锁,允许多个读操作同时访问资源以提高读性能,但是对写操作是互斥的,即 ** 对多读少写的操作效率提升 ** 很显著。

  4. 条件变量:是一种相对比较复杂的线程同步方法,条件变量允许线程睡眠,直到满足某种条件,当 ** 满足条件时,可以给该线程信号通知唤醒 **。

2.3.3 线程同步方法对比(重要)



2.4 Linux 的进程管理

进程的类型

  1. 前台进程:具有终端,可以和用户交互;
  2. 后台进程:没有占用终端,基本不和用户交互,优先级比前台进程低(将需要执行的命令以 “&” 符号结束);
  3. 守护进程:特殊的后台进程,在系统引导时启动,一直运行直到系统关闭(进程名字以 “d” 结尾的一般都是守护进程),如 crond、sshd、httpd、mysqld…
    进程的标记
  4. 进程 ID:非负整数,进程的唯一标记,每个进程拥有不同的 ID;
  5. 进程的状态标记:R 表示进程处于运行状态,S 表示进程处于睡眠状态…

    操作 Linux 进程的相关命令
  6. ps 命令:列出当前的进程,结合 - aux 可以打印进程的详细信息(ps -aux);
  7. top 命令:查看所有进程的状态;
  8. kill 命令:给进程发送信号。

三、作业管理

3.1 作业管理之进程调度

定义:指计算机通过决策决定哪个就绪进程可以获得 CPU 使用权
什么时候需要进程调度

  1. 主动放弃:进程正常终止;运行过程中发生异常而终止;主动阻塞(如等待 I/O);
  2. 被动放弃:分给进程的时间片用完;有更高优先级的进程进入就绪队列;有更紧急的事情需要处理(如 I/O 中断);
    进程调度方式
    非抢占式调度:只能由当前运行的进程主动放弃 CPU
  • 处理器一旦分配给某个进程,就让该进程一直使用下去;
  • 调度程序不以任何原因抢占正在被使用的处理器;
  • 调度程序不以任何原因抢占正在被使用的处理器;
    抢占式调度:可由操作系统剥夺当前进程的 CPU 使用权
  • 允许调度程序以一定的策略暂停当前运行的进程;
  • 保存好旧进程的上下文信息,分配处理器给新进程;

    进程调度的三大机制
    就绪队列的排队机制:为了提高进程调度的效率,将就绪进程按照一定的方式排成队列,以便调度程序可以最快找到就绪进程。

    选择运行进程的委派机制:调度程序以一定的策略,选择就绪进程,将 CPU 资源分配给它。
    新老进程的上下文切换机制:保存当前进程的上下文信息,装入被委派执行进程的运行上下文。

    进程调度算法
  1. 先来先服务算法:按照在就绪队列中的先后顺序执行。
  2. 短进程优先调度算法:优先选择就绪队列中估计运行时间最短的进程,不利于长作业进程的执行。
  3. 高优先权优先调度算法:进程附带优先权,优先选择权重高的进程,可以使得紧迫的任务优先处理。
  4. 时间片轮转调度算法:按照 FIFO 的原则排列就绪进程,每次从队列头部取出待执行进程,分配一个时间片执行,是相对公平的调度算法,但是不能保证就是响应用户。
3.2 作业管理之死锁
3.2.1 进程死锁、饥饿、死循环的区别:

死锁两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。永远在互相等待的进程称为死锁进程。
饥饿:由于长期得不到资源导致进程无法推进;
死循环:代码逻辑 BUG。
死锁的产生:竞争资源(共享资源数量不满足各进程需求)、进程调度顺序不当,当调度顺序为 A->B->C->D 时会产生死锁,但改为 A->D->B->C 则不会产生。

死锁的四个必要条件

  1. 互斥条件必须互斥使用资源才会产生死锁
  2. 请求保持条件进程至少保持一个资源,又提出新的资源请求,新资源被占用,请求被阻塞,被阻塞的进程不释放自己保持的资源;
  3. 不可剥夺条件:进程获得的资源在未完成使用前不能被剥夺(包括 OS),只能由进程自身释放
  4. 环路等待条件:发生死锁时,必然存在进程 - 资源环形链, 环路等待不一定造成死锁,但是死锁一定有循环等待。
    死锁的处理策略
    一. 预防死锁的方法:破坏四个必要条件的中一个或多个。
  5. 破坏互斥条件:将临界资源改造成共享资源(Spooling 池化技术);(可行性不高,很多时候无法破坏互斥条件)
  6. 破坏请求保持条件:系统规定进程运行之前,一次性申请所有需要的资源;(资源利用率低,可能导致别的线程饥饿)
  7. 破坏不可剥夺条件:当一个进程请求新的资源得不到满足时,必须释放占有的资源;(实现复杂,剥夺资源可能导致部分工作失效,反复申请和释放造成额外的系统开销)
  8. 破坏环路等待条件:可用资源线性排序,申请必须按照需要递增申请;(进程实际使用资源顺序和编号顺序不同,会导致资源浪费)
    二. 银行家算法:检查当前资源剩余是否可以满足某个进程的最大需求;如果可以,就把该进程加入安全序列,等待进程允许完成,回收所有资源;重复 1,2,直到当前没有线程等待资源;
    三. 死锁的检测和解除:死锁检测算法,资源剥夺法,撤销进程法(终止进程法),进程回退法;

四、存储管理

存储管理为了确保计算机有足够的内存处理数据;确保程序可以从可用内存中获取一部分内存使用;确保程序可以归还使用后的内存以供其他程序使用。

4.1 存储管理之内存分配与回收

内存分配的过程:单一连续分配(已经过时)、固定分区分配、动态分区分配(根据实际需要,动态的分配内存)。
  动态分区分配算法

  1. 首次适应算法:分配内存时,从开始顺序查找适合内存区,若无合适内存区,则分配失败,每次从头部开始,使得头部地址空间不断被划分;
  2. 最佳适应算法:要求空闲区链表按照容量大小排序,遍历以找到最佳适合的空闲区(会留下越来越多的内部碎片)。
  3. 快速适应算法:要求有多个空闲区链表,每个空闲区链表存储一种容量的空闲区。
    内存回收的过程
    回收区在空闲区下方:不需要新建空闲链表节点;只需要把空闲区 1 的容量增大即可;
    回收区在空闲区上方:将回收区与空闲区合并;新的空闲区使用回收区的地址;
    回收区在空闲区中间方:将空闲区 1、空闲区 2 和回收区合并;新的空闲区使用空闲区 1 的地址;
    仅仅剩余回收区:为回收区创建新的空闲节点;插入到相应的空闲区链表中去;
4.2 存储管理之段页式存储管理

页式存储管理:将进程逻辑空间等分成若干大小的页面,相应的把物理内存空间分成与页面大小的物理块,以页面为单位把进程空间装进物理内存中分散的物理块。
页面大小应该适中,过大难以分配,过小内存碎片过多;页面大小通常是 512B~8K
现代计算机系统中,可以支持非常大的逻辑地址空间 (232~264),具有 32 位逻辑地址空间的分页系统,规定页面大小为 4KB,则在每个进程页表中的页表项可达 1M(2 个 20) 个,如果每个页表项占用 1Byte,故每个进程仅仅页表就要占用 1MB 的内存空间。

段式存储管理:将进程逻辑空间分成若干段(不等分),段的长度由连续逻辑的长度决定。
页式和者段式存储管理相比

  1. 段式存储和页式存储都离散地管理了进程的逻辑空间
  2. 页是物理单位,段是逻辑单位
  3. 分页是为了合理利用空间,分段是满足用户要求页大小由硬件固定段长度可动态变化
  4. 页表信息是一维的,段表信息是二维的
    段页式存储管理:现将逻辑空间按照段式管理分成若干段,再将内存空间按照页式管理分成若干页,分页可以有效提高内存利用率分段可以更好的满足用户需求
4.3 存储管理之虚拟内存

虚拟内存概述:是操作系统内存管理的关键技术,使得多道程序运行和大程序运行成为现实,把程序使用内存划分,将部分暂时不使用的内存放置在辅存,实际是对物理内存的扩充
  局部性原理:指 CPU 访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中
  虚拟内存的置换算法:先进先出(FIFO)、最不经常使用(LFU)、最近最少使用(LRU)
虚拟内存的特征
多次性:无需再作业运行时一次性全部装入内存,而是允许被分成多次调入内存;
对换性:无需在作业运行时一直常驻内存,而是允许在作业运行过程中,将作业换入、换出;
虚拟性:从逻辑上扩充了内存的容量,使用户看到的内存用来,远大于实际的容量;

4.4 Linux 的存储管理

Buddy 内存管理算法:经典的内存管理算法,为解决内存外碎片的问题,算法基于计算机处理二进制的优势具有极高的效率。
  Linux 交换空间:交换空间(Swap)是磁盘的一个分区,Linux 内存满时,会把一些内存交换至 Swap 空间,Swap 空间是初始化系统时配置的。
  Swap 空间与虚拟内存的对比

五、文件管理

5.1 操作系统的文件管理

文件的逻辑结构
逻辑结构的文件类型:有结构文件(文本文件,文档,媒体文件)、无结构文件(二进制文件、链接库)。
顺序文件:按顺序放在存储介质中的文件,在逻辑文件当中存储效率最高,但不适合存储可变长文件。
索引文件:为解决可变长文件存储而发明,需要配合索引表存储。
辅存的存储空间分配
辅存的分配方式:连续分配(读取文件容易,速度快)、链接分配(隐式链接和显式链接)、索引分配
辅存的存储空间管理:空闲表、空闲链表、位示图。
目录树:使得任何文件或目录都有唯一的路径。

Linux 文件的基本操作参考链接



Linux 的文件系统:FAT、NTFS(对 FAT 进行改进)、EXT2/3/4(扩展文件系统,Linux 的文件系统)

六、设备管理

I/O 设备的基本概念:将数据输入输出计算机的外部设备;
广义的 IO 设备
按照使用特性分类:存储设备(内存、磁盘、U 盘)和交互 IO 设备(键盘、显示器、鼠标);
按照信息交换分类:块设备(磁盘、SD 卡)和字符设备(打印机、shell 终端);
按照设备共享属性分类:独占设备,共享设备,虚拟设备;
按照传输速率分类:低速设备,高速设备;
IO 设备的缓冲区:减少 CPU 处理 IO 请求的频率,提高 CPU 与 IO 设备之间的并行性
SPOOLing 技术:虚拟设备技术,把同步调用低速设备改为异步调用,在输入、输出之间增加了排队转储环节 (输入井、输出井),SPoOLing 负责输入(出)井与低速设备之间的调度,逻辑上,进程直接与高速设备交互,减少了进程的等待时间。

七、实现支持异步任务的线程池

线程池:线程池是存放多个线程的容器,CPU 调度线程执行后不会销毁线程,将线程放回线程池重新利用。
使用线程池的原因

  1. 线程是稀缺资源 ,不应该频繁创建和销毁;
  2. 架构解耦,业务创建和业务处理解耦,更加优雅;
  3. 线程池是使用线程的最佳实践。
    实现线程安全的队列 Queue
    队列:用于存放多个元素,是存放各种元素的 “池”。
    实现的基本功能:获取当前队列元素数量,往队列放入元素,往队列取出元素。
    注意:队列可能有多个线程同时操作,因此需要保证线程安全,如下两种情况:

    实现基本任务对象 Task
    实现的基本功能:任务参数,任务唯一标记(UUID),任务具体的执行逻辑
    实现任务处理线程 ProcessThread:任务处理线程需要不断地从任务队列里取任务执行,任务处理线程需要有一个标记,标记线程什么时候应该停止。
    实现的基本功能:基本属性(任务队列、标记),线程执行的逻辑(run),线程停止(stop)。
    实现任务处理线程池 Pool:存放多个任务处理线程,负责多个线程的启停,管理向线程池的提交任务,下发给线程去执行。
    实现的基本过程:基本属性,提交任务(put,batch_put),线程启停(start,join),线程池大小(size)。
    实现异步任务处理 AsyncTask:给任务添加一个标记,任务完成后,则标记为完成;任务完成时可直接获取任务运行结果;任务未完成时,获取任务结果,会阻塞获取线程。
    主要实现的两个函数:设置运行结果(set_result),获取运行结果(get_result)

计算机组成原理

是计算机科学与技术领域的重要基础课程,它主要研究计算机系统的硬件结构和各部件的工作原理。以下是计算机组成原理的核心内容:

1. 计算机硬件的基本组成

根据冯·诺依曼体系结构,计算机硬件由以下五大部件组成:

  • 运算器:负责执行算术运算和逻辑运算。
  • 控制器:负责指挥计算机各部件协调工作,通过控制信号驱动其他部件。
  • 存储器:用于存储程序和数据,分为内存储器(如RAM)和外存储器(如硬盘)。
  • 输入设备:用于将外部信息输入到计算机,如键盘、鼠标、扫描仪等。
  • 输出设备:用于将计算机处理的结果输出到外部,如显示器、打印机等。

2. 数据表示与处理

  • 数据的二进制表示:计算机中所有数据(包括数值、字符、图像等)都以二进制形式存储。
  • 浮点数表示:遵循IEEE 754标准,用于表示小数。
  • 字符编码:如ASCII码,用于将字符转换为二进制表示。
  • 算术逻辑单元(ALU):是CPU的核心部件,负责执行算术和逻辑运算。

3. 中央处理单元(CPU)

  • CPU结构:包括控制单元(CU)、算术逻辑单元(ALU)和寄存器组。
  • 指令集架构(ISA):定义了CPU支持的指令集和操作。
  • 性能指标:如时钟频率、流水线技术等,影响CPU的处理速度。
  • 微架构设计:涉及CPU内部的详细设计,包括指令流水线、缓存管理等。

4. 存储系统

  • 主存储器:如RAM,用于临时存储数据和程序。
  • 辅助存储器:如硬盘(HDD)和固态硬盘(SSD),用于长期存储数据。
  • 存储层次结构:包括缓存(Cache)、主存和辅存,通过缓存机制提高存储系统的性能。
  • 虚拟内存:通过分页和分段技术,扩展可用内存。

5. 输入输出系统

  • I/O设备:包括输入设备(键盘、鼠标等)和输出设备(显示器、打印机等)。
  • I/O接口与总线:用于连接CPU、存储器和I/O设备,总线包括数据总线、地址总线和控制总线。
  • I/O控制方式:如中断驱动、直接存储器访问(DMA)等,用于高效的数据传输。

6. 总线技术

  • 总线分类:按功能分为数据总线、地址总线和控制总线;按连接方式分为并行总线和串行总线。
  • 总线通信协议:如PCI、USB、SATA等,定义了设备间的数据传输规则。

7. 系统软件与硬件的交互

  • 操作系统:管理硬件资源,提供用户与硬件交互的接口。
  • 编译器与汇编器:将高级语言程序转换为机器语言。
  • 虚拟机技术:通过虚拟化技术,允许多个操作系统或应用程序共享硬件资源。
    计算机组成原理不仅涵盖了硬件的基本组成和工作原理,还涉及软件与硬件的交互机制。这些内容为理解计算机系统的整体运行提供了基础框架。

多线程死锁的产生

多线程死锁的产生以及如何避免死锁_不同线程访问同一个库为什么会产生死锁

一、死锁的定义

多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
下面我们通过一些实例来说明死锁现象。
先看生活中的一个实例,2 个人一起吃饭但是只有一双筷子,2 人轮流吃(同时拥有 2 只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2 个人都同时占用一个资源,等待另一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也无法继续吃饭。。。
在计算机系统中也存在类似的情况。例如,某计算机系统中只有一台打印机和一台输入 设备,进程 P1 正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程 P2 所占用,而 P2 在未释放打印机之前,又提出请求使用正被 P1 占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

二、死锁产生的原因

1) 系统资源的竞争

通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

2) 进程推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2 分别保持了资源 R1、R2,而进程 P1 申请资源 R2,进程 P2 申请资源 R1 时,两者都 会因为所需资源被占用而阻塞。
信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程 A 等待进程 B 发的消息,进程 B 又在等待进程 A 发的消息,可以看出进程 A 和 B 不是因为竞争同一资源,而是在等待对方的资源导致死锁。

3) 死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。* 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合 {Pl, P2, …, pn},其中 Pi 等待的资源被 P(i+1) 占有(i=0, 1, …, n-1),Pn 等待的资源被 P0 占有,如图 2-15 所示。观上看,循环等待条件似乎和死锁的定义一样,其实不然。按死锁定义构成等待环所 要求的条件更严,它要求 Pi 等待的资源必须由 P(i+1) 来满足,而循环等待条件则无此限制。 例如,系统中有两台输出设备,P0 占有一台,PK 占有另一台,且 K 不属于集合 {0, 1, …, n}。
    Pn 等待一台输出设备,它可以从 P0 获得,也可能从 PK 获得。因此,虽然 Pn、P0 和其他 一些进程形成了循环等待圈,但 PK 不在圈内,若 PK 释放了输出设备,则可打破循环等待, 如图 2-16 所示。因此循环等待只是死锁的必要条件。
    资源分配图含圈而系统又不一定有死锁的原因是同类资源数大于 1。但若系统中每类资 源都只有一个资源,则资源分配图含圈就变成了系统出现死锁的充分必要条件。
    产生死锁的一个例子
/** 
* 一个简单的死锁类 
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒 
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒 
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; 
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定; 
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 
*/  
public class DeadLock implements Runnable {  
    public int flag = 1;  
    //静态对象是类的所有对象共享的  
    private static Object o1 = new Object(), o2 = new Object();  
    @Override  
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (o1) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o2) {  
                    System.out.println("1");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (o2) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o1) {  
                    System.out.println("0");  
                }  
            }  
        }  
    }  
    public static void main(String[] args) {  
          
        DeadLock td1 = new DeadLock();  
        DeadLock td2 = new DeadLock();  
        td1.flag = 1;  
        td2.flag = 0;  
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。  
        //td2的run()可能在td1的run()之前运行  
        new Thread(td1).start();  
        new Thread(td2).start();  
    }  
}  

三、如何避免死锁


在有些情况下死锁是可以避免的。三种用于避免死锁的技术:

  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测
加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
  lock A 
  lock B
Thread 2:
   wait for A
   lock C (when A locked)
Thread 3:
   wait for A
   wait for B
   wait for C

如果一个线程(比如线程 3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。
例如,线程 2 和线程 3 只有在获取了锁 A 之后才能尝试获取锁 C(译者注:获取锁 A 是获取锁 C 的必要条件)。因为线程 1 已经拥有了锁 A,所以线程 2 和 3 需要一直等到锁 A 被释放。然后在它们尝试对 B 或 C 加锁之前,必须成功地对 A 加了锁。
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁 (译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行 (译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。
以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程 2 比线程 1 早 200 毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程 1 尝试获取锁 A 并且处于等待状态。当线程 2 结束时,线程 1 也可以顺利的获得这两个锁(除非线程 2 或者其它线程在线程 1 成功获得两个锁之前又获得其中的一些锁)。
需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。
此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为 0 到 500 毫秒之间,这种现象可能不会发生,但是如果是 10 个或 20 个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)
这种机制存在一个问题,在 Java 中不能对 synchronized 同步块设置超时时间。你需要创建一个自定义锁,或使用 Java5 中 java.util.concurrent 包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的 Java 并发系列会涵盖自定义锁的内容。

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程 A 请求锁 7,但是锁 7 这个时候被线程 B 持有,这时线程 A 就可以检查一下线程 B 是否已经请求了线程 A 当前所持有的锁。如果线程 B 确实有这样的请求,那么就是发生了死锁(线程 A 拥有锁 1,请求锁 7;线程 B 拥有锁 7,请求锁 1)。
当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程 A 等待线程 B,线程 B 等待线程 C,线程 C 等待线程 D,线程 D 又在等待线程 A。线程 A 为了检测死锁,它需要递进地检测所有被 B 请求的锁。从线程 B 所请求的锁开始,线程 A 找到了线程 C,然后又找到了线程 D,发现线程 D 请求的锁被线程 A 自己持有着。这是它就知道发生了死锁。
下面是一幅关于四个线程(A,B,C 和 D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。
![[IMG-20250113203607866.png]]
那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

根文件系统

什么是根文件系统?

根文件系统首先是一种文件系统,该文件系统不仅具有普通文件系统的存储数据文件的功能,但是相对于普通的文件系统,它的特殊之处在于,它是内核启动时所挂载(mount)的第一个文件系统,内核代码的映像文件保存在根文件系统中,系统引导启动程序会在根文件系统挂载之后从中把一些初始化脚本(如 rcS,inittab)和服务加载到内存中去运行。我们要明白文件系统和内核是完全独立的两个部分。在嵌入式中移植的内核下载到开发板上,是没有办法真正的启动 [Linux 操作系统]的,会出现无法加载文件系统的错误。

根文件系统为什么这么重要?

根文件系统之所以在前面加一个”根 “,说明它是加载其它文件系统的” 根“,那么如果没有这个根,其它的文件系统也就没有办法进行加载的。
根文件系统包含系统启动时所必须的目录和关键性的文件,以及使其他文件系统得以挂载(mount)所必要的文件。例如:
①init 进程的应用程序必须运行在根文件系统上
②根文件系统提供了根目录 “/”
③linux 挂载分区时所依赖的信息存放于根文件系统 / etc/fstab 这个文件中
④shell 命令程序必须运行在根文件系统上,譬如 ls、cd 等命令
Linux 启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。成功之后可以自动或手动挂载其他的文件系统。

怎么制作根文件系统?

制作根文件系统的方法很多,最常用的是使用 BusyBox 来构建。它能迅速方便地建立一套相对完整、功能丰富的文件系统,其中包括大量常用的应用程序,它集成压缩了 Linux 的许多工具和命令。
在制作根文件系统之前,思考一个问题,根文件系统怎么起来的?明白了这个问题才能明白我们需要做什么!
内核在启动完成之后会去挂接根文件系统,然后会去找 init 程序启动,然后就。。。完了!!!没错内核只会去启动 init 进程,剩余的工作就交给 init 进程来完成,内核会尝试去启动几个常见的 init,一旦有一个启动成功就不再返回!如下图所示,一般而言启动 / sbin/init 就 OK 啦(不考虑 initramfs 文件系统)!

![[IMG-20250113205203491.png]]

init 进程执行流程

这里讲述的是 busybox 生成的 init 进程执行流程,执行流程图如下:
![[IMG-20250113205203594.png]]init 进程执行分两部分:
1)初始化,简要来说就是使用 /dev/console 设备作为控制台,如果它不能打开或不存在,则使用 /dev/null 设备。因此根文件目录下要有 /dev/console 和 /dev/null,所以要创建者两个设备文件。
2)执行 inittab 文件中命令,简要来说就是根据 /etc/inittab 文件的配置,来执行相应的动作。
inittab 文件的每一行格式如下:

    <id>:<runlevels>:<action>:<process>
    (1)<id>:表示该子进程使用的控制台,如果该字段省略,则使用与 init 进程一样的控制台。
    (2)<runlevel>:该进程的运行级别,Busybox 的 init 程序不支持运行级别这个概念,因此该字段无意义,如果要支持 runlevel 意义,则建议使用 System V Init 程序。
    (3)<action>:表示 init 如何控制该进程,是一个枚举量,可能的取值及相应的意义如下表:

动作        

说明

sysinit        

系统启动后最先执行,只执行一次,init 进程等待它结束才继续执行其他动作。

wait        

系统执行完 sysinit 进程后执行,只执行一次,init 进程等待它结束后才继续执行其他动作 。

once        

系统执行完 wait 进程后执行,init 进程不会等待它结束。

respawn           

启动玩 once 进程后执行,init 进程监测其子进程,当这个进程退出时,重新启动它. 。

askfist        

启动完 respawn 进程后执行,与 respawn 类似,不过 init 进程先输出 "Please press Enter to activate this console"。等用户输入回车后才启动子进程。

shutdown             

当系统关机或重启时执行。

restart        

Busybox 中配置了 CONFIG_FEATURE_USE_INITTAB,并且 init 进程接收到 SIGUP 信号时执行,现重新读取, 解析 / etc/inittab,在重新执行 restart 程序。

ctrlaltdel        

当按下 ctrl+alt+del 组合键时执行。

    (4)<process>:要执行的程序,可以为可执行程序也可以是脚本,如果 < process > 字段前面有 “-” 字符,代表这个程序是可交互的,例如:/bin/sh 程序。

一个例子:

::sysinit:/etc/init.d/rcS #执行rcS脚本,完成初始化工作
::respawn:-/bin/sh #等待sysinit执行完毕后直接进入串口命令行
# Stuff to do when restarting the init process
::restart:/sbin/init
#debug for tty1@20151116
#::respawn:/sbin/getty 38400 tty1
# Stuff to do before rebooting
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a

所以,我们要做的事很明确,主要为:
1)使用 busybox 制作 init 与常用 shell 命令。
2)创建 /dev/console 和 /dev/null。
3)编写 inittab 文件,又因为 inittab 文件会使用到 /etc/init. D/rcS 脚本,所以还要编辑它,同时又因为 / etc/init. D/rcS 脚本会使用 /etc/fstab 配置,所以还要编写它。
4)因为我们要运行 gcc(arm 架构则是 arm-linux-gcc)编译器所编译的程序,所以我们还要将 gcc 的 lib 库拷贝到 /rootfs/lib 下。

根文件系统制作

(1)使用 BusyBox 制作 init 进程和常用 shell 命令。
下载 busybox 源码,比如这里使用的 busybox-1.24.1. Tar. Bz 2 ,执行命令 tar -xjvf busybox-1.24.1. Tar. Bz 2 解压源码。
由于是在 X 86 平台的 PC 机端给 X 86 平台目标机端制作最小根文件系统,因此不需要配置架构和编译器,但如果是给 arm 平台制作,需要修改 Makefile 文件,将 ARCH=arm、CROSS_COMPILE=arm-linux- 。
配置 BusyBox,进入 busybox-1.24.1 目录,执行 make menuconfig 进行图形化配置,一般配置以下选项:
1)静态编译 BusyBox:
Busybox Settings
Build Options
->Build BusyBox as a static binary (no shared libs)
2)安装位置:
Busybox Settings
Installation Options (“make install” behavior)
/data 1/caodongwang/work/sys/myrootfs/rootfs
3)命令补全:
Busybox Settings
Busybox Library Tuning
Command line editing (FEATURE_EDITING [=y])
->Tab completion
4)模块加载命令:
Linux Module Utilities
->Simplified modutils
默认是勾选的,这里需要取消掉,然后才会弹出 insmod 等命令,将其勾选上。
5)模块自动加载:
Linux System Utilities
->mdev
6)vi 命令选项勾上:
Busybox Settings
Busybox Library Tuning
Command line editing (FEATURE_EDITING [=y])
vi-style line editing commands
配置好之后退出保存,然后执行 make -j 16 命令编译,再执行 make install 即可安装,去指定的安装目录下可以找到安装的文件。
![[IMG-20250113205202888.png]]
可以看到 init 进程就在 sbin 目录中,它其实是指向 busybox 的链接!
![[IMG-20250113205203117.png]]
(2)创建 /dev/console 和 /dev/null
首先,我们要补齐一下目录,进入 rootfs 目录下执行:
Mkdir dev etc lib root proc mnt var tmp nfs sys opt
再执行:
Sudo mknod -m 666 dev/console c 5 1
Sudo mknod -m 666 dev/null c 1 3
(3)创建 etc/inittab 文件
执行 vi etc/inittab 命令
填充以下:

::sysinit:/etc/init.d/rcS 
::respawn:-/bin/sh 
::restart:/sbin/init
				
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a

再创建 inittab 文件需要使用的脚本 etc/init. D/rcS
执行
Mkdir etc/init. D
Vi etc/init. D/rcS
在里面填充以下

#!/bin/sh
mount -a #挂载在etc/fstab中配置的挂载项
ifconfig eth0 10.14.97.198 netmask 255.255.255.0 #配置IP
route add default gw 10.14.97.254 #添加路由
echo /sbin/mdev > /proc/sys/kernel/hotplug #热插拔
mdev –s    #在dev目录下生成内核支持的所有节点
sleep 1 #休眠1s等待路由准备就绪,下面挂接才能进行
mount -t nfs -o nolock 10.1.74.212:/data1/caodongwang/nfs /mnt #挂载网络文件系统

最后创建 etc/init.d/rcS 脚本中 mount -a 命令需要使用的 etc/fstab 文件,它会挂接这个文件上所指定的文件系统
执行 vi etc/fstab
填充以下

# device    mount-point    type options    dum fsck  order
proc  /proc proc defaults    0  0
tmpfs /tmp tmpfsdefaults    0  0
sysfs /sys sysfsdefaults    0  0
tmpfs /dev tmpfsdefaults    0  0

(4)拷贝编译器库文件到 rootfs/lib 中
注意:这里很关键!首先自己编写一个简单程序,使用 gcc 编译之后在编译服务器上执行,没有问题的话使用 ldd xxx 命令查看这个可执行程序,即可知道当前 gcc 编译的程序使用了哪些库,比如:
![[IMG-20250113205203208.png]]
可以看见使用了两个动态库,并且知道了对应的路径为 / lib/x 86_64-linux-gnu / 和 / lib 64/,因此我们根文件系统需要这两个文件路径,并在这两个文件路径下放置动态库!
在 rootfs 目录下执行(注意,以下库路径创建和库文件拷贝均由上图结果指示):
Mkdir lib/x 86_64-linux-gnu
Cd lib/x 86_64-linux-gnu
cp /lib/x 86_64-linux-gnu/. So . -d #注意这个 -d 不可省略,表示将链接符号还按符号复制
回到 rootfs 目录下,执行
Mkdir lib 64
Cp /lib 64/ld-linux-x 86-64. So. 2 lib 64 -d
库文件移植完毕!(注意,如果后续碰到应用程序无法在开发板上运行,均可按此步骤来操作)
(5)为 etc/init. D/rcS 、 etc/fstab、 etc/inittab 文件加上可执行权限
执行:
Chmod a+x etc/init. D/rcS etc/fstab etc/inittab
至此,最小根文件系统就做好了,但是我们还需要将这个根文件系统制作为一个文件,即映像文件,才能真正被内核挂载使用!这个内容,将在后续讲解!
根文件系统制作成映像文件才能被内核使用!
根文件系统制作成映像文件才能被内核使用!
根文件系统制作成映像文件才能被内核使用!
重要的事情强调三遍!
在这,再简要介绍一下 linux 系统常用目录。

根文件系统常用目录介绍

Linux 文件系统中一般有如下几个目录:
/bin 目录
该目录下存放所有用户都可以使用的、基本的命令,这些命令在挂接其它文件系统之前就可以使用,所以 / bin 目录必须和根文件系统在同一个分区中。
/bin 目录下常用的命令有:cat,chgrp,chmod,cp,ls,sh,kill,mount,umount,mkdir,mknod,test 等,我们在利用 Busybox 制作根文件系统时,在生成的 bin 目录下,可以看到一些可执行的文件,也就是可用的一些命令。
/sbin 目录
该目录下存放系统命令,即只有管理员能够使用的命令,系统命令还可以存放在 / usr/sbin,/usr/local/sbin 目录下,/sbin 目录中存放的是基本的系统命令,它们用于启动系统,修复系统等,与 / bin 目录相似,在挂接其他文件系统之前就可以使用 / sbin,所以 / sbin 目录必须和根文件系统在同一个分区中。
/sbin 目录下常用的命令有:shutdown,reboot,fdisk,fsck 等,本地用户自己安装的系统命令放在 / usr/local/sbin 目录下。
/dev 目录
该目录下存放的是设备文件,设备文件是 Linux 中特有的文件类型,在 Linux 系统下,以文件的方式访问各种设备,即通过读写某个设备文件操作某个具体硬件。比如通过”dev/ttySAC0” 文件可以操作串口 0,通过”/dev/mtdblock1” 可以访问 MTD 设备的第 2 个分区。
/etc 目录
该目录下存放着各种配置文件,对于 PC 上的 Linux 系统,/etc 目录下的文件和目录非常多,这些目录文件是可选的,它们依赖于系统中所拥有的应用程序,依赖于这些程序是否需要配置文件。在嵌入式系统中,这些内容可以大为精减。
/lib 目录
该目录下存放共享库和可加载 (驱动程序),共享库用于启动系统。运行根文件系统中的可执行程序,比如:/bin /sbin 目录下的程序。
/home 目录
用户目录,它是可选的,对于每个普通用户,在 / home 目录下都有一个以用户名命名的子目录,里面存放用户相关的配置文件。
/root 目录
根用户的目录,与此对应,普通用户的目录是 / home 下的某个子目录。
/usr 目录
/usr 目录的内容可以存在另一个分区中,在系统启动后再挂接到根文件系统中的 / usr 目录下。里面存放的是共享、只读的程序和数据,这表明 / usr 目录下的内容可以在多个主机间共享,这些主要也符合 FHS 标准的。/usr 中的文件应该是只读的,其他主机相关的,可变的文件应该保存在其他目录下,比如 / var。/usr 目录在嵌入式中可以精减。
/var 目录
与 / usr 目录相反,/var 目录中存放可变的数据,比如 spool 目录 (mail,news),log 文件,临时文件。
/proc 目录
这是一个空目录,常作为 proc 文件系统的挂接点,proc 文件系统是个虚拟的文件系统,它没有实际的存储设备,里面的目录,文件都是由内核临时生成的,用来表示系统的运行状态,也可以操作其中的文件控制系统。
/mnt 目录
用于临时挂载某个文件系统的挂接点,通常是空目录,也可以在里面创建一引起空的子目录,比如 / mnt/cdram /mnt/hda1 。用来临时挂载光盘、硬盘。
/tmp 目录
用于存放临时文件,通常是空目录,一些需要生成临时文件的程序用到的 / tmp 目录下,所以 / tmp 目录必须存在并可以访问。

操作系统分类

按照用户类型分类
  • 个人操作系统:主要面向个人用户,用于个人计算机和移动设备,如Windows、macOS、iOS、Android等.
  • 服务器操作系统:主要面向服务器,用于提供网络服务、数据库服务等,如Linux、Windows Server、Unix等.
  • 嵌入式操作系统:主要嵌入在各种设备中,用于控制设备的运行,如家电、汽车、工业设备等中的操作系统,如VxWorks、FreeRTOS等.
按照操作系统的架构分类
  • 单体系统:所有系统功能都在一个单一的执行环境中运行,如早期的DOS.
  • 分层系统:将操作系统分为多个层次,每一层提供不同的功能,如某些早期的分时系统.
  • 微内核系统:内核只提供最基本的服务,其他功能都作为用户态进程运行,如Minix、L4等.
  • 混合内核系统:结合了微内核和单体系统的特点,内核提供一些基本服务,其他服务可以在内核态或用户态运行,如Windows NT、Linux等.
按照操作系统的交互方式分类
  • 批处理系统:用户将作业提交给系统,系统按照一定的顺序进行处理,用户无法实时交互,如早期的大型机系统.
  • 分时系统:允许多个用户同时使用计算机资源,每个用户可以实时与系统交互,如UNIX系统.
  • 实时系统:能够在规定的时间内完成特定任务,对时间响应有严格要求,如工业控制系统、航空航天系统等中的操作系统.
按照操作系统的源代码开放程度分类
  • 开源操作系统:源代码是公开的,用户可以自由地查看、修改和分发,如Linux、FreeBSD等.
  • 闭源操作系统:源代码是不公开的,用户只能使用操作系统提供的功能,不能查看或修改源代码,如Windows、macOS等.
按照操作系统的硬件平台分类
  • 桌面操作系统:主要运行在个人计算机上,如Windows、macOS.
  • 移动操作系统:主要运行在移动设备上,如智能手机和平板电脑,如iOS、Android.
  • 服务器操作系统:主要运行在服务器硬件上,如Linux Server、Windows Server.
  • 嵌入式操作系统:运行在各种嵌入式设备上,如家电、汽车等中的操作系统.
    这些分类方式并不是互斥的,一个操作系统可能同时属于多个分类.
按照用户类型分类
  • 个人操作系统
    • Windows:微软公司开发的个人计算机操作系统,广泛应用于桌面计算机和笔记本电脑.
    • macOS:苹果公司开发的个人计算机操作系统,仅用于苹果的Mac系列电脑.
    • iOS:苹果公司开发的移动操作系统,用于iPhone、iPad等苹果移动设备.
    • Android:谷歌公司开发的基于Linux内核的开源移动操作系统,广泛应用于各种智能手机和平板电脑.
  • 服务器操作系统
    • Linux:开源的类Unix操作系统,广泛应用于服务器、嵌入式系统等领域,有多种发行版,如Ubuntu Server、Red Hat Enterprise Linux等.
    • Windows Server:微软公司开发的服务器操作系统,广泛应用于企业服务器、数据中心等.
    • Unix:一种多用户、多任务的操作系统,有多种版本和衍生系统,如AIX、HP-UX等.
  • 嵌入式操作系统
    • FreeRTOS:一个小型的、可裁剪的、抢占式实时操作系统内核,广泛应用于嵌入式设备.
    • VxWorks:由Wind River公司开发的实时操作系统,广泛应用于嵌入式系统,如航空航天、工业控制等领域.
    • Android Things:谷歌公司开发的基于Android的嵌入式操作系统,用于物联网设备.
按照操作系统的架构分类
  • 微内核系统
    • Minix:由安德鲁·塔恩内鲍姆开发的微内核操作系统,主要用于教学和研究.
    • L4:一种微内核操作系统,具有高性能和高安全性,用于嵌入式系统和服务器等领域.
  • 混合内核系统
    • Windows NT:微软公司开发的混合内核操作系统,是Windows操作系统系列的基础.
    • Linux:虽然主要是单体系统,但也具有混合内核的特点,内核提供一些基本服务,其他服务可以在内核态或用户态运行.
按照操作系统的交互方式分类
  • 分时系统
    • UNIX:一种多用户、多任务的分时操作系统,广泛应用于服务器、嵌入式系统等领域.
    • Linux:基于UNIX的开源操作系统,具有分时系统的特性,广泛应用于各种计算机平台.
  • 实时系统
    • QNX:由QNX软件系统公司开发的实时操作系统,广泛应用于嵌入式系统,如汽车电子、工业控制等领域.
    • RTLinux:一种实时操作系统,基于Linux内核,用于需要实时响应的应用场景.
按照操作系统的源代码开放程度分类
  • 开源操作系统
    • Linux:开源的类Unix操作系统,广泛应用于服务器、桌面计算机、嵌入式系统等领域.
    • FreeBSD:基于BSD Unix的开源操作系统,广泛应用于服务器和嵌入式设备.
    • Ubuntu:基于Debian的开源Linux发行版,广泛应用于桌面计算机和服务器.
  • 闭源操作系统
    • Windows:微软公司开发的闭源操作系统,广泛应用于个人计算机和服务器.
    • macOS:苹果公司开发的闭源操作系统,仅用于苹果的Mac系列电脑.
    • iOS:苹果公司开发的闭源移动操作系统,用于iPhone、iPad等苹果移动设备.

国产操作系统

桌面及服务器操作系统
  • 银河麒麟:由麒麟软件有限公司开发,已广泛应用于党政、金融、交通、通信、能源、教育等重点行业.
  • 中标麒麟:由中标软件有限公司开发,是国家规划布局内的重点软件产品,以操作系统技术为核心,打造安全创新等差异化特性.
  • 统信UOS:由统信软件技术有限公司开发,专注于桌面用户和企业市场,提供友好的用户界面和广泛的硬件兼容性,得到了众多大型企业和政府机构的青睐.
  • 深度Linux(Deepin):由武汉深之度科技有限公司开发,致力于为全球用户提供美观、易用、安全、免费的使用环境的Linux发行版.
  • 普华Linux(I-soft OS):由普华基础软件股份有限公司开发,包括桌面版、服务器版、国产CPU系列版本等产品.
  • 红旗Linux(redflag linux):由中科红旗(北京)信息科技有限公司开发,是中国较大、较成熟的Linux发行版之一.
  • 华为欧拉 openEuler:由华为科技股份有限公司开发,面向服务器的Linux发行版,开源模式得到了迅速发展.
  • 中科方德:由中科方德软件有限公司开发,提供国产服务器操作系统,支持国产CPU平台,重点服务于电子政务、国防军工、金融、教育、医疗等领域.
  • 优麒麟:由麒麟软件有限公司主导开发,致力于设计出“简单轻松、友好易用”的桌面环境,是全球开源项目.
  • 中兴新支点:由广东新支点技术服务有限公司开发,基于Linux稳定内核,分为嵌入式操作系统、服务器操作系统、桌面操作系统等.
移动操作系统
  • 华为鸿蒙 HarmonyOS:由华为科技股份有限公司开发,是面向万物互联的全场景分布式操作系统,支持手机、平板、智能穿戴、智慧屏等多种终端设备运行.
嵌入式操作系统
  • RT-Thread RTOS:由上海睿赛德电子科技有限公司开发,是一个小型的、可裁剪的、抢占式实时操作系统内核,广泛应用于嵌入式设备.

军用操作系统

通常需要具备高安全性、高稳定性和实时性等特点,以满足军事应用的特殊需求。以下是一些常见的军用操作系统:

国产军用操作系统
  • 银河麒麟:由麒麟软件有限公司开发,广泛应用于军事指挥、情报分析、武器控制等领域。
  • 天脉操作系统(ACoreOS):由中航工业计算机所开发,是一款国产机载嵌入式操作系统,具有实时性、稳定性和安全性,已应用于多种航空装备。
  • 红旗Linux:适用于军事领域的国产操作系统,具有高度的可靠性、安全性和稳定性。
国际军用操作系统
  • VxWorks:由美国风河公司开发,是一款广泛应用于军事和航空航天领域的实时操作系统,具有高度的可靠性和实时性。
  • INTEGRITY RTOS:由Green Hills Software公司开发,具有快速性和实时性,使用硬件内存保护来隔离和保护嵌入式应用程序。
军用操作系统的应用场景
  • 指挥控制系统:用于军事指挥和控制,如美国陆军的“指挥控制系统”(C2)中使用Windows平台。
  • 情报分析系统:用于情报收集和分析,需要强大的数据处理能力。
  • 武器控制系统:用于控制武器系统,如无人机和导弹控制系统。
  • 后勤管理系统:用于军事后勤管理,涉及物资调配和人员管理。
    军用操作系统的选择和应用需要综合考虑其安全性、稳定性和特定应用场景的需求,以确保军事行动的顺利进行和信息安全的保障.

操作系统开发

是一个复杂且系统的过程,涉及到多个方面的技术与管理。以下是一些关键的步骤和考虑因素:

开发步骤
  1. 需求分析:明确操作系统的应用场景和目标用户群体,分析其功能需求、性能需求、安全性需求等,制定详细的需求规格说明书.
  2. 系统设计:根据需求分析的结果,设计操作系统的整体架构,包括内核设计、系统调用接口设计、进程管理设计、内存管理设计、文件系统设计、设备驱动设计等,制定系统设计方案.
  3. 编码实现:按照系统设计方案,使用合适的编程语言(如C、C++、汇编语言等)进行编码实现,编写内核代码、驱动程序代码、系统库代码等,确保代码的可读性、可维护性和可移植性.
  4. 系统集成:将各个模块的代码进行集成,形成完整的操作系统镜像,包括内核、驱动程序、系统库、系统工具等,确保各个模块之间的协调工作.
  5. 测试验证:对操作系统进行全面的测试,包括功能测试、性能测试、稳定性测试、安全性测试等,发现并修复缺陷,确保操作系统的可靠性和稳定性.
  6. 优化改进:根据测试结果和用户反馈,对操作系统进行优化改进,提升其性能、兼容性和用户体验,发布新的版本.
技术考虑
  • 内核设计:内核是操作系统的核心,需要设计高效的调度算法、内存管理机制、文件系统结构等,确保系统的稳定性和性能。
  • 安全性:操作系统的安全性至关重要,需要实现用户权限控制、数据加密、访问控制、安全审计等功能,防止恶意攻击和数据泄露。
  • 兼容性:操作系统需要支持多种硬件平台和外设,与现有的应用程序和中间件兼容,提供良好的API接口和开发工具,方便开发者进行应用开发。
  • 性能优化:通过优化内核代码、改进算法、利用硬件特性等方式,提升操作系统的响应速度、吞吐量和资源利用率,满足高性能计算和实时处理的需求。
  • 用户体验:提供友好的用户界面和交互方式,简化用户操作,提升系统的易用性和舒适度,满足不同用户的需求。
国产操作系统开发进展

近年来,国产操作系统取得了显著的进展:

  • 技术突破:国产操作系统如银河麒麟、统信UOS、深度Linux等,基于Linux内核等开源软件进行二次开发,逐步掌握了核心技术,提升了系统的性能和安全性。
  • 生态建设:国产操作系统厂商积极与硬件厂商、软件开发商合作,构建了较为完善的生态系统,推动了国产软硬件的协同发展。
  • 市场应用:国产操作系统在政府、金融、交通、能源等关键领域得到了广泛应用,市场份额逐步提升,如openEuler在服务器操作系统市场的份额达到了36.8%。
  • 政策支持:国家出台了一系列政策支持国产操作系统的发展,推动了国产化替代进程。

操作系统教材

是一个系统的过程,涉及到理论知识、实践操作和开发技能等多个方面。以下是一些建议,帮助你更好地学习操作系统:==

理论学习
  1. 阅读经典教材
    • 《操作系统概念》(Operating Systems: Internals and Design Principles):作者是William Stallings,这本书详细介绍了操作系统的各个组成部分和工作原理,适合初学者入门.
    • 《现代操作系统》(Modern Operating Systems):作者是Andrew S. Tanenbaum,这本书深入探讨了现代操作系统的概念、设计和实现,适合有一定基础的读者.
    • 《操作系统:设计与实现》(Operating Systems: Design and Implementation):同样由Andrew S. Tanenbaum撰写,这本书以Minix操作系统为例,讲解了操作系统的实现细节,适合对操作系统实现感兴趣的读者.
  2. 学习相关课程
    • 在大学里选修操作系统课程,跟随老师的指导系统地学习.
    • 参加在线课程,如Coursera、edX、MOOC中国等平台上的操作系统课程,可以灵活安排学习时间.
  3. 理解核心概念
    • 掌握进程、线程、调度、内存管理、文件系统、设备驱动等核心概念.
    • 理解并发、同步、死锁、虚拟内存、I/O操作等关键问题.
实践操作
  1. 安装和使用操作系统
    • 在虚拟机中安装不同的操作系统,如Windows、Linux、macOS等,熟悉它们的安装过程、用户界面和基本操作.
    • 尝试在不同的操作系统上运行和配置应用程序,了解它们的兼容性和性能差异.
  2. 学习命令行操作
    • 掌握Linux命令行的基本操作,如文件操作(ls、cp、mv等)、文本处理(grep、sed、awk等)、系统监控(top、ps、df等)等.
    • 了解Windows命令提示符(CMD)和PowerShell的基本命令.
  3. 编写和运行程序
    • 在操作系统上编写简单的程序,如使用C语言编写一个简单的进程或线程程序,理解进程和线程的创建、运行和终止过程.
    • 学习如何在操作系统上编译和运行程序,了解编译器、链接器和调试器的基本使用.
开发技能
  1. 学习编程语言
    • 掌握C语言,因为很多操作系统内核和驱动程序都是用C语言编写的.
    • 学习汇编语言,了解计算机底层的工作原理,有助于理解操作系统的某些实现细节.
  2. 阅读开源代码
    • 阅读Linux内核源代码,了解其内核架构、调度算法、内存管理等实现细节.
    • 参与开源项目,如Linux内核的开发和维护,积累实际的开发经验.
  3. 编写简单的操作系统
    • 可以从编写一个简单的操作系统开始,如实现一个简单的进程调度、内存管理或文件系统等,逐步深入理解操作系统的实现原理.
持续学习和交流
  1. 关注技术动态
    • 关注操作系统领域的最新技术动态和发展趋势,如虚拟化技术、容器技术、微内核等.
    • 阅读相关的技术博客、论文和书籍,了解新的研究成果和应用案例.
  2. 加入技术社区
    • 加入操作系统相关的技术社区和论坛,如GitHub、Stack Overflow、Reddit等,与其他开发者交流经验和问题.
    • 参加技术会议和研讨会,与行业专家和同行交流,拓展视野和人脉.
      通过理论学习、实践操作和开发技能的结合,逐步深入理解操作系统的原理和实现,提升自己的技术水平和解决问题的能力.

使用最广泛的几类操作系统

桌面操作系统
  • Windows:由微软公司开发,是全球使用最广泛的桌面操作系统之一。根据StatCounter的数据,截至2024年8月,Windows的市场份额为71.46%。Windows操作系统具有丰富的应用程序、简洁的界面和良好的兼容性,深受用户喜爱。其主要版本有Windows 10和Windows 11,其中Windows 10的市场份额在2024年12月达到了62.73%。
  • macOS:由苹果公司为其Mac系列计算机开发的操作系统,以其优雅的界面、出色的稳定性和安全性而受到许多专业人士和创意人士的喜爱。截至2024年8月,macOS的市场份额为15.48%。
移动操作系统
  • Android:由谷歌公司开发的基于Linux内核的开源移动操作系统,广泛应用于智能手机和平板电脑。根据Counterpoint的数据,Android在2024年第一季度的市场份额为77%。其免费开源的特性使其被许多手机厂商采用,并衍生出了众多基于Android的厂商定制系统。
  • iOS:由苹果公司为其iPhone、iPad等移动设备开发的操作系统,具有出色的界面设计、强大的应用生态和良好的用户体验。截至2024年11月,iOS的市场份额为15.9%。在平板市场,iOS的市场份额更高,2024年9月达到了55.49%。
服务器操作系统
  • Linux:是一种开源的操作系统,广泛应用于服务器领域。根据Fortune Business Insights的数据,2023年全球服务器操作系统市场中,Linux以62.7%的市场份额占据绝对的领先地位。Linux系统具有高度的可定制性、稳定性、安全性和自由度。
  • Windows Server:由微软公司开发的服务器操作系统,是Windows操作系统系列的一部分,广泛应用于企业服务器、数据中心等。

Linux系统开发资源

书籍
  • 《Linux命令行与shell脚本编程大全》:这本书全面介绍了Linux命令行和shell脚本编程的知识,适合初学者入门。
  • 《Linux系统编程:从零基础到精通》:详细介绍了Linux系统编程的各个方面,包括进程、线程、文件I/O、网络编程等。
  • 《Linux内核开发》:由Robert Love撰写,深入讲解了Linux内核的设计和实现,适合有一定基础的开发者。
  • 《The Linux Programming Interface》:由Michael Kerrisk编写,详细介绍了Linux编程所需的系统调用和库函数。
  • 《深入理解Linux进程与内存》:讲解了进程创建和调度原理、虚拟内存底层机制等内容,帮助理解Linux的底层原理。
在线课程
  • Linux Foundation开源软件学园的Linux入门课程(LFS101):这是一个免费的在线课程,帮助学员掌握Linux的基本操作和开发知识。
  • 哔哩哔哩上的Linux应用开发完整学习路线:提供了从基础到高级的Linux应用开发教程,包括Linux操作命令、shell编程、编译/调试工具、系统环境编程等内容。
  • 电子科技大学的Linux操作系统编程课程:介绍了Linux下应用程序的设计思想、开发特点、开发手段,重点讲述POSIX API的使用方法和编程技巧。
学习平台
  • Linux Journey:这是一个专注于Linux教育的在线学习平台,提供了丰富的学习资源和交互式学习体验。
  • GitHub上的开源知识库:如《二哥的Java进阶之路》中包含了Linux开发的相关知识。
实践项目
  • Linux From Scratch(LFS):这是一个DIY Linux项目,通过从源代码构建自己的Linux系统,可以深入理解Linux的各个组成部分。
    通过这些资源的学习和实践,可以逐步掌握Linux系统开发的知识和技能,提升自己的开发能力.

FreeRTOS知识点

核心概念
  • 任务(Task):FreeRTOS中的任务是并发执行的最小单元,每个任务都有自己的执行栈和优先级。
  • 任务调度(Task Scheduling):FreeRTOS根据任务的优先级进行调度,优先级高的任务会优先执行。
  • 上下文切换(Context Switching):任务切换时,FreeRTOS会保存当前任务的上下文(寄存器等状态),并恢复下一个任务的上下文。
同步与通信
  • 互斥量(Mutex):用于保护共享资源,确保一次只有一个任务访问。
  • 信号量(Semaphore):用于任务间的同步和信号传递,可以是二进制信号量或计数信号量。
  • 事件组(Event Group):用于任务间的同步,可以表示多个事件状态。
  • 消息队列(Queue):用于任务间的消息传递,可以存储多个消息。
时间管理
  • 软件定时器(Software Timer):FreeRTOS提供了软件定时器功能,可以创建定时器来执行周期性任务或在指定时间后执行任务。
  • Tick中断:FreeRTOS的调度器依赖于Tick中断,每个Tick周期称为一个系统节拍。
内存管理
  • 堆内存管理:FreeRTOS提供了多种堆内存管理方案,可以配置使用不同的内存分配策略。
  • 栈内存管理:每个任务都有独立的栈,栈大小需要根据任务需求合理配置。
网络功能
  • FreeRTOS Plus TCP:提供TCP/IP协议栈支持,可以实现网络通信功能。
  • 网络接口:需要根据硬件平台实现网络接口函数,以便FreeRTOS能够与网络硬件交互。
配置与移植
  • 配置文件:FreeRTOS的配置通过FreeRTOSConfig.h文件进行,可以设置任务调度策略、系统节拍周期、内存管理方案等。
  • 移植:FreeRTOS需要根据目标硬件平台进行移植,包括配置时钟中断、实现硬件抽象层函数等。
开发与调试
  • 任务创建与删除:使用xTaskCreate函数创建任务,使用vTaskDelete函数删除任务。
  • 任务状态监控:可以使用FreeRTOS提供的API查询任务的状态、堆栈使用情况等。
  • 调试辅助:FreeRTOS提供了丰富的调试宏和工具,帮助开发者进行系统调试。

Linux 系统开发

是一个复杂的过程,需要掌握多方面的知识。以下是Linux系统开发需要掌握的一些关键知识:

编程语言
  • C语言:Linux系统开发的核心语言,大部分Linux内核和驱动程序都是用C语言编写的。需要熟练掌握C语言的基本语法、数据结构、指针、内存管理等.
  • C++语言:虽然Linux内核主要使用C语言,但在一些用户空间的应用程序和框架中,C++语言也被广泛使用。掌握C++可以更好地进行一些复杂应用程序的开发.
  • 脚本语言:如Shell脚本、Python等,用于编写自动化脚本、配置管理脚本等,提高开发效率.
Linux内核
  • 内核架构:了解Linux内核的整体架构,包括内核模块、进程管理、内存管理、文件系统、设备驱动等各个子系统.
  • 内核机制:掌握进程调度、中断处理、信号处理、虚拟内存管理、I/O子系统等内核机制的原理和实现.
  • 内核编程:学习如何编写内核模块、驱动程序等,包括内核模块的加载、卸载、设备驱动的注册、中断处理函数的编写等.
  • 内核调试:掌握内核调试的方法和工具,如使用GDB进行内核调试,分析内核崩溃的原因等.
用户空间编程
  • 系统调用:了解Linux系统调用的原理和使用方法,掌握常用的系统调用,如进程控制、文件操作、网络通信等.
  • 进程与线程:掌握进程和线程的创建、同步、通信等知识,包括多线程编程、进程间通信(IPC)机制等.
  • 文件系统操作:熟悉文件系统的操作,包括文件的打开、读写、定位、同步等,以及文件系统相关的高级特性,如文件锁、文件映射等.
  • 网络编程:掌握TCP/IP协议栈的基本原理,熟悉套接字编程(Socket编程),能够开发基于网络的应用程序,包括客户端和服务器端的开发.
  • 并发编程:了解并发编程的概念和模型,掌握锁、信号量、条件变量等同步机制,以及线程池等并发编程技术.
构建与部署
  • 构建工具:掌握常用的构建工具,如Make、CMake等,能够编写Makefile或CMakeLists.txt文件,实现项目的自动化构建.
  • 包管理:了解Linux系统中的包管理工具,如APT(Debian系)、YUM(Red Hat系)等,能够安装、卸载、管理软件包.
  • 版本控制:熟练使用版本控制系统,如Git,进行代码的版本管理、分支管理、合并管理等.
  • 部署与维护:了解Linux系统的部署流程,包括系统的安装、配置、升级等,以及系统的维护和故障排查.
性能优化
  • 性能分析:掌握性能分析的方法和工具,如使用perf工具进行性能分析,找出程序的性能瓶颈.
  • 性能优化技巧:了解常见的性能优化技巧,如内存优化、CPU优化、I/O优化、网络优化等,能够对程序进行性能调优.
安全性
  • 权限管理:了解Linux系统的权限管理机制,包括文件权限、用户权限、组权限等,能够合理配置权限,保障系统的安全性.
  • 安全特性:熟悉Linux系统提供的安全特性,如SELinux、AppArmor等,了解它们的原理和配置方法.
  • 安全编程:掌握安全编程的原则和技巧,避免常见的安全漏洞,如缓冲区溢出、SQL注入、跨站脚本攻击等.
硬件相关知识
  • 硬件架构:了解常见的硬件架构和组成,包括CPU、内存、存储设备、输入输出设备等,以及它们的工作原理和接口.
  • 设备驱动开发:掌握设备驱动的开发方法,能够根据硬件设备的规范和接口编写相应的驱动程序,实现硬件设备与操作系统的交互.
软件工程
  • 设计模式:了解常用的软件设计模式,如单例模式、工厂模式、观察者模式等,能够根据项目需求选择合适的设计模式,提高代码的可读性和可维护性.
  • 测试方法:掌握软件测试的方法和技巧,包括单元测试、集成测试、系统测试等,能够编写测试用例,进行软件测试.
  • 文档编写:能够编写清晰、规范的文档,包括需求文档、设计文档、用户手册、API文档等,便于项目的沟通和维护.
    这些知识是Linux系统开发的基础,掌握它们可以帮助你更好地进行Linux系统开发工作,提高开发效率和质量.

Linux内核

是Linux操作系统的核心部分,负责管理系统的硬件资源、提供系统调用接口、实现进程管理、内存管理、文件系统、设备驱动等功能。以下是关于Linux内核的一些重要知识点:

内核架构
  • 模块化设计:Linux内核采用模块化设计,内核功能可以分为多个模块,如进程管理模块、内存管理模块、文件系统模块、设备驱动模块等。模块化设计提高了内核的可扩展性和可维护性.
  • 分层结构:内核可以分为内核空间和用户空间。内核空间运行在较高的权限级别,直接管理硬件资源和提供系统调用接口;用户空间运行在较低的权限级别,运行应用程序,通过系统调用与内核空间交互.
  • 微内核与单体系统:Linux内核属于单体系统,大部分功能都运行在内核空间。与微内核相比,单体系统的设计使得内核运行效率较高,但内核的复杂度也较高.
进程管理
  • 进程与线程:Linux内核将线程视为轻量级进程(LWP),进程和线程在内核中都以进程的形式管理。进程是资源分配的基本单位,线程是CPU调度的基本单位.
  • 进程调度:Linux内核采用完全公平调度算法(CFS),根据进程的优先级和运行时间进行调度,确保进程公平地使用CPU资源.
  • 进程创建与终止:内核提供了fork()、clone()等系统调用用于创建进程,提供了exit()、_exit()等系统调用用于终止进程.
  • 进程间通信(IPC):内核支持多种进程间通信机制,包括管道、信号、消息队列、信号量、共享内存等,用于进程之间的数据交换和同步.
内存管理
  • 虚拟内存:Linux内核实现了虚拟内存机制,将物理内存映射到虚拟地址空间,提供给进程使用。虚拟内存管理包括页表管理、页置换算法、内存映射等.
  • 内存分配:内核提供了kmalloc()、vmalloc()等内存分配函数,用于内核空间的内存分配;用户空间的内存分配通过系统调用mmap()实现.
  • 内存保护:内核通过页表和内存管理单元(MMU)实现内存保护,确保进程之间互不干扰,防止进程访问其他进程的内存空间.
  • 内存回收:内核实现了内存回收机制,包括内核线程kswapd和直接内存回收路径,用于回收不再使用的内存页面.
文件系统
  • 文件系统类型:Linux支持多种文件系统类型,如ext4、XFS、Btrfs、F2FS等,每种文件系统都有其特点和适用场景.
  • 文件系统结构:文件系统由文件和目录组成,文件系统管理文件的存储、访问和组织。内核提供了统一的文件系统接口,供各种文件系统实现.
  • 文件操作:内核实现了文件的打开、读写、定位、同步等操作,提供了系统调用open()、read()、write()、lseek()、fsync()等.
  • 文件系统挂载:文件系统需要挂载到根文件系统上,才能被用户空间访问。内核提供了mount()系统调用用于文件系统的挂载.
设备驱动
  • 驱动程序:驱动程序是内核与硬件设备之间的接口,负责控制硬件设备的工作。驱动程序可以是内核模块,也可以是内核内置的.
  • 设备模型:Linux内核定义了设备模型,包括设备类、设备号、设备节点等,用于管理和识别硬件设备.
  • 驱动程序接口:内核提供了驱动程序接口,驱动程序需要实现相应的接口函数,如probe()、remove()、open()、read()、write()等.
  • 中断处理:驱动程序需要处理硬件设备的中断信号,内核提供了中断处理机制,包括中断请求(IRQ)和中断处理函数.
系统调用
  • 系统调用接口:系统调用是用户空间与内核空间之间的接口,用户空间的程序通过系统调用请求内核提供的服务,如进程控制、文件操作、网络通信等.
  • 系统调用实现:系统调用的实现依赖于中断和异常机制,当用户空间程序执行系统调用时,会触发中断,切换到内核空间执行相应的内核函数.
  • 系统调用表:内核维护了一个系统调用表,用于映射系统调用号与内核函数之间的关系.
安全性
  • 权限管理:内核实现了权限管理机制,包括文件权限、进程权限、用户权限等,确保只有具有相应权限的进程才能访问资源.
  • 安全模块:Linux内核支持安全模块,如SELinux、AppArmor等,提供了更细粒度的安全控制和策略管理.
  • 内核安全特性:内核包含了一些安全特性,如地址空间布局随机化(ASLR)、栈保护、内核模块签名等,用于提高系统的安全性.
内核开发与调试
  • 内核模块开发:内核模块是内核功能的扩展,可以动态加载和卸载。开发内核模块需要了解内核模块的编写、编译、加载和卸载过程.
  • 内核调试:内核调试是内核开发的重要环节,可以使用GDB、kgdb、kdump等工具进行内核调试,分析内核崩溃的原因和内核程序的运行情况.
  • 内核性能分析:可以使用perf、ftrace等工具进行内核性能分析,找出内核的性能瓶颈和优化方向.
    Linux内核是一个庞大而复杂的系统,掌握其核心知识点有助于深入理解Linux操作系统的原理和机制,为Linux系统开发和优化提供坚实的基础.

Linux内核的主要模块

核心模块
  • 进程管理模块:负责进程的创建、调度、终止和同步等,确保多个进程能够有效地共享CPU资源。
  • 内存管理模块:管理物理内存和虚拟内存,包括内存分配、回收、页表管理、页置换算法等。
  • 文件系统模块:提供虚拟文件系统(VFS)层,支持多种文件系统类型,如ext4、XFS等,负责文件的存储、访问和管理。
  • 设备驱动模块:包括字符设备、块设备和网络设备驱动,用于控制和管理各种硬件设备。
  • 网络子系统模块:实现TCP/IP协议栈,提供网络通信功能,包括网络接口、传输层协议、网络配置等。
  • 进程间通信(IPC)模块:支持多种进程间通信机制,如管道、信号、消息队列、信号量、共享内存等。
其他模块
  • 系统调用接口模块:提供用户空间程序与内核空间交互的接口,允许用户程序请求内核执行特权操作。
  • 中断和异常处理模块:处理硬件中断和软件异常,确保系统能够及时响应外部事件和内部错误。
  • 定时器和时钟管理模块:管理内核定时器和硬件时钟,用于实现时间管理和周期性任务。
  • 电源管理模块:负责系统的电源管理,包括节能模式的切换、设备的电源控制等。
  • 安全模块:如SELinux、AppArmor等,提供系统的安全策略管理和访问控制。
    这些模块共同构成了Linux内核的核心结构,使得Linux操作系统能够高效地管理资源、支持多任务运行、与硬件设备交互以及提供网络通信等功能.

FreeRTOS开发

核心概念
  • 任务(Task):FreeRTOS中的任务是并发执行的基本单位,每个任务都有自己的执行栈和优先级。任务管理是FreeRTOS的核心功能之一。
  • 队列(Queue):用于任务间的消息传递,可以存储多个消息。FreeRTOS提供了多种队列操作API,如xQueueSendxQueueReceive等。
  • 信号量(Semaphore):用于任务间的同步和互斥。FreeRTOS支持二进制信号量和计数信号量。
  • 互斥量(Mutex):用于保护共享资源,确保一次只有一个任务访问。
  • 事件组(Event Group):用于任务间的同步,可以表示多个事件状态。
  • 软件定时器(Software Timer):用于实现周期性任务或延迟任务。
开发步骤
  1. 环境搭建:安装适合的编译器和开发工具,如GCC、Keil等。确保开发环境支持FreeRTOS。
  2. 内核配置:根据项目需求配置FreeRTOS内核参数,如任务栈大小、定时器配置等。这些配置通常在FreeRTOSConfig.h文件中进行。
  3. 任务创建:使用FreeRTOS提供的API创建任务,如xTaskCreate。为每个任务分配适当的优先级和栈空间。
  4. 资源管理:使用信号量、互斥量等同步机制管理共享资源,确保任务间的正确同步。
  5. 中断处理:FreeRTOS支持中断处理,可以在中断服务程序(ISR)中使用FreeRTOS API进行任务间的通信。
  6. 调试与测试:使用调试工具和FreeRTOS提供的调试宏进行系统调试,确保任务调度和资源管理的正确性。
特性与优势
  • 可预测性:FreeRTOS提供可预测的实时性能,适合需要严格时间控制的应用。
  • 可配置性:内核高度可配置,可以根据具体需求裁剪功能,减小系统占用。
  • 跨平台:支持多种硬件平台和微控制器。
  • 开源:FreeRTOS是开源软件,拥有活跃的社区支持。
学习资源
  • 官方文档:FreeRTOS官方网站提供了详细的文档和教程,适合初学者和进阶开发者。
  • 社区支持:加入FreeRTOS社区,可以获取专家的答疑和经验分享。
  • 书籍:如《Mastering the FreeRTOS Real Time Kernel - a Hands On Tutorial Guide》等,提供了深入的学习和实践指导。
    通过掌握FreeRTOS的核心概念和开发步骤,可以有效地进行嵌入式系统的实时开发,提高系统的稳定性和实时性.

FreeRTOS 支持的硬件平台

涵盖了多种处理器架构和开发板。以下是一些主要的支持平台:

处理器架构
  • ARM Cortex-M系列:包括Cortex-M0、Cortex-M3、Cortex-M4F等,广泛应用于工业控制、汽车电子、物联网等领域。
  • RISC-V:作为一种新兴的开源指令集架构,RISC-V因其灵活性和可扩展性而受到关注,FreeRTOS也提供了对RISC-V的支持。
  • x86:支持32位平面内存模型的IA32架构。
  • 其他架构:如AVR、PIC、DSP等。
开发板和微控制器
  • ESP32:集成了Wi-Fi和蓝牙功能的微控制器,广泛应用于物联网领域。
  • STM32:由STMicroelectronics生产的高性能、低功耗的嵌入式处理器,广泛应用于各种嵌入式系统开发。
  • Renesas:支持RZ/A1、RX系列等多种处理器。
  • Silicon Labs:包括EFM32 Gecko系列。
  • Texas Instruments:支持MSP430、MSP432等。
  • Xilinx:支持Zynq、Zynq UltraScale+ MPSoC等。
其他支持
  • 模拟器和仿真器:FreeRTOS也支持在模拟器和仿真器上运行,如QEMU。
    FreeRTOS的广泛支持使得它能够在多种嵌入式系统中灵活应用,满足不同开发需求.

计算机专业

是一个涵盖广泛知识领域的专业,课程设置丰富多样,旨在培养学生具备扎实的计算机科学理论基础和较强的实践能力,以下是一些常见的计算机专业课程:

基础课程

  • 高等数学:为计算机专业提供必要的数学工具和思维方法。包括微积分、线性代数、概率论与数理统计等内容。微积分用于描述和分析变化规律,线性代数在计算机图形学、机器学习等领域有广泛应用,概率论与数理统计则为数据处理、人工智能等提供理论支持。
  • 大学物理:帮助学生理解自然界的物理现象和规律,培养科学思维和实验能力。其中的电磁学部分与计算机硬件中的电路原理密切相关,力学、光学等内容也为学生提供了一种科学的分析和解决问题的方法。
  • 英语:计算机领域的文献资料、软件文档等很多都是英文的,良好的英语能力有助于学生获取前沿知识和技术。课程重点培养学生的专业英语阅读、写作和翻译能力,使学生能够熟练阅读和理解计算机专业文献。
  • 计算机导论:作为计算机专业的入门课程,向学生介绍计算机科学的基本概念、发展历程、主要研究领域和应用前景。帮助学生建立对计算机专业的整体认识,明确学习目标和方向。

专业基础课程

  • 计算机组成原理:深入讲解计算机硬件系统的组成和工作原理。包括中央处理器(CPU)的结构与功能、存储器的层次结构与管理、输入输出设备的接口技术、总线的类型与控制等。使学生了解计算机硬件是如何协同工作的,为后续的硬件相关课程和软件优化等打下基础。
  • 操作系统原理:操作系统是计算机系统的核心软件,本课程讲解操作系统的功能、结构和实现原理。包括进程管理(进程的概念、状态转换、调度算法)、内存管理(分区管理、分页管理、分段管理、虚拟内存技术)、设备管理(中断处理、DMA传输、设备驱动程序)、文件管理(文件系统结构、目录管理、文件存储与检索)等内容。让学生掌握操作系统对计算机资源进行管理和调度的方法。
  • 程序设计语言基础:通常选择一种或几种主流的编程语言进行教学,如C语言、Java语言或Python语言等。课程内容包括基本语法、数据类型、运算符、控制结构、函数、数组、指针、结构体、类与对象(面向对象语言)等。通过大量的编程实践,培养学生的编程思维和代码编写能力。
  • 数据结构与算法:数据结构是计算机存储、组织数据的方式,算法是对特定问题求解步骤的描述。课程内容包括线性结构(数组、链表、栈、队列)、树形结构(二叉树、二叉搜索树、平衡二叉树、堆)、图形结构(无向图、有向图、加权图)的定义、特点、存储方法和基本操作;以及排序算法、查找算法、递归算法、分治算法、动态规划算法等常用算法的原理和实现。培养学生分析问题和解决问题的能力,提高程序设计的效率和质量。
  • 计算机网络原理:介绍计算机网络的基本概念、体系结构、协议模型(OSI七层模型和TCP/IP四层模型)、网络设备(交换机、路由器、集线器、网桥等)的工作原理与功能、网络协议(HTTP、FTP、TCP、UDP、IP等)的实现原理与应用、网络应用开发基础(套接字编程、Web开发技术等)。使学生了解计算机网络的组成和通信原理,掌握网络编程的方法。
  • 数据库原理:讲解数据库的基本概念、关系模型的理论基础、SQL语言的使用方法、数据库设计的方法与步骤(需求分析、概念结构设计、逻辑结构设计、物理结构设计)、数据库应用开发基础(数据库编程接口、数据库开发工具的使用、数据库应用系统的开发流程与方法)。让学生掌握数据库的设计、管理和应用开发技术。

专业核心课程

  • 软件工程:软件工程是一门研究如何高效、高质量地开发和维护软件的学科。课程内容包括软件生命周期的各个阶段(需求分析、设计、编码、测试、维护)的方法和工具、软件开发模型(瀑布模型、迭代模型、敏捷开发模型等)、软件项目管理(项目计划、团队协作、风险管理、质量保证等)、软件工程的最新技术和方法(如面向服务的架构、微服务架构、DevOps等)。培养学生系统地、规范地开发软件的能力,提高软件项目的成功率。
  • 算法设计与分析:在数据结构与算法课程的基础上,进一步深入讲解算法设计的高级方法和技巧,如贪心算法、回溯算法、分支限界算法、概率算法等;以及算法的分析方法,包括时间复杂度分析、空间复杂度分析、最坏情况分析、平均情况分析等。使学生能够针对复杂问题设计出高效、优化的算法,并对算法的性能进行准确评估。
  • 计算机体系结构:深入探讨计算机体系结构的设计原理和优化方法。包括处理器架构(指令集架构、流水线技术、多核处理器架构等)、存储体系结构(缓存技术、虚拟存储技术、存储层次结构优化等)、输入输出体系结构(高速缓存、中断处理、DMA传输优化等)、并行处理技术(多线程、多进程、分布式计算等)。让学生了解如何通过体系结构的设计和优化,提高计算机系统的性能和效率。
  • 编译原理:编译原理是研究如何将高级语言程序翻译成机器语言程序的理论和技术。课程内容包括词法分析(正则表达式、有限自动机等)、语法分析(上下文无关文法、LL(1)分析法、LR分析法等)、语义分析(类型检查、中间代码生成等)、代码优化(局部优化、全局优化、循环优化等)、目标代码生成等。使学生掌握编译器的设计和实现方法,了解程序语言的内部处理机制。
  • 人工智能导论:介绍人工智能的基本概念、发展历程、主要研究领域和应用前景。包括搜索策略(盲目搜索、启发式搜索等)、知识表示与推理(逻辑表示、语义网络、推理机等)、机器学习(监督学习、无监督学习、强化学习等基本概念和算法)、自然语言处理(语言模型、文本分类、机器翻译等基础内容)、计算机视觉(图像识别、目标检测等初步知识)等内容。为学生打开人工智能领域的大门,激发学生对人工智能研究的兴趣。
  • 嵌入式系统原理:嵌入式系统是计算机技术与具体应用领域紧密结合的产物。课程内容包括嵌入式处理器(如ARM处理器)的架构与指令系统、嵌入式操作系统(如μC/OS、Linux嵌入式版等)的原理与应用、嵌入式系统的硬件设计(包括微控制器、传感器、执行器等的接口技术)、嵌入式软件开发方法(C语言编程、实时编程技术等)、嵌入式系统的应用案例分析等。培养学生设计和开发嵌入式系统的能力,满足物联网、智能硬件等领域对嵌入式人才的需求。

专业选修课程

  • 大数据技术与应用:随着数据量的爆发式增长,大数据技术成为热门领域。课程内容包括大数据的基本概念、特点(4V:体量、速度、多样性、价值密度)、大数据处理架构(Hadoop生态系统,包括HDFS、MapReduce、HBase、Hive等组件的原理与应用)、大数据分析技术(数据挖掘算法、机器学习在大数据中的应用等)、大数据可视化方法等。让学生掌握大数据的存储、处理和分析技术,能够应对大数据环境下的实际问题。
  • 云计算技术:云计算改变了传统的计算模式和资源管理方式。课程内容包括云计算的基本概念、服务模型(IaaS、PaaS、SaaS)和部署模型(公有云、私有云、混合云),虚拟化技术(服务器虚拟化、存储虚拟化、网络虚拟化)的原理与实现,云平台的构建与管理(如OpenStack、CloudStack等开源云平台的安装、配置与使用),云计算的安全与隐私保护等。使学生了解云计算的技术原理和应用模式,具备搭建和管理云平台的能力。
  • 物联网工程:物联网是将物体通过信息传感设备与互联网相连,实现智能化识别、定位、跟踪、监控和管理的网络。课程内容包括物联网的体系结构(感知层、网络层、应用层),感知层技术(传感器原理与应用、RFID技术、二维码技术等),网络层技术(无线通信技术如ZigBee、Wi-Fi、NB-IoT等,以及物联网网关的设计与实现),应用层技术(物联网应用开发框架、中间件技术、物联网应用案例分析如智能家居、智能交通等)。培养学生设计和实现物联网系统的能力,满足物联网产业的发展需求。
  • 移动应用开发:随着智能手机和平板电脑的普及,移动应用开发成为计算机专业的重要方向。课程内容包括移动操作系统(如Android、iOS)的架构与开发环境搭建,移动应用的界面设计(布局设计、控件使用、事件处理等),移动应用的后台开发(数据存储、网络通信、多线程编程等),移动应用的测试与发布等。让学生掌握移动应用开发的完整流程,能够开发出实用的移动应用软件。
  • 机器学习与深度学习:机器学习和深度学习
    是人工智能领域的核心技术。课程内容包括机器学习的基本算法(如线性回归、逻辑回归、决策树、支持向量机等),深度学习的基本模型(如神经网络、卷积神经网络、循环神经网络等),深度学习框架(如TensorFlow、PyTorch等)的使用方法,机器学习与深度学习在图像识别、自然语言处理、语音识别等领域的应用案例分析等。培养学生运用机器学习和深度学习技术解决实际问题的能力,推动人工智能技术的发展。
  • 区块链技术:区块链作为一种分布式账本技术,具有去中心化、不可篡改等特点,在金融、供应链管理等领域有广阔的应用前景。课程内容包括区块链的基本原理(如分布式账本、共识机制、加密技术等),区块链的架构与实现(如比特币区块链、以太坊区块链的工作原理),智能合约的开发与应用,区块链技术在不同领域的应用案例分析等。让学生了解区块链技术的核心思想和应用模式,具备一定的区块链开发和应用能力。

计算机基础

是一个非常宽泛且重要的领域,涵盖了计算机硬件、软件、网络等诸多方面的基本知识和技能,以下是详细介绍:

计算机硬件基础

  • 计算机组成原理:计算机由中央处理器(CPU)、存储器、输入设备、输出设备和总线等基本部件组成。CPU是计算机的核心部件,负责执行指令,进行数据处理和运算;存储器用于存储程序和数据,包括内存和外存,内存如RAM(随机存取存储器)用于临时存储,外存如硬盘、光盘等用于长期存储;输入设备如键盘、鼠标等用于向计算机输入信息;输出设备如显示器、打印机等用于将计算机处理的结果输出显示。
  • 计算机体系结构:包括冯·诺依曼体系结构和哈佛体系结构等。冯·诺依曼体系结构是现代计算机的主流体系结构,其特点是程序指令和数据统一存储在内存中,CPU按顺序读取指令并执行。哈佛体系结构则将程序存储器和数据存储器分开,适用于一些嵌入式系统和数字信号处理器等。

计算机软件基础

  • 操作系统:操作系统是计算机系统的核心软件,负责管理和调度计算机的硬件资源,为用户提供一个良好的操作界面和运行环境。常见的操作系统有Windows、Linux、macOS等。Windows操作系统具有用户界面友好、软件资源丰富等特点,广泛应用于个人电脑和办公领域;Linux操作系统开源免费,具有高度的稳定性和灵活性,常用于服务器和嵌入式系统;macOS是苹果公司开发的操作系统,主要应用于苹果电脑,具有独特的设计风格和良好的用户体验。
  • 编程语言:编程语言是人与计算机之间进行沟通的桥梁,用于编写计算机程序。常见的编程语言有C、C++、Java、Python等。C语言是一种通用的、过程式的编程语言,具有高效、灵活、可移植性强等特点,广泛应用于系统软件、嵌入式系统等领域;C++是在C语言的基础上发展起来的,支持面向对象编程,可用于开发大型复杂的应用程序;Java语言具有跨平台性、面向对象、多线程等特点,常用于企业级应用开发、Android应用开发等;Python语言语法简洁、易学易用,拥有丰富的库资源,适用于数据分析、人工智能、Web开发等多个领域。
  • 软件工程:软件工程是一门研究如何高效、高质量地开发和维护软件的学科。它涵盖了软件需求分析、设计、编码、测试、维护等各个阶段的方法和工具。需求分析是确定软件要做什么,明确用户需求和功能需求;设计阶段包括概要设计和详细设计,确定软件的架构和模块划分,以及各个模块的具体实现细节;编码阶段是将设计转化为具体的程序代码;测试阶段通过各种测试方法发现软件中的缺陷和错误,确保软件的质量;维护阶段对软件进行修改、完善和升级,以适应用户需求的变化和技术的发展。

计算机网络基础

  • 网络体系结构:计算机网络遵循分层的体系结构,常见的有OSI七层模型和TCP/IP四层模型。OSI模型将网络分为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,每一层负责不同的功能,如物理层负责传输原始的比特流,网络层负责路由和转发数据包等;TCP/IP模型将网络分为链路层、网络层、传输层和应用层,是互联网的基础协议架构,其中网络层的IP协议负责数据包的传输和路由,传输层的TCP协议提供可靠的端到端通信。
  • 网络协议:网络协议是计算机网络中通信双方必须共同遵守的规则和约定。常见的网络协议有HTTP(超文本传输协议),用于浏览器与Web服务器之间的通信,实现网页的请求和传输;FTP(文件传输协议),用于在计算机之间传输文件;TCP(传输控制协议)和UDP(用户数据报协议),TCP提供可靠的、面向连接的传输服务,UDP提供不可靠的、无连接的传输服务,适用于对实时性要求较高的应用,如视频通话等;IP(互联网协议)用于在网络层实现数据包的寻址和路由。
  • 网络设备:网络设备是构建计算机网络的硬件基础,包括交换机、路由器、集线器、网桥等。交换机工作在数据链路层,用于连接多个网络设备,实现数据帧的转发和交换,提高网络的传输效率;路由器工作在网络层,用于连接不同的网络,根据数据包的目的地址进行路由选择和转发,实现不同网络之间的通信;集线器是一种简单的网络设备,工作在物理层,用于将多个网络设备连接在一起,但不具备数据转发和交换的功能;网桥工作在数据链路层,用于连接两个相同或相似的局域网,实现数据帧的过滤和转发。

数据库基础

  • 数据库管理系统(DBMS):数据库管理系统是位于用户和操作系统之间的一层数据管理软件,用于建立、使用和维护数据库。常见的数据库管理系统有MySQL、Oracle、SQL Server等。MySQL是一种开源的数据库管理系统,具有性能高、成本低、易于使用等特点,广泛应用于Web应用开发和中小企业;Oracle是一种大型的商业数据库管理系统,功能强大,适用于大型企业级应用;SQL Server是微软公司开发的数据库管理系统,与Windows操作系统和.NET框架集成紧密,常用于企业级应用开发和数据分析。
  • 数据库模型:常见的数据库模型有关系模型、层次模型和网状模型。关系模型是最常用的数据库模型,它将数据以表格的形式组织,每个表格称为一个关系,表格中的每一行称为一个元组,每一列称为一个属性。关系模型具有概念简单、易于理解和操作的特点,适用于大多数应用领域;层次模型将数据组织成树状结构,每个节点表示一个实体,节点之间的连线表示实体之间的关系,适用于表示具有明显层次关系的数据;网状模型将数据组织成网状结构,实体之间的关系更加灵活多样,但模型复杂,操作难度较大。
  • SQL语言:SQL(Structured Query Language,结构化查询语言)是用于管理和操作关系数据库的标准语言。它包括数据定义语言(DDL)、数据操纵语言(DML)和数据控制语言(DCL)。DDL用于定义和修改数据库的结构,如创建表、修改表结构、删除表等;DML用于对数据库中的数据进行查询、插入、更新和删除操作;DCL用于控制用户对数据库的访问权限,如授权、撤销权限等。例如,使用SQL语句“SELECT * FROM students WHERE age > 18”可以查询年龄大于18岁的学生信息。

计算机信息安全基础

  • 信息安全威胁:计算机信息系统面临着多种安全威胁,如病毒、木马、黑客攻击、网络钓鱼、数据泄露等。病毒是一种具有自我复制能力的恶意程序,能够感染计算机系统中的文件和程序,破坏系统功能和数据;木马是一种远程控制程序,黑客通过木马可以窃取用户的敏感信息,如账号密码、银行账户等;黑客攻击包括拒绝服务攻击(DoS)、分布式拒绝服务攻击(DDoS)、口令破解攻击等,目的是使计算机系统无法正常运行或获取系统的控制权;网络钓鱼是一种通过伪装成合法网站或邮件等方式,诱骗用户输入敏感信息的攻击手段;数据泄露是指用户的个人信息、企业商业机密等数据被非法获取和公开,给用户和企业带来巨大的损失。
  • 信息安全技术:为了保障计算机信息安全,采用了多种安全技术。防火墙是一种位于内部网络和外部网络之间的安全防护设备,能够对进出网络的数据包进行过滤和监控,阻止非法访问和攻击;加密技术通过对数据进行加密处理,将明文数据转换为密文数据,只有持有正确密钥的用户才能解密还原数据,从而保护数据的机密性;数字签名技术用于验证数据的完整性和发送者的身份,防止数据被篡改和伪造;入侵检测系统(IDS)和入侵防御系统(IPS)能够实时监测网络和系统的异常行为,及时发现和阻止入侵攻击;身份认证技术用于验证用户的身份,常见的身份认证方式有用户名和密码认证、指纹认证、面部识别认证、智能卡认证等。

计算机算法与数据结构基础

  • 数据结构:数据结构是计算机存储、组织数据的方式,包括线性结构、树形结构、图形结构等。线性结构中的数据元素之间是一对一的关系,如数组、链表、栈、队列等。数组是一种顺序存储结构,可以快速地通过索引访问元素,但插入和删除操作效率较低;链表是一种动态存储结构,通过指针将数据元素连接起来,插入和删除操作效率较高,但访问元素需要从头开始遍历;栈是一种后进先出(LIFO)的线性表,常用于函数调用、表达式求值等场景;队列是一种先进先出(FIFO)的线性表,常用于任务调度、消息传递等场景。树形结构中的数据元素之间是一对多的关系,如二叉树、二叉搜索树、平衡二叉树、堆等。二叉树是一种特殊的树,每个节点最多有两个子节点,具有许多重要的性质和应用;二叉搜索树是一种特殊的二叉树,左子树上所有节点的值小于根节点的
    值,右子树上所有节点的值大于根节点的值,可用于快速查找、插入和删除操作;平衡二叉树是一种特殊的二叉搜索树,其任意节点的左右子树的高度差不超过1,能够保证查找、插入和删除操作的时间复杂度为O(logn);堆是一种特殊的完全二叉树,分为最大堆和最小堆,最大堆中任意节点的值都大于或等于其子节点的值,最小堆中任意节点的值都小于或等于其子节点的值,常用于实现优先队列。图形结构中的数据元素之间是多对多的关系,如无向图、有向图、加权图等。无向图中的边没有方向,表示两个顶点之间的连接关系;有向图中的边有方向,表示从一个顶点指向另一个顶点的关系;加权图中的边带有权重,可用于表示路径的长度、成本等信息,常用于解决最短路径、最小生成树等图论问题。
  • 算法:算法是对特定问题求解步骤的一种描述,是计算机程序的灵魂。常见的算法有排序算法、查找算法、递归算法、分治算法、动态规划算法等。排序算法用于将一组数据按照一定的顺序排列,如冒泡排序、选择排序、插入排序、快速排序、归并排序等。冒泡排序通过相邻元素的比较和交换,将最大或最小的元素“冒泡”到序列的末尾;选择排序每次从未排序的元素中选择最小或最大的元素,将其放到已排序序列的末尾;插入排序将一个元素插入到已排序的序列中,使其仍然有序;快速排序通过选择一个基准元素,将序列分为两部分,一部分元素小于基准元素,另一部分元素大于基准元素,然后递归地对两部分进行排序;归并排序将序列分为多个子序列,对每个子序列进行排序,然后将有序的子序列合并为一个有序的序列。查找算法用于在一组数据中查找某个特定元素,如顺序查找、二分查找等。顺序查找从序列的第一个元素开始,依次与目标元素进行比较,直到找到目标元素或遍历完整个序列;二分查找适用于有序序列,通过将目标元素与中间元素进行比较,不断缩小查找范围,直到找到目标元素或查找范围为空。递归算法是一种将问题分解为相同问题的子问题进行求解的方法,具有简洁、易理解的特点,但可能会导致重复计算和栈溢出等问题。分治算法将一个大问题分解为若干个规模较小的子问题,递归地求解子问题,然后将子问题的解合并为原问题的解,常用于解决排序、查找、矩阵运算等问题。动态规划算法是一种将问题分解为相互重叠的子问题,通过存储子问题的解来避免重复计算的方法,适用于具有最优子结构和重叠子问题特点的问题,如背包问题、最长公共子序列问题等。

微机原理

是计算机专业中一门重要的专业基础课程,主要研究微型计算机(微机)的硬件结构、工作原理以及软件与硬件之间的接口技术。以下是微机原理课程的详细介绍:

课程目标

  • 使学生掌握微型计算机的基本组成、工作原理和体系结构,理解各部件之间的协同工作方式。
  • 学会使用汇编语言进行程序设计,掌握汇编语言程序的开发流程和调试方法。
  • 理解微机的输入输出接口原理,能够进行简单的接口编程和硬件控制。
  • 培养学生分析和解决微机相关问题的能力,为后续的嵌入式系统、计算机组成原理等课程的学习打下坚实基础。

课程内容

微型计算机概述
  • 微机的发展历程:介绍从早期的微型计算机到现代高性能微处理器的发展过程,让学生了解微机技术的演进趋势。
  • 微机的分类与应用:讲解不同类型微机的特点和应用场景,如台式机、笔记本电脑、嵌入式微机等,使学生对微机的广泛应用有初步认识。
微处理器原理
  • 微处理器的基本结构:详细讲解微处理器的内部结构,包括运算器、控制器、寄存器组等组成部分的功能和相互关系。例如,运算器负责执行算术和逻辑运算,控制器负责协调各部件的工作,寄存器组用于暂存数据和指令等信息。
  • 指令系统:深入分析微处理器的指令系统,包括指令的格式、寻址方式、指令类型等。以常见的x86架构微处理器为例,讲解数据传送指令、算术运算指令、逻辑运算指令、控制转移指令等各类指令的功能和使用方法。例如,MOV指令用于数据传送,ADD指令用于加法运算等。
  • 指令执行过程:阐述微处理器如何执行一条指令,从取指令、译码到执行的各个阶段。通过具体的指令实例,让学生理解指令执行的详细步骤和时序关系。
存储器原理
  • 存储器的分类与特点:介绍不同类型存储器的分类,如随机存取存储器(RAM)、只读存储器(ROM)、高速缓存(Cache)等,讲解它们的存储原理、读写速度、容量大小、成本高低等特点。例如,RAM是易失性存储器,用于临时存储数据和程序;ROM是非易失性存储器,用于存储固件等重要信息。
  • 存储器的组织与管理:讲解存储器的组织方式,如字节编址、半字编址、字编址等,以及存储器的管理技术,如分段管理、分页管理等。阐述如何通过地址映射实现存储器的扩展和管理,提高存储器的利用率。
  • 存储器与微处理器的连接:说明存储器如何与微处理器进行连接,包括地址线、数据线、控制线的连接方式和作用。讲解存储器的读写时序,以及如何通过控制信号实现存储器的读写操作。
输入输出接口原理
  • 输入输出接口的概念:介绍输入输出接口的定义和作用,它是微机与外部设备进行信息交换的桥梁。讲解接口的分类,如并行接口、串行接口、通用接口等,以及它们的特点和应用场景。
  • 接口芯片与接口技术:详细讲解常用的接口芯片,如可编程并行接口芯片8255、可编程串行接口芯片8251等,阐述它们的内部结构、功能和编程方法。例如,8255芯片有三个端口,通过控制字的设置可以实现不同的工作方式,用于连接键盘、显示器等并行设备。
  • 中断技术:中断是微机实现多任务处理和实时处理的重要技术。讲解中断的基本概念,如中断源、中断请求、中断响应、中断优先级等,阐述中断的处理过程,包括中断请求的提出、中断响应的条件、中断服务程序的执行和中断返回等。通过具体的中断实例,让学生理解中断技术在微机系统中的应用。
  • DMA技术:直接存储器访问(DMA)技术可以实现高速外设与存储器之间的直接数据传输,提高数据传输效率。讲解DMA的基本原理,包括DMA控制器的组成、DMA传输过程和DMA与微处理器的协同工作方式。例如,DMA控制器可以接管总线控制权,直接控制数据在存储器和外设之间的传输,无需微处理器的干预。
汇编语言程序设计
  • 汇编语言基础:介绍汇编语言的基本概念,包括指令格式、伪指令、宏指令等。讲解汇编语言程序的结构,如数据段、代码段、堆栈段的定义和作用。例如,数据段用于定义常量、变量等数据,代码段用于存放程序的指令序列。
  • 汇编语言程序设计方法:通过具体的编程实例,讲解顺序结构、分支结构、循环结构等基本程序设计方法在汇编语言中的应用。例如,使用JMP指令实现无条件跳转,使用JZJNZ等条件跳转指令实现分支结构,使用LOOP指令实现循环结构。
  • 子程序设计:讲解子程序的概念和设计方法,包括子程序的调用和返回机制,参数传递方式等。通过子程序的使用,可以提高程序的模块化程度和可重用性。例如,编写一个求两个数最大值的子程序,可以在主程序中多次调用该子程序,实现不同的功能需求。
  • 宏定义与宏调用:介绍宏定义的概念和作用,通过宏定义可以简化程序的编写,提高程序的可读性和可维护性。讲解宏调用的过程和宏展开的原理,让学生掌握如何使用宏定义编写高效的汇编语言程序。

实验与实践环节

  • 微机组成原理实验:通过实验平台,让学生动手搭建和调试简单的微机系统,加深对微机硬件结构和工作原理的理解。例如,连接微处理器、存储器、输入输出接口等部件,观察和分析各部件之间的信号传输和协同工作过程。
  • 汇编语言程序设计实验:提供汇编语言开发环境,让学生编写和调试汇编语言程序,掌握汇编语言程序的开发流程和调试技巧。例如,编写一个实现两个数相加的汇编语言程序,通过调试工具观察程序的运行过程和寄存器的变化情况。
  • 接口技术实验:利用接口实验箱,让学生进行输入输出接口的编程实验,实现对各种外部设备的控制和数据传输。例如,通过编写汇编语言程序控制LED灯的闪烁,读取键盘输入的数据,将数据发送到显示器显示等,提高学生的接口编程能力和硬件控制能力。

应用案例与拓展

  • 嵌入式系统中的应用:介绍微机原理在嵌入式系统中的应用,如嵌入式微处理器的选择、嵌入式系统的硬件设计和软件开发等。通过实际的嵌入式项目案例,让学生了解微机原理在嵌入式领域的具体应用,激发学生对嵌入式系统学习的兴趣。
  • 微机与其他技术的融合:探讨微机原理与其他计算机技术的融合应用,如与网络技术结合实现网络通信,与人工智能技术结合实现智能控制等。通过案例分析,让学生了解微机技术在现代计算机系统中的重要地位和广泛应用前景,培养学生的创新思维和综合应用能力。

微机原理课程设计

简介:微机原理课程设计是计算机科学的核心课程之一,涉及微型计算机的基本结构和工作原理,以及软硬件交互。该设计旨在加深学生对微处理器内部机制的理解,提高汇编语言编程能力,并通过解决实际问题来应用所学知识。课程内容包含微处理器架构、指令系统、汇编语言编程、存储系统、输入/输出系统、总线系统、实模式与保护模式,以及具体的课程设计项目和实验调试。

1. 微处理器架构介绍与应用

微处理器作为现代计算机系统的“大脑”,在信息技术领域扮演着至关重要的角色。本章将从基本的微处理器架构讲起,深入探讨其组成、工作原理以及在不同应用场合中的使用。
微处理器的基本组成
微处理器主要由以下几个部分组成:算术逻辑单元(ALU)、控制单元(CU)、寄存器组和程序计数器(PC)等。算术逻辑单元负责执行所有的算术和逻辑操作;控制单元负责协调整个处理器的工作,它解释指令并控制数据流;寄存器组是处理器内部的快速存储单元,用于临时存放数据和指令;程序计数器则指向即将执行的下一条指令的地址。

微处理器的主要组成部分
  1. 算术逻辑单元(ALU)
    • 功能:ALU是微处理器的核心部件之一,负责执行所有的算术和逻辑运算。它能够进行加、减、乘、除等基本算术运算,以及与、或、非、异或等逻辑运算。
    • 工作原理:ALU接收来自寄存器组的操作数,根据控制单元发出的控制信号执行相应的运算,并将结果送回寄存器组。例如,执行加法运算时,ALU会将两个操作数相加,并将结果存储在指定的寄存器中。
  2. 控制单元(CU)
    • 功能:控制单元负责协调微处理器内部各部件的工作,确保指令的正确执行。它从存储器中取出指令,进行译码,生成相应的控制信号,控制ALU、寄存器组等部件完成特定的操作。
    • 工作原理:控制单元在每个时钟周期内从程序计数器(PC)指定的地址取出一条指令,然后对指令进行译码,生成控制信号,驱动其他部件完成指令规定的操作。例如,当指令是“将寄存器A的内容加到寄存器B中”时,控制单元会生成控制信号,使ALU从寄存器A和B中读取数据,执行加法运算,并将结果写回寄存器B。
  3. 寄存器组
    • 功能:寄存器组用于暂存数据和指令,提高数据访问速度。寄存器组包括多个通用寄存器和专用寄存器,如累加器、数据寄存器、地址寄存器等。
    • 工作原理:寄存器组在微处理器的指令执行过程中起到临时存储的作用。例如,当执行一条指令需要读取数据时,数据会先被加载到寄存器中,然后ALU从寄存器中读取数据进行运算。运算结果也会先存储在寄存器中,再根据需要写回存储器或其他寄存器。
  4. 程序计数器(PC)
    • 功能:程序计数器用于存储下一条要执行的指令的地址。它确保微处理器能够按顺序执行指令,是程序流程控制的关键部件。
    • 工作原理:在每个指令周期结束时,程序计数器会自动更新为下一条指令的地址。通常,程序计数器的值会递增,指向存储器中的下一条指令。当遇到跳转指令时,程序计数器的值会被更新为跳转目标地址,从而实现程序的分支和循环控制。例如,执行一条无条件跳转指令时,程序计数器会被设置为跳转目标地址,微处理器从该地址开始执行新的指令序列。
      ![[未处理器.png]]
      微处理器的工作原理
      微处理器的工作流程是一个重复执行指令周期的过程,它包括取指令(Fetch)、解码(Decode)、执行(Execute)和写回(Write-back)四个主要阶段。每个阶段都对应处理器内部的一个或多个电路模块,相互协作以完成计算任务。
      微处理器的应用领域
      微处理器广泛应用于各种电子设备中,包括但不限于个人电脑、智能手机、嵌入式系统、工业控制系统等。在这些应用中,微处理器负责处理数据、执行程序和管理外设等关键功能。随着技术的进步,微处理器的性能不断提升,应用范围也在持续扩大。
      通过本章的介绍,您将对微处理器有一个基础的了解,为深入学习后续章节打下坚实的基础。

2. 指令系统学习与编程实践

2.1 指令集架构的概念与发展
2.1.1 指令集架构的分类
指令集架构(Instruction Set Architecture,ISA)是微处理器硬件与软件之间的一个界面,它定义了处理器可以理解的所有机器指令及其格式。指令集架构可以分为两大类:复杂指令集计算机(Complex Instruction Set Computer,CISC)和精简指令集计算机(Reduced Instruction Set Computer,RISC)。
CISC架构,如x86架构,由Intel和AMD等公司开发,其特点是拥有大量复杂的指令,每条指令可以执行多项操作。例如,一条CISC指令可以实现数据的加载、运算以及存储等任务。CISC架构的设计理念是尽量减少代码的数量,通过提供功能强大的指令来简化编程工作。
RISC架构,如ARM和MIPS架构,特点是每条指令的执行时间固定,指令集相对简洁。RISC处理器更强调使用简单指令,通过软件优化来实现复杂操作。这种设计可以提高处理器的处理速度和效率,同时简化处理器设计,并有利于编译器优化。
2.1.2 指令集架构的演化路径
随着计算需求的不断增长和计算机技术的进步,指令集架构经历了从CISC到RISC的演化。最初的计算机使用的是简单的指令集,但随着硬件能力的提升,软件工程师们需要更强大的指令来处理更加复杂的任务。于是,CISC架构应运而生。
然而,随着集成电路技术的发展,硬件成本大幅下降,使得RISC架构成为可能。RISC架构通过简化指令集和提高执行效率来满足高性能计算的需求。在RISC架构下,由于指令执行的快速和高效,软件编译器可以在编译时完成更多的优化工作,从而获得更好的性能。
随着时间的推移,一些CISC架构的处理器开始引入RISC风格的设计元素,比如流水线技术和超标量架构,这使得传统CISC处理器的性能大幅度提升,接近了RISC处理器的效率。而RISC处理器为了更好地适应现代计算需求,也开始引入一些复杂指令来支持特定的应用场景。如今的指令集架构往往是CISC和RISC技术的融合产物。
2.2 指令系统的编程基础
2.2.1 基本指令的操作与应用
在编程实践中,理解和掌握基本指令的操作对于编写有效率的代码至关重要。基本指令包括数据传输指令、算术指令、逻辑指令和控制指令等。
数据传输指令主要用于在寄存器、内存、I/O端口之间传输数据。算术指令如加法、减法、乘法和除法用于处理数值运算。逻辑指令则包括位运算指令,如AND、OR、NOT和XOR等,它们用于执行布尔逻辑运算。控制指令则用于程序流程的控制,包括条件分支和循环指令等。
例如,在x86架构下,指令 MOV AX, BX 可以将寄存器BX的值传送到AX中。 ADD AX, BX 则将AX和BX中的值相加,结果存回AX。
在实际编程中,开发者需要根据具体的应用场景选择合适的指令,以便高效地完成任务。例如,在处理大规模数据时,可以使用向量指令来加速数据处理流程,而循环优化则可以通过减少分支预测失败来提高程序的运行速度。
2.2.2 指令系统中的寻址模式
寻址模式是指令系统中获取操作数的方式。在不同的指令集架构中,寻址模式的种类和用法可能有所不同,但其基本概念是相似的。常见的寻址模式包括立即寻址、直接寻址、寄存器寻址、寄存器间接寻址、基址寻址、变址寻址和相对寻址等。
立即寻址 :指令中包含操作数本身,例如 MOV AX, 5 将立即数5加载到AX寄存器。
直接寻址 :指令中包含操作数的内存地址,例如 MOV AX, [1234H] 将内存地址1234H处的数据传送到AX寄存器。
寄存器寻址 :操作数在寄存器中,例如 MOV AX, BX 将寄存器BX的值传送到AX。
寄存器间接寻址 :操作数的地址在寄存器中,例如 MOV AX, [BX] 将由寄存器BX指向的内存地址中的数据传送到AX。
基址寻址 :基址寄存器加上一个偏移量指定操作数的地址,例如 MOV AX, [BX+10H] 。
变址寻址 :基址寄存器加上一个索引寄存器的值作为操作数的地址,例如 MOV AX, [BX+SI] 。
相对寻址 :将程序计数器(PC)与一个偏移量相加来获得操作数的地址,常见于分支指令。
寻址模式的选择对于指令的性能有着直接影响。例如,立即寻址可以减少内存访问,加快执行速度,而直接寻址则可能需要额外的内存访问,速度较慢。在编程时,选择合适的寻址模式是优化程序性能的重要手段之一。
2.2.3 指令系统在编程中的实践
在具体的编程实践中,开发者需要根据指令系统的特性来编写程序。在使用汇编语言时,理解指令集架构是编写有效程序的基础。举一个简单的例子:
section .text
global _start
_start:
mov eax, 1 ; 系统调用号1表示exit
mov ebx, 0 ; 参数0表示退出状态码
int 0x80 ; 触发中断,执行系统调用
在这段汇编代码中,我们使用了 mov 指令进行数据传输, int 指令用于触发系统调用,实现程序退出。我们通过设置 eax 寄存器的值为1,指定了要执行的系统调用(此处为Linux下的 exit 系统调用),而 ebx 寄存器用于传递参数,这里我们传递的参数是0,表示程序正常退出。
编写汇编代码时,需要仔细考虑指令的选择和执行顺序,确保程序逻辑的正确性和执行效率。随着现代编译器的发展,虽然许多底层的指令集操作已经由编译器自动处理,但对指令集架构的理解依旧是优化程序性能和解决复杂问题的关键。
2.3 指令系统编程案例分析
在本节中,我们将通过一个实际的编程案例来深入分析如何在不同指令集架构下实现相同的功能,并讨论其差异和优化策略。这里,我们以在x86架构和ARM架构下实现相同的字符串复制功能为例。
x86架构下的实现
section .text
global _start
_start:
mov esi, str1 ; 将源字符串地址加载到ESI寄存器
mov edi, str2 ; 将目标字符串地址加载到EDI寄存器
cld ; 清除方向标志,保证字符串以正向增长
copy_loop:
lodsb ; 从ESI指向的地址加载一个字节到AL寄存器,并将ESI加1
mov [edi], al ; 将AL寄存器的内容存储到EDI指向的地址,并将EDI加1
test al, al ; 测试AL寄存器的值是否为0
jnz copy_loop ; 如果不是0,继续循环
; 此处省略退出程序的代码
str1 db ‘Hello, World!’, 0
str2 db 13 dup(0)
在这段x86汇编代码中,我们使用了 lodsb 和 stosb 指令来实现字符串的复制,这两个指令专门用于处理字符串操作。通过使用这些专用指令,我们可以避免复杂的循环和索引计算,从而提高程序的执行效率。
ARM架构下的实现
ARM架构使用不同的指令集,其汇编代码实现如下:
.global _start
_start:
ldr r0, =src ; 将源字符串地址加载到R0寄存器
ldr r1, =dest ; 将目标字符串地址加载到R1寄存器
copy_loop:
ldrb r2, [r0], #1 ; 从R0指向的地址加载一个字节到R2寄存器,并将R0加1
strb r2, [r1], #1 ; 将R2寄存器的内容存储到R1指向的地址,并将R1加1
cmp r2, #0 ; 比较R2寄存器的值是否为0
bne copy_loop ; 如果不是0,继续循环
; 此处省略退出程序的代码
src: .asciz “Hello, World!”
dest: .space 20
在ARM架构下,我们使用了 ldrb 和 strb 指令来实现类似的功能。这些指令的命名和用法虽然与x86架构有所不同,但基本原理是相同的。通过对比两个架构下的实现,可以看出,尽管汇编语言的语法和指令有所不同,但基本的编程逻辑和寻址模式在不同架构之间是有共通性的。
通过对本节案例的分析,我们可以发现,在编程实践中,充分理解并合理应用不同指令集架构的特性,对于编写高效和优化良好的代码至关重要。无论是x86还是ARM架构,熟悉其指令集的特点能够帮助开发者更好地控制程序性能,并在不同的硬件平台上发挥出最佳性能。
总结
指令系统是微处理器设计的基础,也是计算机体系结构的核心组成部分。理解指令集架构的概念及其演化路径对于计算机科学和工程专业的学生和从业者来说都是基础技能。本章内容从基础概念出发,逐步深入,不仅介绍了指令集架构的分类和演化,还通过具体编程案例,展示在不同架构下如何实现相同功能,并分析了指令系统在编程实践中的应用。在不断更新和发展的计算机技术中,熟练掌握指令集架构的知识是保持技术竞争力的基石。

3. 汇编语言编程技巧掌握

3.1 汇编语言编程基础
3.1.1 汇编语言的基本语法
汇编语言是微处理器架构最接近硬件的语言,它是微机原理课程中的一个重要环节。掌握汇编语言的基本语法是学习汇编语言编程的基础。汇编语言的指令可以简单分为数据处理指令、控制转移指令、输入输出指令等几类。以下面的汇编代码为例:
mov ax, 0x1234 ; 将立即数0x1234传送到AX寄存器
add ax, bx ; 将AX寄存器与BX寄存器的值相加,并存储到AX寄存器
在这段代码中, mov 是数据处理指令,用于传送数据到寄存器或内存。 add 则是执行加法操作。汇编指令对于操作数有一些格式要求。比如,寄存器名要以字母开头,不能包含特殊符号。
汇编语言指令通常都是小写的,因为这是业内约定的表示法。此外,使用汇编语言编写程序需要熟悉各种寄存器和内存寻址模式。寄存器是CPU内部的小型存储单元,用于存储操作过程中临时数据,如 AX 、 BX 、 CX 、 DX 等。
3.1.2 汇编语言的变量与数据结构
汇编语言中的变量概念与高级语言有所不同,它更接近于内存中的具体位置。变量的声明需要指定其类型,如字节(byte)、字(word)、双字(dword)等。变量可以定义在数据段(data segment)中。
section .data
var1 db 10 ; 定义一个字节型变量var1,并初始化为10
var2 dw 200 ; 定义一个字型变量var2,并初始化为200
在这个例子中, db 是定义字节类型, dw 是定义字类型。通过指定段来区分数据存储的位置。
数据结构在汇编语言中通过定义变量和数组来实现。数组是相同类型数据的集合,可以通过偏移量来访问数组中的元素。
section .data
array db 1, 2, 3, 4 ; 定义一个字节型数组array
数组元素可以使用索引访问,索引从0开始。
3.2 高级汇编语言技巧
3.2.1 子程序设计与调用
在复杂的汇编程序设计中,子程序设计与调用是一种重要的结构化编程技巧。它有助于提高代码的复用性和模块化。子程序(也称作函数或过程)的定义和调用涉及到了 call 和 ret 指令。
section .text
global _start
_start:
call my_subroutine ; 调用子程序my_subroutine
; 其他代码…
my_subroutine:
; 子程序的代码
ret ; 返回调用者
在本例中, call 指令用来调用子程序,执行完子程序后, ret 指令会返回到调用者继续执行。通过这种机制,主程序和子程序之间可以实现跳转和数据交换。
3.2.2 中断处理与系统调用
中断是计算机工作中的一种机制,用于处理突发事件或请求操作系统服务。在汇编语言中,中断处理通常涉及中断向量表和中断服务程序。当发生中断时,CPU会根据中断号查找中断向量表,并跳转到相应的中断服务程序执行。
系统调用是操作系统为应用提供服务的一种接口,它通过中断实现。例如,在x86架构的DOS操作系统中,可以通过中断 int 0x21 来实现文件操作、进程管理等。
mov ah, 0x4C ; 准备退出程序的中断服务号
mov al, 0x00 ; 返回码
int 0x21 ; 执行中断调用
在这个例子中, int 0x21 是DOS操作系统提供的系统调用中断, ah 寄存器用于指定服务号, al 寄存器用于传递参数。
![[程序执行.png]]
这张mermaid流程图描述了调用和执行子程序的基本流程。从开始到调用子程序,执行子程序,返回调用者,然后继续主程序的执行,这整个过程构成了汇编程序的基本结构。
通过本章节的介绍,我们了解了汇编语言编程的两个基础层次:基础语法和高级技巧。通过这两个层次的学习,我们可以编写出能够直接与硬件交互的代码,理解程序的底层执行过程,这在性能优化、系统编程和嵌入式开发等领域具有重要的意义。

4. 存储系统分析与内存管理

存储系统是计算机系统的核心组成部分之一,负责数据的存储、检索与管理。现代计算机系统中的存储系统通常由多个层次构成,包括但不限于CPU高速缓存(Cache)、主存(RAM)、辅助存储(如硬盘、固态硬盘)以及外存(如云存储)。了解存储系统的组成和内存管理技术对于设计高效能、稳定性的计算机系统至关重要。
4.1 存储系统的组成与分类
存储系统按照存储介质、访问速度、容量和用途等因素,可以被划分为多种类型。本节将介绍主存储器与辅助存储器的主要区别,以及不同存储介质的特性分析。
4.1.1 主存储器与辅助存储器的比较
主存储器(通常指RAM)是计算机直接使用的内存,速度快、价格相对较高,但容量有限,且易失性(断电后数据会丢失)。
辅助存储器(如硬盘、固态硬盘)则用于长期保存大量数据,通常速度较慢、价格较低,且是非易失性的。
对于主存储器,一个关键性能指标是其访问速度,通常以纳秒级来衡量。辅助存储器的性能指标包括读写速度、容量大小,以及耐用性和能耗等。在设计存储系统时,通常需要平衡速度、容量和成本等因素。
4.1.2 存储介质的特性分析
存储介质根据其物理或化学特性,可以进一步细分为多种类型,包括磁性存储、半导体存储、光学存储等。每种存储介质都有其特定的优势和应用场景。
磁性存储是利用磁性材料记录数据的技术,硬盘驱动器(HDD)就是此类介质的代表。其优点是成本低、容量大,但速度较慢且易受物理损害。
半导体存储利用电子元件来存储数据,例如固态硬盘(SSD)就使用了闪存技术。与磁性存储相比,其访问速度快,抗震性能好,但价格较高。
光学存储通过激光在介质上记录数据,如CD、DVD。此类存储介质成本低,便于大规模复制和数据分发,但速度慢且容量有限。
4.2 内存管理技术
内存管理是操作系统的核心功能之一,涉及内存的分配与回收,以及优化内存使用,提高系统整体性能。本节将探讨内存的分区与分配策略,以及虚拟内存技术的实现。
4.2.1 内存的分区与分配策略
内存分区是指操作系统将内存空间划分为若干区域的过程。分区可以是静态的,也可以是动态的。静态分区在系统启动时确定,通常每个分区大小固定。动态分区则根据运行进程的需求,动态地分配和回收内存空间。
分区策略通常包括连续分配和非连续分配。连续分配中,每个进程的内存空间是连续的。非连续分配允许一个进程的内存分散在多个不连续区域中,这样可以更好地利用内存空间,减少碎片化。
4.2.2 虚拟内存技术与实现
虚拟内存技术允许计算机系统运行比实际可用物理内存更大的程序。它通过将内存暂时不使用的部分转移到存储设备上(通常是硬盘),从而使得程序可以利用更多的内存资源。
虚拟内存的实现依赖于硬件和软件的协同工作。硬件部分通常包括地址转换机构(如MMU - 内存管理单元)和分页机制。软件部分则是操作系统中的内存管理模块,负责监控内存使用情况,选择合适的页面置换算法(如最近最少使用LRU算法)进行页面的换入换出。
![[内存管理.png]]
虚拟内存的引入极大地提高了系统的多任务处理能力,但同时也带来了额外的性能开销,因为频繁的页面换入换出操作(称为“抖动”)会导致系统性能下降。因此,优化页面置换算法和合理配置内存大小是提高虚拟内存效率的关键。

参数说明
MMU内存管理单元,负责虚拟地址到物理地址的转换
LRU最近最少使用算法,一种常见的页面置换策略
地址映射将虚拟地址转换为物理地址的过程
抖动频繁的页面换入换出导致的系统性能下降现象
通过本章的介绍,我们深入了解了存储系统的组成及其分类,并详细探讨了内存管理技术的关键方面。理解这些概念对于IT专业人员而言,有助于在硬件选型、系统优化、以及解决性能瓶颈等方面做出更加明智的决策。

5. 输入/输出系统通信方法

在现代计算机系统中,输入/输出(I/O)系统是一个至关重要的组成部分,负责主机与外围设备之间的数据传输。本章将深入探讨I/O系统的结构与功能,以及I/O编程与接口技术。
5.1 I/O系统的结构与功能
5.1.1 输入输出系统的工作原理
输入输出系统确保了计算机可以与外部世界进行交互,即数据可以从外部设备传入计算机(输入),也可以从计算机传到外部设备(输出)。工作原理可从以下几方面进行阐述:
数据传输机制 :I/O系统使用不同的机制进行数据传输,包括程序控制I/O、中断驱动I/O和直接内存访问(DMA)。
设备控制器 :每个外围设备都由一个设备控制器管理,它控制设备的操作并处理CPU与设备间的数据传输。
I/O接口 :I/O接口或端口提供了一个在CPU和I/O设备之间传输数据的接口标准,它负责信号的适配与转换。
5.1.2 I/O接口与设备通信
I/O接口是计算机硬件与外围设备通信的桥梁。下面是I/O接口与设备通信的方式:
同步与异步通信 :同步通信按照一定的时序进行数据传输,而异步通信则允许设备在任意时刻发送或接收数据。
串行与并行通信 :串行通信一次只传输一位数据,而并行通信一次传输多位数据,提高了传输速率,但增加了线路复杂性。
通信协议 :确保数据正确传输的规则和标准,例如USB、IEEE 1394(FireWire)、SATA等。
5.2 I/O编程与接口技术
5.2.1 I/O控制方式与程序设计
I/O控制方式主要分为程序控制I/O和中断驱动I/O。程序控制I/O又可以细分为忙等待和轮询。
程序控制I/O :
忙等待 :CPU不断检查设备状态寄存器,直到传输条件满足。
轮询 :程序周期性检查设备状态寄存器,而不是连续等待,提高了效率。
中断驱动I/O :当I/O操作完成或需要CPU关注时,设备会发送中断信号给CPU,CPU响应中断并处理I/O事件。
5.2.2 接口芯片与外设的数据交换
接口芯片是连接计算机系统和外围设备的关键组件,它管理数据流并执行必要的转换和控制逻辑。
数据缓冲 :缓冲机制允许CPU和外设以不同的速度运行,接口芯片内的缓冲区可以存储数据,以避免数据丢失。
状态和控制寄存器 :这些寄存器用于告知CPU设备状态,并允许CPU控制I/O操作。
直接内存访问(DMA) :DMA允许外围设备直接访问内存,减少CPU的负担。
代码块和逻辑分析
在编程实践中,对I/O接口的读写操作往往需要特定的指令和寄存器。以下是一个简化的示例,展示如何使用汇编语言读取一个设备的状态寄存器:
; 假设设备状态寄存器的地址是0x1000
mov dx, 0x1000 ; 将设备状态寄存器的地址加载到DX寄存器
in al, dx ; 从DX指定的端口读取数据到AL寄存器
; AL寄存器现在包含设备状态寄存器的值
逻辑分析
首先,我们使用 mov 指令将设备状态寄存器的端口地址加载到 dx 寄存器中。
接着,使用 in 指令从指定的端口地址读取数据,这里的数据被放入 al 寄存器中,该寄存器用于存储端口读取的数据。
此操作通常发生在检查设备是否已准备就绪时,或是设备状态需要被查询时。
该代码片段演示了程序控制I/O方式之一,具体实现细节依赖于CPU架构及外部设备的接口规范。在现代计算机系统中,这些操作通常由操作系统或设备驱动程序抽象化处理,以提供更简洁的编程接口。
本章内容对I/O系统的结构、功能、编程与接口技术进行了全面介绍。为了实现高效、可靠的I/O通信,了解和掌握这些基础知识是非常重要的。

6. 总线系统的作用与机制

在现代计算机系统中,总线系统是构成微机硬件的骨架,它负责在各个组件之间传输数据与控制信号。本章节将深入探讨总线技术的基本概念,以及它们如何被应用于微机系统中。
6.1 总线技术的基本概念
6.1.1 总线系统的分类与特性
总线系统可以分为内部总线和外部总线,其中内部总线连接微处理器内部的各个组件,而外部总线则连接微处理器与外部设备。内部总线的主要特性包括数据宽度、频率和传输速率,这些参数决定了总线能够携带数据的能力和速度。外部总线则要考虑与各种不同标准的外部设备接口的兼容性问题。
6.1.2 总线的仲裁与数据传输
总线仲裁是为了解决多个部件同时请求总线控制权时的冲突问题。仲裁机制有多种,常见的有固定优先级仲裁、循环优先级仲裁和中央仲裁。数据传输过程中,总线系统必须保证数据的完整性和同步性,这涉及到时钟同步、数据传输协议和错误检测与纠正技术。
6.2 总线系统在微机中的应用
6.2.1 系统总线的设计要点
系统总线的设计关乎整个计算机系统的性能。设计时需要考虑总线宽度、总线时钟频率、总线带宽以及总线负载能力。此外,总线协议的设计也是至关重要的,它包括地址、数据和控制信号的编码方式,以及数据传输的时序和状态管理。
6.2.2 总线技术的扩展与优化
随着硬件需求的不断提高,总线技术也需要不断扩展与优化。这包括增加总线的通道数量、提升传输速率、改进仲裁策略,以及引入新型的总线标准,如PCI Express。优化工作还可能包括对现有总线结构进行重设计以降低延迟,提高数据吞吐量,以及改善电源管理能力。
在现代计算机系统中,总线技术是连接各个硬件组件的桥梁。无论是在提高数据传输效率,还是在扩展系统功能方面,总线技术都扮演着极其重要的角色。通过本章节的学习,我们能够了解总线系统的分类、特性,以及它们在微机中的应用和设计要点,这些知识对于IT专业人员来说是必备的。在后续章节中,我们将继续探讨微机原理的其他核心概念,以及如何将这些知识应用于实际工作中。
简介:微机原理课程设计是计算机科学的核心课程之一,涉及微型计算机的基本结构和工作原理,以及软硬件交互。该设计旨在加深学生对微处理器内部机制的理解,提高汇编语言编程能力,并通过解决实际问题来应用所学知识。课程内容包含微处理器架构、指令系统、汇编语言编程、存储系统、输入/输出系统、总线系统、实模式与保护模式,以及具体的课程设计项目和实验调试。
原文链接:https://blog.csdn.net/weixin_42561464/article/details/144303076

微机原理与应用课程总结及复习

1.STM32 的系统时钟选择与分析

(另有部分在库函数开发指南 122,4.3 STM 32 时钟系统;)
为什么 STM32 要有多个时钟源呢?
因为首先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比 如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干 扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。
三种不同的时钟源可被用来驱动系统时钟 (SYSCLK):
● HSI 振荡器时钟
● HSE 振荡器时钟
● PLL 时钟 这些设备有以下 2 种二级时钟源:
●(LSI) 40kHz 低速内部 RC,可以用于驱动独立看门狗和通过程序选择驱动 RTC。RTC 用于从停机 / 待机模式下自动唤醒系统。
●(LSE) 32.768kHz 低速外部晶体也可用来通过程序选择驱动 RTC(RTCCLK)。 当不被使用时,任一个时钟源都可被独立地启动或关闭,由此优化系统功耗。
![[ceb17faf77be6834d07531292adcf18e_MD5.png]]
1 .当 HSI 被用于作为 PLL 时钟的输入时,系统时钟能得到的大频率是 64MHz。
2 .对于内部和外部时钟源的特性,请参考相应产品数据手册中 “电气特性” 章节。 用户可通过多个预分频器配置 AHB、高速 APB(APB2)和低速 APB(APB1)域的频率。AHB 和 APB2 域的大频率是 72MHz。APB1 域的大允许频率是 36MHz。SDIO 接口的时钟频率固定 为 HCLK/2。 RCC 通过 AHB 时钟 (HCLK)8 分频后作为 Cortex 系统定时器(SysTick) 的外部时钟。通过对 SysTick 控制与状态寄存器的设置,可选择上述时钟或 Cortex(HCLK)时钟作为 SysTick 时钟。ADC 时钟 由高速 APB2 时钟经 2、4、6 或 8 分频后获得。
定时器时钟频率分配由硬件按以下 2 种情况自动设置:

  1. 如果相应的 APB 预分频系数是 1,定时器的时钟频率与所在 APB 总线频率一致,否则,定时器的时钟频率被设为与其相连的 APB 总线频率的 2 倍。
  2. 定时器定时中断时,寄存器的参数设置
    ![[b8c935f302a620717c7a77b358736245_MD5.png]]![[7e458774b769ed9bf67ce34a83bc1384_MD5.png]] ![[4cb599b0d888709a4745082a3b8d783a_MD5.png]]![[c67aefd8b1c44262814b6bebb5bee006_MD5.png]]![[7db346a7089a3686c79042ba1ef7d1fa_MD5.png]]![[23825e321470a0b3f653e7cd521f0006_MD5.png]]

2. 定时器定时中断时,寄存器的参数设置

(1)中文参考手册 282,14.4,TIMx 寄存器描述;
(2)库函数开发指南,216,十三章. 定时器中断实验

3. 串口通信时,特别是波特率寄存器的设置

(1)中文参考手册 540,25.6 USART 寄存器描述
(2)库函数开发指南 188, 第九章 串口实验

4. 单片机选型原则

一、单片机内核 * 不同的内核有不同的性能 / 功耗表现,按需选择;* 内核即代表某系列的单片机;
1、简单基础单片机内核: 51、STM8、AVR、PIC、S08、430;
2、ARM Cortex 系列:Cortex-M0 内核是低功耗的内核;
3、ARM 全系列详解: http://www.myir-tech.com/resource/448.asp4(附)、两种逻辑电路集成器件:
a、FPGA:基于门编程 (altera、xilinux);类似:SDAM 掉电不保存;
b、CPLD:基于块编程;类似:EEPRM FLASH 掉电保存;
二、单片机选型

  • 市面的产品基本都是围绕这几款单片机:51(低端)、ST(中端)、ARM(高端);
    1、单片机的性能;
    2、单片机的自身资源是否满足项目需求,长远考虑后续的更新迭代 (封装、功能)、系统升级和维护难度; a、内存是否足够(储存常量数组、密码等数据); b、I/O 的数量充足; c、外设资源(RTC、IIC(硬件、模拟)、SPI、UART);d、未来需求兼容性
    3、开发周期:熟悉该类型单片机的硬件与软件,可用高级语言编写和调试;
    4、单片机的价格、货源、体积、封装;
    5,开发工具

5. 串口通信中的注意事项

《微机原理与应用》题库

一 填空题

  1. 对于任意的一个三位十进制正整数用二进制表示时,至少需要 10 位,用 BCD 数来表示时至少需要 12 位
  2. 模型计算机 CPU 执行程序的过程是取出指令执行指令两个阶段的循环。
  3. 微处理器是由算术逻辑部件控制部件寄存器内部总线等 4 部分组成。
  4. 微型计算机是由微处理器存储器I/O 接口系统总线等 4 部分组成。
  5. 微型计算机系统是在微型计算机基础上,配置系统软件外部设备组成。
  6. 外部设备有三种:输入设备输出设备输入又输出的设备
  7. 外部设备与 CPU 之间必须经过 I/O 接口电路进行协调和转换。
  8. 微型计算机的系统总线有三种:数据总线地址总线控制总线
  9. 微型计算机系统的主要性能指标有:字长存储容量指令系统运算速度系统配置
  10. 指令:计算机能够识别和执行的基本操作命令,指令系统:计算机所能执行的全部指令,程序:为实现某一任务所编写的指令的有序集合
  11. 指令通常分成**操作码(Operation code,Opcode)操作数(Operand)**两大部分,其中操作码表示计算机执行什么操作,操作数指明参加操作的数本身或操作数所在的地址。
  12. 8086 的标志寄存器中状态标志有:CFAFZFPFSFOF
  13. 8086 的标志寄存器中控制标志有:IFDFTF
  14. 8086CPU 是由总线接口部件(Bus Interface Unit,BIU)执行部件(Execution Unit,EU)两大部分组成,总线接口部件的功能是与 CPU 外部(存储器,I/O 端口)传送指令代码或数据,执行部件的功能是负责指令的执行
  15. 8086CPU 的数据总线宽度为 16 位,地址总线宽度为 20 位,I/O 地址总线宽度为 16 位
  16. 一个计算机系统所具有的物理地址空间大小由地址总线的宽度决定,8086 系统的物理空间地址为 1MB
  17. 代码段的基值存于 CS寄存器,数据段的基值存入 DS寄存器,扩展段的基值存入 ES 寄存器,堆栈段的基值存入 SS寄存器
  18. 8086CPU 引脚中,用来控制 8086 工作方式的引脚为 MN/MX
  19. 8086CPU 中 BP 默认的段寄存器是 SS,BX 默认的段寄存器是 DS
  20. 8086CPU 所访问的存储器为奇区偶区,各区的数据总线分别对应 CPU 数据在线的高八位低八位
  21. 汇编语言指令语句格式:[标号:] 操作码助记符 [操作数1], [操作数2], [操作数3] ;[注释]
  22. CPU 和输入 / 输出设备之间传送的信息有数据信息状态信息控制信息
  23. 8086 处理器的基本数据类型是字节双字
  24. 8086 指令的操作数寻址方式分为:立即寻址寄存器寻址存储器寻址I/O端口寻址
  25. 8086 指令的存储器寻址方式分为:直接寻址寄存器间寻址寄存器相对寻址基址加变址寻址相对的基址和变址寻址
  26. 存储器按制造工艺可分为:双极型存储器MOS型存储器
  27. 存储器按信息存储方式分为:随机存储器 (RAM)只读存储器 (ROM)
  28. 随机存储器 (RAM) 可分为:静态 RAM动态 RAM
  29. 只读存储器 (ROM) 可分为:Mask ROMPROMEPROMEEPROM
  30. 半导体存储器的性能指标:存储容量存取速度可靠性性能 / 价格比功耗
  31. 在半导体存储器中,RAM 指的是随机(易失性)存储器,他可读可写,但断电后信息一般会丢失;而 ROM 指的是只读(非易失性)存储器,断电后信息可保留
  32. 动态 RAM 中,信息是以电荷的形式存储在电容上,读出信息时具有破坏性,因此读出操作后必须进行刷新
  33. 用𝟏𝟎𝟐𝟒×𝟒RAM 组成𝟔𝟒𝑲×𝟖存储器容量要 128 个 RAM 芯片,10 根片内选址地址线。
  34. 存储结构为 8K*8 位的 EPROM 芯片 2764,共有 8个数据引脚,13个地址引脚。用他组成 64KB 的 ROM 存储区共需 8片芯片。
  35. 在 8086CPU 系统中,假设地址总线 A15~A19 输出 01001 时译码电路产生一个有效的片选信号。这个片选信号将占有主存从 48000H4FFFFH 的物理地址范围,共有 32KB 容量。
  36. 一个𝟓𝟏𝟐×𝟒的 RAM 芯片需要多少根地址线(9),多少根数据线(4),若要组成一个𝟔𝟒𝑲×𝟖的存储器,需要多少个 RAM 芯片(256),多少个芯片组(128),多少根芯片组选择地址线(7)。
  37. 存储矩阵中基本存储电路的地址编码产生方式有:单译码方式双译码方式
  38. RAM 存储容量的扩展方法:位扩展方式字扩展方式字位扩展方式
  39. Cache 的地址映像方式有:直接映像全相联映像组相联映像
  40. 中断处理的基本过程包括:中断请求中断判优中断响应中断处理中断返回
  41. 8086CPU 的中断系统中共有 256个中断类型码,与中断类型码 12 对应的向量地址为 48,系统将在内存地址的 00000H~003FFH 处,设置全部中断类型的中断向量。
  42. 8259A 是可编程中断控制器,8259A 有 4个初始化命令字3个操作命令字
  43. 单片 8259A 可管理 8级可屏蔽中断,6 片级联最多可管理 43级
  44. 8237A 是可编程DMA控制器,8237A 有 4个独立的DMA通道
  45. 单片 8237A 有 4个 DMA 通道,5 片 8237A 构成的二级 DMA 系统,可提供 16个 DMA 通道。

二 选择题

  1. 下列各数中,最大的是(A)。
  2. 有一个二进制数为 10101100,表示无符号数,则对应的十进制数为(D),若表示有符号数(补码表示),则对应的十进制数为(A)。
    (A)-84(B)-44(C)-172 (D)172
  3. 下列各数不属于 8421BCD 码的是(A)。
    (A)10100101B (B)01011001B (C)00110011B (D)01010100B
  4. 以下关于字节和字长的说法有误的是(D)。
    (A)一个字节由 8 位二进制位组成。
    (B)字长是计算机内部一次可以处理的二进制数的位数。
    (C)字长依赖于具体的机器,而字节不依赖具体的机器。
    (D)字长越长,处理精度越高,但处理速度越慢。
  5. 计算机中,存储信息的最小单位(A)。
    (A)位 (B)字节 (C)字 (D)存储单元
  6. 存储器中,存储信息的最小单位(B)。
    (A)位 (B)字节 (C)字 (D)存储单元
  7. 8086 微处理器可寻址访问的 I/O 空间为(B)。
    (A)1KB (B)64KB (C)640KB (D)1MB
  8. 8086 微处理器可寻址访问的内存存储空间为(D)。
    (A)1KB (B)64KB (C)640KB (D)1MB
  9. 微处理器由(D)组成。
    (A)运算器和存储器(B)运算器和接口电路 (C)控制器和存储器(D)运算器和控制器
  10. 8086 微处理器的段地址寄存器中(A)是代码段寄存器。
    (A)CS (B)DS (C)ES (D)SS
  11. 8086 微处理器的段地址寄存器中(B)是数据段寄存器。
    (A)CS (B)DS (C)ES (D)SS
  12. 8086 微处理器的段地址寄存器中(C)是扩展段寄存器。
    (A)CS (B)DS (C)ES (D)SS
  13. 8086 微处理器的段地址寄存器中(D)是堆栈段寄存器。
    (A)CS (B)DS (C)ES (D)SS
  14. 8086 微处理器的通用寄存器中(A)是累加器。
    (A)AX (B)BX (C)CX (D)DX
  15. 8086 微处理器的通用寄存器中(B)是基数寄存器。
    (A)AX (B)BX (C)CX (D)DX
  16. 8086 微处理器的通用寄存器中(C)是计数寄存器。
    (A)AX (B)BX (C)CX (D)DX
  17. 8086 微处理器的通用寄存器中(D)是数据寄存器。
    (A)AX (B)BX (C)CX (D)DX
  18. 8086 微处理器的专用寄存器中(A)是堆栈指针寄存器。
    (A)SP (B)BP (C)SI (D)DI
  19. 8086 微处理器的专用寄存器中(B)是基数指针寄存器。
    (A)SP (B)BP (C)SI (D)DI
  20. 8086 微处理器的专用寄存器中(C)是源变址寄存器。
    (A)SP (B)BP (C)SI (D)DI
  21. 8086 微处理器的专用寄存器中(D)是目的变址寄存器。
    (A)SP (B)BP (C)SI (D)DI
  22. 8086CPU 从存储器中预取指令,它们采用的存取原则为(A
    (A)先进先出 (B)先进后出 (C)随情况不同而不同(D)随机
  23. 由 8086CPU 组成 PC 机的数据线是(D)。
    (A)8 根单向线 (B)16 根单向线 (C)8 根双向线 (D)16 根双向线
  24. 8086CPU 的一个典型总线周期需要(A)个状态。
    (A)4 (B)3 (C)2 (D)1
  25. 指令队列的作用是(C)。
    (A)暂存操作数 (B)暂存操作地址(C)暂存指令 (D)暂存指令地址
  26. 在 8086/8088 系统中,内存采用分段结构,段与段之间是(D)。
    (A)分开的 (B)连续的 (C)重叠的 (D)都可以
  27. 8086CPU 中,当 时,CPU 执行的操作是(A)。
    (A)存储器读 (B)I/O 读 (C)存储器写 (D)I/O 写
  28. 8086CPU 存储器可寻址 1MB 的空间,对 I/O 进行读写操作时,20 位地址中只有(B)有效。
    (A)高 16 位 (B)低 16 位 (C)高 8 位 (D)低 8 位
  29. 在 8086CPU 从总线上撤消地址,使总线的低 16 位置成高阻态,其最高 4 位用来输出总线周期的(C)。
    (A)数据信息 (B)控制信息 (C)状态信息 (D)地址信息
  30. CPU 中,运算器的主要功能是(C)。
    (A)算术运算 (B)逻辑运算 (C)算术运算和逻辑运算(D)函数运算
  31. 8086/8088CPU 在复位后,程序重新开始执行的逻辑地址是(B)。
    (A)0000 : 00000H (B)FFFF : 0000H (C)FFFF : FFF0H (D)0000 : FFFFH
  32. 具有指令流水线功能的 CPU 其特点是(A)。
    (A)提高 CPU 运行速度(B)提高存储器的存取速度
    (C)提高 I/O 处理速度 (D)提高 DMA 的传递速度
  33. 下列指令中,不含有非法操作数寻址的指令是(D)。
    (A)ADC [BX], [30] (B)ADD [SI+DI], AX (C)SBB AX, CI (D)SUB [3000H], DX
  34. 以下指令中与 SUB AX, AX 作用相同的是(C)。
    (A)OR AX, AX (B)AND AX, AX (C)XOR AX, AX (D)PUSH AX
  35. 下列指令中,非法指令是(A)。
    (A)OUT [BX], AL(B)ADD [BX+DI], AX (C)SBB AX, [BX] (D)SUB [3000H], AX
  36. 将十进制数 25 以组合式 BCD 码格式送 AL,正确的传送指令是(A)。
    (A)MOV AX, 0025H (B)MOV AX, 0025 (C)MOV AX, 0205H (D)MOV AX, 0205
  37. 指令 MOV AX, 1234H 的寻址方式是(A)。
    (A)立即寻址 (B)寄存器寻址 (C)存储器寻址 (D)I/O 端口寻址
  38. 指令 MOV AX, BX 的寻址方式是(B)。
    (A)立即寻址 (B)寄存器寻址 (C)存储器寻址 (D)I/O 端口寻址
  39. 指令 OUT DX, AL 的寻址方式是(D)。
    (A)立即寻址 (B)寄存器寻址 (C)存储器寻址 (D)I/O 端口寻址
  40. 指令 MOV AX, [1234H] 的寻址方式是(A)。
    (A)直接寻址 (B)寄存器间寻址(C)寄存器相对寻址
    (D)基址加变址寻址 (E)相对的基址和变址寻址
  41. 指令 MOV AX, [BX] 的寻址方式是(B)。
    (A)直接寻址 (B)寄存器间寻址(C)寄存器相对寻址
    (D)基址加变址寻址 (E)相对的基址和变址寻址
  42. 指令 MOV AX, [BX+1234H] 的寻址方式是(C)。
    (A)直接寻址 (B)寄存器间寻址(C)寄存器相对寻址
    (D)基址加变址寻址 (E)相对的基址和变址寻址
  43. 指令 MOV AX, [BX+SI] 的寻址方式是(D)。
    (A)直接寻址 (B)寄存器间寻址(C)寄存器相对寻址
    (D)基址加变址寻址 (E)相对的基址和变址寻址
  44. 指令 MOV AX, [BX+SI+1234H] 的寻址方式是(E)。
    (A)直接寻址 (B)寄存器间寻址(C)寄存器相对寻址
    (D)基址加变址寻址 (E)相对的基址和变址寻址
  45. 定义字节的伪操作助记符是(A)。
    (A)DB (B)DW(C)DD (D)DQ(E)DT
  46. 定义字的伪操作助记符是(B)。
    (A)DB (B)DW(C)DD (D)DQ(E)DT
  47. 定义双字的伪操作助记符是(C)。
    (A)DB (B)DW(C)DD (D)DQ(E)DT
  48. 定义 8 字节的伪操作助记符是(D)。
    (A)DB (B)DW(C)DD (D)DQ(E)DT
  49. 定义 10 字节的伪操作助记符是(E)。
    (A)DB (B)DW(C)DD (D)DQ(E)DT
  50. 在 8086 宏汇编语言中,求变量基址的操作符是(A)。
    (A)SEG (B)OFFSET (C)TYPE (D)SIZE
  51. 在 8086 宏汇编语言中,求变量偏移地址的操作符是(B)。
    (A)SEG (B)OFFSET (C)TYPE (D)SIZE
  52. 在 8086 宏汇编语言中,求变量的类型属性的操作符是(C)。
    (A)SEG (B)OFFSET (C)TYPE (D)SIZE
  53. 在 8086 宏汇编语言中,求变量包含的总字节数的操作符是(D)。
    (A)SEG (B)OFFSET (C)TYPE (D)SIZE
  54. 已知 CNT EQU 1223H,则以下与 MOV BL,23H 等效的指令是(C)。
    (A)MOV BL, TYPE CNT (B)MOV BL, HIGH CNT
    (C)MOV BL, LOW CNT (D)MOV BL, SHORT CNT
  55. 一个静态半导体存贮芯片的引脚有 A13A0,D3D0,VCC,GND 等,该芯片存贮容量为(C)。
    (A)8K x 8 (B)8K x 4 (C)16K x 4 (D)16K x 8
  56. 动态基本存储单元内保存电荷的时间有限,通常在(B)内都必须刷新一次。
    (A)4ms (B)2ms (C)2ns(D)2s
  57. 以下哪项的存在对提高微处理器的处理速度具有重要作用的是(C)。
    (A)DMA 功能 (B)中断处理 (C)Cache 存储器(D)微程序控制
  58. 如果有多个中断申请同时发生,系统将根据中断优先级的高低先响应优先级最高的中断请求。若要调整中断源申请的响应次序, 可以利用(B)。
    (A)中断响应 (B)中断屏蔽 (C)中断向量 (D)中断嵌套
  59. 8086CPU 响应可屏蔽中断时,CPU(B)。
    (A)执行一个中断响应周期 (B)执行两个连续的中断响应周期
    (C)执行两个中断响应周期,中间 2~3 个空闲状态 (D)不执行中断响应周期
  60. 在程序控制传送方式中,哪种方式可以提高系统的工作效率(B)。
    (A)查询传送 (B)中断传送 (C)前二项均可 (D)DMA 方式
  61. 采用 DMA 传送数据时,数据传送过程是由(D)控制的。
    (A)软件 (B)CPU (C)CPU + 软件 (D)硬件控制器
  62. 传送数据时,占用 CPU 时间最长的传送方式是()。
    (A)查询 (B)中断 (C)DMA (D)无条件传送

三 简答题

1. 微处理器、微型计算机和微型计算机系统三者有何联系与区别?
微处理器(CPU)是由算术逻辑部件(ALU)、控制部件、寄存器、内部总线等 4 部分组成。
微型计算机是由微处理器、存储器、I/O 接口、系统总线等 4 部分组成。
微型计算机系统是在微型计算机基础上,配置系统软件和外部设备组成。
2. 计算十进制数 - 47 的原码,反码,补码(8 位二进制的形式表示),并说明 8 位二进制原码,反码,补码所能表示的数值范围(用十进制表示)。

3. 将十进制数 658.125 转换成二进制、八进制、十六进制、BCD 数。

4. 若,要求给出求解过程,并指明运算后的溢出情况。

5. 8086 总线周期的 T1、T2、T3、T4 状态,CPU 分别执行什么动作?
T1 周期:CPU 向 AD 总线上发出地址信息以指出要寻址的存储单元或外设 I/O 端口的地址。
T2 周期:对读操作,CPU 从 AD 总线上撤消地址信息使总线的低 16 位成高阻状态,为 16 位数据输入作准备;对写操作,CPU 输出数据信息。总线的最高 4 位用来输出总线周期状态信息。
T3 周期:AD 总线的高 4 位继续输出状态信息,低 16 位上输出由 CPU 提供的数据(写操作)或者 CPU 从存储器(或端口)读入的数据(读操作)。
T4 周期:总线周期结束。
6. 8086CPU 是由哪两个部件组成,各个部件的功能是什么?
总线接口部件(Bus Interface Unit,BIU)和执行部件(Execution Unit,EU)两大部分组成。
总线接口部件的功能是与 CPU 外部(存储器,I/O 端口)传送指令代码或数据。
执行部件的功能是负责指令的执行。
7. 8086CPU 的总线接口单元(BIU)由哪几部分组成?
16 位的段地址寄存器(CS,DS,ES,SS),16 位的指令指针寄存器(IP),20 位的地址加法器,6 字节的指令队列缓冲器,16 位的内部暂存器,总线逻辑控制器。
8. 8086 的执行单元(EU)由哪几部分组成?
通用寄存器(AX,BX,CX,DX),专用寄存器(BP,SP,SI,DI),算术逻辑单元,EU 控制器,标志寄存器。
9. 用一条指令完成下述要求。
(1)将 DX 的高字节清零,低字节不变
(2)将 BX 的高字节置‘1’,低字节不变
(3)将 AX 的偶数位变反,奇数位不变
(1)对某些二进制位‘清零’可采取用逻辑‘与’操作 ANDDX, 00FFH
(2)对某些二进制位‘置位’可采用逻辑‘或’操作 OR BX, 0FF00H
(3)对某些二进制位‘求反’可采用逻辑‘异或’操作 XORAX, 5555H
10. 8086CPU 复位后,存储器和指令队列处于什么状态?试求出程序执行的起始地址。
复位后,8086 处于初始化状态。此时,除 CS 寄存器为 FFFFH 外,其他所有寄存器全部清 0,指令队列亦清空。程序执行地址为 CS:IP,犹豫 IP 等于 0,程序执行的起始地址为 FFFFH:0000,即物理地址为 FFFF0H。
11. (AX)=2345H (DX)=5219H ,指出两个数据相加和相减后,FLAGS 中状态标志位的状态。
相加后,SF=0、ZF=0、PF=0、CF=0、AF=0、OF=0。
相减后,SF=1、ZF=0、PF=0、CF=1、AF=1、OF=0。
12. 请写出如下程序片段中每条逻辑运算指令执行后标志 ZF,SF 和 PF 的状态

13. (DS)=1000H (SS)=2500H (SI)=0100H (BX)=0800H (BP)=0600H ,指出下列指令的目的操作数的寻址方式,并计算目的操作数的物理地址。

14. 请指出下列指令中的错误。
(1)MOV CS,12H
(2)MOV AL,1400
(3)MOV CX, AL
(4)MOV BX, [SI+DI]
(5)OUT 375H, AL
(6)MOV [BX], [1000H]
(7)MOV [DI], 02
(8)PUSHAL
(1)CS 不能作为目的操作数。
(2)1400 超过了一个字节所能表示的范围。
(3)目的操作数是字操作,而源操作数是字节操作,类型不匹配。
(4)没有这种寻址方式。
(5)375H 超过了输出指令中直接寻址的范围 0~0FFH。
(6)源和目的的操作数不能同时为存储器寻址。
(7)源和目的操作数的类型不明确,不能确定是字操作还是字节操作。
(8)PUSH 指令只能是字操作。
15. 半导体存储器的主要性能指标有哪些?
存储容量、存取速度、可靠性、性能 / 价格比、功耗。
16. 存储芯片由哪几部分组成?各部分功能是什么?
存储矩阵、地址译码器、存储器控制电路、三态双向缓冲器。
存储矩阵的功能是存储信息。
地址译码器的功能是 CPU 发送的地址信号进行译码后产生地址编码。
存储器控制电路功能是接收来自 CPU(外部电路)的控制信号,经过组合变换后,对存储矩阵,地址译码器和三态双向缓冲器进行控制,控制对选中的单元进行读 / 写操作。
三态双向缓冲器的功能是使组成半导体 RAM 的各个存储芯片很方便地与系统数据总线相连接。
17. 存储器芯片与 CPU 连接时要注意以下几点

  1. 数据总线的连接
    输入输出电路包含三态缓冲驱动器时,芯片的数据线课直接连接到 CPU 数据总线。
    输入输出电路不包含三态缓冲驱动器时,则须外加三态缓冲驱动器,再与 CPU 数据总线。
  2. 地址总线的连接
    地址总线的一部分:直接与存储器的片内寻址地址线连接。
    地址总线的另一部分:通过译码器产生的片选信号与存储器的片选端连接。
  3. 控制总线的连接
    存储器读信号:用于控制存储芯片上的输出允许信号端。存储器写信号:用于控制存储芯片上的写允许信号端。
    18. 简述 SRAM 和 DRAM 的各自特点。
    静态 RAM 是以触发器原理存储信息。静态 RAM 的读写速度快,集成度低,容量小,主要用于 Cache。
    动态 RAM 是以电容的电荷充放原理存储信息。动态 RAM 的读写速度慢,集成度高,容量大,主要用于存储量较大的系统。
    19. 半导体存储器的分类

    20. 中断的概念
    计算机在执行正常程序的过程中出现内部或外部某些事件的请求时,CPU 暂时停止当前程序的正常执行,转去执行请求事件的处理操作,CPU 在事件处理结束后再回到被暂时中断了的程序继续往下执行。

    21. 中断系统的作用
    (1)能实行并行处理:可以实现 CPU 和多个外设同时工作,只有当它们彼此需要交换信息时才产生 “中断”。
    (2)能实现实时处理:各种外设提出请求的时间都是随机的,要求 CPU 迅速响应和及时处理,有了中断功能,就可以实现实时处理功能。
    (3)能实现故障处理:如电源断电,存储器错误,运算出错等。
    22. 试述 D/A 转换器的主要技术指标。
    (1)分辨率:指 D/A 转换器对数字输入量变化的敏感程度的度量。转换器的位数越多,分辨率越高。
    (2)转换精度:指 D/A 转换器实际输出电压与理论值间的误差,与标准电源精度,电阻网络的电阻精度,增益误差等有关。
    (3)建立时间:指 D/A 转换器中输入代码有满刻度值的变化时,输出模拟量信号达到与满刻度值相差 ±1/2LSB 相当的模拟量所需时间。
    (4)线性度:指实际输出特性偏离理想转换特性的最大值称为线性误差,通常用 LSB 的倍数表示,如 1LSB,1/2LSB 等。
    (5)温度系数:在规定的范围内,相应于温度没变化 1oC,增益,线性度,零点及偏移等参数的变化量。
    23. 试述 A/D 转换器的主要技术指标。
    (1)分辨率:指 A/D 转换器可转换成数字量的最小模拟电压值,用于描述 A/D 转换器对最小输入信号的分辨能力。
    (2)转换精度:指 A/D 转换器的实际输出与理论值之间的差值,通常用最低有效位 LSB 的分数表示。
    (3)转换时间:完成一次 A/D 转换所需要的时间。
    (4)量程:允许转换的模拟电压范围,分为单极性和双极性。
    (5)温度参数:
  4. 假设被传送的字符均为 7 位 ASCII 码,采用异步串行传送方式。其数据传送格式由 1 位起始位,7 位数据位,1 位奇偶校验位,和 1 位停止位组成,若每秒钟传送 120 个这样的字符。

四 设计题

1. 阅读下列程序,写出各语句的注释,说明本程序功能并写出运行结果。 (10 分)

MOV	AH, 0		; 累加器高位清零
MOV	AL, 10		; 累加器低位赋初值10
SAL		AX, 1		; 左移1位,(初值×2)
MOV	BX, AX		; (初值×2)送入BX保存
MOV	CL, 2		; 计数器赋值为2
SAL		AX, CL		; 左移2次,(初值×2)×2×2
ADD	AX, BX		; 累加,(初值×2)×2×2+(初值×2)

说明:本程序功能为将累加器中数值 ×10,运行结果是:累加器 AX 中为 100.
2. 自 BLOCK 开始的内存缓存区中,有 100 个 8 位无符号数,求出其中最大值,存入 MAX 单元。

MOV	BX, OFFSET BLOCK	;设置地址指针
		MOV	AX, [BX]		;取首个数据
		INC	BX			;修改指针,指向下一个数据
		MOV	CX, 99			;设置计数器,比较次数为N-1
AGAIN: 	CMP	AX, [BX]		;比较两个数
		JNC	NEXT			;无借位即AX中为大数
		MOV	AX, [BX]		;有借位即AX中为小数,替换为大数
NEXT:	INC	BX			;修改指针,指向下一个数据
		DEC	CX			;修改计数器
		JNZ	AGAIN			;计数器≠0返回继续比较
		MOV	MAX, AX		;计数器=0存入最大值
		HLT

3. 设计基于 8086 CPU 的汇编语言程序,实现 Z=X+Y,其中 X 和 Y 均为 8 位无符号数。

DATA	SEGMENT
	DX	DB	?
	DY     DB	?
	DZ     DW	?
DATA	ENDS
CODE	SEGMENT
	ASSUME CS:CODE, DS:DATA
	START:	MOV	AX, DATA
		MOV	DX, AX
		MOV	AH, 0
		MOV	AL, DX
		MOV	BL, DY
		ADD	AL, BL
		ADC	AH, 0
		MOV	DY, AX
CODE	ENDS
	END	START

4. 在 DTX 单元中存放了一个小于 16 的数,试用查表方法计算该数的平方,结束保存到 DTY 单元中。
首先建立 0~15 的平方表 TABQ,然后查得平方值。

DATA	SEGMENT
	TABQ	DB 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225
	DTX      DB?
	DTY      DB?
DATA	ENDS
CODE	SEGMENT
	ASSUME CS:CODE, DS:DATA
	START:	MOV	AX, DATA
		MOV	DX, AX
		MOV	SI, OFFSET TABQ
		MOV	AH, 0
		MOV	AL, DTX
		ADD	SI, AX
		MOV	AL, [SI]
		MOV	DTY, AL
		INT	20H
CODE	ENDS
	END	START


采用分支结构。首先判断 X≥0 还是 X<0,如果 X<0,则 Y=-1;如果 X≥0,则在判断 X=0 还是 X>0,从而确定数值 Y。

DATA	SEGMENT
	DTX	DB ?
	DTY	DB ?
DATA	ENDS
CODE	SEGMENT
	ASSUME CS:CODE, DS:DATA
	START:	MOV	AX, DATA
		MOV	DS, AX
		MOV	AL, DTX
		CMP	AL, 0
		JGE	BGE		; X>=0时转移
		MOV	AL, 0FFH	; X<0,则AL=-1
		JMP	EQ1		; 转向出口
	BGE:	JZ	EQ1		; 当X=0,转向出口,AL本身为0
		MOV	AL, 1		; 当X>0,则AL=1
	EQ1:	MOV	DTY, AL		; 把结果送到DTY单元中
		MOV	AX, 4C00H
		INT	21H
CODE	ENDS
	END	START

6. 内存扩展电路如下图示,试写出各芯片的信号名称和存储器地址空间。

数据信号:D0~D7
控制信号:
CE—片选
WE—写允许
OE—数据输出允许
片内地址信号:A12~A0
译码器输入地址信号:A15、A14、A13
RAM 6264:Y0 — 0000H ~ 1FFFH
EEPROM 28C64:Y7 — E000H ~ FFFFH
7. 例如某个使用 8086 的微机系统有二十位地址信号:A19~A0,十六位数据信号:D15~D0,写控制信号 WR,读控制信号 RD,存储器 / 输入输出控制信号 M/IO,在 M/IO=1 时是进行存储器访问。现要求使用 128K 8 位静态 RAM 芯片设计 256KB 内存系统,要求既能够进行字节访问又能够进行十六位访问。*
(1)需要多少片存储器芯片?
(2)要求存贮器空间为 00000H 开始的连续地址空间,选择地址译码方法
(3)画出存储器系统电路原理图。
答:
(1)需要 2 片。
(2)选择全译码方式。
(3)原理图如下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值