MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言命名管道的文件操作

2023-11-176.0k 阅读

1. 命名管道基础概念

在Linux系统中,命名管道(Named Pipe),也称为FIFO(First-In-First-Out),是一种特殊类型的文件,它为不相关进程之间提供了一种单向或双向通信的机制。与普通管道不同,命名管道在文件系统中有一个对应的文件名,这使得即使没有共同祖先的进程也能通过该文件名进行数据传递。

普通管道是一种临时对象,存在于内存中,它依赖于进程关系(通常是父子进程),并且在创建它的进程结束后就会消失。而命名管道以文件的形式存在于文件系统中,只要它不被删除,就可以一直使用,不同进程可以通过打开、读写这个文件来进行通信。

命名管道遵循先进先出的原则,即先写入的数据会先被读出。这确保了数据在管道中的顺序性,对于需要按顺序处理数据的应用场景非常有用。

2. 创建命名管道

在Linux C语言编程中,我们可以使用 mkfifo 函数来创建命名管道。mkfifo 函数的原型如下:

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

int mkfifo(const char *pathname, mode_t mode);
  • pathname:这是要创建的命名管道的路径名,也就是在文件系统中显示的文件名。
  • mode:用于指定命名管道的权限,其设置方式与 open 函数中权限设置类似,例如 0666 表示所有者、组和其他用户都有读写权限。

下面是一个简单的示例代码,展示如何创建一个命名管道:

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

int main() {
    const char *fifo_name = "my_fifo";
    int result = mkfifo(fifo_name, 0666);
    if (result == -1) {
        perror("mkfifo");
        return EXIT_FAILURE;
    }
    printf("Named pipe created successfully.\n");
    return EXIT_SUCCESS;
}

在上述代码中,我们尝试创建一个名为 my_fifo 的命名管道,权限设置为 0666。如果 mkfifo 函数返回 -1,表示创建失败,通过 perror 函数打印错误信息。

3. 打开命名管道

创建好命名管道后,进程需要打开它才能进行读写操作。我们使用 open 函数来打开命名管道,其原型为:

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

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:命名管道的路径名。
  • flags:打开模式,常见的有 O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)等。对于命名管道,当以 O_WRONLY 模式打开时,如果没有进程以读模式打开该管道,open 函数会阻塞,直到有进程以读模式打开;当以 O_RDONLY 模式打开时,如果没有进程以写模式打开该管道,open 函数也会阻塞,直到有进程以写模式打开。为了避免这种阻塞情况,可以使用 O_NONBLOCK 标志,这样在没有相应读写进程时,open 函数不会阻塞,而是立即返回 -1 并设置 errnoENXIO

以下是打开命名管道的示例代码:

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

int main() {
    const char *fifo_name = "my_fifo";
    int fd = open(fifo_name, O_WRONLY);
    if (fd == -1) {
        perror("open");
        return EXIT_FAILURE;
    }
    printf("Named pipe opened successfully for writing.\n");
    close(fd);
    return EXIT_SUCCESS;
}

在这个示例中,我们以只写模式打开名为 my_fifo 的命名管道。如果打开失败,通过 perror 函数打印错误信息。

4. 写入命名管道

打开命名管道后,就可以进行写入操作。我们使用 write 函数来向命名管道写入数据,其原型为:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • fd:命名管道的文件描述符,由 open 函数返回。
  • buf:指向要写入数据的缓冲区指针。
  • count:要写入的字节数。

下面是一个完整的示例,创建命名管道并向其写入数据:

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

#define BUFFER_SIZE 1024

int main() {
    const char *fifo_name = "my_fifo";
    int result = mkfifo(fifo_name, 0666);
    if (result == -1 && errno != EEXIST) {
        perror("mkfifo");
        return EXIT_FAILURE;
    }
    int fd = open(fifo_name, O_WRONLY);
    if (fd == -1) {
        perror("open");
        return EXIT_FAILURE;
    }
    char buffer[BUFFER_SIZE] = "Hello, named pipe!";
    ssize_t bytes_written = write(fd, buffer, strlen(buffer));
    if (bytes_written == -1) {
        perror("write");
        close(fd);
        return EXIT_FAILURE;
    }
    printf("Bytes written: %zd\n", bytes_written);
    close(fd);
    return EXIT_SUCCESS;
}

在上述代码中,我们首先创建命名管道(如果管道已存在,忽略错误),然后以只写模式打开它。接着,我们定义一个字符串缓冲区并写入命名管道,最后关闭文件描述符。

5. 读取命名管道

与写入操作类似,读取命名管道使用 read 函数,其原型为:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • fd:命名管道的文件描述符。
  • buf:用于存储读取数据的缓冲区指针。
  • count:要读取的最大字节数。

以下是从命名管道读取数据的示例代码:

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

#define BUFFER_SIZE 1024

int main() {
    const char *fifo_name = "my_fifo";
    int fd = open(fifo_name, O_RDONLY);
    if (fd == -1) {
        perror("open");
        return EXIT_FAILURE;
    }
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE - 1);
    if (bytes_read == -1) {
        perror("read");
        close(fd);
        return EXIT_FAILURE;
    }
    buffer[bytes_read] = '\0';
    printf("Data read from named pipe: %s\n", buffer);
    close(fd);
    return EXIT_SUCCESS;
}

在这个示例中,我们以只读模式打开命名管道,然后从管道中读取数据到缓冲区,并在读取完成后在缓冲区末尾添加字符串结束符 '\0',最后打印读取到的数据并关闭文件描述符。

6. 命名管道的阻塞与非阻塞模式

如前文所述,命名管道在打开和读写操作时默认是阻塞的。这意味着当以 O_WRONLY 模式打开命名管道时,如果没有进程以读模式打开该管道,open 函数会阻塞,直到有进程以读模式打开;当以 O_RDONLY 模式打开时,如果没有进程以写模式打开该管道,open 函数也会阻塞。

对于读写操作,当管道已满(对于写操作)或者管道为空(对于读操作)时,相应的 writeread 函数也会阻塞。

然而,我们可以通过在 open 函数中使用 O_NONBLOCK 标志来改变这种行为,使操作变为非阻塞。在非阻塞模式下,open 函数在没有相应读写进程时不会阻塞,而是立即返回 -1 并设置 errnoENXIO。对于读写操作,当管道已满(写操作)或者为空(读操作)时,writeread 函数不会阻塞,而是立即返回 -1 并设置 errnoEAGAINEWOULDBLOCK

以下是一个展示非阻塞模式打开命名管道的示例:

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

int main() {
    const char *fifo_name = "my_fifo";
    int fd = open(fifo_name, O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        if (errno == ENXIO) {
            printf("No process has opened the pipe for writing yet.\n");
        } else {
            perror("open");
        }
        return EXIT_FAILURE;
    }
    printf("Named pipe opened in non - blocking mode.\n");
    close(fd);
    return EXIT_SUCCESS;
}

在这个示例中,我们以只读和非阻塞模式打开命名管道。如果打开失败且 errnoENXIO,表示没有进程以写模式打开该管道。

7. 命名管道的应用场景

命名管道在很多实际应用场景中都非常有用。

  • 进程间通信:不同进程之间可以通过命名管道进行数据传递,例如一个日志记录进程可以通过命名管道接收来自其他进程发送的日志信息,然后进行统一的记录和管理。
  • 数据处理流水线:可以构建数据处理的流水线,一个进程将数据写入命名管道,后续的进程依次从管道中读取数据进行处理,如数据采集进程将采集到的数据写入管道,数据分析进程从管道中读取数据进行分析。
  • 服务器 - 客户端模型:在简单的服务器 - 客户端模型中,客户端可以通过命名管道向服务器发送请求,服务器从管道中读取请求并处理,然后将结果通过管道返回给客户端。

8. 命名管道与其他 IPC 机制的比较

与其他进程间通信(IPC)机制相比,命名管道有其独特的特点。

  • 与普通管道相比:普通管道只能在具有共同祖先的进程(如父子进程)之间使用,且是临时对象,存在于内存中。而命名管道可以在不相关进程之间使用,并且以文件形式存在于文件系统中,具有更好的持久性。
  • 与消息队列相比:消息队列允许不同类型的消息在队列中存在,接收者可以根据消息类型有选择地接收消息。而命名管道遵循先进先出原则,数据按顺序传递。消息队列通常有更好的消息管理机制,但命名管道在简单的数据传递场景中更为直接和高效。
  • 与共享内存相比:共享内存是最快的 IPC 机制,它允许进程直接访问共享的内存区域,避免了数据的拷贝。然而,共享内存需要额外的同步机制(如信号量)来保证数据的一致性。命名管道则通过自身的先进先出特性和文件操作的同步性,在一定程度上简化了同步问题,适合对数据顺序要求较高且对性能要求不是极致的场景。

