当前位置: 首页 > news >正文

Linux 进程间通信详解

一.进程间通信介绍

1. 进程间通信概念

进程间通信(Inter-Process Communication, IPC)是指在不同进程之间传递或交换信息的一种机制。在操作系统中,进程是资源分配和独立运行的基本单位,它们拥有各自独立的内存空间和系统资源。因此,进程间不能直接访问对方的内存空间,需要通过特定的通信机制来实现数据交换和同步操作。

进程间为什么要通信?进程也是需要某种协同的,并且进程具有独立性。进程 = 内核数据结构 + 代码和数据。

进程如何进行通信?因为进程具有独立性,所以进程间通信的成本会有点高,进程间通信的前提:先让不同的进程,看到同一份(操作系统)资源(“一段内存”)。

· 资源由操作系统提供。

· 我们进程访问空间,进行进程间通信,本质就是访问操作系统,而进程代表的是用户,用户不能直接访问操作系统内核数据,所以操作系统提供了系统调用接口,所以是从操作系统底层设计,从接口设计,一个独立的通信模块IPC —— 隶属文件系统。当进程通信变多,显而易见,操作系统需要将他们管理起来,先描述再组织。OS创建的共享资源的不同,系统调用
接口的不同---进程间通信会有不同的种类!

2.进程间通信的目的

· 数据传输:一个进程需要将它的数据发送给另一个进程

· 资源共享:多个进程之间共享同样的资源。
· 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
· 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。

进程间通信发展

· 管道
· System V进程间通信
· POSIX进程间通信

进程间通信分类

管道
· 匿名管道pipe
· 命名管道
System V IPC
· System V 消息队列
· System V 共享内存
· System V 信号量
POSIX IPC
· 消息队列
· 共享内存
· 信号量
· 互斥量
· 条件变量
· 读写锁

二.管道

什么是管道

· 管道是Unix中最古老的进程间通信的形式。
· 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

它是单向的,即数据只能从一个方向流动。管道分为匿名管道和命名管道(也称为FIFO,即First In First Out)。

匿名管道

  • 1、管道的作用是在具有亲缘关系的进程之间传递消息,所谓有亲缘关系,是指有同一个祖先。管道并不是只可以用于父子进程通信,也可以在兄弟进程之间还可以用在祖孙之间等,反正只要共同的祖先调用了pipe函数,打开的管道文件就会在fork之后,被各个后代所共享;
  • 2、管道是字节流通信,没有消息边界,多个进程同时发送的字节流混在一起,则无法分辨消息,所有管道一般用于2个进程之间通信;
    • 流:相当于水流,写入数据时,写多少字节和你自己有关系,读的时候没有 格式的要求,完全取决你自己;
    • 字节流:以字节来读取和写入,字节数的大小完全取决于自己
  • 3、管道的内容读完后不会保存;
  • 4、管道是单向的,一边要么读,一边要么写,不可以又读又写,想要一边读一边写那就得创建2个管道

  • 5、管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。
  • 6、管道内部自带同步机制:子进程写一条,父进程读一条
  • 7、当进程退出之时,管道也随之释放,与文件保持一致

 匿名管道原理:

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);参数
fd:文件描述符数组,其中 fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{int fds[2];if(pipe(fds) < 0){//创建一个管道,用于父子间进行通信perror("pipe");return 1;}char buf[1024];//临时数组,用于存放通信的消息printf("Please enter:");fflush(stdout);//对标准输出流的清理,但是它并不是把数据丢掉,而是及时地打印数据到屏幕上ssize_t s =  read(0,buf,sizeof(buf)-1);//0对应文件描述符if(s > 0){//判断读取的字节数buf[s] = 0;}pid_t pid = fork();//fork()子进程if(pid == 0){//子进程只写,关闭读端close(fds[0]);while(1){sleep(1);write(fds[1],buf,strlen(buf));//将buf的内容写入管道}}else{//父进程只读,关闭写端close(fds[1]);char buf1[1024];while(1){ssize_t s = read(fds[0],buf1,sizeof(buf1)-1);//从管道里读数据,放入bufif(s > 0){buf1[s-1] = 0;printf("client->farther:%s\n",buf1);}}}
}

管道的本质

在内核角度:看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。

管道是有固定大小的 ,在不同内核里大小有差别,可使用 ulimit 指令查看对很多重要资源的限制,进程可打开最大文件数、管道大小等等

ulimit -a

管道读写的四种情况:

1.如果管道内部是空的 && write fd没有关闭,读取条件不具备,读进程会被阻塞 --- wait -> 当读取条件具备 <- 写入数据

2.管道被写满 && read fd不读且没有关闭,管道被写满,写进程会被阻塞(管道被写满-写条件不具备) --- wait --> 当写条件具备 <-- 读取数据
3.管道一直在读&&写端关闭了wfd,读端read返回值会读到0,表示读到了文件结尾

4.rfd直接关闭,写端wfd一直在进行写入:写端进程会被操作系统直接使用13号信号关掉。相当于进程出现了异常子进程

管道的5种特征:

1.匿名管道:只用来进行具有血缘关系的进程之同,进行通信,常用与父子进程之间通信

2.管道内部,自带进程之间同步的机制(多执行流执行代码时,具有明显的顺序性)

3.管道文件的生命周期随进程

4.管道文件在通信的时候,是面向字节流的.write的次数和读取的次数不是一一匹配的

5.管道的通信模式,时一种特殊的半双工数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

命名管道

概念:

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

匿名管道是通过子进程继承父进程实现的看到同一份资源,而命名管道是通过 路径+文件名 确定同一份资源,该文件只存一份数据,即一份inode、一份文件缓冲区、一份操作方法集。

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo myfifo

创建出来的文件的类型是p,代表该文件是命名管道文件。

例如:我们打开两个终端,一个终端持续向命名管道追加写入字符串,另一个终端cat命名管道,两个终端靠命名管道实现echo进程与cat进程的通信

echo 指令并不在左边终端打印,而是从命名管道myfifo传到右边终端的cat进程,并且在打印过程中命名管道大小是不变的(因为命名管道不会将通信数据刷新到磁盘当中),进行交流信息的进程中只想用文件缓冲区来交流,只需一个进程把数据放到缓冲区,另一个进程去拿就够了,如果是磁盘文件,它就需要刷盘,管道文件不需要刷到磁盘,因为是一个内存级文件,所以即使追加写到命名管道,它的属性inode也不会改变,因为不会刷到磁盘。

命名管道与匿名管道几乎完全相同,不同的一点就是命名管道可以让毫不相干、没有血缘关系的进程进行通信

命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *filename,mode_t mode);
int main(int argc, char *argv[])
{mkfifo("p2", 0666); return 0;
}

