19. 信号量
一、什么是信号量
信号量(Semaphore)本质上是一个非负整数变量,它可以用来控制对共享资源的访问。在 Linux 中,信号量是用来协调进程或线程的执行的,并不承担传输数据的职责。信号量主要用于两种目的:互斥 和 同步。
- 互斥(Mutex):确保多个进程或线程不会同时访问临界区(即访问共享资源的代码区域)。
 - 同步(Synchronization):协调多个进程或线程的执行顺序,确保它们按照一定的顺序执行。
 
基于不同的目的,信号量可以分为两类:用于实现互斥的 “二进制信号量” 和用于同步的 “计数信号量”。
- 二进制信号量(或称互斥锁):其值只能是 0 或 1,主要用于显示互斥,即一次只允许一个线程进入临界区。通常用于控制共享资源的访问,避免竞态条件的产生。
 - 计数信号量:其值可以是任意非负整数,表示可用资源的数量。。计数信号量允许多个线程根据可用资源的数量进入临界区。通常用于控制不同进程或线程的执行的顺序。
 
在 Linux 中,根据是由具有 唯一的名称,信号量有分为 有名信号量(named semaphore)和 无名信号量(unnamed semaphore)。
- 有名信号量:有名信号量在系统范围内是可见的,可以在任意进程之间进行通信。它们通过名字唯一标识(名字是以 
/开头,以\0结尾的字符串,斜杠后面可以有若干个字符,但不能在出现斜杠),这使得不同的进程可以通过这个名字访问同一个信号量对象。 - 无名信号量:无名信号量不是通过名称标识,而是直接通过 
sem_t结构的内存位置标识。无名信号量在使用前需要初始化,在不需要是应该销毁。它们不需要向有名信号量那样进程创建和链接,因此设置起来更快,运行效率也更高。 
  在 Linux 系统中,有名信号量在临时文件系统的对应文件位于 /dev/shm 目录下,创建它们是可以像普通文件一样设置权限模式,限制不同用户的访问权限。
信号量主要提供了两个操作:P 操作 和 V 操作。
- P 操作(Proberen,尝试):也称为 等待操作,用于减少信号量的值。如果信号量的值大于 0,它就减 1 并继续运行。如果信号量的值为 0,则进程或线程阻塞,知道信号量的值变为非零。
 - V 操作(Verhogen,增加):也成为 信号操作,用于增加信号量的值。如果有其它进程或线程因信号量的值为 0 而阻塞,这个操作可能会唤醒它们。
 
二、无名信号量的API
2.1、创建无名信号量
  我们可以使用 sem_init() 函数 初始化一个无名信号量。
/**
 * @brief 创建一个无名信号量
 * 
 * @param __sem 信号量地址
 * @param __pshared 指明信号量是线程间共享还是进程间共享
 * @param __value 信号量的初始值
 * @return int 0 成功 -1 失败
 */
int sem_init(sem_t *__sem, int __pshared, unsigned int __value);
  参数 __pshared 用来 指明信号量是线程间共享还是进程间共享。如果值为 0,则信号量是线程间共享的,应该被置于所有线程均可见的地址(如全局变量或在堆中动态分配的变量)。如果值非 0,则信号量是进程间共享的,应该被置于共享内存区域,任何进程只要能访问共享内存区域,即可以操作进程间的信号量。
2.2、无名信号量的PV操作
  我们可以使用 sem_post() 函数来 使信号量的值加 1。
/**
 * @brief 使信号量的值加1
 * 
 * @param __sem 信号量地址
 * @return int 成功返回0,失败则信号量的值未改变,返回-1
 */
int sem_post(sem_t *__sem);
如果信号量从 0 变为 1,且其它进程或线程因信号量而阻塞,则阻塞的进程或线程会被唤醒并获取信号量,然后继续执行。POSIX 标准并未明确定义唤醒策略,具体唤醒的是和哪个进程或线程取决于操作系统的调度策略。
  我们还可以使用 sem_wait() 函数来 使信号量的值减 1。
/**
 * @brief 使信号量的值减1
 * 
 * @param __sem 信号量地址
 * @return int 成功返回0,失败则信号量的值未改变,返回-1
 */
