Linux编程:6、进程通信-信号量与共享内存

一、信号量

1. 基本概念

  • 定义:用于实现进程间互斥与同步的机制,分为两种类型:
    • 二进制信号量:只能取 0 或 1,最常用;
    • 通用信号量:可以取多个正整数值。
  • 核心作用:解决多个进程对共享资源(如串口设备)的 “互斥访问” 问题,避免冲突。


2. 核心操作(PV 操作)

  • P 操作
    • 若信号量的值 > 0,将其减 1;
    • 若信号量的值 = 0,当前进程被挂起。
  • V 操作
    • 若有进程因等待该信号量被挂起,唤醒其中一个进程;
    • 若没有等待进程,将信号量的值加 1。
  • 特性:PV 操作是 “原子操作”,执行过程不会被中断。

3. 关键接口函数

  • semget(创建 / 获取信号量)

  • semctl(控制信号量)

    • 功能:设置信号量值、删除信号量等;
    • 许多linux系统都没有 union semun 类型的定义,需要手动定义:
      // linux上面没有默认的semun,需要自己定义
      typedef union semun
      {
          int              val;   /* Value for SETVAL */
          struct semid_ds* buf;   /* Buffer for IPC_STAT, IPC_SET */
          unsigned short*  array; /* Array for GETALL, SETALL */
          struct seminfo*  __buf; /* Buffer for IPC_INFO (Linux-specific) */
      } semun;
    • int val:用于SETVAL命令,设置信号量的值。
       
    • struct semid_ds *buf:用于IPC_STATIPC_SET命令,获取或设置信号量集的状态。
       
    • unsigned short *array:用于GETALLSETALL命令,获取或设置所有信号量的值。
       
    • struct seminfo *__buf:用于IPC_INFO命令(Linux 特定),获取系统信号量限制和状态信息。
  • semop(执行 PV 操作)


4. 代码实战

contorl.cpp

#include <cstdlib>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>

// linux上面没有默认的semun,需要自己定义
typedef union semun
{
    int              val;   /* Value for SETVAL */
    struct semid_ds* buf;   /* Buffer for IPC_STAT, IPC_SET */
    unsigned short*  array; /* Array for GETALL, SETALL */
    struct seminfo*  __buf; /* Buffer for IPC_INFO (Linux-specific) */
} semun;

/*
 * 初始化信号量
 * 参数:
 *   sem_id - 信号量标识符
 * 返回值:
 *   成功返回true,失败返回false
 */
bool sem_init(int sem_id)
{
    semun sem_union;
    sem_union.val = 1;  // 将信号量初始化为1,表示资源可用

    // 设置信号量的值
    int res = semctl(sem_id, 0, SETVAL, sem_union);
    if (res == -1) {
        return false;
    }
    return true;
}

/*
 * 删除信号量
 * 参数:
 *   sem_id - 信号量标识符
 * 返回值:
 *   成功返回true,失败返回false
 */
bool sem_del(int sem_id)
{
    semun sem_union;

    // 注意:删除信号量不需要设置sem_union的值,但有些系统要求传递有效的union
    // 这里保留赋值是为了兼容性考虑

    // 删除信号量集
    int res = semctl(sem_id, 0, IPC_RMID, sem_union);
    if (res == -1) {
        return false;
    }
    return true;
}

int main()
{
    // 创建一个信号量集,包含1个信号量
    // 1234 - 信号量的键值,用于标识信号量
    // 0666 | IPC_CREAT - 权限为0666(所有用户可读可写),如果不存在则创建
    int sem_id = semget(1234, 1, 0666 | IPC_CREAT);

    if (sem_id == -1) {
        perror("创建信号量(1234)失败!");
        exit(1);
    }

    printf("创建信号量(1234)成功!\n");

    // 初始化信号量
    if (!sem_init(sem_id)) {
        perror("信号量初始化失败!");
        exit(1);
    }

    // 主循环:程序持续运行,直到用户输入"stop"
    while (1) {
        char str[256] = {0};
        printf("正在工作,如果想停止请输入stop: ");
        scanf("%s", str);
        if (strcmp(str, "stop") == 0) {
            break;
        }
    }

    // 删除信号量,释放系统资源
    if (!sem_del(sem_id)) {
        perror("信号量删除失败!");
        exit(1);
    }

    return 0;
}

work.cpp

#include <cstdlib>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>

/*
struct sembuf 是信号量操作的核心结构体,用于定义对信号量的具体操作
成员说明:
- sem_num:要操作的信号量编号(在信号量集中的索引,单个信号量时通常取0)
- sem_op:操作类型(-1表示P操作/等待,1表示V操作/释放)
- sem_flg:操作标志(SEM_UNDO表示进程退出时自动撤销操作,避免信号量状态异常)
*/