pathname:表示要创建的命名管道文件。

                若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。

                若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。

mode:表示创建命名管道文件的默认权限。

          将mode设置为0666,则命名管道文件创建出来的权限:prw-rw-rw-  具体权限会受到umask掩码的影响(0002)

mkfifo函数的返回值。

  • 命名管道创建成功,返回0
  • 命名管道创建失败,返回-1

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时:

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

命名管道实现的通信:https://gitee.com/lin-xi-meets-deer/cpp-language/commit/a046327690fd9b33c3725e50b883e1421f5e9f7a

三.system V

共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

直接原理:

可以分为三步

  1. 申请内存
  2. 将这块内存挂接到进程地址空间
  3. 返回首地址

并且不能由进程自己malloc内存,因为进程是独立的,需要操作系统创建内存空间,创建信道,才可以实现不同进程通信。所以需求方进程要求执行方操作系统完成任务的过程就是系统调用

系统调用接口: shmget

 #include <sys/ipc.h>#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);

size

他是创建共享内存的大小 

单位是字节

shmflg

我们知道,我们这个共享内存创建出来了以后,就不需要再次创建了,只需要获取即可

所以就注定了我们需要如何创建,如何获取。

如果需要关注的选项就是下面两个

IPC_CREAT(单独使用)的意思是创建一个共享内存,如果这个共享内存存在,直接获取,如果不存在就创建并返回他。

IPC_CREAT | IPC_EXCL(两个一起使用):如果申请的共享额你存不存在,就创建,如果存在就出错返回。它可以确保如果我们申请成功了,这个共享内存一定是一个新的。

IPC_EXCL不单独使用,,或者说单独使用没意义。

对于它的返回值

image-20240122163219388

如果成功,他会返回一个共享内存的标识,如果失败,返回-1 

key

  • 这个key是一个数字,这个数字几,不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识.
  • 第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key,就可以和第一个进程看到同一个共享内存了!
  • 对于一个已经创建好的共享内存,那么key在共享内存的描述对象中!
  • 第一次创建的时候,必须有一个key了,这个key怎么有呢?key --类似于 --路径 ,具有唯一性

总的来说就是

  • 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
  • 第二个参数size,表示待创建共享内存的大小。
  • 第三个参数shmflg,表示创建共享内存的方式。

我们为保证让不同的进程看到同一个共享内存我们用fotk,通过相同的参数调用ftok函数,获取相同的Key值。传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取。

