Article Directory
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_t
represented 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 nameoflag
, Logo, create or open a created semaphore based on the incoming logomode
,access permissionvalue
, 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 scopePTHREAD_PROCESS_PRIVATE
and 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]
value
Maximum 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_open
function to create it, without additional initialization. sem_open
Refer 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 NULLsvval
, Save the return signal value address, cannot be NULL- Return 0 return success, else return -1, the error code stored in
error
the
sem_getvalue
Get 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
error
the
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 NULLabstime
, Timeout time, unit is clock beat- Return 0 return success, else return -1, the error code stored in
error
the
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
error
the
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
error
the
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
error
the
sem_destroy
Used 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_init
re-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
error
the
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
error
the
sem_unlink
The 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
open
the semaphore, it will increase the reference count of the semaphore file. When thesem_unlink
function 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_PRIVATE
and 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_init
specifies 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_unlink
function 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.