Linux-进程间通信2

**摘要:**本文系统梳理了 Linux 进程间通信的核心机制。首先从命名管道的原理入手,说明其本质是让毫无血缘关系的进程通过一个不落盘的"特殊文件"看到同一份资源,并分别从命令级(mkfifo、echo、cat、unlink)和代码级(mkfifo、open、read、write 系统调用及 NamedPipe 类封装)两个层面演示了完整用法。随后深入讲解共享内存:从虚拟地址空间共享区映射的原理,到 shmget、ftok、shmctl、shmat、shmdt 等系统调用的使用细节,涵盖键值唯一性、权限设置、4KB 对齐、生命周期随内核等关键要点,并对比了共享内存"速度最快但缺乏同步互斥"的特征。最后简述了 System V 的消息队列与信号量,引出并发编程中临界资源、临界区、互斥与同步等概念,并从内核角度说明 IPC 资源以数组方式统一组织管理。全文配有大量代码示例与运行截图,适合作为 Linux 进程间通信的学习与实践参考。

目录

1.命名管道

1.1命名管道的原理

1.2命名管道的操作

1.2.1命令级

1.2.2代码级

1.3命名管道的实践

1.4命名管道的总结

2.共享内存

2.1共享内存的原理

2.2共享内存的操作

2.2.1shmget系统调用

2.2.2创建共享内存

2.2.3shmctl系统调用

2.2.4删除共享内存

2.2.5shmat/shmdt系统调用

2.2.5.1shmat系统调用及使用

2.2.5.2共享内存权限问题

2.2.5.3shmdt系统调用及使用

2.2.6设置共享内存大小

2.3共享内存的实践

2.4共享内存的通信

2.5共享内存的属性

2.6共享内存的特征

2.7共享内存的代码汇总

