IPC Semaphore

Semaphores can be used for communication between processes or between threads within a process. Each semaphore has a semaphore value that will not be less than 0, corresponding to the number of available semaphores. Call sem_init() or sem_open() to assign an initial value to the semaphore value, call sem_post() to increase the semaphore value by 1, and call sem_wait() to decrease the semaphore value by 1. If the current semaphore is 0, the thread that calls sem_wait() is suspended in the waiting queue of the semaphore until the semaphore value is greater than 0 and is available.

Depending on the value of the semaphore (representing the number of available resources), POSIX semaphores can be divided into:

  • Binary semaphore: The semaphore has only two values: 0 and 1, and the initial value is 1. This is the same as a mutex lock. If the resource is locked, the semaphore value is 0, and if the resource is available, the semaphore value is 1. It is equivalent to having only one key. After the thread gets the key, it needs to unlock it after completing the access to the shared resource, and then put the key back for other threads that need the key. The usage method is the same as the mutex lock. The waiting semaphore function must be used in pairs with the sending semaphore function. It cannot be used alone. You must wait before sending.

  • Counting semaphore: The value of the semaphore is between 0 and a limit value greater than 1 (POSIX specifies that the maximum limit value of the system must be at least 32767). The count indicates the number of available semaphores. At this time, the send semaphore function can be called individually to send semaphores, which is equivalent to having multiple keys. When a thread gets a key, it consumes one key, and the used key does not need to be put back.

POSIX semaphores are divided into named semaphores and unnamed semaphores:

  • Named semaphore: Its value is saved in a file and is generally used for synchronization or mutual exclusion between processes.

  • Unnamed semaphore: Its value is stored in memory and is generally used for synchronization or mutual exclusion between threads.

The POSIX semaphore of the RT-Thread operating system is mainly a wrapper of the RT-Thread kernel semaphore, and is mainly used for communication between threads in the system. The usage is similar to the semaphore of the RT-Thread kernel.

Each semaphore corresponds to a semaphore control block. Before creating a semaphore, you need to define a sem_t semaphore control block. sem_t is a redefinition of the posix_sem structure type, defined in the semaphore.h header file.

struct posix_sem
{
    rt_uint16_t refcount;
    rt_uint8_t unlinked;
    rt_uint8_t unamed;
    rt_sem_t sem;    /* RT-Thread 信号量 */
    struct posix_sem* next;     /* 指向下一个信号量控制块 */
};
typedef struct posix_sem sem_t;

rt_sem_t 是 RT-Thread 信号量控制块,定义在 rtdef.h 头文件里。

struct rt_semaphore
{
   struct rt_ipc_object parent;/* 继承自 ipc_object 类 */
   rt_uint16_t value;   /* 信号量的值  */
};
/* rt_sem_t 是指向 semaphore 结构体的指针类型 */
typedef struct rt_semaphore* rt_sem_t;
copymistakeCopy Success

The value of an unnamed semaphore is stored in memory and is generally used for synchronization or mutual exclusion between threads. Before use, you must call sem_init() to initialize it.

Initialize an unnamed semaphore

int sem_init(sem_t *sem, int pshared, unsigned int value);copymistakeCopy Success

This function initializes an unnamed semaphore sem, initializes the semaphore-related data structure according to the given or default parameters, and puts the semaphore into the semaphore linked list. After initialization, the semaphore value is the given initial value value. This function is a wrapper for the rt_sem_create() function.

Destroy an unnamed semaphore

int sem_destroy(sem_t *sem);copymistakeCopy Success

This function destroys an unnamed semaphore sem and releases the resources occupied by the semaphore.

Named semaphores, whose values ​​are stored in files, are generally used for synchronization or mutual exclusion between processes. Two processes can operate a named semaphore with the same name. The implementation of named semaphores in the RT-Thread operating system is similar to that of unnamed semaphores. Both are designed for communication between threads and are used in a similar way.

Create or open a named semaphore

sem_t *sem_open(const char *name, int oflag, ...);copymistakeCopy Success

This function creates a new semaphore or opens an existing semaphore based on the semaphore name name. The optional values ​​of Oflag are 0, O_CREAT or O_CREAT|O_EXCL. If Oflag is set to O_CREAT, a new semaphore will be created. If Oflag is set to O_CREAT|O_EXCL, NULL will be returned if the semaphore already exists, and a new semaphore will be created if it does not exist. If Oflag is set to 0, NULL will be returned if the semaphore does not exist.

Detaching named semaphores

int sem_unlink(const char *name);copymistakeCopy Success

This function will find the semaphore according to the semaphore name name. If the semaphore exists, it will mark the semaphore as detached. Then check the reference count. If the value is 0, the semaphore will be deleted immediately. If the value is not 0, it will not be deleted until all threads holding the semaphore close the semaphore.

Close the named semaphore

int sem_close(sem_t *sem);copymistakeCopy Success

When a thread terminates, the semaphore it occupies will be closed. Whether the thread terminates voluntarily or involuntarily, this closing operation will be performed, which is equivalent to reducing the holding count of the semaphore by 1. If the holding count is 0 after decrementing by 1 and the semaphore is already in the detached state, the sem semaphore will be deleted and the resources it occupies will be released.

int sem_getvalue(sem_t *sem, int *sval);copymistakeCopy Success

This function can get the value of the sem semaphore and save it in the memory pointed to by sval, so as to know the number of semaphore resources.

int sem_wait(sem_t *sem);copymistakeCopy Success

The thread calls this function to obtain the semaphore, which is the encapsulation of the rt_sem_take(sem, RT_WAITING_FOREVER) function. If the semaphore value is greater than zero, it indicates that the semaphore is available, the thread obtains the semaphore, and the semaphore value decreases by 1. If the semaphore value is equal to 0, it indicates that the semaphore is not available, the thread is blocked and enters the suspended state, and queues in a first-in-first-out manner until the semaphore is available.

int sem_trywait(sem_t *sem);copymistakeCopy Success

This function is a non-blocking version of the sem_wait() function and is a wrapper of the rt_sem_take(sem,0) function. When the semaphore is not available, the thread will not block but return directly.

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);copymistakeCopy Success

The difference between this function and sem_wait() function is that if the semaphore is not available, the thread will be blocked for abs_timeout duration. After the timeout, the function returns - 1 and the thread will be awakened from the blocked state to the ready state.

int sem_post(sem_t *sem);copymistakeCopy Success

This function will release a sem semaphore and is a wrapper of the rt_sem_release() function. If the queue of threads waiting for the semaphore is not empty, it means that there are threads waiting for the semaphore. The first thread waiting for the semaphore will switch from the suspended state to the ready state and wait for system scheduling. If there are no threads waiting for the semaphore, the semaphore value will increase by 1.

The typical case of semaphore usage is the producer-consumer model. A producer thread and a consumer thread operate on the same block of memory, the producer fills data into the shared memory, and the consumer reads data from the shared memory.

This program will create 2 threads and 2 semaphores, one semaphore indicates that the shared data is empty, the other semaphore indicates that the shared data is not empty, and a mutex lock is used to protect shared resources. After the producer thread produces data, it will send a full_sem semaphore to the consumer to notify the consumer thread that data is available. After sleeping for 2 seconds, it will wait for the empty_sem semaphore sent by the consumer thread. After the full_sem sent by the producer, the consumer thread will process the shared data, and after processing, it will send the empty_sem semaphore to the producer thread. The program will continue to loop like this.

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

/* 静态方式初始化一个互斥锁用于保护共享资源 */
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/* 2 个信号量控制块,一个表示资源空信号,一个表示资源满信号 */
static sem_t empty_sem,full_sem;

/* 指向线程控制块的指针 */
static pthread_t tid1;
static pthread_t tid2;

/* 函数返回值检查 */
static void check_result(char* str,int result)
{
    if (0 == result)
    {
        printf("%s successfully!\n",str);
    }
    else
    {
        printf("%s failed! error code is %d\n",str,result);
    }
}

/* 生产者生产的结构体数据,存放在链表里 */
struct node
{
    int n_number;
    struct node* n_next;
};
struct node* head = NULL; /* 链表头, 是共享资源 */

/* 消费者线程入口函数 */
static void* consumer(void* parameter)
{
    struct node* p_node = NULL;

    while (1)
    {
        sem_wait(&full_sem);
        pthread_mutex_lock(&mutex);    /* 对互斥锁上锁, */

        while (head != NULL)    /* 判断链表里是否有元素 */
        {
            p_node = head;    /* 拿到资源 */
            head = head->n_next;    /* 头指针指向下一个资源 */
            /* 打印输出 */
            printf("consume %d\n",p_node->n_number);

            free(p_node);    /* 拿到资源后释放节点占用的内存 */
        }

        pthread_mutex_unlock(&mutex);    /* 临界区数据操作完毕,释放互斥锁 */

        sem_post(&empty_sem);  /* 发送一个空信号量给生产者 */
    }
}
/* 生产者线程入口函数 */
static void* product(void* patameter)
{
    int count = 0;
    struct node *p_node;

    while(1)
    {
        /* 动态分配一块结构体内存 */
        p_node = (struct node*)malloc(sizeof(struct node));
        if (p_node != NULL)
        {
            p_node->n_number = count++;
            pthread_mutex_lock(&mutex);    /* 需要操作 head 这个临界资源,先加锁 */

            p_node->n_next = head;
            head = p_node;    /* 往链表头插入数据 */

            pthread_mutex_unlock(&mutex);    /* 解锁 */
            printf("produce %d\n",p_node->n_number);

            sem_post(&full_sem);  /* 发送一个满信号量给消费者 */
        }
        else
        {
            printf("product malloc node failed!\n");
            break;
        }
        sleep(2);    /* 休眠 2 秒 */
        sem_wait(&empty_sem);  /* 等待消费者发送空信号量 */
    }
}

int rt_application_init()
{
    int result;

    sem_init(&empty_sem,NULL,0);
    sem_init(&full_sem,NULL,0);
    /* 创建生产者线程, 属性为默认值,入口函数是 product,入口函数参数为 NULL*/
    result = pthread_create(&tid1,NULL,product,NULL);
    check_result("product thread created",result);

    /* 创建消费者线程, 属性为默认值,入口函数是 consumer,入口函数参数是 NULL */
    result = pthread_create(&tid2,NULL,consumer,NULL);
    check_result("consumer thread created",result);

    return 0;
}copymistakeCopy Success

Last updated

Assoc. Prof. Wiroon Sriborrirux, Founder of Advance Innovation Center (AIC) and Bangsaen Design House (BDH), Electrical Engineering Department, Faculty of Engineering, Burapha University