int sem_wait(sem_t *__sem);
如果信号量的值大于 0,函数可以执行减一操作,然后立即返回,调用进程或线程继续执行。如果当前信号量的值是 0,则调用阻塞直至信号量的值大于 0,或者信号处理函数打断当前调用。
2.3、销毁无名信号量
  我们可以使用 sem_destroy() 函数来 销毁信号量。
/**
 * @brief 销毁信号量
 * 
 * @param __sem 无名信号量
 */
int sem_destroy(sem_t *__sem);
三、无名信号量的使用
3.1、线程间使用无名信号量
新建一个 main.c 文件。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
int ticket = 100;
sem_t unnamed_sem;
void *sell(void *argv);
int main(void)
{
    pthread_t pid[3] = {0};
    char name[3][30] = {0};
    // 初始化信号量,第二个参数0线程间使用,大于0的整数,进程间使用,第三个参数是信号量的初始值
    if (sem_init(&unnamed_sem, 0, 1) == -1)
    {
        perror("semaphore init failed!");
        return -1;
    }
    // 创建线程
    for (int i = 0; i < sizeof(pid) / sizeof(pid[0]); i++)
    {
        sprintf(name[i], "window %d", i + 1);
        pthread_create(&pid[i], NULL, sell, name[i]);
    }
    // 主线程等待线程结束
    for (int i = 0; i < sizeof(pid) / sizeof(pid[0]); i++)
    {
        pthread_join(pid[i], NULL);
    }
    // 销毁信号量
    if (sem_destroy(&unnamed_sem) != 0)
    {
        perror("destory semaphore failed!");
        return -1;
    }
    return 0;
}
void *sell(void *argv)
{
    char *name = (char *)argv;
    while(ticket > 0)
    {
        // 信号量互斥
        sem_wait(&unnamed_sem);
        if (ticket > 0)
        {
            printf("%s sell the number is %d ticket!\n", name, ticket);
            ticket--;
        }
        // 信号量唤醒
        sem_post(&unnamed_sem);
    }
}
新建一个 Makefile 文件,它的内容如下:
CC := gcc
# $@ 表示目标文件名称 $^ 表示所有的依赖文件
main: main.c
	- $(CC) $^ -o $@
	- ./$@
  在终端中输入 make 运行。
