进程通信----命名管道

💬上节内容讲述的是:父子进程可以通过匿名管道的方式进行通信,但匿名管道的致命局限 ------ 只能用于父子进程间通信 ------ 无法实现任意进程间的数据交互。那两个毫不相干的进程想进行通信,或传输数据,该如何呢?

文章目录

一、命名管道

让两个毫不相干的进程进行通信的本质:还是让两个进程看到同一份资源(内存),实际是两个进程看到同一个文件的内容(内容从磁盘拷贝到内存中,即文件内核缓冲区)

至此,引入了命名管道。

命名管道是匿名管道的升级版。在文件系统中创建了一个有名字 的管道文件,打破了亲缘关系的限制,让任意进程都能通过同一个文件路径,实现安全的进程间通信。

讲解管道文件:

Linux支持一种特殊的文件:管道文件(不是普通的文件,普通文件会被刷新到磁盘上)。特点:只会被打开,不需要将内容从内存刷新到磁盘

(只要打开,所有内容按文件来,是内存级)


特性 匿名管道 (pipe) 命名管道 (FIFO)
关系 只能亲缘进程 任意无关进程
文件 无实体文件 有实体管道文件
创建 pipe mkfifo
打开 自动继承 fd 用 open()打开
生命周期 随进程 随文件,可永久存在
通信方式 半双工 半双工
  1. 两个进程,都打开同一个文件,那OS会不会将这个文件 (inode,属性,内容)在内存中加载两次呢?不会的

进程 A和 B都打开同一个文件,他们有自己独立的文件描述符表。但进程B并没有拷贝A的struct file,而是自己有一个不一样的,这是因为这俩进程读写偏移量 f_pos不一样

(若是两个进程共享同一个 struct file:偏移量是同一个变量,A 往后读一点,B 的读写位置也跟着往后跑,互相抢位置、乱套)【匿名管道中,子进程直接拷贝父进程的strut file】



  1. 如何不同的,没有血缘关系的进程,保证看到的就是同一份资源呢?

命名管道 = 一个文件系统路径名 + 一块内核缓冲区

所有进程只要打开同一个路径下的同一个文件,就会自动指向内核里同一块共享内存。

  1. 特点:文件有路径,有名字(基于文件的方式,而且有名字,就决定了这个管道就做命名管道)(匿名管道没有磁盘映像,没有名字,没有路径)

  2. 命名管道如何被看见?

    FIFO是命名管道(a named pipe),mkfifo是在指定路径下创建一个命名管道

    命名管道挂载到文件系统,有路径就能全网可见。

-是普通文件,d是目录,p是管道文件

  1. 两个进程都可以读写,熟练使用read函数和write函数(这两个是系统调用)
cpp 复制代码
//从管道文件里"拿"数据
ssize_t read(int fd, void *buf, size_t count);
//从fd这个被打开的管道文件里,将(最多count)个字节,读到我自己准备好的buf这块内存里

//往管道文件里"放"数据
ssize_t write(int fd, const void *buf, size_t count);
//把buf里的数据,count个字节的内容写到fd这个文件/管道里去
  1. 要使用命名管道来通信,首先得有一个进程创建管道(哪一个进程都可以创建)

int n = mkfifo(const char *pathname,mode_t mode);,然后根据n判断是否创建成功(成功返回0,失败-1)。管道文件fifo就是为了在之后,让两个进程通过管道来通信的。



注意点: mkfifo(path, 0666),最终创建出来的权限不一定是 666,会受系统默认的umask 影响。比如系统 umask 是 022,最终权限会变成:666 - 022 = 644。这是 Linux 安全机制,不影响使用,只是权限更严格。

所以先umask(0);,排除默认umask的影响

  1. 负责创建管道的进程,记得删除管道

在代码中:int unlink(const char *pathname);

在终端的时候,可以unlink fifo来删除管道文件

服务端:创建管道,以读的方式打开

客户端:直接打开管道文件即可,写的方式打开管道文件

  1. 一定要两个端都打开管道才能通信
  2. 如果write方没有打开文件,读方就会堵塞在open内部,直到有人把管道文件打开,open才会返回(基本保证open的时间点一致)
  1. 同时打开两个进程,才能使得成功

  2. 管道通信,简单来理解,就是:新建一种特殊的文件(以p开头,文件被打开是需要载入到内存中的。磁盘上只有文件属性,大小永远 0 字节。管道文件是专门为了通信,数据全程存在内核内存缓冲区,绝不落地磁盘),然后两个进程打开同一个路径下的文件,看/获取同一份资源