[3.System V通信方式(简述)](#3.System V通信方式(简述))

3.1消息队列

3.1.1概念

3.1.2消息队列相关系统调用

3.2并发编程

3.3信号量

3.3.1概念

3.3.2相关系统调用

4.内核组织管理IPC资源


1.命名管道

1.1命名管道的原理

多个进程能同时打开同一个文件吗?

肯定是可以的,比如:显示器文件是被默认打开的,所以肯定可以。那么多个进程打开同一个文件,因为文件加载时会加载文件的属性和内容(struct file),那么其他进程打开这个文件还需要从磁盘加载struct file吗?

因为对于内存来讲,属性和文件缓冲区里的内容是共享的,只要有一份就行,操作系统虽然允许多个进程打开同一个文件,但是操作系统不会重复加载文件数据和内容!

因为进程间通信的本质是让不同的进程看到同一份资源,此时的多个进程已经看到同一份资源了。但是这个文件还是磁盘文件,还会在磁盘上进行刷新,因此,就需要设计一个特殊文件,这种文件在磁盘上就是一个常规文件,只不过这个常规文件不需要再刷新(到磁盘里)了,此时多个进程的文件缓冲区就可以通信了。但是因为struct file只有一个读写位置,因此要设计一些针对这个特殊文件的读写方法,即一旦打开这个文件给他单独创建一个struct file对象,这种管道叫做命名管道!

所以这个命名管道和匿名管道的原理是一样的!

1.2命名管道的操作

1.2.1命令级

创建命名管道命令:mkfifo:

我们知道FIFO是First In First Out,因此管道的本质就是队列!

我们创建管道和创建文件以及创建目录的操作是一样的,如果想要创建一个文件名为fifo的文件可以:

bash 复制代码
mkfifo fifo

我们知道echo可以把一个字符串输出到显示器上:

如果我们输出重定向到fifo文件,会出现这样的情况:

相当于我们这个进程是写端,需要其他进程作为读端来读,新开一台机器,然后用cat做输入重定向有:

当我们回车的一瞬间,对应的hello字符串内容传到了另一个进程里,这就是进程间通信!

当我们继续刚才的操作,但是在另一台机器上并不读有:

我们发现,只要我们不用另一个进程来读取它,这个管道文件的大小始终为0!这代表这个hello字符串没有刷新到磁盘上,而是在内存里!

如果我们想要删除管道文件,除了可以用rm命令,也可以用unlink都行:

1.2.2代码级

因为命名管道需要两个进程,因此我们需要创建两个文件,这两个进程也可以毫无关系(cat和echo),因此我们需要创建client.cpp作为客户端,创建server.cpp作为发送端,并创建一个Makefile来方便操作!

Makefile里写:

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

在client.cpp和server.cpp里都写:

cpp 复制代码
#include<iostream>

int main()
{
    return 0;
}

但是我们编译发现并不会同时形成两个可执行文件:

这是因为Makefile是从上往下执行的,只会执行第一条指令!

如果我们想要同时形成两个可执行文件可以把Makefile改为:

bash 复制代码
.PHONY:all
all:server client

client:client.cpp
	g++ -o $@ $^ -std=c++11
server:server.cpp
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client
	rm -f server

因为Makefile遇到的第一条指令是all,所以它就会发现server和client都不存在,因此执行后续指令:

为了让二者用命名管道通信,因此就需要有一个文件用代码创建出一个管道文件才行,让server进程来创建!

这是mkfifo的系统调用(man 3 mkfifo)。

为了让二者看到同一份资源,我们需要创建一个common.hpp:

cpp 复制代码
#pragma
#include<iostream>
#include<sys/stat.h>
#include<sys/types.h>
#include<string>

//管道名字
const std::string fifoname = "fifo";
//mode_t:无符号整数(mkfifo系统调用要用)
mode_t mode = 0666;

我们在server.cpp里进行创建管道的操作:

cpp 复制代码
#include"common.hpp"

int main()
{
    int n = mkfifo(fifoname.c_str(),mode);
    if(n == 0)
    {
        //创建管道成功
    }
    else
    {
        //创建管道失败
    }
    return 0;
}

然后我们运行server.cpp就能创建成功:

其中我们传递的mode为0666,这是权限对应的八进制,0666对应000110110110,而因为有权限掩码的存在,实际上创建出来的fifo权限是0664(不了解的去看之前的博客:Linux-文件),

如果我们再次运行server进程,实际上会失败,这里不做过多演示,只需要在原来代码的else语句加个打印就行,因此,创建管道文件可能会失败!

当然,我们删除管道文件的指令unlink也可以作为系统调用:

它需要传递一个文件名(可带路径,不带的话默认在当前路径下删除),我们添加完头文件然后把server.cpp代码改为:

cpp 复制代码
#include"common.hpp"

int main()
{
    int n = mkfifo(fifoname.c_str(),mode);
    if(n == 0)
    {
        //创建管道成功
    }
    else
    {
        //创建管道失败
    }
    //把管道文件删除
    int m = unlink(fifoname.c_str());
    //防止报错(有些编译器对于不使用的变量可能会报错)
    (void)m;
    return 0;
}

此时就能够对管道文件的生命周期做管理了,具体就不做结果展示了!

至此,我们所有操作就完成了,后面我们想要通信,只需要把管道文件以文件形式打开和读写就能进行通信了,这是文件相关的操作了!

如:

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

//管道名字
const std::string fifoname = "fifo";
//mode_t:无符号整数(mkfifo系统调用要用)
mode_t mode = 0666;
//缓冲区大小
int size = 128;
cpp 复制代码
//client.cpp
#include"common.hpp"

int main()
{
    //直接以写的方式打开文件
    int fd = open(fifoname.c_str(),O_WRONLY);
    if(fd < 0)
    {
        //打开文件失败
        exit(2);
    }
    //发送的信息
    std::string message;
    while(true)
    {
        //从键盘读取,回车就结束输入
        std::getline(std::cin,message);
        write(fd,message.c_str(),message.size());
    }
    close(fd);
    return 0;
}
cpp 复制代码
//server.cpp
#include"common.hpp"

int main()
{
    //1.创建管道文件
    int n = mkfifo(fifoname.c_str(),mode);
    if(n == 0)
    {
        //创建管道成功
    }
    else
    {
        //创建管道失败
        exit(1);
    }
    //2.打开文件,和常规文件一样
    //以只读方式打开
    int fd = open(fifoname.c_str(),O_RDONLY);
    if(fd<0)
    {
        //打开文件失败
        exit(2);
    }
    //缓冲区
    char buffer[size];//C99的变长数组(也可以使用宏)
    //3.通信
    while(true)
    {
        //每次读取之前清空buffer
        buffer[0] = 0;
        //读取文件
        ssize_t num = read(fd,buffer,sizeof(buffer)-1);
        if(num > 0)
        {
            buffer[num] = 0;
            std::cout << "client say:" << buffer << std::endl;
        }
        else if(num == 0)
        {
            //代表写端退出,需要关闭管道文件并删除
            break;
        }
        else
        {
            //代表读取失败
            perror("read");
            break;
        }
    }
    //4.归还资源
    //关闭管道文件
    close(fd);
    //把管道文件删除
    int m = unlink(fifoname.c_str());
    //防止报错(有些编译器对于不使用的变量可能会报错)
    (void)m;
    return 0;
}

我们发现,命名管道的打开和关闭和普通文件并没有任何区别,但是这是两个进程,两个进程都需要先把管道文件打开,如果没有现成的管道文件,一个进程还需要创建管道文件,然后一个进程读管道文件另一个进程向管道文件里写即可(只是两个进程的打开方式不同),相对于普通文件,管道文件的数据并不会刷新到磁盘上,且管道文件是被特殊处理过的!

①client和server是如何看到同一份资源的?

因为Linux的路径是唯一的,文件名只是表象,路径+文件名才是本质!即特定的路径+文件名才有唯一的inode,因此两个进程是看到特定路径下的特定文件来保证两个进程打开的是同一份资源!

②为什么mkfifo创建的管道叫做命名管道?

因为这个文件是有自己的名字的,甚至它都有独立的inode,只不过打开时会被系统做特殊解释!

此外,命名管道有一个特点:打开一端,在另一端没打开时,open会阻塞!因此之前echo时就会卡住,只有双方同时打开才能进行实时通信!

此时我们运行有(先把fifo删掉才行):

等右侧的机器执行client进程就能成功,在右侧输入,左侧输出即可:

此外命名管道也有四种情况,因为它底层原理和匿名管道完全一样,其次命名管道解决了毫无关系的进程之间进行文件级进程通信(匿名管道只能在有血缘关系的进程之间通信)!

1.3命名管道的实践

我们需要创建一个namedpipe.hpp,整体框架:

cpp 复制代码
#pragma once
#include "common.hpp"
const int defaultfd = -1;
class NamedPipe
{
public:
    //构造函数
    NamedPipe(const std::string& name)
    :_name(name)
    ,_fd(defaultfd)
    {}
    //创建管道
    bool Create()
    {

    }
    //关闭管道
    bool Close()
    {

    }
    //读管道
    bool OpenForRead()
    {

    }
    //写管道
    bool OpenForRead()
    {

    }
    //输入参数:const &
    void Write(const std::string &in)
    {

    }
    //输出参数:*
    bool Read(std::string *out)
    {

    }
    //输入输出参数建议:&
    //移除管道文件
    void Remove(){}
    //析构函数
    ~NamedPipe()
    {}
private:
    //命名管道的名字
    std::string _name;
    int _fd;
};

后面的操作就是把之前的代码拷贝过来,进行一些修改即可:

cpp 复制代码
#pragma once
#include "common.hpp"
const int defaultfd = -1;
class NamedPipe
{
public:
    // 构造函数
    NamedPipe(const std::string &name)
        : _name(name), _fd(defaultfd)
    {
    }
    // 创建管道
    bool Create()
    {
        // 1.创建管道文件
        int n = mkfifo(_name.c_str(), mode);
        if (n == 0)
        {
            // 创建管道成功
            std::cout << "创建管道成功" << std::endl;
            return true;
        }
        // 创建管道失败
        return false;
    }
    // 关闭管道
    bool Close()
    {
        if (_fd == defaultfd)
            // 代表没有打开管道
            return;
        // 打开了管道
        close(_fd);
    }
    // 读管道
    bool OpenForRead()
    {
        // 2.打开文件,和常规文件一样
        // 以只读方式打开
        _fd = open(_name.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            // 打开文件失败
            return false;
        }
        // 打开文件成功
        std::cout << "打开文件成功" << std::endl;
        return true;
    }
    // 写管道
    bool OpenForRead()
    {
        // 直接以写的方式打开文件(往out里读)
        _fd = open(_name.c_str(), O_WRONLY);
        if (_fd < 0)
            // 打开文件失败
            return false;
        // 打开文件成功
        return true;
    }
    // 输入参数:const &
    void Write(const std::string &in)
    {
        // 往in里写
        write(_fd, in.c_str(), in.size());
    }
    // 输出参数:*
    bool Read(std::string *out)
    {
        // 缓冲区
        char buffer[size] = {0}; // C99的变长数组(也可以使用宏)
        // 读取文件
        ssize_t num = read(_fd, buffer, sizeof(buffer) - 1);
        if (num > 0)
        {
            // 读到消息就传给out
            *out = buffer;
            return true;
        }
        return false;
    }
    // 移除管道文件
    void Remove()
    {
        // 把管道文件删除
        int m = unlink(_name.c_str());
        // 防止报错(有些编译器对于不使用的变量可能会报错)
        (void)m;
    }
    // 输入输出参数建议:&
    // 析构函数
    ~NamedPipe()
    {}
private:
    // 命名管道的名字
    std::string _name;
    int _fd;
};

后面我们就可以改server.cpp和client.cpp的代码使用NamedPipe类的方法了,我就不做过多讲解了!

1.4命名管道的总结

命名管道主要解决毫无关系的进程之间进行文件级的通信!

代码已提交到Gitee上,可以自行查看:

namedpipe · 谭旦0000/test_for_csdn - 码云 - 开源中国

剩下的特点和匿名管道相同!

2.共享内存

2.1共享内存的原理

我们使用管道的方式在其他操作系统上并不能进行进程间通信,因此需要设计出一个能在所有操作系统上进行进程间通信的方案,因此设计出了共享内存!(system V已经在Linux淘汰了,所以不用去太多了解了)。

共享内存(shared memory),简称shm。

我们的虚拟地址空间有一个共享区,并且动态库就是映射到共享区的,动态库加载的前提条件是要在物理内存上开辟空间,在当前进程自己的地址空间里也开辟同等大小的内存块,然后把这两段内存块经过进程对应的页表映射起来,就会返回起始虚拟地址,因为我们能在一个进程能这样做,因此,所有进程都能这样做:

只是现在的内存块没有什么东西而已,此时就能让不同的进程看到同一份资源了!因此就能让进程A和进程B进行通信了!

步骤:①开辟物理内存;②申请虚拟空间并建立映射;③把虚拟地址空间映射到当前进程的地址空间里;④如果我们不需要了,就可以删除掉这个共享内存了!简单写:Ⅰ创建或获取共享内存;Ⅱ映射到自己的地址空间;Ⅲ删除。

我们上面三个步骤全部都是且只能由操作系统来完成,只是用户让操作系统来完成这种任务罢了,因此操作系统必须给用户提供系统调用!

有可能有多个进程间通信(如:A和B通信,C和D通信),因此在操作系统中一定会同时存在非常多的共享内存,而每个共享内存可能处于不同的状态(步骤),因此操作系统要把这些共享内存管理起来,先描述,再组织!

其次,共享内存除了可以两个进程间通信外,还可以3个及以上的进程进行通信,管道只能两个进程之间进行单向通信,但为了方便,后面我们只演示两个进程的共享内存。

2.2共享内存的操作

和命名管道一样,也需要建立四个文件:

其中的Makefile和命名管道一样:

bash 复制代码
.PHONY:all
all:server client

client:client.cpp
	g++ -o $@ $^ -std=c++11
server:server.cpp
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client
	rm -f server

2.2.1shmget系统调用

获取共享内存的系统调用:

它的作用申请一个system V版本的共享内存(创建一个共享内存),第二个参数:size代表共享内存的大小(建议是4096字节的整数倍),第三个参数shmflg:创建共享内存的标志位,我们只需要关心(IPC_CREAT和IPC_EXCL),其实就是宏/位图,其次,这个shmflg除了传标记位,还可以传权限(写代码时说)。

因为两个进程只看到同一个共享内存,意味着未来使用共享内存的时候,一定是一个进程创建,一个进程获取,因为如果两个进程都创建的话两个进程就不可能看到同一份资源了!

IPC_CREAT(单独出现时):如果创建的共享内存不存在,就创建它;如果存在,直接获取它!

IPC_EXCL:不能单独使用,单独使用没有意义,必须与前者结合使用,组合使用的意义:如果要创建的共享内存不存在,就创建它;如果已经存在,出错返回!(如果成功,返回的就是全新的shm)

返回值:如果成功了,就返回一个合法的内存标识符,失败了就返回-1,设置错误码表明失败原因。我们可以猜测内存标识符是一个整数,即内存标识符是一个特定的数字,上一次我们在open系统调用也会返回一个文件描述符,本质是文件描述符表的下标。现在我们只需要把它理解成和文件描述符差不多,用来标识系统资源的一种标识方法。

第一个参数key:键值(重要)。

在创建共享内存时,怎么知道共享内存是否存在呢?

这个问题得等键值key讲完后才能解决!

键值往往需要用户去传入,理论上这个键值可以随便写,但是这样会不可控,所以操作系统在进程通信角度,为了能够让用户传入的键值,就有一个函数叫ftok:

ftok函数需要我们传递两个参数,pathname是路径,proj_id是项目名称,这个函数可以为我们形成一个key,将来用户不用自己再去形成key。

一般这个键值是几其实不重要,更重要的是这个键值需要具有唯一性(在某种意义上要保证必须它具有唯一性),这个ftok系统调用只是把这两个参数进行了一系列的算法合并形成对应的键值,只要它具有唯一性就行!

实际上,ftok两个参数想填谁都可以,没有规定,不过建议pathname填自己能访问的路径,只要它能够给你形成一个唯一值就行!

为什么要用户传递key,如果共享内存需要键值,在底层自动实现不更好吗?

如果系统来形成key,那么创建共享内存的进程能通过这个返回的key得到共享内存,但是那些没有创建共享内存的进程对于物理内存上那么多的共享内存,它并不知道将来要访问的共享内存是谁。

那为什么不让创建共享内存的进程直接把这个key交给其他进程呢?

你的意思是说:你都把数据交给其他进程了,你已经可以进行进程间通信了,那还需要创建共享内存干啥子。我们本来的目的是想要把进程A和进程B进行进程通信,现在进程A和进程B没有通信啊,怎么把key传递到另一个进程,如果想要传递key,就需要通信;但是想要通信,就必须要传递key,这不是死循环了!!!

因为形成进程之前,它们两个都是代码,我们可以同时include同一个头文件,我们在这个头文件约定两个数据,让两个进程都能看到,然后两个进程用同一个ftok得到同一个key,只要在创建共享内存的时候,把这个key设置到共享内存里,因为key在共享内存里是唯一的,因此另一个进程可以精准找到这个共享内存!

因此回答如何看到同一个共享内存的:因为我们有一个相同的数据,未来就会形成相同的键值。

这个原理其实和我们之前所说的命名管道是类似的,注意:说的是看到同一个资源的方式是类似的,不是底层原理是类似的!

那么这个key应当保存在哪里?

共享内存是有内核数据结构的struct shmid_ds里面!

当然,如果形成的key冲突了(就算用ftok也是有可能冲突的),我们就需要把那两个参数改一下,直到不冲突为止!

2.2.2创建共享内存

我们在Shm.hpp写下:

cpp 复制代码
#ifndef __Shm_Hpp__
#define __Shm_Hpp__

#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>

//共享内存的路径
std::string gpathname = ".";
//共享内存的项目名称(随便写)
int gproj_id = 0x66;
int gdefaultsize = 4096;

//描述共享内存的类
class SharedMemory
{
public:
    SharedMemory(int size = gdefaultsize)
    :_size(size)
    ,_key(0)
    ,_shmid(-1)
    {
    }
    //创建共享内存
    bool Create()
    {
        //形成共享内存的键值(防止失败先放在这里)
        _key = ftok(gpathname.c_str(),gproj_id);
        if(_key < 0)
        {
            perror("ftok");
            return false;//创建失败
        }
        //创建键值成功
        //打印键值的内容
        printf("创建键值成功:0x%x\n",_key);//%x:以16进制打印
        //创建共享内存
        _shmid = shmget(_key,_size,IPC_CREAT | IPC_EXCL);
        if(_shmid < 0)
        {
            perror("shmget");
            return false;//创建共享内存失败
        }
        //创建共享内存成功
        printf("shmid:%d\n",_shmid);
        return true;
    }
    ~SharedMemory()
    {}
private:
    key_t _key;//键值
    int _size;//共享内存的大小
    int _shmid;//shmget返回的内存标识符
};

#endif

然后在server.cpp里写下:

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

int main()
{
    //构造类
    SharedMemory shm;
    //创建类
    shm.Create();
    return 0;
}

能够成功运行:

但第二次运行时,有:

这是因为我们每次都是使用同一个键值来访问同一块共享内存,而刚刚我们并没有释放内存空间的操作,所以就出现了报错!

虽然我们的代码里并没有释放共享内存的空间,但是,进程一旦结束了,哪怕曾经的代码里有内存泄漏问题,内存泄漏问题也会自动被释放掉(堆空间会被自动释放),我们自己写的程序都是运行不了几分钟就会退出,真正的软件是死循环运行的,如果有内存泄漏问题,那么内存就会越用越少,即常驻内存的进程,比较担心内存泄漏问题!

共享内存的特点:①共享内存的生命周期,不是随进程的,只要你没手动释放掉共享内存,那么它就会一直存在,直到你把操作系统重启,即共享内存的生命周期随内核!

因此我们必须手动删除它,使用:

bash 复制代码
ipcrm -m (shmid的值)

因为第一次是成功的,而且shmid是0,所以我们ipcrm -m 0即可:

如果第一次没有打印shmid的值,我们也可以通过:

bash 复制代码
ipcs -m

来查看具体信息:

如果删除了,那么就会有这样的界面:

其中key代表键值,shmid代表内存标识符,一般从0开始,owner代表创建共享内存的用户,perms代表共享内存的权限,bytes代表共享内存的大小,nattch代表当前有多少个进程和这个共享内存相关,如果没有就是0!

但是我想要的是主动释放这个共享内存,因此要有一个Remove方法,用来移除共享内存!

2.2.3shmctl系统调用

shmctl:shared memory control控制共享内存,文件=内容+属性,所以文件操作要么就是对文件内容操作,要么就是对文件属性操作,系统为了管理每个共享内存,还要开辟内核数据结构,共享内存在系统层面上是由内存块+共享内存的结构体组成,共享内存的结构体本质就是共享内存的属性,因此shmctl控制共享内存主要是用来修改获取或删除共享内存的属性。

因此,key:只有内核使用用来标识shm的唯一性;shmid:给用户使用,用来进行访问共享内存!

即用户设置的key是内核来使用,用户想要访问控制共享内存只能通过shmid来!(shmctl第一个参数是shmid能证明)

第二个参数cmd:你想要对共享内存进行什么样的操作,我们需要传递一个选项给这个参数,有以下选项:

其中IPC_RMID代表立即移除对应的IPC资源,一旦移除,最后的参数设置成NULL就可以!

2.2.4删除共享内存

cpp 复制代码
    //删除共享内存
    bool RemoveShm()
    {
        int n = shmctl(_shmid,IPC_RMID,nullptr);
        //失败
        if(n < 0)
        {
            perror("shmctl");
            return false;
        }
        //成功
        std::cout << "删除共享内存成功" << std::endl;
        return true;
    }

我们现在还需要把共享内存映射到当前进程的地址空间,我们叫做把当前共享内存挂接到当前进程的虚拟地址空间,如何进行挂接呢?

2.2.5shmat/shmdt系统调用

2.2.5.1shmat系统调用及使用
cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

at:attch关联/挂接,因此我们就可以挂接一个指定的共享内存到进程的虚拟地址空间,第一个参数shmid是共享内存,第二个参数代表你想把共享内存挂接到虚拟地址空间的哪一个地址处,这叫做用户指明虚拟地址空间的起始地址,这个参数我们一般不需要指明,设置为NULL即可,除非我们有特殊的用途,否则不用他,特殊用途如:想要动态库映射到指定位置,但是没有人比系统更了解虚拟地址空间,因此这个参数可以不用管!shmflg:当我们把虚拟地址挂接到进程地址空间的时候,挂接所对应的选项就是shmflg,我们只需要设置这个参数为0,使用缺省选项就可以!

shmat返回值,如果成功了,返回一个挂接到虚拟地址空间的一个数据段的起始地址,如果失败了就返回-1,返回值是void*就是从内存块通过页表映射到进程地址空间的虚拟地址。

上一个返回虚拟地址的函数叫做malloc:

malloc就是返回虚拟地址,即shmat的返回值等同于malloc的返回值!

因此我们需要把指定的共享内存挂接到自己的虚拟空间里,我们需要加个成员变量:

cpp 复制代码
void* _start_addr;

就是起始地址,默认的值为nullptr:

cpp 复制代码
    SharedMemory(int size = gdefaultsize)
    :_size(size)
    ,_key(0)
    ,_shmid(-1)
    ,_start_addr(nullptr)
    {
    }

然后我们写下挂载共享内存的代码:

cpp 复制代码
    //挂接共享内存
    bool Attach()
    {
        //shmat系统调用
        _start_addr = shmat(_shmid,nullptr,0);
        //64位下void*占8字节,int占4字节,损失精度了,会报错
        //if((int)_start_addr == -1)
        //long long int占8字节就行!
        if((long long int)_start_addr == -1)
        {
            //映射失败
            perror("shmat");
            return false;
        }
        return true;
    }

然后我们把server.cpp代码改为:

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

int main()
{
    //构造类
    SharedMemory shm;
    //创建共享内存
    shm.Create();
    //挂接共享内存
    shm.Attach();
    //删除共享内存
    shm.RemoveShm();
    return 0;
}
2.2.5.2共享内存权限问题

运行会发现:

这个报错代表权限不足,因此,我们要使用共享内存必须给共享内存设置权限(和设置文件权限一样)!因为权限不足,所以操作系统不允许你进行映射,默认权限是000,所以我们访问不了!

我们想要设置权限,可以在创建共享内存时加个选项设置权限,如:我想要设置权限为0666,由于有权限掩码,因此要这样写:

cpp 复制代码
    //创建共享内存
    bool Create()
    {
        //形成共享内存的键值(防止失败先放在这里)
        _key = ftok(gpathname.c_str(),gproj_id);
        if(_key < 0)
        {
            perror("ftok");
            return false;//创建失败
        }
        //创建键值成功
        //打印键值的内容
        printf("创建键值成功:0x%x\n",_key);//%x:以16进制打印
        //创建共享内存
        //但是这样权限为000
        //_shmid = shmget(_key,_size,IPC_CREAT | IPC_EXCL);
        //这样权限为666
        _shmid = shmget(_key,_size,IPC_CREAT | IPC_EXCL | 0666);
        if(_shmid < 0)
        {
            perror("shmget");
            return false;//创建共享内存失败
        }
        //创建共享内存成功
        printf("shmid:%d\n",_shmid);
        return true;
    }

此时就能有:

此时共享内存的权限是(先把删除共享内存的代码注释掉):

但是我们会发现,哪怕现在有一个进程和这个共享内存相关联,如果执行到删除代码仍然会删除,因为IPC_RMID只要执行到该代码,对应的共享内存立马被删除,不管有没有进程和他建立映射关系!但这样的做法我们不想要,如果要删除共享内存的话,需要进程先把共享内存和虚拟空间去关联,即还需要把这个映射关系去掉!

2.2.5.3shmdt系统调用及使用
cpp 复制代码
int shmdt(const void *shmaddr);

shmdt:shared memory detach,去关联,意思是如果将来不想用共享内存了,不要把它立即删除,应当让当前进程和共享内存"去关联",释放虚拟地址空间,释放页表当中虚拟到物理的映射,然后再把共享内存删掉,其中它的参数就是shmat的返回值,只要哪个进程调用这个函数,它就要拿该进程对应的虚拟地址释放掉!

shmdt成功就返回0,失败就返回-1。

加一个成员函数:

cpp 复制代码
    //让虚拟地址和共享内存"去关联"
    bool Detach()
    {
        int n = shmdt(_start_addr);
        if(n < 0)
        {
            //去关联失败
            perror("shmdt");
            return false;
        }
        std::cout << "将指定的进程共享内存从进程的地址空间移出" << std::endl;
        return true;
    }

并把server.cpp代码改为:

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

int main()
{
    //构造类
    SharedMemory shm;
    //创建共享内存
    shm.Create();
    //挂接共享内存
    shm.Attach();
    //与共享内存去关联
    shm.Detach();
    //删除共享内存
    shm.RemoveShm();
    return 0;
}

有:

2.2.6设置共享内存大小

我们之前说设置共享内存大小的时候只能设置为4096的整数倍,那么如果我设计成4097,即把代码改为:

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

int main()
{
    //构造类
    //SharedMemory shm;
    SharedMemory shm(4097);
    //创建共享内存
    shm.Create();
    //挂接共享内存
    shm.Attach();
    //与共享内存去关联
    shm.Detach();
    //删除共享内存
    shm.RemoveShm();
    return 0;
}

会出现什么情况呢?

为什么我设置成了4097,还是没有任何问题呢?

因为共享内存它要求是4KB的整数倍,如果你没有这样设置,操作系统会做4KB对齐(在内核层面上),因此,如果我设置了4097,那么操作系统就会申请4096*2字节的空间作为共享内存,因此我们建议共享内存的大小设置成4096的整数倍,不然会大大浪费内存空间!

如果我们把最后删除共享内存的代码注释,再查一下有:

不是说要进行4KB对齐吗,为什么这里的bytes还是4097呢,不应该是8192吗?

因为你申请了4097个字节空间,但是操作系统会申请8192个字节空间,因为你只要4097个字节空间,不告诉你我真实申请的空间是多少,不然就会让人很迷糊!即你要多少我给你多少,但你不要管底层真实用了多少,如果我真给你8192,那么你使用了多余的那段空间出错了就会怪操作系统,系统不会干扰你的操作!

为什么是4096?

后面讲解线程的时候会说!

2.3共享内存的实践

client.cpp不是主要的,它不用创建共享内存和删除共享内存,但是它需要获取共享内存,因此我们需要设计一个成员函数来获取共享内存,因为本质上server.cpp和client.cpp大部分方法一样,所以可以把之前的Create函数重命名为CreateHelper,并且不暴露给外界:

cpp 复制代码
private:
    //创建共享内存
    //多加一个参数代表选项(二者只是选项不同罢了)
    bool CreateHelper(int flags)
    {
        //形成共享内存的键值(防止失败先放在这里)
        _key = ftok(gpathname.c_str(),gproj_id);
        if(_key < 0)
        {
            perror("ftok");
            return false;//创建失败
        }
        //创建键值成功
        //打印键值的内容
        printf("创建键值成功:0x%x\n",_key);//%x:以16进制打印
        //创建共享内存
        //但是这样权限为000
        //_shmid = shmget(_key,_size,IPC_CREAT | IPC_EXCL);
        //这样权限为666
        //_shmid = shmget(_key,_size,IPC_CREAT | IPC_EXCL | 0666)
        //选项不同
        _shmid = shmget(_key,_size,flags);
        if(_shmid < 0)
        {
            perror("shmget");
            return false;//创建共享内存失败
        }
        //创建共享内存成功
        printf("shmid:%d\n",_shmid);
        return true;
    }

因为它们底层只是选项不同罢了,所以我们可以这样写!

然后再在public里加:

cpp 复制代码
    //创建共享内存
    bool Create()
    {
        return CreateHelper(IPC_CREAT | IPC_EXCL | 0666);
    }
    //获取共享内存
    bool Get()
    {
        //只需要一个选项就行
        return CreateHelper(IPC_CREAT);
    }

然后把client.cpp代码改为:

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

int main()
{
    //构造类
    SharedMemory shm;
    //获取已存在的共享内存
    shm.Get();
    //挂接共享内存
    shm.Attach();
    //与共享内存去关联
    shm.Detach();
    return 0;
}

我们先把server.cpp代码改一下,休眠几秒钟再执行:

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

int main()
{
    //构造类
    SharedMemory shm;
    //SharedMemory shm(4097);
    //创建共享内存
    shm.Create();
    //挂接共享内存
    shm.Attach();
    sleep(5);
    //与共享内存去关联
    shm.Detach();
    //删除共享内存
    shm.RemoveShm();
    return 0;
}

然后同时开机器执行:

如果出现这种:

代表该共享内存已存在文件,需要先删掉再执行!

2.4共享内存的通信

但是我们现在还没有进行通信,因为没有发送任何数据,我们一直在建立通信信道,因此创建信道是很麻烦的事情(因为进程之间具有独立性)。

结论:我们在使用共享内存的时候,它把共享内存块映射到地址空间,映射到自己进程地址空间的堆栈之间的共享区,即共享区既可以映射动态库,也可以映射共享内存,因此我们之前讲过堆栈之间的地址差(空间)特别大。

那么将来如何访问内存呢?

我们之前用malloc申请的空间拿到指针就能够用指针往物理内存块里写(指针存放虚拟地址,通过页表映射到物理地址),因此,读写共享内存,不需要系统调用,因为共享内存已经映射到自己的用户空间了(进程地址空间),我们想要访问共享内存,直接使用指针就行,因此另一个进程就能立马看到写的内容了!

如:我们想要写一个字符,直接添加一个成员变量:

cpp 复制代码
int index;//下标

而要写一个字符,只需要把共享内存当做数组进行操作即可:

cpp 复制代码
    //往共享内存写一个字符
    void AddChar(const char& ch)
    {
        //因为为void*我们需要强转
        ((char*)_start_addr)[index++] = ch;
        //防止越界
        index %= _size;
    }

但是这样做的话读写只有一个位置,因此,要多一个,改为:

cpp 复制代码
    int _windex;//写下标
    int _rindex;//读下标
    int _num;//含有元素的个数

然后再改一下:

cpp 复制代码
    //往共享内存写一个字符
    void AddChar(const char& ch)
    {
        if(_num == _size)
            return;//代表不能继续插入了
        //因为为void*我们需要强转
        ((char*)_start_addr)[_windex++] = ch;
        //防止越界
        _windex %= _size;
        _num++;
    }
    //往共享内存里删除一个字符
    void PopChar(char* ch)
    {
        if(_num == 0)
            return;//代表不能删除了
        *ch = ((char*)_start_addr)[_rindex++];
        _rindex %= _size;
        _num--;
    }

但是实际上这种方式是不可用的,因为两个进程实例化的两个对象并不会影响对方的num,所以这种方式是不可取的,除非在共享内存的前面几个字节设置num,读数据就--,写数据就--,这样才行!想要实现需要去搜索AI大模型了(因为后续如果要完善这个通信功能代码量是越来越多的,并不是重点)!

2.5共享内存的属性

除了共享内存的内容外,还需要共享内容的属性,因此需要操作系统给用户提供获取共享内存相关属性对应的接口和容器,其中shmctl的第三个参数buf就是给用户层提供用来获取操作系统内部共享内存对应的属性:

cpp 复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

因此,第三个参数是struct结构体,共享内存的属性有:

cpp 复制代码
struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Creation time/time of last                      modification via shmctl() */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
    };