make
如果信号量被用于线程间同步时,当我们使用
sem_init()函数初始化信号量时,该函数的第二个参数的值需要设置为 0。
3.2、进程间使用无名信号量
修改 main.c 文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
#include <semaphore.h>
int main(void)
{
    pid_t pid = 0;
    char shm_name[] = "shm_value";
    char sem_name[] = "shm_semaphore";
    // 1.创建共享内存
    int value_fd = 0;
    if ((value_fd = shm_open(shm_name, O_RDWR | O_CREAT, 0644)) == -1)
    {
        perror("create value shared memory failed.");
        exit(EXIT_FAILURE);
    }
    // 建无名信号量的共享内存
    int sem_fd = 0;
    if ((sem_fd = shm_open(sem_name, O_RDWR | O_CREAT, 0644)) == -1)
    {
        perror("create semaphore shared memory failed.");
        exit(EXIT_FAILURE);
    }
    // 2.设置共享内存对象大小
    if (ftruncate(value_fd, sizeof(int)) == -1)
    {
        perror("set value shared memory size failed.");
        exit(EXIT_FAILURE);
    }
    // 设置无名信号量的共享内存对象大小
    if (ftruncate(sem_fd, sizeof(sem_t)) == -1)
    {
        perror("set semaphore shared memory size failed.");
        exit(EXIT_FAILURE);
    }
    // 3.内存映射
    char *value = NULL;
    if ((value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, value_fd, 0)) == MAP_FAILED)
    {
        perror("value shared memory address map failed.");
        exit(EXIT_FAILURE);
    }
    // 无名信号量的内存映射
    sem_t *sem = NULL;
    if ((sem = mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED, sem_fd, 0)) == MAP_FAILED)
    {
        perror("semaphore shared memory address map failed.");
        exit(EXIT_FAILURE);
    }
    // 4.初始化值
    *value = 0;
    // 初始化无名信号量
    if (sem_init(sem, 1, 1) == -1)
    {
        perror("int unnamed semaphore failed.");
        exit(EXIT_FAILURE);
    }
    // 5.使用内存映射实现进程间通信
    if ((pid = fork()) < 0)
    {
        perror("create child process failed!");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        // 这里是子进程要指定的代码
        // 6.等待信号量
        if (sem_wait(sem) == -1)
        {
            perror("child process wait semaphore failed.");
            exit(EXIT_FAILURE);
        }
        int temp = *value + 1;
        sleep(1);
        *value = temp;
        // 7.信号量唤醒
        if (sem_post(sem) == -1)
        {
            perror("child process awaken semaphore failed.");
            exit(EXIT_FAILURE);
        }
    }
    else
    {
        // 这里是父进程要指定的代码
        // 6.等待信号量
        if (sem_wait(sem) == -1)
        {
            perror("parent process wait semaphore failed.");
            exit(EXIT_FAILURE);
        }
        int temp = *value + 1;
        sleep(1);
        *value = temp;
        // 7.信号量唤醒
        if (sem_post(sem) == -1)
        {
            perror("parent process awaken semaphore failed.");
            exit(EXIT_FAILURE);
        }
        // 等待子进程完成,确保子进程写入数据到共享内存
        waitpid(pid, NULL, 0);
        printf("the final value is %d.\n", *value);
    }
    // 8.无论父子进程都应该释放映射区
    if (munmap(value, sizeof(int)) == -1)
    {
        perror("release value memory map failed!");
        exit(EXIT_FAILURE);
    }
    // 释放无名信号量的映射区
    if (munmap(sem, sizeof(sem_t)) == -1)
    {
        perror("release semaphore memory map failed!");
        exit(EXIT_FAILURE);
    }
    // 9.映射完成之后,需要关闭fd连接不是释放
    if (close(value_fd) == -1)
    {
        perror("close value shared memory failed.");
        exit(EXIT_FAILURE);
    }
    // 关闭无名信号的连接
    if (close(sem_fd) == -1)
    {
        perror("close semaphore shared memory failed.");
        exit(EXIT_FAILURE);
    }
   
    if (pid > 0)
    {
        // 10.释放共享内存对象映射
        if (shm_unlink(shm_name) == -1)
        {
            perror("release value shared memory failed.");
            exit(EXIT_FAILURE);
        }
        // 释放无名信号量的共享内存对象映射
        if (shm_unlink(sem_name) == -1)
        {
            perror("release semaphore shared memory failed.");
            exit(EXIT_FAILURE);
        }
    }
    return 0;
}
  在终端中输入 make 运行。
如果信号量被用于进程间同步时,当我们使用
sem_init()函数初始化信号量时,该函数的第二个参数的值需要设置为 1。信号量必须置于共享内存区域,以确保多个进程都可以访问,否则每个进程各自管理自己的信号量,后者并没有起到进程间通信的作用。
四、有名信号量的API
4.1、创建有名信号量
  我们可以使用 sem_open() 函数创建有名信号量。
/**
 * @brief 创建有名信号量
 * 
 * @param __name 信号量名
 * @param __oflag 标记位
 * @param ... 
 * @return sem_t* 成功返回有名信号量地址,失败返回NULL
 */
sem_t *sem_open(const char *__name, int __oflag, ...);
  参数 __name 是 有名信号量名,名字是以 / 开头,以 \0 结尾的字符串,斜杠后面可以有若干个字符,但不能在出现斜杠
  参数 __oflag 是 标记位,用来控制调用函数的行为,是一个或多个值或运算的记过。常用的是 O_CREAT 值,该值表示 如果信号量不存在则创建。如果指定了 O_CREAT 标记,则必须提供 mode 和 value 参数。mode 参数是 有名信号量在临时文件系统中对应文件的权限。需要注意的是,应确保每个需要访问当前有名信号量的继承都可以获得读写权限。value 参数是 信号量的初始值。
