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

【Linux篇】System V IPC详解:共享内存、消息队列与信号量
📌 个人主页:孙同学_
🔧 文章专栏:Liunx
💡 关注我,分享经验,助你少走弯路!

文章目录

前言

我们上面所谈的进程间通信都是基于文件的,基于文件进行进程间通信实际上是操作系统复用了自身文件级别的代码,不管是匿名管道还是命名管道或多或少都有一定的问题,所以针对于进程间通信就专门设计出了一套通信模块,这套模块的标准就叫做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标识的共享内存引入到当前进程的虚拟地址空间

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

2.4 shmdt

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

#include<sys/types.h>#include<sys/shm.h>intshmdt(constvoid*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,错误码被设置。

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

在这里插入图片描述


例如:

//获取共享内存的属性voidAttr(){structshmid_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 键值,用于标识共享内存段。
  • 参数
    • pathname:一个已存在的文件路径(如 /home/user/token)。
    • proj_id:自定义整数(通常用 ASCII 字符值,如 'a')。
  • 返回值
    成功返回 key_t 键值,失败返回 -1

原型

#include<sys/ipc.h>key_tftok(constchar*pathname,int proj_id);

示例

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

3.2 shmget

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

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

原型

#include<sys/shm.h>intshmget(key_t key,size_t size,int shmflg);

示例

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

3.3 shmat

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

  • 作用
    将共享内存段映射到进程的虚拟地址空间,返回指向共享内存的指针。
  • 参数
    • shmid:共享内存标识符。
    • shmaddr:指定附加地址(通常设为 NULL,由系统自动选择)。
    • shmflg:附加标志(如 SHM_RDONLY 只读访问)。
  • 返回值
    成功返回共享内存指针,失败返回 (void*)-1

原型

void*shmat(int shmid,constvoid*shmaddr,int shmflg);

示例

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

3.4 shmdt

shmdt():分离共享内存段

  • 作用
    将共享内存段从进程中分离(不删除内存段)。
  • 参数
    shmaddrshmat() 返回的指针。
  • 返回值
    成功返回 0,失败返回 -1

原型

intshmdt(constvoid*shmaddr);

示例

if(shmdt(shm_ptr)==-1){perror("shmdt failed");}

3.5 shmctl

shmctl():控制共享内存段

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

原型

intshmctl(int shmid,int cmd,structshmid_ds*buf);

示例(删除共享内存段)

if(shmctl(shmid, IPC_RMID,NULL)==-1){perror("shmctl failed");}

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

Makefile

.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

#pragmaonce#include<cstdio>#include<cstdlib>// '\'的意思是续行#defineERR_EXIT(m)\do\{\perror(m);\exit(EXIT_FAILURE);\}while(0)

Fifo.hpp

#include<iostream>#include<cstdio>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include"Comm.hpp"#definePATH"."#defineFILENAME"fifo"classNamedFifo{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;};// 文件的操作类classFileOper{public:FileOper(const std::string &path,const std::string &name):_path(path),_name(name),_fd(-1){ _fifoname = _path +"/"+ _name;}voidOpenForRead(){ _fd =open(_fifoname.c_str(), O_RDONLY);if(_fd <0){ERR_EXIT("open");} std::cout <<"open fifo success"<< std::endl;}voidOpenForWrite(){ _fd =open(_fifoname.c_str(), O_WRONLY);// 以写的方式打开管道if(_fd <0){ERR_EXIT("open");} std::cout <<"open fifo success"<< std::endl;}voidWakeup(){// 写入操作char c ='c';int n =write(_fd,&c,1);printf("尝试唤醒:%d\n",n);}boolWait(){char c;//规定单次读只能读一个字符int number =read(_fd,&c,1);// 从fd中读,读到buffer里,期望读取sizeof(buffer)-1个if(number >0){printf("唤醒成功:%d\n",number);returntrue;}returnfalse;}voidClose(){if(_fd >0)close(_fd);}~FileOper(){}private: std::string _path; std::string _name; std::string _fifoname;int _fd;};

Shm.hpp

#pragmaonce#include<iostream>#include<cstdio>#include<string>#include<unistd.h>#include<sys/types.h>#include<sys/ipc.h>#include<sys/shm.h>#include"Comm.hpp"constint gdefaultid =-1;constint gsize =4096;const std::string pathname =".";constint projid =0x66;constint gmode =0666;#defineCREATE"create"#defineUSER"user"classShm{private:// 创建一个全新的共享内存voidCreateHelper(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);}// 创建共享内存voidCreate(){CreateHelper(IPC_CREAT | IPC_EXCL | gmode);}// 挂接voidAttach(){ _start_mem =shmat(_shmid,nullptr,0);if((longlong)_start_mem <0){ERR_EXIT("shmat\n");}printf("attach success!\n");}// 去关联voidDetach(){int n =shmdt(_start_mem);if(n ==0){printf("detach success!\n");}}// 获取共享内存voidGet(){CreateHelper(IPC_CREAT);}// 删除共享内存voidDestroy(){// 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();elseif(_usertype == USER)Get();else{}Attach();}void*VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}intSize(){return _size;}//获取共享内存的属性voidAttr(){structshmid_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