key_t ftok(const char *pathname, int proj_id);

 ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。

ftok是一套算法,用路径名和项目id进行数值计算,获得冲突概率极低的数字 。

  1. 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
  2. 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。

在Linux中,查看系统内IPC的指令

ipcs

单独使用 ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。

共享内存挂接到虚拟进程地址空间中

需要一个接口:shmat:

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

· 第一个参数shmid:表示待关联共享内存的用户级标识符。应用层都是以shmid为准。
· 第二个参数shmaddr:指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。最终的挂接位置会被返回。
· 第三个参数shmflg:表示关联共享内存时设置的某些属性。挂接时按什么权限,在创建共享内存时的权限就可以了,不用再修改,所以可以从传0。

其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:

SHM_RDONLY关联共享内存后只进行读取操作
SHM_RND若shmaddr不为NULL,则关联地址自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr%SHMLBA)
0

默认为读写权限

shmat函数的返回值说明:

  • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
  • shmat调用失败,返回(void*)-1。

去掉关联

 

int shmdt(const void *shmaddr);

shmdt函数的参数说明:

  • 待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

shmdt函数的返回值说明:

  • shmdt调用成功,返回0。
  • shmdt调用失败,返回-1。

去掉共享内存的关联,如果直接进程退出,那么进程会释放它的进程虚拟地址空间,和页表映射的物理内存,所以此时共享内存的引用计数--,ipcs -m 就可以查看到 nattck (挂接数)减1

将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。

系统释放共享内存

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

 shmid:由shmget返回的共享内存标识码
 cmd:将要采取的动作(有三个可取值)
 buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:

选项作用
IPC_STAT获取共享内存的当前关联值,此时参数buf作为输出型参数
IPC_SET在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值
IPC_RMID删除共享内存段

 命令释放共享内存 

在shell中使用命令释放共享内存 

ipcrm

ipcrm -m 共享内存shmid

注意: 指定删除时使用的是共享内存的用户层id,即列表当中的shmid。用户层统一使用shmid,命令行输入也是用户层 

 共享内存通信的简单实现:https://gitee.com/lin-xi-meets-deer/cpp-language/commit/48e585423d587e2eca5dddc0c76323a731d7e1e0

 消息队列

消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,这两个进程向对方发数据时,都在消息队列的队尾添加数据块,这两个进程获取数据块时,都在消息队列的队头取数据块。

消息队列当中的某一个数据块是由谁发送给谁的,取决于数据块的类型

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

消息队列的创建

msgget

int msgget(key_t key, int msgflg);

参数接口几乎与共享内存完全相同,但是不用传size

  • 创建消息队列也需要使用ftok函数生成一个key值,这个key值作为msgget函数的第一个参数。
  • msgget函数的第二个参数,与创建共享内存时使用的shmget函数的第三个参数相同。
  • 消息队列创建成功时,msgget函数返回的一个有效的消息队列标识符(用户层标识符)。
消息队列的释放

msgctl

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • msgctl函数的参数与释放共享内存时使用的shmctl函数的三个参数相同,只不过msgctl函数的第三个参数传入的是消息队列的相关数据结构。 

向消息队列发送数据 

msgsnd

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示待发送的数据块。
  • 第三个参数msgsz,表示所发送数据块的大小
  • 第四个参数msgflg,表示发送数据块的方式,一般默认为0即可。

返回值:

  • msgsnd调用成功,返回0。
  • msgsnd调用失败,返回-1。

msgsnd函数的第二个参数必须为以下结构:

struct msgbuf
{long mtype;       /* message type, must be > 0 */char mtext[1];    /* message data */
};
从消息队列获取数据

msgrcv

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数:

  • 第一个参数msqid,表示消息队列的用户级标识符。
  • 第二个参数msgp,表示获取到的数据块,是一个输出型参数。
  • 第三个参数msgsz,表示要获取数据块的大小
  • 第四个参数msgtyp,表示要接收数据块的类型。

返回值:

  • msgsnd调用成功,返回实际获取到mtext数组中的字节数。
  • msgsnd调用失败,返回-1。

信号量

信号量的基本概念

定义:信号量是一种用于保证两个或多个关键代码段不被并发调用的机制。线程在进入一个关键代码段之前,必须先获取一个信号量;一旦该关键代码段执行完成,线程必须释放信号量。
类型:信号量主要分为计数信号量(Counting Semaphore)和二元信号量(Binary Semaphore)或称为互斥量(Mutex)。计数信号量允许多个线程访问一定数量的资源,而二元信号量则只允许一个线程访问资源。
核心操作:信号量的核心操作包括P操作(也称为wait或down操作)和V操作(也称为signal或up操作)。P操作会使信号量的值减1,如果信号量的值已经为0,则调用线程将会被阻塞;V操作则使信号量的值加1,如果有线程因为信号量值为0而被阻塞,则这些线程会被唤醒。

