Inode 结构
inode 在Linux里 算是一个蛮大的结构, 跟super_block比起来可是不惶多让,基本上, 跟super_block结构一样, 我们一样可以把inode结构分成几部分来看: 串行管理栏位, 基本资料, 用来做inode synchronization的资料, 跟记忆体管理有关的资料, Quota管理栏位, 跟file lock有关的栏位, 以及一组用来操作inode的函式。 以下我们分别来说明这些栏位的意义。
inode结构前三个栏位就是用来帮助将inode串起来的栏位, 分别是
struct list_head i_hash; struct list_head i_list; struct list_head i_dentry; |
这 跟我们在super block那里所看到的s_list是属于同样的型别,都是struct list_head。 list_head这种结构在Kernel里实在用的很多, 事实上, 它也的确很好用。 我们将在这篇文章的最后跟您彻底讨论list_head结构以及它的用法。现在我们只要知道list_head可以帮我们将一些结构串行在一起就够了。 在VFS里, 有四个串行是用来管理inode的, 分别是inode_unused用来将目前还没使用的inode串在一起,它就是使用i_list这个栏位。 第二个是inode_in_use用来将目前正在使用的inode串在一起, 当一个inode被使用时, 它会从inode_unused中被取出来,因此, 此时i_list不会被用到, 接着它会利用i_list栏位放到inode_in_use中。 第三个是sb->s_dirty用来将dirty inode串行在一起。 这个串行的开头位于super block的s_dirty栏位, 一样也是使用i_list串接。 所有正在使用中的inode都可以经由inode_in_use串行找到,但是, 因为系统的inode太多, 所以, 串行可能会很长, 如果慢慢找, 在速度上并不理想, 因此, 每个使用中的inode都会计算出其hash value,并且放到hash table, 但是hash table有时会有collision的情形出现, 因此每一个entry是由一个list串接起来, 这个list就是利用i_hash栏位来串接的。至于i_dentry是在dcache中使用的, dcache利用这个栏位将inode串接起来。
inode的基本资料蛮多, 在此我很简略的跟各位介绍一下
unsigned long i_ino; |
每一个inode都有一个序号, 经由super block结构和其序号,我们可以很轻易的找到这个inode。
unsigned int i_count; |
在Kernel里, 很多的结构都会记录其reference count, 以确保如果某个结构正在使用, 它不会被不小心释放掉, i_count就是其reference count。
kdev_t i_dev; /* inode所在的device代码 */ umode_t i_mode; /* inode的许可权 */ nlink_t i_nlink; /* hard link的个数 */ uid_t i_uid; /* inode拥有者的id */ gid_t i_gid; /* inode所属的群组id */ kdev_t i_rdev; /* 如果inode代表的是device的话,那此栏位将记录device的代码 */ |
off_t i_size; /* inode所代表的档案大小 */ time_t i_atime; /* inode最近一次的调用时间 */ time_t i_mtime; /* inode最近一次的修改时间 */ time_t i_ctime; /* inode的产生时间 */ unsigned long i_blksize; /* inode在做IO时的区块大小 */ unsigned long i_blocks; /* inode所使用的block数,一个block为512 byte*/ unsigned long i_version; /* 版本号码 */ unsigned long i_nrpages; /* inode所使用的page个数 */ struct page *i_pages; /* inode使用的page会被放在串行里,这个栏位记录着此串行的开头 */ struct super_block *i_sb; /* inode所属档案系统的super block */ unsigned long i_state; /* inode目前的状态,可以是I_DIRTY, I_LOCK和 I_FREEING的OR组合 */ unsigned int i_flags; /* 记录此inode的参数 */ unsigned char i_pipe; /* 用来记录此inode是否为pipe */ unsigned char i_sock; /* 用来记录此inode是否为socket */ unsigned int i_attr_flags; /* 用来记录此inode的属性参数 */ struct file_lock *i_flock; /* 用来做file lock */ |
在Linux里, 我们可以利用mmap()将档案或device的某个区块映像到记体里使用。在inode里这两个栏位就是跟它有关的:
struct vm_area_struct *i_mmap; int i_writecount; |
i_writecount 这个栏位的值是用来记录目前有多少个行程是以可写入的模式开启此档案的。为什么需要这个值呢? 因为系统没办法支持可以对一个档案写入, 而又同时将这个档案映像为MAP_DENYWRITE的模式, 所以, 用这个栏位来代表目前有多个行程可对此inode做写入的动作或是有多少个行程将它映像成MAP_DENYWRITE的模式。它的值有以下三种情形:
0: 没有行程将它开启为可写入, 也没有行程对它做MAP_DENYWRITE的映像 < 0: 有-i_writecount个行程对它做MAP_DENYWRITE的映像。 > 0: 有i_writecount个行程将它开启为可写入模式。 |
至于i_mmap这个栏位就是用来做记忆体映像的栏位。
就 跟super_block结构一样, Kernel里的重要结构在修改时,都必须做好synchronization的动作, 以免产生race condition, 造成系统出错。 因此, 当我们要修改某个inode结构时, 必须先确定没有人在使用这个inode才行。这件事是使用semaphore和wait queue来完成的。
struct wait_queue *i_wait; struct semaphore i_sem; |
除 了这两个栏位之外, 新版的Kernel又多加了一个栏位叫i_atomic_write,这也是一个semaphore, 那它的用途又是什么呢? 相信如果你用过pipe的话, 一定知道当我们写资料到pipe里的时候, 资料长度必须小于等于PIPE_BUF这个值,所以当写入的资料小于等于PIPE_BUF时, Kernel要确保写入的动作是atomic的, 因此加了这个栏位来做控制。
struct semaphore i_atomic_write; |
struct dquot *i_dquot[MAXQUOTAS]; |
目前的quota管理还可以分为user quota管理和group quota管理, 所以, 其实MAXQUOTAS这个常数的值是2。 在i_dquot里, 一个是用来管理user quota, 另一个则是管理group quota。
就跟super_block结构一样, 每一个inode都有一个i_op的栏位用来记录一组操做inode的函式。
struct inode_operations *i_op; |
接下来, 我们就来看看inode_operations结构里各个函式是做什么用的:
当 我们要产生一个新的档案时, Kernel必须要先为这个档案产生一个inode, 当然, 配置inode记忆体这种事是属于VFS的工作范围, 但是产生一个inode这件事跟档案系统本身有蛮大的关系,因此, VFS会呼叫档案系统里i_op->create()来做些额外的事, 那i_op这个栏位是打那儿来的呢? 因为一个档案一定是位于某个目录底下, 所以, i_op这个栏位就是从档案所在目录的inode里取出来的。 而传给create()的dir就是那个目录的dentry指标, dentry则是我们要产生的档案的dentry (此时dentry已经配置好, 但内部的资料却还没填入), 而mode则是产生档案时所给的模式。 我们知道Linux里有很多种的inode, 有的是代表普通档案,有的则是代表目录, 还有代表socket, pipe的。 不同种类的inode其i_op所提供的函式都不尽相同, 像一个普通的档案, 我们根本不可能去呼叫它的create()函式,因为它不是目录, 它没办法在目录底下产生一个inode。 而像代表目录的inode就必须要提供create()才行, 不然没办法在其底下产生子目录或档案。
这 个函式也是代表目录的inode所应该提供的。 比方说我们有一个档案叫/usr/tmp/hello.txt, 如果我们想读取这个档案的内容时, 第一步就是要开启这个档案,如果要开启这个档案, 我们首先就得先找到这个档案的inode。 那Kernel是怎么找到它的inode的呢? 它会呼叫根目录的inode->i_op->lookup()找到/usr的dentry,则呼叫/usr目录的inode-> i_op_lookup()找到/usr/tmp的dentry, 接着再呼叫/usr/tmp的inode->i_op->lookup()找到/usr/tmp/hello.txt的dentry。而从它 的dentry我们自然可以取得它的inode。 而lookup()的用处就是从dir目录底下找到名称跟dentry指定的相同的档案dentry, 基本上,这是属于档案系统应该做的事, VFS只负责帮你配置好dentry结构, 并填入要找的档案名称。
在Linux 里, 除了symbolic link之外, 还有一种叫hard link的东西, symbolic link有它自己的inode, 只是其内容指到别的档案的路径而已, 但是hard link却是跟指到的档案共享一个inode, 但是, hard link只能跟指到的档案位于同一个档案系统而已。 当被指到的档案被删除时, 只是你看不到那个档案而已,事实上, 档案仍然是存在的, 你可以使用之前建立的hard link来读取它。 系统有提供一个叫ln的命令可以产生hard link, 有兴趣的朋友可以试试看。而就programmer来讲, 系统也提供了一个叫link()的系统呼叫来做hard link。 link()在准备好一切之后, 会呼叫i_op->link()去处理档案系统方面要做的事, i_op->link()至少应该要将inode->i_nlink的值加一才行。 在i_op->link()的参数里, old_dentry是指被指到档案的dentry, dir是指我们所要产生的link所在目录的dentry, 至于dentry则是要产生的link的dentry。 这个函式是代表目录的inode所应该提供的。
相 信很多人都用过unlink()这个系统呼叫,这是用来将dir指到的目录底下的dentry档案删除掉。 在真正删除之前, 它会去检查dentry->d_inode->i_nlink是否归0, 只有在nlink的值是0时才会删除。 unlink()系统呼叫最后会呼叫i_op->unlink()去做档案系统额外要做的事, 它至少应该把dentry->d_inode->i_nlink的值减一才对。跟i_op->link()一样, i_op->unlink()也是目录型别的inode所应提供的。
这 个函式故名思义就是用来产生symbolic link用的。 dir是symbolic link所在的目录dentry, symname则是symbolic link的内容, 通常是个路径名称, 至于dentry则是symbolic link本身的dentry。 系统提供了一个symlink()的系统呼叫, 就是用来做symbolic link的, 它最后也是呼叫i_op->symlink()来处理。 当然, 每个档案系统内部要如何产生symbolic link的方式不尽相同, 以ext2来讲,如果symname的长度小于60个byte的话, 那在Ext2而言, 这是一个fast symbolic link, 因为路径名称就直接存在inode结构里,不用另外读取disk, 所以, 当i_op->symblink()被呼叫时, 它的工作就是将路径名称加到inode里, 但是如果大于等于60个byte,那就称为slow symbolic link, symname的内容会被放到disk上的block里, 此时, i_op->symlink()就需要配置一个block存放symname的内容。这个函式也是目录型别的inode所应提供的, 当然, 如果你不想提供的话, 也可以直接设成NULL。
这 个函式就是在产生一个目录时用, 系统有提供mkdir()系统呼叫来产生目录, 而这个系统呼叫最后会呼叫i_op->mkdir()来做底层的事情。 dir是指我们要产生的目录所在的目录,至于dentry则是要产生的目录dentry, mode则是目录的许可权。 之前我们曾说过, 每个inode->i_nlink记录了hard link的个数,而事实上, 在代表目录的inode里, i_nlink的意义则跟它很像, 它的意思是指目录里有几个档案或子目录, 所以, 每个目录刚产生时, 它的i_nlink的都是2,因为, 每个目录至少有二个子目录, 分别是"."和".."。 当产生完子目录之后, dir->d_inode->i_nlink的值也应该加1才对。 如果inode是目录的话, 那它应该提供这个函式才对。
i_op ->rmdir()所做的事是跟mkdir()是相反的。跟mkdir()一样, i_op->rmdir()最后也会被rmdir()系统呼叫所使用。 当VFS要呼叫rmdir()之前, 它会先替我们把要删除的目录名称dentry找到,并把其父目录的dentry也找到, 其中dir就是其父目录dentry, dentry就是指要删除的目录的dentry。 当然, 在呼叫i_op->rmdir()去删除目录时, VFS会先呼叫permission()并检查我们是否可以删除此目录并检查目录此时的状态, 比方像目录是否现在被mount, 是否为系统根目录, 以及使用者要删除的是否为目录等等。所以, i_op->rmdir()要做的事就是纯粹检查目录是否为空的, 是否目前还有别人在使用它, 并做好删除目录的事情。 如果inode是目录的话, 那它应该提供这个函式才对。
在Linux 里, 也有一个命令是叫mknod。 mknod命令主要是用来产生的special file, 像是character device, block device, 或fifo, socket之类的东西。同时, 系统里也有一个系统呼叫mknod(), 这个mknod()系统呼叫不尽可以产生特殊档案, 也可以产生一般的档案, 详情可见其man page。 而事实上, mknod()系统呼叫最后也是呼叫档案系统的mknod()函式。 mknod()系统呼叫会先检查user给的参数是否对, 像是如果你指定要产生一个目录, VFS就先把你踢掉, 除此之外, VFS还会先替你产生一个空的dentry用来放要产生的档案, 当然, 它也会检查user是否有权力产生这个档案, 最后, 它会把重头戏都交给i_op->mknod()去做。而i_op->mknod()要做什么呢? 当然, 这部分是跟各个档案系统内部有关, 基本上, 这个函式需要在dir这个目录底下产生一个inode, 其模式为mode,如果mknod()要产生的档案是device的话, 那rdev就这个device的major number与minor number组合。 除此之外, 最重要的一件事就是要根据mode,在inode->i_op填入适当的值, 比方说, 如果产生一个character device, 那inode->i->op应该指定一组操作character device的函式, 如果产生的是普通档案的话, 那inode->i_op也应填入操作普通档案inode的函式。 这个函式对代表inode的目录而言, 也是应该要提供的。
这 个函式是用来将位于old_dir里的old_dentry档案改名为new_dir里的new_dentry档案名称。档案系统所提供的 rename()要做的事就是根据系统的implementation把改名字的事情做好, 其它像是许可权的检查在上层VFS会帮我们做好。 要注意的是, old_dir->d_inode->i_nlink的值应该减一, 而new_dir->d_inode->i_nlink的值则是应该加一。这个函式在Kernel里只有被系统呼叫rename()呼叫而 已。 有的人可能会以为这个函式应该由代表档案的inode提供, 但事实上, 这个函式必须要由代表目录的inode提供。理由就留给各位去想了。
只 有当inode是代表一个symbolic link时, 才需要提供这个函式, 其它诸如档案或目录是不用提供这个函式的。 这个函式的用处在于读取symbolic link的内容, 也就是读取symbolic link指到的档案路径。 跟上面其它的函式一样, 这个函式最后也是会被系统呼叫readlink()所呼叫。 至于readlink()要如何做是跟档案系统的implementation有关。像ext2, 当档案路径的长度小于60个时, 会直接从inode里读出资料, 如果不是, 则会读取disk上记录路径的block内容。 dentry是代表symbolic link的inode, buffer是要路径存放的位置, 至于buflen则是buffer的长度。
跟 前一个函式一样, follow_link()这个函式只有symbolic link的inode需要提供。 我们知道, 当我们读到一个symbolic link叫a时, 如果a指到/usr/hello.txt的话,那当我们读a时, 事实上会读到/usr/hello.txt。 这部分的工作就是由follow_link()完成的。这部分的转换就使用者的观点来看是不会感觉到的。在Linux里, 并没有一个系统呼叫会呼叫follow_link()的。 这个函式事实上是由lookup_dentry()呼叫do_follow_link(),再由do_follow_link()呼叫i_op-> follow_link()。 在Kernel里, 寻找某个档案的inode是由namei(), 再由它呼叫lookup_dentry()完成的, lookup_dentry()会由目录的最上层一层一层的找, 如果找到的档案是symbolic link时, 它最后会呼叫symbolic link的follow_link(),而follow_link()应该要读取所指到的档案路径, 并且再呼叫lookup_dentry()去找这个档案, 找到之后, 再把它的dentry传回去。
在Linux 里, 每一个inode都代表一个档案或目录,而每一个档案在系统中则是由一个file结构所记录, readpage()就是将此file里的page内容读进来。 基本上, VFS已经提供了一个readpage()的函式叫generic_readpage(),定义在, 可以直接使用这个函式。
这个函式则是跟readpage()相反,是将page中的内容写回file里。 但是, 在VFS里并没有提供这样的一个函式可供使用, 所以, 如果有需要的话, 需要自己提供。
bmap()主要是用在做记忆体映像时用的。 block是一个数字, 它代表的是inode所代表的档案逻辑上的第几个block, bmap()负责将这个block的序号转换成disk上的区块序号。
truncate ()的作用就是用来将inode所代表的档案长度减小或增加,当然, 详细的implementation是要依照系统而有所不同。 至于最后的长度应该是多少, 则是由VFS在呼叫i_op->truncate()之前将想要改变的长度填在inode->i_size里。
故 名思义, 这个函式用来检查inode的许可权, 一般来讲, i_op->permission()在Kernel里并不会被直接呼叫, VFS提供一个也叫permission()函式,这个函式会去呼叫i_op->permission()。 一般系统里如果要检查许可权都是直接呼叫VFS提供的permission(), 再由VFS的permission()去呼叫i_op->permission()。如果档案系统有提供i_op->permission ()时, 那就以i_op->permission()的结果为准。 如果没有, 就依照VFS的标准来做。 mask的可以是MAY_READ, MAY_WRITE, 和MAY_EXEC这三个值的OR组合。
smap()的作用跟bmap()很像,但是, sector在这里指到是disk上的sector number。 而不是逻辑上的block number。 大部分的档案系统都没有提供这个函式, 除了在umsdos有提供之外。
关于这个函式我也不太清楚, 不过, 可以知道的是, 这个函式目前只有NFS档案系统有提供。 有兴趣的朋友可以参考NFS的源代码。
由于NFS有cache的问题,所以, 这个函式主要也是在NFS中所使用的, 为的是将dentry->i_node的内容做***。 在里有一个函式叫do_revalidate()就会呼叫这个函式,很多系统呼叫像stat等都会呼叫do_revalidate()对inode做***。
如 果我们去看inode_operations结构的内容,就可以发现第一个栏位是default_file_ops。 其实这也是一组的函式, 在Linux里, 每一个档案都会有一个file结构来描述, 而每一个file结构都会定义一组的函式来操作file结构,在inode里, 也同时记录了用来操作inode所代表的档案的函式。 在开始讲操作file结构的函式之前, 让我们来看看file结构的内容。