9. 命名管道的局限性

尽管命名管道在进程间通信中非常有用,但也存在一些局限性。

  • 单向通信限制:默认情况下,命名管道是单向的。虽然可以通过创建两个命名管道来实现双向通信,但这增加了编程的复杂性。
  • 缓冲区大小限制:命名管道有一个固定的缓冲区大小,当写入的数据量超过缓冲区大小时,write 函数会阻塞,直到有足够的空间。这可能会影响数据传输的效率,尤其是在大数据量传输的场景下。
  • 性能问题:与共享内存等直接内存访问的 IPC 机制相比,命名管道通过文件系统进行数据传递,存在一定的性能开销,在对性能要求极高的场景下可能不太适用。

10. 示例综合应用

下面我们通过一个完整的示例,展示如何使用命名管道实现两个不相关进程之间的双向通信。我们将创建两个程序,一个作为发送端,另一个作为接收端。

发送端代码(sender.c)

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

#define BUFFER_SIZE 1024
#define FIFO1 "fifo1"
#define FIFO2 "fifo2"

int main() {
    int fd1 = open(FIFO1, O_WRONLY);
    if (fd1 == -1) {
        perror("open FIFO1 for writing");
        return EXIT_FAILURE;
    }
    int fd2 = open(FIFO2, O_RDONLY);
    if (fd2 == -1) {
        perror("open FIFO2 for reading");
        close(fd1);
        return EXIT_FAILURE;
    }
    char send_buffer[BUFFER_SIZE] = "Hello from sender!";
    ssize_t bytes_written = write(fd1, send_buffer, strlen(send_buffer));
    if (bytes_written == -1) {
        perror("write to FIFO1");
        close(fd1);
        close(fd2);
        return EXIT_FAILURE;
    }
    printf("Bytes written to FIFO1: %zd\n", bytes_written);
    char receive_buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(fd2, receive_buffer, BUFFER_SIZE - 1);
    if (bytes_read == -1) {
        perror("read from FIFO2");
        close(fd1);
        close(fd2);
        return EXIT_FAILURE;
    }
    receive_buffer[bytes_read] = '\0';
    printf("Data received from FIFO2: %s\n", receive_buffer);
    close(fd1);
    close(fd2);
    return EXIT_SUCCESS;
}

接收端代码(receiver.c)

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

#define BUFFER_SIZE 1024
#define FIFO1 "fifo1"
#define FIFO2 "fifo2"

int main() {
    int fd1 = open(FIFO1, O_RDONLY);
    if (fd1 == -1) {
        perror("open FIFO1 for reading");
        return EXIT_FAILURE;
    }
    int fd2 = open(FIFO2, O_WRONLY);
    if (fd2 == -1) {
        perror("open FIFO2 for writing");
        close(fd1);
        return EXIT_FAILURE;
    }
    char receive_buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(fd1, receive_buffer, BUFFER_SIZE - 1);
    if (bytes_read == -1) {
        perror("read from FIFO1");
        close(fd1);
        close(fd2);
        return EXIT_FAILURE;
    }
    receive_buffer[bytes_read] = '\0';
    printf("Data received from FIFO1: %s\n", receive_buffer);
    char send_buffer[BUFFER_SIZE] = "Hello back from receiver!";
    ssize_t bytes_written = write(fd2, send_buffer, strlen(send_buffer));
    if (bytes_written == -1) {
        perror("write to FIFO2");
        close(fd1);
        close(fd2);
        return EXIT_FAILURE;
    }
    printf("Bytes written to FIFO2: %zd\n", bytes_written);
    close(fd1);
    close(fd2);
    return EXIT_SUCCESS;
}

在这个示例中,我们创建了两个命名管道 fifo1fifo2。发送端通过 fifo1 向接收端发送数据,并通过 fifo2 接收接收端返回的数据。接收端则相反,通过 fifo1 接收数据,通过 fifo2 发送数据。这样就实现了两个不相关进程之间的双向通信。

在实际运行时,需要先确保两个命名管道已创建,可以在终端中使用 mkfifo fifo1 fifo2 命令创建,然后分别运行发送端和接收端程序。

通过以上内容,我们详细介绍了Linux C语言中命名管道的文件操作,包括创建、打开、读写、阻塞与非阻塞模式,以及其应用场景、与其他 IPC 机制的比较、局限性和综合应用示例。希望这些内容能帮助你更好地理解和使用命名管道进行进程间通信。