冰冰学习笔记:管道与共享内存

news2024/11/18 23:32:59

欢迎各位大佬光临本文章!!!

 

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool


系列文章推荐

冰冰学习笔记:《哈希表与无序容器》

冰冰学习笔记:《Linux的文件系统》


目录

系列文章推荐

前言

1.进程间通信

2.管道

2.1匿名管道

2.2命名管道

3.共享内存

总结


前言

        我们知道在生活中“管道”是用来传递资源的,那么在操作系统中,如果两个进程之间需要传递资源,那么也需要“管道”进行传输。进程与进程之间传递资源的行为其实就是进程间通信的行为,而管道与共享内存就是进程间通信的一种手段。下面我们就详细介绍一下进程间通信的过程与管道,共享内存等手段的使用。

1.进程间通信

        进程之间进行通信其实并不是那么简单,之前我们学习进程的时候都知道,进程之间具备独立性。每个进程都有自己独特的数据与资源,都有自己独有的task_struct结构体。因此进行进程间通信的前提是先让不同的进程看到同一份资源,然后在进行数据的交换。

        为什么需要进程间通信?

        单进程无法使用并发能力,无法实现多进程的协同,进程间通信是实现多进程协同的手段。当一个任务需要多个进程进行共同协同的时候据必然需要进程之间进行数据的交互。每个进程都是独立的,但是进程之间需要进行数据传输(一个进程需要将它的数据发送给另一个进程)、资源共享(多个进程之间共享同样的资源)、通知事件(一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件)、进程控制(有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变)。而这些都需要进程间通信进行完成。

        进程间怎么进行通信呢?

        如前文所述,进行进程间通信的前提是确保不同的进程能够看到同一块“内存”(特地的结构组织)。但是这一块特定的内存并不属于任何一个进程,而是进程之间共享的。

        随着技术的发展,进程间通信逐渐发展出了以下的标准。1.管道:Linux系统原生提供的通信方式。2.SystemV IPC:进行多进程的单机通信,有共享内存,消息队列,信号量等方式。3.POSIX IPC:多进程通信,通常进行网络的通信。                

2.管道

        什么是管道?管道是Unix中最古老的通信方式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道是单向通信的工具,一个进程将数据写入管道,另一个进程将数据从管道中提取出来。

2.1匿名管道

        管道分为匿名管道和命名管道,匿名管道通常进行具有血缘关系的进程之间进行通信,常用于父子进程。

        父子进程之间进行通信也需要确保父子进程首先看到同一份资源,那么如何确保父子进程能够看到呢?每个进程都有自己的task_struct结构体,当父进程创建子进程的时候,子进程将会拷贝一份父进程的task_struct结构体,作为自己独有的数据。父进程内部的file_struct结构体里面的fd_array[]数组中保存的打开的文件操作符也会被子进程继承,子进程同样会复制一份。但是文件操作符指向的文件信息并不会被重新复制一份,两个进程中的文件操作符指向的还是同一份文件,此时我们发现,父子两个进程通过自己的文件操作符fd看到了同一份文件了。

        两个进程都看到了同一份文件,我们只需要父进程向3号文件描述符指向的文件进行写入操作,子进程从3号文件描述符指向的文件进行读取操作,那么此时就进行了父子进程的通信。此时,这个共同看到的文件我们称之为匿名管道文件。这个文件的数据没必要刷新到磁盘中,在内存中进行数据的交换即可。

因此父子进程之间进行通信的方式我们可以做出以下总结:

(1)父进程以读写的方式打开文件

(2)fork创建子进程

(3)父子进程关闭各自不需要的文件操作符

        下面我们进行代码的实现,完成父子进程的通信操作。

        首先我们需要让父进程以读写的方式打开一个匿名管道文件,我们可以调用pipe函数实现。

创建匿名管道文件:int pipe(int fd [2]);

头文件:#include<unistd.h>

参数:fd:文件描述符数组,fd[0]表示读取的方式访问打开的文件,fd[1]表示写入的方式访问文件。

返回值:int类型,创建管道文件成功返回0,失败返回-1。

        执行下列代码后我们就完成了管道文件的创建,接下来就需要创建子进程,并且子进程关闭写端,父进程关闭读端。这样我们就能进行管道的单向通信,父进程向管道文件内部进行写入,子进程进行数据的读取操作。 

        使用fork函数创建子进程,并且使用close函数关闭不需要的文件操作符。随后父进程拿着fd[1]文件操作符使用write函数将数据写入到管道文件中,子进程则拿着fd[0]使用read函数将数据从管道文件中进行读取,最终完成两个进程的通信操作。