第二个是共享内存的大小,第三个是共享内存最近一次被挂载的时间,第四个是共享内存最后一次失去关联的时间,第五个是最后一次被改变的时间,第六个是创建共享内存的用户,第七个是attach和detach的进程是谁,最后一个是当前有多少个进程正在关联共享内存......

其中共享内存最核心的属性是第一个struct ipc_perm shm_perm:IPC:进程间通信,perm:permission权限,它代表共享内存相关的属性信息,它包含:

cpp 复制代码
struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */
               uid_t          uid;      /* Effective UID of owner */
               gid_t          gid;      /* Effective GID of owner */
               uid_t          cuid;     /* Effective UID of creator */
               gid_t          cgid;     /* Effective GID of creator */
               unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
           };

__key:我们使用ftok形成的key,uid:拥有者,gid:所属组,cuid:共享内存有效创建者,cgid:共享内存有效创建组,mode:给共享内存设置的权限。

因此我们可以设置一个成员函数来获取相关的属性:

cpp 复制代码
    //打印共享内存相关的属性
    void PrintAttr()
    {
        //获取共享内存属性的结构体
        //也可以省略struct:
        //shmid_ds ds;
        //不过最佳实践是:
        struct shmid_ds ds;
        //第二个参数设置成IPC_STAT可以把内核相关的属性拷贝到指定的结构体当中
        int n = shmctl(_shmid,IPC_STAT,&ds);
        if(n < 0)
        {
            //获取失败
            perror("shmctl");
            return;
        }
        //打印共享内存相关属性
        //想要打什么自己输出即可
        printf("key:0x%x\n",ds.shm_perm.__key);
        printf("size:%ld\n",ds.shm_segsz);
        printf("atime:%lu\n",ds.shm_atime);
        printf("attach:%ld\n",ds.shm_nattch);
    }

