1.Introduction to Tasks
A data file or record can be shared by multiple processes. We call processes that only require reading the fileReader stalks, and other processes are called writer processes. Allow multiple processes to read a shared object at the same time, since reading does not confuse the data file. However, a writer process and other reader or writer processes are not allowed to visit the shared object at the same time, as this access can cause confusion. Reader-writer problemThis refers to a step-by-step problem that guarantees that a writer process must mutually exclusively access shared objects with other processes. Reader-writer problems are often tested manually for new synchronization primitives.
Almost all new synchronization primitives have been tested manually since the reader-writer question was raised. There are several variations of this question that are related to priority. The simplest is often referred to as ** "first"Reader-Writer Question**. This question requires no reader's egg to wait unless a writer is allowed to use a shared object, that is, if a reader is accessing an object, subsequent readers can read regardless of whether or not the writer is waiting. SecondThe Reader-Writer Question asks that once the writer is ready, the writer will write as quickly as possible. In other words, if a writer is waiting for an audience, no new reader will start reading. Answering both questions can cause hunger. In the first case, the writer may be hungry; in the second case, the reader may be hungry.
This experiment requires the use of Linux multi-process to achieve reader and writer problems.
2.Idea Analysis
We analyze synchronization and exclusion in the topic:
2.1 Synchronization relationship
There is no synchronization relationship in this topic that requires special attention
2.2 Mutual Exclusion
- When the writer process writes, other write processes cannot write
- The reader process cannot read when the writer process writes
- Writing process cannot be written when the reader process reads
2.3 Overall Ideas
Only mutually exclusive relationships exist in this problem, and the implementation of mutually exclusive problems is more complex than philosophers'problems, mainly considering the mutually exclusive access of the reader-writer process to the files.The Reader-Writer Problem. For the above two problems, we take the read-first and the write-first strategies respectively. In addition, since both problems are likely to cause hunger, we give a third solution to the hunger problem and adopt a fair read-write approach.
Below we will analyze the implementation ideas of the above three issues:
- "First"Reader-writer problem: Set a semaphore reader-writer mutex to allow mutually exclusive access to the reader and writer processes. In addition, set a variable readerCount to record the number of reader processes. When readerCount=0, the first reader process locks the reader-writer mutex to ensure that the writer process cannot write at the same time; when readerCount > 0, the readerThe reader-writer mutex is unlocked when and only when all the reader-writer mutex operations are completed, thereby guaranteeing a read-first strategy.
- "Second"Reader-writer problem: Set a semaphore reader-writer mutex to allow mutually exclusive access to the reader and writer processes. In addition, set a variable, writerCount, to record the number of writer processes. When writerCount=0, the first writer process locks the reader-writer mutex, ensuring that the writer process cannot write at the same time; when readerCount > 0, the readerProcesses can enter directly, but since write operations cannot operate simultaneously, a semaphore, write mutex, is required to implement write exclusion and queue waiting for files. Reader-writer mutex is unlocked when and only when all write operations are completed, thereby guaranteeing write-first policy.
- Fair Reading and Writing:On the basis of the first reader-writer, another semaphore, mutex2, is set up to identify opportunities for readers and writers to access files. When the writer enters, no reader will be able to access the opportunity. When the reader reads, lock mutex2, release it, and start reading the file. This ensures that multiple reading processes can read at the same time, and at the same time, release mutex2 before being read by the reader.The readerCount is locked, so the writer cannot write, which ensures that when the reader process performs a read operation, the write process cannot write. In this way, both the write process and the read process need to be queued, and the principle of service is first come first served, which is fairer.
The program flow is as follows:
- Reader Priority Flowchart:
- Writer Priority Flowchart:
- Fair Scheduling Flow Chart:
3.code implementation
3.1 Header File
First, we include the header file needed to implement the problem:
#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 Predefined and data structures
- We use the mutual negotiation keyword SEMKEY, SHMKEY method to enable different processes to obtain the same semaphore and shared memory
- A struct Buffer is defined to simulate read and write operations for buffers
#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 Initialization function
- We use the negotiated SEMKEY to generate a semaphore set, where the first semaphore, reader-write, represents a read-write lock used to control read-write mutex access, the second semaphore, mutex, identifies mutex access to process timer count, and the third semaphore, writer mutex, controls mutex access to buffers between write processes (which will be used in write-first policies); the fourth semaphore, mutex2, identifies opportunities for readers and writers to access files and directs the read-write process to a fair queue (which will be used in read-write fairness policies)
- Generate a shared memory set with negotiated SHMKEY to hold the structure Buffer
- We use the *returnSemId, *returnShmId, and **returnShm pointers to return the initialization parameters
The specific implementation is as follows:
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 Operation
Given the semId of the semaphore set and the semNum subscript to be operated on, the P and V operations are as follows:
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 Read and write operations
-
For read operations, we will add, delete, and modify one of the three operations to simulate real file write operations.
-
For write operations, we will print the contents of the shared memory buffer to simulate real file read operations.
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); }
Note: When the buffer is empty, we specify that the write process can only add operations; when the buffer is full, we specify that the write process can only delete or modify operations.
3.6 First Reader-Writer
Set a semaphore reader-writer mutex to allow mutually exclusive access to the reader and writer processes. In addition, set a variable readerCount to record the number of reader processes. When readerCount=0, the first reader process gives reader-writerThe mutex is locked, which ensures that the writer process cannot perform the write operation at the same time; when readerCount > 0, the reader process can enter directly, which ensures that different reader processes can perform the read operation at the same time. The reader-writer mutex is unlocked when and only when all the read operations are completed, thereby guaranteeing the read-first strategy.
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 Second Reader-Writer
Set a semaphore reader-writer mutex to allow mutually exclusive access to the reader and writer processes. In addition, set a variable, writerCount, to record the number of writer processes. When writerCount=0, the first writer process gives reader-writerA mutex locks to ensure that the writer process cannot write at the same time; when readerCount > 0, the reader process can enter directly, but since the write operation cannot operate at the same time, a semaphore, write mutex, is needed to implement write exclusion and queue waiting for files. Reader-writer is given only when all write operations are completedThe mutex is unlocked to ensure write-first policy.
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 Reader-Writer Equity Strategy
On the basis of the first reader-writer, another semaphore, mutex2, is set up to identify opportunities for readers and writers to access files. When the writer enters, no reader will be able to access the opportunity. When the reader reads, lock mutex2, release it, and start reading the file. This ensures that multiple reading processes can read at the same time, and at the same time, release mutex2 before being read by the reader.The readerCount is locked, so the writer cannot write, which ensures that when the reader process performs a read operation, the write process cannot write. In this way, whether the write process or the read process wants to enter, it needs to be queued. The principle of service is first come first served, which is fairer. The implementation is as follows:
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 Principal Functions
The primary function is responsible for initializing variables, generating reader-writer processes, and ultimately recycling resources.
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; }
Note: The getchar() function causes the parent process to recycle system resources and end the child processes it generates after receiving a character.
3.10 Experimental Code
The complete experimental code is as follows:
#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.experimental result
We compile the source program reader_writer.c with the gcc compiler to generate the target file reader_writer
We enter the command $. /reader_writer-n 4 from the console to simulate the simultaneous work of four readers and writers:
4.1 First Reader-Writer
By calling Writer1(semId, shm), Reader1(semId, shm), run the first reader program (read first). The results are as follows:
From the figure above, we can see that the program spends most of its time executing the read process, and when there are programs reading, no write process will perform the write operation. This further verifies the correctness of the read-first strategy.
4.2 Second Reader-Writer
By calling Writer2(semId, shm), Reader2(semId, shm), run the second reader program (write first), the results are as follows:
From the figure above, we can see that the program spends most of its time executing the write process, and when a write process waits, no read process will perform the read operation. This further verifies the correctness of the write-first policy.
4.3 Reader-Writer Equity Strategy
By calling Writer3(semId, shm), Reader3(semId, shm), run the Reader-Writer Fair Policy Program (Fair Queuing). The results are as follows:
From the figure above, we can see that most programs execute read and write processes at approximately the same time. Whether the read process or the write process arrives, they need to queue and follow the principle of first come first served. This further verifies the correctness of the read-write fair policy.