背景:
最近在解决一个22TB大小的Raid5的阵列分区信息消失的问题的时候,按照既定的策略对写入分区信息的阵列位置进行清零操作 --- 即在①阵列节点头部从LAB0的位置往后2MB进行写0操作,同时,②对阵列尾部最后2MB进行写0操作。这个两个位置是写入GPT分区的位置,清零操作的原因是:gpt信息的大小不够一个Raid条带,当其中某些磁盘存在脏数据时,掉盘之后,Raid5无法通过校验盘来恢复正确的数据。
操作:
open一下阵列节点md0,ope属性是RW,然后用一个longlong类型来记录偏移,便于lseek64到阵列末尾,写入2MB大小的0数据。为了确认数据确实写入到对应的位置,放开了blockdump和内核打印信息。
echo 1 > /proc/sys/vm/block_dump /*开始记录IO*/
echo 0 > /proc/sys/vm/block_dump /*关闭blockdump*/
echo 9 > /proc/sys/kernel/printk /*放开最高等级内核打印*/
放开信息之后,开始执行清零操作,通过打印信息发现头部写入的数据和偏移是正确的,而尾部写入的数据偏移总是在6TB的位置,加打印确认偏移大小是没有问题的。lseek64返回的信息也是正确的,仔细想了一下应该是32位系统seek寻址最大是16TB的问题导致的,16TB+6TB正好是阵列大小。可是,为什么32位系统只能寻址到16TB呢?难道无法支持16TB以上的写入?显然,这个问题是常见问题,不可能是我第一个发现的,一定是哪里没有了解清楚。
思考:
①为什么要对大于16TB的阵列进行分区?
②为什么明明使用了lseek64,64位的寻址还是不能超过16TB?
③有什么方法可以解决这个问题?
解决:
首先去了解了一下linux 系统下一次IO的过程,
用户态只是调用接口,没有实质性的对硬盘操作。
内核态:
文件系统层:VFS层:我们知道Linux分为用户态和内核态,用户态请求硬件资源需要调用System Call通过内核态去实现。用户的这些文件相关操作都有对应的System Call函数接口,接口调用 VFS对应的函数。 不同的文件系统实现了VFS的这些函数,通过指针注册到VFS里面。所以,用户的操作通过VFS转到各种文件系统。文件系统把文件读写命令转化为对磁盘LBA的操作,起了一个翻译和磁盘管理的作用。
缓存层:文件系统底下有缓存,Page Cache,加速性能。对磁盘LBA的读写数据缓存到这里。
块设备层:块设备接口Block Device是用来访问磁盘LBA的层级,主要是对读写命令组合,后插入到IO命令队列,磁盘的驱动从队列读命令执行。
磁盘驱动层:磁盘的驱动程序把对LBA的读写命令转化为各自的协议,比如变成ATA命令,SCSI命令,或者是自己硬件可以识别的自定义命令,发送给磁盘控制器。
分析可知,最有可能影响到写磁盘的位置应该是文件系统层。查阅资料可知(可以搜索linux IO栈),文件系统层中包含了vfs和块设备文件系统(ext4,ext2等),在vfs层中的page cache 是一段用于缓存磁盘文件的内存(关于page cache 介绍 可搜索 linux page cache)。其内存管理结构是 struct page 结构:
struct page {
unsigned long flags;
atomic_t count;
atomic_t _mapcount;
struct list_head lru;
struct address_space *mapping;
unsigned long index;
...
}
- flags表示page frame的状态或者属性,包括和内存回收相关的PG_active, PG_dirty, PG_writeback, PG_reserved, PG_locked, PG_highmem等。其实flags是身兼多职的,它还有其他用途,这将在下文中介绍到。
- count表示引用计数。当count值为0时,该page frame可被free掉;如果不为0,说明该page正在被某个进程或者内核使用,调用page_count()可获得count值。
- _mapcount表示该page frame被映射的个数,也就是多少个page table entry中含有这个page frame的PFN。
- lru是"least recently used"的缩写,根据page frame的活跃程度(使用频率),一个可回收的page frame要么挂在active_list双向链表上,要么挂在inactive_list双向链表上,以作为页面回收的选择依据,lru中包含的就是指向所在链表中前后节点的指针(参考这篇文章)。
- 如果一个page是属于某个文件的(也就是在page cache中),则mapping指向文件inode对应的address_space(这个结构体虽然叫address_space,但并不是进程地址空间里的那个address space),index表示该page在文件内的offset(以page size为单位)。
注意这个index 的类型是unsigned long 在32位系统中是2^31大小,在64位中是2^63大小。因为每个page frame都需要一个struct page来描述,一个page frame占4KB。所以在内存中可以管理的文件的最大偏移是 index * 4KB,所以32位系统中采用page cache的缓存方式的IO的偏移范围是
2^31 * 4KB = 16TB 。既然找到了最大偏移限制是由于page cache在作怪,那么有没有办法可以无缓冲读写文件呢?
这里其实就是常说的BIO和DIO的不同的地方。pen函数的flag参数中有O_DIRECT参数可以使用DIO写和读来操作硬盘。同时这个也是验证上述结论是否正确的一个方法。
此时也能解释为什么lseek64偏移超过16TB之后为什么会定位到6TB的位置了 -- 由于范围超出了16TB,导致数据高位被截断,从而偏移回环,到了第6TB的位置。
总结:
有时候简单的问题中蕴含着比较深层次的道理,以前的时候只知道32位系统支持的最大容量是16TB,原因是因为32位数据类型导致的,但是没有去验证这个结论的由来。
总结如下:
①32位系统内存寻址范围是4GB,但是由于page 页大小4KB缓存的原因,所以可以支持的虚拟磁盘容量远超4GB。
②硬盘操作的BIO和DIO写都有自己的优缺点,要在合适的时候使用。
③代码实现时不能只依靠函数返回值来判断是否执行成功,要依赖于执行的结果。而结果可以通过多个维度去验证。
④设计和实现时注意32位和64位系统中数据类型的区别