2.6共享内存的特征

共享内存常规的特征是:

①生命周期随内核,只要我们不主动释放这个共享内存一直存在;
②共享内存是IPC速度最快的,因为它减少了数据拷贝的次数,且它在通信期间不用系统调用;

因为共享内存在进行通信的时候是不需要使用系统调用的,直接拿着虚拟地址读写指定的内存块,而学管道的时候,是需要把数据拷贝给操作系统,然后再把数据从操作系统拷贝到用户,用的是系统调用,速度比较慢,而共享内存用的都是指针,只要往内存写,对面立马就能看到(因为访问的都是同一个地址),系统调用也是有成本的!

如:我们之前学习STL中的vector,当我们插入的数据比vector能插入的数据多,它会进行自动扩容,相当于在里面有空间配置器,就算我们只比原来多了一个数据,但是它扩容还是乘以2的(多数情况),我们明显用不到这么多空间为啥还扩容这么多?

如果我们一次超出空间就申请一次,那么底层就会频繁的new空间,就需要频繁向操作系统要内存,因为new/malloc底层要系统调用,而如果我们一次扩容很多个空间,下次就不需要new了,本质上可以减少系统调用的次数,因此,系统调用是有成本的!因此STL速度比较高是因为它做了很多优化速度的事情!

系统调用需要很多拷贝(后面会更加了解,现在只要记住结论即可),所以系统调用有成本!