具体代码如下:

int main()
{
    //1.创建管道
    int pipefd[2]={0};
    int n = pipe(pipefd);
    assert(n!=-1);
    (void)n;
#ifdef DEBUGE
    cout<<"pipefd[0]:"<<pipefd[0]<<endl;
    cout<<"pipefd[1]:"<<pipefd[1]<<endl;
#endif
    //2.创建子进程
    pid_t id =fork();
    assert(id!=-1);
    //3.构建单向通信,父进程写入,子进程读取
    if(id==0)
    {
        //子进程--读取
        //关闭写入端
        close(pipefd[1]);
        char buffer[1024*8];
        int count2=0;
        while(1)
        {
            sleep(1);
            //写入的一方,fd没有关闭,如果管道有数据就读,没有就等
            //写入的一方,fd关闭,读取的一方read会返回0,表示读到文件结尾
            ssize_t s= read(pipefd[0],buffer,sizeof(buffer)-1);
            count2++;
            if(count2==5) 
            {
                cout<<"读端关闭"<<endl;
                break;
            }
            if(s>0)//读取成功
            {
                buffer[s]=0;
                cout<<"child get a message["<<getpid()<<"]"<<"Father#"<<buffer<<endl;
            } 
            else if(s==0)
            {
                cout<<"writer quit(child)"<<endl;
                break;
            }
            
        }
        close(pipefd[0]);
        exit(0);
    }
    //父进程--写入
    //关闭读取端
    close(pipefd[0]);
    string message="我是父进程,我正在给子进程发消息";
    int count=0;
    char send_buffer[1024];
    while(true)
    {
        snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
        ssize_t m=write(pipefd[1],send_buffer,strlen(send_buffer));
        sleep(1);
        cout<<count<<endl;
        if(count==10)
        {
            cout<<"writer quit(father)"<<endl;
            break;

        }
    }
    close(pipefd[1]);
    pid_t ret=waitpid(id,nullptr,0);
    assert(ret>0);
    (void)ret;
    return 0;
}

在这个示例代码中我们可以看到管道的以下特点:

        管道是一个文件,具备读写权限,因此管道文件具备一定的访问控制。管道是基于文件的,文件的生命周期是随进程的,管道的生命周期也是随进程的,进程结束,管道也就关闭了。管道文件也具备一定的大小,写入的块,读取的慢,那么当管道文件写满的时候,管道文件将不能进行写入,需要读取数据后才能写入。而如果是写慢读块,当管道内没有读取的数据时,读取端将会等待写入端的写入。当写入端关闭,读取端将会读到0,表示文件结尾。当读取端关闭的时候,写入端还在写的时候,操作系统将会关闭写入端。

        管道写入数据量的大小最好是不大于PIPE_BUF,这样会保证写入的原子性,当要写入的数据量大于PIPE_BUF时,Linux将不再保证数据的原子性。  管道通常都是半双工的,数据只能流向一个方向,如果需要双向通信则需要两个管道。

2.2命名管道

        匿名管道只能进行具有共同祖先的进程间通信,当我们想在不相关的进程间进行通信时,此时匿名管道就没有作用了,我们需要命名管道进行通信。

        命名管道其实就是一个特殊的文件,我们在学习文件系统的时候提到过,命名管道本质就是一个管道文件,该文件只存在于内存中,不会像磁盘中进行刷新。

        命名管道与匿名管道的通信方式类似,两个进程需要先看到同一个管道文件,然后才能进行通信,这就需要我们在一个进程中创建管道文件。在命令行中,我们可以使用mkfifo+管道文件名称进行管道文件的创建。

        但是我们不能在进程通信前手动创建管道文件进行通信,我们需要在进程中进行管道文件的创建。 这就需要我们使用mkfifo函数在进程中进行管道文件的创建。

创建命名管道文件:int mkfifo(const *filename,modr_t mode);

头文件:#include <sys/types.h>    #include <sys/stat.h>

参数:filename为管道文件的路径和文件名称,mode为创建管道文件后文件的权限

返回值:成功创建管道文件返回0,失败返回-1

        命名管道创建后在使用方式与使用普通文件类似,我们需要使用open函数打开文件才能进行数据的读写操作。如果是以读的方式打开,进程将会阻塞,等待写入进程的数据写入管道。如果是以写入打开管道文件,那么将会等待读取端打开。

        使用命名管道时需要注意进程执行的顺序,创建管道文件的进程需要先执行,后面的进程才能使用管道文件。

命名管道实现客户端与服务端的通信代码:命名管道和共享内存 · 1a650d7 · 冰冰棒/Linux - Gitee.com

