摘要:
本文记录了在 Ubuntu Linux 环境下,使用 GCC 和 POSIX 线程库(pthread)解决经典并发编程问题——“生产者 - 消费者问题”的全过程。文章详细分析了信号量(Semaphore)与互斥锁(Mutex)的配合机制,并提供了完整的可运行代码及编译调试步骤。对于正在学习操作系统课程的同学,这是一份实用的参考指南。


一、前言

最近在进行操作系统的课程设计,核心任务是理解进程 / 线程间的同步与互斥机制。经典的“生产者 - 消费者问题”是理解这一概念的基石。

我的开发环境是在 Windows 宿主机上通过 VMware Workstation 运行的 Ubuntu 64 位 虚拟机。这种环境既能保证系统的安全性,又能提供原汁原味的 Linux 命令行体验,非常适合进行底层系统编程的学习。

二、问题分析

在这个实验中,我们需要模拟多个生产者和多个消费者共享一个固定大小的缓冲区:

  • 生产者(Producer):负责生产数据并放入缓冲区。如果缓冲区满了,必须等待。
  • 消费者(Consumer):负责从缓冲区取出数据。如果缓冲区空了,必须等待。
  • 互斥访问:无论生产还是消费,同一时刻只能有一个线程操作缓冲区,以防止数据错乱。

为了实现上述逻辑,我们需要三个关键的同步工具:

  1. mutex(互斥锁):保护缓冲区的互斥访问。
  2. empty(信号量):记录空闲缓冲区的数量,初始值为缓冲区总大小 $M$。
  3. full(信号量):记录已填充数据的缓冲区数量,初始值为 $0$。
三、核心代码实现

以下是我在 Ubuntu 终端中编写并验证通过的完整代码(producer_consumer.c)。代码使用了 <semaphore.h><pthread.h> 库。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define BUFFER_SIZE 5   // 缓冲区大小 M
#define PRODUCER_NUM 2  // 生产者数量
#define CONSUMER_NUM 2  // 消费者数量

int buffer[BUFFER_SIZE];
int in = 0; // 生产者写入位置
int out = 0; // 消费者读取位置

// 定义信号量和互斥锁
sem_t empty_sem;
sem_t full_sem;
pthread_mutex_t mutex;

// 生产者函数
void *producer(void *arg) {
    int id = *(int *)arg;
    while (1) {
        sleep(1); // 模拟生产耗时
        sem_wait(&empty_sem);       // P(empty): 申请空位
        pthread_mutex_lock(&mutex); // Lock: 进入临界区

        // --- 写入数据 ---
        buffer[in] = 1;
        printf("[生产者 %d] 放入数据到位置 %d\n", id, in);
        in = (in + 1) % BUFFER_SIZE;

        pthread_mutex_unlock(&mutex); // Unlock: 离开临界区
        sem_post(&full_sem);          // V(full): 增加满位计数
    }
    return NULL;
}

// 消费者函数
void *consumer(void *arg) {
    int id = *(int *)arg;
    while (1) {
        sleep(2); // 模拟消费耗时
        sem_wait(&full_sem);        // P(full): 申请数据
        pthread_mutex_lock(&mutex); // Lock: 进入临界区

        // --- 读取数据 ---
        printf("[消费者 %d] 从位置 %d 取出数据\n", id, out);
        buffer[out] = 0;
        out = (out + 1) % BUFFER_SIZE;

        pthread_mutex_unlock(&mutex); // Unlock: 离开临界区
        sem_post(&empty_sem);         // V(empty): 增加空位计数
    }
    return NULL;
}

int main() {
    pthread_t prod_threads[PRODUCER_NUM], cons_threads[CONSUMER_NUM];
    int prod_ids[PRODUCER_NUM], cons_ids[CONSUMER_NUM];

    // 初始化
    sem_init(&empty_sem, 0, BUFFER_SIZE);
    sem_init(&full_sem, 0, 0);
    pthread_mutex_init(&mutex, NULL);

    // 创建线程
    for (int i = 0; i < PRODUCER_NUM; i++) {
        prod_ids[i] = i;
        pthread_create(&prod_threads[i], NULL, producer, &prod_ids[i]);
    }
    for (int i = 0; i < CONSUMER_NUM; i++) {
        cons_ids[i] = i;
        pthread_create(&cons_threads[i], NULL, consumer, &cons_ids[i]);
    }

    // 等待线程结束(此处为死循环演示,实际需配合退出逻辑)
    for (int i = 0; i < PRODUCER_NUM; i++) pthread_join(prod_threads[i], NULL);
    for (int i = 0; i < CONSUMER_NUM; i++) pthread_join(cons_threads[i], NULL);

    // 销毁资源
    sem_destroy(&empty_sem);
    sem_destroy(&full_sem);
    pthread_mutex_destroy(&mutex);

    return 0;
}
四、编译与运行指南

在 Ubuntu 虚拟机中,请按照以下步骤操作:

  1. 保存文件:将上述代码保存为 producer_consumer.c

  2. 编译命令:注意需要链接 pthread 库。

    gcc producer_consumer.c -o pc_demo -lpthread
    
  3. 运行程序

    ./pc_demo
    
  4. 观察输出:你会看到生产者和消费者交替打印日志,且不会出现缓冲区溢出或读取空数据的情况。按 Ctrl+C 终止程序。

五、实验心得与注意事项
  1. P/V 操作顺序至关重要:在生产者中,必须先 wait(empty)lock(mutex);在消费者中,必须先 wait(full)lock(mutex)。如果顺序反了(先锁后等信号量),极易导致死锁
  2. 编译报错处理:如果在 Ubuntu 上编译提示找不到 semaphore.h,请确保安装了 libc6-dev 包(通常默认已安装)。
  3. 虚拟机的优势:如果在实验中不小心写错了代码导致系统卡死,直接关闭 VMware 虚拟机重启即可,完全不会影响宿主机的 Windows 系统,这大大降低了试错成本。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