二、共享内存

💬管道虽然简单易用,但性能并不是最优的。每次通信都需要经过内核缓冲区,涉及两次数据拷贝。有没有更快的IPC方式呢?答案是共享内存------最快的进程间通信方式!

  1. System V IPC主要有三种通信方式:共享内存、消息队列、信号量;

  2. 共享内存的原理:

动态库(本质上普通文件)加载到内存里,然后映射给进程的地址空间,进程就能看见了。所以动态库可以在内存里实现多进程共享

原理:两个进程都有自己独立的地址空间

第一步:先在物理内存中申请一段空间4KB

第二步:将这段内存映射到进程A的地址空间中:在(进程A的地址空间的)共享区划分出4KB,通过页表映射。进程B同样如此

在双方的应用层,两进程使用各自的虚拟地址的起始地址,完成对物理内存中的公共内存的访问

以上步骤由OS完成,而我们使用系统调用即可

  1. 不再使用共享内存,则
    进程A将该虚拟地址空间free掉,再去掉页表的映射关系,B同样如此。当关联关系被取消,OS会将(没人使用的)共享内存释放掉
  2. 在OS内,会存在多对进程,都使用不同的共享内存通信
  3. 4导致在OS内,有多个共享内存同时存在(有的正在使用,有的新建还没和进程关联,有的正准备释放),所以OS需要使用[ 先描述,后组织 ]的方法管理共享内存

共享内存:一定有:对应的(描述共享内存的)内核结构体对象+物理内存。(结构体包含了共享内存的属性,大小,who创建,when,谁和该内存关联)。管理共享内存 --->管理结构体 --->对链表的增删查改

同时,进程也有内核结构体对象,所以进程和共享内存的关系:两个内核结构体之间的关系

这里可以看出:不单单是在物理内存申请共享内存,同时还要在内核创建描述共享内存的结构体

  1. 当共享内存的引用计数为0,OS就知道该释放掉它了

创建共享内存

  1. 创建共享内存使用的接口
cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

(1)size_t size:指定要创建的共享内存字节数。

(2)int shmflg:IPC_CREAT:如果(目标共享内存)不存在,则新建;存在则直接获取(这个可以用于客户端,不用创建共享内存,只需要获取时使用这个)
IPC_CREAT | IPC_EXCL:强制新建,已存在直接失败。使用这个,则保证:如果shmget成功返回,一定是一个全新的内存(服务端可使用)

(3)返回值:合法的共享内存标识符

  1. 如何让另一个进程也知道共享内存的id呢?

所以,不能由OS来给共享内存申请id。在创建共享内存之前,A和B先约定一个key(用来标识共享内存的唯一性),创建方将key设置进共享内存结构体内部,A和B约定的,它俩都能看见。(key是在用户层构建并传入OS)

(1) 如何评估:目标共享内存是否存在?

创建方使用的是IPC_CREAT | IPC_EXCL:强制新建。

在使用key创建时,如果返回值 ≥0:之前不存在,现在新建成功一个共享内存,返回合法 shmid;返回 -1:共享内存已经存在!(我不覆盖,直接报错)

(2)如何保证两个进程拿到同一个共享内存

两个进程约定同一个key,一个创建,一个查找

内核在 IPC 维护一张共享内存表,以 key 为唯一索引:

进程 A、进程 B:相同的 key 调用 shmget

内核查到同一个 key 对应的 shm 段,返回同一个 shmid

再 shmat 挂载,就映射到同一块物理内存

(3)如何约定一个统一的key呢? 使用ftok

使用算法构建一个冲突概率比较小的key:
key_t key = ftok(const char *pathname, int proj_id);

路径写本路径即可,proj_id自己随便写一个,日常开发直接固定一个整数即可,不用纠结含义,纯区分标识

在创建时,使用key来判断共享内存的唯一性

在使用时,使用的是:shmget的int返回值

(1)key:只有一种用途:在操作系统内部 区分共享内存的唯一性

(2)shmid:(在创建好共享内存之后,会返回标识符shmid)用它来管理共享内存

(3)创建方把key设计进来,而获取方拿着对应的key查对应的共享内存的数据结构体

  1. 如何查看已经创建出来的共享内存:ipcs -m

