Bootstrap

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

目录

进程间通信概念:

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

不同方式的基本概念:

1. 无名管道(Pipe)

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

3. 信号(Signals)

信号的种类:

4. 共享内存(Shared Memory)

5. 消息队列(Message Queues)

6. 信号量(Semaphores)

7. 套接字(Sockets)

应用实例:

无名管道:

有名管道:

消息队列:

共享内存:

经典面试题:


进程间通信概念:

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

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

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

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)
2. 命名管道(Named Pipe or FIFO)
  • 概念:命名管道是具有名字的管道,可以在文件系统中创建,允许任意进程通过名字打开并进行通信。
  • 特点:克服了无名管道的局限,支持无关进程间的通信,但仍然保持了管道的简单性。
  • 函数
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
       mode:权限
返回值:成功:0
       失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)
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函数设置定时,当到设定的时间时,内核会向进程发送此信号结束进程
  • 信号的响应方式:
1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
3)执行缺省操作:Linux对每种信号都规定了默认操作 。
4. 共享内存(Shared Memory)
  • 概念:共享内存允许多个进程直接访问同一块内存区域,是最快的IPC机制之一,因为数据无需在用户态和内核态之间复制。
  • 特点:速度快,但需要额外的同步机制(如信号量)来避免数据竞争。
  • 函数
key_t ftok(const char *pathname, int proj_id);
功能:产生一个独一无二的key值
参数:
    Pathname:已经存在的可访问文件的名字
    Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
      失败:-1
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
    key  键值
    size   共享内存的大小
    shmflg   IPC_CREAT|IPC_EXCL(判错)|0666
返回值:成功   shmid
      出错    -1
void  *shmat(int  shmid,const  void  *shmaddr,int  shmflg);
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
    shmid   共享内存的id号
    shmaddr   一般为NULL,表示由系统自动完成映射
              如果不为NULL,那么由用户指定
    shmflg:SHM_RDONLY就是对该共享内存只进行读操作
                0     可读可写
返回值:成功:完成映射后的地址,
      失败:-1的地址
int shmdt(const void *shmaddr);
功能:取消映射
参数:要取消的地址
返回值:成功0  
      失败的-1
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);

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

有名管道:

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;
}

消息队列:

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;
}

共享内存:

经典面试题:

        

  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. 在使用共享内存时,为什么需要信号量?

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

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

;