Linux进程间通信(超详细介绍各种IPC方式)

Linux进程间通信

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

一、管道

管道,通常指的是无名管道(匿名管道), 是UNIX 系统IPC最古老的形式。

1、特点

  1. **半双工通信:**管道是一种半双工的通信方式(即数据只能在一个方向上流动),具有固定的读端和写端;
  2. 亲缘关系:通常情况下,管道只能在具有亲缘关系的进程之间使用,例如父子进程或兄弟进程;
  3. 字节流:管道中的数据以字节流的形式传输,没有消息边界。写入管道的数据会被缓冲,直到读取端读取为止;
  4. 同步:管道的读取和写入是同步的,即读取操作会阻塞,直到有数据可供读取;写入操作会阻塞,直到有足够的空间可供写入;
  5. 有限容量:管道具有一定的容量限制,如果写入的数据超过了管道的容量,写入操作可能会阻塞或导致错误。
  6. **特殊文件:**它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

2、函数原型

#include <unistd.h>
int pipe(int fd[2]);    
// 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]用于读取管道,fd[1]用于写入管道。如下图:

在这里插入图片描述

3、通信案例

单个进程中的管道几乎没有任何用处。通常,进程会先调用 pipe ,接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。

在这里插入图片描述

fork之后要确定数据流的方向,若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
   
    int fd[2]; // 用于存储管道的文件描述符
    pid_t pid; // 用于存储子进程的 PID
    char *writebuf = "hello chiled,i'm father"; // 要写入的字符串
    char readbuf[128]; // 用于存储读取的数据

    // 创建管道
    if (pipe(fd) == -1) {
    
        perror("pipe"); // 如果创建管道失败,输出错误信息
        exit(0); 
    }

    // 创建子进程
    if ((pid = fork()) < 0) {
    
        perror("fork"); // 如果创建子进程失败,输出错误信息
        exit(0); 
    }

    // 父进程
    if (pid > 0) {
    
        sleep(1); // 休眠一秒,让子进程先运行
        printf("this is father\n"); 
        close(fd[0]); // 关闭读端
        write(fd[1], writebuf, strlen(writebuf)); // 向管道中写入数据
        wait(NULL); // 等待子进程结束
    } else if (pid == 0) {
    
        printf("this is chiled\n"); 
        close(fd[1]); // 关闭写端
        read(fd[0], readbuf, sizeof(readbuf)); // 从管道中读取数据
        printf("from father: %s\n", readbuf); // 打印读取到的数据
        exit(0); 
    }
}
/*运行结果:
this is chiled
this is father
from father: hello chiled,i'm father*/

二、FIFO(命名管道)

FIFO,也称为命名管道,是一种特殊的文件类型。

1、特点

  1. 先进先出:数据以先进先出的方式在FIFO中排队,先写入的数据先被读取;
  2. 无缓冲区:FIFO本身不提供缓冲区,操作系统会为每个打开的FIFO文件描述符分配缓冲区;
  3. 文件系统对象:FIFO在文件系统中表现为一个特殊类型的文件,可以被任何有权限的进程访问;
  4. 非阻塞性:默认情况下,FIFO是阻塞的,但可以通过设置使其变为非阻塞;
  5. 进程间通信:FIFO允许不具有亲缘关系的进程之间进行通信;
  6. 数据流:FIFO支持数据流的传输,数据以字节流的形式存在。

2、函数原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 返回值:成功返回0,出错返回-1
  • pathname:指定要创建的 FIFO 的路径名。这是一个字符串,包含了 FIFO 的名称和路径。
  • mode: 指定 FIFO 的权限模式,是一个八进制数,类似于文件的权限设置。参数与open函数中的 mode 相同,一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
    • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
    • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

3、创建FIFO

#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>

int main()
{
   
    int fd;
    fd = mkfifo("./file",0666);
    if(fd == -1){
   
        perror("why");
    }  
    close(fd);

    //unlink("./file");//删除fifo文件     
    return 0;
}

运行结果:创建了一个名为file的特殊格式文件。

在这里插入图片描述

