1.写在前面:
本实验只完成了信号量的应用,即在linux下写了一个利用信号量解决生产者和消费者的程序。
实验提示中写到,在生产者进程和消费者进程中需要文件读写,我们可以通过标准C的库函数来实现,也可以直接通过对应的系统调用来实现。
如果通过系统调用来实现文件读写的话,文件头应该加上那3行定义的系统调用宏从而进入内核中使用对应的系统调用(像实验二系统调用中iam.c和whoami.c文件那样)。有同学是通过其它C库(非标准C库)来使用了read()、write()函数然后说是使用系统调用完成的。这不属于直接使用系统调用,本质上用的是库函数,只是库不是标准C库,然后库函数的名字跟系统调用名字一样而已。只有通过系统调用宏进入内核的方法才算是直接使用了系统调用,而无论使用标准C库还是其它C库,都是对系统调用的封装,不算是直接使用系统调用。
本文中使用的是标准C的库函数,要注意由于写函数fwrite()是将内容写到了进程空间内的文件缓冲区,父子进程是不共享这个缓冲区的,因此需要执行fflush()将数据更新到磁盘,才能让其他进程读到数据;printf()函数也需要fflush(一般写c程序的时候,只用printf(),加不加fflush(stdout)好像没什么区别,都是立即就显示出结果了,但是用fork()的时候效果就不一样了:在使用多个输出函数连续进行多次输出时,有可能发现输出错误,比如下一个数据在上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。 所以要在 prinf()后加上fflush(stdout); 强制马上输出,避免错误。)
关于文件读写的这3个函数的说明:
1.fseek():函数头如下,该函数的作用就是修改指向FILE文件类型的指针stream,即第一个参数stream。第二个参数offset是偏移量,以字节为单位。第三个参数whence有一些常量供选择,如SEEK_SET指从文件的开头位置来数的意思。
参考https://www.runoob.com/cprogramming/c-function-fseek.html
int fseek(FILE *stream, long int offset, int whence)
如fseek( fp, 5, SEEK_SET )就是把文件指针fp移动到距离文件开头5个字节处。
2.fread():函数头如下,该函数的作用是从stream指针指向的文件中,读取nmemb个元素,每个元素size个字节,将读到的内容放到ptr指向的地址处。
参考https://www.runoob.com/cprogramming/c-function-fread.html
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
如fread(&productid,sizeof(int),1,fp) 就是从fp指向的文件中,读取一个int长度的元素,然后保存到productid变量中
3.fwrite():函数头如下,该函数的作用是向stream指针指向的文件中写入nmemb个元素,每个元素size个字节,写入的内容是ptr 指向的内容。
参考https://www.runoob.com/cprogramming/c-function-fwrite.html
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
如fwrite(&Outpos,sizeof(int),1,fp)是向fp指向的文件中写入1个int长度的Outpos变量中的内容
2.开始写:
先声明一些需要用到的头文件和一些变量的初始化,定义要用到的宏,声明函数。(实际上本部分是在写代码的过程中用到了什么就加进来的,在这里先写出来是为了方便大家看后面代码的过程中可以清楚各个变量的意义)
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/wait.h>
#define ALLNUM 550
#define CUSTOMERNUM 5
#define BUFFERSIZE 10
void Producters(FILE *fp);
void Customer(FILE *fp);
sem_t *empty,*full,*mutex;
FILE *fp;
int Inpos=0;
int Outpos=0;
生产者:
首先确定用什么样的数据结构来实现共享缓冲区,在这里用一个包含11个元素的数组来表示,前10个元素即0~9号用来存放生产者存放的数据,而最后一个元素有着特殊的作用,在之后消费者进程中会用到。
题目要求建立一个生产者进程,它存放数据是从0号位置开始,把0存在0号位置,然后把1存在1号位置、把2存在2号位置。。把9存到9号位置后再次轮转回来把10放到0号位置,把11放到1号位置。。。因此写一个循环,使要存入的数从0开始逐渐增加,那么存放的位置可以通过该数对缓冲区长度10取余数来得到。
void Producters(FILE *fp)
{
int i=0;
for (i=0;i<ALLNUM;i++)
{
sem_wait(empty);
sem_wait(mutex);
fseek( fp, Inpos * sizeof(int), SEEK_SET ); //把fp指针移动到离文件开头Inpos个整数处(Inpos从0开始)
fwrite(&i,sizeof(int),1,fp); //把i写入fp指针指向的文件地址中
fflush(fp);//执行fflush()将数据更新到磁盘,才能让其他进程读到数据。
Inpos=(Inpos +1) % BUFFERSIZE; //使inpos增大1然后对BUFFERSIZE取余数
sem_post(mutex);
sem_post(full);
}
}
消费者:
对于消费者进程,题目要求创建N个,这里我们创建5个消费者进程。题目要求从缓冲区取数据也是按照顺序取的即01234567…所以站在缓存区的角度来看,也是先从0号位置取出数据,再从1号位置取。。。一直到9号,然后轮转回来再次从0号取。但是站在某一个消费者进程的角度看,它取数据的顺序可能是间隔的,比如先是1号消费者取缓冲区0位置的数据,然后2号消费者取缓冲区1位置的数据,然后又是1号消费者取缓冲区2位置的数据,那么对于1号消费者来说它就是间隔取数据的。由于有多个消费者进程,所以我们无法像生产者进程那样设计一个循环就可以知道该读取哪个位置,那么如何告诉每个消费者该取哪个数据呢?我们就要用缓冲区的最后一个元素来表示读到哪个位置了,即每次消费者读取数据时,先要读缓冲区的最后一个元素即10位置,然后根据该位置的值,再从缓冲区对应的位置读数据。然后将读到的数据printf输出,(该数据是不用在缓冲区删除的,因为下次用fwrite函数写入的时候就自动覆盖了),最后更新下次要读的位置(即原来的值+1然后对10取余数)后保存到缓冲区的10位置,以供下次使用。
void Customer(FILE *fp)
{
int j,productid;
for (j=0;j<ALLNUM/CUSTOMERNUM;j++) //指平均每个消费者要取走的次数
{
sem_wait(full);
sem_wait(mutex);
fseek(fp,10*sizeof(int),SEEK_SET); //使fp指向第10个位置(该位置记录了当前读到的位置)
fread(&Outpos,sizeof(int),1,fp); //从fp指针指向的文件地址中读取1次,每次读取sizeof(int)个字节,读取的内容保存到Outpos变量中
fseek(fp,Outpos*sizeof(int),SEEK_SET); //根据刚才判断的要读的位置,改变成对应的指针
fread(&productid,sizeof(int),1,fp); //从对应的地址处读出数据赋值给变量productid
printf("%d: %d\n",getpid(),productid); //输出进程号和读到的内容
fflush(stdout);
Outpos=(Outpos+1)% BUFFERSIZE; //然后更新下一个要读的位置
fseek(fp,10*sizeof(int),SEEK_SET); //接下来两行是把更新后的位置写入缓冲区的10号位置
fwrite(&Outpos,sizeof(int),1,fp);
fflush(fp);
sem_post(mutex);
sem_post(empty);
}
}
main函数:
在main函数中创建并初始化信号量的函数如下:
需要的头文件:#include <semaphore.h>
函数原型:sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
参数name 是信号量的外部名;参数oflag是选择创建或打开一个现有的信号量,该值可以是0、O_CREAT(创建一个信号量)或O_CREAT|O_EXCL;参数mode是文件权限(0777表示最高权限,具体参考linux系统的权限位);参数value是信号量的初始值。该函数返回指向该信号量的指针,如果出错返回SEM_FAILED。
在main函数中打开文件需要用到fopen函数,可参考https://www.runoob.com/cprogramming/c-function-fopen.html
int main(int argc, char ** argv)
{
pid_t producter;
pid_t customer;
empty=sem_open("empty",O_CREAT,0777,10); //创建三个信号量
full=sem_open("full",O_CREAT,0777,0);
mutex=sem_open("mutex",O_CREAT,0777,1);
fp=fopen("./products.txt","wb+"); //建立一个二进制文件,允许读和写。文件指针赋给fp
fseek(fp,10*sizeof(int),SEEK_SET);//填写缓冲区的10号位置,使其初始位置是0
fwrite(&Outpos,sizeof(int),1,fp);
fflush(fp);
//下面父进程作为生产者,然后子进程再创建5次子进程,作为5个消费者
producter=fork();
if(producter != 0)
{
Producters(fp);
}
else
{
for (int i=0;i<CUSTOMERNUM;i++)
{
customer=fork();
if(customer==0)
{
Customer(fp);
break;
}
}
}
wait(NULL); //使父进程最后退出,让5个子进程先退出
wait(NULL);
wait(NULL);
wait(NULL);
wait(NULL);
sem_unlink("empty"); //关闭信号量
sem_unlink("full");
sem_unlink("mutex");
fclose(fp); //关闭文件
return 0;
}
完整代码:
所以完整pc.c文件的代码如下:(我在windows的编辑器中写好后复制粘贴到vim中,发现格式会乱,查了一些资料,说是在vim中输入如下指令:set paste后,再按i进入插入模式粘贴,就不会格式出错了,粘贴完后按ESC进入普通模式后执行如下指令:set nopaste结束paste模式。具体可参考https://www.cnblogs.com/jianyungsun/archive/2012/07/31/2616671.html
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/wait.h>
#define ALLNUM 550
#define CUSTOMERNUM 5
#define BUFFERSIZE 10
void Producters(FILE *fp);
void Customer(FILE *fp);
sem_t *empty,*full,*mutex;
FILE *fp;
int Inpos=0;
int Outpos=0;
int main(int argc, char ** argv)
{
pid_t producter;
pid_t customer;
empty=sem_open("empty",O_CREAT,0777,10);
full=sem_open("full",O_CREAT,0777,0);
mutex=sem_open("mutex",O_CREAT,0777,1);
fp=fopen("./products.txt","wb+");
fseek(fp,10*sizeof(int),SEEK_SET);
fwrite(&Outpos,sizeof(int),1,fp);
fflush(fp);
producter=fork();
if(producter != 0)
{
Producters(fp);
}
else
{
for (int i=0;i<CUSTOMERNUM;i++)
{
customer=fork();
if(customer==0)
{
Customer(fp);
break;
}
}
}
wait(NULL);
wait(NULL);
wait(NULL);
wait(NULL);
wait(NULL);
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
fclose(fp);
return 0;
}
void Producters(FILE *fp)
{
int i=0;
for (i=0;i<ALLNUM;i++)
{
sem_wait(empty);
sem_wait(mutex);
fseek( fp, Inpos * sizeof(int), SEEK_SET );
fwrite(&i,sizeof(int),1,fp);
fflush(fp);
Inpos=(Inpos +1) % BUFFERSIZE;
sem_post(mutex);
sem_post(full);
}
}
void Customer(FILE *fp)
{
int j,productid;
for (j=0;j<ALLNUM/CUSTOMERNUM;j++)
{
sem_wait(full);
sem_wait(mutex);
fseek(fp,10*sizeof(int),SEEK_SET);
fread(&Outpos,sizeof(int),1,fp);
fseek(fp,Outpos*sizeof(int),SEEK_SET);
fread(&productid,sizeof(int),1,fp);
printf("%d: %d\n",getpid(),productid);
fflush(stdout);
Outpos=(Outpos+1)% BUFFERSIZE;
fseek(fp,10*sizeof(int),SEEK_SET);
fwrite(&Outpos,sizeof(int),1,fp);
fflush(fp);
sem_post(mutex);
sem_post(empty);
}
}
一开始我的代码中是没有使用wait(NULL)的,因为我觉得没必要一定让父进程最后退出。不加wait的代码结果看起来有一点点小问题,如图第五行系统以为已经运行完了所以出了绿色字体了,但实际上还没有运行完,之后又打印了最后几行,我觉得可能是父进程一退出系统就会以为运行完毕,所以为了让输出的格式整齐点需要加上wait,即让父进程最后退出。由于子进程最多的父进程有5个子进程,所以要用5次wait(NULL)。加上5个wait后打印的结果就正常了。
还有一个小问题就是我写好pc.c文件编译时,输入gcc -o pc pc.c后出现如下图报错:
我查了一些资料,说是:“因为pthread库并非Linux系统的默认库,编译时注意加上-lpthread参数,以调用链接库”,那么就是说sem_open等函数用到了pthread库,而pthread库看名字应该是与线程相关的,为什么会用到这个库呢?有知道的小伙伴麻烦告诉我一下o(╥﹏╥)o
我在ubuntu中通过指令man sem_open 来查看该函数的信息,如下:给出了所需的头文件,下面箭头处提示了连接时要使用-pthread。我写代码的时候是man过这个函数的,只是当时没有留意下面提示要用-pthread连接,看来以后得看仔细啊。
所以用如下代码编译连接即可:
gcc -o pc pc.c -pthread
编译完成后./pc 运行即可看到屏幕上输出的进程id和读取的数字,可以看到是5个进程交替执行的,读取的数字是按照顺序打印在屏幕上,部分截图如下
3.实验报告题
1.在 pc.c 中去掉所有与信号量有关的代码,再运行程序,执行效果有变化吗?为什么会这样?
如果没有信号量的话,进程之间无法同步协作,因此会出现缓冲区满了生产者还在试图向缓冲区写数据导致抹掉现有数据,或者缓冲区已经空了(实际上可能没有空,只是该读的数据已经读过了)消费者还试图读数据,导致数据被重复读。
2.实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的(实验楼中的题目代码顺序写错了,实验指导书中正确的代码如下图),这样可行吗?
Producer()
{
P(Mutex);
P(Empty);
// 将item放到空闲缓存中;
V(Full);
V(Mutex);
}
Consumer()
{
P(Mutex);
P(Full);
//从缓存区取出一个赋值给item;
V(Empty);
V(Mutex);
}
这样可能会发生死锁,课程视频死锁处理这一节讲过这个问题。比如生产者占用了一个mutex资源,mutex变成0,然后申请empty资源的时候发现已经没有资源了,必须等待消费者释放,而消费者释放不了,因为消费者需要的资源mutex已经被生产者占用了,于是发生死锁。也就是说当一个占用着资源的进程再去申请资源,就有可能发生死锁,除非你能保证要申请的这个资源早晚可以申请上。正确的做法是把mutex信号量的申请和释放放在了内侧,这样的话就使mutex这个信号量变成了一定可以申请得上的信号量,因为如果该信号量被占用的话,完成文件操作后早晚会被释放,就不会出现一直等待这个信号量的情况。