Linux中的高级IO函数(一)pipe & socketpair & dup

发布于:2024-04-26 ⋅ 阅读:(28) ⋅ 点赞:(0)

Linux提供了很多高级的I/O函数。它们并不像Linux基础I/O函数(比如open和read)那么常用(编写内核模块时一般要实现这些I/O函数),但在特定的条件下却表现出优秀的性能。这些函数大致分为三类:

用于创建文件描述符的函数,包括pipe、socketpair、dup/dup2函数。
用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数。
用于控制I/O行为和属性的函数,包括fcntl函数。

本节先介绍第一类

一、pipe函数

pipe 函数用于创建一个管道,以实现进程间通信。

#include <unistd.h>

int pipe(int fd[2]);

pipe函数创建的这两个文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出。并且,fd[0]只能用于从管道读出数据,fd[1]则只能用于往管道写入数据,而不能反过来使用。如果要实现双向的数据传输,就应该使用两个管道。

默认情况下,这一对文件描述符都是阻塞的,若用read来读取一个空的管道,则read将被阻塞,直到管道内有数据可读。如果应用程序将fd[0]和fd[1]都设置为非阻塞的,则read和write会有不同的行为。

  • 如果管道的写端文件描述符fd[1]的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End Of File,EOF);

  • 如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。

管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量。

下面举一个在多进程程序中管道通信的例子:

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

int main()
{
    int pipefd[2];
    char buf[1024];

    // 创建管道
    if (pipe(pipefd) == -1)
    {
        perror("pipe error! \n");
        return 1;
    }

    for (int i = 0; i < 8; i++)
    {
        // 创建子进程
        pid_t pid = fork();

        if (pid == -1)
        {
            perror("fork error!\n");
            return 1;
        }
        else if (pid == 0)
        {
            // 子进程关闭写端
            close(pipefd[1]);
            // 从管道读取数据
            memset(buf, 0, 1024);
            read(pipefd[0], buf, sizeof(buf));
            // 打印数据
            printf("pid = %d :Child process received: %s\n", getpid(), buf);
            // 子进程关闭读取端
            close(pipefd[0]);
            // 子进程直接break,以免创建更多的子进程
            break;
        }
    }

    if (getpid() != 0)
    {
        // 父进程关闭读取端
        close(pipefd[0]);
        // 向管道写入数据
        const char *message = "Hello from parent process";
        for (int i = 0; i < 8; i++)
        {
            write(pipefd[1], message, strlen(message));
            sleep(1);
        }
        // 在所有数据都写入后再关闭写入端
        close(pipefd[1]);
    }

    return 0;
}

生成了八个子进程,八个子进程阻塞在read处,等待父进程的消息,父进程发了八次消息,每次间隔1秒。看一下仿真

image-20240421223438140

二、socketpair函数

socketpair可以用于创建一堆相互连接的套接字,通常用于在进程之间通信

int socketpair(int domain, int type, int protocol, int sv[2]);

参数说明:

  • domain:指定地址族,通常为 AF_UNIX 表示 UNIX 域套接字。
  • type:指定套接字类型,通常为 SOCK_STREAM 表示面向连接的套接字。
  • protocol:指定使用的协议,通常为 0 表示默认协议。
  • sv[2]:用于存放创建的两个套接字的文件描述符的数组。

下面举个多进程的例子

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

int main()
{
    int socketfd[2];
    char buf[1024];

    // 创建一对相互连接的套接字
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, socketfd) == -1) {
        perror("socketpair");
        return -1;
    }


    for (int i = 0; i < 8; i++)
    {
        // 创建子进程
        pid_t pid = fork();

        if (pid == -1)
        {
            perror("fork error!\n");
            return 1;
        }
        else if (pid == 0)
        {
            // 子进程关闭写端
            close(socketfd[1]);
            // 从管道读取数据
            memset(buf, 0, 1024);
            read(socketfd[0], buf, sizeof(buf));
            // 打印数据
            printf("pid = %d :Child process received: %s\n", getpid(), buf);
            // 子进程关闭读取端
            close(socketfd[0]);
            // 子进程直接break,以免创建更多的子进程
            break;
        }
    }

    if (getpid() != 0)
    {
        // 父进程关闭读取端
        close(socketfd[0]);
        // 向管道写入数据
        const char *message = "Hello from parent process";
        for (int i = 0; i < 8; i++)
        {
            write(socketfd[1], message, strlen(message));
            sleep(1);
        }
        // 在所有数据都写入后再关闭写入端
        close(socketfd[1]);
    }

    return 0;
}

例子和上面的 pipe 是一样的,看仿真:

image-20240422101424911

三、dup函数和dup2函数

dup函数和dup2函数可以把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接

#include <unistd.h>

int dup(int file_descriptor);
int dup2(int oldfd, int newfd);

dup函数创建一个新的文件描述符,该新文件描述符和原有文件描述符file_descriptor指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值。

dup2函数可以将 oldfd 重定向到 newfd。如果newfd已经被程序使用,则系统会先将newfd所指的文件关闭。

**注意:**通过dup和dup2创建的文件描述符并不继承原文件描述符的属性,比如close-on-execnon-blocking等。

3.1、使用 dup2 将标准输出重定向到一个文件

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

int main() {
    // 打开或创建文件,以便将标准输入内容写入到该文件中
    int file_fd = open("output.txt", O_RDWR | O_CREAT, 0666);
    if (file_fd == -1) {
        perror("open");
        return 1;
    }

    // 将标准输出重定向到文件描述符指向的文件
	dup2(file_fd, STDOUT_FILENO)
    printf("newfd: hello, world\n");

    const char* buf = "oldfd: hello, world\n";
    write(file_fd, buf, strlen(buf)); 
    close(file_fd);
    return 0;
}

将标准输出重定向到 output.txt 文件,所以printf打印函数打印的字符串直接输出到了文件中,而不是控制台上。

image-20240422104522033

3.2、使用 dup2 将标准输入重定向到一个文件

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

int main() {
    // 打开或创建文件,以便将标准输入内容写入到该文件中
    int file_fd = open("output.txt", O_RDWR | O_CREAT, 0666);
    if (file_fd == -1) {
        perror("open");
        return 1;
    }

	dup2(file_fd, STDIN_FILENO)
    char buff[1024] = {0};
    read(STDIN_FILENO,buff,1024);
    printf("%s",buff);

    close(file_fd);
    return 0;
}

相当于直接从文件中读取数据作为标准输入了

image-20240422104857848

3.2、使用 dup 将标准输出重定向到一个文件

int main() {
    // 打开或创建文件,以便将标准输入内容写入到该文件中
    int file_fd = open("output.txt", O_RDWR | O_CREAT, 0666);
    if (file_fd == -1) {
        perror("open");
        return 1;
    }

    close(STDOUT_FILENO);
    dup(file_fd);

    printf("123");

    close(file_fd);
    return 0;
}

image-20240422110314962