从管道路由到共享内存:进程间通信的演变(内附通信方式经典面试题及详解)

简介: 进程间通信(Inter-Process Communication, IPC)是计算机科学中的一个重要概念,指的是运行在同一系统或不同系统上的多个进程之间互相发送和接收信息的能力。IPC机制允许进程间共享数据、协调执行流程,是实现分布式系统、多任务操作系统和并发编程的基础。

进程间通信概念:

进程间通信(Inter-Process Communication, IPC)是计算机科学中的一个重要概念,指的是运行在同一系统或不同系统上的多个进程之间互相发送和接收信息的能力。IPC机制允许进程间共享数据、协调执行流程,是实现分布式系统、多任务操作系统和并发编程的基础。

而进程的间的通信方式也主要分为6种,分别是:

1. 无名管道(pipe)和 有名管道(fifo)

image.gif 编辑

2. 信号(sem)

3. 共享内存(share memory)

4. 消息队列(message queue)

5. 信号灯(semaphore)

6. 套接字(socket)

不同方式的基本概念:

1. 无名管道(Pipe)

  • 概念:无名管道是一种半双工的通信方式,数据只能从管道的一端写入,从另一端读出。无名管道只能在有亲缘关系的进程间使用,通常是父子进程。
  • 特点:简单、快速,但通信方向固定,且局限于相关联进程。
  • 函数
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
      失败 -1
注意:
1. 当管道中无数据时,读会阻塞;管道中无数据,将写端关闭,读操作立即返回
2. 当管道中写满(64k)数据时,写阻塞,一旦有4k空间,写继续,直到写满为止
3. 将读端关闭,继续写数据,会导致管道破裂,进程会收到内核发送过来的SIGPIPE信号(Broken pipe)

image.gif

2. 命名管道(Named Pipe or FIFO)

  • 概念:命名管道是具有名字的管道,可以在文件系统中创建,允许任意进程通过名字打开并进行通信。
  • 特点:克服了无名管道的局限,支持无关进程间的通信,但仍然保持了管道的简单性。
  • 函数
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
       mode:权限
返回值:成功:0
       失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)

image.gif

3. 信号(Signals)

  • 概念:信号是一种异步通信机制,用于通知接收进程某个事件已经发生。
  • 特点:主要用于异常处理,如中断和错误报告,不是一种通用的数据传输机制。
  • 信号的种类:
2)SIGINT:结束进程,对应快捷方式ctrl+c
3)SIGQUIT:退出信号,对应快捷方式ctrl+\
9)SIGKILL:结束进程,不能被忽略不能被捕捉
15)SIGTERM:结束终端进程,kill 使用时不加数字默认是此信号
17)SIGCHLD:子进程状态改变时给父进程发的信号
19)SIGSTOP:暂停进程,不能被忽略不能被捕捉
20)SIGTSTP:暂停信号,对应快捷方式ctrl+z
26)SIGALRM:闹钟信号,alarm函数设置定时,当到设定的时间时,内核会向进程发送此信号结束进程

image.gif

  • 信号的响应方式:
1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作 。

image.gif

4. 共享内存(Shared Memory)

  • 概念:共享内存允许多个进程直接访问同一块内存区域,是最快的IPC机制之一,因为数据无需在用户态和内核态之间复制。
  • 特点:速度快,但需要额外的同步机制(如信号量)来避免数据竞争。
  • 函数
key_t ftok(const char *pathname, int proj_id);
功能:产生一个独一无二的key值
参数:
    Pathname:已经存在的可访问文件的名字
    Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
      失败:-1

image.gif

int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
    key  键值
    size   共享内存的大小
    shmflg   IPC_CREAT|IPC_EXCL(判错)|0666
返回值:成功   shmid
      出错    -1

image.gif

void  *shmat(int  shmid,const  void  *shmaddr,int  shmflg);
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
    shmid   共享内存的id号
    shmaddr   一般为NULL,表示由系统自动完成映射
              如果不为NULL,那么由用户指定
    shmflg:SHM_RDONLY就是对该共享内存只进行读操作
                0     可读可写
返回值:成功:完成映射后的地址,
      失败:-1的地址

image.gif

int shmdt(const void *shmaddr);
功能:取消映射
参数:要取消的地址
返回值:成功0  
      失败的-1

image.gif

int  shmctl(int  shmid,int  cmd,struct  shmid_ds   *buf);
功能:(删除共享内存),对共享内存进行各种操作
参数:
    shmid   共享内存的id号
    cmd     IPC_STAT 获得shmid属性信息,存放在第三参数
            IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
            IPC_RMID:删除共享内存,此时第三个参数为NULL即可
