【Linux】18. 进程间通信 --- System V IPC(选学)

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

system V 共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核。

换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存示意图

共享内存数据结构

cpp 复制代码
struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
};

共享内存函数

shmget函数

功能:用来创建共享内存

原型
int shmget(key_t key, size_t size, int shmflg);

参数

key:这个共享内存段名字

size:共享内存大小

shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1


深入理解key值:

OS需要对共享内存进行管理,既然要管理就遵循先描述再组织的原则

所以共享内存 = 内存块+共享内存的相关属性

共享内存的相关属性就是上述的struct shmid_ds数据结构进行管理,而key值就存储在shm_perm当中

shmat函数

功能:将共享内存段连接到进程地址空间

原型
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数

shmid: 共享内存标识

shmaddr:指定连接的地址

shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY

返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

说明

shmaddr为NULL,核心自动选择一个地址

shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。

shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -

(shmaddr % SHMLBA)

shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

shmdt函数

功能:将共享内存段与当前进程脱离

原型
int shmdt(const void *shmaddr);

参数

shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

shmctl函数

功能:用于控制共享内存

原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

shmid:由shmget返回的共享内存标识码

cmd:将要采取的动作(有三个可取值)

buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

查看IPC资源命令:


代码实现

通过代码的方式进一步认清共享内存的使用

cpp 复制代码
// comm.hpp 文件
#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#define MAX_SIZE 4096

// 这里的PATHNAME为当前路径
#define PATHNAME "."
// 这里的PROJ_ID为随机值
#define PROJ_ID 0x66

