引言

操作系统(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维护访问顺序,新访问移到末尾。满时移除头部(最旧)。

常见问题探讨

进程管理常见问题

  1. 死锁(Deadlock):进程循环等待资源。四个必要条件:互斥、持有并等待、非抢占、循环等待。

    • 解决方案:银行家算法检查安全状态;资源有序分配破坏循环等待。
    • 例子:两个进程各持有一个资源并等待对方的。预防:所有进程同时请求所有资源。
  2. 饥饿(Starvation):低优先级进程长期得不到CPU。

    • 解决方案:使用老化(aging)动态提升优先级。
  3. 僵尸进程:子进程结束但父进程未回收。

    • 解决方案:父进程使用wait(),或init进程(PID 1)回收。

内存分配常见问题

  1. 碎片(Fragmentation):内部(固定分区)和外部(可变分区)碎片。

    • 解决方案:分页消除外部碎片;分页+分段减少内部碎片。定期压缩内存(compaction)合并空闲块。
  2. 抖动(Thrashing):页面频繁置换,CPU利用率低。

    • 原因:工作集过大,帧不足。
    • 解决方案:增加内存、优化程序局部性,或使用工作集模型调整分配。
  3. 内存泄漏:程序未释放动态分配内存。

    • 解决方案:使用智能指针(C++)或垃圾回收(Java)。代码审查和工具如Valgrind检测。

主题句:这些问题源于并发和资源有限性,通过算法优化和系统设计可缓解。

支持细节:在多核系统中,进程同步问题放大;虚拟内存需平衡磁盘I/O开销。实际调试使用工具如strace(跟踪系统调用)或gdb(调试)。

结论

进程管理和内存分配是操作系统的核心,确保系统高效、安全运行。通过理解生命周期、调度、同步、分页和置换,我们能构建可靠的软件。常见问题如死锁和碎片提醒我们设计时考虑边界情况。建议实践:编写多线程程序、模拟调度器,并使用OS工具监控。参考书籍如《操作系统概念》(Silberschatz)以深化知识。本文提供基础,欢迎进一步探讨具体场景。