[Linux application programming] POSIX thread mutual exclusion and synchronization mechanism-semaphore



1 Introduction

  The mutexes, read-write locks, and spin locks described in the previous article are used for mutual exclusion to protect shared resources, barriers are used to synchronize multiple threads, and condition variables are used for thread synchronization. The barrier cannot pass information between threads, and the condition variable has the risk of signal loss (of course, measures can be added to avoid it). This article describes another thread synchronization mechanism-semaphore. Compared with the previous synchronization (mutual exclusion) mechanism, the semaphore is more flexible and reliable. There is no possibility of signal loss. It is often used in the "producer-consumer" application model.


2 Semaphore

  POSIX semaphore (Seamphore) is a mechanism for synchronization between processes and threads. The semaphore is essentially a non-negative integer variable used to record the available number of resources. The operation of the semaphore is an atomic operation on the integer variable to achieve mutual exclusion or synchronization of threads. Adding an available resource to perform plus one is also called a V operation; after acquiring a resource, performing a minus one is also called a P operation. When the resource count is 0, it means that the resource is not available, and the thread acquiring the resource is blocked and suspended until the semaphore is available and then awakened for execution. Semaphore uses a variable to calculate the signal value, so there is no possibility of signal loss like condition variables, even if the waiting semaphore thread is not ready.


  Semaphores can be divided into binary semaphores and counting semaphores according to the signal value (the number of available resources).

  • Binary semaphore, the semaphore value is only 0 and 1, the initial value is 1, 1 means the resource is available, 0 means the resource is not available; the binary semaphore is similar to a mutex
  • Count the semaphore, the value of the semaphore is between 0 and a limit value greater than 1, and the signal value indicates the number of available resources

  Semaphores can be divided into named semaphores and unnamed semaphores according to different objects.

  • Well-known semaphore, the signal value is saved in a file, used for inter-process synchronization
  • Unnamed semaphore, also known as memory-based semaphore, the signal value is stored in memory for synchronization between threads

2.1 Semaphore characteristics

  • Used for thread synchronization
  • Can cause the thread to sleep, there is no available semaphore, the thread goes to sleep
  • Persistent
  • No signal loss

2.2 Semaphore applicable scenarios

   The scenario used by the semaphore is typically the "producer-consumer" model. The producer thread and the consumer thread access the same block of memory, the producer thread writes data to the shared memory, the consumer thread reads data from the shared memory, and the two threads are synchronized through a semaphore. Typical scenarios are:

  • Serial (USB, UART) data receiving and processing process

  • Data asynchronous output (terminal, file)

  • File read and write


2.3 Principles of using semaphores

  • None

3 Semaphore use

  The basic steps for semaphore use are:

[1] Create a semaphore instance

[2] Initialize the semaphore

[3] Semaphore P operation

[4] Semaphore V operation

[5] Destroy the semaphore


3.1 Create semaphore

  The posix semaphore is sem_trepresented by a data structure. The creation of unnamed semaphores and named semaphores is slightly different.

3.1.1 Unnamed semaphore creation

  Unnamed semaphore instances can be created statically and dynamically.

sem_t sem;

3.1.2 Create a famous semaphore

  If it is a well-known semaphore, you only need to define a semaphore pointer. The semaphore instance is created by the named semaphore function sem_open.

sem_t *sem_open(const char *name,int oflag, mode_t mode, unsigned int value);
  • name, The semaphore name
  • oflag, Logo, create or open a created semaphore based on the incoming logo
  • mode,access permission
  • value, The initial value of the semaphore
Logo meaning
O_CREAT Create a semaphore
O_CREAT|O_EXCL Create if the semaphore does not exist, return NULL if it already exists
0 Return NULL if the semaphore does not exist

Create the pseudo code of the famous semaphore:

sem_t *psem;
psem = sem_open(SEM_NAME, O_CREAT, 0555, 0);

3.2 Initialize the semaphore

  Semaphore initialization is divided into unnamed semaphore and well-known semaphore, and their initialization functions are different.

3.2.1 Unnamed semaphore initialization

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem, Semaphore instance address, cannot be NULL

  • pshared, Semaphore scope, divided into in-process scope PTHREAD_PROCESS_PRIVATEand cross-process scopePTHREAD_PROCESS_SHARED

  • value, The initial value of the semaphore

  • Return, success returns 0, failure returns -1, the error code is stored in error, common errors are:

    [1] Invalid parameter (EINVAL)

    [2] valueMaximum exceededSEM_VALUE_MAX

    [3] Set cross-process scope, but the current system does not support it

    [4] ENOSYS


