目录
这些函数大致分为三类:
- 用于创建文件描述符的函数,包括pipe、dup/dup2函数
- 用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数
- 用于控制I/O行为和属性的函数,包括fcntl函数
pipe函数
pipe函数可以用于创建一个管道,以实现进程间通信。本章只介绍其基本使用方式。pipe函数的定义如下:
#include<unistd.h>
int pipe(int fd[2]);
pipe函数的参数是一个包含两个int型整数的数组指针。该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。如果失败,则返回-1并设置errno。
通过pipe函数创建的这两个文件描述符fd[0],fd[1]分别构成管道的两端,往fd[1]写入数据可以从fd[0]读出。并且,fd[0]只能用于从管道读出数据,fd[1]则只能用于往管道写入数据,而不能反过来使用。如果要实现双向的数据传输,就应该使用两个管道。默认情况下,这一对文件描述符都是阻塞的。此时如果我们用read函数调用来读取一个空的管道,则read将被阻塞,直到管道内有数据可读;如果我们用write系统调用来往一个满的管道中写入数据,则write亦将被阻塞,直到管道有足够多的空闲空间可用。但如果应用程序将fd[0]和fd[1]都设置为非阻塞的,则read和write会有不同的行为。关于阻塞和非阻塞的讨论,见其他章。如果管道的写端文件描述符fd[1]的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End Of File, EOF);反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发SIGPIPE信号。关于SIGPIPE信号,我们将在其他章节讨论。
管道内部传输的数据是字节流,这和TCP字节流的概念相同。但两者又有细微的区别。应用层程序能往一个TCP连接中写入多少字节的数据,取决于对方的接收通告窗口的大小和本端的拥塞窗口的大小。而管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据。自Linux2.6.11内核起,管道容量的大小默认65536字节。我们可以使用fcntl函数来修改管道容量。
此外,socket的基础API中有一个socketpair函数。它能够方便地创建双向管道。其定义如下:
#include<sys/types.h>
#include<sys/socket.h>
int socketpair(int domain , int type , int protocol , fd[2]);
sockpair前三个参数的含义与socket系统调用的三个参数完全相同,但domain只能使用UNIX本地域协议族AF_UNIX,因为我们仅能在本地使用这个双向管道。最后一个参数则和pipe系统调用的参数一样,只不过socketpair创建的这对文件描述符都是即可读又可写的。socketpair成功时返回0,失败时返回-1并设置errno。
dup函数和dup2函数
有时我们希望把标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接(比如CGI编程)。这可以通过下面的用于复制文件描述符的dup和dup2函数来实现:
#include<unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one , int file_descriptor_two) ;
dup函数创建一个新的文件描述符1,该文件描述符和原有文件描述符file_descriptor指向相同的文件、管道或者网络连接。并且dup返回的文件描述符总是取系统当前可用的最小整数值。dup2和dup类似,不过它将返回第一个不小于file_descriptor_two的整数值。dup和dup2系统调用失败时返回-1并设置errno。
代码省略:
在代码中,我们先关闭标准输出文件描述符STDOUT_FILENO(其值是1),然后复制socket文件描述符connfd。因为dup总是返回系统中最小的可用文件描述符,所以它的返回值实际上是1,即之前关闭的标准输出文件描述符的值。这样一来,服务器输出到标准输出的内容(这里是“abcd”)就会直接发送到与客户连接对应的socket上,因此printf调用的输出将被客户端获得(而不是显示在服务器程序的终端上)。这就是CGI服务器的基本工作原理。
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结构数组。我们在第5章讨论过结构体iovec,该结构体描述一块内存区。count参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd。readv和writev在成功时返回读出/写入fd的字节数,失败则返回-1并设置errno。它们相当于简化版的recvmsg和sendmsg函数
讨论过的Web服务器,当Web服务器解析完一个HTTP请求之后,如果目标文档存在且客户具有读取该文档的权限,那么它就需要发送一个HTTP应答来传输该文档。这个HTTP应答包含1个状态行、多个头部字段、1个空行和文档的内容。其中,前3部分的内容可能被Web服务器放置在一块内存中,而文档的内容则通常被读入到另外一块单独的内存中(通过read函数或mmap函数)。我们并不需要把这两部分内容拼接到一块再发送,而是可以通过writev函数将它们同时写出。
#include <sys/uio.h> struct iovec { ptr_t iov_base; /* Starting address */ size_t iov_len; /* Length in bytes */ };
struct iovec定义了一个向量元素。通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。
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。该函数的man手册指出,in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket。由此可见,sendfile几乎是专门为在网络上传输文件而设计的。
接收数据的话可以参考:
https://raw.githubusercontent.com/hiyoyolumi/ChatRoom/master/file_test/
sendfile无论如何都会成功返回,不阻塞
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参数用来设置内存段的访问权限。它可以取以下几个值的按位或:
- PROT_READ,内存段可读。
- PROT_WRITE,内存段可写
- PROT_EXEC,内存段可执行
- PROT_NONE,内存段不能被访问
flags参数控制内存段内容被修改后程序的行为。它可以被设置为表中的某些值(这里列出了常用值)的按位与(其中MAP_SHARED和MAP_PRIVATE是互斥的,不能同时指定)。
fd参数是被映射文件对应的文件描述符。它一般通过open系统调用获得。offset参数设置从文件的何处开始映射(对于不需要读入整个文件的情况)
常用值 含义 MAP_SHARED 在进程间共享这段内存。对该内存段的修改将反映到被映射的文件中。它提供了进程间共享内存的POSIX方法 MAP_PRIVATE 内存段为调用进程所私有。对该内存段的修改不会被反映到被映射的文件中 MAP_ANONYMOUS 这段内存不是从文件映射而来的。其内容被初始化为全0.这种情况下,mmap函数的最后两个参数将被忽略 MAP_FIXED 内存段必须位于start参数指定。start必须是内存页面的大小(4096字节)的整数倍 MAP_HUGETLB 按照“大内存页面”来分配内存空间。“大内存页面”的大小可通过/proc/meminfo文件来查看
mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED((void*)-1)并设置errno。munmap函数成功时返回0,失败则返回-1并设置errno。
可以使用mmap函数实现进程间共享内存
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_F_MOVE 如果合适的话,按整页内存移动数据。这只是给内核的一个提示。不过,因为它的实现存在BUG,所以自内核2.6.21后,它实际上没有任何效果 SPLICE_F_NONBLOCK 非阻塞的splice操作,但实际效果还会受文件描述符本身的阻塞状态的影响 SPLICE_F_MORE 给内核一个提示:后续的splice调用将读取更多数据 SPLICE_F_GIFT 对splice没有效果 使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(fd_in是管道文件描述符)而该管道没有被写入任何数据时。splice函数失败时返回-1并设置errno。常见的errno如表:
错误 含义 EBADF 参数所指文件描述符有错 EINVAL 目标文件系统不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道文件描述符,或者某个offset参数被用于不支持随机访问的设备(比如字符设备) ENOMEM 内存不够 ESPIPE 参数fd_in(或fd_out)是管道文件描述符,而off_in(或off_out不为NULL) 我们通过splice函数将客户端的内容读入到pipefd[1]中,然后再使用splice函数从pipefd[0]中读出该内容到客户端,从而实现了简单高效的回射服务。整个过程未执行recv/send操作,因此也未涉及用户空间和内核空间之间的数据拷贝。
tee函数
tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。tee函数的原型如下:
#include<fcntl.h>
ssize_t tee(int fd_in , int fd_out , size_t len , unsigned int flags);
该函数的参数含义与splice相同(但fd_in和fd_out必须都是管道文件描述符)。tee函数成功时返回在两个文件描述符之间复制的数据数量(字节数)。返回0表示没有复制任何数据。tee失败时返回-1并设置errno
fcntl函数
fcntl函数,正如其名字(file control)描述那样,提供了对文件描述符的各种控制操作。另外一个常见的控制文件描述符属性和行为的系统调用是ioctl,而且ioctl比fcntl能够执行更多的控制。但是,对于控制文件描述符常用的属性和行为,fcntl函数是由POSIX规范指定的首选方法。所以本书仅讨论fcntl函数。fcntl函数的定义如下:
#include<fcntl.h>
int fcntl(int fd , int cmd , ...)
fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操作。根据操作类型的不同,该函数可能还需要第三个可选参数arg。fcntl函数支持的常用操作及其参数如表所示。
操作分类 操作 含义 第三个参数类型 成功时的返回值 复制文件描述符 F_DUPFD 创建一个新的文件描述符,其值大于或等于arg long 新创建的文件描述符的值 F_DUPFD_CLOEXEC 与F_DUPFD相似,不过在创建文件描述符的同时,设置其close-on-exec标志 long 新创建的文件描述符的值 获取和设置文件描述符的标志 F_GETFD 获取fd的标志,比如close-on-exec标志 无 fd的标志 F_SETFD 设置fd的标志 long 0 获取和设置文件描述符的状态标志 F_GETFL 获取fd的状态标志,这些标志包括可由open系统调用设置的标志(O_APPEND、O_CREAT等)和访问模式(O_RDONLY、O_WRONLY和O_RDWR) void fd的状态标志 F_SETFL 设置fd的状态标志,但部分标志是不能被修改的(比如访问模式标志) long 0 管理信号 F_GETOWN 获取SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID 无 信号的宿主进程的PID或进程组的组ID F_SETOWN 设定SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID long 0 F_GETSIG 获取当前应用程序被通知fd可读或可写时,是哪个信号通知该事件的 无 信号值,0表示SIGIO F_SETSIG 设置当fd可读或可写时,系统应该触发哪个信号来通知应用程序 long 0 操作管道容量 F_SETPIPE_SZ 设置由fd指定的管道的容量,/proc/sys/fs/pipe-size-max内核参数指定了fcntl能设置的管道容量的上限 long 0 F_GETPIPE_SZ 获取由fd指定的管道的容量 无 管道容量 fcntl函数成功时的返回值如表所示,失败则返回-1并设置errno
在网络编程中,fcntl函数通常用来将一个文件描述符设置为非阻塞的
此外SIGIO和SIGURG这两个信号与其他Linux信号不同,它们必须与某个文件描述符相关联方可使用:当被关联的文件描述符可读或可写时,系统将触发SIGIO信号;当被关联的文件描述符(而且必须是一个socket)上有带外数据可读时,系统将触发SIGURG信号。将信号和文件描述符关联的方法,就是使用fcntl函数为目标文件描述符指定宿主进程或进程组,那么被指定的宿主进程或进程组将捕获这两个信号。使用SIGIO时,还需要利用fcntl设置其O_ASYNC标志(异步I/O标志,不过SIGIO信号模型并非真正意义上的异步I/O模型)