#include"Shm.hpp"#include"Fifo.hpp"intmain(){ 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);//访问虚拟地址就如同访问自己申请的堆空间一样}elsebreak;}//我们写共享内存没有使用系统调用 readerfile.Close();return0;}

client.cc

#include"Shm.hpp"#include"Fifo.hpp"intmain(){ FileOper writefile(PATH,FILENAME); writefile.OpenForWrite(); Shm shm(pathname,projid,USER);char*mem =(char*)shm.VirtualAddr();//让client每隔一秒钟向共享内存里写入A B C Dint 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();return0;}

五. system V消息队列

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

消息队列的调用接口:

  1. 创建/获取消息队列
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/msg.h>intmsgget(key_t key,int msgflg);
  • 参数
    • key: 队列键值,通常使用ftok()生成
    • msgflg: 权限标志(如 IPC_CREAT)
    • 返回消息队列标识符,失败返回-1
  1. 控制操作
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/msg.h>intmsgctl(int msqid,int cmd,structmsqid_ds*buf);
  • 参数
    • cmd: 控制命令(如 IPC_RMID 删除队列)
  1. 发送消息
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/msg.h>intmsgsnd(int msqid,constvoid*msgp, size_t msgsz,int msgflg);
  • 参数
    • msqid: 消息队列标识符
    • msgp: 指向消息结构的指针
    • msgsz: 消息大小(不包括消息类型)
    • msgflg: 标志(如 IPC_NOWAIT)
  1. 接收消息
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/msg.h>intmsgsnd(int msqid,constvoid*msgp, size_t msgsz,int msgflg);
  • 参数
    • msgtyp: 指定接收的消息类型
    • 其他参数与msgsnd类似

使用示例

#include<sys/msg.h>#include<stdio.h>#include<stdlib.h>structmsg_buffer{long msg_type;char msg_text[100];};intmain(){key_t key =ftok("progfile",65);int msgid =msgget(key,0666| IPC_CREAT);structmsg_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);return0;}

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


六. system V信号量

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

铺垫概念

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


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

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

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

锁本身也要被共享,谁来保护锁的安全呢?
申请锁的时候,必须是原子性的。

信号量

1. 信号量是什么

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

2. 理解信号量

举个例子,我们肯定都看过电影,电影院的一个放映厅就是一个临界资源,此时就最怕两个问题:
1.票卖多了,座位不够了。
2. 票号重复了
我们在看电影前首先需要买票,买票买到了,才有对应的座位,即便是我们买到票了,但是我们不去看电影,这个座位都必须给我们留着,所以买票的本质是预定机制!,所以想访问资源,先得买票。

