Linux信号量

1. 为什么要发明信号量?

这种多进程争抢访问的共享资源(如共享内存、打印机),被称为 临界资源 (Critical Resource) 。访问这些资源的代码段,叫 临界区 (Critical Section)

我们面临的问题是:原子性 (Atomicity)

  • 你在 C++ 里写 count++,汇编层面其实是 3 条指令(读入寄存器、加1、写回内存)。
  • 如果进程 A 执行了一半被切走了,进程 B 来了,数据就会乱套。

信号量 就是为了解决这个问题而生的。它本质上是一个内核中的计数器 ,但它的增减操作是原子的(要么全做完,要么不做,不会被打断)。

2. 核心原理:PV 原语

这是荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出的概念,是所有并发编程的基石。

假设我们需要一把"锁"(互斥量),信号量的初始值设为 1

  • P 操作 (Proberen, 测试/申请)
    • 逻辑:sem-- (计数器减 1)。
    • 判断
      • 如果减完后值 >= 0:申请资源成功,进程继续执行(拿到锁了)。
      • 如果减完后值 < 0(或者减之前是0):资源没了,进程挂起阻塞,放入等待队列。
  • V 操作 (Verhogen, 增加/释放)
    • 逻辑:sem++ (计数器加 1)。
    • 动作
      • 如果加完后值 <=0:说明等待队列里还有人,唤醒一个等待的进程。
      • 进程继续执行。

3. 复杂的 System V 接口

Linux 的 System V 信号量设计得比较复杂(它设计的初衷是让你可以一次操作一组信号量),所以接口参数很多。我们要学会把复杂变简单。

我们要用的三个核心函数:

A. semget**------ 创建/获取**

复制代码
int semget(key_t key, int nsems, int semflg);
  • key:和共享内存一样,用 ftok 生成。
  • nsems你要申请几个信号量? 通常我们只需要 1 个(作为互斥锁)。
  • semflgIPC_CREAT | 0666 等。

B. semctl**------ 控制/初始化**

复制代码
int semctl(int semid, int semnum, int cmd, ...);
  • semnum:操作第几个信号量?(下标从 0 开始)。
  • cmd
    • SETVAL:设置信号量的初始值(比如设为 1)。
    • IPC_RMID:删除信号量集。

C. semop**------ 核心 PV 操作**

复制代码
int semop(int semid, struct sembuf *sops, size_t nsops);

这是最难用的函数,我们需要定义一个结构体:

复制代码
struct sembuf {
    unsigned short sem_num;  // 操作第几个信号量 (0)
    short sem_op;            // -1 是 P操作,+1 是 V操作
    short sem_flg;           // 通常设为 0,或 SEM_UNDO
};

SEM_UNDO 是个很重要的标志:如果进程崩溃了没来得及释放锁,操作系统会自动帮你"撤销"之前的 P 操作,防止死锁。

4.信号量实现进程通信(代码)
common
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include<cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/sem.h>


// 生成key的路径和ID
const std::string PATH_NAME = ".";
const int PROJ_ID = 6666;

// 设置共享内存的大小
const int MEM_SIZE = 4096;

// 用于同步的命名管道
const std::string FIFO_NAME = "./my_pipe";

// 获取唯一的key

key_t GetKey()
{
    key_t key = ftok(PATH_NAME.c_str(), PROJ_ID);
    if (key < 0)
    {
        std::cout << "创建共享key获取失败" << std::endl;
        exit(1);
    }
    return key;
}

//信号量创建
int CreateSem(int nsems)
{
    int key=GetKey();
    int sem=semget(key,nsems,IPC_CREAT|0666);
    if(sem<0)
    {
        std::cout<<"信号量创建失败"<<std::endl;
        exit(1);
    }
    return sem;
}



//信号量获取

int GetSem()
{
     int key=GetKey();
    int sem=semget(key,0,0);
    if(sem<0)
    {
        std::cout<<"信号量获取失败"<<std::endl;
        exit(1);
    }
    return sem;
}



//信号量初始化
void InitSem(int semid, int which, int value)
{
    union semun {
               int              val;   
               struct semid_ds *buf;   
               unsigned short  *array; 
           };
        union semun se;
        se.val=value;
        semctl(semid,which,SETVAL,se);
}