③共享内存,没有同步、互斥机制来对多个进程的访问进行协同!

我们可以试一下代码,单独启动读端它并不会阻塞在那里等待写端传入数据,它会直接读取数据,只不过读取的数据为空罢了;写端也不管读端有没有在,一直在写。也就是说它们两者都不知道对方有没有存在,它们只知道共享内存的存在!因此有可能一方把数据刚写到一半另一方先把数据取走了!

这样就会导致两者并发运行,那么如何保证它们两端同步、互斥呢?

后面我们讲解信号量的时候就可以实现共享内存的同步或互斥相关的保护了;后面也会讲解多线程,那个时候会专门来讲同步和互斥!

我们之前的进程池里也讲过父进程可以通过发送信息来控制子进程的运行与否,也可以让这个东西与共享内存结合来完成共享内存的通信(用管道的同步机制和共享内存的快速来控制),可以让进程执行产生一定的顺序性!

2.7共享内存的代码汇总

代码已提交到Gitee上感兴趣的可以去查看:

shared_memory · 谭旦0000/test_for_csdn - 码云 - 开源中国

3.System V通信方式(简述)

3.1消息队列

3.1.1概念

操作系统内部可以提供一个管道,让两个进程之间进行通信,我们可以让操作系统内部提供一个队列,这个队列叫做msgqueue(message queue消息队列),我们知道队列底层在实现时要么就是顺序表,要么就是链表(消息队列本质是用链表实现的)。当A进程给B进程通信时,A进程可以把数据拷贝给消息队列,在拷贝的时候,就需要有一个队列结点,将来A进程把数据拷贝到结点内部,然后把该结点入队列,未来B进程也可以把自己的数据这样传递到消息队列内部。

