简介:清华大学提供的操作系统课程资料,是一份包含理论与实践的全面集合,涉及操作系统的核心概念、关键技术、实验操作和权威读物。这些资料通过详尽的课堂笔记、实验指导、项目案例以及经典参考书籍的讲义,深入讲解了进程管理、内存管理、文件系统、设备管理、调度算法、死锁预防、资源分配策略、安全性与并发控制等主题。课程资料旨在通过实践加深理论知识的理解,并提升对操作系统底层机制的深入理解。
1. 操作系统核心概念介绍与实验
操作系统是管理计算机硬件与软件资源的系统软件,为用户提供了一个易于操作的界面。本章将介绍操作系统的基本概念以及如何通过实验加深理解。
1.1 操作系统的定义与作用
操作系统是计算机系统的核心软件,其主要任务是管理计算机的硬件资源,并提供一个用户与计算机交互的平台。它可以抽象化硬件资源,提供多任务处理能力,保证系统的安全性和稳定性。
1.2 基本组成部分
操作系统的组成部分包括内核(Kernel)、进程管理(Process Management)、内存管理(Memory Management)、文件系统(File System)和设备驱动(Device Drivers)。每一个部分都扮演着关键角色,共同维护系统的正常运行。
1.3 实验与操作系统的理解
通过安装、配置和运行不同的操作系统,用户可以加深对系统工作原理的认识。实验不仅有助于理解理论知识,而且能够提高解决实际问题的能力。例如,通过安装Linux操作系统,了解其启动过程、文件系统结构和基本命令的使用。
在实验中,用户可以设置虚拟机,尝试不同发行版的Linux系统,体验它们各自的特点。同时,实验还可以包括在操作系统内创建和管理进程,观察它们如何在CPU调度下运行。
以上是第一章的核心内容,接下来的章节将详细展开操作系统的各个组成部分及其关键技术和概念。
2. 进程管理及其通信机制
2.1 进程与线程的基本概念
2.1.1 进程的定义与状态
进程是操作系统中一个非常重要的概念,它是一个程序在其自身地址空间的一次执行活动。进程的出现标志着计算机由批处理系统进入到分时系统,它使计算机可以同时处理多个任务,极大的提升了资源利用率和用户友好性。
进程不仅包含程序代码,更重要的是它还包含了当前的活动,通过程序计数器、寄存器和变量来实现。一个进程的状态通常包括:就绪(Ready)、运行(Running)、阻塞(Blocked)和终止(Terminated)。
代码块: 进程状态转换图示例
graph LR
A[创建] --> B[就绪]
B --> C[调度]
C --> D[运行]
D --> E[时间片用完]
D --> F[等待I/O]
E --> B
F --> G[阻塞]
G --> H[被唤醒]
H --> B
D --> I[退出]
I --> J[终止]
2.1.2 线程的概念及其优势
线程是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
线程的优势在于:
- 资源共享 :同一个进程中的线程可以共享该进程的所有资源。
- 通信效率高 :线程之间的通信相对于进程间的通信要容易得多。
- 创建和切换效率高 :线程创建和上下文切换的开销较进程小。
- 多核处理器利用 :多线程能够更好地利用多核处理器资源。
2.2 进程调度算法
2.2.1 调度的基本原理
进程调度算法用于确定如何在一个处理器的多个进程之间分配CPU时间。调度算法的性能通常根据平均等待时间、平均周转时间和CPU利用率等指标来衡量。
表格: 常见的调度算法对比 | 调度算法 | 描述 | 优点 | 缺点 | | --- | --- | --- | --- | | 先来先服务(FCFS) | 按照请求顺序执行进程 | 公平,易于理解 | 长进程导致短进程饥饿 | | 短作业优先(SJF) | 选择执行时间最短的进程 | 减少平均等待时间 | 实现复杂,长进程饥饿 | | 时间片轮转(RR) | 按时间片轮流执行每个进程 | 公平,响应时间短 | 较高上下文切换开销 | | 优先级调度 | 根据进程的优先级来调度 | 适用于实时系统 | 低优先级进程可能饥饿 |
2.2.2 各种调度算法的比较
不同的调度算法在不同的应用场景下有不同的表现。例如,FCFS简单但可能导致长作业排队;SJF性能优越但可能导致较长的进程饥饿;RR算法适合于分时系统,但可能引起频繁的上下文切换。
代码块: 时间片轮转调度算法伪代码示例
def round_robin_scheduling(processes, quantum):
time = 0
ready_queue = []
for process in processes:
ready_queue.append(process)
while ready_queue:
current_process = ready_queue.pop(0)
run_time = min(current_process.get_runtime(), quantum)
time += run_time
current_process.update_runtime(run_time)
if current_process.has_not_finished():
ready_queue.append(current_process)
else:
current_process.terminate()
if not ready_queue:
break
return time
2.3 进程间通信(IPC)
2.3.1 管道(Pipe)与消息队列
管道是一种最基本的IPC机制,用于在同一台计算机上的进程间传输数据。管道可以是半双工的,数据只能单向流动,如果需要双向通信,则需要两个管道。
消息队列是另一种形式的IPC,它允许一个或多个进程发送消息给一个或多个其他进程。消息队列在操作系统和网络应用中广泛使用,因为它提供了异步通信的能力。
Mermaid流程图: 管道与消息队列的数据传输比较
graph LR
A[进程A] -->|写入| B(管道)
B -->|读取| C[进程B]
A -.->|发送消息| D(消息队列)
D -.->|接收消息| C
2.3.2 信号量(Semaphore)与共享内存
信号量是一种广泛使用的IPC技术,主要用于进程间同步和互斥。信号量可以是有信号的整数变量,通过P(等待)和V(信号)操作来控制对共享资源的访问。
共享内存是另一种有效的IPC机制,允许两个或多个进程共享一个给定的存储区。进程之间可以通过对共享内存区域的读写操作来实现信息传递。
代码块: 信号量机制的基本使用示例
#include <semaphore.h>
sem_t sem;
void P() {
sem_wait(&sem); // P操作(等待)
}
void V() {
sem_post(&sem); // V操作(信号)
}
进程管理及其通信机制是操作系统中的核心内容,它们共同作用于保证系统内多个程序高效、协调地运行。进程管理主要关注进程的创建、调度、状态转换及终止,而进程间通信则是确保不同进程间可以有效协作和数据交换的关键。在实际应用中,理解这些概念对于编写高效稳定的应用程序至关重要。
3. 内存管理与虚拟内存实现
3.1 内存管理基础
3.1.1 连续分配与非连续分配
内存管理是操作系统的核心功能之一,它负责为运行中的程序提供足够的内存空间以及优化内存的使用效率。在内存管理中,我们可以将内存分配的机制主要分为两种:连续分配和非连续分配。
连续分配方式要求为每个程序分配一个连续的内存空间。早期的计算机系统中,由于硬件和软件的限制,基本采用连续分配方式。这种方法简单直观,程序运行速度快,但存在明显的不足。主要是难以满足动态增长的需求和存在内部碎片问题。随着计算机技术的发展,多任务操作系统的需求日益增长,连续分配已不能满足现代操作系统的要求。
为解决这些问题,现代操作系统中普遍采用了非连续分配机制。非连续分配允许程序在内存中分散存储,解决了连续分配的局限性。它主要包括分页机制和分段机制。在分页系统中,内存被划分为固定大小的页框,程序被分割成与页框大小相同的小块,这些块在物理内存中可以不连续存放。而在分段机制中,程序被看作是一系列段的集合,每个段是一个逻辑上具有完整意义的信息集合,比如代码段、数据段等。
3.1.2 分页与分段机制
分页(Paging)和分段(Segmentation)是两种主要的内存非连续分配技术。它们各有特点,同时也常常结合使用。
分页机制是将物理内存划分成固定大小的页框,每一页的大小通常是2的幂次方字节。每个进程被划分为大小相等的页,这些页的大小和物理内存中的页框大小一致。分页机制可以有效利用内存空间,减少外部碎片,还能够实现虚拟内存的概念。
分段机制则是将程序按逻辑模块划分为多个段,每个段具有自己的特性,比如代码段、数据段等。程序运行时,各个段可以不连续地存放在内存中。分段在解决保护和共享问题上优于分页,它可以允许不同的段具有不同的保护属性,而且更符合人们编程的习惯。
实际中,由于分页和分段各有优势,现代操作系统常常将两者结合起来,形成分段分页(Segmentation-Paging)机制。在这种机制下,逻辑地址空间被分为多个段,每个段又进一步被划分为多个页。
在进行内存管理时,操作系统需要维护一系列数据结构来记录哪些内存块是空闲的,哪些是被占用的。例如,页表用于跟踪分页系统中的页;段表则用于跟踪分段系统中的各个段。这些数据结构允许系统快速找到空闲内存空间,进行分配,同时也支持内存访问的地址转换过程。
代码示例和分析
下面展示了一个简单的分页系统中的页表管理代码示例。
// 页表项的结构体定义
typedef struct {
unsigned int present:1; // 是否在内存中
unsigned int rw:1; // 读写权限
unsigned int user:1; // 用户模式访问权限
unsigned int pwt:1; // 写透特性
unsigned int pcd:1; // 缓存禁用
unsigned int accessed:1; // 是否被访问过
unsigned int dirty:1; // 是否被修改过
unsigned int pat:1; // 页面属性表
unsigned int global:1; // 全局页
unsigned int reserved:31; // 保留位
unsigned long address_frame; // 框架号
} PageTableEntry;
// 页表的初始化函数
void initPageTable(PageTableEntry *pageTable, int numPages) {
for (int i = 0; i < numPages; i++) {
pageTable[i].present = 0; // 初始时所有页不在内存中
// 其他权限位根据实际情况设置,这里省略
}
}
// 页表项的访问和修改示例
void accessPage(PageTableEntry *pageTable, int pageNumber) {
if (pageTable[pageNumber].present) {
// 页在内存中,可以进行内存访问
} else {
// 页不在内存中,需要从磁盘加载到内存中
}
}
在上述代码中, PageTableEntry
结构体定义了页表项的格式,每个页表项都记录了该页是否在内存中,是否允许读写,以及对应的内存地址框架号等信息。 initPageTable
函数用于初始化页表,所有页默认不在内存中。 accessPage
函数演示了如何根据页表项的内容来决定是否可以访问该页。
通过这些代码,我们可以了解到在分页系统中,操作系统如何通过页表来管理内存。每一条指令或数据访问都需要通过页表项来查找实际的物理地址,这是实现虚拟内存的关键技术之一。
3.2 虚拟内存技术
3.2.1 虚拟内存的概念
虚拟内存(Virtual Memory)是计算机系统内存管理的一种技术。它使得程序能够拥有比实际物理内存更大的地址空间,通过映射关系将程序的虚拟地址空间映射到物理内存上。
虚拟内存解决了两个核心问题:一是可以运行比物理内存大得多的程序;二是能够提高内存的使用效率,因为系统可以将不常用的内存块(页或段)交换到磁盘上,释放物理内存供其他需要的进程使用。
虚拟内存通常由操作系统和硬件共同协作完成,操作系统负责管理虚拟地址空间和物理内存空间之间的映射关系,硬件(通常是内存管理单元MMU)则负责在运行时将虚拟地址转换为物理地址。
3.2.2 页面置换算法与实现
页面置换算法是在虚拟内存系统中,当物理内存不足以容纳所有页面时,决定哪个页面应当被换出的算法。常用的页面置换算法包括先进先出(FIFO)算法、最近最少使用(LRU)算法和时钟(Clock)算法等。
这些算法在决定哪个页面可以被置换时,会参考页面的使用情况和访问频率等信息。页面置换算法的效率对系统性能有着重大的影响。
以LRU算法为例,它是基于这样的原则:最近最少使用的页面在未来被访问的可能性最小。LRU算法可以使用链表或栈等数据结构实现,下面展示一个简单的LRU算法的实现。
class Node:
def __init__(self, key, val):
self.key = key
self.val = val
self.next = None
self.prev = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = {}
self.capacity = capacity
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key: int) -> int:
if key in self.cache:
# 将该键值对移动到链表头部
self._move_to_front(self.cache[key])
return self.cache[key].val
else:
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
# 更新键值对
self._remove(self.cache[key])
self.cache[key] = Node(key, value)
self._add_to_front(self.cache[key])
else:
if len(self.cache) == self.capacity:
# 删除尾部节点
self._pop_tail()
self.cache[key] = Node(key, value)
self._add_to_front(self.cache[key])
def _move_to_front(self, node: Node):
self._remove(node)
self._add_to_front(node)
def _remove(self, node: Node):
prev = node.prev
next = node.next
prev.next = next
next.prev = prev
def _add_to_front(self, node: Node):
prev = self.head
next = self.head.next
prev.next = node
next.prev = node
node.prev = prev
node.next = next
def _pop_tail(self):
node = self.tail.prev
self._remove(node)
del self.cache[node.key]
上述代码实现了一个简单的LRU缓存机制,它用链表来记录元素的使用顺序,并通过两个指针 head
和 tail
来维护链表的两端。当 put
操作发生时,如果缓存已满,则会从链表尾部移除最久未使用的元素,同时新元素被加入到链表头部。 get
操作会将相应的元素移动到链表头部,表示最近被访问过。
通过这个LRU缓存的实现,我们可以看到操作系统如何在内部使用数据结构来高效地管理内存页面。这只是一个简化的例子,实际上,操作系统需要处理更复杂的情况,如多线程环境下的同步问题和不同页面大小的处理。
以上就是内存管理与虚拟内存实现章节的内容。在下一章节中,我们将深入探讨文件系统的基本原理以及设备管理机制。
4. 文件系统、设备管理与调度算法
4.1 文件系统的基本原理
4.1.1 文件的组织与存储
文件系统作为操作系统中管理文件存储、检索和更新的子系统,其基本原理对用户体验至关重要。文件是操作系统中用于存储信息的基本逻辑单位,它封装了数据、属性和文件元数据。文件的组织通常指的是文件系统如何对文件进行分类、排序和存储。
文件的存储方式大致可以分为连续分配、链接分配和索引分配。连续分配是指在物理存储介质上,为文件分配一块连续的存储空间,这在文件较小时能够提供快速的读写性能,但碎片问题和扩展性是其主要缺点。链接分配则是将文件分散存储,每个文件的数据块通过指针链式连接,提高了空间利用率,但读取性能会下降。索引分配结合了前两者的优点,通过一个索引表记录文件各个数据块的位置,既支持大文件,也便于快速访问。
4.1.2 目录结构与文件访问权限
目录结构是文件系统中组织文件和目录的一种方式。常见的目录结构有单级目录、两级目录和树状(或称为层次)目录结构。层次目录结构是目前被广泛采用的一种,它不仅支持了文件的层次化组织,还简化了文件命名机制。
文件访问权限是操作系统为了保护文件数据的安全性而引入的机制。通过设置读、写、执行等权限,可以控制不同用户对文件的访问。权限管理机制确保了文件数据的安全性,防止了未授权访问和修改。
4.2 设备管理机制
4.2.1 设备驱动程序
设备驱动程序(Device Driver)是操作系统中用来实现对硬件设备的控制和通信的一类软件组件。每个硬件设备都需要一个与之对应的驱动程序来管理设备的操作和数据流。驱动程序处理所有与设备相关的操作,从而为上层应用提供统一的设备访问接口。
驱动程序的设计和实现非常关键,它直接关系到操作系统的稳定性和性能。驱动程序通常具有很高的执行权限,因此错误的设计可能导致整个系统的不稳定甚至崩溃。在多任务操作系统中,驱动程序也必须实现并发控制,以确保多个进程对设备资源的有序访问。
4.2.2 I/O调度算法
I/O调度算法负责管理对I/O设备的请求并优化其执行顺序。它在满足用户进程I/O请求的同时,力求提高磁盘等存储设备的利用率,减少寻道时间,从而加快I/O操作。
常见的I/O调度算法有先来先服务(FCFS)、最短寻道时间优先(SSTF)、扫描(SCAN)和循环扫描(C-SCAN)等。其中,扫描算法(SCAN)以磁头当前位置为起点,沿一个方向扫描,直到到达最后一个请求,然后反向进行。这种算法能有效减少磁盘臂的移动次数,提高磁盘的I/O性能。
4.3 输入输出系统调度
4.3.1 I/O系统的基本结构
I/O系统是操作系统中负责数据输入输出的子系统。它的基本结构包括了硬件设备、设备驱动程序、中断处理程序和设备独立的I/O软件。I/O系统需要处理从设备到内存的数据传输,保证数据传输的准确性和效率。
I/O系统的高效运作需要软件和硬件的紧密配合。硬件设备负责数据的物理移动,而驱动程序和I/O软件则负责处理数据传输的逻辑控制。此外,中断处理程序负责处理设备完成数据传输后产生的中断信号。
4.3.2 磁盘调度算法
磁盘调度是I/O调度的一个重要组成部分,直接影响系统的I/O性能。磁盘调度算法的目标是减少寻道时间,提高数据传输效率,降低I/O操作的延迟。
实际应用中,常见的磁盘调度算法有最短寻道时间优先(SSTF)和扫描(SCAN)算法。扫描算法可以保证磁盘臂移动的方向性,减少无谓的来回移动,从而提高整体效率。除算法选择外,操作系统还需考虑多个进程对磁盘设备的竞争和调度,确保系统能够公平有效地管理多个I/O请求。
graph TD
A[开始I/O请求] --> B[选择调度算法]
B -->|SSTF| C[最短寻道时间优先]
B -->|SCAN| D[扫描算法]
C --> E[处理I/O请求]
D --> E
E --> F[完成I/O操作]
表格展示了各种磁盘调度算法的特点:
| 算法名称 | 特点 | 优点 | 缺点 | | ------ | ---- | ---- | ---- | | SSTF | 选择距离当前磁头位置最近的请求进行服务 | 寻道时间较短 | 可能导致“饥饿”问题 | | SCAN | 磁头从一端开始扫描,直到有请求才改变方向 | 移动距离较短,I/O效率高 | 对于两端的请求不公平 |
代码示例展示了如何在操作系统内核中实现扫描算法:
void scan_disk(struct request_queue *queue) {
// 初始化磁盘臂位置和方向
int direction = 1; // 1 表示向上扫描,-1 表示向下扫描
int head_position = 0;
while (!list_empty(&queue->queue)) {
struct request *rq;
// 获取队列头部的请求
rq = list_entry(queue->queue.next, struct request, queue);
// 根据磁盘臂当前位置和方向执行请求
if ((direction == 1 && rq->sector >= head_position) ||
(direction == -1 && rq->sector < head_position)) {
// 执行I/O操作
// ...
// 更新磁盘臂位置
head_position = rq->sector;
}
// 移动到下一个请求
list_del(&rq->queue);
kfree(rq);
}
}
通过上述代码块可以看出,在执行磁盘调度时,算法会根据当前磁头的位置和移动方向来选取下一个执行的I/O请求,以此来最小化总的寻道时间并提高效率。
以上所述,文件系统、设备管理机制以及I/O调度算法是操作系统中负责管理数据存储和访问的核心部分。随着技术的发展和需求的增长,这些部分不断演进,以适应更大容量、更快速度和更高可靠性的存储介质和设备。
5. 死锁预防与资源分配策略
死锁是多道程序运行时发生的一种特殊现象,它会导致系统资源无法释放,进而引起系统挂起,影响操作系统的稳定性和效率。为了理解和解决死锁问题,我们需要从死锁的概念和产生条件入手,进而深入探讨死锁的预防、避免及资源分配策略。
5.1 死锁的概念与产生条件
5.1.1 死锁的定义与特性
死锁,是指在两个或两个以上的进程在执行过程中,因为争夺资源而造成的一种僵局。其特点包括资源的互斥性、持有和等待、不可剥夺性以及循环等待。每个进程持有资源,并等待获取其他进程所占有的资源,造成进程间相互等待,无法向前推进。
5.1.2 死锁产生的四个必要条件
- 互斥条件:至少有一个资源必须处于非共享模式,即一次只有一个进程可以使用。如果其他进程请求该资源,则请求进程必须等待直到资源被释放。
- 占有和等待条件:一个进程至少持有一个资源,并且正在等待获取附加的资源,而该资源被其他进程占有。
- 不可剥夺条件:进程已获得的资源在未使用完之前,不能被强行剥夺,只能由占有资源的进程自愿释放。
- 循环等待条件:存在一种进程资源的循环等待链,每个进程都在等待下一个进程所占有的资源。
5.2 死锁预防与避免
5.2.1 死锁预防的基本方法
死锁预防主要是通过破坏死锁的四个必要条件中的一个或多个来实现。例如,通过资源的预分配可以预防持有和等待条件,资源预分配意味着进程在开始执行前必须获得所有必需的资源。这种方法可能会导致资源的低效利用,因为资源在某些情况下可能会长时间闲置。
5.2.2 银行家算法与死锁避免
银行家算法是一种避免死锁的著名算法。它通过模拟资源分配,提前检查资源请求后系统是否仍能保持安全状态,从而避免可能导致不安全状态的资源请求。银行家算法通过维护一个数据结构来跟踪每个进程的最大资源需求、已分配资源和可用资源,并通过一系列的检查来防止系统进入不安全状态。
5.3 资源分配策略
5.3.1 资源的分配与回收
资源分配策略决定了系统如何决定将资源分配给进程。最简单的方法是顺序分配,即按照请求的顺序依次分配资源。但这种方法不能防止死锁,通常需要结合其他策略,如银行家算法,以安全地分配资源。资源的回收过程也很重要,需要确保当进程不再需要资源时,资源能够被释放并可用于其他进程。
5.3.2 多种资源分配策略分析
在实际操作系统中,资源分配策略的选择依赖于特定的系统要求和预期的性能目标。常见的策略包括先请求先服务(FCFS)、最高优先级优先(P-SCAN)和最小等待时间优先等。每种策略都有其优势和劣势,并适用于不同的应用场景。例如,最高优先级优先策略可以优先为高优先级的进程分配资源,但可能导致低优先级进程饥饿。
graph LR
A[进程请求资源]
B[判断资源是否可分配]
C[资源分配]
D[进程使用资源]
E[资源释放]
A --> B
B --> |可分配| C --> D --> E --> A
B --> |不可分配| A
流程图说明: 死锁预防与资源分配策略流程图。当进程请求资源时,系统首先判断是否能够安全地分配资源。如果可以,则资源会被分配给进程,进程开始使用资源,并在完成时释放资源,之后返回等待其他进程请求。如果在某一点资源不可分配,则进程必须等待,系统尝试其他解决方案以避免死锁。
为了深入理解死锁预防和资源分配策略,在本章节中我们探讨了死锁的定义、产生条件,以及预防和避免死锁的基本方法。我们也了解了资源的分配和回收过程,以及多种资源分配策略的应用场景。通过以上内容,读者应能对操作系统中的死锁问题有较全面的认识,并能应用相关策略来提高操作系统的稳定性与效率。
简介:清华大学提供的操作系统课程资料,是一份包含理论与实践的全面集合,涉及操作系统的核心概念、关键技术、实验操作和权威读物。这些资料通过详尽的课堂笔记、实验指导、项目案例以及经典参考书籍的讲义,深入讲解了进程管理、内存管理、文件系统、设备管理、调度算法、死锁预防、资源分配策略、安全性与并发控制等主题。课程资料旨在通过实践加深理论知识的理解,并提升对操作系统底层机制的深入理解。