返回:成功0 
     失败-1
用法:shmctl(shmid,IPC_RMID,NULL);

image.gif

5. 消息队列(Message Queues)

  • 概念:消息队列是系统维护的一个链表,链表中的元素是消息,进程可以将消息添加到队列尾部,也可以从队列头部删除消息。
  • 特点:提供了比管道更灵活的数据传输机制,可以指定消息的长度和类型,适合传输结构化的数据。
  • 函数
int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数:  key值
       flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
       失败:-1

image.gif

int msgsnd(int msqid, const void *msgp, size_t size, int flag); 
功能:添加消息
参数:msqid:消息队列的ID
      msgp:指向消息的指针。常用消息结构msgbuf如下:
          struct msgbuf{
            long mtype;          //消息类型
            char mtext[N]};   //消息正文
   size:发送的消息正文的字节数
   flag:IPC_NOWAIT消息没有发送完成函数也会立即返回    
         0:直到发送完成函数才返回
返回值:成功:0
      失败:-1
使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。

image.gif

int msgrcv(int msgid,  void* msgp,  size_t  size,  long msgtype,  int  flag);
功能:读取消息
参数:msgid:消息队列的ID
     msgp:存放读取消息的空间
     size:接受的消息正文的字节数
    msgtype:0:接收消息队列中第一个消息。
            大于0:接收消息队列中第一个类型为msgtyp的消息.
            小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
     flag:0:若无消息函数会一直阻塞
        IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
      失败:-1

image.gif

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,删除消息队列
参数:msqid:消息队列的队列ID
     cmd:
        IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
        IPC_SET:设置消息队列的属性。这个值取自buf参数。
        IPC_RMID:从系统中删除消息队列。
     buf:消息队列缓冲区
返回值:成功:0
      失败:-1