因为进程间通信本质是让不同的进程看到同一份资源,同一份资源就是公共的队列结构,无非就是内存提供形式上不一样,共享内存提供的是一个内存块,而消息队列是提供多个内存块,只是用了链表连接起来罢了。因此,两个进程看到同一个队列,一个进程放结点,另一个进程拿结点,此时就完成了进程通信,这就叫做消息队列。

消息队列提供了一种:一个进程向另一个进程发送 数据块的方式!(也支持双向通信)

可是这样带来了一个问题,接收数据(读数据)的那一方怎么知道哪些结点是另一个进程发送的呢?

也就是说每个进程只能看到里面的数据,但是不知道具体是哪个进程发送的这个数据。因此每个进程往消息队列放数据时,不能单纯放数据,而是要给每个瓶子带上标签,只要发现标签是别人的就能拿这个结点的数据了。

因此,补全一下:消息队列提供了一种:一个进程向另一个进程提供发送有类型数据块的方式!

因此,必定存在多个消息队列,因此,消息队列也需要被操作系统管理,先描述,再组织!

3.1.2消息队列相关系统调用

msgget该系统调用可以获取一个消息队列

第一个key和之前共享内存的key是一样的道理,不做过多解释;msgflg消息队列的选项,常见有两个:

返回值:

成功时会得到一个消息队列的标识符,失败会返回-1。

msgctl:用于删除一个消息队列:

第一个参数msqid代表消息队列的标识符;第二个参数cmd,代表你想要做的操作,有以下操作:

如果想要删除消息队列,就用IPC-RMID;第三个参数,如果你想进行删除操作,不用这个参数即可;如果想要获取消息队列相关属性,传递一个相关结构体即可(其操作和共享内存差不多)。

查看消息队列使用:

bash 复制代码
ipcs -q

想要删除消息队列使用:

bash 复制代码
ipcrm -q msgid

msgid代表消息队列的标识符!

实际上消息队列和共享内存的操作差不多!

消息队列的发送消息和接受消息:

3.2并发编程

①多个执行流,能看到的同一份资源我们叫做共享资源(前面说的所有通信方式都是一种共享资源),如果没有对共享资源进行保护,如:两个进程同时往一个区域里写,没有区分的话可能覆盖所写的内容;一个进程在还没写完数据的过程中被另一个进程先把前面的数据读取了,会导致数据缺失。这两种情况都称之为因为并发访问公共资源导致数据不一致的问题!

这就说明一旦资源共享,资源就可能处于一种比较不可控的状态,因此就需要资源共享的双方进行同步,多并发的情况下就很有可能导致这个问题!

为什么会有共享资源呢?

因为多进程要协作,因此进程间需要通信,因此需要看到共享资源,如果没有保护好,就导致数据不一致了!上面的所有通信方式,只有管道才能保护好共享资源,共享内存和消息队列速度虽然快,但是也产生了一些问题,只要有解决方案就会带来其他问题!

②被保护的资源叫做临界资源。

③保护临界资源的方式常见有:互斥与同步!

④任何时刻,只允许一个执行流访问资源,这叫做互斥!

⑤多个执行流,访问临界资源的时候,具有一定的顺序性,叫做同步!

⑥系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源!

我们做为程序员,需要关注两套资源,一套叫做资源本身(管道文件/共享内存/消息队列),访问资源是通过代码去访问资源的,如访问管道的代码read/write,访问共享资源的指针*ptr,访问消息队列的msgsnd/msgrcv。我们把访问资源的代码叫做临界区,被访问的资源叫做临界资源,而我们将来写的代码里也有不访问资源的代码,因此:

⑦在进程中涉及到互斥资源的程序段叫临界区,你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(非临界区)。

⑧所谓对共享资源进行保护,本质是对访问共享资源的代码进行保护。(如何保护:后面会讲解同步和互斥,到时候会讲)

3.3信号量

3.3.1概念

在现实世界当中,很多共享资源都是被整体使用的,还有很多资源是公共资源,它是被分散的一个一个使用的:

如:ATM机取钱是公共资源,但是每次取钱的时候只允许一个人去存取,这叫做资源整体使用;电影院也是公共资源,放映厅不止一个人进去看电影,很多人都能同时看到一个电影,因此这叫做资源不被整体使用!

看电影的时候,放映厅是所有买了票的人共享的一个资源,将来每一个人都要有一份自己的小资源,但是每一个人只能占一个位置。看电影之前我们必须要买票,但是这个座位是你买到票的时候属于你,还是你坐到座位上才属于你呢?

只要我们把票买了,座位资源就是我的了,就算将来我不去看电影了,这个座位也不能被别人占着,因为它本来就是你的!

因此买票的本质:对资源的预订机制。

对于电影院的经营者来说,①如果多卖票了,那未来肯定是有人没有座位了,因此就一定会出问题;②如果我一个座位同时卖给了多个人。

因此,为了保证不发生上面两种情况,我们需要设置一个count,只要卖出一张票,就把count当前值给这个人,然后count--,直到为0就不卖票了,这样就能保证不多卖票了(卖重票还需要多做工作)。

因此信号量本质就是一个计数器(count),它主要保证资源不会出现多申请的情况,还要保证所有进程未来想通过临界区访问临界资源,必须先申请信号量(让计数器做--)。因为信号量是描述临界资源多少的计数器,先申请信号量就如同电影票看电影之前要先买票一样,此时就完成了对公共资源的预定,即便当前进程还没有访问公共资源,但是还会等待你去访问!