注意点:

1.共享内存也有权限perms(和文件类似,都有权限问题),共享内存的权限在shmget时设置

2.nattach表示有n个进程和该共享内存关联)

  1. 如何查看已经创建出来的资源:ipcs

  2. 共享内存的资源:声明周期随内核!

文件的生命周期随进程(进程通过 "打开 / 关闭" 控制文件的引用计数,通过 "删除文件名" 切断目录映射,两者共同决定文件的生命周期终点。)

对于共享内存来说,就算进程结束了,只要没有显示地删除共享内存,共享内存资源将一直存在,IPC资源也依旧被占用(共享内存的资源,生命周期随内核)

  1. 共享内存的删除方法:
    (1)终端指令:删除创建的共享内存:ipcrm -m shmid
    (2)代码删除:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    参数shmid:shmget 返回的那个 ID
    参数cmd:让内核做的操作(命令):IPC_RMID:删除共享内存
    参数struct shmid_ds *buf:用来存放 / 设置共享内存信息的结构体指针
cpp 复制代码
// 删除 shmid 对应的共享内存
shmctl(shmid, IPC_RMID, NULL);

代码:

  1. 代码:共享内存的建立&&删除

共享内存映射到进程的地址空间

共享内存弄好之后,就要将共享内存映射到进程A和B的地址空间中。(如何映射:当共享内存创建好之后,共享内存的物理地址也就有了,然后在进程的堆和栈之间(共享区)开辟一段虚拟空间。物理地址、虚拟地址都有了,修改页表,映射关系就有了。可以在进程这返回内存块的起始地址

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

让调用(该系统调用shmat的)进程,它的堆栈之间的映射区和共享内存进行映射。映射成功返回起始虚拟地址(申请内存malloc也是返回虚拟地址)

shmat的作用:把内核里的共享内存段,挂载到当前进程的虚拟地址空间,让进程可以像读写普通内存一样读写共享内存。

(1)shmid:表示想要映射的共享内存

(2)const void *shmaddr:指定挂载到进程的哪个虚拟地址(99% 场景直接填 NULL→ 让内核自动选择合适的地址)

(3)int shmflg(挂载标志)填 0:默认读写模式;SHM_RDONLY:只读挂载(不能写)

未来访问共享内存,有起始虚拟地址+共享内存大小,就可以线性访问共享内存的任意一个字节

共享内存并不是单纯地在物理内存里开辟一块内存即可,它有专门描述共享内存的结构体

共享内存一旦建立映射,通信双方拿到的是共享内存的起始虚拟地址,它访问共享内存就像访问自己的空间一样,直接用就行

如何将进程和共享内存去关联:shmdt,然后再删除共享内存:ipcrm -m 内存的shmid

问题:

  1. 在命名管道中,读写都是使用系统调用read、write(它们是向内核缓冲区写入)。但是在共享内存中读写没有使用,直接向共享内存中写入

堆和栈之间的空间(共享区)属于用户空间,可以让用户直接使用【可知:读写共享内存,并没有出现系统调用】

  1. 之前是两个进程通信,那如果把其中的一个进程换成磁盘呢?
    通过 mmap 系统调用,把磁盘上的文件内容映射到进程的虚拟地址空间,进程以访问内存的方式读写文件内容;
    对映射内存的修改会首先作用于内核页缓存,由内核后台异步延迟刷盘同步到磁盘;
    多个进程映射同一个磁盘文件,共享同一份内存映射,从而实现基于文件的共享内存进程通信。

这也是动态库加载到内存,让进程看到的底层原理(利用:mmap 文件映射 + 共享内存机制,达到:代码段物理内存只保留一份,多进程共享同一块物理内存)

(动态库只读代码段是共享内存;可写数据段用 写时复制 COW,进程修改时单独拷贝一份,互不干扰。)

  1. 管道通信,如果读端打开了,但是写端没有打开,那读端也
    管道自带同步阻塞机制 ,内核帮你做好读写等待(举例:读端已经打开,若写端没打开/没写入数据,那读端就不许读)
    共享内存本身无任何同步机制,只是一块裸内存,必须自己加信号量 / 互斥锁 / 条件变量做同步,否则进程互相干扰,数据乱套(不管有没有数据、对方有没有写完,进程都能直接读写内存地址,立刻执行、绝不阻塞。)也就是共享内存,对于数据没有保护机制

  2. 如何有同步机制呢?可以在共享内存的基础上,再在两进程之间设置一个管道(通过管道来告知读端何时读取0)

三、System V消息队列(用的很少,链接即可)

再来回顾一下IPC 的本质:让进程看到同一份资源。

管道是让进程看到同一个文件、共享内存是让进程看到同一份内存;消息队列是让两个进程看到同一个队列。

在 Linux 原生进程间通信体系中,System V 消息队列是传统且经典的 IPC 机制,它允许互不相关的多个进程,以带类型的消息 为单位,往内核(OS)维护的队列里发送、接收数据,无需管道的字节流流式传输,也不用共享内存手动同步锁,内核负责帮进程排队存储消息、按消息类型精准检索读取,天然支持异步通信、按类型筛选收发,成为单机多进程之间结构化数据异步交互的经典实现方式。

消息队列可以两个朝向的发送消息

  1. 问题1:带类型的消息:

    消息本身是没有类型的,(但为了知道该消息是哪个进程的,哪个消息是A发的,A收哪个类型的消息)我们给消息设置类型,比如:int type = 1或2;

  2. 过程:OS提供一个队列结构,进程A使用某种系统调用,将消息/数据拷贝到队列(为消息新创建一个队列结点,数据放在结点中,进程就把消息交给OS),把所有通信数据放在OS,一个一个的队列结点形成一个消息队列

  3. 数据块的形式

cpp 复制代码
struct msgbuf {
	long mtype; /* message type, must be > 0 */
	char mtext[1];/* message data */
           };

mtype是消息的类型(大于0)。进程A想发送几个消息,就建立几个队列节点。同样进程B也是。那A和B还能认清哪个是自己的数据吗?所以,不能只新建独立结点,还需要进程A,B给自己的数据打上类型标签(主要是为了在一个队列中区分哪一个数据是我要的,哪一个数据是我发的)。

mtext是数据的正文

  1. 消息队列提供了一个(从一个进程)向(另外一个进程)发送一块有类型的数据的方法。
  2. 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
    特性方面:
  3. IPC 资源必须手动删除,否则不会自动清除,除非重启系统;
    所以 System V IPC 资源的生命周期随内核。

操作系统提供一个队列结构(queue),让进程A将发送的数据通过某种调用,将数据拷贝到队列。同时在内核中新建一个队列结点,至此,进程就将数据交给了操作系统。

  1. 但n对进程之间都想进行互传的时候,操作系统里就会有多个消息队列,这个时候OS需要将这些消息队列管理起来(先描述,后管理),在内部一定有信息队列的结构体

  2. 和共享内存一样,想确保两个进程看到的是同一个消息队列 :两进程约定一个key(仍然是ftok),然后由创建者将key设置到消息队列的描述结构体中。共享内存是关联以及去关联,信息队列是发和收

  3. 和共享内存的接口很类似,所以叫做System V 标准

    创建消息队列:msgget

cpp 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
          //IPC_RMID是用来删除
cpp 复制代码
//发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 
//失败返回值是-1        //指定的缓冲区地址   //指定的大小
cpp 复制代码
//读取数据块
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);                                            //读取数据的类型
                                                  
  1. 查看信息队列:ipcs -q
    消息队列的生命周期也是随内核(需要显式的删除)

