第6章多进程并发控制:互斥机制与同步协调技术详解

第6章 多进程并发控制:互斥机制与同步协调技术详解

在多任务操作系统中,并发处理是一个核心挑战。当多个进程或线程同时访问共享资源时,如何协调它们的行为以确保系统的正确性和效率是操作系统设计的关键问题。本章将深入探讨并发处理的基本原理、互斥与同步机制,以及它们在操作系统中的实现方法。

6.1 并发处理的基础理论

并发是指多个计算任务在重叠的时间段内进行。在操作系统中,这些任务可能是进程或线程。当这些并发任务需要访问共享资源时,就需要适当的控制机制来确保数据的一致性和完整性。

6.1.1 并发问题示例分析

为了理解并发处理的挑战,让我们从一个简单的例子开始。考虑两个进程同时访问一个共享计数器的情况:

c

// 共享计数器示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 共享变量
int counter = 0;

// 线程函数
void* increment_counter(void* arg) {
    int i;
    int local_id = *((int*)arg);
    
    for (i = 0; i < 100000; i++) {
        // 读取 - 修改 - 写入操作序列
        int temp = counter;      // 读取
        temp = temp + 1;         // 修改
        counter = temp;          // 写入
    }
    
    printf("线程 %d 完成,计数器值 = %d\n", local_id, counter);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    int id1 = 1, id2 = 2;
    
    printf("初始计数器值 = %d\n", counter);
    
    // 创建两个线程
    pthread_create(&thread1, NULL, increment_counter, &id1);
    pthread_create(&thread2, NULL, increment_counter, &id2);
    
    // 等待线程完成
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    printf("预期计数器值 = %d\n", 200000);
    printf("实际计数器值 = %d\n", counter);
    
    return 0;
}

当运行这个程序时,你会发现最终的计数器值通常小于预期的200000。这是因为线程1和线程2可能同时读取counter,然后各自递增,最后写回相同的值,导致一些递增操作被"丢失"。

这个简单的例子展示了并发程序中的一个基本问题:当多个线程同时访问共享资源时,如果没有适当的同步机制,可能导致不一致和错误的结果。

6.1.2 竞态条件的本质

上述例子中的问题被称为竞态条件(Race Condition)。竞态条件是指多个进程或线程的执行结果依赖于它们执行的相对时序,而这个时序是不可预测的。

竞态条件的关键特征:

  • 多个进程或线程并发执行
  • 它们访问相同的共享资源
  • 至少有一个进程/线程在修改资源
  • 操作的最终结果依赖于进程/线程执行的顺序

c

// 竞态条件的详细示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 共享状态
typedef struct {
    int balance;  // 银行账户余额
    int transactions;  // 交易次数
} Account;

Account account = {1000, 0};  // 初始余额1000元,0次交易

// 存款操作
void* deposit(void* arg) {
    int amount = *((int*)arg);
    
    printf("存款线程: 开始存款 %d 元\n", amount);
    
    // 读取当前余额
    int temp_balance = account.balance;
    printf("存款线程: 读取当前余额 %d 元\n", temp_balance);
    
    // 模拟处理延时
    usleep(rand() % 100000);
    
    // 更新余额
    temp_balance += amount;
    printf("存款线程: 计算新余额 %d 元\n", temp_balance);
    
    // 模拟处理延时
    usleep(rand() % 100000);
    
    // 写回余额
    account.balance = temp_balance;
    printf("存款线程: 更新后余额 %d 元\n", account.balance);
    
    // 更新交易次数
    account.transactions++;
    
    return NULL;
}

// 取款操作
void* withdraw(void* arg) {
    int amount = *((int*)arg);
    
    printf("取款线程: 开始取款 %d 元\n", amount);
    
    // 读取当前余额
    int temp_balance = account.balance;
    printf("取款线程: 读取当前余额 %d 元\n", temp_balance);
    
    // 模拟处理延时
    usleep(rand() % 100000);
    
    // 更新余额
    temp_balance -= amount;
    printf("取款线程: 计算新余额 %d 元\n", temp_balance);
    
    // 模拟处理延时
    usleep(rand() % 100000);
    
    // 写回余额
    account.balance = temp_balance;
    printf("取款线程: 更新后余额 %d 元\n", account.balance);
    
    // 更新交易次数
    account.transactions++;
    
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    int deposit_amount = 500;
    int withdraw_amount = 300;
    
    // 设置随机种子
    srand(time(NULL));
    
    printf("初始账户状态: 余额 %d 元, %d 次交易\n", 
           account.balance, account.transactions);
    
    // 创建存款和取款线程
    pthread_create(&tid1, NULL, deposit, &deposit_amount);
    pthread_create(&tid2, NULL, withdraw, &withdraw_amount);
    
    // 等待线程完成
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    // 打印最终状态
    printf("预期账户状态: 余额 %d 元, %d 次交易\n", 
           1000 + deposit_amount - withdraw_amount, 2);
    printf("实际账户状态: 余额 %d 元, %d 次交易\n", 
           account.balance, account.transactions);
    
    return 0;
}

