【进程间通信】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;
}

<think> 我们被要求详细讲解Linux进程间通信信号量。根据用户提供的引用,我们需要整合这些信息,并构建一个详细的回答。 引用[1]提到有名信号量(Named Semaphore)是由操作系统内核维护的,具有全局唯一名字,可以在不同进程之间共享。 引用[2]介绍了信号是软中断,用于进程间通信和处理异常,是异步通知机制。 引用[3]解释了信号量是一种计数器,用于协调多个进程对共享资源的访问。它描述了信号量的基本操作过程:当进程想要使用共享资源时,先测试信号量,如果值大于0则使用并减1,否则挂起等待;当资源被释放时,信号量加1。 引用[4]提到了同步的概念,即进程之间按照依赖关系运行,并提到实现信号量的操作。 注意:这里用户询问的是“信号量”(semaphore),而引用[2]中提到了“信号”(signal),这是两个不同的概念。我们需要区分清楚,避免混淆。 根据以上引用,我们组织回答如下: 1. 信号量的定义和作用:信号量是一个计数器,用于控制多个进程对共享资源的访问。它主要用于进程间的同步和互斥。 2. 信号量的类型:分为有名信号量和无名信号量(也叫基于内存的信号量)。有名信号量通过名字在系统中唯一标识,可用于不同进程之间;无名信号量通常存在于内存中,一般用于同一进程内的线程间同步,但也可以通过共享内存用于进程间。 3. 信号量的操作:P操作(等待,信号量减1)和V操作(释放,信号量加1)。当信号量值为0时,试图进行P操作的进程将被阻塞,直到有其他进程执行V操作。 4. 在Linux中,信号量的实现:System V信号量POSIX信号量POSIX信号量又分为有名信号量和无名信号量。 5. 使用示例:我们将给出一个简单的使用POSIX有名信号量进行进程间同步的代码示例。 注意:由于引用中提到了有名信号量,我们将重点放在有名信号量的介绍上,但也会简要提及无名信号量。 我们将按照以下结构组织回答: 1. 信号量的基本概念 2. 信号量的类型(有名信号量和无名信号量) 3. 信号量的工作原理(P/V操作) 4. 信号量的操作函数(POSIX有名信号量) 5. 使用示例 6. 注意事项 同时,根据要求,我们需要在最后生成相关问题。 注意:引用标识要自然地添加在引用内容的末尾。我们参考了用户提供的引用[1][3][4](引用[2]是关于信号的,与信号量不同,所以我们不引用它)。 下面开始回答: </think> ### Linux 进程间通信信号量详解 信号量(Semaphore)是 Linux 进程间通信(IPC)的核心机制之一,本质是一个**受保护的计数器**,用于协调多个进程对共享资源的访问控制。其核心功能是通过原子操作确保资源的互斥访问和同步协调。 --- #### **1. 信号量基础原理** - **计数器机制**: 信号量维护一个整数值,表示可用资源的数量: - 值 $ > 0 $:当前有可用资源 - 值 $ = 0 $:资源耗尽,请求进程需等待 - 值 $ < 0 $:绝对值表示等待队列中的进程数量 - **P/V 操作**(原语操作): - **P 操作**(`sem_wait()`): $$ P(S): \text{ if } S > 0 \text{ then } S \leftarrow S-1 \text{ else block} $$ 进程尝试获取资源,若信号量值 $ S \leq 0 $ 则阻塞 - **V 操作**(`sem_post()`): $$ V(S): S \leftarrow S+1 \text{, wake a blocked process} $$ 进程释放资源,唤醒等待队列中的进程[^3] --- #### **2. 信号量类型** | 类型 | 标识方式 | 作用范围 | 生命周期 | |------|----------|----------|----------| | **有名信号量** | 文件系统路径名 (如 `/mysem`) | 跨进程共享 | 持续到显式删除或系统重启 | | **无名信号量** | 共享内存地址 | 父子进程/线程间 | 随进程终止销毁[^1] | > 有名信号量通过全局唯一路径名访问,适合无亲缘关系进程;无名信号量需配合共享内存使用,适合相关进程。 --- #### **3. POSIX 信号量核心 API** ```c #include <semaphore.h> // 创建/打开有名信号量 sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 初始化无名信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); // P操作(等待资源) int sem_wait(sem_t *sem); // 阻塞等待 int sem_trywait(sem_t *sem); // 非阻塞尝试 // V操作(释放资源) int sem_post(sem_t *sem); // 关闭信号量 int sem_close(sem_t *sem); // 删除有名信号量 int sem_unlink(const char *name); ``` **参数说明**: - `value`:信号量初始值(资源数量) - `pshared`:`0`为线程共享,`非0`为进程共享 - `oflag`:`O_CREAT`(不存在则创建)或 `O_EXCL`(配合`O_CREAT`确保新建) --- #### **4. 使用场景与同步模型** 1. **互斥锁(Mutex)**: 初始值设为1,实现临界区保护: ```c sem_t mutex; sem_init(&mutex, 1, 1); // 进程间共享,初始值1 // 进程A sem_wait(&mutex); // 进入临界区 /* 访问共享资源 */ sem_post(&mutex); // 离开临界区 ``` 2. **生产者-消费者问题**: 使用两个信号量控制缓冲区的空/满状态: ```c sem_t empty, full; sem_init(&empty, 1, BUFFER_SIZE); // 初始空槽数量 sem_init(&full, 1, 0); // 初始满槽数量 // 生产者 sem_wait(&empty); // 等待空槽 /* 生产数据放入缓冲区 */ sem_post(&full); // 增加满槽计数 // 消费者 sem_wait(&full); // 等待满槽 /* 取出缓冲区数据 */ sem_post(&empty); // 增加空槽计数 ``` --- #### **5. 实战示例:有名信号量同步进程** ```c #include <fcntl.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> int main() { sem_t *sem = sem_open("/demo_sem", O_CREAT, 0644, 1); // 初始值=1 if (fork() == 0) { // 子进程 sem_wait(sem); printf("Child enters critical section\n"); sleep(2); // 模拟操作共享资源 printf("Child exits critical section\n"); sem_post(sem); } else { // 父进程 sem_wait(sem); printf("Parent enters critical section\n"); sleep(1); // 模拟操作共享资源 printf("Parent exits critical section\n"); sem_post(sem); wait(NULL); sem_unlink("/demo_sem"); // 清理信号量 } return 0; } ``` **输出**: ```plaintext Parent enters critical section Parent exits critical section Child enters critical section # 子进程等待父进程释放信号量 Child exits critical section ``` --- #### **6. 信号量与信号的区别** | 特性 | 信号量 (Semaphore) | 信号 (Signal) | |--------------|----------------------------------|-----------------------------| | **本质** | 资源计数器 | 软件中断通知 | | **用途** | 控制共享资源访问 | 事件通知/异常处理 | | **通信方向** | 无明确发送方/接收方 | 有明确发送方(如`kill()`) | | **同步机制** | 内置阻塞/唤醒队列 | 依赖信号处理函数 | | **数据携带** | 仅计数器数值 | 可附带`siginfo_t`信息[^2] | --- #### **7. 注意事项** 1. **死锁预防**:避免循环等待(如进程A持有信号量S1等待S2,进程B持有S2等待S1) 2. **信号量泄漏**:确保`sem_close()`/`sem_unlink()`配对调用 3. **原子性保障**:P/V 操作是原子指令,确保计数器更新不可分割 4. **优先级反转**:高优先级进程可能因等待低优先级进程持有的信号量而阻塞 > 信号量是实现进程同步的底层原语,在数据库连接池、线程池等资源受限场景中广泛应用[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值