1. 任务简介
一个数据文件或记录可被多个进程共享,我们把只要求读该文件的进程称为 reader
进稈,其他进程称为writer
进程。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但不允许一个writer
进程和其他reader
进程或writer
进程同时访回共享对象,因为这种访问将会引起混乱。所谓读者-写者问题(reader-writer problem)
是指保证一个writer
进程必须与其他进程互斥地访问共享对象的向步问题。读者-写者问题常被用手测试新同步原语。
读者-写者问题被提出后,就一直被用手测试几乎所有的新同步原语。该问题有多个变种它们都与优先级有关。最为简单的通常被称为 “第一”读者-写者问题。该问题要求没有读者蛋要保持等待,除非有一个写者已被允许使用共享对象,换言之,如果有读者在访问对象则不管有没有写者在等待,后续读者都可以进行读操作。“第二”读者-写者问题要求:一旦写者就绪,那么写者会尽可能快地执行写操作。换言之,如果有一个写者在等待访问对象,那么就不会有新读者开始读操作。对这两个问题的解答都有可能导致饥饿。第一种情况下,写者可能饥饿;第二种情况下,读者可能饥饿。
本实验要求利用Linux多进程实现读写者问题。
2. 思路分析
我们分析题目中的同步和互斥关系:
2.1 同步关系
本题中并不存在需要特别关注的同步关系
2.2 互斥关系
- 当写者进程执行写操作时,其他的写进程无法写
- 当写者进程执行写操作时,读者进程无法读
- 当读者进程执行读操作时,写着进程无法写
2.3 整体思路
这个问题中只有互斥关系,单相较于哲学家问题,其互斥的实现更为复杂,主要要考虑读写者进程对于文件的互斥访问。根据 “2.实验任务,我们按照读写进程的优先顺序,可将读写者问题分为 “第一”读者-写者问题 和 “第二”读者-写者问题。针对以上两个问题,我们分别采取读优先和写优先的策略。此外,由于两种问题都有可能会产生饥饿,我们针对饥饿问题给出了第三种解决方案,采取读写公平法。
下面我们针对以上三种问题的实现思路进行解析:
- “第一”读者-写者问题: 设置一个信号量
reader-writer mutex
,用于实现读者进程和写者进程的互斥访问。另外,设置一个变量readerCount
用于记录读者进程的数目,当readerCount=0
时,第一个读者进程给reader-writer mutex
上锁,保证写者进程无法同时执行写操作;当readerCount>0
时,读者进程可以直接进入,保证不同的读者进程可以同时执行读操作。当且仅当所有的读操作执行完毕时才会给reader-writer mutex
解锁,从而保证了读优先的策略。 - “第二”读者-写者问题: 设置一个信号量
reader-writer mutex
,用于实现读者进程和写者进程的互斥访问。另外,设置一个变量writerCount
用于记录写者进程的数目,当writerCount=0
时,第一个写者进程给reader-writer mutex
上锁,保证写者进程无法同时执行写操作;当readerCount>0
时,读者进程可以直接进入,但由于写操作无法同时操作,所以又需要一个信号量write mutex
,实现对于文件的写互斥和排队等待。当且仅当所有的写操作执行完毕时才会给reader-writer mutex
解锁,从而保证写优先的策略。 - 读写公平法: 在第一”读者-写者的基础上,另外设置一个信号量
mutex2
用于标识读者和写者访问文件的机会。当写者进入时,任何读者无法取得该机会;当读者进行读操作时,先给mutex2
上锁,然后释放,再开始读取文件,由此可以保证多个读进程可以同时读,同时,释放mutex2
后由于readerCount
已经上锁,故写者无法进行写操作,从而保证了读者进程执行读操作时,写着进程无法写。这样无论写进程还是读进程想要进入,都需要进行排队,服务的原则满足先到先得,较为公平。
程序具体流程如下图所示:
- 读者优先流程图:
- 写者优先流程图:
- 公平调度流程图:
3. 代码实现
3.1 头文件
首先,我们包含实现问题所需的头文件:
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
3.2 预定义和数据结构
- 我们采用共同协商关键字
SEMKEY
,SHMKEY
的方法使得不同进程间可以取得同一个信号量和共享内存 - 定义了一个结构体
Buffer
用于模拟缓冲区的读写操作
#define SEMKEY 123
#define SHMKEY 456
#define BUFNUM 50
#define SEMNUM 4
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* union semun is defined by including <sys/sem.h> */
#else
/* according to X/OPEN we have to define it ourselves */
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
#endif
struct Buffer
{
int length, writerCount, readerCount;
char buffer[BUFNUM];
};
3.3 初始化函数
- 我们利用协商好的
SEMKEY
生成一个信号量集,其中第1
个信号量为reader-write
,表示读写锁,用于控制读写互斥访问;第2
个信号量为mutex
,标识对于进程计时器count
的互斥访问;第3
个信号量为writer mutex
,控制写进程间对于缓冲区的互斥访问(写优先策略中将会使用);第4
个信号量为mutex2
,标识读者和写者访问文件的机会,并引导读写进程进行公平排队(读写公平策略中将会使用) - 利用协商好的
SHMKEY
生成一个共享内存集,用于存放结构体Buffer
- 我们利用
*returnSemId
,*returnShmId
和**returnShm
三个指针来返回初始化的参数
具体实现如下:
void Initialize(int *returnSemId, int *returnShmId, struct Buffer **returnShm)
{
int semId = -1, shmId = -1, values[SEMNUM] = {1, 1, 1, 1};
/* semSet[0]: reader-writer mutex, initial value 1
semSet[1]: mutex, initial value 1
semSet[2]: writer mutex, initial value 1
setSet[3]: mutex2, inital value 1*/
semId = semget(SEMKEY, SEMNUM, IPC_CREAT | 0666);
if(semId == -1)
{
printf("semaphore creation failed!\n");
exit(EXIT_FAILURE);
}
int i = 0;
union semun semUn;
for(i = 0; i < SEMNUM; i ++)
{
semUn.val = values[i];
if(semctl(semId, i, SETVAL, semUn) < 0)
{
printf("semaphore %d initialization failed!\n", i);
exit(EXIT_FAILURE);
}
}
shmId = shmget(SHMKEY, sizeof(struct Buffer), IPC_CREAT | 0666);
if(shmId == -1)
{
printf("share memory creation failed!\n");
exit(EXIT_FAILURE);
}
void *temp = NULL;
struct Buffer *shm = NULL;
temp = shmat(shmId, 0, 0);
if(temp == (void *) -1)
{
printf("share memory attachment failed!\n");
exit(EXIT_FAILURE);
}
shm = (struct Buffer *) temp;
shm -> length = 0;
shm -> writerCount = 0;
shm -> readerCount = 0;
shm -> buffer[0] = '\0';
*returnSemId = semId;
*returnShmId = shmId;
*returnShm = shm;
}
3.4 PV操作
给定信号量集的semId
以及待操作的信号量下标semNum
,其P
操作和V
如下所示:
void SemWait(int semId, int semNum)
{
struct sembuf semBuf;
semBuf.sem_num = semNum;
semBuf.sem_op = -1;
semBuf.sem_flg = SEM_UNDO;
if(semop(semId, &semBuf, 1) == -1)
{
printf("semaphore P operation failed!\n");
exit(EXIT_FAILURE);
}
}
void SemSignal(int semId, int semNum)
{
struct sembuf semBuf;
semBuf.sem_num = semNum;
semBuf.sem_op = 1;
semBuf.sem_flg = SEM_UNDO;
if(semop(semId, &semBuf, 1) == -1)
{
printf("semaphore V operation failed!\n");
exit(EXIT_FAILURE);
}
}
3.5 读写操作
-
对于读操作,我们将对共享内存
buffer
进行增加、删除、修改三种之一的操作,以此来模拟真实的文件写操作情况; -
对于写操作,我们将对共享内存
buffer
的内容进行打印,以此来模拟真实的文件读操作情况;
void Write(struct Buffer *shm)
{
char ch, ch1, ch2;
int mode = rand() % 3;
/* mode 0: append
mode 1: delete
mode 2: modify */
// appended when empty
if(mode != 0 && shm -> length == 0) mode = 0;
// deleted when full
else if (mode == 0 && shm -> length == BUFNUM - 1) mode = 1;
switch (mode)
{
case 0:
ch = 'A' + rand() % 26;
printf("writer %d: appended %c into file:\t\t", getpid(), ch);
shm -> buffer [shm -> length ++] = ch;
shm -> buffer [shm -> length] = '\0';
break;
case 1:
ch = shm -> buffer [-- shm -> length];
printf("writer %d: removed %c from file:\t\t", getpid(), ch);
shm -> buffer [shm -> length] = '\0';
break;
case 2:
ch1 = shm -> buffer [shm -> length - 1];
ch2 = 'A' + rand() % 26;
printf("writer %d: modified %c into %c in the file:\t", getpid(), ch1, ch2);
shm -> buffer [shm -> length - 1] = ch2;
break;
default:
printf("writer %d: done nothing", getpid());
break;
}
printf("|%s|\n", shm -> buffer);
}
void Read(struct Buffer *shm)
{
printf("Reader %d: read the file:\t\t\t", getpid());
printf("|%s|\n", shm -> buffer);
}
注意:当缓冲区为空时,我们规定写进程只可以进行添加操作;当缓冲区为满时,我们规定写进程只能进行删除或修改操作。
3.6 “第一”读者-写者
设置一个信号量reader-writer mutex
,用于实现读者进程和写者进程的互斥访问。另外,设置一个变量readerCount
用于记录读者进程的数目,当readerCount=0
时,第一个读者进程给reader-writer mutex
上锁,保证写者进程无法同时执行写操作;当readerCount>0
时,读者进程可以直接进入,保证不同的读者进程可以同时执行读操作。当且仅当所有的读操作执行完毕时才会给reader-writer mutex
解锁,从而保证了读优先的策略。具体实现如下:
void Writer1(int semId, struct Buffer *shm)
{
do{
// wait reader-writer mutex
SemWait(semId, 0);
// write
Write(shm);
// signal reader-writer mutex
SemSignal(semId, 0);
sleep(random() % 2);
}while(1);
}
void Reader1(int semId, struct Buffer *shm)
{
do{
// wait mutex
SemWait(semId, 1);
// wait reader-writer mutex
if(shm -> readerCount ++ == 0) SemWait(semId, 0);
// signal mutex
SemSignal(semId, 1);
// read
Read(shm);
// wait mutex
SemWait(semId, 1);
// signal reader-writer mutex
if(-- shm -> readerCount == 0) SemSignal(semId, 0);
// signal mutex
SemSignal(semId, 1);
sleep(random() % 2);
}while(1);
}
3.7 “第二”读者-写者
置一个信号量reader-writer mutex
,用于实现读者进程和写者进程的互斥访问。另外,设置一个变量writerCount
用于记录写者进程的数目,当writerCount=0
时,第一个写者进程给reader-writer mutex
上锁,保证写者进程无法同时执行写操作;当readerCount>0
时,读者进程可以直接进入,但由于写操作无法同时操作,所以又需要一个信号量write mutex
,实现对于文件的写互斥和排队等待。当且仅当所有的写操作执行完毕时才会给reader-writer mutex
解锁,从而保证写优先的策略。具体实现如下:
void Writer2(int semId, struct Buffer *shm)
{
do{
// wait mutex
SemWait(semId, 1);
// wait Reader-writer mutex
if(shm -> writerCount ++ == 0) SemWait(semId, 0);
// signal mutex
SemSignal(semId, 1);
// wait writer mutex
SemWait(semId, 2);
// write
Write(shm);
// signal writer mutex
SemSignal(semId, 2);
// wait mutex
SemWait(semId, 1);
// signal Reader-writer mutex
if(-- shm -> writerCount == 0) SemSignal(semId, 0);
// signal mutex
SemSignal(semId, 1);
sleep(random() % 2);
}while(1);
}
void Reader2(int semId, struct Buffer *shm)
{
do{
// wait reader-writer mutex
SemWait(semId, 0);
// read
Read(shm);
// signal reader-writer mutex
SemSignal(semId, 0);
sleep(random() % 2);
}while(1);
}
3.8 读者-写者公平策略
在第一”读者-写者的基础上,另外设置一个信号量mutex2
用于标识读者和写者访问文件的机会。当写者进入时,任何读者无法取得该机会;当读者进行读操作时,先给mutex2
上锁,然后释放,再开始读取文件,由此可以保证多个读进程可以同时读,同时,释放mutex2
后由于readerCount
已经上锁,故写者无法进行写操作,从而保证了读者进程执行读操作时,写着进程无法写。这样无论写进程还是读进程想要进入,都需要进行排队,服务的原则满足先到先得,较为公平。具体实现如下:
void Writer3(int semId, struct Buffer *shm)
{
do{
// wait mutex2
SemWait(semId, 3);
// wait reader-writer mutex
SemWait(semId, 0);
// write
Write(shm);
// signal reader-writer mutex
SemSignal(semId, 0);
// signal mutex2
SemSignal(semId, 3);
sleep(random() % 2);
}while(1);
}
void Reader3(int semId, struct Buffer *shm)
{
do{
// wait mutex2
SemWait(semId, 3);
// wait mutex
SemWait(semId, 1);
// wait reader-writer mutex
if(shm -> readerCount ++ == 0) SemWait(semId, 0);
// signal mutex
SemSignal(semId, 1);
// signal mutex2
SemSignal(semId, 3);
// read
Read(shm);
// wait mutex
SemWait(semId, 1);
// signal reader-writer mutex
if(-- shm -> readerCount == 0) SemSignal(semId, 0);
// signal mutex
SemSignal(semId, 1);
sleep(random() % 2);
}while(1);
}
3.9 主函数
主函数负责对变量的初始化,产生读者-写者进程,以及最后的资源回收。
int main(int argc, char *argv[])
{
int semId = -1, shmId = -1, i=0;
int processNum = atoi(argv[2]);
if(processNum <= 0) processNum = 1;
struct Buffer *shm = NULL;
Initialize(&semId, &shmId, &shm);
for(i = 0; i < 2 * processNum; i ++)
{
pid_t pid = fork();
if(pid < 0)
{
printf("fork failed!\n");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
sleep(1);
if(i % 2 == 0)
{
printf("writer process %d created\n", getpid());
// Writer1(semId, shm);
// Writer2(semId, shm);
Writer3(semId, shm);
}
else
{
printf("reader process %d created\n", getpid());
// Reader1(semId, shm);
// Reader2(semId, shm);
Reader3(semId, shm);
}
return 0;
}
}
getchar();
Destroy(semId, shmId, shm);
return 0;
}
注意:
getchar()
函数使得父进程接收到一个字符之后,回收系统资源,结束其产生的子进程。
3.10 实验代码
完整实验代码如下:
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#define SEMKEY 123
#define SHMKEY 456
#define BUFNUM 50
#define SEMNUM 4
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* union semun is defined by including <sys/sem.h> */
#else
/* according to X/OPEN we have to define it ourselves */
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
#endif
struct Buffer
{
int length, writerCount, readerCount;
char buffer[BUFNUM];
};
void Initialize(int *returnSemId, int *returnShmId, struct Buffer **returnShm)
{
int semId = -1, shmId = -1, values[SEMNUM] = {1, 1, 1, 1};
/* semSet[0]: reader-writer mutex, initial value 1
semSet[1]: mutex, initial value 1
semSet[2]: writer mutex, initial value 1
setSet[3]: mutex2, inital value 1*/
semId = semget(SEMKEY, SEMNUM, IPC_CREAT | 0666);
if(semId == -1)
{
printf("semaphore creation failed!\n");
exit(EXIT_FAILURE);
}
int i = 0;
union semun semUn;
for(i = 0; i < SEMNUM; i ++)
{
semUn.val = values[i];
if(semctl(semId, i, SETVAL, semUn) < 0)
{
printf("semaphore %d initialization failed!\n", i);
exit(EXIT_FAILURE);
}
}
shmId = shmget(SHMKEY, sizeof(struct Buffer), IPC_CREAT | 0666);
if(shmId == -1)
{
printf("share memory creation failed!\n");
exit(EXIT_FAILURE);
}
void *temp = NULL;
struct Buffer *shm = NULL;
temp = shmat(shmId, 0, 0);
if(temp == (void *) -1)
{
printf("share memory attachment failed!\n");
exit(EXIT_FAILURE);
}
shm = (struct Buffer *) temp;
shm -> length = 0;
shm -> writerCount = 0;
shm -> readerCount = 0;
shm -> buffer[0] = '\0';
*returnSemId = semId;
*returnShmId = shmId;
*returnShm = shm;
}
void Write(struct Buffer *shm)
{
char ch, ch1, ch2;
int mode = rand() % 3;
/* mode 0: append
mode 1: delete
mode 2: modify */
// appended when empty
if(mode != 0 && shm -> length == 0) mode = 0;
// deleted when full
else if (mode == 0 && shm -> length == BUFNUM - 1) mode = 1;
switch (mode)
{
case 0:
ch = 'A' + rand() % 26;
printf("writer %d: appended %c into file:\t\t", getpid(), ch);
shm -> buffer [shm -> length ++] = ch;
shm -> buffer [shm -> length] = '\0';
break;
case 1:
ch = shm -> buffer [-- shm -> length];
printf("writer %d: removed %c from file:\t\t", getpid(), ch);
shm -> buffer [shm -> length] = '\0';
break;
case 2:
ch1 = shm -> buffer [shm -> length - 1];
ch2 = 'A' + rand() % 26;
printf("writer %d: modified %c into %c in the file:\t", getpid(), ch1, ch2);
shm -> buffer [shm -> length - 1] = ch2;
break;
default:
printf("writer %d: done nothing", getpid());
break;
}
printf("|%s|\n", shm -> buffer);
}
void Read(struct Buffer *shm)
{
printf("Reader %d: read the file:\t\t\t", getpid());
printf("|%s|\n", shm -> buffer);
}
void ShmDestroy(int semId, struct Buffer * shm)
{
if(shmdt(shm) < 0)
{
printf("share memory detachment failed!\n");
exit(EXIT_FAILURE);
}
if(shmctl(semId, IPC_RMID, 0) < 0)
{
printf("share memory destruction failed!\n");
exit(EXIT_FAILURE);
}
}
void SemWait(int semId, int semNum)
{
struct sembuf semBuf;
semBuf.sem_num = semNum;
semBuf.sem_op = -1;
semBuf.sem_flg = SEM_UNDO;
if(semop(semId, &semBuf, 1) == -1)
{
printf("semaphore P operation failed!\n");
exit(EXIT_FAILURE);
}
}
void SemSignal(int semId, int semNum)
{
struct sembuf semBuf;
semBuf.sem_num = semNum;
semBuf.sem_op = 1;
semBuf.sem_flg = SEM_UNDO;
if(semop(semId, &semBuf, 1) == -1)
{
printf("semaphore V operation failed!\n");
exit(EXIT_FAILURE);
}
}
void SemDestroy(int semId)
{
union semun semUn;
if(semctl(semId, 0, IPC_RMID, semUn) < 0)
{
printf("semaphore destruction failed!\n");
exit(EXIT_FAILURE);
}
}
void Destroy(int semId, int shmId, struct Buffer *shm)
{
SemDestroy(semId);
ShmDestroy(shmId, shm);
printf("destruction finished! exit\n");
}
void Writer1(int semId, struct Buffer *shm)
{
do{
// wait reader-writer mutex
SemWait(semId, 0);
// write
Write(shm);
// signal reader-writer mutex
SemSignal(semId, 0);
sleep(random() % 2);
}while(1);
}
void Reader1(int semId, struct Buffer *shm)
{
do{
// wait mutex
SemWait(semId, 1);
// wait reader-writer mutex
if(shm -> readerCount ++ == 0) SemWait(semId, 0);
// signal mutex
SemSignal(semId, 1);
// read
Read(shm);
// wait mutex
SemWait(semId, 1);
// signal reader-writer mutex
if(-- shm -> readerCount == 0) SemSignal(semId, 0);
// signal mutex
SemSignal(semId, 1);
sleep(random() % 2);
}while(1);
}
void Writer2(int semId, struct Buffer *shm)
{
do{
// wait mutex
SemWait(semId, 1);
// wait Reader-writer mutex
if(shm -> writerCount ++ == 0) SemWait(semId, 0);
// signal mutex
SemSignal(semId, 1);
// wait writer mutex
SemWait(semId, 2);
// write
Write(shm);
// signal writer mutex
SemSignal(semId, 2);
// wait mutex
SemWait(semId, 1);
// signal Reader-writer mutex
if(-- shm -> writerCount == 0) SemSignal(semId, 0);
// signal mutex
SemSignal(semId, 1);
sleep(random() % 2);
}while(1);
}
void Reader2(int semId, struct Buffer *shm)
{
do{
// wait reader-writer mutex
SemWait(semId, 0);
// read
Read(shm);
// signal reader-writer mutex
SemSignal(semId, 0);
sleep(random() % 2);
}while(1);
}
void Writer3(int semId, struct Buffer *shm)
{
do{
// wait mutex2
SemWait(semId, 3);
// wait reader-writer mutex
SemWait(semId, 0);
// write
Write(shm);
// signal reader-writer mutex
SemSignal(semId, 0);
// signal mutex2
SemSignal(semId, 3);
sleep(random() % 2);
}while(1);
}
void Reader3(int semId, struct Buffer *shm)
{
do{
// wait mutex2
SemWait(semId, 3);
// wait mutex
SemWait(semId, 1);
// wait reader-writer mutex
if(shm -> readerCount ++ == 0) SemWait(semId, 0);
// signal mutex
SemSignal(semId, 1);
// signal mutex2
SemSignal(semId, 3);
// read
Read(shm);
// wait mutex
SemWait(semId, 1);
// signal reader-writer mutex
if(-- shm -> readerCount == 0) SemSignal(semId, 0);
// signal mutex
SemSignal(semId, 1);
sleep(random() % 2);
}while(1);
}
int main(int argc, char *argv[])
{
int semId = -1, shmId = -1, i=0;
int processNum = atoi(argv[2]);
if(processNum <= 0) processNum = 1;
struct Buffer *shm = NULL;
Initialize(&semId, &shmId, &shm);
for(i = 0; i < 2 * processNum; i ++)
{
pid_t pid = fork();
if(pid < 0)
{
printf("fork failed!\n");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
sleep(1);
if(i % 2 == 0)
{
printf("writer process %d created\n", getpid());
// Writer1(semId, shm);
// Writer2(semId, shm);
Writer3(semId, shm);
}
else
{
printf("reader process %d created\n", getpid());
// Reader1(semId, shm);
// Reader2(semId, shm);
Reader3(semId, shm);
}
return 0;
}
}
getchar();
Destroy(semId, shmId, shm);
return 0;
}
4. 实验结果
我们通过gcc
编译器编译源程序reader_writer.c
,生成目标文件reader_writer
我们从控制台输入命令$ ./reader_writer -n 4
,来模拟4
个读写者同时工作的情况:
4.1 “第一”读者-写者
通过调用Writer1(semId, shm),Reader1(semId, shm)
,运行第一读写者程序(读优先),结果如下:
从上图我们可以看出程序大部分时间都在执行读进程,当有程序进行读时,不会有写进程执行写操作。进一步验证了读优先的策略的正确性。
4.2 “第二”读者-写者
通过调用Writer2(semId, shm),Reader2(semId, shm)
,运行第二读写者程序(写优先),结果如下:
从上图我们可以看出程序大部分时间都在执行写进程,当有写进程等待时,不会有读进程执行读操作。进一步验证了写优先的策略的正确性。
4.3 读者-写者公平策略
通过调用Writer3(semId, shm),Reader3(semId, shm)
,运行读者-写者公平策略程序(公平排队),结果如下:
从上图我们可以看出程序大部分执行读写进程的时间大致相同,无论读进程还是写进程到来,都需要排队,遵循先到先得的原则。进一步验证了读写-公平策略的正确性。