1, Experimental content
- Simulate process synchronization and mutual exclusion in the operating system.
2, Experimental purpose
- Familiar with the definition and physical meaning of critical resources, semaphores and PV operation
- Understand the method of process communication
- Master the knowledge of process mutual exclusion and process synchronization
- Master the semaphore mechanism to solve the problems of synchronization and mutual exclusion between processes
- Realize the producer consumer problem and deeply understand the process synchronization problem
3, Experimental topic
Process implementation
Using C to realize the classic synchronization problem under Linux operating system: producer consumer. The specific requirements are as follows:
- A buffer with the size of 10 and the initial state is empty.
- Two producers wait randomly for a period of time to add data to the buffer. If the buffer is full, wait for the consumer to take the data before adding it, and repeat 10 times.
- Two consumers randomly wait for a period of time to read data from the buffer. If the buffer is empty, wait for the producer to add data before reading, and repeat 10 times.
Tips
The main purpose of this experiment is to simulate the process synchronization and mutual exclusion in the operating system. In the process of concurrent asynchronous advancement of system processes, the mutual restriction between processes is caused by resource sharing and inter process cooperation. There are two different ways to restrict each other between processes.
- Indirect constraints. This is caused by multiple processes sharing the same resources (such as CPU and shared input / output devices), that is, multiple processes sharing resources restrict each other due to the coordinated use of resources by the system.
- Direct constraints. It is only caused by each process in process cooperation to complete the same task, that is, the execution results of concurrent processes are each other's execution conditions, which limits the execution speed of each process.
Producers and consumers are classic process synchronization problems. In this problem, producers constantly write data to the buffer, while consumers read data from the buffer. The operations of the producer process and the consumer on the buffer are mutually exclusive, that is, at present, only one process can operate on this buffer. Before entering the operation buffer, the producer must first see whether the buffer is full. If the buffer is full, it must wait for the consumer process to take out the data before writing the data. Similarly, before the consumer process reads the data from the buffer, It is also necessary to judge whether the buffer is empty. If it is empty, it must wait for the producer process to write data before reading data.
In this problem, we use semaphore mechanism to communicate between processes, and set two semaphores, empty semaphore and full semaphore. In Linux system, one or more semaphores form a semaphore set. Using semaphore mechanism can realize the synchronization and mutual exclusion between processes, and allow concurrent processes to perform the same or different operations on a group of semaphores at a time. Each P, V operation is not limited to minus 1 or plus 1, but can add or subtract any integer. When the process terminates, the system can automatically eliminate the influence of all semaphores operated by the process as needed
-
The buffer is represented by a circular queue, which uses the head and tail pointers to store and read data, and judge whether the queue is empty. The size of the array in the buffer is 10;
-
A random character of a ~ Z is obtained by using the random function rand(), which is stored in the buffer as the data produced by the producer each time;
-
Use the shmget() system call to create the shared main memory segment, and shmget() returns the ID of the shared memory area. For the shared segment that has been applied for, the process needs to attach it to its own virtual space to read and write it.
-
The semaphore is established by semget() function, and the number of semaphores is established at the same time. After the semaphore is set up, call semctl() to initialize the semaphore. For example, in this experiment, two semaphore SEM_ can be set up. EMPTY,SEM_FULL, set SEM during initialization_ Empty = 10, SEM_FULL is 0. Use the function semop() of the operation signal for exclusive operation. Use this function to prevent simultaneous operation on shared memory. After the shared memory operation is completed, use the shmctl() function to undo the shared memory segment.
-
Using the loop, create two producers and two consumers, and use the function fork() to create a new process.
-
After one operation of a process is completed, the buffer is refreshed by the function fflush().
-
Finally, the program uses the semctl() function to free up memory.
- Main program flow chart:
- Producer process flow chart
- Consumer process flow chart
- P operation flow chart
- V operation flow chart
Thread implementation of thinking questions
Using threads to achieve
4, Experimental design and process
Process implementation
Preparatory knowledge
Like semaphores, Linux also provides a set of function interfaces for using shared memory, and the interface using shared coexistence is very similar to that of semaphores, and it is simpler than that of semaphores. They are declared in the header file sys / SHM H medium.
1. shmget() function
This function is used to create shared memory. Its prototype is:
int shmget(key_t key, size_t size, int shmflg);
-
The first parameter is the same as semget function of semaphore. The program needs to provide a parameter key (non-zero integer), which effectively names the shared memory segment. When shmget() function succeeds, it returns a shared memory identifier (non negative integer) related to key for subsequent shared memory functions. Call failed, return - 1
Unrelated processes can access the same shared memory through the return value of the function, which represents a resource that the program may use. The program accesses all shared memory indirectly. The program first calls shmget() function and provides a key, and then the system generates a corresponding shared memory identifier (return value of shmget() function), Only the shmget() function uses the semaphore key directly, and all other semaphore functions use the semaphore identifier returned by the semget function.
-
The second parameter, size, specifies the memory capacity to be shared in bytes
-
The third parameter, shmflg, is the permission flag. Its function is the same as the mode parameter of the open function. If you want to create the shared memory identified by the key when it does not exist, you can work with IPC_CREAT does or operates. The permission flag of shared memory is the same as the read-write permission of files. For example, 0644 indicates that the shared memory created by a process is allowed to be read and written to the shared memory by the process owned by the memory creator. At the same time, the processes created by other users can only read the shared memory.
2. shmat() function
– at: attach
When the shared memory is created for the first time, it cannot be accessed by any process. The shmat() function is used to start the access to the shared memory and connect the shared memory to the address space of the current process. Its prototype is as follows:
void *shmat(int shm_id, const void *shm_addr, int shmflg);
-
First parameter, shm_id is the shared memory ID returned by the shmget() function.
-
Second parameter, shm_addr specifies the address where the shared memory is connected to the current process. It is usually empty, indicating that the system is allowed to select the address of the shared memory.
-
The third parameter, shm_flg is a set of flag bits, usually 0.
When the call succeeds, it returns a pointer to the first byte of shared memory. If the call fails, it returns - 1
3. shmdt() function
– dt: detach
This function is used to separate shared memory from the current process. Note that separating shared memory does not delete it, but just makes the shared memory no longer available to the current process. Its prototype is as follows:
int shmdt(const void *shmaddr);
Parameter shmaddr is the address pointer returned by shmat() function. It returns 0 when the call is successful and - 1 when it fails
4. shmctl() function
– ctl: control
Like semaphore semctl() function, it is used to control shared memory. Its prototype is as follows:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
-
First parameter, shm_id is the shared memory identifier returned by the shmget() function.
-
The second parameter, command, is the operation to be taken. It can take the following three values:
- IPC_STAT: shmid_ The data in the DS structure is set as the current association value of the shared memory, that is, the shmid is overwritten with the current association value of the shared memory_ The value of DS.
- IPC_SET: if the process has sufficient permissions, set the current association value of shared memory to shmid_ The value given in the DS structure
- IPC_RMID: delete shared memory segment
-
The third parameter, buf, is a structure pointer, which points to the structure of shared memory mode and access rights.
shmid_ The DS structure includes at least the following members:
struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
Data structure and symbol description
//The index of each semaphore in the signal set, #define SEM_FULL 0 #define SEM_EMPTY 1 #define MUTEX 2 //Buffer structure struct my_buffer { int out; int in; char str[MAX_BUFFER_SIZE]; int num; //Number of letters in buffer int is_empty; }; const int N_CONSUMER = 2; //Number of consumers const int N_PRODUCER = 2; //Number of producers const int N_BUFFER = 10; //Buffer capacity const int N_WORKTIME = 10; //Number of work int shm_id = -1;//Semaphore id int sem_id = -1;//Shared memory id //id of the child process pid_t consumer_id; pid_t producer_id; //Get a random number within 10 int get_random() //sem_id represents the id of the semaphore set //sem_num indicates the index of the semaphore to be processed in the semaphore set //P operation void waitSem(int sem_id, int sem_num) //V operation void signalSem(int sem_id, int sem_num) //Print process run time void printTime() //Consumer process consumption and information printing void consume_print(int i, struct my_buffer *shmptr) //Consumer process consumption and information printing void consume_print(int i, struct my_buffer *shmptr)
source code
In the main function, first initialize the semaphore (mutex indicates the mutually exclusive access of the buffer, set to 1; SEM_FULL indicates the number of full buffers, set to 0; SEM_EMPTY indicates the number of empty buffers, set to 10), allocate shared memory, initialize the buffer, create producers and consumers, release memory, and then exit.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <sys/types.h> #include <sys/wait.h> #define MAX_BUFFER_SIZE 10 #define SHM_MODE 0600 #define SEM_MODE 0600 //The index of each semaphore in the signal set, #define SEM_FULL 0 #define SEM_EMPTY 1 #define MUTEX 2 //Buffer structure struct my_buffer { int out; int in; char str[MAX_BUFFER_SIZE]; int num; //Number of letters in buffer int is_empty; }; const int N_CONSUMER = 2; //Number of consumers const int N_PRODUCER = 2; //Number of producers const int N_BUFFER = 10; //Buffer capacity const int N_WORKTIME = 10; //Number of work //Semaphore id int shm_id = -1; //Shared memory id int sem_id = -1; //id of the child process pid_t consumer_id; pid_t producer_id; //Get a random number within 10 int get_random() { int digit; digit = rand() % 3; return digit; } //Get A random letter from A to Z char getRandChar() { char letter; letter = (char)((rand() % 26) + 'A'); return letter; } //sem_id represents the id of the semaphore set //sem_num indicates the index of the semaphore to be processed in the semaphore set //P operation void waitSem(int sem_id, int sem_num) { struct sembuf sb; sb.sem_num = sem_num; sb.sem_op = -1; //It means to reduce the semaphore by one sb.sem_flg = 0; // //The second parameter is of type sembuf [] and represents an array //The third parameter represents the size of the array represented by the second parameter if (semop(sem_id, &sb, 1) < 0) { perror("waitSem failed"); exit(1); } } //V operation void signalSem(int sem_id, int sem_num) { struct sembuf sb; sb.sem_num = sem_num; sb.sem_op = 1; sb.sem_flg = 0; //The second parameter is of type sembuf [] and represents an array //The third parameter represents the size of the array represented by the second parameter if (semop(sem_id, &sb, 1) < 0) { perror("signalSem failed"); exit(1); } } //Print process run time void printTime() { //Print time time_t now; struct tm *timenow; //Instantiate tm structure pointer time(&now); timenow = localtime(&now); printf("| %02d:%02d:%02d | ", timenow->tm_hour, timenow->tm_min, timenow->tm_sec); } //Production and information printing of producer process void produce_print(int i, struct my_buffer *shmptr) { //sleep(get_random()); // Random sleep for a period of time, which is equivalent to artificially expanding the operation time of the process printTime(); //Program running time //yield a product char c = getRandChar(); //Random acquisition of letters shmptr->str[shmptr->in] = c; shmptr->in = (shmptr->in + 1) % MAX_BUFFER_SIZE; shmptr->is_empty = 0; //Write new product shmptr->num++; //Print buffer int p = (shmptr->in - 1 >= shmptr->out) ? (shmptr->in - 1) : (shmptr->in - 1 + MAX_BUFFER_SIZE); for (p; !(shmptr->is_empty) && p >= shmptr->out; p--) { printf("%c", shmptr->str[p % MAX_BUFFER_SIZE]); } for (int j = 0; j < 10 - shmptr->num; j++) { printf("%c", '-'); } printf(" | "); //Control output format printf("producer_%d | ", i + 1); //Process id information printf("+ %c |\n", c); //Process operation fflush(stdout); } //Consumer process consumption and information printing void consume_print(int i, struct my_buffer *shmptr) { //sleep(get_random()); printTime(); //Program running time /*yield a product*/ char lt = shmptr->str[shmptr->out]; shmptr->out = (shmptr->out + 1) % MAX_BUFFER_SIZE; shmptr->is_empty = (shmptr->out == shmptr->in); // shmptr->num--; //Print buffer int p = (shmptr->in - 1 >= shmptr->out) ? (shmptr->in - 1) : (shmptr->in - 1 + MAX_BUFFER_SIZE); for (p; !(shmptr->is_empty) && p >= shmptr->out; p--) { printf("%c", shmptr->str[p % MAX_BUFFER_SIZE]); } for (int j = 0; j < 10 - shmptr->num; j++) { printf("%c", '-'); } printf(" | "); //Control output format printf("consumer_%d | ", i + 1); //Process id information printf("- %c |\n", lt); //Process operation fflush(stdout); } int main(int argc, char **argv) { srand((unsigned)(getpid() + time(NULL))); shm_id = shmget(IPC_PRIVATE, MAX_BUFFER_SIZE, SHM_MODE); //Request shared memory if (shm_id < 0) { perror("create shared memory failed"); exit(1); } struct my_buffer *shmptr; shmptr = shmat(shm_id, 0, 0); //Attach the requested shared memory to the process space of the requested communication if (shmptr == (void *)-1) { perror("add buffer to using process space failed!\n"); exit(1); } if ((sem_id = semget(IPC_PRIVATE, 3, SEM_MODE)) < 0) { //Create three semaphores, SEM_EMPTY,SEM_FULL and MUTEX perror("create semaphore failed! \n"); exit(1); } if (semctl(sem_id, SEM_FULL, SETVAL, 0) == -1) { //Set semaphore with index 0 to 0 -- > SEM_ FULL perror("sem set value error! \n"); exit(1); } if (semctl(sem_id, SEM_EMPTY, SETVAL, 10) == -1) { //Set the semaphore with index 1 to 10 -- > SEM_ EMPTY perror("sem set value error! \n"); exit(1); } if (semctl(sem_id, MUTEX, SETVAL, 1) == -1) { //Set the semaphore with index 3 to 1 -- > mutex perror("sem set value error! \n"); exit(1); } //Initialize buffer shmptr->out = 0; shmptr->in = 0; shmptr->is_empty = 1; shmptr->num = 0; printf("---------------------Process Execution Table-----------------\n"); printf("-------------------------------------------------------------\n"); printf("| time | buffer data |current process| operation |\n"); printf("-------------------------------------------------------------\n"); for (int i = 0; i < N_PRODUCER; i++) { sleep(get_random()); producer_id = fork(); if (producer_id < 0) { perror("the fork failed"); exit(1); } else if (producer_id == 0) //If it is a child process, it performs production operations { for (int j = 0; j < N_WORKTIME; j++) { sleep(get_random()); waitSem(sem_id, SEM_EMPTY); waitSem(sem_id, MUTEX); produce_print(i, shmptr); signalSem(sem_id, MUTEX); signalSem(sem_id, SEM_FULL); } exit(0); } } for (int i = 0; i < N_CONSUMER; i++)// { sleep(get_random()); consumer_id = fork(); if (consumer_id < 0) //Failed to call fork { perror("the fork failed"); exit(1); } else if (consumer_id == 0) //If it is a child process, it performs consumption operation { for (int j = 0; j < N_WORKTIME; j++) { sleep(get_random()); waitSem(sem_id, SEM_FULL); waitSem(sem_id, MUTEX); consume_print(i, shmptr); signalSem(sem_id, MUTEX); signalSem(sem_id, SEM_EMPTY); } exit(0); } } //The main process finally exited while (wait(0) != -1) ; //Disconnect the shared segment from the process shmdt(shmptr); //Perform control operations on shared memory areas shmctl(shm_id, IPC_RMID, 0); //When cmd is IPC_ When rmid, delete the shared segment printf("-------------------------------------------------------------\n"); printf("The main process has finished running!\n"); fflush(stdout); exit(0); return 0; }
Program initial value and running result
process
Thread implementation
Preparatory knowledge
pthread_create
pthread_create is a function for creating threads in UNIX environment
Header file:
#include<pthread.h>
Function declaration:
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg); Parameters:
- The first parameter is the pointer to the thread identifier. Pthread can be defined as follows_ t thread1;
- The second parameter is used to set thread properties.
- The third parameter is the address of the thread running function, that is, the function name. The function includes a loop.
- The last parameter is the parameter of the running function.
Return value:
If successful, return 0; otherwise, return the error number
be careful:
When compiling, pay attention to adding the - l pthread parameter to call the static link library. Because pthread is not the default library of Linux system.
pthread_join
Function introduction:
Function pthread_join is used to wait for the end of a thread. pthread_ The join() function waits for the thread specified by thread to end in a blocking manner. When the function returns, the resources of the waiting thread are reclaimed. If the thread has ended, the function returns immediately. And the thread specified by thread must be joinable.
Header file:
#include<pthread.h>
Function declaration:
int pthread_join(pthread_t thread, void **retval);
Parameters:
-
Thread: thread identifier, i.e. thread ID, which identifies the unique thread and is the identifier of the waiting thread.
-
retval: a user-defined pointer used to store the return value of the waiting thread.
Return value:
If the execution is successful, it will return 0. If it fails, it will return an error number.
Data structure and symbol description
const int N_WORKTIME = 10; //Number of work //Three semaphores pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //Mutual exclusion between threads sem_t full; //Number of filled sem_t empty; //Number of empty slots struct my_buffer { int out; int in; char str[MAX_BUFFER_SIZE]; int num; //Number of letters in buffer int is_empty; } buffer; //Buffer structure //struct my_buffer *buffer int get_random()//Get a random number within 3 char getRandChar()//Get A random letter from A to Z void printTime() //Print time void produce_print(int *x)//Production and information printing of producer process void consume_print(int *x)//Consumer process production, consumption and information printing void *produce(void *arg)//Producer thread { int i; int *x = (int *)arg; for (i = 0; i < N_WORKTIME; i++) { sleep(get_random()); sem_wait(&empty); //If the number of empty slots is less than 0, it is blocked pthread_mutex_lock(&mutex); produce_print(x); pthread_mutex_unlock(&mutex); sem_post(&full); } return (void *)1; } void *consume(void *arg)//Consumer thread { int i; int *x = (int *)arg; for (i = 0; i < N_WORKTIME; i++) { sleep(get_random()); sem_wait(&full); //If the number of fills is less than 0, it will be blocked pthread_mutex_lock(&mutex); consume_print(x); pthread_mutex_unlock(&mutex); sem_post(&empty); } return (void *)2; }
source code
#include <stdio.h> #include <pthread.h> #include <time.h> #include <unistd.h> #include <semaphore.h> #include <stdlib.h> #define MAX_BUFFER_SIZE 10 const int N_WORKTIME = 10; //Number of work //Three semaphores pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //Mutual exclusion between threads sem_t full; //Number of filled sem_t empty; //Number of empty slots struct my_buffer { int out; int in; char str[MAX_BUFFER_SIZE]; int num; //Number of letters in buffer int is_empty; } buffer; //Buffer structure //struct my_buffer *buffer; //Get a random number within 3 int get_random() { int digit; digit = rand() % 3; return digit; } //Get A random letter from A to Z char getRandChar() { char letter; letter = (char)((rand() % 26) + 'A'); return letter; } void printTime() //Print time { time_t now; struct tm *timenow; //Instantiate tm structure pointer time(&now); timenow = localtime(&now); printf("| %02d:%02d:%02d | ", timenow->tm_hour, timenow->tm_min, timenow->tm_sec); } void produce_print(int *x)//Production and information printing of producer process { //sleep(get_random()); printTime(); //Program running time /*yield a product*/ char c = getRandChar(); //Random acquisition of letters buffer.str[buffer.in] = c; buffer.in = (buffer.in + 1) % MAX_BUFFER_SIZE; buffer.is_empty = 0; //Write new product buffer.num++; //Print buffer int p = (buffer.in - 1 >= buffer.out) ? (buffer.in - 1) : (buffer.in - 1 + MAX_BUFFER_SIZE); for (p; !(buffer.is_empty) && p >= buffer.out; p--) { printf("%c", buffer.str[p % MAX_BUFFER_SIZE]); } for (int j = 0; j < 10 - buffer.num; j++) { printf("%c", '-'); } printf(" | "); //Control output format printf("producer_%d | ", *x); //Process id information printf("+ %c |\n", c); //Process operation } void consume_print(int *x)//Consumer process production, consumption and information printing { //sleep(get_random()); printTime(); //Program running time /*yield a product*/ char c = buffer.str[buffer.out]; buffer.out = (buffer.out + 1) % MAX_BUFFER_SIZE; buffer.is_empty = (buffer.out == buffer.in); // buffer.num--; //Print buffer int p = (buffer.in - 1 >= buffer.out) ? (buffer.in - 1) : (buffer.in - 1 + MAX_BUFFER_SIZE); for (p; !(buffer.is_empty) && p >= buffer.out; p--) { printf("%c", buffer.str[p % MAX_BUFFER_SIZE]); } for (int j = 0; j < 10 - buffer.num; j++) { printf("%c", '-'); } printf(" | "); //Control output format printf("consumer_%d | ", *x); //Process id information printf("- %c |\n", c); //Process operation } void *produce(void *arg) { int i; int *x = (int *)arg; for (i = 0; i < N_WORKTIME; i++) { sleep(get_random()); sem_wait(&empty); //If the number of empty slots is less than 0, it is blocked pthread_mutex_lock(&mutex); produce_print(x); pthread_mutex_unlock(&mutex); sem_post(&full); } return (void *)1; } void *consume(void *arg) { int i; int *x = (int *)arg; for (i = 0; i < N_WORKTIME; i++) { sleep(get_random()); sem_wait(&full); //If the number of fills is less than 0, it will be blocked pthread_mutex_lock(&mutex); consume_print(x); pthread_mutex_unlock(&mutex); sem_post(&empty); } return (void *)2; } int main(int argc, char *argv[]) { srand(time(NULL)); //Randomization time seed pthread_t thid1; pthread_t thid2; pthread_t thid3; pthread_t thid4; int ret1; int ret2; int ret3; int ret4; //Initialization semaphore sem_init(&full, 0, 0); sem_init(&empty, 0, 10); //Thread id int t1 = 1; int t2 = 2; //Initialize buffer buffer.out = 0; buffer.in = 0; buffer.is_empty = 1; buffer.num = 0; printf("---------------------Thread Execution Table------------------\n"); printf("-------------------------------------------------------------\n"); printf("| time | buffer data | current thread | operation |\n"); printf("-------------------------------------------------------------\n"); //Create thread pthread_create(&thid1, NULL, produce, (void *)&t1); pthread_create(&thid2, NULL, consume, (void *)&t1); pthread_create(&thid3, NULL, produce, (void *)&t2); pthread_create(&thid4, NULL, consume, (void *)&t2); //Block until the thread ends pthread_join(thid1, (void **)&ret1); pthread_join(thid2, (void **)&ret2); pthread_join(thid3, (void **)&ret3); pthread_join(thid4, (void **)&ret4); printf("-------------------------------------------------------------\n"); printf("All threads have been executed,The main process has finished running!\n"); return 0; }
Program initial value and running result
thread
5, Experimental summary
In this experiment, I learned how to make use of shared memory communication to achieve synchronization and mutual exclusion among processes. At the beginning, I didn't know much about these functions such as shmget, shmat, shmdt and shmctl. After that, I learned their usage through consulting data, and encountered many difficulties in writing code. After watching for a long time, I found that the parameters designed for the semaphore in the pv operation were wrong, resulting in a reset at the end of each process, which led to a deadlock. But fortunately, errors were found in the end and solved smoothly.
Think about it. Let's implement it with threads, which is much easier than processes. You only need to apply for a global variable to be used as shared memory. For threads, you need to learn pthread_create and pthread_join usage. Finally, most of the function designs follow the process design and are successfully implemented!