信号量本质是一种描述临界资源数量的计数器,该计数器本质是对资源的预订机制!(可以暂时理解成信号量就是一个整型变量)

如果电影院放映厅只有一个座位呢,应该让当前信号量计数器等于1,当有人买票时就要把这个计数器设置为0,等到资源被使用(预订时也算)的时候,就没法给别人使用这份资源了,必须得等这个人把电影看完,才能重新把计数器设置为1。因为这个信号量只能为1或者0,我们把这种信号量称之为二元信号量!这就是互斥功能,之前的多人使用同一份资源叫做同步功能!

因此,信号量分为多元信号量和二元信号量,多元信号量未来主要用来提供互斥或同步功能,二元信号量用来提供互斥功能!

因此,未来进程在执行的时候,也必须申请信号量,只有申请到信号量的进程才能继续执行,没有申请到信号量的就要阻塞,直到等信号量没有被占用再申请并成功才能继续执行。

但是,这样就必须保证所有进程看到同一个信号量,那么信号量本身是共享资源,也就是说信号量要保护共享资源,但是信号量本身也是共享资源,谁来保护信号量呢?

所以信号量设计出了P操作,P操作是对信号量计数器进行--,但是必须要保证P操作必须是原子的;还设计出了V操作,对信号量计数器进行++,它也是原子的!即信号量本质是一种描述临界资源数量的原子性计数器,是对资源的预订机制!

凭什么让多个进程看到同一个信号量?

因此信号量也归纳到进程间通信的范畴了,实际上看到同一个信号量是很简单的!

3.3.2相关系统调用

创建信号量:

第二个参数:nsems,创建信号量的个数,如果创建了多个信号量,就会形成一个数组,nsems嗲表的是信号量集当中对应的信号量个数,剩下的系统调用和之前差不多:

还缺了对信号量初始化以及对信号量P和V操作。

对指定信号量做操作:

semid:信号量的编号,sops:信号量的操作方法,nspos:操作信号量的个数,其中sembuf核心成员:

第一个代表你想要操作的信号量编号(数组下标),第二个设置为-1,就会对操作的信号量进行--;设置为1或+1,代表对指定信号量进行++,即这个参数可以指定信号量的P/V操作;第三个参数我们暂时设置为0即可,因为现在没法理解!

semctl除了对信号量进行删除以外,我们还可以对信号量进行初始化操作

其中:

cpp 复制代码
int semctl(int semid, int semnum, int cmd, ...);

这个函数是可变参数的,其中的SETVAL传递给cmd用来指定信号量进行初始化,应当设置成什么值,三个点里面填:

为什么设计信号量?

信号量是用来保护共享资源的 ----- 共享内存。

查询信号量用:

bash 复制代码
ipcs -s

删除信号量:

bash 复制代码
ipcrm -s semid

删除所有的IPC资源:

bash 复制代码
ipcrm -m/-s/-q shmid

所有的System V资源,生命周期全部都随内核!此外,所有的System V资源,都要被OS管理起来!

4.内核组织管理IPC资源

System V是操作系统专门设计的进程间通信的模块,操作系统是如何管理IPC资源呢?

我们刚刚看到的每个通信方式的属性都是从IPC资源里来的,从操作系统内部来的,只不过它拷贝时只拷贝部分,不拷贝全部。

所有的通信方式对应的属性第一个变量都是一样的:perm,在内核当中,这个成员叫做:kern_ipc_perm。

我们知道,结构体变量的地址和结构体第一个元素的地址在数字上是一样的,类型可能不一样。我们定义一个整型变量,由于它占四个字节,因此它就有四个地址,但是我们取地址只有一个地址,这个地址是你所开辟的最小的字节作为当前变量的地址数字,所以操作系统存在类型的概念,汇编之后就被转化为读取的字节数!因此我们就可以使用数组来对IPC资源进行管理,只需要通过强制类型转化就能把这个数组变成其他的通信方式了,这就是C语言实现多态的过程!

结论①:Linux++内核中管理IPC资源(进程间通信资源),用的是数组来管理!++

结论②:我们可以理解key是唯一标识一个结构的,而我们也可以把这个shmid理解为数组下标,因为我们之前演示的时候发现,这个shmid是从0开始越来越大的,因此后续挂接等操作都需要用到这个id,它们都通过这个shmid来找到该结构体的对象(通过shmid的数组下标来访问数组),因为内核里采用柔性数组管理IPC资源,这就意味着只要我们源源不断的申请IPC资源,那么柔性数组就有可能扩容,但这个得到的shmid就需要按照最大的文件描述符来决定柔性数组的容量,因为选择了柔性数组,所以内核里就需要对IPC资源进行动态的管理,因此就需要记下最大的fd来进行缩容,因此返回的shmid都是线性增长的,方便动态管理。因为这个System V方式相对于其他的方式,它与文件方式没用很多的关联性,因此学习这种通信方式成本很高,因此这个方式就已经被淘汰了!

结论③:动态库是如何映射到进程地址空间的?是如何找到动态库加载的起始地址?是如何做到加载时got重定向的?

动态库是一个库文件,它的原理和共享内存一样,只不过比共享内存多了一个从磁盘把文件加载到内存,因此获取动态库起始地址不是一件难事,因此就可以在链接时修改可执行程序的got来进行内存级的地址重定向,此时就能把ELF文件加载进来了。

具体的内核源代码各位自己去网上下载然后搜索验证即可!

相关推荐
电科_银尘1 小时前
【深度学习项目实战·02】Linux服务器环境搭建:从SSH登录到稳定运行项目
linux·服务器·深度学习
Yana.nice1 小时前
清除密码历史记录,解决pam安全策略密码无法复用问题
运维
海宇服务1 小时前
零信任架构实战:基于海宇风控经营异常预警构建自动化央企物资采购供应商资信排查网关
运维·人工智能·架构·自动化
开开心心就好2 小时前
发票信息录入太麻烦?离线工具一键转表格
运维·服务器·前端·pdf·ocr·maven·intellij-idea
运维小斌2 小时前
麒麟 V10 部署 OceanBase:故障速查与验收清单
linux·数据库·oceanbase·麒麟
吴声子夜歌2 小时前
Docker入门与实战——热门镜像介绍
运维·docker·容器
水饺编程2 小时前
第2章:Linux 基础,在 root 中创建用户账号
linux·c语言·汇编·1024程序员节
ITxiaobing20232 小时前
IP 定位服务怎么选:没有“公认第一”,只有场景匹配
服务器·网络协议·tcp/ip