2021-11-02 操作系统实验3——生产者消费者实验

一、实验目的

了解和熟悉linux系统下的信号量集和共享内存。

二、实验任务

使用linux系统提供的信号量集和共享内存实现生产者和消费者问题。

三、实验要求

  1. 写两个程序,一个模拟生产者过程,一个模拟消费者过程;
  2. 创建一个共享内存模拟生产者-消费者问题中缓冲队列,该缓冲队列有N(例如N=10)个缓冲区,每个缓冲区的大小为1024B,每个生产者和消费者对缓冲区必须互斥访问;
  3. 由第一个生产者创建信号量集和共享内存,其他生产者和消费者可以使用该信号量集和共享内存;
  4. 生产者程序:生产者生产产品(即是从键盘输入长度小于1024B的字符)放入空的缓冲区;
  5. 消费者程序:消费者消费产品(即从满的缓冲区中取出内容在屏幕上打印出来),然后满的缓冲区变为空的缓冲区;
  6. 多次运行生产者程序和消费者进程,可以产生多个生产者进程和多个消费者进程,这些进程可以共享这些信号量和共享内存,实现生产者和消费者问题;
  7. 在生产者程序程序中,可以选择:
    (1)生产产品;
    (2)退出。退出进程,但信号量和共享内存仍然存在,其他生产者进程和消费者进程还可以继续使用;
    (3)删除信号量和共享内存。显性删除信号量和共享内存,其他生产者进程和消费者进程都不能使用这些信号量和共享内存。
  8. 在消费者程序中,可以选择:
    (1)消费产品;
    (2)退出。退出进程,但信号量和共享内存仍然存在,其他生产者进程和消费者进程还可以继续使用;
    (3)删除信号量和共享内存。显性删除信号量和共享内存,其他生产者进程和消费者进程都不能使用这些信号量和共享内存。

四、实验过程

1、实验结构:

实验一共包括2个文件,一个是producer.c,负责生产产品;一个是consumer.c,负责消费产品。编译后生成producer和consumer,负责展示生产者和消费者的过程。

2、producer.c文件

(1)宏定义

#define N 5                       // 缓冲队列个数
#define BUFFERSIZE 1024         // 缓冲存储区大小为 1024B
#define SEMNUM 3                 // 信号量集中信号量个数为 3,分别为 mutex、full 和 empty

宏定义使得程序的修改方便。

(2)结构定义

struct sharedMemory {
    
                // 共享内存结构
   char buffer[N][BUFFERSIZE];  // 消息存储区
   int put, take;                  // 两个指针
};

union semun {
    
    
   int val;                       // value for setval
   struct semid_ds *buf;       // buffer for IPC_STAT & IPC_SET
   unsigned short *array;      // array for GETALL & SETALL
   struct seminfo *_buf;       // buffer for IPC_INFO
   void *_pad;
};

sharedMemory是我们自己定义的共享内存结构,其中包括N个大小为BUFFERSIZE的消息队列和存放指针put、take。
semun是用来操作信号量的共用体,这里由我们自己定义,程序中主要使用它的val成员。

(3)封装函数操作

  1. P、V操作:
    // 封装 P 操作
    int P(int semid, unsigned short semnum) {
          
          
       struct sembuf semaphore;
       semaphore.sem_num = semnum;
       semaphore.sem_op = -1;
       semaphore.sem_flg = SEM_UNDO;
       return semop(semid, &semaphore, 1);
    }
    
    // 封装 V 操作
    int V(int semid, unsigned short semnum) {
          
          
       struct sembuf semaphore;
       semaphore.sem_num = semnum;
       semaphore.sem_op = 1;
       semaphore.sem_flg = SEM_UNDO;
       return semop(semid, &semaphore, 1);
    }
    
    这里为了mian函数代码的简洁和直观性,封装了P、V操作。其中第一个参数是信号量集标识semid,第二个参数标识信号量集中第semnum个信号量。这里P、V操作对信号量的资源改变数量都为1.
  2. 信号量的读写操作:
    // 封装对信号量的赋值操作
    int setSemVal(int semid, int semnum, int val) {
          
          
       union semun smun;
       smun.val = val;
       return semctl(semid, semnum, SETVAL, smun);
    }
    
    // 封装读取信号量的操作
    int getSemVal(int semid, int semnum) {
          
          
       return semctl(semid, semnum, GETVAL, 0);
    }
    
    出于和上面P、V操作一样的目的,这里前两个参数和上面一样,setSemVal操作中第三个参数val表示对信号量的赋值。

