【Linux篇】System V IPC详解:共享内存、消息队列与信号量

📌 个人主页: 孙同学_

🔧 文章专栏: Liunx

💡 关注我,分享经验,助你少走弯路!

文章目录

    • 前言
    • [一. 共享内存示意图](#一. 共享内存示意图)
    • [二. 共享内存接口的使用](#二. 共享内存接口的使用)
      • [2.1 shmget](#2.1 shmget)
      • [2.2 删除共享内存](#2.2 删除共享内存)
      • [2.3 shmat](#2.3 shmat)
      • [2.4 shmdt](#2.4 shmdt)
    • [三. 共享内存核心函数](#三. 共享内存核心函数)
      • [3.1 ftok](#3.1 ftok)
      • [3.2 shmget](#3.2 shmget)
      • [3.3 shmat](#3.3 shmat)
      • [3.4 shmdt](#3.4 shmdt)
      • [3.5 shmctl](#3.5 shmctl)
    • [四. 实例:共享内存实现通信](#四. 实例:共享内存实现通信)
    • [五. system V消息队列](#五. system V消息队列)
    • [六. system V信号量](#六. system V信号量)
      • 铺垫概念
      • 信号量
        • [1. 信号量是什么](#1. 信号量是什么)
        • [2. 理解信号量](#2. 理解信号量)
        • [3. 复盘共享资源的使用问题](#3. 复盘共享资源的使用问题)
        • [4. 熟悉信号量接口和系统调用](#4. 熟悉信号量接口和系统调用)
          • [4.1 semget](#4.1 semget)
          • [4.2 semop](#4.2 semop)
          • [4.2 semctl](#4.2 semctl)
        • [5. 内核是如何组织管理IPC资源的](#5. 内核是如何组织管理IPC资源的)

前言

我们上面所谈的进程间通信都是基于文件的,基于文件进行进程间通信实际上是操作系统复用了自身文件级别的代码,不管是匿名管道还是命名管道或多或少都有一定的问题,所以针对于进程间通信就专门设计出了一套通信模块,这套模块的标准就叫做System V

一. 共享内存示意图

  • 共享内存的原理
    共享内存通过在物理内存中申请空间,并映射到各进程的页表,是虚拟地址对应同一物理地址。这些工作都是由OS来完成的,我们调用系统调用来完成这些工作。
  • 释放共享内存
    取消页表的关联关系,OS释放物理内存
  • 多组进程进行通信
    多组进程同时通信,那么就会有多个共享内存同时存在,而这些共享内存有的是正在使用,有的是新建的,有的是新建的还没有和其他进程进行关联,操作系统内存在多个共享内存操作系统就需要管理这些共享内存。共享内存一定有描述对应的描述共享内存的内核结构体对象。结构体对象+物理内存就构成了共享内存。进程和共享内存的关系就是内核数据结构的关系。

二. 共享内存接口的使用

2.1 shmget

系统调用接口:shmget

成功返回一个合法的共享内存标识符,失败返回-1

  • size_t siz:创建共享内存的大小。
  • int shmflg:它是一个int类型的flg标记位。
    IPC_CREAT:创建共享内存,如果目标共享内存不存在就创建,否则,打开这个共享内存,并返回。
    IPC_EXCL:单独使用无意义,IPC_CREAT | IPC_EXCL才有意义:如果共享内存(shm)不存在就创建,如果已经存在shmget就会出错返回。(所以只要shmget成功返回,共享内存一定是一个全新的共享内存)
  • key_t key:
    我们怎么评估共享内存存在还是不存在呢?
    怎么保证两个不同的进程拿到的就是同一个共享内存呢?
     这两个问题的答案其实是同一个,就是我们的key_t key参数,不同进程进行通信,我们需要一种方法来标识共享内存的唯一性,这个唯一性就由key来区分,但是key不是直接形成的,而是在用户层创建并传给OS的。
    为什么由用户级传入这个key参数呢?
     用户传入key相当于建立了一种"契约",所有需要共享内存的进程必须使用相同的key,才能正确关联到同一内存段。
    key作为全局唯一标识符,确保不同的进程能够通过相同的key访问同一块共享内存区域。用户显示指定key使得多个进程能够基于约定协同定位共享资源。

用户可以通过自定义策略(ftok生成唯一的key),确保不同应用或模块使用不同的key,防止意外覆盖或干扰。

成功了key就被创建,这个key一定是个大于等于0的数字,失败了返回-1

ftok并没有涉及到操作系统内部,是一种纯用户级别的算法。

生成唯一键值:通过文件路径和用户指定的proj_id生成一个key_t类型的键值,用于标识同一块共享资源。

ipcs指的是查看所有系统级的IPC资源,-m指的是查看共享内存。

我们创建了共享内存,进程结束了没有删除共享内存,共享内存资源会一直存在,共享内存的资源的生命周期随内核。即便进程退出了,如果没有显示的删除,即便进程退出了,它的IPC资源依旧被占用。

2.2 删除共享内存

1.ipcrm

ipcrm -m + 共享资源的shmid

为什么不能用key删除呢?

删除,控制共享内存,在用户层我们不能用keykey未来只给内核区分唯一性,需要用shmid来管理共享内存

2.代码删除,接口:shmctl

shmctl接口是对共享内存的管理,其中包含删除共享内存。

返回值:

失败返回-1,成功返回非-1

参数:
int cmd:表示要对共享内存进行什么命令

IPC_STAT:表示获取共享内存的属性

IPC_RMID:表示标识的共性内存的段立马被删除
struct shmid_ds *buf:表示共享内存的相关属性

在用户层,我们将来用共享内存使用的是shmid,在内核层,标识共享内存的唯一性我们用的是key

2.3 shmat

以上操作我们已经开辟出了一块共享内存资源了,可是怎么让两个进程关联这同一块共享内存呢?

我们先来认识一个新的接口:shmat

映射共享内存shmat

该函数将shmid标识的共享内存引入到当前进程的虚拟地址空间

cpp 复制代码
       #include <sys/types.h>
       #include <sys/shm.h>

       void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:共享内存的IPC对象ID
  • shmaddr(虚拟地址,可采用固定位置进行挂接):若为NULL,共享内存会被attach到一个合适的虚拟地址空间,建议使用NULL。若不为NULL,系统会根据参数以及地址边界对齐等分配一个合适的地址(固定地址挂接)
  • shmflg:IPC_RDONLY附加只读权限,不指定的话默认是读写权限。IPC_REMAP替换位于shmaddr处的任意既有映射,共享内存段或内存映射。我们一般使用的时候直接设置为0,表示使用共享内存的默认设置
  • 返回值:映射成功后虚拟地址空间起始的地址,失败返回-1,错误码被设置

2.4 shmdt

解除内存映射shmdt

该函数解除内存映射,将共享内存分离出当前进程的虚拟地址空间

cpp 复制代码
       #include <sys/types.h>
       #include <sys/shm.h>

   
       int shmdt(const void *shmaddr);
  • shmaddr:共享内存地址
    注意: 函数shmdt仅仅是使进程和共享内存脱离关系,将共享内存的引用计数-1,并未删除共享内存。

🍉读写共享内存时并未使用系统调用是为什么?

因为我们一旦把共享内存映射成功了,共享内存是在堆栈之间的,堆栈之间的空间属于用户,也就是共享区属于用户空间,可以让用户直接使用。

对共享内存而言,在堆栈之间的共享内存进程A的认为是它自己的,进程B的也认为是它自己的,只不过他们两个公用同一块物理空间。

对管道而言,这个管道文件本质是内核文件缓冲区,属于操作系统,所以用户向管道里面读和写就必须用系统调用来完成。

🎯总结:
共享内存的优点:

共享内存是进程间通信速度最快的方式。

  • 映射之后读写直接被对方看到。
  • 不需要系统调用获取或者写入内容,直接以指针地址的方式访问。

拓展:进程间通信两个进程能通,如果把一个换成文件,把文件内容直接放入内存中,让进程将内存直接映射到虚拟地址空间,做内存级别的重定向,就可以让进程看到映射的文件。(动态库加载到内存的底层原理,只不过动态库不是采用system V的shm方案,它用的是mmap的方式)

共享内存的缺点:

通信双方没有所谓的"同步机制 "!!!

而我们之前的管道通信,当我们把读端打开,写端没有启动,我们的读端就不读。因为我们的共享内存没有所谓的同步机制,当我们的写端写了一部分并没有写完,我们的数据就有可能被读走了,可能导致我们读到的数据出现理解偏差,这种现象叫做"数据不一致"。一句话总结就是共享内存没有保护机制(对数据的保护)!

那我们若想对我们的共享内存进行保护呢?

  • 方法一:system V版本的信号量
  • 方法二:我们可以通过命名管道,让两个进程建立一条管道,这两个进程建立管道的目的不是为了通信准备的,而是为了同通知准备的。如果进程A写入符合我们的读取要求,那么就用管道唤醒进程B,进程B默认不许读取共享内存,它必须首先读取管道,管道为空,进程B就不做读取。所以我们就可以使用命名管道,实现自己的同步机制,完成对共享内存局部的保护。

在内核中,共享内存在创建的时候,它的大小必须是4kb(4096)的整数倍。如果我们创建的时候写入的大小是4097,那么它实际的大小就是4096*2,也就是向上4kb取整。但是我们能用的也就只有4097。

我们上面讲到的shmat是把共享内存和我们的进程虚拟地址空间进行关联,而接下来要讲的shmdt是把共享内存和进程的虚拟地址空间去关联。

参数是shmat虚拟地址的起始地址。成功时返回0,失败时返回-1,错误码被设置。

共享内存不仅仅是在内核当中为我们开辟一段空间,它还有描述共享内存的结构体对象,我们可以获取或者设置它。

例如:

cpp 复制代码
//获取共享内存的属性
    void Attr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid,IPC_STAT,&ds);//ds:输出型参数,会在操作系统内部,将创建好的共享内存的属性获取出来
        printf("shm_segsz:%ld\n",ds.shm_segsz);
        printf("key:0x%x\n",ds.shm_perm.__key);
    }

三. 共享内存核心函数

3.1 ftok

ftok():生成唯一键值(Key)

  • 作用
    通过文件路径和项目标识符生成唯一的 key_t 键值,用于标识共享内存段。

  • 原型

    c 复制代码
    #include <sys/ipc.h>
    key_t ftok(const char *pathname, int proj_id);
  • 参数

    • pathname:一个已存在的文件路径(如 /home/user/token)。
    • proj_id:自定义整数(通常用 ASCII 字符值,如 'a')。
  • 返回值
    成功返回 key_t 键值,失败返回 -1

示例

c 复制代码
key_t key = ftok("/tmp/myfile", 'A');  // 需确保文件存在
if (key == -1) {
    perror("ftok failed");
    exit(1);
}

3.2 shmget

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

  • 作用
    创建新共享内存段或获取已有段的标识符。

  • 原型

    c 复制代码
    #include <sys/shm.h>
    int shmget(key_t key, size_t size, int shmflg);
  • 参数

    • key:由 ftok() 生成的键值,或 IPC_PRIVATE(创建私有段)。
    • size:共享内存大小(字节)。若获取已有段,可设为 0
    • shmflg:权限标志组合(如 IPC_CREAT | 0666)。
  • 返回值
    成功返回共享内存标识符 shmid,失败返回 -1

示例

c 复制代码
int shmid = shmget(key, 4096, IPC_CREAT | 0666);
if (shmid == -1) {
    perror("shmget failed");
    exit(1);
}

3.3 shmat

shmat():将共享内存附加到进程地址空间

  • 作用
    将共享内存段映射到进程的虚拟地址空间,返回指向共享内存的指针。

  • 原型

    c 复制代码
    void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 参数

    • shmid:共享内存标识符。
    • shmaddr:指定附加地址(通常设为 NULL,由系统自动选择)。
    • shmflg:附加标志(如 SHM_RDONLY 只读访问)。
  • 返回值
    成功返回共享内存指针,失败返回 (void*)-1

示例

c 复制代码
char *shm_ptr = (char*)shmat(shmid, NULL, 0);
if (shm_ptr == (void*)-1) {
    perror("shmat failed");
    exit(1);
}

3.4 shmdt

shmdt():分离共享内存段

  • 作用
    将共享内存段从进程中分离(不删除内存段)。

  • 原型

    c 复制代码
    int shmdt(const void *shmaddr);
  • 参数
    shmaddrshmat() 返回的指针。

  • 返回值
    成功返回 0,失败返回 -1

示例

c 复制代码
if (shmdt(shm_ptr) == -1) {
    perror("shmdt failed");
}

3.5 shmctl

shmctl():控制共享内存段

  • 作用
    获取或修改共享内存段的信息(如删除段)。

  • 原型

    c 复制代码
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 参数

    • shmid:共享内存标识符。
    • cmd:控制命令(常用 IPC_RMID 删除段)。
    • buf:指向 struct shmid_ds 的指针(用于获取信息时传入)。
  • 返回值
    成功返回 0,失败返回 -1

示例(删除共享内存段)

c 复制代码
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
    perror("shmctl failed");
}

四. 实例:共享内存实现通信

Makefile

cpp 复制代码
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f server client 

Comm.hpp

cpp 复制代码
#pragma once
#include <cstdio>
#include <cstdlib>

// '\'的意思是续行
#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

Fifo.hpp

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "Comm.hpp"

#define PATH "."
#define FILENAME "fifo"

class NamedFifo
{
public:
    NamedFifo(const std::string &path, const std::string &name)
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        umask(0);
        // 新建管道
        int n = mkfifo(_fifoname.c_str(), 0666);
        if (n < 0)
        {
            ERR_EXIT("mkfifo");
        }
        else
        {
            std::cout << "mkfifo success" << std::endl;
        }
    }

    ~NamedFifo()
    {
        // 删除管道文件
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            //ERR_EXIT("unlink"); 先调用fifo,进程直接结束导致Shm的析构没被调用
        }
        else
        {
            std::cout << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path; // 将来创建的管道文件的路径
    std::string _name; // 创建的管道文件的名字
    std::string _fifoname;
};

// 文件的操作类
class FileOper
{
public:
    FileOper(const std::string &path, const std::string &name) : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }

    void OpenForRead()
    {
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }
    void OpenForWrite()
    {
        _fd = open(_fifoname.c_str(), O_WRONLY); // 以写的方式打开管道
        if (_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }
    void Wakeup()
    {
        // 写入操作
        char c = 'c';
        int n = write(_fd,&c,1);
        printf("尝试唤醒:%d\n",n);
    }
    bool Wait()
    {
        char c;//规定单次读只能读一个字符
        int number = read(_fd, &c, 1); // 从fd中读,读到buffer里,期望读取sizeof(buffer)-1个
        if(number > 0) 
        {
            printf("唤醒成功:%d\n",number);
            return true;
        }

        return false;
    }
    void Close()
    {
        if (_fd > 0)
            close(_fd);
    }

    ~FileOper()
    {
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
    int _fd;
};

Shm.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "Comm.hpp"

const int gdefaultid = -1;
const int gsize = 4096;
const std::string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;

#define CREATE "create"
#define USER "user"

class Shm
{
private:
    // 创建一个全新的共享内存
    void CreateHelper(int flg)
    {
        key_t k = ftok(pathname.c_str(), projid);
        if (k < 0) // 构建失败
        {
            ERR_EXIT("ftok\n");
        }
        printf("key: 0x%x\n", _key);
        // 共享内存的生命周期随内核
        _shmid = shmget(k, _size, flg);
        if (_shmid < 0)
        {
            ERR_EXIT("shmget\n");
        }
        printf("shmid: %d\n", _shmid);
    }
    // 创建共享内存
    void Create()
    {
        CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
    }
    // 挂接
    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if ((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat\n");
        }
        printf("attach success!\n");
    }
    // 去关联
    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("detach success!\n");
        }
    }
    // 获取共享内存
    void Get()
    {
        CreateHelper(IPC_CREAT);
    }
    // 删除共享内存
    void Destroy()
    {
        // if (_shmid == gdefaultid)
        //     return; // 说明共享内存没有成功被建立
        Detach();
        if (_usertype == CREATE)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if (n > 0)
            {
                // 删除成功
                printf("shmclt delete shm: %d success!\n", _shmid);
            }
            else
            {
                // 删除失败
                ERR_EXIT("shmctl\n");
            }
        }
    }

public:
    Shm(const std::string &pathname, int projid, const std::string &usertype)
        : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0) // 构建失败
        {
            ERR_EXIT("ftok");
        }
        if (_usertype == CREATE)
            Create();
        else if (_usertype == USER)
            Get();
        else
        {
        }
        Attach();
    }
    void *VirtualAddr()
    {
        printf("VirtualAddr: %p\n", _start_mem);
        return _start_mem;
    }
    int Size()
    {
        return _size;
    }
    //获取共享内存的属性
    void Attr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid,IPC_STAT,&ds);//ds:输出型参数,会在操作系统内部,将创建好的共享内存的属性获取出来
        printf("shm_segsz:%ld\n",ds.shm_segsz);
        printf("key:0x%x\n",ds.shm_perm.__key);
    }

    ~Shm()
    {
        // 只有创建者才需要析构
        // if(_usertype == CREATE)
        std::cout << _usertype << std::endl;
        Destroy();
    }

private:
    int _shmid;
    key_t _key;
    int _size;
    void *_start_mem;
    std::string _usertype;
};

server.cc

cpp 复制代码
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    Shm shm(pathname,projid,CREATE);//让server端先创建共享内存,防止client端先创建
    //创建命名管道
    NamedFifo fifo(PATH,FILENAME);
    //文件操作
    FileOper readerfile(PATH,FILENAME);
    readerfile.OpenForRead();

    char *mem = (char*)shm.VirtualAddr();
    //让server把共享内存当成一个大字符串来用
    while(true)
    {
        if(readerfile.Wait())//默认它在这里会阻塞
        {
            printf("%s\n",mem); //访问虚拟地址就如同访问自己申请的堆空间一样
        }
        else
            break;
    }
    //我们写共享内存没有使用系统调用
  
    readerfile.Close();

    return 0;
}

client.cc

cpp 复制代码
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    FileOper writefile(PATH,FILENAME);
    writefile.OpenForWrite();

    Shm shm(pathname,projid,USER);
    char *mem = (char*)shm.VirtualAddr();
    //让client每隔一秒钟向共享内存里写入A B C D
    int index = 0;
    for(char c = 'A';c <= 'Z'; c++,index+=2)
    {
        //循环体内部才是向共享内存中写
        sleep(1);
        mem[index] = c;
        mem[index + 1] = c;
        sleep(1);
        mem[index + 2] = 0; //字符串必须是以\0结尾的,保证不要让后续乱码。

        writefile.Wakeup();
    }
    //我们写共享内存没有使用系统调用
    writefile.Close();
    return 0;
}

五. system V消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。
  • OS要对消息队列进行管理,现描述,在组织。
  • 每个数据块都被认为是有一个类型,接收者进程接受的数据块可以有不同的类型值。(要有类型主要是在同一个队列内部区分哪一个是我要的,那个数据是我发的)

消息队列的调用接口:

  1. 创建/获取消息队列
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgget(key_t key, int msgflg);
  • 参数
    • key: 队列键值,通常使用ftok()生成
    • msgflg: 权限标志(如 IPC_CREAT)
    • 返回消息队列标识符,失败返回-1
  1. 控制操作
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 参数
    • cmd: 控制命令(如 IPC_RMID 删除队列)
  1. 发送消息
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • 参数
    • msqid: 消息队列标识符
    • msgp: 指向消息结构的指针
    • msgsz: 消息大小(不包括消息类型)
    • msgflg: 标志(如 IPC_NOWAIT)
  1. 接收消息
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • 参数

    • msgtyp: 指定接收的消息类型
    • 其他参数与msgsnd类似

使用示例

c 复制代码
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

struct msg_buffer {
    long msg_type;
    char msg_text[100];
};

int main() {
    key_t key = ftok("progfile", 65);
    int msgid = msgget(key, 0666 | IPC_CREAT);
    
    struct msg_buffer message;
    message.msg_type = 1;
    sprintf(message.msg_text, "Hello Message Queue");
    
    msgsnd(msgid, &message, sizeof(message), 0);
    
    msgrcv(msgid, &message, sizeof(message), 1, 0);
    printf("Received: %s\n", message.msg_text);
    
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

消息队列的生命周期也随内核


六. system V信号量

信号量主要用于同步和互斥的

铺垫概念

  • 多个执行流(进程),能看到同一份公共资源:共享资源
  • 被保护起来的共享资源叫做临界资源
  • 在进程中涉及到互斥资源的程序段叫临界区(访问资源对应的代码)

有临界区,也就存在非临界区,非临界区就是在我们众多的代码中,只有少部分代码会访问公共资源,大部分代码并没有访问公共资源,所以与数据不一致问题无关,执行这部分代码时不会出错,这部分称之为非临界区。在我们未来并发编程时,整个代码就可以分为上图几个部分。

保护临界区的代码就是在变相的保护临界资源。

怎么保护临界区呢?

  • 任何时刻,只允许一个执行流访问资源,叫做互斥
  • 多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步
  • 所谓对公共资源进行保护,本质上是对访问公共资源的代进行保护。

原子性:

指操作作为不可分割的单元执行,确保其要么完全成功,要么完全失败,不会处于中间状态。

锁本身也要被共享,谁来保护锁的安全呢?

申请锁的时候,必须是原子性的。

信号量

1. 信号量是什么

信号量也被叫做信号灯,本质是一个计数器,用来表示临界资源中,临界资源的数量多少。

2. 理解信号量

举个例子,我们肯定都看过电影,电影院的一个放映厅就是一个临界资源,此时就最怕两个问题:

1.票卖多了,座位不够了。

  1. 票号重复了

我们在看电影前首先需要买票,买票买到了,才有对应的座位,即便是我们买到票了,但是我们不去看电影,这个座位都必须给我们留着,所以买票的本质是预定机制!,所以想访问资源,先得买票。

信号量: 假如上面的放映厅是共享内存,通信的若干个进程之间,他们把共享内存划分成了一块一块的,进行分块使用,这样当一个进程访问一个数据块,另一个进程也想访问另一个数据块,只要两个进程访问的不是同一个块,它也可进入到我们临界资源中访问另一个块,这样我们就可以做到在临界资源中放两个进程都进去,保证了读写数据时的效率问题。

1.不要访问共享内存中的同一个位置

2.不要放过多的进程进来

此时就可以实现让多个进程并发的访问共享资源。

所以信号量的本质是一个计数器,该计数器描述的是临界资源中,资源数量的多少。

所有进程,要访问临界资源中的一小块,就必须先申请信号量,对信号量进行--操作。

进程访问资源时,先申请信号量,本质是对资源的预定机制!

预定成功了,资源就给你了,等你随时访问,没人和你抢。

细节1: 信号量本身就是共享资源!申请信号量本身就是对信号量计数器原子性做--操作(P操作)

当我们不用了就对信号量进行原子性++操作(V操作),所以信号量的本质是计数器,通过PV操作对资源进行预定机制。
细节2: 如果有一个超级VIP厅,一次只允许一个进程进来。这种超级放映厅的人在看电影时不喜欢被人打扰该怎么办呢?它只需将信号量的值设为1,资源数量只有1个,此时把信号量只有1或者0的两态的信号量,叫做二元信号量! 这就是互斥!

总结:信号量

  • 特性方面
    IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核
  • 理解方面
    信号量是⼀个计数器
  • 作用方面
    保护临界区
  • 本质方面
    信号量本质是对资源的预订机制
  • 操作方面
    申请资源,计数器--,P操作
    释放资源,计数器++,V操作
3. 复盘共享资源的使用问题

资源的整体使用叫做二元信号量 ,资源不是整体使用叫做多元信号量

信号量和通信有什么关系?

1.先访问信号量P,每个进程先得看到同一个信号量。

2.不是传递数据,才是通信IPC,同步,互斥也算。

4. 熟悉信号量接口和系统调用
4.1 semget
  • 作用
    创建/获取信号量集
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semget(key_t key, int nsems, int semflg);
  • 参数
  • key:由 ftok() 生成的键值,或 IPC_PRIVATE(创建私有段)。
  • nsems:信号量集中信号量的数量。
  • semflg:标志位,如IPC_CREAT | 0666创建新信号量。
  • 返回值
    成功返回信号量集ID,失败返回-1

ipcs -s查看信号量

4.2 semop
  • 作用
    对信号量进行原子操作,即P/V操作
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semop(int semid, struct sembuf *sops, size_t nsops);

sembuf:sembuf是一个结构体,第一个成员表示对哪个信号量进行操作,第二个参数表示对信号量进行什么操作(-1表示对该信号量进行P操作,+1表示对该信号量进行V操作)第三个参数sem_flg我们设置为0就可以了。

我们将来如果有10个信号量,sembuf数组就会有10个元素,然后用循环把每个信号量的要进行什么操作一写,如此我们就可以批量化的对信号量进行操作。

  • 参数
    • semid:信号量标识符。
    • sops:指向操作数组的指针
    • nsops:数组元素的个数
  • 返回值
    成功返回 0,失败返回 -1
4.2 semctl
  • 作用
    控制信号量集,如初始化,删除,或查询状态
  • 原型
cpp 复制代码
       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semctl(int semid, int semnum, int cmd, ...);
  • 参数
  • cmd:命令参数
    IPC_RMID:删除信号量集
    SETVAL:设置单个信号量的值(需要联合体semun)
    联合体semun需要自定义(某些系统需要手动定义)

    OS内部存在大量的信号量集合,所以就需要对信号量进行管理。
    共享内存,消息队列,信号量同一都用key来作为标识符。所以就要防止它们之间的key值的冲突。OS中,共享内存,消息队列,信号量当作了同一种资源。所以它们叫做system V IPC

shmid,msgid,semid都是数组下标。

5. 内核是如何组织管理IPC资源的


IPC在操作系统层面会存在一个全局的数据结构叫做ipc_ids,它里面有个指针叫做entries,它是一个指针,会指向一个叫ipc_id_ary的柔性数组。这个指针扩容后里面的类型是kern_ipc_perm *
msg_queue是内核层面消息队列的结构,它里面包含了各种重要的属性。sem_array是一个信号量集,里面有一个base指针指向sem,这个里面是信号量计数器。信号量里面还包含了一个sem_queue,当我们的一个进程申请信号量申请失败了,我们可以把我们的进程投递到sem_queue当中,让我们的进程休眠。shmid_kerne这个是共享内存,共享内存里面有一个指针,这个指针shm_file它是一个struct_file的结构体,最终会找到内存所对应的页面,它也是基于文件实现的内存共享。

上面的kern_ipc_perm就相当于是基类,消息队列msg_queue,信号量sem_array,共享内存shmid_kernel相当于子类。未来消息队列,信号量,共享内存的地址全都赋给柔性数组的某个元素,这样的化整个系统将来就可以拿指针找到对应的资源了,访问权限相关的资源可以直接访问,访问其他资源的化可以把指针强转为特定的类型。

那怎么知道对应的是什么类型呢?

对应的结构里面是有类型的,消息队列,共享内存,信号量用的是不一样的系统调用接口,在操作系统当中,当我们使用柔性数组的下标时它对应的是什么资源,在系统接口层面上是能区分出来的。


👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔
相关推荐
铭哥的编程日记2 小时前
【Linux网络】传输层协议TCP
linux·网络·tcp/ip
它说我会飞耶2 小时前
开机视频动画
linux
大聪明-PLUS2 小时前
Linux 上的 GitOps:使用 Git 进行无缝基础设施管理
linux·嵌入式·arm·smarc
LCG元3 小时前
Linux 环境变量 PATH 详解:为什么你装的命令"找不到"?
linux
web安全工具库3 小时前
Linux进程的:深入理解子进程回收与僵尸进程
java·linux·数据库
赖small强3 小时前
Linux 用户态与内核态及其切换机制
linux·内核态·用户态(user mode)·硬件中断与异常·调度与抢占
虚伪的空想家3 小时前
记录次etcd故障,fatal error: bus error
服务器·数据库·k8s·etcd
偶像你挑的噻3 小时前
Linux应用开发-17-套接字
linux·网络·stm32·嵌入式硬件
鸢尾掠地平4 小时前
DNS的正向、反向解析的服务配置知识点及实验
运维·服务器·网络