Linux中的高级IO函数(二)readv & writev & sendfile & mmap & splice & tee

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

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

本节接着介绍第二类

一、readv函数与writev函数

readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。它们的定义如下:

#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec* vector, int count)ssize_t writev(int fd, const struct iovec* vector, int count);
  • fd:文件描述符,用于指定读取或写入数据的文件或套接字。
  • vector:一个指向 iovec 结构体数组的指针,每个结构体描述一个缓冲区的位置和长度。
  • countiovec 结构体数组的长度,即缓冲区的数量。
  • 成功时返回操作的字节数,失败时返回-1并且置errno

struct iovec 结构体定义:

struct iovec {
    void  *iov_base; // 缓冲区的起始地址
    size_t iov_len;  // 缓冲区的长度
};

举个分散写的例子:

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

int main() {
    // 打开文件
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

    // 准备多个缓冲区
    char buf1[] = "This is the first buffer.\n";
    char buf2[] = "This is the second buffer.\n";
    char buf3[] = "This is the third buffer.\n";

    // 构建 struct iovec 数组
    struct iovec iov[3];
    iov[0].iov_base = buf1;
    iov[0].iov_len = strlen(buf1);
    iov[1].iov_base = buf2;
    iov[1].iov_len = strlen(buf2);
    iov[2].iov_base = buf3;
    iov[2].iov_len = strlen(buf3);

    // 使用 writev() 将多个缓冲区的内容一次性写入文件
    ssize_t bytes_written = writev(fd, iov, 3);

    printf("Total bytes written: %zd\n", bytes_written);

    // 关闭文件
    close(fd);
    return 0;
}

image-20240422111634350

二、sendfile函数

sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。

#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
  • out_fd:输出文件描述符,用于指定要写入数据的目标文件。
  • in_fd:输入文件描述符,用于指定从中读取数据的源文件。
  • offset:用于指定输入文件中的起始位置,如果为 NULL,表示从当前文件偏移量开始读取。
  • count:要传输的字节数。
  • sendfile成功时返回传输的字节数,失败则返回-1并设置errno。

这个函数就很简单,主要是因为他的高效率,不需要在用户空间操作。所以我们就不举例子了。

三、mmap函数与munmap函数

#include <sys/mman.h>

void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void* start, size_t length);
  • start:指定映射的起始地址,通常设置为 NULL,让系统自动选择合适的地址。

  • length:指定映射区域的长度,以字节为单位。如果指定的长度超过文件的实际长度,系统会自动增加文件长度,但是并不会实际读写磁盘上的数据。

  • prot

    :指定映射区域的保护方式,可以是以下几种组合:

    • PROT_READ:映射区域可读。
    • PROT_WRITE:映射区域可写。
    • PROT_EXEC:映射区域可执行。
    • PROT_NONE:映射区域不可访问。
  • flags

    :指定映射区域的选项,可以是以下几种组合:

    • MAP_SHARED:映射区域可共享,对映射区域的修改会影响到文件内容。
    • MAP_PRIVATE:映射区域私有,对映射区域的修改不会影响到文件内容,而是在内存中进行。
    • MAP_FIXED:强制将映射区域放置到 start 参数指定的地址处,如果无法满足,则映射失败。
    • MAP_ANONYMOUS:不与任何文件关联,创建一个匿名映射区域,常用于创建共享内存区域。
  • fd:指定要映射的文件的文件描述符,如果不需要映射文件,可以设置为 -1

  • offset:指定文件中的偏移量,表示从文件的哪个位置开始映射数据,通常设置为 0,表示从文件的起始位置开始。

mmap() 函数成功调用后,将返回指向映射区域的指针,如果映射失败,则返回 MAP_FAILED 宏。需要注意的是,映射区域的大小和对齐方式取决于系统和硬件的限制,在使用时需要仔细考虑。此外,映射区域需要通过 munmap() 函数进行解除映射,以释放相关资源。

3.1、将一个文件映射到内存中

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

int main() {
    int fd;
    struct stat sb;
    void *mapped;

    // 打开文件
    fd = open("output.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 获取文件状态
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        exit(EXIT_FAILURE);
    }
    
    // 检查文件大小,文件大小不能为0,否则的话
    if (sb.st_size == 0) {
        fprintf(stderr, "File size is 0.\n");
        exit(EXIT_FAILURE);
    }
    
    // 将文件映射到内存中
    mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    // 关闭文件
    close(fd);

    // 修改映射区域中的内容
    strcpy((char*)mapped, "Hello, memory mapped file!\n");

    // 输出映射区域的内容
    printf("%s", (char*)mapped);

    // 解除映射
    if (munmap(mapped, sb.st_size) == -1) {
        perror("munmap");
        exit(EXIT_FAILURE);
    }

    return 0;
}

image-20240422115246020

3.2、共享内存的映射

这个函数还有一个更大的作用就是将将共享内存区域映射到当前进程的地址空间中。以实现多进程之间的信息交换。这部分在后面的多进程中会提到,这里就先不讲了。

四、splice函数

splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。

#include <fcntl.h>

ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
  • fd_in: 源文件描述符,从该文件描述符读取数据。
  • off_in: 源文件偏移量指针,指向源文件的读取偏移量, NULL:表示从当前偏移位置开始读取。
  • fd_out: 目标文件描述符,向该文件描述符写入数据。
  • off_out: 目标文件偏移量指针,指向目标文件的写入偏移量, NULL:表示从当前偏移位置开始写入。
  • len: 要移动的数据长度。
  • flags: 控制函数行为的标志,可以为 0 或以下标志的按位或:
    • SPLICE_F_MOVE: 合适的话,按照整页移动数据
    • SPLICE_F_NONBLOCK: 非阻塞的splice操作,但实际效果还是会受文件描述符本身的阻塞状态的影响.
    • SPLICE_F_MORE:给内核一个下面还有数据的提示

该函数可以在文件描述符之间移动数据而不涉及用户空间的数据拷贝,因此对于大量数据的传输操作可以提高效率。使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。

看一个回射服务器的示例:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <error.h>
#define __USE_GNU 1

int main(int argc, const char *argv[])
{
    const char *ip = "127.0.0.1";
    const int port = 8080;

    int ret;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(sockfd, (struct sockaddr *)&address, sizeof(address));
    listen(sockfd, 5);
    int connfd;

    while (1)
    {
        struct sockaddr_in peer;
        bzero(&peer, sizeof(peer));
        socklen_t len = sizeof(peer);

        connfd = accept(sockfd, (struct sockaddr *)&peer, &len);
        if (connfd > 0)
        {
            int pipefd[2];
            pipe(pipefd);
            /*将connfd上流入的客户端数据定向到管道中*/
            splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
            /*将管道中的数据定向到connfd的客户端文件描述符上*/
            splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
        }
        close(connfd);
    }
    close(sockfd);
    return 0;
}

image-20240422193639479

五、tee函数

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作

#include <fcntl.h>

ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
  • fd_infd_out必须都是管道文件描述符

  • 其他参数与splice函数一样

tee函数成功时返回在两个文件描述符之间复制的数据字节数。返回0表示没有复制任何数据。tee失败时返回-1并设置errno。

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

LyaJpunov

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

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

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

打赏作者

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

抵扣说明:

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

余额充值