高级I/O函数

我们来介绍几个和网络编程相关的高级I/O函数。
用于创建文件描述符的函数:pipe、dup/dup2
用于读写数据的函数:readv/writev、fendfile、mmap/munmap、splice和tee函数。
用于控制I/O行为和属性的函数:fcntl

1,pipe
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将被阻塞直到有数据可读;如果我们用write系统调用来往一个满的管道中写数据,则write将被阻塞,直到管道有足够多的空闲空间可用。

如果管道的写端文件描述符fd[1]的引用计数减少到0,即没有任何进程往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读到了文件结束标志(EOF);
反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道中读取数据,则针对管道的写端文件描述符fd[1]的write操作失败,并引发SIGPIPE信号。
管道内部传输的数据是字节流,这和TCP字节流的概念相同,但二者又有细微的差别。应用层程序能往一个TCP连接中写入多少字节的数据,取决于对方的接收通告窗口的大小和本端拥塞窗口的大小。而管道本身拥有一个容量控制,它规定如果应用程序不将数据从管道读走的话,该管道最多能写入多少字节的数据。

2,dup和dup2函数
有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接。此时我们就可以用dup和dup2函数来实现。

#include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one,int file_descriptor_two);

dup()
利用dup(),我们可以复制一个文件描述符。传给该函数一个已有的文件描述符,它就会返回一个新的文件描述符,这个新的文件描述符是传给它的描述符的拷贝。即两个文件描述符共享同一个数据结构,例如,如果我们对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。

dup2()
dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。dup2函数成功返回时,目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件。

注意:通过dup和dup2创建的文件描述符并不继承原文件描述符的属性。比如:close-on-exec和non-blocking等。
这里写图片描述

在这段代码中,我们首先创建一个管道,然后fork出一个子进程,在子进程中,我们首先关闭stdout,然后利用dup2让输出重定向到管道的fds[1]中,然后关闭管道的输入端,用execlp进程替换函数把子进程进程映像替换为ls -l.
父进程担任管道的接收端,我们关闭父进程的stdin,然后用dup2输入重定向到fds[0],关闭管道的fds[1],最后通过execlp将父进程的进程映像替换为wc -l.

3,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结构体,该结构体描述一块内存区。count参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd。readv和writev在成功时返回读出/写入fd的字节数,失败返回-1并设置errno。
这两个函数的第二个参数是指向iovec结构数组的一个指针:

 struct iovec
 {
      void *iov_base;
      size_t iov_len;
 };

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

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

这里写图片描述

这里写图片描述

4,sendfile函数
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之间传输的字节数。sendfile成功时返回传输的字节数,失败则返回-1并设置errno。
in_fd必须是一个支持类似mmap函数的文件描述符,即它必须执行真实的文件,不能是socket和管道;而out_fd则必须是一个socket。

如果我们使用read和write函数,需要进行如下几个步骤:
1. 调用read函数,文件数据被copy到内核缓冲区
2. read函数返回,文件数据从内核缓冲区copy到用户缓冲区
3. write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。
4. 数据从socket缓冲区copy到相关协议引擎。

以上细节是传统read/write方式进行网络文件传输的方式,我们可以看到,在这个过程当中,文件数据实际上是经过了四次copy操作:
硬盘—>内核buf—>用户buf—>socket相关缓冲区—>协议引擎

而使用sendfile函数,需要进行如下几个步骤:
1. sendfile系统调用,文件数据被copy至内核缓冲区
2. 再从内核缓冲区copy至内核中socket相关的缓冲区
3. 最后再socket相关的缓冲区copy到协议引擎

对比我们可以得出,相比使用sendfile函数,Read & Write方式带来的性能损耗主要有两点:
1. 不必要的内存拷贝。即多了一次从内核缓冲区到用户缓冲区的数据拷贝。
2. 系统调用带来的额外的用户态/内核态上下文切换。我们知道,使用系统调用会发生从内核态到用户态之间的相互转换。使用read&write方式,多使用了一次系统调用,就多了一次上下文切换,降低了性能。

综上我们可以得出,sendfile函数不但能减少切换次数而且还能减少拷贝次数,是一个非常高效的数据拷贝函数,尤其在传输一个非常大的文件时,sendfile函数的高性能体现的更为明显。因此,在以后我们设计到网络传输文件时,应该使用sendfile函数,提高效率。

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

#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参数用来设置内存段的访问权限。它可以取以下几个值的按位或:
PORT_READ:内存段可读
PORT_WRITE:内存段可写
PORT_EXEC:内存段可执行
PORT_NONE:内存段不能被访问

flags参数控制内存段内容被修改后程序的行为。它可以被设置为下表中国的某些值(这里仅列出了常用的值)的按位或(其中MAP_SHARED和MAP_PRIVATE是互斥的,不能同时指定)。

这里写图片描述

fd参数是被映射文件对应的文件描述符。它一般通过open系统调用获得。offset参数设置从文件的何处开始映射(对于不需要读入整个文件的情况)
mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED((void*)-1)并设置errno。munmap函数成功时返回0,失败则返回-1并设置errno.

内存映射的步骤:
(1)用open系统调用打开文件, 并返回描述符fd.
(2)用mmap建立内存映射, 并返回映射首地址指针start.
(3)对映射(文件)进行各种操作, 显示(printf), 修改(sprintf).
(4)用munmap(void *start, size_t lenght)关闭内存映射.
(5)用close系统调用关闭文件fd.

UNIX网络编程第二卷进程间通信对mmap函数进行了说明。该函数主要用途有三个:
1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

mmap用于把文件映射到内存空间中,简单说mmap就是把一个文件的内容在内存里面做一个映像。映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<—->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。

这里写图片描述

这里写图片描述

使用mmap的注意事项:
1. 创建映射区的过程中,隐含着一次对映射文件的读操作。
2. 当flags为MAP_SHARED时,要求:映射区的权限应<=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!! mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
6. 如果文件偏移量必须为4K的整数倍
mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

使用mmap进行父子间通信
这里写图片描述

这里写图片描述
父子进程的p值相同,说明进程间通信了,共享mmap建立的内存区域。

如果要在不创建文件的的条件下建立匿名映射区,只需要将flags参数改为上述我们介绍的MAP_ANONYMOUS即可。

使用mmap进行无血缘关系间的进程间通信
这里写图片描述

实质上,mmap是内核借助文件帮我们创建了一个映射区,多个进程间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。

6,splice函数
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参数是待输入数据的文件描述符。如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL。如果fd_in不是一个管道文件描述符(比如socket),那么off_in表示从输入数据流的何处开始读取数据。此时,若off_in被设置为NULL,则表示从输入数据流的当前偏移位置读入;若off_in不为NULL,则它指出具体的偏移位置。
fd_out/off_out参数的含义与fd_in/off_in相同,不过用于输出数据流。len参数指定移动数据的长度,flags参数则控制数据如何移动,它可以设置为下表中某些值的按位或。
这里写图片描述

使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(fd_in是管道文件描述符)而管道没有被写入任何数据时。splice函数返回失败时返回-1,并设置errno。

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

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

该函数的参数含义与splice相同(但fd_in和fd_out必须都是管道文件描述符)。tee函数成功时返回在两个文件描述符之间复制的数据数量(字节数)。返回0表示没有复制任何数据。tee失败时返回-1并设置errno。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值