Implementation and application of semaphore
1, Experimental environment
The operating environment of this experiment is the same as the experimental environment. The environmental documents are as follows:
If not, please refer to previous blogs.
2, Experimental objectives and contents
1. Objectives:
-
Deepen the understanding of the concepts of process synchronization and mutual exclusion;
-
Master the use of semaphore and apply it to solve the problem of producer consumer;
-
Master the implementation principle of semaphore.
2. Content:
The basic contents of this experiment are:
- Write programs under Ubuntu to solve the producer consumer problem with semaphores;
- Semaphores are implemented in linux-0.11 and verified by producer consumer programs.
(1) Solving producer consumer problems with semaphores
Write the application pc.c on Ubuntu to solve the classic producer consumer problem and complete the following functions:
- Establish a producer process and N consumer processes (n > 1);
- Create a shared buffer with files;
- The producer process writes integers 0,1,2,..., m, M > = 500 to the buffer in turn;
- The consumer process reads from the buffer, one at a time, deletes the read number from the buffer, and then outputs the process ID and number to the standard output;
- A maximum of 10 buffers can be saved at the same time.
One possible output effect is:
10: 0 10: 1 10: 2 10: 3 10: 4 11: 5 11: 6 12: 7 10: 8 12: 9 12: 10 12: 11 12: 12 ...... 11: 498 11: 499
The order of ID S will change greatly, but the number after the colon must be increased by one from 0.
(2) Implementation semaphore
Linux hasn't implemented semaphores in version 0.11. Linus leaves this challenging task to you. If we can realize a set of Shanzhai semaphores that fully comply with POSIX specification, it will undoubtedly be a great sense of achievement. However, time does not allow us to do so for the time being, so we first get a reduced version of POSIX semaphore. Its function prototype and standard are not exactly the same, and only includes the following system calls:
sem_t *sem_open(const char *name, unsigned int value); int sem_wait(sem_t *sem); int sem_post(sem_t *sem); int sem_unlink(const char *name);
Create a new sem.c file in the kernel directory to achieve the above functions. Then transplant pc.c from Ubuntu to 0.11 to test the semaphore.
3, Experimental principle
1. Process synchronization and mutual exclusion
(1) What is process synchronization
In a multi-channel batch processing system, multiple processes can be executed concurrently. However, due to the limited resources of the system, the execution of processes is not always in the end, but go and stop and move forward at an unpredictable speed. This is the * * asynchrony * * of processes.
So, "what's the problem with the asynchrony of processes"? For example, if A and B processes are responsible for reading and writing data respectively, the two threads cooperate and depend on each other. Then writing data should occur before reading data. In fact, due to the existence of asynchrony, the situation of reading before writing may occur. At this time, because the buffer has not been written to the data, the reading process A has no data to read, so the reading process A is blocked.
Process synchronization is used to solve this problem. From the above example, we can see that the execution of one process may affect the execution of another process. "The so-called process synchronization refers to coordinating these concurrent threads completing a common task, specifying the execution order of threads, transmitting signals or messages at some locations.".
For example, the process of life is synchronized. You want to drink hot water, so you make a pot of water and start to boil. Before this pot of water boils, you can only wait. After the water boils, the kettle will make a sound to remind you to drink water, so you can drink water. That is to say * * "the boiling of water must happen before you drink water" * *.
Be careful not to confuse process synchronization with process scheduling:
- Process scheduling is to maximize the use of CPU resources and select an appropriate algorithm to schedule the processes in the ready queue.
- Process synchronization is to coordinate some processes to complete a task, such as reading and writing. You must write before reading. You can't read before writing. That's what process synchronization does. Specify the execution order of these processes so that a task can be completed smoothly.
(2) What is process mutex
Similarly, because of the concurrency of processes, threads executing concurrently inevitably need to share some system resources, such as memory, printer, camera, etc. For example: we went to the school print shop to print papers. You pressed the "print" option of WPS, and the printer began to work. When your paper was half printed, another student pressed the "print" button of Word and began to print his own paper. Imagine what would happen if two processes could freely and concurrently share printer resources?
Obviously, the two processes run concurrently, resulting in the printer equipment alternately receiving print requests from WPS and Word processes. As a result, the contents of the two papers are mixed together.
Process mutual exclusion is used to solve this problem. When A process A is accessing the printer, if another process B also wants to access the printer, it must wait until process A finishes accessing and releases printer resources.
In fact, a printer like the above * * "a resource that is only allowed to be used by one process in a time period" (which means mutual exclusion), we call it a "critical resource", and the code that accesses the critical resource is called a "critical area" * *.
Let's make a popular comparison between process mutual exclusion and process synchronization:
- Process synchronization: process A should execute before process B
- Process mutual exclusion: process A and process B cannot be executed at the same time
From the above, it is not difficult to see that "process mutual exclusion is a special process synchronization", that is, the successive use of critical resources is also a coordination of the sequence of execution of the resources used by the process.
2. Semaphore
Semaphore is semaphore in English. It was first designed by E. W. Dijkstra, a Dutch scientist and Turing prize winner. It will be described in detail in the "process synchronization" part of any operating system textbook.
Linux semaphores adhere to POSIX specification and use man sem_overview can view relevant information. The semaphore system calls involved in this experiment include: sem_open(),sem_wait(),sem_post() and sem_unlink().
sem_t *sem_open(const char *name, unsigned int value);//Create or turn on semaphores int sem_wait(sem_t *sem);//P operation int sem_post(sem_t *sem);//V operation int sem_unlink(const char *name);//Delete semaphore
-
sem_ The function of open() is to create a semaphore or open an existing semaphore.
-
- sem_t is the semaphore type, which can be customized according to the needs of implementation.
- Name is the name of the semaphore. Different processes can share the same semaphore by providing the same name. If the semaphore does not exist, create a new semaphore named name; If it exists, open the existing semaphore named name.
- Value is the initial value of the semaphore. This parameter is valid only when a semaphore is newly created. In other cases, it is ignored. When successful, the return value is the unique identification of the semaphore (for example, the address and ID in the kernel), which is used by the other two system calls. If failed, the return value is NULL.
-
sem_wait() is the P atomic operation of semaphores. If the conditions to continue running are not met, make the calling process wait on the semaphore SEM. Returning 0 means success, and returning - 1 means failure.
-
sem_post() is the V atom operation of semaphores. If there are processes waiting for SEM, it will wake up one of them. Returning 0 means success, and returning - 1 means failure.
-
sem_ The function of unlink() is to delete the semaphore named name. Returning 0 means success, and returning - 1 means failure.
3. Producer consumer problem
Producer consumer problem, also known as bounded buffer problem, is a classic case of multi process synchronization. This problem describes the problems that occur when two processes sharing a fixed size buffer -- the so-called "producer" and "consumer" -- actually run. The main role of the producer is to generate a certain amount of data into the buffer, and then repeat the process. At the same time, consumers consume this data in the buffer. The key to this problem is to ensure that producers will not add data when the buffer is full, and consumers will not consume data when the buffer is empty.
The solution to the producer consumer problem is found in almost all operating system textbooks. Its basic structure is:
Producer() { // Produce a product item; // Free cache resources P(Empty); // Mutually exclusive semaphore P(Mutex); // Put the item into the free cache; V(Mutex); // Product resources V(Full); } Consumer() { P(Full); P(Mutex); //Fetch an assignment from the cache to item; V(Mutex); // Consumer product item; V(Empty); }
Obviously, when demonstrating this process, you need to create two types of processes, one executing the function Producer() and the other executing the function Consumer().
4. Summary
The general idea of this experiment is to realize SEM respectively_ open(),sem_wait(),sem_post() and sem_unlink() system calls, in the pc.c program calls 4 functions to complete the PV operation, solve the producer consumer problem.
4, Experimental steps
1. Implementation of semaphore
Because there is no semaphore definition in Linux 0.11, we should write a semaphore header file (i.e. Library) sem.h. using the knowledge of data structure, we can write the following code:
#ifndef _SEM_H #define _SEM_H #include <linux/sched.h> #define SEMTABLE_LEN 20 #define SEM_NAME_LEN 20 typedef struct semaphore{ char name[SEM_NAME_LEN]; int value; struct task_struct *queue; } sem_t; extern sem_t semtable[SEMTABLE_LEN]; #endif
With the semaphore header file, you can implement the semaphore code. sem.c
#include <linux/sem.h> #include <linux/sched.h> #include <unistd.h> #include <asm/segment.h> #include <linux/tty.h> #include <linux/kernel.h> #include <linux/fdreg.h> #include <asm/system.h> #include <asm/io.h> //#include <string.h> sem_t semtable[SEMTABLE_LEN]; //Number of semaphores int cnt = 0; //Create a semaphore, or open an existing semaphore sem_t *sys_sem_open(const char *name,unsigned int value) { char kernelname[100]; int isExist = 0; int i=0; //Semaphore name characters int name_cnt=0; //Gets the number of characters in the semaphore name while( get_fs_byte(name+name_cnt) != '\0') name_cnt++; //If the number of characters is greater than the set number of characters, null is returned if(name_cnt>SEM_NAME_LEN) return NULL; //Gets the name of the incoming semaphore for(i=0;i<name_cnt;i++) kernelname[i]=get_fs_byte(name+i); int name_len = strlen(kernelname); int sem_name_len =0; sem_t *p=NULL; //If the semaphore already exists, find it and turn it on for(i=0;i<cnt;i++) { sem_name_len = strlen(semtable[i].name); if(sem_name_len == name_len) { if( !strcmp(kernelname,semtable[i].name) ) { isExist = 1; break; } } } if(isExist == 1) { p=(sem_t*)(&semtable[i]); //printk("find previous name!\n"); } //If the semaphore does not exist, create the number of semaphores + 1 else { i=0; for(i=0;i<name_len;i++) { semtable[cnt].name[i]=kernelname[i]; } semtable[cnt].value = value; p=(sem_t*)(&semtable[cnt]); //printk("creat name!\n"); cnt++; } return p; } /* cli()Function and sti() function are off interrupt and on interrupt operations respectively to ensure the atomicity of P operation */ //P operation int sys_sem_wait(sem_t *sem) { cli(); //If the value of the semaphore is less than 0, sleep waits while( sem->value <= 0 ) sleep_on(&(sem->queue)); sem->value--; sti(); return 0; } //V operation int sys_sem_post(sem_t *sem) { cli(); sem->value++; if( (sem->value) <= 1) wake_up(&(sem->queue)); sti(); return 0; } //Delete semaphore //sys_ sem_ The same is true for open code int sys_sem_unlink(const char *name) { char kernelname[100]; int isExist = 0; int i=0; int name_cnt=0; while( get_fs_byte(name+name_cnt) != '\0') name_cnt++; if(name_cnt>SEM_NAME_LEN) return NULL; for(i=0;i<name_cnt;i++) kernelname[i]=get_fs_byte(name+i); int name_len = strlen(name); int sem_name_len =0; for(i=0;i<cnt;i++) { sem_name_len = strlen(semtable[i].name); if(sem_name_len == name_len) { if( !strcmp(kernelname,semtable[i].name)) { isExist = 1; break; } } } if(isExist == 1) { int tmp=0; for(tmp=i;tmp<=cnt;tmp++) { semtable[tmp]=semtable[tmp+1]; } cnt = cnt-1; return 0; } else return -1; }
Put the written sem.c in the linux-0.11/kernel Directory:
Put the written sem.h in the linux-0.11/include/linux Directory:
2. Realize the system call of semaphore
Combined with the operation of the previous experiment, it is easy to realize the system call of semaphore
(1) Add system call number
Modify the unistd.h file in the linux-0.11/include directory and add the corresponding system call number.
Since it follows the previous experiment, the system call number starts from 74.
#define __NR_sem_open 74 #define __NR_sem_wait 75 #define __NR_sem_post 76 #define __NR_sem_unlink 77
(2) Total number of modified system calls
Modify the system in the linux-0.11/kernel directory_ Call. S file
Change the code on line 61 to 78. (system call number starts from 0)
(3) Add a new system call function to the system call function table
Modify the sys.h file in the linux-0.11/include/linux directory
Add system call functions in the same format at the end of the code, and add system call functions in the same order in the system call function table.
extern int sys_sem_open(); extern int sys_sem_wait(); extern int sys_sem_post(); extern int sys_sem_unlink();
sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink
(4) Modify Makefile
Modify the Makefile file in the linux-0.11/kernel directory
2 modifications
Place 1: add sem.o at the end of OBJS
OBJS = sched.o system_call.o traps.o asm.o fork.o \ panic.o printk.o vsprintf.o sys.o exit.o \ signal.o mktime.o who.o sem.o
Place 2: add related Dependencies in Dependencies
sem.s sem.o: sem.c ../include/linux/sem.h ../include/unistd.h
(5) Compile kernel
Enter the linux-0.11 directory and enter the make command to compile.
sync appears on the last line, indicating that the compilation is successful.
(6) Mount file
Use. / mount in the oslab directory_ HDC command, run mount_hdc file.
Put the unistd.h file into the hdc/usr/include directory
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
Put the sys.h file and sem.h file into the hdc/usr/include/kernel directory
cp ./linux-0.11/include/linux/sys.h ./hdc/usr/include/linux/
cp ./linux-0.11/include/linux/sem.h ./hdc/usr/include/linux/
3. Solving the producer consumer problem
(1) Write pc.c
Create a new pc.c file in the hdc/uer/root directory. The code is as follows:
#define __LIBRARY__ #include <unistd.h> #include <linux/sem.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <linux/sched.h> _syscall2(sem_t *,sem_open,const char *,name,unsigned int,value) _syscall1(int,sem_wait,sem_t *,sem) _syscall1(int,sem_post,sem_t *,sem) _syscall1(int,sem_unlink,const char *,name) const char *FILENAME = "/usr/root/buffer_file"; /* The path of the buffer file stored in the products produced by consumption */ const int NR_CONSUMERS = 5; /* Number of consumers */ const int NR_ITEMS = 50; /* Maximum quantity of product */ const int BUFFER_SIZE = 10; /* Buffer size, indicating the number of products that can exist at the same time */ sem_t *metux, *full, *empty; /* 3 Semaphores */ unsigned int item_pro, item_used; /* Product number just produced; Product number just consumed */ int fi, fo; /* Handle to the buffer file for the producer to write to or the consumer to read from */ int main(int argc, char *argv[]) { char *filename; int pid; int i; filename = argc > 1 ? argv[1] : FILENAME; /* O_TRUNC Indicates that when the file is opened as read-only or write only, if the file exists, its length will be truncated to 0 (i.e. empty the file) * 0222 And 0444 indicate that the file is write only and read-only respectively (the preceding 0 is octal identification) */ fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222); /* Open the file in write only mode and write the product number to the producer */ fo = open(filename, O_TRUNC| O_RDONLY, 0444); /* Open the file in read-only mode and read out the product number to the consumer */ metux = sem_open("METUX", 1); /* Mutually exclusive semaphores to prevent simultaneous production and consumption */ full = sem_open("FULL", 0); /* The remaining semaphore of the product can be consumed if it is greater than 0 */ empty = sem_open("EMPTY", BUFFER_SIZE); /* Empty semaphore, which changes with the remaining semaphore of the product. When it is greater than 0, the producer can continue to produce */ item_pro = 0; if ((pid = fork())) /* The parent process is used to perform consumer actions */ { printf("pid %d:\tproducer created....\n", pid); /* printf()The output information is saved to the output buffer first, and is not immediately output to the standard output (usually the terminal console). * To avoid the influence of accidental factors, we call fflush(stdout) in stdio.h every time printf() * To ensure that the output is immediately output to standard output. */ fflush(stdout); while (item_pro <= NR_ITEMS) /* Production of required products */ { sem_wait(empty); sem_wait(metux); /* After a round of products are produced (the file buffer can only hold BUFFER_SIZE product numbers) * Reposition the position pointer of the buffered file to the file header. */ if(!(item_pro % BUFFER_SIZE)) lseek(fi, 0, 0); write(fi, (char *) &item_pro, sizeof(item_pro)); /* Write product number */ printf("pid %d:\tproduces item %d\n", pid, item_pro); fflush(stdout); item_pro++; sem_post(full); /* Wake up consumer process */ sem_post(metux); } } else /* Child processes to create consumers */ { i = NR_CONSUMERS; while(i--) { if(!(pid=fork())) /* Create i consumer processes */ { pid = getpid(); printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i); fflush(stdout); while(1) { sem_wait(full); sem_wait(metux); /* read()Return 0 when reading the end of the file, and reposition the position pointer of the file to the head of the file */ if(!read(fo, (char *)&item_used, sizeof(item_used))) { lseek(fo, 0, 0); read(fo, (char *)&item_used, sizeof(item_used)); } printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used); fflush(stdout); sem_post(empty); /* Wake up producer process */ sem_post(metux); if(item_used == NR_ITEMS) /* End if the last item has been consumed */ goto OK; } } } } OK: close(fi); close(fo); return 0; }
(2) Compiling pc.c
Use the. / run command to enter the pc.c virtual machine
Compile pc.c using the GCC - O pc.c command
After compilation, two warnings will appear, which will not affect the compilation.
(3) View results
I don't know whether it's Linux 0.11 or bochs. If more information is output to the terminal, the virtual screen of bochs will be confused. At this time, press ctrl+L to reinitialize the screen, but if more information is output, it will still be confused. It is recommended to redirect the output information to a file, and then use vi, more and other tools to view the file on the screen, which can basically solve this problem.
Use the. / PC > out command to enter the results into the out file and view them with the vi command.
result
It can be seen that it is normal production and consumption from 0 in sequence.
The experiment is over.