这个银行账户示例清晰地展示了竞态条件。如果存款线程和取款线程的操作交叉执行,最终账户余额可能不是预期的1200元,而是某个不正确的值。

6.1.3 操作系统层面的关键问题

并发问题不仅存在于用户应用程序中,也是操作系统内核需要解决的核心挑战。操作系统中的并发问题包括:

  1. 中断处理:中断处理程序可能会访问与内核共享的数据结构
  2. 系统调用:多个进程同时进行系统调用可能会导致竞态条件
  3. 内核数据结构:如进程表、内存分配表、I/O缓冲区等需要保护
  4. 设备驱动:多个进程可能同时请求设备访问

操作系统必须提供有效的机制来解决这些并发问题,保证系统的稳定性和正确性。

c

// 模拟操作系统内核中的并发问题
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 模拟操作系统内核数据结构
typedef struct {
    int free_pages;        // 空闲页面数
    int allocated_pages;   // 已分配页面数
} MemoryManager;

// 全局内存管理器
MemoryManager mem_manager = {100, 0};  // 初始100个空闲页面

// 模拟页面分配系统调用
void* allocate_pages(void* arg) {
    int num_pages = *((int*)arg);
    int thread_id = *((int*)(arg) + 1);
    
    printf("线程 %d: 请求分配 %d 个页面\n", thread_id, num_pages);
    
    // 检查是否有足够的空闲页面
    if (mem_manager.free_pages >= num_pages) {
        // 读取当前空闲页面数
        int temp_free = mem_manager.free_pages;
        int temp_allocated = mem_manager.allocated_pages;
        
        // 模拟处理延时
        usleep(rand() % 50000);
        
        // 更新页面计数
        temp_free -= num_pages;
        temp_allocated += num_pages;
        
        // 模拟处理延时
        usleep(rand() % 50000);
        
        // 写回更新的计数
        mem_manager.free_pages = temp_free;
        mem_manager.allocated_pages = temp_allocated;
        
        printf("线程 %d: 成功分配 %d 个页面, 剩余空闲页面: %d\n", 
               thread_id, num_pages, mem_manager.free_pages);
        return (void*)1;  // 成功
    } else {
        printf("线程 %d: 分配失败,空闲页面不足\n", thread_id);
        return (void*)0;  // 失败
    }
}

// 模拟页面释放系统调用
void* free_pages(void* arg) {
    int num_pages = *((int*)arg);
    int thread_id = *((int*)(arg) + 1);
    
    printf("线程 %d: 请求释放 %d 个页面\n", thread_id, num_pages);
    
    // 读取当前页面计数
    int temp_free = mem_manager.free_pages;
    int temp_allocated = mem_manager.allocated_pages;
    
    // 模拟处理延时
    usleep(rand() % 50000);
    
    // 更新页面计数
    temp_free += num_pages;
    temp_allocated -= num_pages;
    
    // 模拟处理延时
    usleep(rand() % 50000);
    
    // 写回更新的计数
    mem_manager.free_pages = temp_free;
    mem_manager.allocated_pages = temp_allocated;
    
    printf("线程 %d: 成功释放 %d 个页面, 当前空闲页面: %d\n", 
           thread_id, num_pages, mem_manager.free_pages);
    return (void*)1;  // 成功
}

int main() {
    pthread_t threads[4];
    int args[4][2] = {
        {30, 1},  // 线程1分配30页
        {50, 2},  // 线程2分配50页
        {40, 3},  // 线程3释放40页
        {20, 4}   // 线程4分配20页
    };
    
    // 设置随机种子
    srand(time(NULL));
    
    printf("初始内存状态: 空闲页面 = %d, 已分配页面 = %d\n", 
           mem_manager.free_pages, mem_manager.allocated_pages);
    
    // 创建线程模拟并发系统调用
    pthread_create(&threads[0], NULL, allocate_pages, &args[0]);
    pthread_create(&threads[1], NULL, allocate_pages, &args[1]);
    pthread_create(&threads[2], NULL, free_pages, &args[2]);
    pthread_create(&threads[3], NULL, allocate_pages, &args[3]);
    
    // 等待所有线程完成
    for (int i = 0; i < 4; i++) {
        pthread_join(threads[i], NULL);
    }
    
    // 检查最终内存状态
    printf("最终内存状态: 空闲页面 = %d, 已分配页面 = %d\n", 
           mem_manager.free_pages, mem_manager.allocated_pages);
    printf("总页面数 = %d (应为100)\n", 
           mem_manager.free_pages + mem_manager.allocated_pages);
    
    // 验证计数是否一致
    if (mem_manager.free_pages + mem_manager.allocated_pages != 100) {
        printf("错误: 内存管理器状态不一致!\n");
    }
    
    return 0;
}

这个例子模拟了操作系统内核中的内存管理器。当多个线程同时进行页面分配和释放时,如果没有适当的同步机制,可能导致内存管理器的状态不一致,从而影响整个系统的稳定性。

6.1.4 进程间交互的模式

在多进程环境中,进程之间的交互可以分为多种模式:

  1. 竞争关系:多个进程竞争使用共享资源,如打印机、内存、CPU等。
  2. 协作关系:进程之间通过消息传递或共享内存等方式合作完成任务。
  3. 生产者-消费者关系:一个进程生产数据,另一个进程消费数据。
  4. 读者-写者关系:多个进程同时读取数据,而写入操作需要独占访问。

不同的交互模式需要不同的同步策略。下面是一个简单的进程交互示例:

c

// 进程交互示例:共享内存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>

#define SHM_SIZE 1024

int main() {
    int shmid;
    key_t key;
    char *shm, *s;
    
    // 生成一个key
    key = ftok(".", 'x');
    
    // 创建共享内存段
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        exit(1);
    }
    
    // 附加共享内存段
    if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {
        perror("shmat");
        exit(1);
    }
    
    // 创建子进程
    pid_t pid = fork();
    
    if (pid < 0) {
        perror("fork");
        exit(1);
    }
    
    if (pid == 0) {
        // 子进程(生产者)
        printf("子进程(生产者)开始运行\n");
        
        // 向共享内存写入数据
        strcpy(shm, "Hello from child process!");
        printf("子进程写入: %s\n", shm);
        
        exit(0);
    } else {
        // 父进程(消费者)
        printf("父进程(消费者)开始运行\n");
        
        // 等待子进程完成
        wait(NULL);
        
        // 从共享内存读取数据
        printf("父进程读取: %s\n", shm);
        
        // 删除共享内存段
        shmdt(shm);
        shmctl(shmid, IPC_RMID, NULL);
    }
    
    return 0;
}

这个例子展示了父子进程通过共享内存进行交互的基本方式。在实际系统中,进程间的交互通常更加复杂,需要更精细的同步机制。

6.1.5 互斥的基本需求

互斥(Mutual Exclusion)是解决竞态条件的基本需求,它确保在任何时刻,只有一个进程或线程可以访问共享资源或关键代码段(临界区)。

一个有效的互斥解决方案应满足以下条件:

  1. 互斥性:在任何时刻,只有一个进程可以在临界区内执行。
  2. 进展性:如果没有进程在临界区内,且有进程想要进入临界区,那么只有那些不在剩余代码部分(即非临界区)的进程才能参与选择下一个进入临界区的进程,且这个选择不能无限推迟。
  3. 有限等待:从一个进程发出进入临界区请求,到该请求得到允许,其间允许其他进程进入临界区的次数有上限。
  4. 无忙等待(可选):进程不应该在等待进入临界区时消耗CPU资源。

c

// 互斥需求的结构化表示
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

// 共享资源
int shared_resource = 0;

// 互斥锁(用于保护共享资源)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 使用互斥锁的线程函数
void* safe_thread(void* arg) {
    int thread_id = *((int*)arg);
    int i;
    
    for (i = 0; i < 100000; i++) {
        // 进入临界区前获取锁
        pthread_mutex_lock(&mutex);
        
        // 临界区 - 访问共享资源
        shared_resource++;
        
        // 离开临界区,释放锁
        pthread_mutex_unlock(&mutex);
    }
    
    printf("安全线程 %d 完成,共享资源值 = %d\n", thread_id, shared_resource);
    return NULL;
}

// 不使用互斥锁的线程函数
void* unsafe_thread(void* arg) {
    int thread_id = *((int*)arg);
    int i;
    
    for (i = 0; i < 100000; i++) {
        // 直接访问共享资源,没有互斥保护
        shared_resource++;
    }
    
    printf("不安全线程 %d 完成,共享资源值 = %d\n", thread_id, shared_resource);
    return NULL;
}

int main() {
    pthread_t threads[4];
    int thread_ids[4] = {1, 2, 3, 4};
    
    printf("初始共享资源值 = %d\n", shared_resource);
    
    // 测试1: 使用互斥锁
    printf("\n--- 使用互斥锁的测试 ---\n");
    shared_resource = 0;  // 重置共享资源
    
    // 创建使用互斥锁的线程
    pthread_create(&threads[0], NULL, safe_thread, &thread_ids[0]);
    pthread_create(&threads[1], NULL, safe_thread, &thread_ids[1]);
    
    // 等待线程完成
    pthread_join(threads[0], NULL);
    pthread_join(threads[1], NULL);
    
    printf("使用互斥锁的最终结果 = %d (预期: 200000)\n", shared_resource);
    
    // 测试2: 不使用互斥锁
    printf("\n--- 不使用互斥锁的测试 ---\n");
    shared_resource = 0;  // 重置共享资源
    
    // 创建不使用互斥锁的线程
    pthread_create(&threads[2], NULL, unsafe_thread, &thread_ids[2]);
    pthread_create(&threads[3], NULL, unsafe_thread, &thread_ids[3]);
    
    // 等待线程完成
    pthread_join(threads[2], NULL);
    pthread_join(threads[3], NULL);
    
    printf("不使用互斥锁的最终结果 = %d (预期: 200000)\n", shared_resource);
    
    // 清理资源
    pthread_mutex_destroy(&mutex);
    
    return 0;
}

这个例子对比了使用互斥锁和不使用互斥锁时对共享资源访问的差异。使用互斥锁可以确保正确的结果,而不使用互斥锁则可能导致不一致的结果。

6.2 互斥实现:硬件支持机制

操作系统通常依赖硬件提供的特殊机制来实现高效的互斥操作。这些硬件支持包括中断禁用和原子操作指令。

6.2.1 中断禁用技术

在单处理器系统中,最简单的互斥实现方法是通过禁用中断。进程在进入临界区之前禁用中断,离开时重新启用中断。这样可以防止上下文切换,确保临界区的原子执行。

c

// 中断禁用的概念性示例(伪代码)
void enter_critical_section() {
    // 禁用中断
    disable_interrupts();
}

void exit_critical_section() {
    // 重新启用中断
    enable_interrupts();
}

void process_function() {
    // 非临界区代码
    
    enter_critical_section();
    // 临界区代码 - 访问共享资源
    exit_critical_section();
    
    // 非临界区代码
}

中断禁用方法的优缺点:

  • 优点:简单,适用于内核代码
  • 缺点
    • 只适用于单处理器系统
    • 禁用中断会影响系统响应性
    • 不适合用户程序(需要特权指令)
    • 临界区执行时间过长会导致中断响应延迟

在现代多处理器系统中,中断禁用并不能提供完全的互斥,因为其他处理器上的线程仍然可以并发访问共享资源。

6.2.2 原子指令实现互斥

为了支持更高效的互斥操作,现代处理器提供了特殊的原子指令,如:

  1. 测试并设置(Test-and-Set):原子地读取一个内存位置的值并将其设置为1
  2. 交换(Swap):原子地交换两个内存位置的值
  3. 比较并交换(Compare-and-Swap, CAS):原子地比较内存值与期望值,如果相等则更新为新值
  4. 获取并增加(Fetch-and-Add):原子地读取一个值并增加它

这些指令可以用来实现各种互斥机制,如自旋锁、互斥锁等。

c

// 使用原子操作实现自旋锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdbool.h>
#include <unistd.h>

// 自旋锁变量
volatile int spinlock = 0;

// 使用GCC内置原子操作函数
bool test_and_set(volatile int *lock) {
    return __sync_lock_test_and_set(lock, 1);
}

void spin_lock_release(volatile int *lock) {
    __sync_lock_release(lock);
}

// 获取自旋锁
void acquire_spinlock() {
    // 自旋等待直到获取锁
    while (test_and_set(&spinlock)) {
        // 自旋等待(忙等待)
    }
}

// 释放自旋锁
void release_spinlock() {
    spin_lock_release(&spinlock);
}

// 共享计数器
int counter = 0;

// 线程函数
void* thread_function(void* arg) {
    int thread_id = *((int*)arg);
    int i;
    
    for (i = 0; i < 1000000; i++) {
        // 获取锁
        acquire_spinlock();
        
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小宝哥Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值