Mutual exclusion of critical sections

Problem Description

1. What is a critical section?
The critical section is a piece of program code that is accessed by shared resources (such as shared files, shared variables, global variables, etc.). Access to shared memory is the difference between the critical section and other codes. When multiple processes running in the computer all have code that executes the critical section, there will be competition for shared memory at this time. If multiple critical sections are reading shared memory, the problem is not big and there will be no competition; when a critical section wants to modify the shared memory, the problem will be big at this time! If process A wants to modify a shared variable, after reading the variable for modification, it is about to write back but it is suspended, and another process B reads the shared variable while it is executing. At this time, a race condition occurred . The value read by process B is the old value, not the updated value. There are many similar situations, which will lead to race conditions. When two or more processes read and write some shared data, the final result depends on the precise order in which the processes run.

So the question is, how to prevent race conditions when each critical section reads and writes shared data? That is, how to achieve mutual exclusion of critical sections?
2. What are the solutions?
Commonly used methods can be divided into busy waiting methods and blocking other critical areas.
There are several ways to busy wait:

  • Shield interrupt
  • Lock variable
  • Strict rotation
  • Peterson solution
  • TSL instructions

There are several ways to block:

  • Use sleep and wakeup primitives
  • signal
  • Guancheng

Next, we will introduce them one by one.

Solution

Busy waiting method

1. Shielded interrupts
In a single-processor system, a large part of the reason for competition is that when accessing shared memory, the process is switched , that is, the process executing the critical section is suspended, and then the other critical sections go again. Shared memory was accessed. A simple and rude method is to directly shield the interrupt when the critical section is executed. After shielding the interrupt, the clock interrupt is also shielded, and the CPU can only switch processes when a clock interrupt occurs. The inability to switch processes when accessing the shared memory also maintains the continuity of access to the shared memory, and cannot be interrupted by other processes, and the access to the shared memory remains consistent. If other processes want to access, they can only wait (busy waiting) .
But this method is not good. It is very unsafe to hand over the right to shield the interrupt to the user process. If the user process enters the critical area and does not turn on the interrupt, the entire system may crash due to this. And for multi-core processors, it cannot prevent processes running on other CPUs from accessing shared variables.

2. Lock variables. To put
it vividly, it is to add a lock to the shared memory. When a critical section of a process wants to access the shared memory, you must first try to open it. If you can open it, you can access it, if you can’t open it, then Wait until it can be opened (that is, busy waiting) . The specific implementation method: first create a shared variable (that is, a lock, this lock also needs to be shared so that other processes can also access it. However, because the lock is also shared, the code that accesses the lock variable has become a critical section , This introduces a new problem ), and initialize the lock to 0, which means that shared memory can be accessed. If it is 1, it means that a process is accessing shared memory, and it can only be accessed when the lock is 0 (busy, etc.) .
The problem with lock variables is obvious. When different processes test locks, they are also executing critical sections. So how can we guarantee mutual exclusion when accessing lock variables?

3. Strict rotation method
Strict rotation method is actually a kind of lock variable. In the strict rotation method, there is an integer variable turn to record which process is the turn to enter the critical section, and the initial value is 0. Pseudo code to implement strict rotation method:

进程 0 的代码:
while(True){
	while( turn != 0) ; // 循环,忙等
	临界区代码,进行共享内存的访问;
	turn = 1;
	非临界区;
}

进程 1 的代码:
while(True){
	while( turn != 1) ; // 循环,忙等
	临界区代码,进行共享内存的访问;
	turn = 0;
	非临界区;
}

The strict rotation method also has the problem of locking variables. Who will guarantee the mutual exclusion of turn access ? So it is still possible that both processes are accessing the same shared memory. In addition, the difference from the lock variable is the rotation feature of the strict rotation method. It can also be seen from the above code that in order to access the shared memory, process 0 and process 1 need to be accessed alternately , but if this One of the two processes executes fast, while the other is slow? At this time, the fast process can only keep looping in while, busy waiting, ( the lock used for busy waiting is also called spin lock ). For processes with large differences in execution speed, the round robin method is not suitable.

4. Peterson's solution
Peterson's method also uses the turn variable in the rotation method, and also uses an array to record who wants to execute the critical section. The code for Peterson's solution is as follows:

#define FALSE 0
#define TRUE 1
#define N 2									//进程数量

int turn;									//轮到谁进入临界区
int interested[N];							//所有值初始化为 FALSE

void enter_region(int process)				//进程号是0或1
{
	int other;								//另一进程号
	 other = 1 - process;
	 interested[process] = TRUE;			//表示 process 进程想要访问临界区
	 turn = process;
	 while(turn == process && interested[other] == TRUE) ;		//忙等,关键语句,这是这个语句解决了turn的同时访问问题
}

void leave_region(int process)				//process 进程离开临界区
{
	interested[process] = FALSE;
}

The interested array here solves the problem mentioned above when the turn is accessed by multiple processes . When multiple processes write turn at the same time, set it to their own process number (0 or 1), but only the process number written later is valid, and the turn written first is overwritten by the turn written later. Assuming that process 1 is written later, turn is 1. When both processes run to the while statement, process 01 executes 0 cycles and enters the critical section, while process 1 loops until process 0 exits the critical section.

5. TSL instruction
This method only needs to be implemented by a TSL instruction,

TSL RX, LOCK