3.共享内存

        共享内存是除了管道外另一种进程间执行通信的方式。管道是基于文件的方式进行通信,两个进程通过看到同一个文件,并对该文件进行读取和写入的操作来完成通信。共享内存是通过映射同一块物理空间来进行通信的。

        每个进程都有自己独特的task_struct结构体,并且有自己的虚拟内存地址,并通过自己的页表映射到物理内存中。共享内存就是操作系统提供的一块内存空间,并且通过进程的页表将该空间映射到每个进程自己的虚拟地址空间中,这样每个进程就可以拿着这一块公共的地址进行数据的通信。

那么如何创建共享内存呢?这就需要调用函数shmget。

创建共享内存:int shmget(key_t key, size_t size, int shmflg);

头文件:#include<sys/shm.h>

参数:key,关键码,两个进程通过唯一的key找到同一块共享内存

        size,共享内存的大小(最好是4096字节的倍数,如果不是系统申请的实际空间是倍数,

        返回的空间则是你申请的大小)

        shmflg,状态,含有两个参数IPC_CREAT,IPC_EXCL 。一般情况下填写0,表示直接

        获取已经存在的共享内存。当单独使用IPC_CREAT时,表示创建共享内存,如果底层

        已经存在,则直接获取它并返回,如果不存在就创建并返回。两个参数都使用则表示如

        果底层不存在则创建并返回,如果底层存在则返回错误,这将保证我们获得的共享内存

        一定是新创建的。单独使用IPC_EXIT没有意义。一般情况下我们还需要添加共享内存

        的权限。

返回值:失败返回-1,成功返回共享内存的标识符。

        通过函数我们知道,获取共享内存并不能单独的使用shmget函数进行获取,我们需要提前得到一个key,让不同的进程通过唯一的key来获得同一块共享内存。 系统提供了获取key的函数:

创建唯一的key:key_t ftok(const char *pathname, int proj_id);

头文件:#include <sys/types.h>    #include <sys/ipc.h>

参数:pathname,文件路径,该路径并没有特殊的含义,我们只需要传入一个文件的路径即

        可,函数将会根据路径的inode值进行计算得到key,传入的路径最好是自己有访问权

        限。一定要确保该路径存在。

        proj_id,函数会根据该id与路径一起创建一个唯一的key,该数值可以随意定义,通常取

        值为1~255。

返回值:当函数执行成功,则会返回key_t键值,否则返回-1。

        我们只需要在不同的进程中使用同一个ftok的参数进行创建,就会得到同一个key值。然后调用shmget就可以获得同一块共享内存空间。

        共享内存创建后,我们需要获取到共享内存的地址才能进行进程间的通信,在通信完成后需要将进程与共享内存之间在断开连接,并且将共享内存释放掉。

连接共享内存的函数为shmat

将共享内存块连接到进程地址空间:

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

头文件:#include <sys/ipc.h>

参数:shmid: 共享内存标识

        shmaddr:指定连接的地址,默认为NULL
        shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY,默认为0

        shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标

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

        则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr %

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

返回值:成功返回一个指针,指向共享内存起始地址;失败返回-1。

         挂接完成后我们将得到一个地址,我们可以将该地址类比为使用malloc函数申请的空间地址,我们可以在里面进行写入和读取。 

        操作完毕后需要将共享内存断开连接,需要使用shmdt函数。

将共享内存段与当前进程脱离  int shmdt(const void *shmaddr);

头文件:#include <sys/ipc.h>

参数: shmaddr: 由shmat所返回的指针。

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

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

        当共享内存使用完毕后,我们需要将共享内存删除掉,如果没有删除共享内存,那么我们在进行运行的时候会报错。
        例如进程异常退出后,共享内存并没有被删除,我们在运行该进程时会获取共享内存失败。

        当我们使用ipcs -m 命令查看共享内存时,发现共享内存依然存在,并没有被删除。

        我们可以手动使用命令ipcrm -m + shmid进行删除,也可以使用函数在进程中调用删除。

        使用shmctl函数进行删除:

用于控制共享内存 int shmctl(int shmid, int cmd, struct shmid_ds *buf);

头文件:#include <sys/ipc.h>

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

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

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

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

        切记,我们在那个进程中创建的共享内存就在那个进程中删除共享内存,其他的进程不需要删除。 

通过以上的操作我们就完成了使用共享内存进行进程间通信的例子。

代码连接:命名管道和共享内存 · 1a650d7 · 冰冰棒/Linux - Gitee.com

