【进程间通信】Posix 信号量

目录

概述

 sem_open、sem_close 和 sem_unlink 函数

sem_wait 和 sem_trywait 函数

 sem_post 和 sem_getvalue 函数

有名信号量代码示例

生产者进程

消费者进程

sem_init 和 sem_destory 函数

基于内存的信号量的代码示例

公共头文件

生产者代码

消费者代码


概述

        信号量是用于不同进程之间,或者同一进程中的不同线程之间的一种同步的手段。

        一个进程可以在某个信号量上执行3种操作:

        (1)创建一个信号量。要求调用者指定信号量初始值,如果是二值信号量,通常来说值是1,但也可以是0;

        (2)等待一个信号量。该操作会检测信号量的值,如果值小于或等于0,那就阻塞等待。一旦值大于0就将他减1;

        (3)挂出一个信号量。该操作将信号量值加1。如果一些进程在阻塞等待着信号量的值变为大于0,就会唤醒一个进程。

        这个过程又可以称为 PV 操作,等待信号量就是一个 P 操作,挂出是一个 V 操作。

         Posix提供两种信号量:有名信号量和无名信号量,后者也称为基于内存的信号量。有名信号量是必须在内核中去维护这个信号量,基于内存的信号量是可以在用户空间去维护信号量的。

        命名信号量通过一个名字来标识,这个名字可能与文件系统中的路径对应,但并不要求实际存储在文件系统中。在类Unix系统中,命名信号量通常以 / 开头的路径形式命名,例如 /mysem,但这只是一个标识符,内核会管理信号量在两个进程间的的实际存储和同步。相比之下,基于内存信号量因为没有公共名字的这样一个操作,只能通过两个进程去共享同一块内存空间,然后将信号量存于这个共享空间,从而实现同步。

        基于内存的信号量下图是这两种信号量使用函数的对比。

        下图是一个进程中两个线程共享一个基于内存的信号量。

        下图是两个进程共享一个共享区域中的基于内存的信号量。

 sem_open、sem_close 和 sem_unlink 函数

          sem_open() 用于创建一个有名信号量或者打开一个已经存在的有名信号量。

#include <semaphore.h>

sem_t *sem_open(const char *name, int oflag, ...
                 /* mode_t mode, unsigned int value */);
/* 成功返回指向信号量的指针, 失败返回SEM_FAILED */

        oflag 位可以是0、O_CREAT 或 O_CREAT | O_EXCL。如果指定为O_CREAT,就需要去设置第三个和第四个参数,mode 指定权限位,value 指定信号量初始值,初始值不能超过SEM_VALUE_MAX(至少为32767)。二值信号量初始值通常为1,计数信号量初始值通常大于1。标志位详细介绍可以去看【进程间通信】Posix IPC 这篇文章。

        sem_close() 用于关闭一个有名信号量。

#include <semaphore.h>
int sem_close(sem_t *sem);
/* 成功返回0,出错返回-1 */

        即使一个进程终止了,内核也会对打开的有名信号量执行这样的关闭操作。不论进程是自愿终止还是非自愿终止。

        关闭一个信号量并不能将其从系统中删除,即使没有进程去打开这个信号量,信号量的值也会随着内核一直存在。

        sem_unlink() 用于删除一个有名信号量。

#include <semaphore.h>
int sem_unlink(const char *name);
/* 成功返回0,出错返回-1 */

        每个信号量都有一个引用计数器记录着打开的次数。调用 sem_unlink() 立马就能删除信号量的名字,但是内存要在最后一个 sem_close() 调用时才能释放。

sem_wait 和 sem_trywait 函数

        sem_wait() 函数会去判断信号量的值。如果值大于0,就将其减1并立即返回。如果值等于0,调用线程就会进入睡眠,直到其大于0,这时再将其减1,函数随后返回。

#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
/* 成功返回0,失败返回-1 */

        sem_trywait() 检测到信号量为0后,不会进入睡眠状态,函数会立即返回一个 ENGAIN 的错误。

        如果被某个信号中断,就会导致 sem_wait() 过早的返回,返回一个 EINTR。

 sem_post 和 sem_getvalue 函数

        当一个线程使用完信号量之后,需调用 sem_post() 函数,将信号量进行加1的操作,这样就可以去唤醒其他等待的线程。

        sem_getvalue() 在由 valp 指向的整数中返回所指定信号量的当前值。如果该信号量当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号量解锁的线程数。

#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *valp);
/* 成功返回0,失败返回-1 */

有名信号量代码示例

生产者进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/stat.h>

#define SEM_NAME "/demo_sem"
#define ITEMS 5

int main() 
{
    sem_t *sem;
    int i;

    /* 创建有名信号量,初始值为1 */ 
    sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0644, 1);
    if (sem == SEM_FAILED) 
    {
        perror("信号量创建失败");
        exit(EXIT_FAILURE);
    }

    printf("生产者启动...\n");

    for (i = 0; i < ITEMS; i++) 
    {
        sem_wait(sem);  
        printf("生产者: 生产物品 %d\n", i);
        sem_post(sem);   
        sleep(2);        
    }

    int val;
    sem_getvalue(sem, &val);
    printf("生产者: 最终信号量值 = %d\n", val);

    /*
    * 注意:这里不关闭和删除信号量,留给消费者使用
    * 实际应用中应该更优雅地处理清理工作
    */ 
    sem_close(sem);

    return EXIT_SUCCESS;
}

消费者进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/stat.h>

