一、实验目的
了解和熟悉linux系统下的信号量集和共享内存。
二、实验任务
使用linux系统提供的信号量集和共享内存实现生产者和消费者问题。
三、实验要求
- 写两个程序,一个模拟生产者过程,一个模拟消费者过程;
- 创建一个共享内存模拟生产者-消费者问题中缓冲队列,该缓冲队列有N(例如N=10)个缓冲区,每个缓冲区的大小为1024B,每个生产者和消费者对缓冲区必须互斥访问;
- 由第一个生产者创建信号量集和共享内存,其他生产者和消费者可以使用该信号量集和共享内存;
- 生产者程序:生产者生产产品(即是从键盘输入长度小于1024B的字符)放入空的缓冲区;
- 消费者程序:消费者消费产品(即从满的缓冲区中取出内容在屏幕上打印出来),然后满的缓冲区变为空的缓冲区;
- 多次运行生产者程序和消费者进程,可以产生多个生产者进程和多个消费者进程,这些进程可以共享这些信号量和共享内存,实现生产者和消费者问题;
- 在生产者程序程序中,可以选择:
(1)生产产品;
(2)退出。退出进程,但信号量和共享内存仍然存在,其他生产者进程和消费者进程还可以继续使用;
(3)删除信号量和共享内存。显性删除信号量和共享内存,其他生产者进程和消费者进程都不能使用这些信号量和共享内存。 - 在消费者程序中,可以选择:
(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)封装函数操作
- P、V操作:
这里为了mian函数代码的简洁和直观性,封装了P、V操作。其中第一个参数是信号量集标识semid,第二个参数标识信号量集中第semnum个信号量。这里P、V操作对信号量的资源改变数量都为1.// 封装 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); }
- 信号量的读写操作:
出于和上面P、V操作一样的目的,这里前两个参数和上面一样,setSemVal操作中第三个参数val表示对信号量的赋值。// 封装对信号量的赋值操作 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); }
(4)main函数
-
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操作而被挂起。 -
运行的前期操作,包括:
① 创建/获取共享内存// 获取共享内存的键值 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 }
-
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; // 退出循环 }
-
输出并退出
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());
(完整程序见实验源代码)
五、实验测试
-
打开四个终端窗口,分别为2个producer和2个consumer
-
生产者生产一些产品,消费者消费。
-
打开第2个生产者生产,能够进行同步。
-
生产者生产满后,被挂起。消费者消费后,生产者才能继续生产。
-
退出一个生产者,开启一个消费者,程序能够继续运行。
这里说明一下,左下角的consumer显示没有777,这是因为其打印过程在先,生产者生产在后,但实际上共享内存中还是有777的。例如我们消费一个消息更新一下,发现777被更新出来了。
-
共享内存为空时消费者进行消费,会被挂起。
当有生产者生产时,消费者会被唤醒。 -
终结共享内存和信号量后,其他进程无法继续运行。
-
测试结束。
本文仅供实验参考,重点是为读者提供一些实验思路,并不是标准的实验过程。因此部分源码不给予提供,望读者能够从本文中受到启发独立完成实验。