用法:msgctl(msgid, IPC_RMID, NULL

image.gif

6. 信号量(Semaphores)

  • 概念:信号量是一种计数器,用于控制进程对共享资源的访问,可以解决多个进程同时访问共享内存时的同步问题。
  • 特点:主要用于进程间的同步,而不是通信。
  • 函数
1. semget
int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
    nsems:信号灯集中包含的信号灯数目
    semflg:信号灯集的访问权限,通常为IPC_CREAT |IPC_EXCL|0666
返回值:成功:信号灯集ID
       失败:-1

image.gif

2. semctl
int semctl ( int semid, int semnum,  int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
    semnum: 要操作的集合中的信号灯编号
     cmd:
        GETVAL:获取信号灯的值,返回值是获得值
        SETVAL:设置信号灯的值,需要用到第四个参数:共用体
        IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
      失败 -1
用法:初始化:
union semun{
    int val;
}mysemun;
mysemun.val = 10;
semctl(semid, 0, SETVAL, mysemun);
获取信号灯值:函数semctl(semid, 0, GETVAL)的返回值

image.gif

3. semop
int semop ( int semid, struct sembuf  *opsptr,  size_t  nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
     opsptr:操作方式
     nops:  要操作的信号灯的个数 1个
返回值:成功 :0
      失败:-1
struct sembuf {
   short  sem_num; // 要操作的信号灯的编号
   short  sem_op;  //    0 :  等待,直到信号灯的值变成0
                   //   1  :       
                   //   -1 :  申请资源,P操作                    
    short  sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};
用法:
申请资源 P操作:
    mysembuf.sem_num = 0;
    mysembuf.sem_op = -1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);
释放资源 V操作:
    mysembuf.sem_num = 0;
    mysembuf.sem_op = 1;
    mysembuf.sem_flg = 0;
    semop(semid, &mysembuf, 1);

image.gif

7. 套接字(Sockets)

  • 概念:套接字是一种用于网络通信的IPC机制,也适用于本地进程间的通信。
  • 特点:提供了非常灵活的通信机制,支持多种通信协议,如TCP和UDP,适用于分布式系统和网络编程。

应用链接:TCP和UDP的服务器和客户端通信代码实现,非常简单易懂!(附源码,小白必看!)_tcp或者udp代码实现客户端与服务器之间通讯-CSDN博客

应用实例:

无名管道:

pipe.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#define BUFFER_SIZE 100
int main() {
    int fd[2]; // 文件描述符数组,用于无名管道
    pid_t pid;
    // 创建无名管道
    if (pipe(fd) == -1) {
        perror("Pipe creation failed");
        return 1;
    }
    pid = fork();
    if (pid < 0) { // 错误
        perror("Fork failed");
        return 1;
    } else if (pid > 0) { // 父进程
        close(fd[0]); // 关闭读端
        char message[] = "Hello from parent";
        write(fd[1], message, strlen(message)+1); // 写入消息
        close(fd[1]); // 关闭写端
    } else { // 子进程
        close(fd[1]); // 关闭写端
        char buffer[BUFFER_SIZE];
        read(fd[0], buffer, BUFFER_SIZE); // 读取消息
        printf("Received: %s\n", buffer);
        close(fd[0]); // 关闭读端
    }
    return 0;
}

image.gif

image.gif 编辑

有名管道:

name_pipe.c

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE 100
int main() {
    mkfifo(FIFO_NAME, 0666); // 创建有名管道
    int fd;
    char buffer[BUFFER_SIZE];
    // 父进程:写入消息
    pid_t pid = fork();
    if (pid > 0) {
        fd = open(FIFO_NAME, O_WRONLY);
        if (fd == -1) {
            perror("Open failed");
            return 1;
        }
        write(fd, "Hello from named pipe", strlen("Hello from named pipe")+1);
        close(fd);
    } else if (pid == 0) {
        // 子进程:读取消息
        fd = open(FIFO_NAME, O_RDONLY);
        if (fd == -1) {
            perror("Open failed");
            return 1;
        }
        read(fd, buffer, BUFFER_SIZE);
        printf("Received: %s\n", buffer);
        close(fd);
    }
    unlink(FIFO_NAME); // 删除有名管道
    return 0;
}

image.gif

image.gif 编辑

消息队列:

message_queue.c

#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSGKEY 1234 // 定义消息队列的key
typedef struct msgbuf {
    long mtype; // 消息类型
    char mtext[100]; // 消息文本
} message_buffer;
int main(int argc, char *argv[]) {
    int msqid; // 消息队列标识符
    message_buffer my_msg;
    long msgflg = IPC_CREAT | 0666; // 创建消息队列的标志和权限掩码
    // 创建消息队列
    if ((msqid = msgget(MSGKEY, msgflg)) < 0) {
        perror("msgget error");
        exit(1);
    }
    // 发送消息
    my_msg.mtype = 1; // 设置消息类型
    strcpy(my_msg.mtext, "Hello, this is a test message!"); // 设置消息文本
    if (msgsnd(msqid, &my_msg, sizeof(my_msg.mtext), 0) < 0) {
        perror("msgsnd error");
        exit(1);
    }
    // 接收消息
    if (msgrcv(msqid, &my_msg, sizeof(my_msg.mtext), 1, 0) < 0) {
        perror("msgrcv error");
        exit(1);
    }
    printf("Received message: %s\n", my_msg.mtext);
    // 清理消息队列
    if (msgctl(msqid, IPC_RMID, NULL) < 0) {
        perror("msgctl error");
        exit(1);
    }
    return 0;
}

image.gif

image.gif 编辑

共享内存:

image.gif 编辑

经典面试题:

       

  1. 什么是进程间通信(IPC)?
       进程间通信(IPC)是指在多任务操作系统中,两个或多个进程之间交换数据和控制信息的机制。它是实现分布式计算和多进程系统协作的基础。
  2. 进程间通信的主要方式有哪些?
       主要方式包括管道(Pipes)、消息队列(Message Queues)、共享内存(Shared Memory)、信号量(Semaphores)、套接字(Sockets)和远程过程调用(RPCs)。
  3. 无名管道(Pipe)和有名管道(FIFO)的主要区别是什么?
          无名管道仅限于有亲缘关系的进程(如父子进程)之间通信,且存在于内存中;有名管道(FIFO)则是命名的,存在于文件系统中,允许任何进程打开并进行通信。
  4. 信号量(Semaphore)的作用是什么?
       信号量用于控制对共享资源的访问,防止多个进程同时访问同一资源造成冲突,实现进程间的同步。
  5. 共享内存是如何实现进程间通信的?
      共享内存允许多个进程映射同一块物理内存区域,直接读写这块内存来交换数据,是最快的IPC方式之一,但需要额外的同步机制(如信号量)来防止数据竞争。
  6. 套接字(Socket)在进程间通信中扮演什么角色?
      套接字提供了一种通用的网络通信接口,既可用于本地进程间通信,也可用于不同机器间的远程通信。
  7. 在Linux中,如何创建一个消息队列?
     使用msgget系统调用,传入消息队列的键(key)和标志(flags)来创建或访问一个消息队列。
  8. 进程间通信中,什么是死锁?
      死锁发生在两个或多个进程互相等待对方持有的资源而不释放自己所占有的资源,导致所有相关进程都无法继续执行的状态。
  9. 进程间通信中,哪种通信方式最快?为什么?

           共享内存,因为共享内存是通过地址映射的方式进行数据的通信,无需多余的拷贝赋值 操         作。

     10. 在使用共享内存时,为什么需要信号量?

           信号量用于确保多个进程在访问共享内存时不会发生数据竞争或不一致,实现对共享资源的安全访问。

       至此文章结束,希望可以帮到大家~

相关文章
|
5天前
|
存储 安全 Linux
深入理解操作系统:从进程管理到内存分配
【6月更文挑战第30天】在数字时代的心脏,操作系统是现代计算不可或缺的组成部分。本文将深入探讨操作系统的核心功能,包括进程管理、内存分配以及文件系统管理。我们将通过实际案例分析,揭示这些机制如何在提高计算机性能的同时保证资源的有效利用。文章旨在为读者提供对操作系统工作原理的深刻理解,并展示其在现代技术中的应用价值。
|
5天前
|
存储 算法 程序员
深入理解操作系统:从进程管理到内存分配
【7月更文挑战第1天】在数字时代的心脏,操作系统(OS)扮演着枢纽的角色。本文将探索操作系统的核心概念,包括进程管理、内存分配和文件系统,同时揭示这些机制如何协同工作以确保计算机系统的高效运行。我们将从用户和程序员的视角出发,分析操作系统如何在幕后默默支撑着我们的数字生活。
|
9天前
|
监控 Linux
深入了解Linux的pmap命令:进程内存映射的利器
`pmap`是Linux下分析进程内存映射的工具,显示内存区域、权限、大小等信息。通过`/proc/[pid]/maps`获取数据,特点包括详细、实时和灵活。参数如`-x`显示扩展信息,`-d`显示设备。示例:`pmap -x 1234`查看进程1234的映射。注意权限、实时性和准确性。结合其他工具定期监控,排查内存问题。
|
10天前
|
存储 缓存 算法
深入理解操作系统:从进程管理到内存分配
本文深入探讨操作系统的核心组件,特别关注进程管理和内存分配机制。通过分析现代操作系统中这两个关键领域的设计原理和实现技术,文章揭示了它们如何共同确保系统资源的有效利用和任务的高效执行。我们将从理论到实践,逐步解析进程状态变迁、调度算法以及内存分配策略,旨在为读者提供对操作系统内部工作原理的深刻见解。
8 0
有 3 个进程 P1、P2、P3 协作解决文件打印问题。P1 将文件记录从磁盘读入内存的缓冲区 1,每执行一次读一个记录 ;P2 将缓冲区 1 中的内容复制到缓冲区 2 中,每执行一次复制一个记录 ;
有 3 个进程 P1、P2、P3 协作解决文件打印问题。P1 将文件记录从磁盘读入内存的缓冲区 1,每执行一次读一个记录 ;P2 将缓冲区 1 中的内容复制到缓冲区 2 中,每执行一次复制一个记录 ;
|
20天前
|
消息中间件 存储 Kafka
实时计算 Flink版产品使用问题之 从Kafka读取数据,并与两个仅在任务启动时读取一次的维度表进行内连接(inner join)时,如果没有匹配到的数据会被直接丢弃还是会被存储在内存中
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
12天前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
23 2
|
16天前
|
存储
数据在内存中的存储(2)
数据在内存中的存储(2)
25 5
|
16天前
|
存储 小程序 编译器
数据在内存中的存储(1)
数据在内存中的存储(1)
29 5
|
16天前
|
存储 安全 Java
SpringSecurity6从入门到实战之初始用户如何存储到内存
Spring Security 在 SpringBoot 应用中默认使用 `UserDetailsServiceAutoConfiguration` 类将用户信息存储到内存中。当classpath有`AuthenticationManager`、存在`ObjectPostProcessor`实例且无特定安全bean时,此配置生效。`inMemoryUserDetailsManager()`方法创建内存用户,通过`UserDetails`对象填充`InMemoryUserDetailsManager`的内部map。若要持久化到数据库,需自定义`UserDetailsService`接口实

热门文章

最新文章