文章目录
- 创建文件描述符的函数
- pipe函数
- dup函数、dup2函数
- 读取或写入数据
- readv函数、writev函数
- 零拷贝
- sendfile函数
- splice函数
- tee函数
- 进程间通信——共享内存
- mmap函数 和 munmap函数
- 控制文件描述符
- fcntl函数
创建文件描述符的函数
pipe函数
不再赘述,详情见我的另一篇博客。
值得一提的是,socket
基础API中有一个 socketpair函数
,能够方便地创建双向管道:
#include<sys/types.h>
#include<sys/socket.h>
int socketpair(int domain, int type, int protocol, inf fd[2]);
// domain只能使用 UNIX 本地域协议族 AF_UNIX,因为我们仅能在本地使用这个双向管道。
// 成功时返回0,失败时返回-1并设置error。
dup函数、dup2函数
这两个函数会在 CGI服务器
中用到。CGI服务器: 主要是通过把服务器本地标准输入、输出或者文件重定向到网络连接中,以达到向标准输入、输出缓冲区中输入的信息,能在网络连接中发送的效果。
#include<unistd.h>
int dup( int file_descriptor );
int dup2( int file_descriptor_one, int file_descriptor_two );
- dup: 创建一个新的文件描述符,该文件描述符和原有文件描述符
file_descriptor
指向相同的文件、管道或网络连接。返回的文件描述符总是取系统当前可用的最小整数值。 - dup2: 与
dup
类似,只是返回第一个不小于file_descriptor_two
的整数值。
两个系统调用失败时都返回 -1
,并设置 error
。
通过 dup
和 dup2
创建的文件描述符并不继承原文件描述符的属性。比如:close-on-exec
和 non-blocking
等。
用 dup
实现一个基本的 CGI服务器
的局部代码:
close( STDOUT_FILENO );
dup( connfd );
printf( "hello\n" );
close( connfd );
流程:
- 先关闭标准输出文件描述符
STDOUT_FILENO
(其值是1); - 通过
dup
复制socket
文件描述符connfd
; - 由于
dup
总是返回系统中最小的可用文件描述符,所以它的返回值实际上是1
,即之前关闭的标准输出文件描述符的值; - 服务器输出到标准输出的内容(“hello”)就会直接发送到与客户端对应的
socket
上,因此printf
调用的输出将被客户端获得,而不是显示在服务器程序的终端上。
读取或写入数据
readv函数、writev函数
- readv函数: 将数据从文件描述符读到分散的内存块中,即分散读;
- writev函数: 将多块分配的内存数据一并写入文件描述符中,即集中写。
相当于简化版的 recvmsg
和 sendmsg
。
#include<sys/uio.h>
ssize_t readv( int fd, const struct iovec* vector, int count );
ssize_t weitev( int fd, const struct iovec* vector, int count );
// fd:被操作的目标文件描述符
// vector:iovec结构数组,iovec封装了一块内存的起始位置和长度
// count:vector数组的长度,即有多少块内存数据需要从fd读出或写入到fd
// 成功时返回读出/写入fd的字节数,失败则返回-1并试着errno。
零拷贝
sendfile函数
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:待读出内容的文件描述符,必须是一个支持类似mmap函数的文件描述符,即必须指向真实的文件,不能是socket和管道
// offset:指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置
// count:传输的字节数
// 成功时返回传输的字节数,失败返回-1,并设置errno
sendfile
几乎是专门为在网络上传输文件而设计的。
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:如果fd_in是一个管道描述符,那么off_in必须被设置为NULL;反之,如果fd_in不是一个管道描述符(如socket),那么off_in表示从输入数据流开始读取数据的起始位置。总而言之,若off_in不为NULL,则它将指出具体的偏移位置。
// fd_out/off_out:与fd_in/off_in类似,不过用于输出数据流。
// len:移动数据的长度
// flags:控制数据的移动方式
使用 splic函数
时 fd_in
和 fd_out
必须至少有一个是管道文件描述符。splice函数
调用成功时返回移动字节的数量。可能返回 0
,表示没有数据需要移动,这发生在从管道中读取数据(fd_in
是管道文件描述符)而管道没有被写入任何数据时(fd_out
不是管道文件描述符)。splice函数失败时返回 -1
并设置 errno
。
常见的
errno
:
errno | 含义 |
---|---|
EBADF | 参数所指文件描述符有错 |
ENOMEM | 内存不够 |
EINVAL | 目标文件系统不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道文件描述符,或者某个 offset 参数被用于不支持随机访问的设备(如字符设备) |
ESPIPE | 参数 fd_in(或fd_out) 是管道文件描述符,而 off_in(或off_out) 不为NULL |
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 必须同时是管道文件描述符
// 成功时返回两个文件之间复制的数据数量(字节数),返回0表示没有复制任何数据,tee失败返回-1并设置errno。
tee
常和 splice
一起用,splice
将非管道与管道绑定,tee
将 splice
操作后得到的管道绑定在一起。
进程间通信——共享内存
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:控制内存段内容被修改后程序的行为。
// fd:被映射文件对应的文件描述符,一般通过open调用获得。
// offset:参数设置从文件的何处开始映射(对于不需要读入整个文件的情况)。
// 成功时返回0,失败返回-1并设置errno。
mmap 的 flags 参数的常用值及其含义:
常用值 | 含义 |
---|---|
MAP_SHARED | 进程间共享这段内存,对该内存段的修改将反映到被映射的文件中。提供了进程间共享内存的 POSIX 方法 |
MAP_PRIVATE | 内存段为调用进程所私有,所有修改不会反映到被映射的文件中 |
MAP_ANONYMOUS | 这段内存不是从文件映射而来的,其内容被初始化为全0。此时 mmap 最后两个参数将被忽略。 |
MAP_FIXED | 内存段必须位于 start 指定的地址处,start 必须是内存页面大小(4096字节=4KB)的整数倍 |
MAP_HUGETLB | 按照“大内存页面”来分配内存空间,“大内存页面”由 /proc/meminfo 文件来擦查看 |
控制文件描述符
fcntl函数
fcntl函数: 全名 file control
,提供了对文件描述符的各种控制操作(类似于系统调用 ioctl
,但 ioctl
比 fcntl
提供的控制更多。)但是 fcntl
是 POSIX
规定的首选方法。
#include<fcntl.h>
int fcntl(int fd, int cmd, ...);
// fd:被操作的文件描述符
// cmd:执行何种操作
// 有可能需要第三个可选参数 arg
// 成功时返回值根据操作不同有所不同,失败时返回-1并设置errno
将文件描述符设置为非阻塞的:
int setnonblocking( int fd ){// F_GETFL 获取 fd 的标志,成功时返回 fd 的标志int old_option = fcntl(fd, F_GETFL); // 获取文件描述符旧的状态标志int new_option = old_option | O_NONBLOCK; // 设置非阻塞标志fcntl(fd, F_SETFL, new_option); // F_SETFL 设置 fd 的标志return old_option; // 返回文件描述符旧的状态标志,以便日后恢复该状态标志
}
题外话:SIGIO
和 SIGURG
这两个信号与其他 Linux
信号不同,他们必须与某个文件描述符相关联方可使用:
- 被关联文件描述符可读或可写时,系统将触发
SIGIO
信号。 - 被关联文件描述符是一个
socket
且有带外数据可读时,系统将触发SIGURG
信号。
这两个信号就是通过 fcntl函数
与文件描述符关联的,具体做法是:fcntl函数
为目标文件描述符指定宿主进程或进程组,被指定的宿主进程或进程组去捕获这两个信号。
特别的,使用 SIGIO
时,还需利用 fcntl
设置其 O_ASYNC标志
(异步I/O标志,不过SIGIO信号模型并非真正意义上的异步I/O模型)。