Linux:进程间通信

文章目录

  • 前言
  • 一、进程间通信介绍
    • [1.1 进程间通信的目的](#1.1 进程间通信的目的)
    • [1.2 进程间通信的发展与分类](#1.2 进程间通信的发展与分类)
  • 二、管道
    • [2.1 匿名管道原理](#2.1 匿名管道原理)
    • [2.2 通信管道会出现的情况和特性(重要)](#2.2 通信管道会出现的情况和特性(重要))
    • [2.3 命名管道](#2.3 命名管道)
      • [2.3.1 命名管道与匿名管道的区别](#2.3.1 命名管道与匿名管道的区别)
  • [三、system V](#三、system V)
    • [3.1 共享内存原理](#3.1 共享内存原理)
    • [3.2 键值](#3.2 键值)
      • [3.2.1 键值生成原理](#3.2.1 键值生成原理)
    • [3.3 共享内存相关接口函数创建信道](#3.3 共享内存相关接口函数创建信道)
    • [3.4 简略介绍其余IPC和代码结构(仅作了解)](#3.4 简略介绍其余IPC和代码结构(仅作了解))
      • [3.4.1 消息队列](#3.4.1 消息队列)
      • [3.4.2 代码数据结构](#3.4.2 代码数据结构)
  • [四、system V 信号量](#四、system V 信号量)
  • [五、内核中的system V资源管理](#五、内核中的system V资源管理)
  • 总结

前言

进程间通信其实就是让不同进程看到同一份资源的思想,然后设计不同的模式或者数据结构来达到这一目的


一、进程间通信介绍

1.1 进程间通信的目的

由于进程地址空间的独立性设计,我们在面对下面情况时需要一种机制来实现进程间的交互,这便是进程间通信的目的

  • 数据传输:⼀个进程需要将它的数据发送给另⼀个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进
    程终⽌时要通知⽗进程)。
  • 进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够
    拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。

1.2 进程间通信的发展与分类

  • 管道
  1. 匿名管道pipe
  2. 命名管道
  • System_V进程间通信
  1. System_V 消息队列
  2. • System_V 共享内存
  3. • System_V 信号量
  • POSIX进程间通信
  1. 消息队列
  2. 共享内存
  3. 信号量
  4. 互斥量
  5. 条件变量
  6. 读写锁

二、管道

2.1 匿名管道原理

管道是Unix最古老的进程间通信方式,管道是在内核空间中开辟的,管道的数据存储在内核的一个环形缓冲区中(因此匿名管道的本质是由内核在内存中维护的一个文件)!一般发生在父子进程的通信使用中
当父进程创建子进程的时候,会进行数据上的浅拷贝,所以子进程也拥有父进程相同的指针,它们共同指向相同的文件(也就是拥有相同的文件描述符表),因此它们能使用同一个管道,并且管道是单向的,意味着两个进程只能有一个能读,另一个只能写

cpp 复制代码
#include <iostream>
#include<unistd.h>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;

void ChildWrit(const int wfd)
{
    char buffer[1024];
    int cnt=0;
    while(true)
    {
        //snprintf是C语言的字符串处理函数,在需要读取的字符串末尾会自动添加/0,
        //而下面的read函数是系统级别的函数,所以在处理字符串的时候需要我们需要预留一个字节空间来添加/0
        snprintf(buffer,sizeof(buffer),"i am child,witing now,my pid:%d,cnt:%d",getpid(),cnt++);
        write(wfd,buffer,strlen(buffer));
        sleep(1);
    }

}
void FatherRead(const int rfd)
{
    char buffer[1024];

    while(true)
    {
        buffer[0]=0;
        ssize_t n=read(rfd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"child say:"<<buffer<<endl;
        }
        else if(n==0)
        {
        }
    }
}
int main() {
    //创建管道
    int fds[2]={0};
    int n=pipe(fds);
    if(n<0)
    {
        cout<<"pipe error"<<endl;
    }
    else
    {
        cout<<"fds[0]"<<fds[0]<<endl;
        cout<<"fds[1]"<<fds[1]<<endl;
    }
    //创建子进程
    //关闭读写端形成单向通信信道
    pid_t id=fork();
    if(id==0)
    {
        close(fds[0]);
        //子进程在写的时候,父进程会阻塞在read函数上面等待读取
        ChildWrit(fds[1]);

        exit(0);
    }

    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(id,nullptr,0);
 
    cout<<"test pipe"<<endl;
    return 0;
}

2.2 通信管道会出现的情况和特性(重要)

  1. 读的慢,写端要等读端
  2. 读端关闭,写端收到SIGPIPE信号直接终止
  3. 写端不写或写的慢,读端要等写端
  4. 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾

其中情况1,2,3都是基于Linux下的read,write等函数的特性进行的,而情况4则是进程本身的管理

2.3 命名管道

上面匿名管道能进行带有血缘关系进程的通信,那么接下来的命名管道就可以让两个毫不相关的进程进行通信。
命名管道的性质除了能让任意进程间通信外,与匿名管道基本一致,即创建一个文件(与匿名管道的区别是在磁盘上创建的文件,而匿名管道是内核上创建)一个进程往文件中写数据,一个进程读数据,且不让文件内容刷新到磁盘上(这也是磁盘上的普通文件与管道文件的区别),从而实现任意进程间的通信。

以下是有关在代码层上如何创建一个管道文件,它使用mkfifo()函数,其实在我们用户层上,所谓的管道文件也只不过是特殊的文件罢了,相较于在代码中对普通文件的操作,我们操作有名管道只不过多了创建这一步。

当一个文件打开命名管道时,无论是读写端,如果另一端没有打开,那么这个文件就会阻塞(卡)在open上!

2.3.1 命名管道与匿名管道的区别

• 匿名管道由pipe函数创建并打开。

• 命名管道由mkfifo函数创建,打开⽤open

• FIFO(命名管道)与pipe(匿名管道)之间唯⼀的区别在它们创建与打开的⽅式不同,⼀但这些⼯作完成之后,它们具有相同的语义。

三、system V

system V是一套 Unix/Linux 系统中进程间通信的标准化机制,,共享内存区是最快的IPC形式。⼀旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执⾏进⼊内核的系统调⽤来传递彼此的数据。

3.1 共享内存原理

共享内存实际上就是通过一定的技术手段来将两个进程的虚拟地址空间进行相连,我们只需要执行一次系统级别上的调用,创建完共享内存后,那么两个进程就能直接传递数据而不依靠内核。

共享内存没有保护机制(对共享内存中数据的保护),就是两个进程间的数据并不同步。system V 体系通信创建的共享内存它的生命周期随操作系统,所以我们必须手动删除我们创建的共享内存

3.2 键值

在创建信道的过程中我们需要通过某种约定,来达到两个进程可以连接同一块内存的目的,以此键值出现

键值的实现通常依赖于操作系统提供的函数ftok()【file to key】

函数原型:

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>

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

它通过传入文件路径pathname(如"."),以及proj_id(项目标识符,用于在同一文件的不同场景中生成不同键值),成功时返回生成的 key_t 类型键值,失败返回 -1,并设置 errno(如文件不存在)。失败返回 -1,并设置 errno(如文件不存在)

3.2.1 键值生成原理

  1. 提取文件的 inode 号设备号
  2. proj_id 的低 8 位与 inode 号的低 16 位、设备号的低 16 位组合:key = (proj_id & 0xFF) << 24 | (st_dev & 0xFF) << 16 | (st_ino & 0xFFFF)

3.3 共享内存相关接口函数创建信道

我们学习共享内存并理解只需要将相关函数理解就能大概明白其原理方向

函数 作用&&用例
shmget 创建或获取共享内存段 int shmid = shmget(key, size, IPC_CREAT 0666);
shmat 将共享内存段附加到进程的地址空间 void *shm_ptr = shmat(shmid, NULL, 0);
shmdt 将共享内存段从进程地址空间分离 hmdt(shm_ptr);
shmctl 控制共享内存段(删除、查询信息等) shmctl(shmid, IPC_RMID, NULL);

shmget 参数:

  • key: 共享内存的唯一标识符,通常用 ftok("path", id) 生成。
  • size: 共享内存段大小(字节)。在操作系统中通常为4kb的整数倍大小,但在个人操作空间上是给多少显示多少,操作系统内部实际向上取整
  • flags: 权限标志(如 IPC_CREAT | 0666)。
    取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
    取值为IPC_CREAT | IPC_EXCL(不单独存在):共享内存不存在,创建并返回;共享内存已存在,出
    错返回。

|-------------------------------------------------------------------------------------------------------------------------|
| key值是共享内存的"身份证",它是进程间的约定,使不同进程事先通过相同key找到同一块共享内存,属于操作系统层面,而开辟完之后的返回值shmid则是用户层面上操作这块共享内存的标识符,这样做的目的是将用户和操作系统分层以实现解耦合的目的 |

shmat 返回值:使用标识符将开辟的共享空间关联到虚拟地址。成功返回共享内存的起始地址,失败返回 (void*)-1

参数:

shmid: 共享内存标识

shmaddr:指定连接的地址,通常用null让系统自动返回一个地址

shmflg:一般设置为0,使用shmget中设置的缺省权限

返回值:成功返回⼀个指针,指向共享内存第⼀个节;失败返回-1
shdt:去关联
shmctl 命令:

IPC_RMID: 标记共享内存段为待删除(当所有进程分离后实际删除)。

IPC_STAT: 获取共享内存段的状态信息。
成功返回0,失败返回-1

3.4 简略介绍其余IPC和代码结构(仅作了解)

3.4.1 消息队列

消息队列的技术已经是落后的了,对于我们初学者来说,已经失去了学习的意义,所以仅作了解

消息队列提供了⼀个从⼀个进程向另外⼀个进程发送⼀块数据的⽅法

• 每个数据块都被认为是有⼀个类型,接收者进程接收的数据块可以有不同的类型值

• 特性⽅⾯

◦ IPC资源必须删除,否则不会⾃动清除,除⾮重启,所以system_V IPC资源的⽣命周期随内核

其中接口函数不作描述。

3.4.2 代码数据结构

cpp 复制代码
struct msqid_ds 
{ 
    struct ipc_perm msg_perm;     /* Ownership and permissions */
    time_t          msg_stime;    /* Time of last msgsnd(2) */
    time_t          msg_rtime;    /* Time of last msgrcv(2) */
    time_t          msg_ctime;    /* Time of last change */
    unsigned long   __msg_cbytes; /* Current number of bytes in
                                     queue (nonstandard) */
    msgqnum_t       msg_qnum;     /* Current number of messages
                                     in queue */
    msglen_t        msg_qbytes;   /* Maximum number of bytes
                                     allowed in queue */
    pid_t           msg_lspid;    /* PID of last msgsnd(2) */
    pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

其中第一个结构体的数据结构如下

cpp 复制代码
struct ipc_perm 
{
   key_t          __key;       /* Key supplied to msgget(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 */
   unsigned short __seq;       /* Sequence number */
};

操作系统通过维护这种数据结构体来然后通过函数来进行通信

四、system V 信号量

|---------------------------------------------------------------------------------------------------------------------------------|
| 通信的前提是看到同一份资源,那么在对资源进行读写的时候,如果一方开始读了但另一方仅仅把想传输的资源写了一半,那么就会发生数据不一致的情况,但是如果我们加入一种保护机制,例如管道里面通过write和read函数的特性进行阻塞,就可以控制并达到想要的通信结果 |

4.1 并发编程,铺垫概念

在了解怎么去保护之前,我们需要知道一些概念

  • 临界资源:被多个执行流同时访问的资源,一次只允许一个进程使用。比如管道、共享内存、消息队列、信号量。
  • 临界区:进程中访问临界资源的代码(和临界资源配套)为了保护数据安全,就要把临界区保护起来,就有了信号量。
  • 原子性:一件事情要么做完,要么不做,没有中间状态。
  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

• 多个执⾏流(进程),能看到的同⼀份公共资源:共享资源
• 被保护起来的资源叫做临界资源
• 保护的⽅式常⻅:互斥与同步
• 任何时刻,只允许⼀个执⾏流访问资源,叫做互斥
• 多个执⾏流,访问临界资源的时候,具有⼀定的顺序性,叫做同步
• 系统中某些资源⼀次只允许⼀个进程使⽤,称这样的资源为临界资源或互斥资源。
• 在进程中涉及到互斥资源的程序段叫临界区。你写的代码=访问临界资源的代码(临界区)+不访问临界资源的代码(⾮临界区)
• 所谓的对共享资源进⾏保护,本质是对访问共享资源的代码进⾏保护

4.2 信号量

信号量是一个抽象概念,有时候也被叫做信号灯,本质上是一个计数器可以先用智能指针中或者文件结构体中的count去理解。信号量初始化的时候用来分割临界资源中资源。所有进程想要访问临界资源中的一小块,就必须申请信号量,本质上就是预定机制。

|-------------|
| 信号量同时也是共享资源 |

  • p操作:申请信号量,对应计数器- -,原子性,如果小于某个阈值,那么就会阻塞
    -v操作: 退出,对应计数器++,原子性

在信号量中只有0和1两态,叫做二元信号量,也就是互斥!
信号量也属于通信,都先访问信号量P,所有进程就都能看到同一个信号量,不是传递数据才属于IPC,通知,同步互斥也算

4.3信号量接口和系统调用

|-------------------|
| 信号量接口的设计跟共享内存大致相同 |

semget:

其中参数key_t以及semflg与共享内存获取函数一致,参数nsems则是想要创建多少个信号量,成功返回信号量级标识符(非0整数),失败返回-1,并设置错误码
semctl:

同样是使用对应的标识符然后根据cmd指令进行删除等操作

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
 
int semop(int semid, struct sembuf *sops, unsigned nsops);

struct sembuf
{
    unsigned short  sem_num;        /* semaphore index in array */
    short           sem_op;         /* semaphore operation */
    short           sem_flg;        /* operation flags */
};
sem_num 指定要操作的信号量,0表示第一个信号量,1表示第二个信号量,......
sem_op 信号量操作,设置1表示P操作,-1表示V操作
sem_flg 操作标识

信号量同样可以用指令查看

五、内核中的system V资源管理

在OS中,消息队列,共享内存,信号量被当作同一种资源

内核分配一个ipc_perm数组,用来指向每一个IPC资源。,其实这里是以C语言的方式来实现多态所以我们通过XXXget函数的返回值拿到的什么shmid,semid,其实就是这个(柔性)数组的下标!==


总结

本文描述了进程间通信的概念以及相关函数接口,并简略介绍了内核层面上如何管理通信需要的数据,仍有诸多欠缺与文章内容上深度的赘述不足。

相关推荐
唐青枫42 分钟前
Linux whatis 命令使用详解
linux
万博智云OneProCloud2 小时前
解锁服务器迁移的未来:《2025 服务器迁移效率白皮书》(附下载)
服务器·hypermotion云迁移·it基础设施
白云~️5 小时前
uniappx 打包配置32位64位x86安装包
运维·服务器·github
在河之洲木水6 小时前
现代多核调度器的本质 调度三重奏
linux·服务器·系统架构
程序员JerrySUN6 小时前
驱动开发硬核特训 · Day 22(下篇): # 深入理解 Power-domain 框架:概念、功能与完整代码剖析
linux·开发语言·驱动开发·嵌入式硬件
白总Server6 小时前
多智能体系统的中间件架构
linux·运维·服务器·中间件·ribbon·架构·github
未来会更好yes7 小时前
Centos 7.6安装redis-6.2.6
linux·redis·centos
二猛子7 小时前
Linux(Centos版本)中安装Docker
linux·docker·centos
浪裡遊8 小时前
跨域问题(Cross-Origin Problem)
linux·前端·vue.js·后端·https·sprint
Johny_Zhao8 小时前
OpenStack 全套搭建部署指南(基于 Kolla-Ansible)
linux·python·信息安全·云计算·openstack·shell·yum源·系统运维