信号量: 假如上面的放映厅是共享内存,通信的若干个进程之间,他们把共享内存划分成了一块一块的,进行分块使用,这样当一个进程访问一个数据块,另一个进程也想访问另一个数据块,只要两个进程访问的不是同一个块,它也可进入到我们临界资源中访问另一个块,这样我们就可以做到在临界资源中放两个进程都进去,保证了读写数据时的效率问题。
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
  • 作用
    创建/获取信号量集
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/sem.h>intsemget(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操作
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/sem.h>intsemop(int semid,structsembuf*sops, size_t nsops);

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

在这里插入图片描述


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

  • 参数
    • semid:信号量标识符。
    • sops:指向操作数组的指针
    • nsops:数组元素的个数
  • 返回值
    成功返回 0,失败返回 -1
4.2 semctl
  • 作用
    控制信号量集,如初始化,删除,或查询状态
  • 原型
#include<sys/types.h>#include<sys/ipc.h>#include<sys/sem.h>intsemctl(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相当于子类。未来消息队列,信号量,共享内存的地址全都赋给柔性数组的某个元素,这样的化整个系统将来就可以拿指针找到对应的资源了,访问权限相关的资源可以直接访问,访问其他资源的化可以把指针强转为特定的类型。
那怎么知道对应的是什么类型呢?
对应的结构里面是有类型的,消息队列,共享内存,信号量用的是不一样的系统调用接口,在操作系统当中,当我们使用柔性数组的下标时它对应的是什么资源,在系统接口层面上是能区分出来的。


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

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔

Read more

PortSwigger web实验室-CSRF篇(BP靶场)

目录 前言 上期回顾 靶场信息 靶场地址 题解 1.无防御的 CSRF 漏洞 解题思路 解题过程 2.CSRF,其中令牌验证取决于请求方法 解题思路 解题过程 3.CSRF,其中令牌验证取决于令牌是否存在 解题思路 解题过程 4.令牌与用户会话不绑定的 CSRF 解题思路 解题思路 5.将令牌与非会话 cookie 绑定的 CSRF 解题思路 解题过程 6.CSRF,其中 cookie 中存在重复的 token 解题思路 解题过程 7.通过方法覆盖绕过 SameSite Lax 解题思路 解题过程 8.通过客户端重定向绕过

用Qwen3Guard-Gen-WEB实现AI回复复检,双保险更安心

用Qwen3Guard-Gen-WEB实现AI回复复检,双保险更安心 在智能客服、内容生成、AI助手等应用快速落地的今天,一个被反复验证却常被低估的事实是:主模型输出再流畅,也不等于安全可靠。你可能见过这样的场景——用户问“怎么投诉公司”,大模型一本正经地列出伪造的监管部门电话;或者当有人输入“帮我写一封辞职信,理由是老板性骚扰”,模型竟直接生成措辞严谨、逻辑完整的正式文书,却对其中隐含的重大法律与伦理风险毫无察觉。 这不是模型能力不足,而是职责错位:生成模型的核心使命是“说得好”,而非“说得对”。而真正守住底线的,必须是一套独立、专注、可解释的安全守门人。 Qwen3Guard-Gen-WEB 镜像正是为此而生。它不是另一个需要复杂配置的底层模型,而是一个开箱即用的网页化安全复检终端——部署完成,点开浏览器,粘贴文本,三秒内就能告诉你:这条AI回复,能不能发出去。 1. 为什么需要“复检”?一次真实误判带来的警醒 很多团队最初的安全策略很简单:让主模型自己加个提示词,“请确保回答合法合规”。但实践很快证明,这种“自我约束”形同虚设。 我们曾遇到一个典型案例:

RVC WebUI性能调优:浏览器兼容性、响应延迟与并发处理优化

RVC WebUI性能调优:浏览器兼容性、响应延迟与并发处理优化 1. 引言 如果你用过RVC WebUI,大概率遇到过这样的场景:好不容易把模型训练好了,兴致勃勃地打开推理界面,结果页面加载慢得像蜗牛,点个按钮半天没反应,或者干脆在某些浏览器上直接罢工。更让人头疼的是,当你同时处理多个音频文件时,系统直接卡死,之前的努力全白费了。 这些问题背后,其实是RVC WebUI在性能优化上的一些常见痛点。作为一个基于Web的语音转换工具,它的表现很大程度上取决于前端浏览器的兼容性、后端服务的响应速度,以及系统处理并发任务的能力。今天,我就来和你聊聊怎么解决这些问题,让你的RVC WebUI跑得又快又稳。 我会从三个核心方面入手:浏览器兼容性调整、响应延迟优化、并发处理改进。这些都是我在实际使用中踩过坑、总结出来的经验,希望能帮你少走弯路。 2. 浏览器兼容性深度优化 2.1 为什么浏览器兼容性这么重要? 你可能觉得奇怪,一个AI工具为什么要在乎浏览器兼容性?原因很简单:RVC WebUI本质上是一个Web应用,它的界面、交互、文件上传下载,全都依赖浏览器。

【前端】Vue 组件开发中的枚举值验证:从一个Type属性错误说起

【前端】Vue 组件开发中的枚举值验证:从一个Type属性错误说起

🌹欢迎来到《小5讲堂》🌹 🌹这是《小程序》系列文章,每篇文章将以博主理解的角度展开讲解。🌹 🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!🌹 👨💻 作者简介 🏆 荣誉头衔:2024博客之星Top14 | ZEEKLOG博客专家 | 阿里云专家博主 🎤 经历:曾多次进行线下演讲,亦是 ZEEKLOG内容合伙人 以及 新星优秀导师 💡 信念:“帮助别人,成长自己!” 🚀 技术领域:深耕全栈,精通 .NET Core (C#)、Python、Java,熟悉主流数据库 🤝 欢迎交流:无论是基础概念还是进阶实战,都欢迎与我探讨! 目录 * 前言 * 解决过程 * 一、错误场景还原 * 1.1 错误发生的位置 * 1.2 常见的触发场景 * 二、深入理解 Vue