/**
 * 信号量P操作(等待操作)
 * 功能:尝试获取信号量,若信号量值为0则阻塞等待,直到可用
 * 参数:sem_id - 信号量标识符
 * 返回值:成功返回true,失败返回false
 */
bool sem_p(int sem_id)
{
    struct sembuf sem_buf;       // 定义信号量操作结构体
    sem_buf.sem_num = 0;         // 操作第0个信号量(信号量集的第一个)
    sem_buf.sem_op  = -1;        // P操作:信号量值减1(获取资源)
    sem_buf.sem_flg = SEM_UNDO;  // 进程异常退出时自动恢复信号量状态

    // 执行信号量操作,参数分别为:信号量ID、操作结构体、操作数量(1个)
    if (semop(sem_id, &sem_buf, 1) == -1) {
        perror("sem P failed");  // 输出P操作失败原因
        return false;
    }
    return true;
}

/**
 * 信号量V操作(释放操作)
 * 功能:释放信号量,将信号量值加1,唤醒等待的进程
 * 参数:sem_id - 信号量标识符
 * 返回值:成功返回true,失败返回false
 */
bool sem_v(int sem_id)
{
    struct sembuf sem_buf;       // 定义信号量操作结构体
    sem_buf.sem_num = 0;         // 操作第0个信号量
    sem_buf.sem_op  = 1;         // V操作:信号量值加1(释放资源)
    sem_buf.sem_flg = SEM_UNDO;  // 进程异常退出时自动恢复信号量状态

    // 执行信号量操作
    if (semop(sem_id, &sem_buf, 1) == -1) {
        perror("sem V failed");  // 输出V操作失败原因
        return false;
    }
    return true;
}

int main()
{
    // 获取已创建的信号量(键值1234,包含1个信号量,权限0666)
    // 注意:此处依赖control程序先创建并初始化信号量,否则会失败
    int sem_id = semget(1234, 1, 0666);
    if (sem_id == -1) {
        perror("信号量还没有创建好");  // 提示信号量未就绪
        exit(1);                       // 退出程序
    }

    // 无限循环:持续通过信号量控制资源访问
    while (1) {
        sem_p(sem_id);  // 执行P操作,获取信号量(进入临界区)

        // 临界区操作:打印当前进程ID及工作状态
        printf("进程-%d 正在工作...\n", getpid());  // getpid()获取当前进程ID
        sleep(3);                                   // 模拟工作耗时3秒
        printf("进程-%d 工作结束\n", getpid());
        sleep(3);  // 模拟后续处理耗时3秒

        sem_v(sem_id);  // 执行V操作,释放信号量(退出临界区)
    }

    return 0;
}

运行结果:

结论:可以看出,每次都需要一个进程从开始到完整的结束为止,才会开始运行新的进程,信号量可以实现进程间互斥与同步的机制,且PV操作具有原子性


二、共享内存

1. 基本概念

  • 原理:多个进程将同一块物理内存映射到各自的地址空间,直接读写该内存实现通信;
  • 特点:
    • 无内置同步机制,需程序员手动处理(如通过 “生产者 - 消费者” 逻辑控制读写顺序);
    • 适用于传递大块数据,效率较高。
  • 共享内存的原理

2. 关键接口函数

  • shmget(创建 / 获取共享内存)

  • shmat(映射共享内存):

  • shmctl(控制共享内存):

  • shmdt(解除映射):


3. 代码实战

  • 场景:两个进程通过共享内存传递文本数据;
  • 实现
    • 定义共享数据结构struct shared_data,包含is_new_data(标志是否有新数据)和text(存储数据);
    • 进程 1(shm1.cpp):读取共享内存中的数据,读完后将is_new_data设为 0;
    • 进程 2(shm2.cpp):写入数据后将is_new_data设为 1,等待对方读取后再写入新数据,实现简单同步。

common.h

#ifndef COMMON_H
#define COMMON_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define TEXT_SIZE 4096

typedef struct shared_data
{
    int is_new_data;    // 1: 有新的数据  0: 数据已经被读取
    char text[TEXT_SIZE];
} shared_data;

#endif // !COMMON_H

shm1.cpp

#include "common.h"