The TSL instruction becomes Test and Lock. The above code reads the memory word LOCK into the register RX, and then stores a non-zero value on the memory address. Note that the read and write operations are atomic (inseparable). The CPU executing the TSL instruction will lock the memory bus to prevent other CPUs from accessing the memory before the end of this instruction. Locking the storage bus is not the same as shielding interrupts. The shielding processor is not feasible in a multi-core processor, but other processors cannot access the memory after the bus is locked . Use TSL instructions to achieve mutual exclusion as follows:

enter_region:
	TSL REGISTER, LOCK					;复制锁到寄存器并设锁为1
	CMP REGISTER, #0 					;测试锁是否为0
	JNE enter_region					;若不是0,说明已被设置,则循环,忙等
	RET									;返回调用者,调用者进入临界区

leave_region:
	MOVE LOCK, #0						;在锁中存0
	RET

The TSL instruction copies the original value of LOCK into the register and sets it to 1, and then compares this original value with 0. If it is non-zero, it has been locked and retested; if it is 0, RET and the caller executes the critical section.

Blocking method

1. Use sleep and wakeup primitives
Primitives are uninterruptible processes. The sleep primitive will cause the caller to enter the sleep state and be blocked; the wakeup primitive will wake up a process . slepp and wakeup are generally used as system calls. When using slepp and wakeup, slepp and wakeup are usually called to achieve mutual exclusion based on the value of a certain variable. Such as producer-consumer issues:

#define N 100					//缓冲区的槽数目
int count = 0;					//缓冲区中的数据项数目

void  producer()
{
	int item;
	while(TRUE){
		item = produce_item();			//生产数据
		if (count == N) sleep();			//如果缓冲区满则调用sleep进入睡眠
		insert_item(item);
		count++;
		if(count == 1) wakeup(consumer);		//如果刚才缓冲区是空的则唤醒睡眠的消费者
	}	
}


void consumer()
{
	int item;
	while(TRUE){
		if(count == 0) sleep();			//如果缓冲区为空则睡眠
		item = remove_item();
		count--;
		if(count == N-1) 	wakeup(producer);			//如果刚才缓冲区是满的,则唤醒生产者
		consume_item(item);
			
	}

}

The problem with this method is that the access to count competes, which easily causes the wake-up signal to be lost. For example, when the consumer finds that the buffer is empty and reads that the count is 0, it is suspended before sleep . Then the producer runs. After producing an item, it finds that the count is 1, thinking that the consumer is sleeping, and then wankeup(consumer ), wake-up signal is sent to the consumer, but this time consumers did not sleep, so wake up signal loss . When the consumer is running, because the previously read count is 0, it sleeps. Then the producers will sooner or later fills the buffer, then SLEEP . So both consumers and producers sleep, and no one wakes up any of them.

2. Semaphore The
semaphore uses an integer variable to accumulate the number of wake-ups . This variable is called the semaphore. The value of the semaphore can be 0 and positive. Two operations are also used in the semaphore, down and up (also called p and v). Performing a down operation on a semaphore is to check whether its value is greater than 0. If the value is greater than 0, the value is subtracted by 1 (that is, a saved wake-up signal is used) and execution continues; if it is 0, the process will sleep, and the down is not over at this time. Performing an up operation on a semaphore will increase the semaphore by 1. If one or more processes sleep on this semaphore, that is, the down operation of a process is not completed, wake up one of the sleeping processes and continue to complete its down operating. **Note that the check values, modification of variables, and possible sleep operations for down and up operations are inseparable. **Use semaphores to solve the producer-consumer problem:

#define N 100					//缓冲区的槽数目
typedef int semaphore 			//信号量是一种特殊的整形数据
semaphore mutex = 1;			//控制对临界区的访问
semaphore empty = N;			//计数缓冲区的空槽数
semaphore full = 0;				//技术缓冲区的满槽数
int count = 0;					//缓冲区中的数据项数目

void  producer()
{
	int item;
	while(TRUE){
		item = produce_item();			//生产数据
		down(&empty);			//将空槽数减一,若已满则会阻塞
		down(&mutex);			//进入临界区
		insert_item(item);
	    up(&mutex);				//离开临界区
		up(&full);	 			//将满槽数加1
	}	
}


void consumer()
{
	int item;
	while(TRUE){
		down(&full);			//将满槽数减一,若为空则会阻塞
		down(&mutex);			//进入临界区
		insert_item(item);
	    up(&mutex);				//离开临界区
		up(&empty);	 			//将空槽数加1
	}

}

It seems that the semaphore solves the mutual exclusion problem very well, but consider a problem, if the code exchange sequence is as follows:

down(&empty);			//将空槽数减一,若已满则会阻塞
down(&mutex);			//进入临界区

It is possible that the producer will be blocked and the consumer will be blocked at the same time. So both processes will be blocked. Note that you need to be very careful when using semaphores and pay attention to the order of operations!

3. Monitor
A monitor is a collection composed of processes, condition variables, and data structures. They form a special module or software package. Processes can call procedures in the monitor at any time they need it, but they cannot access the data structures in the monitor in a declared process outside of the monitor. An important characteristic of the monitoring process is that there can only be one active process in the monitoring process at any time . It should be noted that monitors are an integral part of the programming language, and the compiler knows their particularities, so it can use different methods from other procedure calls to handle the calls to monitors. Condition variables are usually used in the monitor to block the inoperable process, and two operations: wait and signal.

Guess you like

Origin blog.csdn.net/Miha_Singh/article/details/90383838