1 读文件
其实Linux驱动程序最重要,也是难点就是那个块设备驱动程序。要全面研究这个问题不是那么容易,从本博开始,我们独辟蹊径,从一个文件读写的角度把这个问题阐述干净。
大部分程序员可能会有这样的疑问:当我们在应用程序中调用库函数 read 时,这个请求是经过哪些处理最终到达磁盘的呢,数据又是怎么被拷贝到用户缓存区的呢?我们就从 read 系统调用发出到结束处理的全过程,来解密整个内核块设备驱动的内幕。
1.1 系统调用VFS层的处理
用户要读写文件首先是通过open、read和write等系统调用接口来进入内核。Linux 系统调用接口(SCI,system call interface)的实现机制实际上是一个多路汇聚以及分解的过程,该汇聚点就是 0x80 中断这个入口点(X86 系统结构)。也就是说,所有系统调用都从用户空间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将根据系统调用号对不同的系统调用分别处理。
当调用发生时,用户程序通过C语言的库函数在保存 read 系统调用号以及参数后,陷入 0x80 中断。这时库函数工作结束。Read 系统调用在用户空间中的处理也就完成了。
0x80 中断处理程序接管执行后,先检察其系统调用号,然后根据系统调用号查找系统调用表,并从系统调用表中得到处理 read 系统调用的内核函数sys_read ,最后传递参数并运行 sys_read 函数。至此,内核真正开始处理 read系统调用(sys_read 是 read 系统调用的内核入口):
asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count) { struct file *file; ssize_t ret = -EBADF; int fput_needed;
file = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_read(file, buf, count, &pos); file_pos_write(file, pos); fput_light(file, fput_needed); }
return ret; } |
首先调用fget_light()函数 :根据 fd 指定的索引,从当前进程描述符中取出相应的file对象。我们不去详细分析这个函数了,至于fd和file的关系,请参考博客“把Linux中的VFS对象串联起来” http://blog.csdn.net/yunsongice/archive/2010/06/21/5683859.aspx。
随后,根据得到的file结构,获得它的当前读写位置file->f_pos赋给内部变量pos。然后调用vfs_read() 执行文件读取操作,而这个函数最终调用file->f_op->read() 指向的函数,代码如下:
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { …… if (!ret) { if (file->f_op->read) ret = file->f_op->read(file, buf, count, pos); else ret = do_sync_read(file, buf, count, pos); …… } } return ret; } |
到此,虚拟文件系统层所做的处理就完成了,控制权交给了 ext2 文件系统层。在解析 ext2 文件系统层的操作之前,先让我们看一下 file 对象中 read 指针来源。
读数据之前,必须先打开文件。处理 open 系统调用的内核函数为 sys_open 。这个系统调用的核心是调用filp_open() ,后者又通过调用 open_namei() 函数取出和该文件相关的 dentry 和inode (因为前提指明了文件已经存在,所以 dentry 和 inode 能够查找到,不用创建),然后调用 dentry_open() 函数创建新的 file 对象,并用 dentry 和 inode 中的信息初始化 file 对象(文件当前的读写位置在 file 对象中保存)。注意到 dentry_open() 中有一条语句:
f->f_op = fops_get(inode->i_fop);
这个赋值语句把和具体文件系统相关的,操作文件的函数指针集合赋给了 file对象的 f_op 变量(这个指针集合是保存在 inode 对象中的),在接下来的sys_read 函数中将会调用 file->f_op 中的成员 read 。
至于inode的i_fop又是什么时候被初始化的,我们在下一节探索第二扩展文件系统的时候,会讲到。
1.2 第二扩展文件系统Ext2层的处理
当内核安装Ext2文件系统时,一般使用mount命令,如:mount -f ext2 /dev/sda2 /mnt/test,则会调用sys_mount系统调用。而在sys_mount又是调用do_mount()函数执行文件系统安装实务,传入的参数就是mount命令给的设备名“/dev/sda2”、挂载目录“/mnt/test”和文件系统类型“ext2”。do_mount()函数又会do_new_mount函数,后者进一步调用do_kern_mount()函数,给它传递的参数为文件系统类型、安装标志以及块设备名。do_kern_mount()函数得到ext2文件系统类型的file_system_type结构,进而调用vfs_kern_mount函数,触发file_system_type结构中的get_sb函数。
那么ext2文件系统是在什么时候注册的呢,这个get_sb函数又是什么呢?看到fs/ext2/super.c代码最后几行,系统初始化时,通过module_init(init_ext2_fs)调用init_ext2_fs函数对ext2文件系统进行注册:
static int __init init_ext2_fs(void)
{
……
err = register_filesystem(&ext2_fs_type);
……
}
而ext2_fs_type在同一个文件中被定义为:
static struct file_system_type ext2_fs_type = {
.owner = THIS_MODULE,
.name = "ext2",
.get_sb = ext2_get_sb,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
ext2文件系统对象file_system_type结构中的get_sb方法就是ext2_get_sb,来自同一文件,它调用get_sb_bdev,传进去的参数是文件系统类型、设备名和加载目录,还有一个最重要的就是ext2_fill_super函数的地址。于是在这个函数体内,就会调用这个ext2_fill_super函数。关于文件系统的安装我讲的比较简略,如果愿意相信探究的同学请访问博客“文件系统安装 ”http://blog.csdn.net/yunsongice/archive/2010/06/21/5685067.aspx。
1.2.1 Ext2的磁盘布局
对于 ext2 文件系统来说,硬盘分区首先被划分为一个个的块(block),一个 ext2 文件系统上的每个块都是一样大小的,但是对于不同的 ext2 文件系统,块的大小可以有区别。Linux ext2文件系统对块的大小有一个严格的限制,必须大于1024字节,小于4096字节,定义在include/linux/ext2_fs.h中:
#define EXT2_MIN_BLOCK_SIZE 1024
#define EXT2_MAX_BLOCK_SIZE 4096
这个大小在创建 ext2 文件系统的时候被决定,它可以由系统管理员指定,也可以由文件系统的创建程序根据硬盘分区的大小,自动选择一个较合理的值。但是,为了跟一个页面大小对齐,也就是4k对齐,供我们选择的也就只有三种大小,1024、2048和4096。尽量跟页面对齐的目的是高效地使用页高速缓存,所以我们后面都会默认设置这些块的大小为1024,也就是说一页包含4个块。在磁盘中,块被聚在一起分成几个大的块组(block group)。每个块组中有多少个块是固定的,那么这个固定大小到底是多少呢?别着急,请您继续往下学习。
每个块组都相对应一个组描述符(group descriptor),这些组描述符被聚在一起放在硬盘分区的开头部分,紧跟在超级块(super block)的后面。所谓超级块,我们下面还要讲到。在这个组描述符当中有几个重要的块指针。我们这里所说的块指针,跟C语言指针概念不一样。后者描述的是内存地址值,而前者就是指硬盘分区上的块的号数,比如,指针的值为 0,我们就说它是指向硬盘分区上的 block 0;指针的值为 1023,我们就说它是指向硬盘分区上的 block 1023。我们注意到,一个硬盘分区上的块计数是从 0 开始的,并且这个计数对于这个硬盘分区来说是全局性质的。
磁盘中的超级块在C语言中是这样描述的,来自include/linux/ext2_fs.h:
struct ext2_super_block { __le32 s_inodes_count; /* Inodes count */ __le32 s_blocks_count; /* Blocks count */ __le32 s_r_blocks_count; /* Reserved blocks count */ __le32 s_free_blocks_count; /* Free blocks count */ __le32 s_free_inodes_count; /* Free inodes count */ __le32 s_first_data_block; /* First Data Block */ __le32 s_log_block_size; /* Block size */ __le32 s_log_frag_size; /* Fragment size */ __le32 s_blocks_per_group; /* # Blocks per group */ __le32 s_frags_per_group; /* # Fragments per group */ __le32 s_inodes_per_group; /* # Inodes per group */ __le32 s_mtime; /* Mount time */ __le32 s_wtime; /* Write time */ __le16 s_mnt_count; /* Mount count */ __le16 s_max_mnt_count; /* Maximal mount count */ __le16 s_magic; /* Magic signature */ __le16 s_state; /* File system state */ __le16 s_errors; /* Behaviour when detecting errors */ __le16 s_minor_rev_level; /* minor revision level */ __le32 s_lastcheck; /* time of last check */ __le32 s_checkinterval; /* max. time between checks */ __le32 s_creator_os; /* OS */ __le32 s_rev_level; /* Revision level */ __le16 s_def_resuid; /* Default uid for reserved blocks */ __le16 s_def_resgid; /* Default gid for reserved blocks */
__le32 s_first_ino; /* First non-reserved inode */ __le16 s_inode_size; /* size of inode structure */ __le16 s_block_group_nr; /* block group # of this superblock */ __le32 s_feature_compat; /* compatible feature set */ __le32 s_feature_incompat; /* incompatible feature set */ __le32 s_feature_ro_compat; /* readonly-compatible feature set */ __u8 s_uuid[16]; /* 128-bit uuid for volume */ char s_volume_name[16]; /* volume name */ char s_last_mounted[64]; /* directory where last mounted */ __le32 s_algorithm_usage_bitmap; /* For compression */
__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/ __u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */ __u16 s_padding1;
__u8 s_journal_uuid[16]; /* uuid of journal superblock */ __u32 s_journal_inum; /* inode number of journal file */ __u32 s_journal_dev; /* device number of journal file */ __u32 s_last_orphan; /* start of list of inodes to delete */ __u32 s_hash_seed[4]; /* HTREE hash seed */ __u8 s_def_hash_version; /* Default hash version to use */ __u8 s_reserved_char_pad; __u16 s_reserved_word_pad; __le32 s_default_mount_opts; __le32 s_first_meta_bg; /* First metablock block group */ __u32 s_reserved[190]; /* Padding to the end of the block */ }; |
看到上面 ext2 文件系统的超级块,每个块组开头(磁盘的开头第一个 byte 是 byte 0,存放的是引导块MBR)第一个块都存放其一个拷贝。
其中 __u32 是表示 unsigned 不带符号的 32 bits 的数据类型,其余类推。这是 Linux 内核中所用到的数据类型,如果是开发用户空间(user-space)的程序,可以根据具体计算机平台的情况,用 unsigned long 等等来代替。
其实,对应Ext2文件系统的磁盘布局来说,比超级块更重要的是组描述符。在每个块组中紧接着超级块后若干个块中,存放着整个磁盘所有的组描述符,其C语言定义同样来自来自include/linux/ext2_fs.h:
struct ext2_group_desc { __le32 bg_block_bitmap; /* Blocks bitmap block */ __le32 bg_inode_bitmap; /* Inodes bitmap block */ __le32 bg_inode_table; /* Inodes table block */ __le16 bg_free_blocks_count; /* Free blocks count */ __le16 bg_free_inodes_count; /* Free inodes count */ __le16 bg_used_dirs_count; /* Directories count */ __le16 bg_pad; __le32 bg_reserved[3]; }; |
在块组的组描述符中,其中有一个块指针指向这个组描述符的块位图(block bitmap),块位图中的每个 bit 表示一个 block,如果该 bit 为 0,表示该 block 中有数据,如果 bit 为 1,则表示该 block 是空闲的。注意,这个块位图本身也正好只有一个block那么大小。假设 block 大小为 1024 bytes,那么 block bitmap 当中只能记载 8*1024 个 block 的情况(因为一个 byte 等于 8 个 bits,而一个 bit 对应一个 block)。这也就是说,一个块组最多只能有 8*1024*1024字节,即8MB这么大。
在块组的组描述符中另有一个 block 指针指向磁盘索引节点位图(inode bitmap),这个位图同样也是正好有一个块那么大,里面的每一个bit相对应一个磁盘索引节点。磁盘索引节点跟我们在VFS看到的那个inode有联系,但不是一个概念,关于 inode,我们下面还要进一步讲到。
在块组的组描述符中还有一个重要的 block 指针,是指向所谓的磁盘索引节点表(inode table)。这个磁盘索引节点表就不止一个 block 那么大了。这个磁盘索引节点表就是这个块组中所聚集到的全部磁盘索引节点表放在一起形成的。
一个磁盘索引节点表当中记载的最关键的信息,是这个磁盘索引节点表中的用户数据存放在什么地方。一个磁盘索引节点表大体上相对应于文件系统中的一个文件,那么用户文件的内容存放在什么地方,这就是一个 inode 要回答的问题。一个 inode 通过提供一系列的 block 指针,来回答这个问题。这些 block 指针指向的 block,里面就存放了用户文件的内容。
我们通过一个图,把以上内容组织起来,希望大家对Ext2文件系统的磁盘布局有个感性认识,这样我们才好继续后面的内容: