命名管道:让两个“陌生人”进程也能聊天

目录

一、命名管道是什么

二、创建命名管道

三、打开命名管道的规则

1. 为读而打开

2. 为写而打开

四、用命名管道实现服务端和客户端通信

1. 共用头文件

2. 服务端代码

3. 客户端代码

4. 服务端与客户端运行流程

5. 进程关系验证

6. 服务端和客户端的退出关系

7. 通信在内存中进行的验证

五、用命名管道实现派发计算任务

六、用命名管道实现进程遥控

七、命名管道和匿名管道的区别

1. 创建方式

2. 使用方式

3. 打开方式

4. 通信范围

5. 生命周期

6. 性能

7. 使用场景


一、命名管道是什么

        命名管道就像一个特殊的“信封”。我们平时用的文件,就像是存储在硬盘上的“宝盒”,里面可以装各种各样的数据。而命名管道呢,它有点不一样。它更像是住在内存里的一个“小精灵”。虽然它在硬盘上也有一个小小的“家”,但这个“家”其实只是个标记,大小永远是0。因为这个“小精灵”真正工作的时候,数据都是在内存里快速地跑来跑去,不会存到硬盘里。

        这就意味着,命名管道能让两个进程像朋友一样聊天。普通的文件虽然也能让进程之间交流,但不太安全。比如,一个进程写的信息,另一个进程可以随意修改,这样就可能出问题。而命名管道和匿名管道不一样,它们是内存文件,数据都存在电脑的内存里,就像把信息放在一个快速传递的信封里,不会把数据真正写到硬盘上。

        所以,命名管道就像是一个能让两个进程快速、安全交流的神奇工具。它让那些没有亲缘关系的进程也能轻松地分享信息,完成各种任务。

二、创建命名管道

        要创建命名管道,最简单的方法就是使用 mkfifo 命令。只需要在终端里输入 mkfifo fifo,瞬间就能创建一个叫 fifo 的命名管道文件。你会看到这个文件的类型是 p,这表示它是一个命名管道文件。


         这个命名管道文件就像一个特殊的“信箱”,两个没有亲缘关系的进程可以通过它进行通信。比如,一个进程(进程A)可以用shell脚本每秒向命名管道写入一个字符串,另一个进程(进程B)可以用 cat 命令从命名管道中读取数据。当进程A启动后,进程B就会每秒从命名管道中读取一个字符串并打印到显示器上,这就证明了两个毫不相关的进程可以通过命名管道进行数据传输。

说明:

  1. while :; do ... done:这是一个无限循环,: 是一个空命令,条件永远为真。

  2. echo "Hello,命名管道!":向命名管道中写入字符串 "Hello,命名管道!"。

  3. sleep 1:暂停 1 秒。

  4. > fifo:将 echo 的输出重定向到命名管道 fifo

使用方法:

  1. 确保命名管道 fifo 已经存在。如果不存在,先创建它:

    mkfifo fifo
  2. 在一个终端中运行上述命令,开始每秒向 fifo 写入字符串。

  3. 在另一个终端中,可以用以下命令读取命名管道中的内容:

    cat fifo

这样,就能看到每秒从命名管道中读取到的字符串了。


        还有一点需要注意,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉。比如,当你终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以bash会被操作系统杀掉,云服务器也会退出。


        如果想在程序中创建命名管道,可以使用 mkfifo 函数。这个函数的原型是:

int mkfifo(const char *pathname, mode_t mode);

   mkfifo 函数的第一个参数是 pathname,表示要创建的命名管道文件的路径。如果是以路径的方式给出,命名管道文件就会创建在这个路径下如果是以文件名的方式给出,命名管道文件就会默认创建在当前路径下

        第二个参数是 mode,表示创建命名管道文件的默认权限。比如,把 mode 设置为 0666,命名管道文件的权限就会是 -rw-rw-r--。不过,实际创建出来的文件权限还会受到 umask(文件默认掩码)的影响。实际权限是 mode & (~umask)。如果想让权限不受 umask 影响,可以在创建文件前用 umask(0) 把文件默认掩码设置为0。

   mkfifo 函数的返回值很简单:创建成功返回0创建失败返回-1

下面是一个创建命名管道的示例代码:

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

