手把手写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()
基本读写函数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);
参数详解
参考:
- 《Linux高性能服务器编程》
- https://blog.csdn.net/u014303647/article/details/82081451
- https://my.oschina.net/u/4870865/blog/4805886
- https://www.cnblogs.com/jialin0x7c9/p/12146321.html
- https://baike.baidu.com/item/mmap/1322217?fr=aladdin
- https://blog.csdn.net/jasonliuvip/article/details/22600569
- https://blog.csdn.net/fengxinlinux/article/details/73522302
- https://blog.csdn.net/fengxinlinux/article/details/51980837
以上是关于手把手写C++服务器(27):五大文件描述符零拷贝控制总结的主要内容,如果未能解决你的问题,请参考以下文章
手把手写C++服务器(25):万物皆可文件之socket fd
手把手写C++服务器(31):服务器性能提升关键——IO复用技术两万字长文