//申请资源

void P(int semid, int which)
{
    //int semop(int semid, struct sembuf *sops, size_t nsops);
    sembuf se;
    se.sem_num=which;
    se.sem_op=-1;
    se.sem_flg=0;

    semop(semid,&se,1);
}



//释放资源
void V(int semid, int which)
{
    //int semop(int semid, struct sembuf *sops, size_t nsops);
    sembuf se;
    se.sem_num=which;
    se.sem_op=1;
    se.sem_flg=0;

    semop(semid,&se,1);
}


//信号量删除
void DleSem(int semid)
{
    //int semop(int semid, struct sembuf *sops, size_t nsops);
    semctl(semid,0,IPC_RMID);
}
server.cc
cpp 复制代码
#include "common.hpp"

// 创建共享内存,读取数据

class Init
{
public:
    Init()
    {
       

        // 共享内存
        key_t k = GetKey();
        std::cout << "server key: " << std::hex << k << std::dec << std::endl;

        // 不存在就创建,存在就报错
        _shmid = shmget(k, MEM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
        if (_shmid < 0)
        {
            perror("创建共享内存失败");
            exit(2);
        }
        std::cout << "创建共享内存成功:" << _shmid << std::endl;

        // 挂载
        _start = (char *)shmat(_shmid, nullptr, 0);
        if (_start == (void *)(-1))
        {
            perror("挂载失败");
            exit(3);
        }

        // 创建信号量 (申请1个)
        _semid = CreateSem(1);
        if (_semid < 0)
        {
            perror("信号量创建失败");
            exit(4);
        }

        // 3. 初始化信号量为 0
        // 含义:目前没有资源(数据),消费者必须等
        InitSem(_semid, 0, 0);
    }

    ~Init()
    {
        // 去关联
        shmdt(_start);

        // 删除共享内存
        shmctl(_shmid, IPC_RMID, nullptr);

        // 删除信号量
        DleSem(_semid);
        std::cout << "资源清理完毕...." << std::endl;
    }

public:
    int _shmid;
    int _semid;
    char *_start;
};

int main()
{
    Init init;
    std::cout << "server ready...." << std::endl;

    while (true)
    {
        P(init._semid, 0);

        // 收到信号
        std::cout << "客户端说:" << init._start << std::endl;

        if (strcmp(init._start, "quit") == 0)
        {
            break;
        }
    }

    return 0;
}
client.cc
cpp 复制代码
#include"common.hpp"

int main()
{
    //获取key
    key_t k=GetKey();

    //获取共享内存ID
    int shmid=shmget(k,MEM_SIZE,IPC_CREAT);
    if(shmid<0)
    {
        perror("共享内存获取失败");
        return 1;
    }

    //挂接
    char* start=(char*)shmat(shmid,nullptr,0);
    if(start==(char*)(-1))
    {
        perror("共享内存挂接失败");
        return 2;
    }

    int semid = GetSem();

    //开始写入数据
    while(true)
    {
        std::cout<<">";
        std::string buffer;
        std::getline(std::cin,buffer);

        //直接把数据写到共享内存
        snprintf(start,MEM_SIZE,"%s",buffer.c_str());

        //通知server
        V(semid,0);
        
        if(buffer=="quit")
        {
            break;
        }
    }

    //去关联,但是不需要删除共享内存
    shmdt(start);

    return 0;
}
相关推荐
顾安r6 小时前
12.17 脚本工具 自动化全局跳转
linux·前端·css·golang·html
三小尛6 小时前
linux项目自动化构建工具(make和makefile)
linux
大聪明-PLUS6 小时前
如何修补 Linux 内核:完整指南
linux·嵌入式·arm·smarc
kk5796 小时前
ubuntu20.04运行todesk显示网络连接异常无网络
linux·运维·服务器
阿蒙Amon6 小时前
JavaScript学习笔记:16.模块
javascript·笔记·学习
im_AMBER6 小时前
Leetcode 79 最佳观光组合
笔记·学习·算法·leetcode
郝学胜-神的一滴6 小时前
Linux下创建线程:从入门到实践
linux·服务器·开发语言·c++·程序人生·软件工程
阿恩.7706 小时前
国际水电与电力能源期刊精选
经验分享·笔记·考研·动态规划·能源·制造