3.2.2 Famous semaphore initialization

  The initialization of a well-known semaphore is generally done by calling the sem_openfunction to create it, without additional initialization. sem_openRefer to section 3.1.2 for function use.


3.3 Get semaphore

int sem_getvalue(sem_t *sem, int *sval);
  • sem, Semaphore instance address, cannot be NULL
  • svval, Save the return signal value address, cannot be NULL
  • Return 0 return success, else return -1, the error code stored in errorthe

sem_getvalueGet the current semaphore value   through the function, which can be used to judge the available semaphore.


3.4 Waiting for the semaphore (P operation)

  The semaphore waiting mode is divided into blocking and non-blocking modes, and the blocking mode is divided into infinite blocking mode and designated timeout blocking mode. Waiting for a semaphore is a P operation. When a semaphore is acquired, the signal value is reduced by one. The P operation of the semaphore is atomic.

3.4.1 Blocking method

int sem_wait(sem_t *sem);
  • sem, Semaphore instance address, cannot be NULL
  • Return 0 return success, else return -1, the error code stored in errorthe

  This function waits for the semaphore in blocking mode. If the signal value is greater than 0, the signal value is reduced by one, and the function returns; if the signal value is equal to 0, the resource is not available, and the calling thread is blocked and suspended until the semaphore is available and then awakened for execution. The threads waiting for the semaphore follow the first-in first-out principle to ensure fairness.


3.4.2 Specify the timeout time blocking mode

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
  • sem, Semaphore instance address, cannot be NULL
  • abstime, Timeout time, unit is clock beat
  • Return 0 return success, else return -1, the error code stored in errorthe

  This function will not wait indefinitely. After the specified clock beat, the function will not wait until the available semaphore function returns ETIMEDOUT, and the thread will be awakened for execution.


3.4.3 Non-blocking mode

int sem_trywait(sem_t *sem);
  • sem, Semaphore instance address, cannot be NULL
  • Return 0 return success, else return -1, the error code stored in errorthe

  This function will return immediately even if the semaphore is not available, without blocking.


3.5 Semaphore generation (V operation)

int sem_post(sem_t *sem);
  • sem, Semaphore instance address, cannot be NULL
  • Return 0 return success, else return -1, the error code stored in errorthe

  If there is a thread waiting for the semaphore, the waiting thread will be awakened for execution; if no thread is waiting for the semaphore, the signal value is executed plus one, which is the V operation. Both the P operation and V operation of the semaphore are atomic and will not cause race conditions.


3.6 Semaphore destruction

  Semaphore destruction is divided into unnamed semaphore and well-known semaphore, and their destruction functions are different.


3.6.1 Unnamed semaphore destruction

int sem_destroy(sem_t *sem);
  • sem, Semaphore instance address, cannot be NULL
  • Return 0 return success, else return -1, the error code stored in errorthe

  sem_destroyUsed to destroy an unnamed semaphore that has been initialized. The destroyed semaphore is in an uninitialized state, and the properties of the semaphore and control block parameters are in an unusable state. There are several points to note when using the destroy function:

  • The unnamed semaphore that has been destroyed can be sem_initre-initialized
  • Unnamed semaphores that have been destroyed cannot be destroyed repeatedly
  • When no thread holds an unnamed semaphore, and the semaphore does not block any threads, it can be destroyed

3.6.2 Destruction of a famous semaphore

  The destruction of a well-known semaphore involves two steps, the first is to close the semaphore, and then to separate the semaphore (delete). Calling the separation function to destroy a semaphore only deletes the semaphore name. The kernel will only destroy the semaphore after all processes holding the semaphore close the semaphore.


【1】Close the famous semaphore

int sem_close(sem_t *sem);
  • name, The famous semaphore name
  • Return 0 return success, else return -1, the error code stored in errorthe

   When a process terminates, it will perform this shutdown operation on the semaphore it occupies, regardless of whether the process is actively terminated or inactively terminated, this function will be called to execute the shutdown semaphore operation.


[2] Separation of famous semaphores

int sem_unlink(const char *name);
  • name, The famous semaphore name
  • Return 0 return success, else return -1, the error code stored in errorthe

   sem_unlinkThe function will immediately delete the specified semaphore name. At this time, the semaphore itself is not deleted, and the process holding the semaphore can still use the semaphore normally until all processes holding the semaphore close the semaphore, the kernel Will actually delete the semaphore.

Note:

