高级I/O函数二

二、用于读写数据的函数

readv/writev,sendfile,mmap/munmap,splice,tee

2.1、readv和writevv函数

readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。

#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);

vector参数的类型是iovec结构数组;
count参数是vector数组长度,即有多少块内存数据需要从fd读出或写入到fd;

两个函数的返回值:
若成功则返回已读、写的字节数,若出错则返回-1
struct iovec {
    void      *iov_base;      /* starting address of buffer */
    size_t    iov_len;        /* size of buffer */
};

在这里插入图片描述
writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

readv则将读入的数据按上述同样顺序散布到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。

2.2、sendfile函数(零拷贝)

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

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

参数:
in_fd参数是待读出内容的文件描述符;
out_fd参数是待写入内容的文件描述符;
offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置;
count参数指定文件描述符in_fd和out_fd之间传输的字节数。

注意:
in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;
而out_fd必须是一个socket;

由此可见,sendfile几乎是专门为在网络上传输文件而设计的。

传统的read/write方式进行socket传输

首先我们来看看传统的read/write方式进行socket的传输。

当需要对一个文件进行传输的时候,具体流程细节如下:

1:调用read函数,文件数据copy到内核缓冲区
2:read函数返回,文件数据从内核缓冲区copy到用户缓冲区
3:write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区
4:数据从socket缓冲区copy到相关协议引擎。

在这个过程中发生了四次copy操作:
硬盘->内核->用户->socket缓冲区(内核)->协议引擎。

sendfile的工作原理

1、系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到 kernel buffer,然后数据被 kernel 直接拷贝到另外一个与 socket 相关的 kernel buffer。这里没有 用户态和核心态 之间的切换,在内核中直接完成了从一个 buffer 到另一个 buffer 的拷贝。
2、DMA 把数据从 kernel buffer 直接拷贝给协议栈,没有切换,也不需要数据从用户态和核心态,因为数据就在 kernel 里。

注:
DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。

2.3、mmap/munmap函数(零拷贝)

mmap 函数用于申请一段内存空间,我们将这块内存作为进程间通信的共享内存,也可以将文件映射到其中,munmap则释放由mmap申请的这块内存空间。

mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。在要求高性能的应用中比较常用。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。

它们的定义如下:

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

参数:

  • start:start参数允许用户使用某个特定的地址作为这段内存的起始地址。如果他被设置为NULL,则系用自动分配一个地址。

  • fd:为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间进行通信)。

  • length:是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。

  • prot:指定空想内存的访问权限。可取如下几个值的或:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(不可访问)。

  • flag:由以下几个常值指定:MAP_SHARED、MAP_PRIVATE、MAP_FIXED,其中,MAP_SHARED,MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。

  • offset:一般设为0,表示从文件头开始映射。

  • addr:指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。

函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。

mmap的作用是映射文件描述符和指定文件的(off_t off)区域至调用进程的(addr,addr *len)的内存区域,如下图所示:

在这里插入图片描述
在这里插入图片描述

2.4、splice函数(零拷贝)

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

#include <fcntl.h>
ssize_t splice(int fdin, loff_t *offin, int fdout, loff_t *offout, size_t len, unsigned int flags);

参数意义:

fdin参数:待读取数据的文件描述符。
offin参数:指示从输入数据的何处开始读取,为NULL表示从当前位置。如果fdin是一个管道描述符,则offin必须为NULL。
fdout参数:待写入数据的文件描述符。
offout参数:同offin,不过用于输出数据。
len参数:指定移动数据的长度。
flags参数:表示控制数据如何移动,可以为以下值的按位或:

  • SPLICE_F_MOVE:按整页内存移动数据,存在bug,自内核2.6.21后,实际上没有效果。
  • SPLICE_F_NONBLOCK:非阻塞splice操作,实际会受文件描述符本身阻塞状态影响。
  • SPLICE_F_MORE:提示内核:后续splice将调用更多数据。
  • SPLICE_F_GIFT:对splice没有效果。

fdin和fdout必须至少有一个是管道文件描述符。

返回值:

  • 返回值>0:表示移动的字节数。
  • 返回0:表示没有数据可以移动,如果从管道中读,表示管道中没有被写入数据。
  • 返回-1;表示失败,并设置errno。

errno值如下:

  • EBADF:描述符有错。
  • EINVAL:目标文件不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道描述符。
  • ENOMEM:内存不够。
  • ESPIPE:某个参数是管道描述符,但其偏移不是NULL。

2.5、tee函数(零拷贝)

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

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

参数意义:

  • fdin参数:待读取数据的文件描述符。
  • fdout参数:待写入数据的文件描述符。
  • len参数:表示复制的数据的长度。
  • flags参数:同splice( )函数。

fdin和fdout必须都是管道文件描述符。

返回值:

  • 返回值>0:表示复制的字节数。
  • 返回0:表示没有复制任何数据。
  • 返回-1:表示失败,并设置errno。

2.6、splice()和tee()实例

