手把手写C++服务器(27):五大文件描述符零拷贝控制总结

Posted 沉迷单车的追风少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器(27):五大文件描述符零拷贝控制总结相关的知识,希望对你有一定的参考价值。

 本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】 

前言:前文《手把手写C++服务器(26):常用I/O操作、创建文件描述符》、《手把手写C++服务器(25):万物皆可文件之socket fd》总结了常见的IO操作和文件描述符的相关知识,这一讲将详解五大数据读写函数:readv/writev、sendfile、mmap/munnmap、splice、tee和控制IO行为和属性的函数fcntl()。

目录

基本读写函数read()/write()和基本打开关闭函数open()/close()

分散读readv()和集中写writev()

参数详解:

内核操作零拷贝传递数据:sendfile()

参数详解

测试源码

共享内存映射mmap()/munmap()

参数详解

函数返回

与read/write对比

两个文件描述符之间零拷贝移动数据:splice()

参数详解

函数返回

两管道文件描述符之间零拷贝复制数据:tee()

函数返回

文件描述符控制函数:fcntl()

参数详解

参考:


基本读写函数read()/write()和基本打开关闭函数open()/close()

在上一讲《手把手写C++服务器(26):常用I/O操作、创建文件描述符》讲的很详细了,如果还不清楚什么是文件描述符fd的同学可以参看《手把手写C++服务器(25):万物皆可文件之socket fd

分散读readv()和集中写writev()

readv():分散读,将数据从文件描述符中读到分散的内存块中。

writev():集中写,将多块分散的内存数据一并写入文件描述符中。

操作成功返时返回读出/写入fd的字节数,失败则返回-1并设置errno。相当于简化版的recvmsg和sendmsg函数。

#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t write(int fd, const struct iovec* vector, int count);

参数详解:

fd:目标文件描述符

vector:用以描述一块内存区,iovec结构体定义如下:

struct iovec {
    void* iov_base;    // 内存的起始地址
    size_t iov_len;    // 内存的长度
}

count:vector数组的长度。

内核操作零拷贝传递数据:sendfile()

在两个文件描述符之间直接传递数据,完全在内核中操作,从而避免内核缓冲区和用户缓冲区之间的数据拷贝,高效快捷,称之为零拷贝。

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);

参数详解

  • out_fd:待写入内容的文件描述符。必须是一个socket。
  • in_fd:待读出内容的文件描述符。必须指向真实的文件,不能是socket和管道。
  • offset:指定从读入文件流的哪个位置开始读。如果为空,则从文件流默认的起始位置开始读。
  • count:指定文件描述符in_fd和out_fd之间传输的字节数。

注意:从上面in_fd和out_fd的要求来看,sendfile几乎是专门为socket准备的。

测试源码

(来自游双《Linux高性能服务器》)

其实先open一个真实文件,就是把接受到的文件用sendfile拷贝到真实的文件中。这样的拷贝没有缓冲区,非常高效。

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
#include<string.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<errno.h>

int main(int argc,char *argv[])
{
    if(argc<=3)
    {
        printf("usage:%s ip_address port_number filename\\n",basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);
    const char* file_name = argv[3];

    int filefd = open(file_name,O_RDONLY);
    assert(filefd>0);

    struct stat stat_buf;
    fstat(filefd,&stat_buf);

    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 sock = socket(PF_INET,SOCK_STREAM,0);
    assert(sock>=0);

    int ret = bind(sock,(struct sockaddr*)&address,sizeof(address));
    assert(ret!=-1);

    ret = listen(sock,5);
    assert(ret!=-1);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);

    int connfd = accept(sock,(struct sockaddr*)&client,&client_addrlength);

    if(connfd<0)
    {
        printf("errno is %d\\n",errno);
    }
    else 
    {
        sendfile(connfd,filefd,NULL,stat_buf.st_size);
        close(connfd);
    }
    close(sock);
    return 0;
}

共享内存映射mmap()/munmap()

mmap:申请一段内存空间,可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。

munmap:释放由mmap创建的内存空间。

#include <sys/mman.h>
void* mmap(void* start, size_t length, int port, int flags, int fd, off_t offset);
int munmap(void* start, size_t length);

参数详解

start:要映射的起始地址,这里一般用NULL,这样系统会在0地址附近随机分配一块内存。

length:所要映射的文件长度。

port:所映射的文件的权限,其中包含(PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONE)这四种参数。

flags:参数是映射类型,常见类型如下:

  • MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
  • MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
  • MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
  • MAP_DENYWRITE //这个标志被忽略。
  • MAP_EXECUTABLE //同上
  • MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
  • MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
  • MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
  • MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
  • MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
  • MAP_FILE //兼容标志,被忽略。
  • MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
  • MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
  • MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

fd:文件描述符,一般由open系统调用获得。

offset:偏移量。设置文件从何处开始映射。

函数返回

mmap函数的返回值为void *,那么如果映射成功则会返回映射的首地址,如果出错则返回常数MAP_FAILED。

munmap解除映射,解除成功返回0,出错返回-1并设置errno。

与read/write对比

对于小文件的读写操作,内核完全有可能直接从内核缓冲区读写数据并不见得会比mmap慢,但是对于大文件且频繁读写的操作,mmap会比read/write要快。

两个文件描述符之间零拷贝移动数据: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;如果off_in不是一个管道文件描述符(比如socket),那么off_in表示从输入数据流的何处开始读取数据,此时若为NULL,则从输入数据流的当前偏移位置读入。

fd_out/off_out:与fd_in/off_in相同,不过用于输出数据流。

len:指定移动数据的长度。

flags:控制数据如何移动,它可以设置成下表中的某些值的按位或。常见flags含义如下:

函数返回

调用成功时返回移动的字节数量。它可能返回0,表示没有数据需要移动,这通常发生在从管道中读数据(fd_in是管道文件描述符)而该管道没有被写入任何数据时。
失败时返回-1,并设置errno。常见的errno如下表所示。

两管道文件描述符之间零拷贝复制数据:tee()

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。与splice函数不同,它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。特别注意:fd_in和fd_out必须都是管道文件描述符。

#include<fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len,unsigned int flags);

函数返回

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

文件描述符控制函数:fcntl()

file control,文件描述符控制。与之类似的系统调用是ioctl。

#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);

参数详解

参考:

 

以上是关于手把手写C++服务器(27):五大文件描述符零拷贝控制总结的主要内容,如果未能解决你的问题,请参考以下文章

手把手写C++服务器(26):常用I/O操作创建文件描述符

手把手写C++服务器(25):万物皆可文件之socket fd

手把手写C++服务器(31):服务器性能提升关键——IO复用技术两万字长文

手把手写C++服务器:专栏文章-汇总导航更新中

手把手写C++服务器(35):手撕代码——高并发高QPS技术基石之非阻塞send万字长文

手把手写C++服务器(29):手撕echo回射服务器代码