int main()
{
    umask(0); // 将文件默认掩码设置为0
    if (mkfifo(“myfifo”, 0666) < 0) { // 使用mkfifo创建命名管道文件
        perror("mkfifo");
        return 1;
    }

    // 创建成功...

    return 0;
}

运行这段代码后,当前路径下就会创建一个名为 myfifo 的命名管道文件。

三、打开命名管道的规则

1. 为读而打开

  • O_NONBLOCK禁用:系统会阻塞读操作,直到有相应进程为写而打开该FIFO。这意味着读进程会一直等待,直到有一个写进程出现并打开同一个命名管道。

  • O_NONBLOCK启用:系统不会阻塞读操作,而是立即返回成功。读进程可以继续执行其他任务,而不需要等待写进程出现。

2. 为写而打开

  • O_NONBLOCK禁用:系统会阻塞写操作,直到有相应进程为读而打开该FIFO。写进程会一直等待,直到有一个读进程出现并打开同一个命名管道。

  • O_NONBLOCK启用:系统不会阻塞写操作,而是立即返回失败,错误码为ENXIO。这表示当前没有可用的读进程,写进程需要自行处理这种情况。

四、用命名管道实现服务端和客户端通信

1. 共用头文件

        为了让客户端和服务端用同一个命名管道文件,可以写个头文件,把文件名放进去。代码如下:

#pragma once

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

#define FILE_NAME "myfifo" // 让客户端和服务端使用同一个命名管道

2. 服务端代码

        服务端先运行,创建命名管道文件,然后以读的方式打开,等着客户端发信息。代码如下:

#include "comm.h"

int main()
{
    // 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
    umask(0); 
    
    // 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,0666是权限设置
    // 如果创建失败,返回错误信息并退出程序
    if (mkfifo(FILE_NAME, 0666) < 0){ 
        perror("mkfifo");
        return 1;
    }
    
    // 以只读方式打开命名管道文件
    // 如果打开失败,返回错误信息并退出程序
    int fd = open(FILE_NAME, O_RDONLY); 
    if (fd < 0){
        perror("open");
        return 2;
    }
    
    // 定义一个字符数组用于存储读取到的消息
    char msg[128];
    
    // 无限循环,持续从命名管道中读取数据
    while (1){
        // 每次读取前将msg数组清空,避免残留数据干扰
        msg[0] = '\0'; 
        
        // 从命名管道中读取数据,sizeof(msg)-1确保不会超出数组范围
        ssize_t s = read(fd, msg, sizeof(msg)-1);
        
        // 如果读取到有效数据(字节数大于0)
        if (s > 0){
            // 在消息末尾添加字符串结束符,确保输出正确
            msg[s] = '\0'; 
            
            // 打印客户端发送的消息
            printf("client: %s\n", msg); 
        }
        // 如果读取到0字节,表示客户端已退出
        else if (s == 0){
            printf("client quit!\n");
            break; // 退出循环
        }
        // 如果读取失败,打印错误信息并退出循环
        else{
            printf("read error!\n");
            break;
        }
    }
    
    // 关闭命名管道文件描述符,释放资源
    close(fd); 
    
    return 0;
}

注释说明:

  1. umask(0):

    • umask 用于设置文件的默认权限掩码。这里将其设置为 0,确保后续创建的命名管道文件权限不受系统默认掩码的影响,能够以最大权限(0666)创建。

  2. mkfifo(FILE_NAME, 0666):

    • mkfifo 函数用于创建命名管道文件。FILE_NAME 是管道文件的名称,0666 表示权限设置为所有用户可读可写(rw-rw-rw-)。

    • 如果创建失败(例如文件已存在或权限不足),mkfifo 返回 -1,程序通过 perror 打印错误信息并退出。

  3. open(FILE_NAME, O_RDONLY):

    • 以只读模式打开命名管道文件。O_RDONLY 表示只读。

    • 如果打开失败(例如文件不存在或权限不足),open 返回 -1,程序通过 perror 打印错误信息并退出。

  4. read(fd, msg, sizeof(msg)-1):

    • 从命名管道中读取数据,fd 是文件描述符,msg 是存储数据的缓冲区,sizeof(msg)-1 确保不会超出数组范围。

    • 返回值 s 表示读取到的字节数。如果 s > 0,表示读取到有效数据;如果 s == 0,表示管道另一端已关闭;如果 s < 0,表示读取失败。

  5. msg[s] = '\0':

    • 在读取到的数据末尾添加字符串结束符,确保输出正确。

  6. close(fd):

    • 关闭文件描述符,释放资源。

