Linux同步原语-信号量与PV问题(哈工大OS实验五)
这次分享一下HITOS实验五,关于信号量的实现以及生产者消费者问题。之前的进程切换实验还有bug没有解决,所以重新配置了一个环境。整个实验做了好久,实在有点坐牢了。同步原语这块内容强烈推荐去b站看一下jyy老师的课,讲的很清楚。
实验内容
内容简单来说就是两部分,具体细节还是可以看蓝桥云。
第一个部分是实现信号量函数:
包括:sem_open()、sem_wait()、sem_post() 和 sem_unlink()。
第二个部分是利用实现的信号量函数解决消费者生产者问题。(相当于自己写一个测试用例来验证自己的信号量实现对不对。)
- 编写用户态的程序pc.c,在这个程序中,需要创建一个生产者进程,多个消费者进程。创建进程会用到之前学过的fork,为了让这几个进程对同一块内存的读和写不发生错误,需要使用信号量进行控制;
- 信号量的实现需要我们在内核态编写系统调用代码,在用户态通过int 0x80中断进入内核态执行相应的系统调用。需要编写的系统调用为:sys_sem_open, sys_sem_wait, sys_sem_post, sys_sem_unlink。
实验原理
根据惯例还是先讲实验原理,再讲实验的实现过程(但是过程不会讲的太具体)。
1. 生产者与消费者问题
https://blog.csdn.net/m0_52169086/article/details/125585654?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169366134716800222819362%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=169366134716800222819362&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-125585654-null-null.142v93insert_down28v1&utm_term=POSIX 生产者消费者模式&spm=1018.2226.3001.4187
1.基本概念
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过这个容器来通讯,所以生产者生产完数据之后不用等待 消费者处理,直接将生产的数据放到这个容器当中,消费者也不用找生产者要数据,而是直接从这个容器里取数据,这个容器就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这个容器实际上就是用来给生产者和消费者解耦的。
简单来说就是:生产者消费者问题是用来是一个用来描述针对共享内存一致性的一个问题,当生产者(向共享内存中存储内容)与消费者(向共享内存中读取内容)共同对共享内存进行store与load操作时,如何保证两者直接的操作不会对互相造成障碍。同时还有大量的共享者对内存进行store,大量的消费者同时对内存进行load时,如何保证运行的正确性。
2.模型特点
(1)生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:
三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
两种角色: 生产者和消费者。(通常由进程或线程承担)
一个交易场所: 通常指的是内存中的一段缓冲区。(可以自己通过某种方式组织起来)
我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。
(2)生产者和生产者、消费者和消费者、生产者和消费者,它们之间为什么会存在互斥关系?
介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。
其中,所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系。
(3)生产者和消费者之间为什么会存在同步关系?
如果让生产者一直生产,那么当生产者生产的数据将容器塞满后,生产者再生产数据就会生产失败。
反之,让消费者一直消费,那么当容器当中的数据被消费完后,消费者再进行消费就会消费失败。
虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的。我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。
注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来。
((4)让消费者和生产者协同工作,合适的时候可能一直运行,生产者和消费者并不会因为要互相等待对方的结果而阻塞,相当于双方可以并发执行
因此针对消费者与生产者问题,存在如下两种方法:
3. 基于BlockingQueue的生产者消费者模型
这个就不具体介绍了,简答来说就是:整个共享内存的访问权限都由阻塞队列来控制。生产者向队列中添加元素,当队列满了,生产者就会被阻塞直到有剩余空间为止。消费者读取队列中的元素,当队列中为空时,消费者线程/进程被阻塞。
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。
其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素。当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进行操作时会被阻塞。
需要确保,队列中的存放与读取操作需要时原子级别的。
4. 基于信号量方法
在理解信号量如何解决生产者消费者问题时,我们要先理解究竟什么是信号量。
1.信号量
(1)为什么要使用信号量/什么是信号量
首先需要了解信号量究竟是做什么的,才能了解信号量在生产者与消费者问题中起到了什么样的作用。信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。
每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特定的临界资源的权限,当操作完毕后就应该释放信号量
信号量存在的价值 : 1.进行同步与互斥 2.更细粒度的临界资源的管理
我们可以假设将临界资源分成若干份,每一份资源可以理解为被独立访问。信号量则是表示当前可以被使用的资源的数量。如果我们将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,这些执行流访问的是临界资源的不同区域,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致等问题。
(2)信号量的P/V操作
- P操作:我们将申请信号( count-- )量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。
- V操作:我们将释放信号量( count++ )称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。
- PV操作必须是原子操作:多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。
但信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。内存当中变量的++、–操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、–操作。 - PV挂起操作:当执行流在申请信号量时,可能此时信号量的值为0,也就是说信号量描述的临界资源已经全部被申请了,此时该执行流就应该在该信号量的等待队列当中进行等待,直到有信号量被释放时再被唤醒。信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。
大致的伪代码如下:(但是在Linux0.11中由于等待队列的wake up和wait操作的特殊性,代码可能会有一定的变化,但大致逻辑如下:)
对于一个需要申请的信号量SEM:需要有一个count描述信号量的值,以及一个等待队列队首,以及一个互斥锁来保证对这个信号量进行原子级别操作
P:本质上是获取一个信号量(相当于当前进程/线程获取了一个共享内存的资源快),因此整体剩余信号量需要-1,如果当前信号量值为0,则需要进入等待队列,直到信号量变为正数
V:是当SEM信号量管理的任务在当前进程中被执行完,此时就需要释放掉一个信号量(相当于释放了对一个共享内存资源快的所有权)。对于Linux0.11中的话,在等待队列中存在等待进程时,还需要额外的进行wake_up唤醒操作。
2.信号量函数
1. 初始化
函数 : int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:需要初始化的信号量。
pshared:传入0值表示线程间共享,传入非零值表示进程间共享(常用0)
value:信号量的初始值(计数器的初始值)。
返回值: 初始化信号量成功返回0,失败返回-1。
注意: POSIX信号量和System V信号量作用相同,都是用于同步操作,
达到无冲突的访问共享资源目的,但POSIX信号量可以用于线程间同步。
2. 销毁函数
函数 : int sem_unlink(sem_t *sem);
参数: sem:需要销毁的信号量。
返回值:销毁信号量成功返回0,失败返回-1。
3. 等待信号量(P操作)
函数 : int sem_wait(sem_t *sem);
参数:sem:需要等待的信号量。
返回值:
等待信号量成功返回0,信号量的值减一
等待信号量失败返回-1,信号量的值保持不变
4. 释放信号量(V操作)
函数 : int sem_post(sem_t *sem);
参数:sem:需要发布的信号量。
返回值:
发布信号量成功返回0,信号量的值加一
发布信号量失败返回-1,信号量的值保持不变
3. 二元信号量模拟实现互斥功能
信号量本质是一个计数器,如果将信号量的初始值设置为1,那么此时该信号量叫做二元信号量。
信号量的初始值为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本等价于互斥锁。
示例,实现一个多线程抢票系统,使用二元信号量模拟实现多线程互斥,,让每个线程在访问全局变量tickets之前先申请信号量,访问完毕后再释放信号量,此时二元信号量就达到了互斥的效果。
(具体流程:由于信号量为二元值,此时的信号量可以理解为一种互斥锁,当一个进程对资源进行了访问,就获取了当前的信号量,其他的消费者都不能再对共享内存进行读取)
void* TicketGrabbing(void* arg)
{
std::string name = (char*)arg;
while (true){
sem.P();
if (tickets > 0){
usleep(1000);
std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;
sem.V();
}
else{
sem.V();
break;
}
}
std::cout << name << " quit..." << std::endl;
pthread_exit((void*)0);
}
int main()
{
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");
pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");
pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");
pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
return 0;
}
4.基于环形队列利用多值信号量解决生产者消费者问题
基于环形队列(多值信号量)的生产者消费者模型会引入针对不同资源的信号量(很多地方会叫 full 和empty),很多人第一次看时候会很蒙蔽,因为本质上来说empty和full都是针对内存可用资源的度量,所以很多时候理解上会很懵逼。接下来会解释一下到底为何使用empty与full两个信号量,以及信号量之间如何配合如何使用。
1. 生产者和消费者关心不同资源
(1) 生产者关注的是空间资源,消费者关注的是数据资源
生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产。
消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。
(2) blank_sem和data_sem的初始值设置
现在我们用信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:
blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。
data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据。
(3) 生产者和消费者申请和释放资源
①生产者申请空间资源,释放数据资源
对于生产者来说,生产者每次生产数据前都需要先申请blank_sem:
如果blank_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作。
如果blank_sem的值为0,则信号量申请失败,此时生产者需要在blank_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。
当生产者生产完数据后,应该释放data_sem:
虽然生产者在进行生产前是对blank_sem进行的P操作,但是当生产者生产完数据,应该对data_sem进行V操作而不是blank_sem。
生产者在生产数据前申请到的是blank位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而应该是data位置。
当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作。
②消费者申请数据资源,释放空间资源
对于消费者来说,消费者每次消费数据前都需要先申请data_sem:
如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作。
如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。
当消费者消费完数据后,应该释放blank_sem:
虽然消费者在进行消费前是对data_sem进行的P操作,但是当消费者消费完数据,应该对blank_sem进行V操作而不是data_sem。
消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作blank位置,而不是data位置。
当消费者消费完数据后,意味着环形队列当中多了一个blank位置,因此我们应该对blank_sem进行V操作。
2. 需要遵守的两个原则
(1)生产者和消费者不能对同一个位置进行访问(互斥)
如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这当然是不允许的。
而如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题。
(2)无论是生产者还是消费者,都不应该将对方套一个圈以上(格子的数量有限)
生产者从消费者的位置开始一直按顺时针方向进行生产,如果生产者生产的速度比消费者消费的速度快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费者消费的数据。
同理,消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据。
基于线程实现的部分代码如下:
public:
//向环形队列插入数据(生产者调用)
void Push(const T& data)
{
P(_blank_sem); //生产者关注空间资源
_q[_p_pos] = data;
V(_data_sem); //生产
//更新下一次生产的位置
_p_pos++;
_p_pos %= _cap;
}
//从环形队列获取数据(消费者调用)
void Pop(T& data)
{
P(_data_sem); //消费者关注数据资源
data = _q[_c_pos];
V(_blank_sem);
//更新下一次消费的位置
_c_pos++;
_c_pos %= _cap;
}
3. 信号量保护环形队列
(1)在blank_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。
① 因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:
环形队列为空时
环形队列为满时
②但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:
当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源为0
当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源为0
(2) 当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行.
2. 实验实现
接下来就来讲讲实验实现:
1. 信号量
#include <unistd.h> /* NULL */
#include <string.h> /* strcmp strcpy */
#include <linux/sem.h> /* sem_t */
#include <asm/segment.h> /* get_fs_byte */
#include <asm/system.h> /* cli, sti */
#include <linux/kernel.h> /* printk */
//信号量最大数量
#define SEM_LIST_LENGTH 5
//信号量数组(都初始化为没有的状态)
sem_t sem_list[SEM_LIST_LENGTH] = {
{"\0",0,NULL}, {"\0",0,NULL},{"\0",0,NULL},{"\0",0,NULL},{"\0",0,NULL}
};
sem_t *sys_sem_open(const char *name,unsigned int value)
{
if (name == NULL)
{
printk("name == NULL\n");
return NULL;
}
/* 首先将信号量的名称赋值到新建的缓冲区中 */
char nbuf[20];
int i = 0;
for(; i< 20; i++)
{
nbuf[i] = get_fs_byte(name+i);
}
nbuf[i]='\0';
/* 然后开始遍历已有的信号量数组,如果有该名字的信号量,直接返回信号量的地址 */
sem_t *result;
i = 0;
int k=0;
int tmp=-1;
for(; i < SEM_LIST_LENGTH; i++)
{
if(sem_list[i].name[0] == '\0'){
tmp=i;
continue;
}
for(k=0;nbuf[k]!='\0';k++)
{
if(sem_list[i].name[k]!=nbuf[k])
break;
}
if(nbuf[k]!='\0')
continue;
if(sem_list[i].name[k]=='\0'){
result=&sem_list[i];
printk("sem %s is found\n",result->name);
return result;
}
/* if(!strcmp(sem_list[i].name, nbuf))
{
result = &sem_list[i];
printk("sem %s is found\n",result->name);
return result;
}
*/
}
if(tmp==-1)
printk("sem_list is full");
/* 如果找不到信号量,就开始新建一个名字为name的信号量,值=value,队列指针=NULL,然后返回信号量的地址 */
for(k=0;nbuf[k]!='\0';k++)
sem_list[tmp].name[k]=nbuf[k];
sem_list[tmp].name[k]='\0';
sem_list[tmp].value = value;
sem_list[tmp].queue = NULL;
result = &sem_list[tmp];
printk("sem %s is created , value = %d\n",sem_list[tmp].name,sem_list[tmp].value);
/*printk("sem %s is created , value = %d\n",result->name,result->value);*/
return result;
}
int sys_sem_wait(sem_t *sem){
//judge
if(sem==NULL || sem<sem_list || sem>(sem_list+SEM_LIST_LENGTH)){
printk("P(sem) error!\n");
return -1;
}
//while(condition){sleep_on}
//sem->value--
cli();
while(sem->value == 0){
sleep_on(&(sem->queue));
}
sem->value--;
sti();
return 0;
}
int sys_sem_post(sem_t *sem){
//judge
if(sem==NULL || sem<sem_list || sem>(sem_list+SEM_LIST_LENGTH)){
printk("V(sem) error!\n");
return -1;
}
cli();
sem->value++;
if(sem->value<=1){
wake_up(&(sem->queue));
}
sti();
return 0;
}
int sys_sem_unlink(const char *name){
if(name==NULL){
printk("name is NULL!\n");
return -1;
}
char nbuf[20];
sem_t *result = NULL;
int i = 0;
for(i = 0;i<20;i++){
nbuf[i] = get_fs_byte(name+i);
if (nbuf[i] == '\0')
break;
}
int j = 0;
for(j = 0;j<SEM_LIST_LENGTH;j++){
if(!(strcmp(nbuf,sem_list[j].name))){
printk("Delete the sem,find the name %s",sem_list[j].name);
strcpy(sem_list[j].name,"\0");
sem_list[j].value = 0;
sem_list[j].queue = NULL;
break;
}
}
if(j==SEM_LIST_LENGTH){
return -1;
}
return 0;
}
这里说一下大致的实现思路:
对于sem_open 以及sem_link两个函数来说比较简单,sem_open就是在sem_list队列中查找名字为name的信号量,如果有就返回,如果没有就新建一个。sem_unlink也类似,只是改成查找到了就删除这个信号量即可。这两个函数不难,基本上就涉及字符处理。
接下来两个函数,sem_wait P操作,sem_post V操作。我看了好多blog,每个人的答案好像都不太一样,我就说说我的思路。
对于wait来说,wait就相当于这个进程获取到了一个信号量,因此信号量的值–即可,但是如果信号量为0即没有可用资源,那么进程进入等待队列,伪代码应当如下:
cli();
if sem->value()<=0: //这里等于0也行,因为sem.value>=0
pause(sem->queue)
else:
sem->value--;
sti();
但是鉴于Linux0.11特殊的等待队列设计,这个之前的进程切换实验应该接触过,代码被写成了如下这样:
cli();
while(sem->value == 0){
sleep_on(&(sem->queue));
}
sem->value--;
sti();
这里解释一下,由于Linux0.11中wake_up函数会解锁等待队列中所有的进程,因此假设有两个等待进程a和b都在等待队列中,这个时候他们应该执行到sleep_on();函数的分号。如果是if判断式,那么a和b进程在执行完分号后都会被wake_up函数唤醒,从而都跳出 },进入了就绪状态。那么这两个进程接下来都可以执行sem->value–操作,但是信号量只+1,会出现同步错误。因此应当使用while判别,这样当a,b两个进程都执行完了sleep_on的;,那么他们不会立刻执行sem->value–操作,而是会再进行一个判定比较,比较sem->value == 0。我们假设a先执行,那么a在判定过后(此时value=1)会执行sem->value–,从而使b进程继续堵塞。
对于post函数来说,主要的逻辑就是回收信号量,所以可用的信号量 ++即可,但是需要判断是否等待队列中还有进程,如果有的话应当唤起沉睡进程。:
cli();
sem->value++;
if(sem->value<=1){
wake_up(&(sem->queue));
}
sti();
我看了很多blog对于这个sem->value<=1还是<=0还是>0都有歧义,下面说说我的理解。当要执行V操作时,假如此刻等待队列中存在进程,那么sem->value应该=0。那么在sem->value+=1后,sem->value的值应当变成1。而sem->value > 1时,说明原先在执行V操作时,信号量就已经有剩余,那么就不需要执行唤醒操作,不过唤醒一下也没关系。
2. 生产者与消费者(pc.c)
首先我们需要了解一下关于文件的IO操作
https://blog.csdn.net/zhangts318/article/details/123833102?ops_request_misc=&request_id=&biz_id=102&utm_term=C语言IO&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-123833102.nonecase&spm=1018.2226.3001.4187
接下来是我的pc.c函数:
#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value);
_syscall1(int,sem_wait,sem_t *,sem);
_syscall1(int,sem_post,sem_t *,sem);
_syscall1(int,sem_unlink,const char *,name);
int consumerNum = 5; /* 消费者个数 */
const int maxNum = 500; /* 写入的数据量 */
const int bufSize = 10; /* 缓冲区大小 */
int data = -404; /* 存放读取的数据*/
pid_t p_pid[5]; /* 进程号数组*/
sem_t *empty, *full, *mutex; /*三个信号量 */
void producer(){
int i;
int end_count = 0;
int endpos_produce = 0; /* 记录消费者进程的消费次数*/
int fo,fx;
char buf[4];
printf("Ready to open!\n");
fo = open("report.txt", O_RDWR|O_CREAT|O_TRUNC, 0666);
fx = open("out.txt", O_RDWR | O_CREAT | O_APPEND);
/*printf("the number of fo is:%d\n",fo);*/
if (fo == -1||fx==-1)
{
perror("打开文件失败!\n");
return;
}
lseek(fo, bufSize * sizeof(int), SEEK_SET);
sprintf(buf, "%d", end_count);
write(fo, buf, sizeof(int));
i = 0;
for(i = 1; i <= maxNum+10; i++){
printf("print i: %d",i);
sem_wait(empty);
sem_wait(mutex);
lseek(fo, endpos_produce * sizeof(int), SEEK_SET);
sprintf(buf, "%d", i);
write(fo, buf, sizeof(int));
/*将生产信息写到out文件*/
write(fx, "p", 1);
write(fx, ":", 1);
write(fx, buf, sizeof(int));
write(fx, "\n", 1);
fflush(stdout);
endpos_produce = (endpos_produce + 1) % bufSize;
sem_post(mutex); /* 出了临界区,需要mutex++,以便下一次可以进入 */
sem_post(full); /* 看是不是需要唤醒阻塞 */
}
close(fo);
}
void consumer(){
char buf[4];
char data[4];
char pid[4];
int endpos_consumer = 0;
int n = 0;
int end_data = 0;
int id;
int fi = open("report.txt", O_RDONLY);
int fx = open("outc.txt", O_RDWR | O_CREAT | O_APPEND);
if (fi == -1 || fx == -1) {
perror("创建文件缓冲区失败!\n");
return;
}
while(1){
sem_wait(full);
sem_wait(mutex);
lseek(fi, bufSize * sizeof(int), SEEK_SET);
n = read(fi, buf, sizeof(int));
if(n==0){
endpos_consumer = 0;
}
sscanf(buf, "%d", &endpos_consumer);
lseek(fi, endpos_consumer * sizeof(int), SEEK_SET);
read(fi,data,sizeof(int));
sprintf(data, "%d", end_data);
if(end_data>=500){
sem_post(mutex);
sem_post(empty);
break;
}
write(fx, "c", 1);
write(fx, "-", 1);
id = getpid();
sprintf(pid, "%d", id);
write(fx, pid, sizeof(int));
write(fx, ":", 1);
write(fx, data, sizeof(int));
write(fx, "\n", 1);
endpos_consumer = (endpos_consumer + 1) % bufSize;
sprintf(buf, "%d", endpos_consumer);
lseek(fi, bufSize * sizeof(int), SEEK_SET);
write(fi,buf,sizeof(int));
fflush(stdout);
sem_post(mutex);
sem_post(empty);
}
close(fi);
close(fx);
}
int main(){
empty = sem_open("empty", bufSize);
if (empty == NULL)
{
perror("empty create falied!\n");
return -1;
}
full = sem_open("full", 0);
if (full == NULL)
{
perror("full create failed!\n");
return -1;
}
mutex = sem_open("mutex", 1);
if (mutex == NULL)
{
perror("mutex create failed!\n");
return -1;
}
if (empty && full && mutex)
{
printf("create semphore successed!\n");
}
if (!fork())
{
printf("producer is running!\n");
producer();
exit(0); /* 生产者任务完成后,杀死该子进程 */
}
while (consumerNum--)
{
p_pid[consumerNum] = fork();
if (!p_pid[consumerNum])
{
printf("consumer %d is running!\n", getpid());
consumer();
exit(0);
}
}
wait(NULL);
/* 关闭信号量 */
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
return 0;
}
这里大致说一下我的思路,首先我们应当了解生产者与消费者的整体框架:
Producer()
{
// 生产一个产品 item;
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
// 产品资源
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}
所有的pc.c都应该按照这个框架进行设计。只是我们需要用文件IO操作,模拟一个共享内存下生产者与消费者如何利用信号量对内存内容进行写与读取。
对于生产者:我是设计生产者固定的向report.txt中写入内容,每次写的位置都在lseek(fo, 0, SEEK_SET)到lseek(fo, (bufSize-1) * sizeof(int), SEEK_SET);如果把他理解为一个数组,就是num[0:bufSize-1]。
而对于消费者:由于消费者有多个,所以应当由一个变量endposs_consumer用来存储消费者在report.txt的读取位置。那么多个消费者怎么保证这个endposs_consumer是共享一致的,我是将他存放在了report.txt的seek(fo, bufSize * sizeof(int), SEEK_SET)的位置,用数组理解就是num[bufsize]。当然你也可以将他固定的存放到另一个文件中。然后每个消费者读取完数据,就将他读取的内容存放到一个日志文件里,用来验证实验的正确性。
有一个比较麻烦的问题是,对于每一个消费者进程来说,他们能够读取到的数据的数量都不一致,所以要用什么判断条件来终止这些消费者进程。有些人的方法是每个进程只处理(total_num/consumer_num),但似乎有点bug?我的方法就相对来说比较偷懒,我让生产者额外的多打印了bufsize组的数据,即总共打印(bufsize+total_num)个,一旦进程读取到了大于total_size的数,就结束进程。(这样做似乎也有点问题?)
到这里,整个实验就完成了。