When a process creates a famous semaphore, it will generate a "sem.xxx" file in the "/dev/shm" directory. If the process does not have openthe semaphore, it will increase the reference count of the semaphore file. When the sem_unlinkfunction is called , the "sem.xxx" file in the "/dev/shm" directory will be deleted immediately, and then the reference count of the semaphore file will be checked. When the reference count is 0, the semaphore will be deleted, otherwise it will wait until the reference count is 0. Delete the semaphore.


3. 7 write an example

  Code realization function:

  • Create a "producer-consumer" model
  • Create two threads, one thread is responsible for generating data, the other reads the data and outputs it to the terminal
  • Two threads are synchronized by semaphore
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <semaphore.h>
#include "pthread.h" 

struct _buff_node
{
    
    
	uint8_t buf[64];
	uint32_t occupy_size;
};

/* 信号量 */
sem_t sem;

/* 共享缓存 */
static struct _buff_node s_buf;	

void *thread0_entry(void *data)  
{
    
    /* 消费者线程 */
	uint8_t  i =0;

	for (;;)
	{
    
    
		sem_wait(&sem);
		if (s_buf.occupy_size != 0)
		{
    
    
			printf("thread0 read data:");
			for (i=0; i<s_buf.occupy_size; i++)
			{
    
    
				printf("0x%02x ", s_buf.buf[i]);
			}
			printf("\r\n");
		}
	}
}

void *thread1_entry(void *data)  
{
    
    /* 生产者线程 */
	uint8_t  i =0;

	for (;;)
	{
    
    
		s_buf.occupy_size = 0;
		for (i = 0;i<8; i++)
		{
    
    
			s_buf.buf[i] = rand();
			s_buf.occupy_size++;
		}
		printf("thread1 write %dByte data\n", s_buf.occupy_size);
		sem_post(&sem);
		sleep(1);	/* 1秒产生数据 */	
	}
}

int main(int argc, char **argv)  
{
    
    
	pthread_t thread0,thread1,thread2;  
	void *retval;

	sem_init(&sem, PTHREAD_PROCESS_PRIVATE, 0);
    pthread_create(&thread0, NULL, thread0_entry, NULL);
	pthread_create(&thread1, NULL, thread1_entry, NULL);
    pthread_join(thread0, &retval);
    pthread_join(thread1, &retval);
	
	return 0;
 }

Output result

acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/sem$ gcc sem.c -o sem -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/sem$ ./sem
thread1 write 8Byte data
thread0 read data:0x67 0xc6 0x69 0x73 0x51 0xff 0x4a 0xec 
thread1 write 8Byte data
thread0 read data:0x29 0xcd 0xba 0xab 0xf2 0xfb 0xe3 0x46 

4 semaphore attributes

4.1 Semaphore types

  Semaphores can be divided into named semaphores and unnamed semaphores according to different objects.

  • Famous semaphore, used for inter-process synchronization
  • Unnamed semaphore, used for synchronization between threads

4.2 Unnamed semaphore scope

  The unnamed semaphore is used for synchronization between threads. Like other thread synchronization (mutual exclusion) mechanisms, it has a scope. The unnamed semaphore scope indicates the scope of the semaphore, which is divided into the in-process (creator) scope PTHREAD_PROCESS_PRIVATEand the cross-process scope PTHREAD_PROCESS_SHARED. In-process scope can only be used for intra-process thread synchronization, and cross-process can be used for synchronization between all threads of the system.

  The unnamed semaphore scope sem_initspecifies the parameters when calling the function initialization pshared.


4.3 Semaphore persistence

  Persistence refers to the life cycle range of a semaphore. The persistence of a named semaphore is different from that of an unnamed semaphore, and the persistence of an unnamed semaphore is related to the scope.

  • The famous semaphore continues with the kernel. When a named semaphore is created, even if no process currently opens a semaphore, its value remains until the kernel re-bootstraps or calls a sem_unlinkfunction to delete the semaphore.
  • The scope of the unnamed semaphore is within the process. At this time, the semaphore is continuous with the process. The semaphore is destroyed after the creation process terminates and exits.
  • The scope of the unnamed semaphore is cross-process (in the system). At this time, the semaphore is continuous with the kernel, because the semaphore always exists in the shared.

5 Summary

  Semaphore is a mechanism for synchronization between processes and threads. The well-known semaphore is used for process synchronization, and the unnamed semaphore is used for synchronization between threads. Semaphores are the same as mutexes, read-write locks, spin locks, condition variables, and barriers. They all have the function of protecting shared resources. The threads of shared resources have mutually exclusive access; at the same time, semaphores also have the function of "passing information". Makes synchronization between threads. Semaphore has the reliability that the signal is not lost, and is often used in the "producer-consumer" application model.

Guess you like

Origin blog.csdn.net/qq_20553613/article/details/106386140