3. 客户端代码

        客户端看到服务端运行后,命名管道文件已经存在,就以写的方式打开,把信息写进去。代码如下:

#include "comm.h"

int main()
{
    // 以只写方式打开命名管道文件
    // O_WRONLY 表示只写模式
    int fd = open(FILE_NAME, O_WRONLY); 
    // 如果打开失败,返回错误信息并退出程序
    if (fd < 0){
        perror("open");
        return 1;
    }
    
    // 定义一个字符数组用于存储用户输入的消息
    char msg[128];
    
    // 无限循环,持续从标准输入读取数据并写入命名管道
    while (1){
        // 每次读取前将msg数组清空,避免残留数据干扰
        msg[0] = '\0'; 
        
        // 提示用户输入信息
        printf("Please Enter:"); 
        fflush(stdout); // 刷新输出缓冲区,确保提示信息立即显示
        
        // 从标准输入读取用户输入的信息,sizeof(msg)-1确保不会超出数组范围
        ssize_t s = read(0, msg, sizeof(msg)-1);
        
        // 如果读取到有效数据(字节数大于0)
        if (s > 0){
            // 去掉输入的换行符
            msg[s - 1] = '\0'; 
            
            // 将用户输入的信息写入命名管道
            write(fd, msg, strlen(msg));
        }
    }
    
    // 关闭命名管道文件描述符,释放资源
    close(fd); 
    
    return 0;
}

注释说明:

  1. open(FILE_NAME, O_WRONLY):

    • open 函数用于打开命名管道文件。O_WRONLY 表示以只写模式打开。

    • 如果打开失败(例如文件不存在或权限不足),open 返回 -1,程序通过 perror 打印错误信息并退出。

  2. read(0, msg, sizeof(msg)-1):

    • read 函数从标准输入(文件描述符 0)读取用户输入的数据。

    • sizeof(msg)-1 确保不会超出数组范围,避免缓冲区溢出。

    • 返回值 s 表示读取到的字节数。如果 s > 0,表示读取到有效数据;如果 s == 0,表示输入流结束;如果 s < 0,表示读取失败。

  3. msg[s - 1] = '\0':

    • 去掉输入的换行符,确保写入管道的是干净的字符串。

  4. write(fd, msg, strlen(msg)):

    • write 函数将用户输入的数据写入命名管道。fd 是命名管道的文件描述符,msg 是要写入的数据,strlen(msg) 是数据的长度。

  5. close(fd):

    • 关闭文件描述符,释放资源。

4. 服务端与客户端运行流程

4.1 服务端启动

        首先运行服务端进程,此时服务端会创建一个命名管道文件。这一操作相当于搭建起了一个供后续通信使用的 “数据通道”。

4.2 客户端启动

        接着运行客户端进程,客户端会连接到服务端先前创建的命名管道文件。此时,客户端与服务端通过命名管道建立了联系,尽管它们是两个毫无亲缘关系的进程。

4.3 通信实现

        客户端将信息写入命名管道,服务端从命名管道中读取这些信息并打印在服务端的显示器上。这一过程证明了两个不相关的进程可以通过命名管道进行数据交互。

5. 进程关系验证

        通过 ps 命令查看服务端和客户端的进程信息,可以发现它们的 PID(进程标识符)和 PPID(父进程标识符)都不相同。这表明两个进程是相互独立的,进一步验证了命名管道能够实现不相关进程间的通信。

6. 服务端和客户端的退出关系

6.1 客户端退出

        当客户端退出后,服务端会将管道中剩余的数据读取完毕。由于之后服务端再也读取不到新的数据,会执行其他代码逻辑(在示例代码中是直接退出)。

6.2 服务端退出

        服务端退出后,客户端若再次向管道写入数据,会收到操作系统发来的 13 号信号(SIGPIPE),客户端会被操作系统强制杀掉。这是因为此时已没有进程去读取客户端写入的数据,客户端的写入操作变得没有意义。

