Linux提供了很多高级的I/O函数。它们并不像Linux基础I/O函数(比如open和read)那么常用(编写内核模块时一般要实现这些I/O函数),但在特定的条件下却表现出优秀的性能。这些函数大致分为三类:
用于创建文件描述符的函数,包括pipe、socketpair、dup/dup2函数。
用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数。
用于控制I/O行为和属性的函数,包括fcntl函数。
本节接着介绍第二类
一、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
:iovec
结构体数组的长度,即缓冲区的数量。- 成功时返回操作的字节数,失败时返回-1并且置errno
struct iovec
结构体定义:
struct iovec {void *iov_base; // 缓冲区的起始地址size_t iov_len; // 缓冲区的长度
};
举个分散写的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 打开文件int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);// 准备多个缓冲区char buf1[] = "This is the first buffer.\n";char buf2[] = "This is the second buffer.\n";char buf3[] = "This is the third buffer.\n";// 构建 struct iovec 数组struct iovec iov[3];iov[0].iov_base = buf1;iov[0].iov_len = strlen(buf1);iov[1].iov_base = buf2;iov[1].iov_len = strlen(buf2);iov[2].iov_base = buf3;iov[2].iov_len = strlen(buf3);// 使用 writev() 将多个缓冲区的内容一次性写入文件ssize_t bytes_written = writev(fd, iov, 3);printf("Total bytes written: %zd\n", bytes_written);// 关闭文件close(fd);return 0;
}
二、sendfile函数
sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。
#include <sys/sendfile.h>ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
out_fd
:输出文件描述符,用于指定要写入数据的目标文件。in_fd
:输入文件描述符,用于指定从中读取数据的源文件。offset
:用于指定输入文件中的起始位置,如果为NULL
,表示从当前文件偏移量开始读取。count
:要传输的字节数。sendfile
成功时返回传输的字节数,失败则返回-1并设置errno。
这个函数就很简单,主要是因为他的高效率,不需要在用户空间操作。所以我们就不举例子了。
三、mmap函数与munmap函数
#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
:映射区域私有,对映射区域的修改不会影响到文件内容,而是在内存中进行。MAP_FIXED
:强制将映射区域放置到start
参数指定的地址处,如果无法满足,则映射失败。MAP_ANONYMOUS
:不与任何文件关联,创建一个匿名映射区域,常用于创建共享内存区域。
-
fd
:指定要映射的文件的文件描述符,如果不需要映射文件,可以设置为-1
。 -
offset
:指定文件中的偏移量,表示从文件的哪个位置开始映射数据,通常设置为0
,表示从文件的起始位置开始。
mmap()
函数成功调用后,将返回指向映射区域的指针,如果映射失败,则返回 MAP_FAILED
宏。需要注意的是,映射区域的大小和对齐方式取决于系统和硬件的限制,在使用时需要仔细考虑。此外,映射区域需要通过 munmap()
函数进行解除映射,以释放相关资源。
3.1、将一个文件映射到内存中
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main() {int fd;struct stat sb;void *mapped;// 打开文件fd = open("output.txt", O_RDWR);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}// 获取文件状态if (fstat(fd, &sb) == -1) {perror("fstat");exit(EXIT_FAILURE);}// 检查文件大小,文件大小不能为0,否则的话if (sb.st_size == 0) {fprintf(stderr, "File size is 0.\n");exit(EXIT_FAILURE);}// 将文件映射到内存中mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (mapped == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}// 关闭文件close(fd);// 修改映射区域中的内容strcpy((char*)mapped, "Hello, memory mapped file!\n");// 输出映射区域的内容printf("%s", (char*)mapped);// 解除映射if (munmap(mapped, sb.st_size) == -1) {perror("munmap");exit(EXIT_FAILURE);}return 0;
}
3.2、共享内存的映射
这个函数还有一个更大的作用就是将将共享内存区域映射到当前进程的地址空间中。以实现多进程之间的信息交换。这部分在后面的多进程中会提到,这里就先不讲了。
四、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
: 源文件偏移量指针,指向源文件的读取偏移量,NULL
:表示从当前偏移位置开始读取。fd_out
: 目标文件描述符,向该文件描述符写入数据。off_out
: 目标文件偏移量指针,指向目标文件的写入偏移量,NULL
:表示从当前偏移位置开始写入。len
: 要移动的数据长度。flags
: 控制函数行为的标志,可以为 0 或以下标志的按位或:SPLICE_F_MOVE
: 合适的话,按照整页移动数据SPLICE_F_NONBLOCK
: 非阻塞的splice操作,但实际效果还是会受文件描述符本身的阻塞状态的影响.SPLICE_F_MORE
:给内核一个下面还有数据的提示
该函数可以在文件描述符之间移动数据而不涉及用户空间的数据拷贝,因此对于大量数据的传输操作可以提高效率。使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。
看一个回射服务器的示例:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <error.h>
#define __USE_GNU 1int main(int argc, const char *argv[])
{const char *ip = "127.0.0.1";const int port = 8080;int ret;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int sockfd = socket(AF_INET, SOCK_STREAM, 0);bind(sockfd, (struct sockaddr *)&address, sizeof(address));listen(sockfd, 5);int connfd;while (1){struct sockaddr_in peer;bzero(&peer, sizeof(peer));socklen_t len = sizeof(peer);connfd = accept(sockfd, (struct sockaddr *)&peer, &len);if (connfd > 0){int pipefd[2];pipe(pipefd);/*将connfd上流入的客户端数据定向到管道中*/splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);/*将管道中的数据定向到connfd的客户端文件描述符上*/splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);}close(connfd);}close(sockfd);return 0;
}
五、tee函数
tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作
#include <fcntl.h>ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
-
fd_in
和fd_out
必须都是管道文件描述符 -
其他参数与
splice
函数一样
tee函数成功时返回在两个文件描述符之间复制的数据字节数。返回0表示没有复制任何数据。tee失败时返回-1并设置errno。