引言
操作系统(Operating System, OS)作为计算机系统的核心软件,负责管理硬件资源并为应用程序提供运行环境。在现代操作系统中,进程管理和内存分配是两个至关重要的子系统。第三讲通常聚焦于进程管理,涵盖进程的创建、调度、同步与通信;第四讲则深入内存分配策略,包括虚拟内存、分页和分段机制。本文将结合这两讲内容,提供深入解析,帮助读者理解核心概念、机制及常见问题。我们将通过通俗易懂的语言、详细的解释和实际例子(包括代码示例)来阐述这些主题,确保内容逻辑清晰、支持细节丰富。
进程管理确保多个程序高效并发运行,而内存分配则解决有限物理内存与无限程序需求之间的矛盾。通过本文,您将掌握这些机制的原理,并学会诊断常见问题。文章结构如下:首先探讨进程管理,其次分析内存分配策略,最后讨论常见问题及解决方案。
第三讲:进程管理深入解析
进程的基本概念与生命周期
进程是操作系统中资源分配和调度的基本单位。它不仅仅是一个运行中的程序,还包括程序计数器、寄存器状态、堆栈和打开的文件等执行上下文。进程的生命周期包括创建、就绪、运行、阻塞和终止五个状态。这些状态通过进程控制块(Process Control Block, PCB)来管理,PCB是操作系统内核维护的结构体,记录进程的PID(进程ID)、优先级、状态等信息。
主题句:进程的创建和终止是进程管理的基础,操作系统通过系统调用(如fork()和exit())实现这些操作。
支持细节:在Unix-like系统中,进程创建使用fork()系统调用,它复制当前进程创建子进程。子进程继承父进程的资源,但拥有独立的PID。终止则通过exit()释放资源,并通知父进程回收。
代码示例:以下是一个简单的C语言程序,演示fork()的使用。编译并运行此代码,您将看到父进程和子进程的输出。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
// fork失败
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程 (PID: %d) 正在运行。\n", getpid());
sleep(2); // 模拟工作
printf("子进程结束。\n");
exit(0);
} else {
// 父进程
printf("父进程 (PID: %d) 创建了子进程 (PID: %d)。\n", getpid(), pid);
wait(NULL); // 等待子进程结束
printf("父进程回收了子进程资源。\n");
}
return 0;
}
解释:fork()返回两次:在父进程中返回子进程PID,在子进程中返回0。如果fork失败,返回-1。wait()确保父进程等待子进程,避免僵尸进程(zombie process)。
进程调度算法
进程调度决定哪个就绪进程获得CPU时间。常见算法包括先来先服务(FCFS)、短作业优先(SJF)、轮转调度(Round Robin, RR)和多级反馈队列(Multilevel Feedback Queue, MLFQ)。
主题句:调度算法的目标是最大化CPU利用率、最小化响应时间和周转时间,同时保证公平性。
支持细节:FCFS简单但可能导致长作业阻塞短作业;SJF优化平均等待时间,但需预知作业长度;RR通过时间片轮转实现交互式系统的公平性;MLFQ结合多种优先级,动态调整进程优先级。
实际例子:假设三个进程:P1(到达时间0,执行时间10)、P2(到达时间1,执行时间1)、P3(到达时间2,执行时间2)。在FCFS下,平均等待时间为(0 + 9 + 10)/3 ≈ 6.33;在SJF下(非抢占),为(0 + 1 + 3)/3 ≈ 1.33;在RR(时间片=4)下,Gantt图如下:
- 时间0-4: P1运行
- 时间4-5: P2运行(完成)
- 时间5-7: P3运行(完成)
- 时间7-10: P1剩余运行
平均等待时间计算:P1等待0+1=1,P2等待3,P3等待3,平均≈2.33。
在代码中,我们可以模拟RR调度。以下是一个简化的Python实现,用于计算调度结果(假设单CPU)。
import collections
def round_robin(processes, time_quantum):
queue = collections.deque(processes)
current_time = 0
gantt = []
waiting_times = {}
while queue:
process = queue.popleft()
burst = process['burst']
if burst > time_quantum:
current_time += time_quantum
gantt.append((process['id'], current_time - time_quantum, current_time))
process['burst'] = burst - time_quantum
queue.append(process)
else:
current_time += burst
gantt.append((process['id'], current_time - burst, current_time))
waiting_times[process['id']] = current_time - process['arrival'] - process['original_burst']
avg_wait = sum(waiting_times.values()) / len(waiting_times)
return gantt, avg_wait
# 示例进程:id, arrival, burst, original_burst
processes = [
{'id': 'P1', 'arrival': 0, 'burst': 10, 'original_burst': 10},
{'id': 'P2', 'arrival': 1, 'burst': 1, 'original_burst': 1},
{'id': 'P3', 'arrival': 2, 'burst': 2, 'original_burst': 2}
]
gantt, avg_wait = round_robin(processes, 4)
print("Gantt图:", gantt)
print("平均等待时间:", avg_wait)
输出示例:Gantt图: [(‘P1’, 0, 4), (‘P2’, 4, 5), (‘P3’, 5, 7), (‘P1’, 7, 10)];平均等待时间: 2.33。
进程同步与通信
并发进程共享资源时需同步,避免竞态条件(race condition)。常见机制包括信号量(semaphore)、互斥锁(mutex)和管程(monitor)。
主题句:进程通信(IPC)允许进程交换数据,包括管道、消息队列、共享内存和套接字。
支持细节:信号量用于控制访问共享资源,如生产者-消费者问题。生产者向缓冲区添加数据,消费者移除数据,使用信号量确保互斥和同步。
代码示例:使用POSIX信号量解决生产者-消费者问题(C语言)。需链接-lpthread。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define BUFFER_SIZE 5
sem_t mutex, empty, full;
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
void* producer(void* arg) {
int item;
for (int i = 0; i < 10; i++) {
item = i; // 生产项目
sem_wait(&empty); // 等待空槽
sem_wait(&mutex); // 互斥访问缓冲区
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
printf("生产者: 添加 %d\n", item);
sem_post(&mutex);
sem_post(&full); // 增加满槽
sleep(1);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
sem_wait(&full); // 等待满槽
sem_wait(&mutex);
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
printf("消费者: 移除 %d\n", item);
sem_post(&mutex);
sem_post(&empty); // 增加空槽
sleep(2);
}
return NULL;
}
int main() {
sem_init(&mutex, 0, 1); // 互斥锁初始为1
sem_init(&empty, 0, BUFFER_SIZE); // 空槽初始为缓冲区大小
sem_init(&full, 0, 0); // 满槽初始为0
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
sem_destroy(&mutex);
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}
解释:empty信号量跟踪空槽,full跟踪满槽,mutex确保缓冲区互斥访问。生产者等待empty和mutex,添加后释放full;消费者反之。这防止缓冲区溢出或下溢。
第四讲:内存分配策略深入解析
内存管理基础与连续分配
操作系统管理物理内存,确保进程隔离和高效利用。连续分配策略如固定分区和可变分区,将内存划分为连续块。
主题句:固定分区将内存预先划分,导致内部碎片;可变分区动态分配,但产生外部碎片。
支持细节:固定分区大小固定,适合简单系统,但浪费空间。可变分区使用首次适应(First Fit)、最佳适应(Best Fit)或最坏适应(Worst Fit)算法分配空闲块。
实际例子:假设内存100KB,进程A(20KB)、B(30KB)、C(15KB)。首次适应:分配A到0-20,B到20-50,C到50-65。释放B后,空闲20-30和50-100,外部碎片为20KB(无法合并的空闲块)。
在代码中,我们可以模拟首次适应分配。以下是一个简化的Python类。
class MemoryAllocator:
def __init__(self, total_size):
self.total_size = total_size
self.free_blocks = [(0, total_size)] # (start, size)
self.allocated = {}
def allocate(self, process_id, size):
for i, (start, block_size) in enumerate(self.free_blocks):
if block_size >= size:
allocated_start = start
allocated_end = start + size
self.allocated[process_id] = (allocated_start, allocated_end)
# 更新空闲块
if block_size == size:
self.free_blocks.pop(i)
else:
self.free_blocks[i] = (allocated_end, block_size - size)
return allocated_start
return None # 分配失败
def deallocate(self, process_id):
if process_id in self.allocated:
start, end = self.allocated.pop(process_id)
# 简单合并:实际需检查相邻
self.free_blocks.append((start, end - start))
self.free_blocks.sort() # 按起始地址排序
# 示例
allocator = MemoryAllocator(100)
print(allocator.allocate('A', 20)) # 输出: 0
print(allocator.allocate('B', 30)) # 输出: 20
print(allocator.allocate('C', 15)) # 输出: 50
allocator.deallocate('B')
print("空闲块:", allocator.free_blocks) # [(20, 30), (65, 35)]
解释:分配成功返回起始地址,失败返回None。deallocate添加空闲块,但实际需合并相邻块以减少碎片。
分页与分段
现代OS使用非连续分配:分页将内存和进程划分为固定大小页(如4KB),通过页表映射逻辑地址到物理地址。分段则按逻辑单位(如代码段、数据段)划分,支持动态增长。
主题句:分页解决外部碎片,但有内部碎片;分段提供保护,但有外部碎片。两者结合为段页式。
支持细节:逻辑地址=页号+页内偏移。页表项包括物理帧号、有效位、修改位等。TLB(Translation Lookaside Buffer)缓存页表项加速访问。
实际例子:假设页大小4KB,进程逻辑地址0x1234(页号0,偏移0x234)。页表映射页0到物理帧5,则物理地址=5*4096 + 0x234 = 20734。
分段例子:进程有代码段(基址0x1000,限长0x500)和数据段(基址0x2000,限长0x300)。逻辑地址(段号1,偏移0x100)检查限长后映射到物理0x2100。
代码示例:模拟分页地址转换(Python)。
class PagingSystem:
def __init__(self, page_size=4096):
self.page_size = page_size
self.page_table = {} # 页号 -> 物理帧号
self.next_frame = 0
def map_page(self, page_num):
if page_num not in self.page_table:
self.page_table[page_num] = self.next_frame
self.next_frame += 1
return self.page_table[page_num]
def translate(self, logical_addr):
page_num = logical_addr // self.page_size
offset = logical_addr % self.page_size
if page_num not in self.page_table:
raise ValueError("Page fault: 页未映射")
frame = self.map_page(page_num)
physical_addr = frame * self.page_size + offset
return physical_addr
# 示例
paging = PagingSystem()
logical = 0x1234 # 4660 十进制
physical = paging.translate(logical)
print(f"逻辑地址: {logical}, 物理地址: {physical}") # 页0 -> 帧0, 偏移4660, 物理=4660
解释:translate计算页号和偏移,映射到物理地址。实际系统中,页表可能多级(如两级页表)以节省空间。
虚拟内存与页面置换
虚拟内存允许进程使用比物理内存更大的地址空间,通过页面置换将不活跃页换出到磁盘。
主题句:页面置换算法如FIFO(先进先出)、LRU(最近最少使用)和Clock算法决定换出哪页。
支持细节:FIFO简单但有Belady异常(增加帧数反而增加缺页率);LRU近似最优,但实现复杂(需硬件支持);Clock是LRU的近似,使用引用位。
实际例子:参考串3,1,4,1,5,2,帧数3。FIFO:缺页率5/6;LRU:缺页率4/6。
代码示例:模拟LRU置换(使用Python的OrderedDict)。
from collections import OrderedDict
class LRUPageReplacement:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
self.page_faults = 0
def access(self, page):
if page in self.cache:
self.cache.move_to_end(page)
else:
if len(self.cache) >= self.capacity:
self.cache.popitem(last=False) # 移除最旧
self.cache[page] = True
self.page_faults += 1
# 示例
lru = LRUPageReplacement(3)
pages = [3, 1, 4, 1, 5, 2]
for p in pages:
lru.access(p)
print(f"缺页次数: {lru.page_faults}") # 4
解释:OrderedDict维护访问顺序,新访问移到末尾。满时移除头部(最旧)。
常见问题探讨
进程管理常见问题
死锁(Deadlock):进程循环等待资源。四个必要条件:互斥、持有并等待、非抢占、循环等待。
- 解决方案:银行家算法检查安全状态;资源有序分配破坏循环等待。
- 例子:两个进程各持有一个资源并等待对方的。预防:所有进程同时请求所有资源。
饥饿(Starvation):低优先级进程长期得不到CPU。
- 解决方案:使用老化(aging)动态提升优先级。
僵尸进程:子进程结束但父进程未回收。
- 解决方案:父进程使用wait(),或init进程(PID 1)回收。
内存分配常见问题
碎片(Fragmentation):内部(固定分区)和外部(可变分区)碎片。
- 解决方案:分页消除外部碎片;分页+分段减少内部碎片。定期压缩内存(compaction)合并空闲块。
抖动(Thrashing):页面频繁置换,CPU利用率低。
- 原因:工作集过大,帧不足。
- 解决方案:增加内存、优化程序局部性,或使用工作集模型调整分配。
内存泄漏:程序未释放动态分配内存。
- 解决方案:使用智能指针(C++)或垃圾回收(Java)。代码审查和工具如Valgrind检测。
主题句:这些问题源于并发和资源有限性,通过算法优化和系统设计可缓解。
支持细节:在多核系统中,进程同步问题放大;虚拟内存需平衡磁盘I/O开销。实际调试使用工具如strace(跟踪系统调用)或gdb(调试)。
结论
进程管理和内存分配是操作系统的核心,确保系统高效、安全运行。通过理解生命周期、调度、同步、分页和置换,我们能构建可靠的软件。常见问题如死锁和碎片提醒我们设计时考虑边界情况。建议实践:编写多线程程序、模拟调度器,并使用OS工具监控。参考书籍如《操作系统概念》(Silberschatz)以深化知识。本文提供基础,欢迎进一步探讨具体场景。