四、System V信号量(用的很少,链接即可)

信号量重要!但system v版本的不重要

共享内存没有保护机制,会导致数据不一致----解决方案:信号量

数据不一致的原因:

1). 共享资源没有被保护

2). 双方代码都访问了没被保护的共享资源

概念

共享资源:多个进程能够看到的同一份资源(共享资源没有被保护,保护起来就叫临界资源)

无论是多个进程还是多个线程,一旦涉及到并发编程,都绕不开5个概念:

被保护起来的共享资源 --->临界资源

在进程中访问临界资源的程序段 --->临界区

没有访问临界资源的代码 --->非临界区 (保护机制就是保护临界区的代码)

那如何保护临界区代码呢?(互斥)

任意时刻,只允许一个进程(执行流)访问临界资源 --->叫作:互斥

两个进程访问临界区,需要竞争那把锁,获得锁之后再访问。访问时加锁,访问完解锁。可以看出来,锁也要被共享(谁来保证锁的安全)

多个执行流在访问临界资源时,具有一定的顺序性 --->同步

所谓的对共享资源进行保护,本质是对访问共享资源的代码进行保护

原子性:要么做,要么不做(只看结果,学习了考第一名,没学习)

信号量

  1. 举个例子:电影院放映厅有n个位置,将一个大的共享资源(放映厅)变成x个小的共享资源(座位),买票就是对资源的预订机制