key_t getKey()
{
    // ftok函数创建key值
    // ftok函数根据所提供的路径和值会通过算法确定一个唯一值
    // 当server和client看到同一个key值 也就意味着看到同一份共享内存
    key_t k = ftok(PATHNAME, PROJ_ID);
    if(k == -1)
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

int getShmHelper(key_t k, int flags)
{
    // shmget函数创建共享内存
    int shmid = shmget(k, MAX_SIZE, flags);
    if (shmid < 0)
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int getshm(key_t k)
{
    return getShmHelper(k, IPC_CREAT);
}

int createShm(key_t k)
{
    // 创建新的shm权限为0600 只有自己有读写权限
    return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}

#endif
cpp 复制代码
// shm_server.cc 文件
#include "comm.hpp"

// server端进行创建和删除共享内存

int main()
{
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid:%d\n", shmid);
    return 0;
}
cpp 复制代码
// shm_client.cc文件
#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = getshm(k);
    printf("shmid:%d\n", shmid);
    return 0;
} 
shell 复制代码
[hx@iZ0jl69kyvg0h181cozuf5Z shared_memory]$ ./shm_server 
key:0x66010470
17: File exists

出现 File exists 错误 说明文件已经存在
说明共享内存的生命周期是随OS的,而不是随进程的(不像管道,当没有文件描述符指向管道文件时,会自行关闭)

cpp 复制代码
// comm.hpp文件
void *attachShm(int shmid)
{
    void *mem = shmat(shmid, nullptr, 0);
    // 这里为啥要强转成longlong类型呢?
    // 因为当前OS为64位 指针占8个字节
    if ((long long)mem == -1L)
    {
        // 挂接失败
        std::cerr << "shmat: " << errno << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

void detachShm(void *start)
{
    if (shmdt(start) == -1)
    {
        // 去关联失败
        std::cerr << "shmdt: " << errno << strerror(errno) << std::endl;
    }
}

void delShm(int shmid)
{
    if (shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
        std::cerr << "shmctl: " << errno << strerror(errno) << std::endl;
    }
}
cpp 复制代码
// shm_client.cc
#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = getshm(k);
    printf("shmid:%d\n", shmid);

    sleep(5);

    char *start = (char*)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    sleep(5);

    detachShm(start);

    return 0;
} 
cpp 复制代码
// shm_server.cc
#include "comm.hpp"

// server端进行创建和删除共享内存

int main()
{
    // 创建
    key_t k = getKey();
    printf("key:0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid:%d\n", shmid);
    
    sleep(5);
    
    // 挂接
    // 这里为啥用start命名呢?
    // 因为挂接成功后返回的是进程地址空间的起始地址
    // 进一步加深先描述再组织的概念
    // OS不会直接让用户直接对共享内存进行操作,
    // 要先管理进进程地址空间中 
    char *start = (char*)attachShm(shmid);
    printf("attach success,address start:%p\n",start);

    sleep(5);

    // 去关联
    detachShm(start);

    sleep(10);

    // 删除共享内存
    delShm(shmid);
    return 0;
}

观察上述代码现象,如下所示:

进行最后一步通信:

cpp 复制代码
// shm_server文件
    //通信
    while(true)
    {
        //直接从start中读取数据
        printf("client say:%s\n",start);
        sleep(1);
    }
cpp 复制代码
// shm_client文件
    //通信
    const char* message = "hello server,我是另一个进程,正在与你进行通信";
    pid_t id = getpid();
    int cnt = 1;
    while(true)
    {
        sleep(1);
        // snprintf函数直接往start当中写入数据
        snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,cnt++);
    }

通信成功!!!

共享内存的缺点:
没有进行同步和互斥的操作,没有对数据做任何保护措施!

如果写入的很慢,读取的很快就总是会读取到重复冗余数据

进程独立性的原因:1. 各进程代码和数据独立 2.内核数据结构独立 3. 页表映射独立

system V 消息队列

system V 信号量

什么是信号量?

信号量的本质就是计数器,通常用来表示公共资源当中资源数量的多少

既然是计数器,那么能不能在代码中定义一个全局的count来进行统计呢?

答案:不能 。如果是父子进程,往往会发生写时拷贝,二者看到的就不是同一份资源。(无法看到同一个count)

如果是毫不相干的进程,那么就更加需要提供通信技术来保证看到的是同一份资源

什么是公共资源?

公共资源就是指可以被多个进程同时进行访问的资源 (像:管道/共享内存/消息队列...)

而需要访问公共资源又会引发新的问题

在访问没有保护的公共资源就会出现数据不一致问题 (类似于MySQL事务当中的脏读情况)

(假设现在的场景:输入abcd且abcd只有连在一起才有意义,但是输入一半就被另一个进程读取了)

为什么要让不同的进程看到同一份资源呢?

因为想要实现通信(进程间实现协同),但是进程具有独立性,如何让独立的进程实现协同呢? 让进程看到同一份资源

提出方法->引入了新的问题(数据不一致问题)

临界资源:未来被保护起来的公共资源

进程的大部分资源是独立的,只有少部分资源属于多进程共享,被多进程共享的资源称之为公共资源 而被保护起来的就叫做临界资源

资源(内存,文件,网络等)创建出来是要被使用的。

资源如何被进程使用呢?

一定是该进程有对应的代码来访问这部分临界资源: 临界区 非临界区

总结:
多进程在通信时本质是要看到一份公共的资源 ,这份公共资源未来被保护起来称其为临界资源,
访问临界资源的代码称之为临界区,不访问临界资源的代码称之为非临界区

保护资源的策略分为两种:互斥 && 同步(同步这里不涉及)

互斥: 当两个人同时访问一份资源时,只有当你访问完我才能访问
要么不做,要做就做完 只有两态的这种情况称之为原子性

共享资源的使用方式:1. 作为一个整体使用(管道/共享内存) 2. 划分为一个个的资源子部分使用(信号量)

对于共享资源划分成子部分(可以供不同的进程使用 -- 并发) 而整体使用就是互斥串行(效率低)

所有的进程在访问公共资源之前,都必须先申请sem信号量 (先申请sem信号量的前提是所有进程必须先得看到同一个信号量)

信号量本身作为公共资源,信号量是不是也要保证自己的安全呢?

--,++信号量必须保证自身操作的安全性。
--,++操作是原子(只有两态:成功 or 失败)

申请信号量成功时相当于预定了共享资源当中的某一部分小资源,允许进程进行访问

如果申请信号量不成功,就不允许进程进入共享资源当中,进而达到保护共享资源以及其他进程的目的

通过计数器的方式,对临界资源进行保护 这种计数器我们称之为信号量

申请信号量后,进程可能会访问同一份子资源,需要程序员自己写代码保护资源

接口使用


system V 资源总结:

共享内存,消息队列,信号量,IPC资源 都是先描述再组织

操作系统为了维护IPC资源必须花费大量的数据结构来描述


相关推荐
序属秋秋秋42 分钟前
《C++初阶之内存管理》【内存分布 + operator new/delete + 定位new】
开发语言·c++·笔记·学习
许白掰1 小时前
Linux入门篇学习——Linux 工具之 make 工具和 makefile 文件
linux·运维·服务器·前端·学习·编辑器
ruan1145142 小时前
MySQL4种隔离级别
java·开发语言·mysql
quant_19863 小时前
R语言如何接入实时行情接口
开发语言·经验分享·笔记·python·websocket·金融·r语言
longze_75 小时前
Ubuntu连接不上网络问题(Network is unreachable)
linux·服务器·ubuntu
Dirschs5 小时前
【Ubuntu22.04安装ROS Noetic】
linux·ubuntu·ros
qianshanxue115 小时前
ubuntu 操作记录
linux
百锦再7 小时前
详细解析 .NET 依赖注入的三种生命周期模式
java·开发语言·.net·di·注入·模式·依赖
风吹落叶花飘荡7 小时前
2025 Next.js项目提前编译并在服务器
服务器·开发语言·javascript
AmosTian8 小时前
【系统与工具】Linux——Linux简介、安装、简单使用
linux·运维·服务器