#define SEM_NAME "/demo_sem"
#define ITEMS 5

int main() 
{
    sem_t *sem;
    int i;

    /* 打开已存在的信号量 */ 
    sem = sem_open(SEM_NAME, 0);
    if (sem == SEM_FAILED) 
    {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }

    printf("消费者启动...\n");

    for (i = 0; i < ITEMS; i++) 
    {
        sem_wait(sem);  
        printf("消费者: 消费物品 %d\n", i);
        sem_post(sem); 
        sleep(1); 
    }

    int val;
    sem_getvalue(sem, &val);
    printf("消费者: 最终信号量值 = %d\n", val);

    sem_close(sem);
    sem_unlink(SEM_NAME); 

    return EXIT_SUCCESS;
}

sem_init 和 sem_destory 函数

        前面处理的内容是用于有名信号量,这些信号量由一个 name 参数表示,通常代指文件系统中的某个文件。Posix 也有基于内存的信号量。由应用程序分配信号量的内存,由系统初始化它们。

#include <semaphore.h>
int sem_init(sem_t *sem, int shared, unsigned int value);
/* 失败返回-1 */
int sem_destroy(sem_t *sem);
/* 成功返回0,失败返回-1 */

        如果 shared 为0,就是同一个进程中的所有线程共享这个信号量,否则信号量是在多进程之间共享。当 shared 非0的时候,信号量必须放在某种类型的共享内存中,要使用它的进程都要有权限访问这个共享内存。value 是信号量的初始值。

        sem_destory() 用于销毁一个基于内存的信号量。

        在实际应用中,基于内存的信号量通常是和共享内存一起用的。使用基于内存的信号量时,因为不会去操作内核,所以无法在多个进程之前进行信号量的共享,就需要共享内存去在两个进程之间共享出公共的信号量,实现同步。

        接下来,给大家一个两个进程通过共享内存的信号量的方式处理一个环形的任务队列的生产者-消费者模型。

基于内存的信号量的代码示例

        该代码定义了一个大小为 10 的环形队列,生产者和消费者共享了一个信号量和任务队列的结构体,任务队列满了,消费者就去消费。并且生产者和消费者进行的操作通过另一个信号量进行互斥。共享内存部分代码不理解的可以参考后续文章。

公共头文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <semaphore.h>

#define QUEUE_SIZE 10
#define SHM_KEY "/shm_ring_queue"

typedef struct 
{
    int data[QUEUE_SIZE];  /*环形队列*/
    int head;              /*消费者读取位置*/
    int tail;              /*生产者写入位置*/
} RingQueue;

typedef struct 
{
    RingQueue queue;
    sem_t empty;           /*空闲槽位信号量*/
    sem_t full;            /*已填充槽位信号量*/
    sem_t mutex;           /*互斥锁*/
} SharedData;

生产者代码 

#include "common.h"

int main() {
    /* 创建或打开共享内存 */
    int fd = shm_open(SHM_KEY, O_CREAT | O_RDWR, 0666);
    if (fd == -1) 
    {
        perror("打开共享内存失败");
        exit(1);
    }
    ftruncate(fd, sizeof(SharedData));

    /* 映射共享内存 */
    SharedData *shared = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared == MAP_FAILED) 
    {
        perror("映射共享内存失败");
        exit(1);
    }

    /* 初始化信号量(仅在第一个进程创建) */
    sem_init(&shared->empty, 1, QUEUE_SIZE);  /*初始空闲槽位=队列容量*/
    sem_init(&shared->full, 1, 0);            /*初始已填充槽位=0*/
    sem_init(&shared->mutex, 1, 1);           /*互斥锁初始=1*/

    int item = 0;
    while (1) 
    {
        sem_wait(&shared->empty);  /*等待空闲槽位*/
        sem_wait(&shared->mutex);  /*进入临界区*/

        /*生产数据到环形队列*/
        shared->queue.data[shared->queue.tail] = item++;
        shared->queue.tail = (shared->queue.tail + 1) % QUEUE_SIZE;
        printf("生产了: %d\n", item - 1);

        sem_post(&shared->mutex);  /*离开临界区*/
        sem_post(&shared->full);   /*增加已填充槽位*/

        sleep(1);  /*模拟生产耗时*/
    }

    /*实际应用中需添加退出逻辑*/
    munmap(shared, sizeof(SharedData));
    shm_unlink(SHM_KEY);
    return 0;
}

消费者代码

#include "common.h"

int main() {
    /* 打开共享内存 */
    int fd = shm_open(SHM_KEY, O_RDWR, 0666);
    if (fd == -1) 
    {
        perror("打开共享内存失败");
        exit(1);
    }

    /* 映射共享内存 */
    SharedData *shared = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared == MAP_FAILED) 
    {
        perror("映射共享内存失败");
        exit(1);
    }

    while (1) 
    {
        sem_wait(&shared->full);   /*等待已填充槽位*/
        sem_wait(&shared->mutex);  /*进入临界区*/

        /*从环形队列消费数据*/
        int item = shared->queue.data[shared->queue.head];
        shared->queue.head = (shared->queue.head + 1) % QUEUE_SIZE;
        printf("Consumed: %d\n", item);

        sem_post(&shared->mutex);  /*离开临界区*/
        sem_post(&shared->empty);  /*增加空闲槽位*/

        sleep(2);  /*模拟消费耗时*/
    }

    munmap(shared, sizeof(SharedData));
    return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值