/*splice()和tee()实现将文件"./1.txt"同时拷贝到文件"./2.txt"和"./3.txt"中*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
    int fd1 = open("./1.txt", O_RDONLY);
    int fd2 = open("./2.txt", O_RDWR| O_CREAT | O_TRUNC, 0666);
    int fd3 = open("./3.txt", O_RDWR| O_CREAT | O_TRUNC, 0666);

    /*用于向"./2.txt"输入数据*/
    int pipefd2[2];
    /*用于向"./3.txt"输入数据*/
    int pipefd3[2];
    pipe(pipefd2);
    pipe(pipefd3);

    /*将fd1文件的内容输入管道pipefd2中*/
    splice(fd1, NULL, pipefd2[1], NULL, 10086, SPLICE_F_MORE);
    /*将管道pipefd2的内容复制到管道pipefd3中,不消耗管道pipefd2上的数据,管道pipefd2上的数据可以用于后续操作*/
    tee(pipefd2[0], pipefd3[1], 10086, SPLICE_F_NONBLOCK);
    /*将管道pipefd2的内容写入fd2文件中*/
    splice(pipefd2[0], NULL, fd2, NULL, 10086, SPLICE_F_MORE);
    /*将管道pipefd3的内容写入fd3文件中*/
    splice(pipefd3[0], NULL, fd3, NULL, 10086, SPLICE_F_MORE);

    close(fd1);
    close(fd2);
    close(fd3);
    close(pipefd2[0]);
    close(pipefd2[1]);
    close(pipefd3[0]);
    close(pipefd3[1]);
    return 0;
}

三、用于I/O行为和属性

3.1、fcntl函数

fcntl函数,正如其名字一样(file control),提供了对已打开的文件描述符的各种控制操作,以改变已打开文件的的各种属性。另外一种常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fcntl能够执行更多的控制。但是对于控制恩健描述符常用的属性和行为,fcntl函数是由POSIX规范指定的首选方法。

#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);  

fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:
在这里插入图片描述
(1)F_DUPFD

与dup函数功能一样,复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件。

(2)F_GETFD

读取文件描述符close-on-exec标志

(3)F_SETFD

将文件描述符close-on-exec标志设置为第三个参数arg的最后一位

(4)F_GETFL

获取文件打开方式的标志,标志值含义与open调用一致

(5)F_SETF

设置文件打开方式为arg指定方式


(6)F_SETLK

此时fcntl函数用来设置或释放锁。当short_l_type为F_RDLCK为读锁,F_WDLCK为写锁,F_UNLCK为解锁。

如果锁被其他进程占用,则返回-1;

这种情况设的锁遇到锁被其他进程占用时,会立刻停止进程。

(7)F_SETLKW

此时也是给文件上锁,不同于F_SETLK的是,该上锁是阻塞方式。当希望设置的锁因为其他锁而被阻止设置时,该命令会等待相冲突的锁被释放。

(8)F_GETLK

第3个参数lock指向一个希望设置的锁的属性结构,如果锁能被设置,该命令并不真的设置锁,而是只修改lock的l_type为F_UNLCK,然后返回该结构体。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构。

文件记录锁

文件记录锁是fcntl函数的主要功能。

记录锁:
实现只锁文件的某个部分,并且可以灵活的选择是阻塞方式还是立刻返回方式

当fcntl用于管理文件记录锁的操作时,第三个参数指向一个struct flock *lock的结构体。

struct flock  
{  
    short_l_type;    /*锁的类型*/  
    short_l_whence;  /*偏移量的起始位置:SEEK_SET,SEEK_CUR,SEEK_END*/  
    off_t_l_start;     /*加锁的起始偏移*/  
    off_t_l_len;    /*上锁字节*/  
    pid_t_l_pid;   /*锁的属主进程ID */  
};   

short_l_type用来指定设置共享锁(F_RDLCK,读锁)还是互斥锁(F_WDLCK,写锁).

当short_l_type的值为F_UNLCK时,传入函数中将解锁。

每个进程可以在该字节区域上设置不同的读锁。

但给定的字节上只能设置一把写锁,并且写锁存在就不能再设其他任何锁,且该写锁只能被一个进程单独使用。

这是多个进程的情况。

单个进程时,文件的一个区域上只能有一把锁,若该区域已经存在一个锁,再在该区域设置锁时,新锁会覆盖掉旧的锁,无论是写锁还时读锁。

l_whence,l_start,l_len三个变量来确定给文件上锁的区域。

l_whence确定文件内部的位置指针从哪开始,l_star确定从l_whence开始的位置的偏移量,两个变量一起确定了文件内的位置指针先所指的位置,即开始上锁的位置,然后l_len的字节数就确定了上锁的区域。

特殊的,当l_len的值为0时,则表示锁的区域从起点开始直至最大的可能位置,就是从l_whence和l_start两个变量确定的开始位置开始上锁,将开始以后的所有区域都上锁。

为了锁整个文件,我们会把l_whence,l_start,l_len都设为0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值