(4)main函数

  1. main函数中主要变量

    int main() {
          
          
       int key, i;                   // key 为共享内存的键值
       int shmexist = 0;             // 表示共享内存是否已存在
       void *shm = NULL;             // 连接共享内存的标记
       struct sharedMemory *share;   // 连接到共享内存的指针
       int shmid;                    // 共享内存标识符
       int semid;                    // 信号量集标识符
       int choice;                   // 选择标识
       int ifbreak = 0;              // 跳出循环的标识
       char message[BUFFERSIZE];     // 临时消息存储
    

    其中需要说明的是:

    shmexist表示该producer是否为第一个producer,以便后面对信号量、共享内存的初始化操作。
    ifbreak表示循环是否需要退出,在该实验中,当用户输入2和3时需要退出循环,结束程序。
    message用来缓存用户输入的消息。我们不能直接将用户输入的消息存入共享内存中,因为producer可能会因P操作而被挂起。

  2. 运行的前期操作,包括:
    ① 创建/获取共享内存

    // 获取共享内存的键值
    key = ftok("./consumer.c", 0);
    // 判断是否获取成功
    if (key == -1) {
          
          
       printf("ftok() failed\n");
       exit(EXIT_FAILURE);
    }
    
    // 请求创建新共享内存
    shmid = shmget(key, sizeof(struct sharedMemory), 0666 | IPC_CREAT | IPC_EXCL);
    // 如果创建新的共享内存失败,则试图获取已有的共享内存
    if (shmid == -1) {
          
          
       shmexist = 1;      // 共享内存已存在
       shmid = shmget(key, sizeof(struct sharedMemory), 0666 | IPC_CREAT);
       if (shmid == -1) {
          
          
          printf("shmget() failed\n");
          exit(EXIT_FAILURE);
       }
    }
    

    ② 进程与共享内存的连接

    // 进程请求连接共享内存,若失败则退出
    if ((shm = shmat(shmid, NULL, 0)) == (void *) (-1)) {
          
          
       printf("shmat() failed\n");
       exit(EXIT_FAILURE);
    }
    
    // share 访问共享内存
    share = (struct sharedMemory *) shm;
    

    ③ 创建/获取信号量集

    // 创建含有 3 个信号量的信号量集
    // 0 : mutex
    // 1 : full
    // 2 : empty
    semid = semget(key, SEMNUM, IPC_CREAT | 0666);
    

    ④ producer特有的初始化

    // 如果共享内存是第一次创建,则初始化
    if (shmexist == 0) {
          
          
       for (i = 0; i < N; ++i)
          strcpy(share->buffer[i], "");
       share->put = share->take = 0;
       setSemVal(semid, 0, 1);    // 互斥信号量 mutex
       setSemVal(semid, 1, 0);    // 信号量 full
       setSemVal(semid, 2, N);        // 信号量 empty
    }
    
  3. while函数(主运行程序)
    ① 输出程序状态,包括:
    producer的进程id;
    full、empty信号量的值;
    put、take指针位置;
    共享内存中的数据;
    可供用户选择的操作。
    int full, empty;

    // 获得信号量的值
    full = getSemVal(semid, 1);
    empty = getSemVal(semid, 2);
    
    // 一系列输出
    printf("This is %d pid producer,\n\n", getpid());
    printf("full = %d, empty = %d, put = %d, take = %d.\n\n", full, empty, share->put, share->take);
    for (i = 0; i < N; ++i)
       if (i == share->put)
          printf("%d --- %s \t <- put\n", i, share->buffer[i]);
       else
          printf("%d --- %s\n", i, share->buffer[i]);
    printf("\n");
    printf("1 : produce a product.\n");
    printf("2 : exit.\n");
    printf("3 : delete semaphores and sharedMemory.\n");
    printf("Enter your choice:\n");
    

    ② 得到用户操作后,判断共享内存和信号量集是否存在,若不存在则结束运行。

    // 重新获得信号量的值
    full = getSemVal(semid, 1);
    empty = getSemVal(semid, 2);
    // 检测共享内存和信号量集是否存在,若不存在,程序退出
    if (semid == -1 || full == -1 && empty == -1) {
          
          
       printf("semaphores and sharedMemory don't exist.\n");
       printf("%d pid consumer ended.\n", getpid());
       exit(EXIT_FAILURE);
    }
    

    ③ 判断用户的选择

    scanf("%d", &choice);
    switch (choice) {
          
          
    

    1)选择1,生产产品

    // 选择 1:生产产品,produce a product
    case 1:
       // 输出,尝试生产中
       printf("Input your message:\n");
       scanf("%s", message);
       printf("processing...\n");
    
       P(semid, 2);           // wait (empty);
       P(semid, 0);           // wait (mutex);
    
       // 生产成功
       strcpy(share->buffer[share->put], message);
       share->put = (share->put + 1) % N;        // 移动指针
       printf("produce successfully\n\n");
    
       V(semid, 0);           // signal (mutex);
       V(semid, 1);           // signal (full);
       break;
    

    这里我们选择先让用户进行输入,再执行进入区。这样可以使得用户更好地观察到生产者被阻塞的情况。

    2)选择2,退出程序,不删除信号量和共享内存

    // 选择 2:退出,exit
    case 2:
       printf("exit...\n");
       ifbreak = 1;      // 可以退出循环
       shmdt(shm);
       break;
    

    3)选择3,删除信号量集和共享内存,此时程序无法继续运行,故退出

    // 选择 3:删除信号量集和共享内存并退出,delete semaphores and sharedMemory
    case 3:
       // 断开共享内存的连接
       shmdt(shm);
    
       // 删除共享内存和信号量集
       semctl(semid, IPC_RMID, 0);
       shmctl(shmid, IPC_RMID, 0);
    
       ifbreak = 1;      // 可以退出循环
       printf("delete semaphores and sharedMemory done!\n");
       break;
    

    4)错误选择

    // 错误选择
       default:
          printf("input invalid, try again!\n");
          break;
    }
    

    ④ 判断是否退出循环

        if (ifbreak) break;      // 退出循环
    }
    
  4. 输出并退出

        printf("%d pid producer ended.\n", getpid());
    
       return 0;
    }
    