小结:

        key与shmid是不同的,可以值是在创建共享内存的时候用来在系统内部标识同一块内存的标识符,只在创建的时候才会用到。shmid是共享内存的身份id,进行操作的基本都是shmid。共享内存属于用户空间,并不需要使用系统调用就可以直接访问。使用共享内存通信,一方向内存中写入数据,另一方就会立刻看到,共享内存不需要过多的拷贝环节,是通信最快的。但是共享内存缺乏访问控制,会带来并发问题,我们可以使用管道进行控制。

总结

        进程间通信的前提都是让不同的进程看到同一份共同的资源,共享内存会带来一些时序问题,造成数据的不一致问题。我们把多个进程看到的公共资源称为临界资源。我们把自己进程访问临界资源的代码称为临界区。为了更好的保护临界区,可以让多个执行流在任何时候都只有一个执行流进入临界区,这就是互斥。在非临界区,资源之间互不干扰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/128363.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

直播榜单正式上线,超店有数让你数秒内找到热卖直播间和高转化带货达人

众所周知&#xff0c;国内抖音直播带货正处于火爆的状态&#xff0c;大部分电商商家都在抖音通过直播带货实现流量变现。那么TikTok作为国内抖音复制到海外的短视频App&#xff0c;是全球最火爆的App之一&#xff0c;被视为品牌出海的新风口。它的直播变现模式也和抖音大同小异…

ThinkPHP 路由使用

最近在使用ThinkPHP6做项目的开发&#xff0c;故整理了一些常用的路由使用方式&#xff0c; 可以方便之后的使用。 目录 引用路由门面 基础路由 快捷路由 规则表达式 静态地址 静态结合动态地址 方法设置 完全匹配 默认路由规则 设置完全匹配 路由别名 变量规则 局…

再次飙升GitHub榜首?这份“保姆级”的SpringBoot笔记,不服不行

Spring Boot 延续了 Spring 框架的核心思想 IOC 和 AOP&#xff0c;简化了应用的开发和部署&#xff0c;通过少量的代码就能创建一个独立的、产品级别的 Spring 应用。在继承了Spring 一切优点的基础上&#xff0c;其最大的特色就是简化了Spring 应用的集成、配置、开发&#x…

怎样提高美国独立服务器的安全性?

目前&#xff0c;越来越多的用户都开始使用起了美国独立服务器&#xff0c;而这种服务器通常都比共享服务器的安全性要高出许多&#xff0c;但是管理起来比较复杂。下面将为大家介绍怎样才能提高美国独立服务器的安全性&#xff0c;包括下面几点&#xff1a; 1.使用强密码 有时…

队列------数据结构

队列:Queue是一个普通的队列&#xff0c;Deque是一个双端队列 普通的队列:只能是队尾进&#xff0c;队头出; 双端队列:可以从队头进队尾出&#xff0c;也可以从队尾进&#xff0c;队头出&#xff0c;也可以用作一个栈; 1)下面我们来介绍一下Queue的实现方法: 在有容量限制的情况…

你是真的“C”——详解函数递归

详解函数递归运用&#x1f60e;前言&#x1f64c;一、什么是递归&#x1f64c;二、递归运用的两个必要条件&#x1f64c;三、递归与迭代&#x1f64c;总结撒花&#x1f49e;哈喽&#xff01;&#x1f604;各位CSDN的uu们&#xff0c;我是你的博客好友小梦&#xff0c;希望我的文…

长安链 VM Engine架构设计深度解读

VM Engine是长安链智能合约引擎的推荐选型&#xff0c;采用Docker容器化架构&#xff0c;容器内部由一个任务调度器和多个合约进程组成&#xff0c;实现了多合约隔离与多进程并发&#xff0c;支持独立部署&#xff0c;目前支持Golang语言合约。 1. 背景说明 自2009年11月以来&…

CUDA 冬令营1

基本概念 1.CPU的任务&#xff1a;为串行任务优化 2.GPU的任务&#xff1a;为并行任务优化 3.L4T ubuntu&#xff1a;基于NVIDIA Tegra芯片的ubuntu系统&#xff08;linux for Tegra&#xff09; 4.Tegra&#xff1a;继承了ARM CPU 和 NVIDIA GPU的处理器芯片 5.X86&#xff1…

2023京东年货节全民炸年兽活动最详细规则

2023京东全民炸年兽活动规则 1、活动时间 整体活动时间: 2022年12月26日00: 00: 00–2023年01月15日23: 59: 59 2、活动玩法 (1)玩法一:全民炸年兽瓜分10亿压岁钱 活动时间: 2022年12月26日00: 00: 00–2023年01月15日23: 59: 59 (2) 玩法二:每晚8点分百万红包 活动时间…

