一、文件操作
在实际的开发过程中,文件的操作可以说是非常常见一种需求。特别是对于与数据打交道的开发者,只要数据最终要落盘,就无法绕开各种各样的文件的读写操作。文件操作作为一种基础的应用,是开发者必须掌握的一种基本技能。本文将以Linux为基础环境进行相关的分析说明。
二、缓存
在教科书和相关的资料中,大家都明白,CPU与内存和IO操作速度完全不在一量级上。而此时最常见的应对方式之一就是使用缓存。缓存使用的依据就是计算机访问的局部性原理,通过高命中率来重复使用相同的数据。不过,这也体现出来其缺点,如果不是相同的数据,也就是说局部性访问的丧失 ,则会导致更高的时间成本。
不同的硬件设备有不同的缓存,但其基本的缓存原理和缓存数据的替换算法基本都类似。有兴趣可以看一下相关的操作系统的书籍或资料。而对于文件操作来说一般是把IO的数据缓存到内存中去。
缓存的优势是勿庸置疑的,它不但可以减少对IO操作的次数(直接访问缓存),提高数据访问的速度(毕竟内存的读写速度要高很多)。最关键的一点是它可以实现批量的合并数据读写,特别是写操作,能极大的提高磁盘的利用率并提高写的速度。
三、优化
既然知道了缓存的优势和特点,那么它就可以被应用到对文件操作的过程中。也就是说,可以在实际的开发编程中提前对缓存进行处理,这样可以提高文件操作的效率。缓存的使用有两种情况:
- 使用工具或配置文件对硬件进行配置管
使用工具如vmstat等进行缓存页面的监控,然后对/proc/sys/vm/的相关参数进行调优 - 直接编程在代码中对缓存进行处理
这个也比较常见如动态适配缓存的大小、设置文件读写块的大小以及预读取技术等。当然,也不能忘记重要的内存映射技术 - 优化文件读写的操作
这个很好理解,比如使用操作如fsync()和fdatasync()以及延迟写入等技术配合实现批量的操作
下面看一个简单的例子:
c
#include <fstream>
#include <array>
int main() {
std::ofstream f("a.txt");
if (!f) return 1;
// 使用一个 1M 的栈上缓冲区
std::array<char, 1024*1024> buf;
// pubsetbuf使用char*类型进行大小设置;nullptr代表禁用缓冲
f.rdbuf()->pubsetbuf(buf.data(), buf.size());
f << "set buffer!\n";
return 0;
}
四、直接IO
技术往往具有两面性,所以导致其无法普遍适配在所有的应用场景下。在实际的开发中,可能会遇到一些场景需要将数据实时的写入到硬盘中。即对文件操作需要立刻、实时而不通过缓存进行直接落盘。那么此时就必须使用直接IO操作了。那么什么情况会有这种需要呢?包括以下几种:
- 实时系统
这种比较好理解,比如军事和航天等要求极高的情况下,数据要求必须实时写入 - 强一致系统
在一些金融交易中,数据缓存有可能会各种情况丢失,这就需要实时的写入到硬盘中 - 海量数据处理
海量数据处理,如拷贝等,缓存已经起不到太大的作用,可以考虑直接写硬盘 - 硬件测试系统
就是为了测试IO,不需要使用缓存 - 其它
其它一些特殊情况
直接IO操作,主要是性能可能比较难把握;另外,开发上可能会考虑一些细节,比如内存的对齐,文件偏移对齐以及长度对齐等等导致代码开发难度增加;最后,跨平台的开发复杂性也明显增加。这都需要开发者提前有所考虑。
直接IO的方法可以考虑使用Linux下的接口控制和mmap映射中的相关支持。看一个简单的例子:
c
#define _GNU_SOURCE // 引入O_DIRECT
#define BLOCK_SIZE 4096
int main() {
int fd;
char *buf;
ssize_t ret;
const char *fName = "test.txt";
// 分配对齐内存
if (posix_memalign((void **)&buf, BLOCK_SIZE, BLOCK_SIZE) != 0) {
exit(EXIT_FAILURE);
}
// 填充数据
memset(buf, '1', BLOCK_SIZE);
// 打开文件O_DIRECT
fd = open(fName, O_RDWR | O_CREAT | O_TRUNC | O_DIRECT, 0666);
if (fd == -1) {
free(buf);
exit(EXIT_FAILURE);
}
// 直接写入 ,长度和偏移都必须对齐
ret = write(fd, buf, BLOCK_SIZE);
if (ret != BLOCK_SIZE) {
close(fd);
free(buf);
exit(EXIT_FAILURE);
}
//读缓冲区
char *rBuffer;
if (posix_memalign((void **)&rBuffer, BLOCK_SIZE, BLOCK_SIZE) != 0) {
close(fd);
free(buf);
exit(EXIT_FAILURE);
}
// 偏移到文件开头并对齐
lseek(fd, 0, SEEK_SET);
//直接读取
ret = read(fd, rBuffer, BLOCK_SIZE);
if (ret != BLOCK_SIZE) {
close(fd);
free(buf);
free(rBuffer);
exit(EXIT_FAILURE);
}
close(fd);
free(buf);
free(rBuffer);
return 0;
}
五、总结
文件读写IO操作作为一种必备技术,简单使用看不出什么难点。但一旦涉及到海量数据、各种异常处理等等细节时,往往让开发者陷入一种无处下手的感觉。这就需要开发者不但要掌握相关文件存储知识还要明白计算机系统对IO操作的具体流程。新技术的应用和传统技术的应用场景等。共同协作,才可能保证数据的安全、快捷的落盘。