7. 通信在内存中进行的验证

        在服务端不读取管道数据的情况下,客户端依然向管道写入数据。通过 ll 命令查看命名管道文件的大小,发现其大小依旧为 0。这表明即使服务端不读取数据,数据也并未被刷新到磁盘,通信过程始终在内存中进行,与匿名管道通信的机制一致。

#include "com.h"

int main()
{
    // 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
    umask(0);

    // 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,0666是权限设置
    // 如果创建失败,返回错误信息并退出程序
    if (mkfifo(FILE_NAME, 0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }

    // 以只读方式打开命名管道文件
    // 如果打开失败,返回错误信息并退出程序
    int fd = open(FILE_NAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 2;
    }

    // 定义一个字符数组用于存储读取到的消息
    char msg[128];

    // 无限循环,持续从命名管道中读取数据
    while (1)
    {
        //服务端不读取管道信息

    }

    // 关闭命名管道文件描述符,释放资源
    close(fd);

    return 0;
}

五、用命名管道实现派发计算任务

        客户端可以给服务端派发计算任务,比如双操作数的加减乘除。服务端收到信息后,进行计算并打印结果。代码如下:

#include "com.h"

int main()
{
    // 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
    umask(0);

    // 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,0666是权限设置
    // 如果创建失败,返回错误信息并退出程序
    if (mkfifo(FILE_NAME, 0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }

    // 以只读方式打开命名管道文件
    // 如果打开失败,返回错误信息并退出程序
    int fd = open(FILE_NAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 2;
    }

    // 定义一个字符数组用于存储读取到的消息
    char msg[128];

    // 无限循环,持续从命名管道中读取数据
    while (1)
    {
        // 每次读取前将msg数组清空,避免残留数据干扰
        msg[0] = '\0';

        // 从命名管道中读取数据,sizeof(msg)-1确保不会超出数组范围
        ssize_t s = read(fd, msg, sizeof(msg) - 1);

        // 如果读取到有效数据(字节数大于0)
        if (s > 0)
        {
            // 在消息末尾添加字符串结束符,确保输出正确
            msg[s] = '\0';

            // 打印客户端发送的消息
            printf("client: %s\n", msg);

            // 定义支持的运算符字符串
            char *lable = "+-*/%";
            // 指向消息字符串的指针
            char *p = msg;
            // 用于存储运算符类型的标志变量
            int flag = 0;

            // 遍历消息字符串,查找运算符
            while (*p)
            {
                switch (*p)
                {
                case '+':
                    flag = 0; // 加法运算符
                    break;
                case '-':
                    flag = 1; // 减法运算符
                    break;
                case '*':
                    flag = 2; // 乘法运算符
                    break;
                case '/':
                    flag = 3; // 除法运算符
                    break;
                case '%':
                    flag = 4; // 取模运算符
                    break;
                }
                p++;
            }

            // 使用strtok函数按运算符分割字符串,获取第一个操作数
            char *data1 = strtok(msg, "+-*/%");
            // 获取第二个操作数
            char *data2 = strtok(NULL, "+-*/%");

            // 将字符串转换为整数
            int num1 = atoi(data1);
            int num2 = atoi(data2);

            // 定义结果变量
            int ret = 0;

            // 根据运算符标志执行相应的计算
            switch (flag)
            {
            case 0:
                ret = num1 + num2; // 加法
                break;
            case 1:
                ret = num1 - num2; // 减法
                break;
            case 2:
                ret = num1 * num2; // 乘法
                break;
            case 3:
                ret = num1 / num2; // 除法
                break;
            case 4:
                ret = num1 % num2; // 取模
                break;
            }

            // 打印计算结果
            printf("%d %c %d = %d\n", num1, lable[flag], num2, ret);
        }
        // 如果读取到0字节,表示客户端已退出
        else if (s == 0)
        {
            printf("client quit!\n");
            break; // 退出循环
        }
        // 如果读取失败,打印错误信息并退出循环
        else
        {
            printf("read error!\n");
            break;
        }
    }

    // 关闭命名管道文件描述符,释放资源
    close(fd);

    return 0;
}

六、用命名管道实现进程遥控

服务端可以执行客户端发来的命令。比如,客户端输入ls,服务端就列出文件。代码如下:

#include "com.h"

int main()
{
    // 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
    umask(0);

    // 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,0666是权限设置
    // 如果创建失败,返回错误信息并退出程序
    if (mkfifo(FILE_NAME, 0666) < 0)
    {
        perror("mkfifo");
        return 1;
    }

    // 以只读方式打开命名管道文件
    // 如果打开失败,返回错误信息并退出程序
    int fd = open(FILE_NAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 2;
    }

    // 定义一个字符数组用于存储读取到的消息
    char msg[128];

    // 无限循环,持续从命名管道中读取数据
    while (1)
    {
        // 每次读取前将msg数组清空,避免残留数据干扰
        msg[0] = '\0';

        // 从命名管道中读取数据,sizeof(msg)-1确保不会超出数组范围
        ssize_t s = read(fd, msg, sizeof(msg) - 1);

        // 如果读取到有效数据(字节数大于0)
        if (s > 0)
        {
            // 在消息末尾添加字符串结束符,确保输出正确
            msg[s] = '\0';

            // 打印客户端发送的消息
            printf("client: %s\n", msg);

            // 创建子进程
            if (fork() == 0)
            {
                // 子进程执行客户端发送的命令
                execlp(msg, msg, NULL);
                exit(1);
            }

            // 父进程等待子进程完成
            waitpid(-1, NULL, 0);
        }
        // 如果读取到0字节,表示客户端已退出
        else if (s == 0)
        {
            printf("client quit!\n");
            break; // 退出循环
        }
        // 如果读取失败,打印错误信息并退出循环
        else
        {
            printf("read error!\n");
            break;
        }
    }

    // 关闭命名管道文件描述符,释放资源
    close(fd);

    return 0;
}

七、命名管道和匿名管道的区别

1. 创建方式

  • 匿名管道:通过 pipe() 系统调用创建。pipe() 会创建两个文件描述符,一个用于读(pipefd[0]),一个用于写(pipefd[1])。

  • 命名管道:通过 mkfifo() 函数或 mknod() 系统调用创建。命名管道需要指定一个路径名,创建后会在文件系统中出现一个特殊文件。

2. 使用方式

  • 匿名管道:只能在具有亲缘关系的进程之间通信(如父子进程)。通常在父进程中创建管道,然后通过 fork() 创建子进程,父子进程通过管道的读写端进行通信。

  • 命名管道:可以在任意两个进程之间通信,即使这两个进程没有亲缘关系。进程通过打开命名管道文件进行通信。

3. 打开方式

  • 匿名管道:不需要显式打开,创建后直接使用 read()write() 进行操作。

  • 命名管道:需要使用 open() 函数打开,可以指定只读(O_RDONLY)或只写(O_WRONLY)模式。

4. 通信范围

  • 匿名管道:仅限于父子进程之间通信。

  • 命名管道:可以用于任意两个进程之间通信。

5. 生命周期

  • 匿名管道:随进程生命周期结束而消亡。当所有使用管道的进程都退出后,管道也被自动销毁。

  • 命名管道:在文件系统中存在,即使创建它的进程已经退出,其他进程仍然可以打开和使用它。需要显式删除(unlink())才能从文件系统中移除。

6. 性能

  • 匿名管道:通常比命名管道稍微快一点,因为它们不需要文件系统支持。

  • 命名管道:由于涉及文件系统操作,可能会稍微慢一些。

7. 使用场景

  • 匿名管道:适合父子进程之间的简单通信,尤其是当通信是单向且短期的。

  • 命名管道:适合需要在多个不相关进程之间进行通信的场景,尤其是当通信是长期的或需要多个进程同时访问时。


特性匿名管道命名管道
创建方式通过 pipe() 系统调用创建。通过 mkfifo() 函数或 mknod() 系统调用创建。
使用方式仅限父子进程间通信,通常由父进程创建,子进程继承。可在任意两个进程间通信,进程通过打开命名管道文件进行通信。
打开方式不需显式打开,创建后直接使用。需使用 open() 函数打开,可指定只读或只写模式。
通信范围仅限具有亲缘关系的进程间通信。可用于任意两个进程间通信,无论是否有亲缘关系。
生命周期随进程生命周期结束而消亡。在文件系统中存在,需显式删除才能移除。
性能通常比命名管道快,无需文件系统支持。涉及文件系统操作,可能稍慢。
使用场景适合父子进程间简单、短期的单向通信。适合需要长期通信或多进程同时访问的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南风与鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值