Houdini和C4D区别在哪?哪个更好用

Houdini和C4D作为当前软件市场上非常热门的设计软件&#xff0c;现在越来越多的小伙伴开始学习。所以咱们今天就从行业应用、建模、动画和使用难易度等进行多方面对比&#xff0c;帮助小伙伴们更清楚地了解这两款软件—— Houdini作为一款非常受欢迎的3D&VFX制作工具&#…

美国公司是如何搞创新的,又是如何失败的......

PARC&#xff0c;施乐帕洛阿图研究中心&#xff0c;和贝尔实验室媲美的IT界圣地。从这里走出了一大批伟大的发明&#xff0c;直接改变了整个计算机行业。Alto PC &#xff0c;图形用户界面&#xff0c;所见即所得&#xff0c;以太网&#xff0c;PostScript&#xff0c;面向对象…

复购高,退货低的日本市场成为跨境电商新风口,新手如何快速入局

据统计预测&#xff0c;未来几年将会有越来越多的跨境卖家涌入日本电商市场。但由于在语言、文化和消费习惯上存在一定的差异&#xff0c;很多中国卖家并不熟悉日本的文化与市场需求&#xff0c;也很难在短时间内快速适应日本电商平台的运营规则与服务。 日本作为一个经济发达的…

【PS-选区编辑】变换选区、反向命令、建立工作路径

目录 变换选区 1、位置 2、多种操作 反向命令 1、反选选区&#xff1a;ctrlshifti 2、边界 3、平滑 4、扩展和收缩 5、羽化 建立工作路径 变换选区 1、位置 制作了一个制作了选区后&#xff0c;鼠标右击找到【变换选区】&#xff0c;或在【选择】菜单中找到【变换…

JavaScript 入门基础 - 运算符(三)

JavaScript 入门基础 - 运算符&#xff08;三&#xff09; 文章目录JavaScript 入门基础 - 运算符&#xff08;三&#xff09;1.什么是运算符2.表达式和返回值3.算术运算符概述4. 赋值运算符5.递增和递减运算符5.1 递增和递减运算符概述5.2 递增运算符5.2.1 前置递增运算符5.2.…

Ajax(1)---了解Ajax和XML

目录 Ajax XML了解 Ajax的特点 HTTP协议请求报文与响应文本结构 HTTP 请求报文 响应报文 Ajax AJAX全称为Asynchronous JavaScript And XML&#xff0c;就是异步的JS和XML通过AJAX可以在浏览器中向服务器发送异步请求&#xff0c;最大的优势:无刷新获取数据。AJAX不是新…

一篇带你彻底弄懂SpringBoot项目jdk版本及依赖不兼容问题

&#x1f49f;&#x1f49f;前言 ​ 友友们大家好&#xff0c;我是你们的小王同学&#x1f617;&#x1f617; 今天给大家打来的是 一篇带你彻底弄懂SpringBoot项目jdk版本及依赖不兼容问题 希望能给大家带来有用的知识 觉得小王写的不错的话麻烦动动小手 点赞&#x1f44d; 收…

MySQL 大表优化方案

单表优化 读写分离 缓存 表分区 垂直拆分 水平拆分 兼容MySQL且可水平扩展的数据库 NoSQL 当MySQL单表记录数过大时&#xff0c;增删改查性能都会急剧下降&#xff0c;可以参考以下步骤来优化&#xff1a; 单表优化 除非单表数据未来会一直不断上涨&#xff0c;否则不…

通过Django发送邮件

通过Django发送邮件非常的简单&#xff0c;在Python中已经内置了一个smtplib邮件发送模块&#xff0c;Django在此基础上进行了简单地封装&#xff0c;我们可以在Django的环境中方便的发送邮件。大部分邮件发送成功主要是邮件的参数配置&#xff0c;本文以常用的126邮箱和QQ邮箱…

《2022中国数据智能产业图谱3.0版》重磅发布

‍数据猿出品本次“数据猿行业盘点季大型主题策划活动——《2022中国数据智能产业图谱3.0版》”为2022年度图谱版本的升级更新版&#xff0c;下一次版本迭代将于2023年4月底发布2023年1.0版&#xff0c;敬请期待&#xff0c;欢迎报名。‍数据智能产业创新服务媒体——聚焦数智 …

svn修改提交日志信息

参考&#xff1a;唐小码个人博客 一、svn修改提交的msg信息和作者信息 鼠标右键找到show log> 选择要修改的日志行&#xff0c;第一个是修改作者信息&#xff0c;第二个是修改日志信息 二、svn修改提交的日期信息 修改日期信息的话&#xff0c;你得先有svn服务器的权限&…