int main()
{
    // 创建或获取共享内存段,键值为1234,大小为struct shared_data的大小
    // 0666表示读写权限,IPC_CREAT表示若不存在则创建
    int shmid = shmget(1234, sizeof(struct shared_data), 0666 | IPC_CREAT);

    // 检查共享内存创建/获取是否失败
    if (shmid == -1) {
        perror("shmget failed.");
        exit(1);
    }

    // 将共享内存段映射到当前进程的地址空间
    // 第二个参数为0,表示由操作系统自动选择映射地址
    // 第三个参数为0,表示默认具有读写权限
    void* shared_memory = shmat(shmid, 0, 0);
    
    // 检查映射是否失败
    if(shared_memory == (void*)-1) {
        perror("shmat failed.");
        exit(1);
    }

    // 打印映射后的内存地址,用于调试
    printf("A映射后的地址: %lld\n", (long long)shared_memory);

    // 运行标志,控制循环读取共享内存
    int running = 1;

    // 将共享内存转换为shared_data结构体类型的指针
    shared_data* shared_buf = static_cast<shared_data*>(shared_memory);
    
    // 初始化随机数种子,用于模拟不同的处理延迟
    srand(getpid());
    
    // 循环读取共享内存中的数据
    while (running)
    {
        // 检查生产者是否写入了新数据
        if(shared_buf->is_new_data) {
            // 读取并打印新数据
            printf("A读取到的数据: %s", shared_buf->text);
            
            // 模拟数据处理过程,随机睡眠0-2秒
            sleep(rand() % 3);
            
            // 标记数据已处理,通知生产者可以写入新数据
            shared_buf->is_new_data = 0;

            // 判断是否接收到结束信号
            // 注意:此处使用strcmp比较字符串,若返回0表示相等
            // 原代码逻辑有误,应改为strcmp(shared_buf->text, "end") == 0
            if(strcmp(shared_buf->text, "end") == 0) {
                running = 0;  // 接收到结束信号,退出循环
            }
        }
    }

    // 将共享内存从当前进程的地址空间分离
    if(shmdt(shared_memory) == -1) {
        perror("shmdt failed.");
        exit(1);
    }

    // 删除共享内存段,释放系统资源
    // 注意:只有当所有进程都分离后,共享内存才会真正被删除
    if(shmctl(shmid, IPC_RMID, 0) == -1)
    {
        perror("shmctl IPC_RMID failed");
        exit(1);
    }

    return 0;
}

shm2.cpp

#include "common.h"

int main()
{
    // 创建或获取共享内存段,键值为1234,大小为struct shared_data的大小
    // 0666表示读写权限,IPC_CREAT表示若不存在则创建
    int shmid = shmget(1234, sizeof(struct shared_data), 0666 | IPC_CREAT);

    // 检查共享内存创建/获取是否失败
    if (shmid == -1) {
        perror("shmget failed.");
        exit(1);
    }

    // 将共享内存段映射到当前进程的地址空间
    // 第二个参数为0,表示由操作系统自动选择映射地址
    // 第三个参数为0,表示默认具有读写权限
    void* shared_memory = shmat(shmid, 0, 0);

    // 检查映射是否失败
    if (shared_memory == (void*)-1) {
        perror("shmat failed.");
        exit(1);
    }

    // 打印映射后的内存地址,用于调试
    printf("B映射后的地址: %lld\n", (long long)shared_memory);

    // 运行标志,控制循环写入共享内存
    int running = 1;

    // 将共享内存转换为shared_data结构体类型的指针
    shared_data* shared_buf = static_cast<shared_data*>(shared_memory);
    
    
    // 用于临时存储用户输入的缓冲区
    char buff[BUFSIZ];
    
    // 循环等待用户输入并写入共享内存
    while (running) {
        // 检查消费者是否已处理完上一批数据
        if (shared_buf->is_new_data) {
            // 数据未处理,等待1秒后重试
            sleep(1);
            printf("等待数据被处理\n");
            continue;
        }
        
        // 提示用户输入数据
        printf("请输入需要发送的数据: ");
        
        // 从标准输入读取一行数据
        fgets(buff, BUFSIZ, stdin);

        // 将用户输入复制到共享内存的text字段
        // 使用strncpy防止缓冲区溢出,TEXT_SIZE为预设的最大长度
        strncpy(shared_buf->text, buff, TEXT_SIZE);
        
        // 标记数据已更新,通知消费者可以读取
        shared_buf->is_new_data = 1;

        // 检查用户是否输入"end"作为结束指令
        // 比较前3个字符,避免包含换行符的影响
        if(strncmp(buff, "end", 3) == 0) {
            running = 0;  // 设置结束标志,退出循环
        }
    }

    // 将共享内存从当前进程的地址空间分离
    if (shmdt(shared_memory) == -1) {
        perror("shmdt failed.");
        exit(1);
    }

    // 删除共享内存段(实际为标记删除,所有进程分离后才真正释放)
    if (shmctl(shmid, IPC_RMID, 0) == -1) {
        perror("shmctl IPC_RMID failed");
        exit(1);
    }

    return 0;
}

运行结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值