深入理解Linux进程间通信IPC -- 管道,共享内存,消息队列,信号量

进程间通信

每个进程都有自己独立的虚拟地址内存空间,因此不同的进程具有独立性,一般情况下,是不会互相影响的。但是不同进程经常需要进行数据传输或者数据共享等一些操作。
于是便引出了进程间通信IPC这个概念。

进程间通信的作用和目的:

  1. 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

  2. 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  4. 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

  5. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

IPC方式有以下几种:
管道 – 匿名管道,命名管道、系统IPC – 信号量、消息队列、共享内存 和套接字。
其中管道和消息队列主要用于数据传输,共享内存主要用于数据共享,信号量用于进程控制方面的一些操作。
本片文章对管道,共享内存,消息队列和信号量进行剖析。套接字在本人的网络部分文章中进行说明。

管道

操作系统分为内核态和用户态,管道就是在内核中开辟一块缓冲区,不同的进程通过对这个缓冲取进行读写操作实现IPC。

管道其实有三种不同的形式,

  • 匿名管道,半双工通信,只能在父子或者兄弟进程间使用.,
  • 命令流管道s_pipe: 全双工。父子兄弟间使用。
  • 命名管道FIFO:半双工通信。任意进程间使用。

下面对匿名管道和命名管道进行详细介绍。

匿名管道

匿名管道是在内核中开辟一块缓冲区,而这块缓冲取没有具体的标识符,因此只能用于具有亲缘关系的进程间通信。
子进程可以通过复制父进程获得这块缓冲取(管道)的操作句柄,因为父进程创建管道的时候会接收到管道的操作句柄(文件描述符)。
这也是linux下一切皆文件的一种体现。

使用pipe函数进行创建:

int pipe(int fd[2]);

由参数fd返回两个文件描述符,fd[0]为读而打开 fd[1]为写而打开
通过这两个读写的端口就轻松实现了同步与互斥。
以代码为例:
在这里插入图片描述
运行结果:
在这里插入图片描述

上面的操作通过父子进程间的匿名管道实现了父子进程间的通信,父进程写入数据子进程读取。

关于匿名管道需要注意的是管道的读写特性

  • 管道无数据,调用read阻塞
  • 管道数据满了,调用write阻塞
  • 管道所有读端被关闭,继续调用写会导致异常进程退出
  • 管道所有写端被关闭,继续调用读,当读完管道中数据的时候,会返回0

命名管道

命名管道也是内核中的一块缓冲区,但是这块缓冲区有具体的标识符,不同进程可以通过标识符来对管道进行操作,从而达到通信的目的。

可以通过命令 mkfifo创建管道文件。

也可以通过代码 int mkfifo(char* filename , mode_t mode)
参数一是管道文件名,参数二是创建文件的一些权限,与IO操作类似
成功返回0,失败返回-1.
在这里插入图片描述

在这里插入图片描述

运行结果:
在这里插入图片描述
在这里插入图片描述

注意:当我们通过open打开这个命名管道时,

  • 若文件以只读方式打开则会阻塞直到以只写方式打开。
  • 若文件以只写方式打卡则会阻塞直到以只读方式打卡。

总结管道特性。

  1. 管道是半双工通信
  2. 匿名读写特性与命名管道的打开特性。
  3. 生命周期随进程(当没有手动释放时)
  4. 管道提供字节传输服务 – 可靠,有序,基于连接的传输服务。
  5. 自带同步与互斥。
    同步:通过条件判断实现对临界资源操作的合理性。
    互斥:通过唯一访问实现对临界资源操作的安全性。

共享内存

共享内存的最大特性: 最快的进程间通信方式。

共享内存的本质是直接在物理空间上开辟的一块物理内存,而非pcb的虚拟内存,多个进程可以将自己的虚拟地址映射到这块内存上面从而达到通信的目的,相比于其他方式,很明显,这种方式有效的降低了输入输出数据的拷贝次数,从而降低了效率。

映射方式如图: 中间是共享内存,两边分别是不同的进程通过页表映射。
在这里插入图片描述
共享内存的操作流程:

  1. 创建共享内存 – 在物理内存上开辟空间。
  2. 进程将自己的共享内存映射到自己的虚拟地址空间。
  3. 进行基本的IO操作
  4. 操作完毕,接触映射关系
  5. 释放内存空间。

函数说明:

shmget函数
该函数用来创建共享内存

int shmget(key_ t key, size_t size, int shmflg);
  • 第一个参数,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.

  • 第二个参数,size以字节为单位指定需要共享的内存容量

  • 第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样

shmat函数
把共享内存连接到当前进程的地址空间。

void *shmat(int shm_id, const void *shm_addr, int shmflg);
  • 第一个参数,shm_id是由shmget函数返回的共享内存标识。
  • 第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
  • 第三个参数,shm_flg是一组标志位,通常为0。

shmdt函数
解除映射关系
注意,不是删除它,只是使该共享内存对当前进程不再可用。

int shmdt(const void *shmaddr);
  • 参数shmaddr是shmat函数返回的地址指针,
  • 调用成功时返回0,失败时返回-1.

shmctl函数
对共享内存进行一些特别的操作,比如删除。

注意:
删除共享内存只是将其状态置为销毁状态,为了不让其他进程连接,当这个共享内存的attach数为0的时候才会在内存中真正删除。

int shmctl(int shm_id, int command, struct shmid_ds *buf);
  • 第一个参数,shm_id是shmget函数返回的共享内存标识符。

  • 第二个参数,command是要采取的操作,它可以取下面的三个值 :
    IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    IPC_RMID:删除共享内存段

  • 第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。不用置为NULL。

我们写一个代码用于实现共享内存的通信方式:
在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述
运行结束之后查看共享内存可以发现我们创建的这一个共享内存。
于此也可以得出共享内存的生命周期随内核。并不是进程。
在这里插入图片描述
注意:
共享内存自带同步与互斥,因此需要用户手动去添加。否则多进程访问时可能会出现安全问题。

消息队列

消息队列本质上是一个内核中的优先级队列,多个进程通过访问同一个队列,进行节点的添加或者获取来实现通信。
在这里插入图片描述
操作流程:

  1. 创建消息队列:int msgget(key_t key, int msgflg)
  2. 添加节点:int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
  3. 获取节点:size_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp,int msgflg)
  4. 删除消息队列:int msgctl(int msqid, int cmd, struct msqid_ds *buf);

当获取节点的时候我们发现参数多了一个 long msgtyp,因为数据在消息队列中以结构体为类型存储的,结构体中有很多成员,其中一个就是这个参数,因此获取时我们要自己定义,从而回去我们想要的数据。struct msgbuf 中有两个必须指定的成员就是long msgtyp,和char mtext[1].

消息队列的有两个特性:

  • 自带同步互斥。
  • 生命周期随内核。

信号量

信号量时一种实现进程间同步与互斥的方式,因为共享内存并不提供同步与互斥,因此需要信号量帮助。

信号量本质上是一个计数器加上一个pcb等待队列和一些让进程等待/唤醒接口构成的。

同步实现:
通过计数器对资源进行计数, 计数器大于一可以访问,小于等于一不能访问,加入等待队列,等到有资源唤醒。

互斥实现:
计数器的数值永远不大于一,同一时间只有一个进程给可以访问。

如此便实现了同步与互斥。

猜你喜欢

转载自blog.csdn.net/ifwecande/article/details/107470579