共享内存中,如果进程A正在写入10个数字,只写了一部分,例如5个数字,此时进程B直接就拿走了这5个数字,但是这10个数字必须组合在一起才有意义,所以B拿了也没用,这就会导致数据不一致问题,即A发的数据和B收的数据不一致,这就是没有互斥的弊端。而管道就不会出现这种问题,因为管道数据有原子性

        1. A、B看到的同一份资源——共享资源,如果不加保护,会导致数据不一致问题

        2. 加锁 -- 互斥访问 -- 任何时刻只允许一个执行流访问共享资源 -- 互斥 

        3. 共享资源,任何时刻只允许一个执行流访问的资源——临界资源 --- 一般是内存空间

        4. 如果有100行代码,其中只有5-10行在访问临界资源(访问IPC资源都是代码干的),那么我们访问临界资源的代码叫做临界区

多进程在显示器上打印的数据是错乱的、还会和命令行混在一起,因为显示器也是被多个进程共享的共享资源,没有互斥保护所以各打各的

信号量原理

将临界资源整合,作为一个整体,此时就是互斥原理

信号量是描述临界资源中资源数量的多少,我们怕的是多个执行流访问同一个资源,所以引入计数器,当计数器为0时,再有执行流申请资源,就不会同意了

但是信号量计数器也是共享资源!它的目的是保护别人的安全,但是前提是它自己是安全的! 

  所以信号量的申请和释放是原子的!要么不做,要做就做完,是两态,没有正在做的情况

信号量本质是一把计数器,信号量的申请和释放(PV)操作是原子的
执行流申请资源,必须先申请信号量资源,得到信号量之后才能访问临界资源
信号量值为0或1两态的是二元信号量,对应互斥功能
申请信号量的本质就是对临界资源的预定机制,就是对计数器的--,即P操作
释放资源,释放信号量,本质是对计数器的++,即V操作

  • 通信不仅仅是通信数据,互相协同也算通信
  • 协同本质也是通信,信号量首先要被所有的通信进程看到,即不同的进程看到同一份资源

信号量不能用来通信资源,它是来帮助通信的 

信号量的优缺点:

  • 优点
    • 灵活性强:信号量可以用于多种同步场景,如进程同步、资源管理和死锁预防。
    • 可扩展性:信号量可以扩展为计数信号量,用于管理多个同类型资源的并发访问。
  • 缺点
    • 编程复杂度:信号量的使用需要开发者仔细设计同步逻辑,避免出现死锁、优先级反转等问题。

相关文章:

  • 鼠标移动操作
  • C++原码、反码和补码
  • YuE本地部署完整教程,可用于ai生成音乐,歌曲
  • 6. 话题通信 ---- 使用自定义msg,发布方和订阅方cpp,python文件编写
  • Linux cmp 命令使用详解
  • Python语法系列博客 · 第8期[特殊字符] Lambda函数与高阶函数:函数式编程初体验
  • git合并分支并推送
  • FPGA系列之DDS信号发生器设计(DE2-115开发板)
  • firewalld 防火墙
  • 从零开始学A2A五:A2A 协议的安全性与多模态支持
  • 第三届世界科学智能大赛新能源赛道:新能源发电功率预测-数据处理心得体会1
  • 压滤机与锡泥产生效率
  • 解决echarts饼图label显示不全的问题
  • Keil MDK中禁用半主机(No Semihosting)
  • LINUX419 更换仓库(没换成)find命令
  • 深度补全网络:CSPN++ 有哪些开源项目
  • FFUF指南
  • 【langchain4j】Springboot如何接入大模型以及实战开发-AI问答助手(一)
  • C 语 言 --- 指 针 4(习 题)
  • [Java EE] Spring AOP 和 事务
  • 在全社会营造浓郁书香氛围,上海市全民阅读工作会议召开
  • 北京航空航天大学强基计划今年新增4个招生培养方向
  • 习近平会见柬埔寨国王西哈莫尼
  • 言短意长|大学本科招生,提前抢跑
  • 核观察|为核潜艇打造“安全堡垒”,印度系统性提升海基核威慑力
  • 西藏旅游:2024年营业收入2.13亿元,出入境商旅业务激增378%