4、编程实现FIFO数据通信

  • fifo_write:写数据
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>

int main()
{
   
    int fd; // 文件描述符
    char *writeBuf = "fifo write success"; // 要写入的字符串

    // 以只写方式打开文件
    fd = open("./file", O_WRONLY); 
    if (fd == -1) {
    // 如果打开失败
        perror("open"); // 输出错误信息
    }
    printf("open success\n"); // 打印打开成功的信息

    // 将字符串写入文件
    write(fd, writeBuf, strlen(writeBuf)); 

    close(fd); // 关闭文件描述符

    return 0;
}
  • fifo_read:读数据
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main()
{
   
    int ret; // 用于存储函数返回值
    int fd; // 文件描述符
    int n_read; // 读取的字节数
    char readBuf[128] = {
   '\0'}; // 用于存储读取的数据

    // 创建命名管道
    ret = mkfifo("./file", 0600); 
    if ((ret == -1) && (errno!= EEXIST)) {
    // 如果创建失败且错误码不是 EEXIST(文件已存在)
        perror("mkfifo"); // 输出错误信息
    }
    printf("fifo ok\n"); // 打印创建成功的信息

    // 以只读方式打开命名管道
    if ((fd = open("./file", O_RDONLY)) < 0) {
    
        perror("open"); // 输出打开文件失败的错误信息
    }
    printf("open ok\n"); // 打印打开文件成功的信息

    // 从命名管道中读取数据
    n_read = read(fd, readBuf, sizeof(readBuf)); 
    printf("read %d byte, content: %s\n", n_read, readBuf); // 打印读取的字节数和内容

    close(fd); // 关闭文件描述符

    unlink("./file"); // 删除命名管道
    return 0;
}

运行结果:默认情况下创建FIFO后的mode是阻塞状态,所以先运行只读程序会阻塞在"fifo ok"等待写入数据,等到执行写入程序后只读程序会执行完毕,读到写入程序的写入内容。
在这里插入图片描述

三、消息队列

  • 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识;
  • 消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。

1、特点

  1. 异步性:发送和接收消息是异步进行的;
  2. 持久性:消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
  3. 消息顺序:在同一个消息类型中,消息队列通常保证消息的顺序,即先发送的消息先被接收。在不同的类型或优先级中,允许接收者根据类型或优先级处理消息;
  4. 多生产者和多消费者:多个进程可以同时向同一个消息队列发送或接收消息;
  5. 解耦生产者和消费者:消息队列允许生产者和消费者在不同的时间点独立工作,从而降低了系统组件之间的耦合度。

2、相关API介绍

#include <sys/msg.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
// 通常用于生成消息队列、信号量或共享内存的键值:成功返回生成的键值,失败返回-1

int msgget(key_t key, int flag);
// 创建或打开消息队列:成功返回队列ID,失败返回-1

int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 添加消息:成功返回0,失败返回-1

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 控制消息队列:成功返回0,失败返回-1
(1)ftok

ftok函数用于将一个路径名和一个项目标识符转换为一个键值,通常用于生成消息队列、信号量或共享内存的键。

#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
//返回值:成功返回生成的键值,失败返回-1,并设置error错误码
/*在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算
成16进制为0x26,则最后的key_t返回值为0x26010002。*/

参数说明:

  • pathname:指向文件路径的指针,这个文件必须是已经存在且可以访问的。
    • 一般使用当前目录,如key = ftok(“.”,1),"."代表当前目录;
    • ls -i:可以查找文件的索引节点号。
  • proj_id:一个 8 bit的整数,虽然是int类型,不过值的范围通常是 1 到 255。
(2)msgget

msgget函数用于获取一个已存在的消息队列的标识符,或者在 IPC_CREAT 标志被设置的情况下创建一个新的消息队列。通常,msgget函数与其他消息队列操作函数(如msgsndmsgrcv)一起使用,用于实现进程间通信。

#include <sys/msg.h>

int msgget(key_t key, int flag);
// 返回值:成功返回队列ID,失败返回-1,并设置error错误码