4.2、关闭有名信号量
  我们可以使用 sem_close() 函数来 关闭有名信号量。
/**
 * @brief 关闭有名信号量
 * 
 * @param __sem 信号量指针
 * @return 成功返回0,失败返回-1
 */
int sem_close(sem_t *__sem);
4.3、释放有名信号量
  我们可以使用 sem_close() 函数来 释放有名信号量。
/**
 * @brief 释放有名信号量
 * 
 * @param __name 信号量名
 * @return 成功返回0,失败返回-1
 */
int sem_unlink(const char *__name);
五、有名信号量的使用
修改 main.c 文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
#include <semaphore.h>
int main(void)
{
    pid_t pid = 0;
    char shm_name[] = "shm_value";
    char sem_name[] = "/named_semaphore";
    // 1.创建共享内存
    int value_fd = 0;
    if ((value_fd = shm_open(shm_name, O_RDWR | O_CREAT, 0644)) == -1)
    {
        perror("create value shared memory failed.");
        exit(EXIT_FAILURE);
    }
    // 2.设置共享内存对象大小
    if (ftruncate(value_fd, sizeof(int)) == -1)
    {
        perror("set value shared memory size failed.");
        exit(EXIT_FAILURE);
    }
    // 3.内存映射
    char *value = NULL;
    if ((value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, value_fd, 0)) == MAP_FAILED)
    {
        perror("value shared memory address map failed.");
        exit(EXIT_FAILURE);
    }
    // 4.初始化值
    *value = 0;
    // 5.创建有名信号量
    sem_t *sem = NULL;
    if ((sem = sem_open(sem_name, O_CREAT, 0644, 1)) == NULL)
    {
        perror("create named semaphore failed.");
        exit(EXIT_FAILURE);
    }
    // 6.使用内存映射实现进程间通信
    if ((pid = fork()) < 0)
    {
        perror("create child process failed!");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        // 这里是子进程要指定的代码
        // 7.等待信号量
        if (sem_wait(sem) == -1)
        {
            perror("child process wait semaphore failed.");
            exit(EXIT_FAILURE);
        }
        int temp = *value + 1;
        sleep(1);
        *value = temp;
        // 8.信号量唤醒
        if (sem_post(sem) == -1)
        {
            perror("child process awaken semaphore failed.");
            exit(EXIT_FAILURE);
        }
    }
    else
    {
        // 这里是父进程要指定的代码
        // 7.等待信号量
        if (sem_wait(sem) == -1)
        {
            perror("parent process wait semaphore failed.");
            exit(EXIT_FAILURE);
        }
        int temp = *value + 1;
        sleep(1);
        *value = temp;
        // 8.信号量唤醒
        if (sem_post(sem) == -1)
        {
            perror("parent process awaken semaphore failed.");
            exit(EXIT_FAILURE);
        }
        // 等待子进程完成,确保子进程写入数据到共享内存
        waitpid(pid, NULL, 0);
        printf("the final value is %d.\n", *value);
    }
    // 9.无论父子进程都需要关闭信号量
    if (sem_close(sem) == -1)
    {
        perror("close semaphore failed.");
        exit(EXIT_FAILURE);
    }
    // 10.无论父子进程都应该释放映射区
    if (munmap(value, sizeof(int)) == -1)
    {
        perror("release value memory map failed!");
        exit(EXIT_FAILURE);
    }
    // 11.映射完成之后,需要关闭fd连接不是释放
    if (close(value_fd) == -1)
    {
        perror("close shared memory failed.");
        exit(EXIT_FAILURE);
    }
   
    if (pid > 0)
    {
        // 12.释放信号量
        if (sem_unlink(sem_name) == -1)
        {
            perror("release semaphore failed.");
            exit(EXIT_FAILURE);
        }
        // 12.释放共享内存对象映射
        if (shm_unlink(shm_name) == -1)
        {
            perror("release shared memory failed.");
            exit(EXIT_FAILURE);
        }
    }
    return 0;
}
  在终端中输入 make 运行。
                    
                
                
            
        
浙公网安备 33010602011771号