回归正题:放映厅也就是共享内存,将共享内存里的空间划分为一小块一小块,进程会分块使用。只要:进程不访问同一个位置;不要让过多的进程都访问 ---->就可以实现进程的并发访问(运行多个进程访问共享内存的不同区域。虽然都在访问共享内存,但是它们访问的不是同一个区域,互不干扰)

通俗来说:信号量就是计数器,用来描述临界资源中,资源数量的多少


流程:每一个进程想访问临界资源(共享内存)的一小部分 ----> 需要申请信号量 (申请成功之后才能访问某一小块资源) --->申请信号量的本质:对资源的预定机制 ;作用:保护临界区


细节1:首先,信号量要能被进程看到才可以。使得信号量本身就是共享资源了

细节2:二元信号量:互斥

  1. 信号量并不是一个简单的整型值int,而是一个结构体

为什么信号量 可以算作通信的范畴?

(不管是命名还是匿名管道,都是传输数据,可以理解是通信。共享内存,两个映射,一个写,一个读,能理解是通信。消息队列,有类型数据块,一个写一个读,也是)信号量好像并没有传递数据,为什么也是通信

2.1 根据刚刚所学,进程拥有独立性,想访问同一份资源,需要让他们看到同一份资源,但进程直接不可能看到同一份资源,所以才有这么多通信方法。为了保护临界资源,需要先访问信号量,每个进程都先看到同一个信号量。 (System V解决这个问题)

2.2 不是传递数据才是通信IPC,通信的本质是(特定信息的传递)。通知/同步/互斥也算。

信号量的接口和系统调用

  1. 信号量(Semaphore):sem
    创建信号量:semget

  1. 信号量,查看:ipcs -s
cpp 复制代码
hpr@hyc-alicloud:~$ ipcs -s
------ Semaphore Arrays --------
    key        semid      owner      perms      nsems 
//自己设置的key, semget的返回值,谁创建的, 权限,    创建了几个信号量
  1. 删除信号量:semctl
    也可以删除信号量:ipcsrm -s semid
cpp 复制代码
int semctl(int semid, int semnum, int cmd, ...);
                                  IPC_RMID
  1. 信号量的PV操作:semop

  2. 信号量的初始值:信号量的初始化值并不是在创建时获取的,而是在调用semctl的时候

  3. OS内有大量的信号量集合,也需要进行管理:先描述,后管理

    信号量在内核中,一定是某种结构体(包含信号量的锁,计数器等等)

  4. 共享内存、消息队列、信号量,这三个 System V IPC 对象,内核维护的结构体格式完全统一(三个结构体:shmid_ds / msqid_ds / semid_ds)。三大结构体首成员全是 ipc_perm 类型成员,ipc_perm内部第一个成员都是key

    共享内存、消息队列、信号量都是用key来表示IPC资源的唯一性

相关推荐
如竟没有火炬2 小时前
至少有K个重复字符的最长子串
开发语言·数据结构·python·算法·leetcode·动态规划
Oll Correct2 小时前
实验二十四:网络地址与端口号转换NAPT
网络·笔记
Mapleay2 小时前
FE-BE 动态路机制之 DPCM 与 DAPM 协作
linux
围巾哥萧尘2 小时前
EchoBird + Codex + DeepSeek:让AI编程触手可及@围巾哥萧尘[特殊字符]EchoBird + Code
经验分享
想带你从多云到转晴2 小时前
优选算法---双指针
java·算法
竹之月2 小时前
【AutoCAD 2020】打印为PDF时卡住——解决方法记录
经验分享·auto cad2020
三品吉他手会点灯2 小时前
C语言学习笔记 - 32.嵌入式C语言学习阶段对初学编程者的建议
c语言·开发语言·笔记·学习
数据皮皮侠AI2 小时前
基于经济学季刊方法测算的中国城市蔓延指数
大数据·人工智能·笔记·数据挖掘·回归
IT大白鼠2 小时前
Linux故障分析与排查:系统日志、启动故障与文件系统修复
linux·运维·服务器