参数说明:

  • key:用于指定消息队列的唯一标识符。如果使用 IPC_PRIVATE 作为 key,将创建一个私有的消息队列,只对当前进程可见。
  • flag:标志位,用于指定消息队列的访问权限和操作方式。常见的标志位有:
    • IPC_CREAT:如果消息队列不存在,则创建它。
    • IPC_EXCL:与IPC_CREAT一起使用,如果消息队列已经存在,则返回错误。
    • IPC_NOWAIT:如果消息队列已满或为空,立即返回错误,而不是阻塞等待。
(3)msgsend

msgsend函数用于将消息发送到指定的消息队列中。

#include <sys/msg.h>

int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 返回值:成功返回0,失败返回-1,并设置error错误码

参数说明:

  • msqid:消息队列的标识符,通过msgget函数获取。

  • ptr:指向要发送的消息的指针。消息通常是一个结构体,包含一个 long mtype 类型的消息类型字段和一个 char mtext[1] 类型的字符数组,用于存储消息的实际内容。

  • size:消息的大小,不包括消息类型的长度。

  • flag:标志位,用于指定发送消息的方式。常见的标志位有:

    • 0:正常发送消息;

    • IPC_NOWAIT:如果消息队列已满,立即返回错误,而不是阻塞等待。

(4)msgrcv

msgrcv函数用于从消息队列中接收消息。

#include <sys/msg.h>

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 返回值:成功返回消息数据的长度,失败返回-1,并设置error错误码

参数说明:

  • msqid:消息队列的标识符,通过msgget函数获取。
  • ptr::指向用于存储接收消息的缓冲区的指针。
  • size:要接收的消息的大小(不包括消息类型的长度)。
  • type:指定要接收的消息类型。type值非 0 时用于以非先进先出次序读消息,也可以把 type 看做优先级的权值。可以是以下几种情况之一:
    • type == 0:接收队列任意类型的第一条消息;
    • type > 0:接收队列中消息类型为 type 的第一个消息;
    • type < 0:接收队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
  • flag:标志位,用于指定接收消息的方式。常见的标志位有:
    • IPC_NOWAIT:如果没有满足条件的消息,立即返回错误,而不是阻塞等待。
    • MSG_NOERROR:如果接收到的消息长度大于msgsz,则截断消息。
(5)msgctl

msgctl函数用于对消息队列进行控制操作,例如获取消息队列的信息、删除消息队列等。

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//返回值:成功返回0,失败返回-1,并设置error错误码

参数说明:

  • msqid:消息队列的标识符,通过msgget函数获取。
  • cmd:控制命令,可以是以下几种值之一:常用IPC_RMID
    • IPC_STAT:获取消息队列的信息,并将其存储在buf指向的结构体中。
    • IPC_SET:设置消息队列的属性,使用buf指向的结构体中的值。
    • IPC_RMID:删除消息队列。
  • buf:指向一个msqid_ds结构体的指针,用于存储或设置消息队列的信息,通常设置为NULL。

3、编程实现消息队列收发消息

  • msg_read:读取消息队列的消息,然后通知发送端已经收到消息
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>

// 定义消息结构体
struct msgbuf {
   
    long mtype;    // 消息类型
    char mtext[128];  // 消息内容
};

int main() {
   
    int ret;  // 用于存储函数返回值
    int msgID;  // 消息队列标识符
    key_t key; //键值
    struct msgbuf readBuf;  // 用于存储接收的消息
    struct msgbuf sendBuf = {
   999, "tankyou for read"};  // 要发送的消息

    // 使用 ftok 函数生成键值
    key = ftok(".",'a');//第二个参数id可以用字符,系统会根据Ascll码转换成相应数字
     if (msgID == -1) {
   
        perror("ftok");  // 如果键值生成失败,输出错误信息
    }
    printf("key = %x\n", key);  // 输出生成的键值
    
    // 获取消息队列,IPC_CREAT 表示如果不存在则创建,0777 表示权限
    msgID = msgget(0x1234, IPC_CREAT | 0777);
    if (msgID == -1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值