前言:这次操作系统课上老师介绍了信号量同步机制,并且给我们留了实践作业,在此记录一下比较具有代表性的实践作业。
题目:将共享内存中的例子中加入信号量机制,从而使得每个写入共享内存的信息读且只被读一次。
分析:在例题中老师已经给我们实现了共享内存,因此这道题本质上就是让我们自己实现信号量机制,然后在读取和写入共享内存时使用P、V操作即可。
在这里我就不对信号量进行过多的描述,直接开始分析我的核心代码。
(1).sem头文件
在此文件中主要用来实现信号量的初始化,删除和PV操作。
头文件和要实现的方法:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
void init_sem(int, int);
void del_sem(int);
void sem_p(int);
void sem_v(int);
信号量的union semum结构:
union semun { //信号量
int val; //值
//struct semid_ds *buf;
//unsigned short *array;
};
初始化信号量函数:
void init_sem(int id, int val) {
union semun sem;
sem.val = val;
if (semctl(id, 0, SETVAL, sem) < 0) {
fprintf(stderr, "init failed:%s\n", strerror(errno));
exit(-1);
}
}
其中用到了semctl()函数,该函数可以直接控制信号量信息。也就是直接删除信号量或者初始化信号量。
原型:int semctl(int semid,int semnum,int cmd, /*union semun arg*/);
参数:
int semid //信号量(集)的标识符,可通过semget获取。
int semnum //要给该信号量集中的第semnum个信号量设置初值。
int cmd //通常用SETVAL/GETVAL设置和获取信号量集中的一个单独的信号量。
参数cmd中可以使用的命令如下:
·IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
·IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
·IPC_RMID将信号量集从内存中删除。
·GETALL用于读取信号量集中的所有信号量的值。
·GETNCNT返回正在等待资源的进程数目。
·GETPID返回最后一个执行semop操作的进程的PID。
·GETVAL返回信号量集中的一个单个的信号量的值。
·GETZCNT返回正在等待完全空闲的资源的进程数目。
·SETALL设置信号量集中的所有的信号量的值。
·SETVAL设置信号量集中的一个单独的信号量的值。
第四个参数arg代表一个semun的实例。semun是在linux/sem.h中定义的:
union semun {
int val; //信号量初始值(通常有他就够了)
struct semid_ds *buf; //在IPC_STAT/IPC_SET中使用,代表了内核中使用的信号量的数据结构
unsigned short int *array; //使用GETALL/SETALL命令时使用的指针
struct seminfo *__buf; //IPC_INFO的缓冲区
};
返回值:
成功执行时,则为一个正数;
如果失败,则返回-1,errno被设为以下的某个值:
EACCES(权限不够)
EFAULT(arg指向的地址无效)
EIDRM(信号量集已经删除)
EINVAL(信号量集不存在,或者semid无效)
EPERM(EUID没有cmd的权利)
ERANGE(信号量值超出范围)
删除信号量的函数:
void del_sem(int id) {
union semun sem;
if (semctl(id, 0, IPC_RMID, sem) < 0) { //(第三个参数即为删除操作)
fprintf(stderr, "delete failed:%s\n", strerror(errno));
exit(-1);
}
}
P操作函数:
void sem_p(int id) {
struct sembuf sb;
//sb = {0, -1, 0}; //两种赋值方式
sb.sem_num = 0; //信号在信号集中的索引
sb.sem_op = -1; //操作类型
sb.sem_flg = 0; //操作标志
if (semop(id, &sb, 1) < 0) { //系统调用(此函数具有原子性)
printf("failed to execute P (id:%d)\n", id);
fprintf(stderr, "failed to execute P:%s\n", strerror(errno));
exit(-1);
}
}
其中用semop()函数来改变信号量的值。
原型:int semop(int semid,struct sembuf *sops,size_t nsops)
参数:
int semid //信号量(集)的标识符,可通过semget获取。
第二个参数sops是指向存储信号操作结构的数组指针,该结构定义在 linux/sem.h中:
struct sembuf {
unsigned short sem_num; //信号量编号
short sem_op; //信号量操作
short sem_flg; //操作标志
};
/*
注意:
这里的sem_op如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。
而sem_flg信号操作标志,可能的选择有两种:
IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
SEM_UNDO : 程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
*/
int nsops //信号操作结构的数量,恒大于或等于1。
返回值:
成功执行时,两个系统调用都返回0;
如果失败,则返回-1,errno被设为以下的某个值:
E2BIG:一次对信号的操作数超出系统的限制
EACCES:调用进程没有权能执行请求的操作,并且不具有CAP_IPC_OWNER权能
EAGAIN:信号操作暂时不能满足,需要重试
EFAULT:sops或timeout指针指向的空间不可访问
EFBIG:sem_num指定的值无效
EIDRM:信号集已被移除
EINTR:系统调用阻塞时,被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ERANGE:信号所允许的值越界
V操作函数(同上,仅需把sem_op设置为1即可):
void sem_v(int id) {
struct sembuf sb;
//sb = {0, 1, 0};
sb.sem_num = 0;
sb.sem_op = 1;
sb.sem_flg = 0;
if (semop(id, &sb, 1) < 0) {
printf("failed to execute V (id:%d)\n", id);
fprintf(stderr, "failed to execute V:%s\n", strerror(errno));
exit(-1);
}
}
(2).reader部分
首先,通过semget函数获得semid,然后初始化信号量:
if ((semid = semget(key, 1, IPC_CREAT | 0666)) < 0) {
perror("failed to semget");
exit(-1);
}
init_sem(semid, 0);
原型:int semget(key_t key, int nsems, int semflg)
参数:
key_t key //所创建或打开信号量集的键值。
int nsems //创建的信号量集中的信号量的个数,需要注意的是,该参数只在创建信号量集时有效。
int semflg //调用函数
返回值:
成功执行时,返回信号量集的IPC标识符。
如果失败,则返回-1,errno被设为以下的某个值:
EACCES:没有访问该信号量集的权限
EEXIST:信号量集已经存在,无法创建
EINVAL:有下面两种可能:
1.参数nsems的值小于0或者大于该信号量集的限制
2.该key关联的信号量集已存在,并且nsems大于该信号量集的信号量数
ENOENT:信号量集不存在,同时没有使用IPC_CREAT
ENOMEM :没有足够的内存创建新的信号量集
ENOSPC:超出系统限制
然后,我们就可以实现程序的主体部分:
while (1) {
sem_p(semid); //在读之前必须要求信号量大于0(即写过数据)
strncpy(buf1, shmaddr1, SIZE); //读出数据
printf(" read : %s\n", buf1);
}
当然,这样写是有很大问题的,我后面会做说明。
(3).writer部分(初始化部分同reader部分,此处省略)
主体部分:
for (i = 0; i <= 10; i++) { //循环发送数据
buf2[17] = (char)('0' + i);
strncpy(shmaddr2, buf2, SIZE); //写入数据
printf("write %d : %s\n", i, buf2);
sem_v(semid); //写完后增加信号量(V操作),使得reader可以读到其中信息
}
同样,这样写是有很大问题的。
(4).运行结果
我的reader只读到了writer发送的最后一个消息,很显然,运行结果有很大的问题,那么问题出在哪里呢?又该怎么解决呢?
(5).解决方法一
思考:首先,我目前的信号量是没有问题的,如果我加信号量有问题的话reader和writer的读写次数会不一样,所以我是否能想出一个办法,使我的reader在writer每次写完数据后能读到数据。
于是,我想到了让writer在每次读完后sleep(1),给reader留足够的时间去读数据,实现起来也很简单,只需在writer的最后部分加上sleep(1)即可。
for (i = 0; i <= 10; i++) {
buf2[17] = (char)('0' + i);
strncpy(shmaddr2, buf2, SIZE);
printf("write %d : %s\n", i, buf2);
sem_v(semid);
sleep(1);
}
然后让我们运行一下:
这样看貌似没啥问题了哈,但是总感觉有些问题,于是去问了下老师:
呵......(我明明完成题目要求了呀)
但是这样看来sleep()就不能用了(其实我感觉这也有点投机取巧),没办法,在想想看喽。
(6).解决方法二
既然不能sleep(),那我只要让reader和writer不同时运行不就好了吗,所以我只要再加一个信号量(其实从上面运行结果的截图就可以看出来我用了两个信号量),使我的进程互斥即可。
第二个信号量初始化只需要换一个key的值即可(下面以writer为例)
for (i = 0; i <= 10; i++) {
sem_p(semid2);
//...原来的代码
sem_v(semid2);
}
运行结果:
呵...
但是,作为一个热爱学习的人,这怎么能放弃呢?
于是,在我不断的调试过程中我发现,如果把writer写成死循环的话:
咦,这样好像没啥问题,但是原来的运行结果是怎么回事呢......
哎,反正这个方法也不能用(但其实只要稍微改一下代码就是正解)。
(7).解决方法三(正解)
其实早在讲进程的同步问题中,我们就遇到过此类问题,即读者-写者问题,它要求允许多个进程同时读一个共享对象,但不允许writer与其他writer或reader同时访问共享对象。
因此我们只要确保writer写完后,reader才能读;reader读完后,writer才能写即可。
下面附上代码:
writer部分
for (i = 0; i <= 10; i++) {
sem_p(semid2); //申请信号量(P操作,初始值为1),使得信息写完(进行P操作)以后才能读
buf2[17] = (char)('0' + i);
strncpy(shmaddr2, buf2, SIZE);
printf("write %d : %s\n", i, buf2);
sem_v(semid); //写完后增加信号量(V操作),使得reader可以读到其中信息
}
reader部分
while (1) {
sem_p(semid); //等信息写完(进行P操作)以后才能读
strncpy(buf1, shmaddr1, SIZE);
printf(" read : %s\n", buf1);
sem_v(semid2); //等信息读完(进行V操作)以后才能写
}
这里需要特别注意:semid2的初始值必须为1(必须先要给writer写的权限,不然整个程序没有意义)
最后的运行结果:
这样的话问题就解决了!不容易呀