3、consumer.c文件

consumer.c文件和producer.c文件大同小异,这里我们主要说明不同的部分。

(1)变量message的定义

char message[BUFFERSIZE] = "";// 消费后消息清除

这里我们并不用message存储用户输入的数据(事实上用户在consumer中不输入数据),而是当消息被消费后,使用message覆盖原来的消息,使其为空,表示消息消耗完成。

(2)获取共享内存

// 试图获取已有的共享内存,若失败则退出
shmid = shmget(key, sizeof(struct sharedMemory), 0666 | IPC_CREAT);
if (shmid == -1) {
    
    
   printf("shmget() failed\n");
   exit(EXIT_FAILURE);
}

在consumer中,并不新创建共享内存,因此这一段程序比producer的简洁一些。

(3)没有producer的初始化步骤

(4)消息的输出部分不同

int full, empty;

// 获得信号量的值
full = getSemVal(semid, 1);
empty = getSemVal(semid, 2);

// 一系列输出
printf("This is %d pid consumer,\n", getpid());
printf("full = %d, empty = %d, put = %d, take = %d.\n\n", full, empty, share->put, share->take);
for (i = 0; i < N; ++i)
   if (i == share->take)
      printf("%d --- %s \t <- take\n", i, share->buffer[i]);
   else
      printf("%d --- %s\n", i, share->buffer[i]);
printf("\n");
printf("1 : consume a product.\n");
printf("2 : exit.\n");
printf("3 : delete semaphores and sharedMemory.\n");
printf("Enter your choice:\n");

这里换成了consumer和take指针,其他地方无明显区别。

(5)用户选择1时,是消费消息而不是生产消息

// 选择 1:消费产品,consume a product
case 1:
   // 输出,尝试消费中
   printf("consuming...\n");

   P(semid, 1);           // wait (full);
   P(semid, 0);           // wait (mutex);

   // 消费成功
   printf("done successfully! %s consumed.\n\n", share->buffer[share->take]);
   strcpy(share->buffer[share->take], message);
   share->take = (share->take + 1) % N;        // 移动指针

   V(semid, 0);           // signal (mutex);
   V(semid, 2);           // signal (empty);
   break;

(6)程序退出时,输出不同

printf("%d pid consumer ended.\n", getpid());

(完整程序见实验源代码)

五、实验测试

  1. 打开四个终端窗口,分别为2个producer和2个consumer

  2. 生产者生产一些产品,消费者消费。

  3. 打开第2个生产者生产,能够进行同步。

  4. 生产者生产满后,被挂起。消费者消费后,生产者才能继续生产。

  5. 退出一个生产者,开启一个消费者,程序能够继续运行。

    这里说明一下,左下角的consumer显示没有777,这是因为其打印过程在先,生产者生产在后,但实际上共享内存中还是有777的。例如我们消费一个消息更新一下,发现777被更新出来了。

  6. 共享内存为空时消费者进行消费,会被挂起。
    当有生产者生产时,消费者会被唤醒。

  7. 终结共享内存和信号量后,其他进程无法继续运行。

  8. 测试结束。

本文仅供实验参考,重点是为读者提供一些实验思路,并不是标准的实验过程。因此部分源码不给予提供,望读者能够从本文中受到启发独立完成实验。

猜你喜欢

转载自blog.csdn.net/zheliku/article/details/121099764