接上一篇文章
一、引言
此问题是操作系统中的一个经典的同步异步问题,是我们操作系统课程中非常重要的一部分。实验要求我们用C语言在Linux操作系统下利用信号量函数和共享内存函数实现经典的生产者消费者问题。也借此博客把所学知识记录下来。实验要求如下:
在Linux操作系统下用C实现经典同步问题:生产者—消费者,具体要求如下:
(1)一个大小为10的缓冲区,初始状态为空。
(2)2个生产者,随机等待一段时间,往缓冲区中添加数据,若缓冲区已满,等待消费者取走数据之后再添加,重复10次。
(3)2个生产者,随机等待一段时间,往缓冲区中读取数据,若缓冲区为空,等待生产者添加数据之后再读取,重复10次。
(4)当前只能有一个进程对这个缓冲区进行操作 。
(5)缓冲区采用循环队列表示,利用头、尾指针来存放、读取数据,以及判断队列是否为空。缓冲区中数组大小为10。
(6)利用随机函数rand()得到A~Z的一个随机字符,作为生产者每次生产的数据,存放到缓冲区中。
(7)使用shmget()系统调用实现共享主存段的创建,shmget()返回共享内存区的ID。对于已经申请到的共享段,进程需把它附加到自己的虚拟空间中才能对其进行读写。
(8)信号量的建立采用semget()函数,同时建立信号量的 数量。在信号量建立后,调用semctl()对信号量进行初始2化 ,例如本实验中,可以建立两个信号量SEM_EMPTY、 SEM_FULL,初始化时设置SEM_EMPTY为10,SEM_FULL 为0。使用操 作信号的函数semop()做排除式操作,使用这个 函数防止对共享内存的同时操作。对共享内存操作完毕后采用shmctl()函数撤销共享内存段。
(9)使用循环,创建2个生产者以及2个消费者,采用函数 fork()创建一个新的进程。
(10)一个进程的一次操作完成后,采用函数fflush()刷新缓冲区。
(11)程序最后使用semctl()函数释放内存。
二、生产者消费者问题
三、信号量
抽象的来讲,信号量(signal)的特性如下:信号量是一个非负整数,所有通过它的线程/进程都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。在通用术语上我们将此两种操作成为P、V操作。而操作的对象就是信号量(signal),用来实现线程/进程的同步、异步和互斥。
四、共享内存函数
共享内存函数由shmget、shmat、shmdt、shmctl四个函数组成。下面的表格列出了这四个函数的函数原型及其具体说明。
shmget(得到一个共享内存标识符或创建一个共享内存对象) |
||
所需头文件 |
#include #include |
|
函数说明 |
得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符 |
|
函数原型 |
int shmget(key_t key, size_t size, int shmflg) |
|
函数传入值 |
key |
0(IPC_PRIVATE):会建立新共享内存对象 |
大于0的32位整数:视参数shmflg来确定操作。通常要求此值来源于ftok返回的IPC键值 |
||
size |
大于0的整数:新建的共享内存大小,以字节为单位 |
|
0:只获取共享内存时指定为0 |
||
shmflg |
0:取共享内存标识符,若不存在则函数会报错 |
|
IPC_CREAT:当shmflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符 |
||
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建一个消息队列;如果存在这样的共享内存则报错 |
||
函数返回值 |
成功:返回共享内存的标识符 |
|
出错:-1,错误原因存于error中 |
||
附加说明 |
上述shmflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600表示用户可以读写改内存)进行|运算来确定信号量集的存取权限 |
|
错误代码 |
EINVAL:参数size小于SHMMIN或大于SHMMAX EEXIST:预建立key所指的共享内存,但已经存在 EIDRM:参数key所指的共享内存已经删除 ENOSPC:超过了系统允许建立的共享内存的最大值(SHMALL) ENOENT:参数key所指的共享内存不存在,而参数shmflg未设IPC_CREAT位 EACCES:没有权限 ENOMEM:核心内存不足 |
在Linux环境中,对开始申请的共享内存空间进行了初始化,初始值为0x00。
如果用shmget创建了一个新的消息队列对象时,则shmid_ds结构成员变量的值设置如下:
shm_lpid、shm_nattach、shm_atime、shm_dtime设置为0。
msg_ctime设置为当前时间。
shm_segsz设成创建共享内存的大小。
shmflg的读写权限放在shm_perm.mode中。
shm_perm结构的uid和cuid成员被设置成当前进程的有效用户ID,gid和cuid成员被设置成当前进程的有效组ID。
shmat(把共享内存区对象映射到调用进程的地址空间) |
||
所需头文件 |
#include #include |
|
函数说明 |
连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问 |
|
函数原型 |
void *shmat(int shmid, const void *shmaddr, int shmflg) |
|
函数传入值 |
msqid |
共享内存标识符 |
shmaddr |
指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置 |
|
shmflg |
SHM_RDONLY:为只读模式,其他为读写模式 |
|
函数返回值 |
成功:附加好的共享内存地址 |
|
出错:-1,错误原因存于error中 |
||
附加说明 |
fork后子进程继承已连接的共享内存地址。exec后该子进程与已连接的共享内存地址自动脱离(detach)。进程结束后,已连接的共享内存地址会自动脱离(detach) |
|
错误代码 |
EACCES:无权限以指定方式连接共享内存 EINVAL:无效的参数shmid或shmaddr ENOMEM:核心内存不足 |
shmat(断开共享内存连接) |
|
所需头文件 |
#include #include |
函数说明 |
与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存 |
函数原型 |
int shmdt(const void *shmaddr) |
函数传入值 |
shmaddr:连接的共享内存的起始地址 |
函数返回值 |
成功:0 |
出错:-1,错误原因存于error中 |
|
附加说明 |
本函数调用并不删除所指定的共享内存区,而只是将先前用shmat函数连接(attach)好的共享内存脱离(detach)目前的进程 |
错误代码 |
EINVAL:无效的参数shmaddr |
shmctl(共享内存管理) |
||
所需头文件 |
#include #include |
|
函数说明 |
完成对共享内存的控制 |
|
函数原型 |
int shmctl(int shmid, int cmd, struct shmid_ds *buf) |
|
函数传入值 |
msqid |
共享内存标识符 |
cmd |
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中 |
|
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内 |
||
IPC_RMID:删除这片共享内存 |
||
buf |
共享内存管理结构体。具体说明参见共享内存内核结构定义部分 |
|
函数返回值 |
成功:0 |
|
出错:-1,错误原因存于error中 |
||
错误代码 |
EACCESS:参数cmd为IPC_STAT,确无权限读取该共享内存 EFAULT:参数buf指向无效的内存地址 EIDRM:标识符为msqid的共享内存已被删除 EINVAL:无效的参数cmd或shmid EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行 |
信号量函数由semget、semop、semctl三个函数组成。下面的表格列出了这三个函数的函数原型及具体说明。
semget(得到一个信号量集标识符或创建一个信号量集对象) |
||
所需头文件 |
#include #include #include |
|
函数说明 |
得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符 |
|
函数原型 |
int semget(key_t key, int nsems, int semflg) |
|
函数传入值 |
key |
0(IPC_PRIVATE):会建立新信号量集对象 |
大于0的32位整数:视参数semflg来确定操作,通常要求此值来源于ftok返回的IPC键值 |
||
nsems |
创建信号量集中信号量的个数,该参数只在创建信号量集时有效 |
|
semflg |
0:取信号量集标识符,若不存在则函数会报错 |
|
IPC_CREAT:当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符 |
||
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个消息队列;如果存在这样的信号量集则报错 |
||
函数返回值 |
成功:返回信号量集的标识符 |
|
出错:-1,错误原因存于error中 |
||
附加说明 |
上述semflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600如共享内存函数)进行|运算来确定信号量集的存取权限 |
|
错误代码 |
EACCESS:没有权限 EEXIST:信号量集已经存在,无法创建 EIDRM:信号量集已经删除 ENOENT:信号量集不存在,同时semflg没有设置IPC_CREAT标志 ENOMEM:没有足够的内存创建新的信号量集 ENOSPC:超出限制 |
如果用semget创建了一个新的信号量集对象时,则semid_ds结构成员变量的值设置如下:
sem_otime设置为0。
sem_ctime设置为当前时间。
msg_qbytes设成系统的限制值。
sem_nsems设置为nsems参数的数值。
semflg的读写权限写入sem_perm.mode中。
sem_perm结构的uid和cuid成员被设置成当前进程的有效用户ID,gid和cuid成员被设置成当前进程的有效组ID。
semop(完成对信号量的P操作或V操作) |
|
所需头文件 |
#include #include #include |
函数说明 |
对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作 |
函数原型 |
int semop(int semid, struct sembuf *sops, unsigned nsops) |
函数传入值 |
semid:信号量集标识符 |
sops:指向进行操作的信号量集结构体数组的首地址,此结构的具体说明如下: struct sembuf { short semnum; /*信号量集合中的信号量编号,0代表第1个信号量*/ short val;/*若val>0进行V操作信号量值加val,表示进程释放控制的资源 */ /*若val<0进行P操作信号量值减val,若(semval-val)<0(semval为该信号量值),则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误*/ /*若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误*/ short flag; /*0 设置信号量的默认操作*/ /*IPC_NOWAIT设置信号量操作不等待*/ /*SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值*/ }; |
|
nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作 |
|
函数返回值 |
成功:返回信号量集的标识符 |
出错:-1,错误原因存于error中 |
|
错误代码 |
E2BIG:一次对信号量个数的操作超过了系统限制 EACCESS:权限不够 EAGAIN:使用了IPC_NOWAIT,但操作不能继续进行 EFAULT:sops指向的地址无效 EIDRM:信号量集已经删除 EINTR:当睡眠时接收到其他信号 EINVAL:信号量集不存在,或者semid无效 ENOMEM:使用了SEM_UNDO,但无足够的内存创建所需的数据结构 ERANGE:信号量值超出范围 |
sops为指向sembuf数组,定义所要进行的操作序列。下面是信号量操作举例。
struct sembuf sem_get={0,-1,IPC_NOWAIT}; /*将信号量对象中序号为0的信号量减1*/
struct sembuf sem_get={0,1,IPC_NOWAIT}; /*将信号量对象中序号为0的信号量加1*/
struct sembuf sem_get={0,0,0}; /*进程被阻塞,直到对应的信号量值为0*/
flag一般为0,若flag包含IPC_NOWAIT,则该操作为非阻塞操作。若flag包含SEM_UNDO,则当进程退出的时候会还原该进程的信号量操作,这个标志在某些情况下是很有用的,比如某进程做了P操作得到资源,但还没来得及做V操作时就异常退出了,此时,其他进程就只能都阻塞在P操作上,于是造成了死锁。若采取SEM_UNDO标志,就可以避免因为进程异常退出而造成的死锁。
semctl (得到一个信号量集标识符或创建一个信号量集对象) |
||
所需头文件 |
#include #include #include |
|
函数说明 |
得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符 |
|
函数原型 |
int semctl(int semid, int semnum, int cmd, union semun arg) |
|
函数传入值 |
semid |
信号量集标识符 |
semnum |
信号量集数组上的下标,表示某一个信号量 |
|
cmd |
见下文表15-4 |
|
arg |
union semun { short val; /*SETVAL用的值*/ struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/ unsigned short* array; /*SETALL、GETALL用的数组值*/ struct seminfo *buf; /*为控制IPC_INFO提供的缓存*/ } arg; |
|
函数返回值 |
成功:大于或等于0,具体说明请参照表15-4 |
|
出错:-1,错误原因存于error中 |
||
附加说明 |
semid_ds结构见信号量集内核结构定义 |
|
错误代码 |
EACCESS:权限不够 EFAULT:arg指向的地址无效 EIDRM:信号量集已经删除 EINVAL:信号量集不存在,或者semid无效 EPERM:进程有效用户没有cmd的权限 ERANGE:信号量值超出范围 |
表15-4 semctl函数cmd形参说明表
命令 |
解 释 |
IPC_STAT |
从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中 |
IPC_SET |
设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值 |
IPC_RMID |
从内核中删除信号量集合 |
GETALL |
从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组中 |
GETNCNT |
返回当前等待资源的进程个数 |
GETPID |
返回最后一个执行系统调用semop()进程的PID |
GETVAL |
返回信号量集合内单个信号量的值 |
GETZCNT |
返回当前等待100%资源利用的进程个数 |
SETALL |
与GETALL正好相反 |
SETVAL |
用联合体中val成员的值设置信号量集合中单个信号量的值 |
struct sembuf
{
unsigned short int sem_num; /* 信号量的序号从0~nsems-1 */
short int sem_op; /* 对信号量的操作,>0, 0, <0 */
short int sem_flg; /* 操作标识:0, IPC_WAIT, SEM_UNDO */
};
sem_num标识信号量集中的第几个信号量,0表示第1个,1表示第2个,nsems - 1表示最后一个。
sem_op标识对信号量的所进行的操作类型。对信号量的操作有三种类型:
sem_flag:信号量操作的属性标志,如果为0,表示正常操作,如果为IPC_WAIT,使对信号量的操作时非阻塞的。即指定了该标志,调用线程在信号量的值不满足条件的情况下不会被阻塞,而是直接返回-1,并将errno设置为EAGAIN。如果为SEM_UNDO,那么将维护进程对信号量的调整值,以便进程结束时恢复信号量的状态。
下面解释一下与单个信号量相关的几个值:
semval:信号量的当前值,在文章开头信号量的结构中已提到。
semncnt:等待semval变为大于当前值的线程数。在文章开头信号量的结构中已提到。
semzcnt:等待semval变为0的线程数。在文章开头信号量的结构中已提到。
semadj:指定信号量针对某个特定进程的调整值。只有sembuf结构的sem_flag指定为SEM_UNDO后,semadj才会随着sem_op而更新。讲简单一点:对某个进程,在指定SEM_UNDO后,对信号量semval值的修改都会反应到semadj上,当该进程终止的时候,内核会根据semadj的值,重新恢复信号量之前的值。
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_BUFFER_SIZE 10
#define SHM_MODE 0600
#define SEM_MODE 0600
#define SEM_FULL 0
#define SEM_EMPTY 1
#define MUTEX 2
/*
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
// union semun is defined by including
#else
// according to X/OPEN we have to define it ourselves
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
#endif
union semun su;//sem union,用于初始化信号量
*/
struct my_buffer
{
int head;
int tail;
char str[MAX_BUFFER_SIZE];
int num; //缓冲区里字母数量
int is_empty;
};
const int N_CONSUMER = 2;//消费者数量
const int N_PRODUCER = 2;//生产者数量
const int N_BUFFER = 10;//缓冲区容量
const int N_WORKTIME = 10;//工作次数
int shm_id = -1;
int sem_id = -1;
pid_t child;
pid_t parent;
//得到10以内的一个随机数
int get_random()
{
int digit;
srand((unsigned)(getpid() + time(NULL)));
digit = rand() % 10;
return digit;
}
//得到A~Z的一个随机字母
char getRandChar()
{
char letter;
srand((unsigned)(getpid() + time(NULL)));
letter = (char)((rand() % 26) + 'A');
return letter;
}
//sem_id 表示信号量集合的 id
//sem_num 表示要处理的信号量在信号量集合中的索引
//P操作
void waitSem(int sem_id,int sem_num)
{
struct sembuf sb;
sb.sem_num = sem_num;
sb.sem_op = -1;//表示要把信号量减一
sb.sem_flg = SEM_UNDO;//
//第二个参数是 sembuf [] 类型的,表示数组
//第三个参数表示 第二个参数代表的数组的大小
if(semop(sem_id,&sb,1) < 0){
perror("waitSem failed");
exit(1);
}
}
//V操作
void sigSem(int sem_id,int sem_num)
{
struct sembuf sb;
sb.sem_num = sem_num;
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
//第二个参数是 sembuf [] 类型的,表示数组
//第三个参数表示 第二个参数代表的数组的大小
if(semop(sem_id,&sb,1) < 0){
perror("sigSem failed");
exit(1);
}
}
//打印进程运行结果
void printTime()
{
//打印时间
time_t now;
struct tm *timenow; //实例化tm结构指针
time(&now);
timenow = localtime(&now);
printf("执行时间: %s ",asctime(timenow));
}
int main(int argc, char ** argv)
{
shm_id = shmget(IPC_PRIVATE,MAX_BUFFER_SIZE,SHM_MODE); //申请共享内存
if(shm_id < 0)
{
perror("create shared memory failed");
exit(1);
}
struct my_buffer *shmptr;
shmptr = shmat(shm_id, 0, 0); //将申请的共享内存附加到申请通信的进程空间
if (shmptr == (void*)-1)
{
perror("add buffer to using process space failed!\n");
exit(1);
}
if((sem_id = semget(IPC_PRIVATE,3,SEM_MODE)) < 0)
{ //创建三个信号量,SEM_EMPTY,SEM_FULL和MUTEX
perror("create semaphore failed! \n");
exit(1);
}
if(semctl(sem_id,SEM_FULL,SETVAL,0) == -1)
{ //将索引为0的信号量设置为0-->SEM_FULL
perror("sem set value error! \n");
exit(1);
}
if(semctl(sem_id,SEM_EMPTY,SETVAL,10) == -1)
{ //将索引为1的信号量设置为10-->SEM_EMPTY
perror("sem set value error! \n");
exit(1);
}
if(semctl(sem_id,MUTEX,SETVAL,1) == -1)
{ //将索引为3的信号量设置为1-->MUTEX
perror("sem set value error! \n");
exit(1);
}
shmptr -> head = 0;
shmptr -> tail = 0;
shmptr -> is_empty = 1;
shmptr -> num = 0;
for(int i = 0; i < N_PRODUCER; i++)
{
parent = fork();
if(parent < 0)
{
perror("the fork failed");
exit(1);
}
else if(parent == 0)
{
shmptr = shmat(shm_id, 0, 0); //将申请的共享内存附加到申请通信的进程空间
if (shmptr == (void*)-1)
{
perror("add buffer to using process space failed!\n");
exit(1);
}
int count = 0;
for(int j = 0; j < N_WORKTIME; j++)
{
waitSem(sem_id, SEM_EMPTY);
waitSem(sem_id, MUTEX);
sleep(get_random());
printf("-------------------------------------------------------------\n");
printf("我是第 %d 个生产者进程,PID = %d\n", i + 1, getpid());
/*生产产品*/
char c = getRandChar(); //随机获取字母
shmptr -> str[shmptr->tail] = c;
shmptr -> tail = (shmptr->tail + 1) % MAX_BUFFER_SIZE;
shmptr -> is_empty = 0; //写入新产品
shmptr -> num++;
/*打印输出结果*/
printTime(); //程序运行时间
int p;
printf("缓冲区数据(%d个):",shmptr -> num); //打印缓冲区中的数据
p = (shmptr->tail-1 >= shmptr->head) ? (shmptr->tail-1) : (shmptr->tail-1 + MAX_BUFFER_SIZE);
for (p; !(shmptr -> is_empty) && p >= shmptr -> head; p--)
{
printf("%c", shmptr -> str[p % MAX_BUFFER_SIZE]);
}
printf("\t 生产者 %d 放入 '%c'. \n", i + 1, c);
printf("-------------------------------------------------------------\n");
fflush(stdout);
sigSem(sem_id, MUTEX);
sigSem(sem_id, SEM_FULL);
}
//将共享段与进程之间解除连接
shmdt(shmptr);
exit(0);
}
}
for(int i = 0; i < N_CONSUMER; i++)
{
child = fork();
if(child < 0)//调用fork失败
{
perror("the fork failed");
exit(1);
}
else if(child == 0)
{
int count = 0;
shmptr = shmat(shm_id, 0, 0); //将申请的共享内存附加到申请通信的进程空间
if (shmptr == (void*)-1)
{
perror("add buffer to using process space failed!\n");
exit(1);
}
for(int j = 0; j < N_WORKTIME; j++)
{
waitSem(sem_id, SEM_FULL);
waitSem(sem_id, MUTEX);
sleep(get_random());
printf("-------------------------------------------------------------\n");
printf("我是第 %d 个消费者进程,PID = %d\n", i + 1, getpid());
/*消费数据*/
char lt = shmptr -> str[shmptr -> head];
shmptr -> head = (shmptr -> head + 1) % MAX_BUFFER_SIZE;
shmptr -> is_empty = (shmptr->head == shmptr->tail); //
shmptr -> num--;
/*打印输出结果*/
printTime(); //程序运行时间
int p;
printf("缓冲区数据(%d个):",shmptr -> num); //打印缓冲区中的数据
p = (shmptr -> tail - 1 >= shmptr -> head) ? (shmptr -> tail-1) : (shmptr -> tail - 1 + MAX_BUFFER_SIZE);
for (p; !(shmptr -> is_empty) && p >= shmptr -> head; p--)
{
printf("%c", shmptr -> str[p % MAX_BUFFER_SIZE]);
}
printf("\t 消费者 %d 取出 '%c'. \n", i + 1, lt);
printf("-------------------------------------------------------------\n");
fflush(stdout);
sigSem(sem_id,MUTEX);
sigSem(sem_id,SEM_EMPTY);
}
//将共享段与进程之间解除连接
shmdt(shmptr);
exit(0);
}
}
//主进程最后退出
while (wait(0) != -1);
//将共享段与进程之间解除连接
shmdt(shmptr);
//对共享内存区执行控制操作
shmctl(shm_id,IPC_RMID,0);//当cmd为IPC_RMID时,删除该共享段
shmctl(sem_id,IPC_RMID,0);
printf("主进程运行结束!\n");
fflush(stdout);
exit(0);
return 0;
}