讨论与网络编程相关的三类高级I/O函数:
- 用于创建文件描述符的函数:pipe / dup / dup2
- 用于读写数据的函数:readv / writev , sendfile , splice , tee , mmap / munmap
- 用于控制I/O行为和属性的函数:fcntl
备注:根据个人理解,调整部分函数的介绍顺序
1.pipe
用于创建一个管道,以实现进程间通信。
#include <unistd.h>
int pipe(int fd[2]);
通过pipe创建的两个文件描述符构建管道的两端:进程往fd[1]里写数据,往fd[0]读数据。可见,用一个pipe只能创建单向的数据流。若想实现双向数据传输,需要两个pipe。
默认情况下,这一对文件描述符是阻塞的。阻塞模式下,读写行为一致。非将一对文件描述符设置为非阻塞,读写会有不同的行为。
- 读:若fd[1]的引用计数为0,则针对该管道的读端文件描述符fd[0]的读操作将返回0,即读到End of File
- 写:若fd[0]的引用计数为0,则针对该管道的写端文件描述符fd[1]的写操作将引发SIGPIPE信号
管道内部传输的是字节流,和TCP字节流的概念相同。但二者有细微差别:
- 管道字节流:容量受管道本身的限制,通常为65535字节,可通过fcntl改变其容量。
- TCP字节流:受对端的接口通告窗口大小和本端的拥塞窗口的大小的限制。
除了创建两个管道来构建双向传输,socket基础API的socketpair可以方便的创建双向管道。
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);
2.dup / dup2
可通过dup / dup2实现把标准输入重定向到一个文件,或把标准输出重定向网络连接(比如CGI编程)。
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd_min);
dup创建新文件描述符,该文件描述符和fd指向相同的文件、管道或网络连接,并且其返回的文件描述符总是取系统当前可用的最小整数值。
dup2与dup类似,不过它返回第一个不小于fd_min的整数值。
注意:通过dup和dup2创建的新文件描述符并不继承原有文件描述符的属性,比如close-on-exec和non-blocking等。
3.readv / writev
- 分散读(scatter read):readv将文件描述符的数据读到分散的内存块中
- 集中写(gather write):writev将多块分散的内存数据一并写入文件描述符中
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count);
sszie_t writev(int fd, const struct iovec* vector, int count);
4.sendfile
sendfile实现在两个文件描述符中直接传递数据,此过程完全在内核进行,省去了用户缓冲区与内核缓冲区之间的数据拷贝,效率很高,称为零拷贝。
#include <sys/sendfile.h>
// offset是in_fd的偏移值
// in_fd是一个支持类似mmap函数的文件描述符,即它必须指向真实文件,不能是socket和管道
// out_fd必须是一个socket
sszie_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
由以上注释可见,sendfile几乎是专门为在网络上传输文件而设计的。
5.splice
与sendfile一样,也是零拷贝。
#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。
- 若fd_in不是管道文件描述符,那么off_in表示从输入数据流的off_in处开始读取数据。若off_in为NULL,表示从输入数据流的当前位置读入。
fd_out及off_out含义与fd_in及off_in类似。
6.tee
tee函数在两个管道文件描述符间复制数据,也是零拷贝。它不消耗数据,因此源文件描述符上的数据仍可以用于后续的读操作。
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t length, unsigned int flags);
7.mmap / munmap
mmap函数用于申请一段内存空间,可以将申请到的内存用于进程间通信的共享内存,也可以将文件直接映射其中。munmap函数则释放由mmap创建的这段内存空间。
#include <sys/mman.h>
// start允许用户使用某个特定的地址作为这段内存的起始地址(亦即指定起始地址),如果它被设置成NULL,则系统自动分配一个地址
// prot:内存段访问权限,可取PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE
// flags:控制内存段内容被修改后程序的程序的行为,可取MAP_SHARED, MAP_PRIVATE, MAP_ANONYMOUS, MAP_FIXED, MAP_HUGETLB。若取MAP_ANONYMOUS,则内存不映射至文件,此时fd和offset无意义。
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void* start, size_t length);
8.fcntl
顾名思义,fcntl用于控制文件描述符的属性和行为,另一个常见的用于控制文件描述符属性和行为的函数ioctl更具有普通性,控制范围更广。但fcntl是POSIX规范指定的用于专门控制文件描述符的首选。
#include <fcntl.h>
int fcntl(int fd, int cmd, …);
cmd可分为5类12项:
- 复制文件描述符:F_DUPFD, FDUPFD_CLOEXEC
- 获取和设置描述符的标志:F_GETFD, F_SETFD
- 获取和设置描述符的状态标志:F_GETFL, FSETFL
- 管理信号:F_GETOWN, F_SETOWN, F_GETSIG, F_SETSIG
- 操作管道容量:F_GETPIPE_SZ, F_SETPIPE_SZ
此外,SIGIO和SIGURG与linux其他信号不同,它们必须与某个文件描述符关联方可使用:
- 当被关联的文件描述符可读或可写时,系统将触发SIGIO信号
- 当被关联的文件描述符(必须是socket)有带外数据时,系统将触发SIGURG信号
将信号与文件描述符关联的方法,就是使用fcntl函数为目标文件描述符指定宿主进程或进程组,由指定的宿主进程或进程组捕获这两个信号