在Linux里,每一个档案都有一个file结构和inode结构,inode结构是用来让Kernel做管理的,而file结构则是我们平常对档案读写或开启,关闭所使用的。当然,从user的观点来看是看不出什么的。在Linux里,档案的观念应用的蛮广泛的,甚至是写一个driver你也只要提供一组的file operations就可以完成了。我们现在来看看File结构的内容。 strUCt file { struct file *f_next,**f_pprev; struct dentry *f_dentry; struct file_operations *f_op; mode_t f_mode; loff_t f_pos; unsigned int f_count,f_flags; unsigned long f_reada,f_ramax,f_raend,f_ralen,f_rawin; struct fown_struct f_owner; unsigned long f_version; void *private_data; }; 比起super_block和inode结构,file结构就显得小多了,file结构也是用串行来管理,f_next会指到下一个file结构,而f_pprev则是会指到上一个file结构地址的地址,不过,这个字段的用法跟一般指到前一个file结构的用法不太一样,有机会再跟各位讨论,f_dentry会记录其inode的dentry地址,f_mode为档案存取的种类,f_pos则是目前档案的offset,每次读写都从offset记录的位置开始读写,f_count是此file结构的reference cout,f_flags则是开启此档案的模式,f_reada,f_ramax,f_raend,f_ralen,f_rawin则是控制read ahead的参数,f_owner记录了要接收SIGIO和SIGURG的行程ID或行程群组ID,private_data则是tty driver所使用的字段。最后,我们来看看f_op这个字段。这个字段记录了一组的函式是专门用来使用档案的。 · llseek(file,offset,where) 我们写程序会呼叫lseek()系统呼叫设定从档案那个位置开始读写,这个函式你可以不用提供,因为系统已经有一个写好的,但是系统提供的llseek()没有办法让你将where设为SEEK_END,因为系统不知道你的档案长度是多少,所以没办法提供这样的服务。如果你不提供llseek()的话,那系统会直接使用它已经有的llseek()。llseek()必须要将file->offset的值做改新。 · read(file,buf,buflen,poffset) 当我们读取一个档案时,最终就是会呼叫read()这个函式来读取档案内容。这些参数VFS会替我们准备好,至于poffset则是offset的指针,这是要告诉read()从那里开始读,读完之后必须更新poffset的内容。请注意,在这里buf是一个地址,而且是一个位于user space的地址。 · write(file,buf,buflen,poffset) write()的动作就跟read()是相反的,参数也都一样,buf依然是位于user space的地址。 · readdir(file,dirent,filldir) 这是用来读取目录的下一个direntry的,其中file是file结构地址,dirent则是一个readdir_callback结构,这个结构里包含了使用者呼叫readdir()系统呼叫时所传过去的dirent结构地址,filldir则是一个函式指针,这个函式在VFS已经有提供了,这个函式其实是增加了kernel在读取dirent方面的弹性。当档案系统的readdir()被呼叫时,在它把下一个dirent取出来之后,应该要呼叫filldir(),让它把所需的资料写到user space的dirent结构里,也许还会多做些处理。有兴趣的朋友可以参考的filldir()函式。 · poll(file,poll_table) 之前的Kernel版本本来是在file_operations结构里有select()函式而不是poll()函式的。但是,这并不代表Linux不提供select()系统呼叫,相反的,Linux仍然提供select()系统呼叫,只不过select()系统呼叫implement的方式是使用poll()函式来做的。 · ioctl(inode,file,cmd,arg) ioctl()这个函式其实有很大的用途,尤其它可以做为user space的程序对Kernel的一个沟通管道。那ioctl()是什么时候被呼叫呢? 还记得平常写程序时偶而会用到ioctl()系统呼叫来直接控制档案或device吗? 是的,ioctl()系统呼叫最后就是把命令交给档案的f_op->ioctl()来执行。f_op->ioctl()要做的事很简单,只要根据cmd的值,做出适当的行为,并传回值即可。但是,ioctl()系统呼叫其实是分几个步骤的,第一,系统有几个内定的command它自己可以处理,在这种情形下,它不会呼叫f_op->ioctl()来处理。如果user指定的command是以下的一种,那VFS会自己处理。 o FIONCLEX 清除档案的close-on-exec位。 o FIOCLEX 设定档案的close-on-exec位。 o FIONBIO 如果arg传过来的值为0的话,就将档案的O_NONBLOCK属性去掉,但是如果不等于0的话,就将O_NONBLOCK属性设起来。 o FIOASYNC 如果arg传过来的值为0的话,就将档案的O_SYNC属性去掉,但是如果不等于0的话,就将O_SYNC属性设起来。只是在Kernel 2.2.1时,这个属性的功能还没完成。 如果cmd的值不是以上数种,而且如果file所代表的不是普通的档案的话,像是device之类的特殊档案,VFS会直接呼叫f_op->ioctl()去处理。但是如果file代表普通档案的话,那VFS会呼叫file_ioctl()做另外的处理。何谓另外的处理呢? file_ioctl()会再过澽一次cmd的值,如果是以下数种,它会先做些处理,然后再呼叫f_op->ioctl(),不管怎么样,file_ioctl()最后都会再呼叫f_op->ioctl()去处理。 o FIBMAP 先将arg指到的档案block number取出来,并呼叫f_op->bmap()计算出其disk上的block number,最后再将计算出来的block number放到arg参数里。 o FIGETBSZ 先取得档案系统block的大小并放入arg的参数里。 o FIONREAD 将档案剩下尚未读取的长度写到arg里。比方说档案大小是1000,而f_op->offset的值是300,表示还有700个byte尚未读取,所以,将700写到arg参数里。 · mmap(file,vmarea) 这个函式是用来将档案的部分内容映像到内存中的,file是指要被映像的档案,而vmarea则是用来描述到映像到内存的那里。 · open(inode,file) 当我们呼叫open()系统呼叫来开启档案时,open()会把所有的事都做好,最后则会呼叫f_op->open()看档案系统是否要做些什么事,一般来讲,VFS已经把事做好了,所以很多系统事实上根本不提供这个函式,当然,你要提供也可以,比方说,你可以在这个函式里计算这个档案系统的档案被使用过多少次等。 · flush(file) 这个函式也是新增加的,这个函式是在我们呼叫close()系统呼叫来关闭档案时所呼叫的。只要你呼叫close()系统呼叫,那close()就会呼叫flush(),不管那个时候f_count的值是否为0。这个函式我不是很确定在做什么的,事实上,在Ext2里也没有提供这么一个函式,也许是在关闭档案之前,VFS允许档案系统先做些backup的动作吧。 · release(inode,file) 这个函式也是在close()系统呼叫里使用的,当然,不尽在close()中使用,在别的地方也是有使用到。基本上,这个函式的定位跟open()很像,不过,当我们对一个档案呼叫close()时,只有当f_count的值归0时,VFS才会呼叫这个函式做处理。一般来讲,如果你在open()时配置了一些东西,那应该在release()时将配置的东西释放掉。至于f_count的值则是不用在open()和release()中控制,VFS已经在fget()和fput()中增减f_count了。 · fsync(file,dentry) fsync()这个函式主要是由buffer cache所使用,它是用来跟file这个档案的资料写到disk上。事实上,Linux里有两个系统呼叫,fsync()和fdatasync(),都是呼叫f_op->fsync()。它们几乎是一模一样的,差别在于fsync()呼叫f_op->fsync()之前会使用semaphore将f_op->fsync()设成critical section,而fdatasync()则是直接呼叫f_op->fsync()而不设semaphore。 · fasync(fd,file,on) 当我们呼叫fcntl()系统呼叫,并使用F_SETFL命令来设定档案的参数时,VFS就会呼叫fasync()这个函式,而当读写档案的动作完成时,行程会收到SIGIO的讯息。 · check_media_change(dev) 这个函式只对可以使用可移动的disk的block device有效而已,像是MO,CDROM,floopy disk等等。为什么对这些可以把disk随时抽取的需要提供这么一个函式呢? 其实,从字面上我们大概可以知道,这是用来检查disk是否换过了,以CDROM为例,每一个光盘片都代表一个档案系统,如果今天我们把光盘片换掉了,那表示这个档案系统不存在了,如果user此时去读取这个档案系统的资料,那会发生什么事? 很有可能系统就这么出事了。所以,对于这种的device,每当在mount时,我们就必须检查其中的disk是否换过了,如何检查呢? 当然只有档案系统本身才知道,所以,档案系统必须提供此函式。 · revalidate(dev) 这个函式跟上面的check_media_change()有着相当的关系。当user执行mount要挂上一个档案系统时,mount会先呼叫里的check_disk_change(),如果档案系统所属的device有提供这个函式的话,那check_disk_change()会先呼叫f_op->check_media_change()来检查是否其中的disk有换过,如果有则呼叫invalidate_inodes()和invalidate_buffers()将跟原本disk有关的buffer或inode都设为无效,如果档案系统所属的device还有提供revalidate()的话,那就再呼叫revalidate()将此device的资料记录好。 · lock(file,cmd,file_lock) 这个函式也是新增加的,在Linux里,我们可对一个档案呼叫fcntl()对它使用lock。如果呼叫fcntl()时,cmd的参数我们给F_GETLK,F_SETLK,或F_SETLKW时,那系统会间接呼叫f_op->lock来做事。当然,如果你的档案系统不想提供lock的功能的话,你可以不用提供这个函式。 list_head结构的介绍 list_head结构定义在里,它是一个double linked list的结构。底下是它的结构宣告: struct list_head { struct list_head *next, *prev; }; 有的人可能看到这样的结构会觉得很奇怪这样的结构可以存放资料吗? 当然是不行的啰,因为这个结构根本是拿来让人当资料存的。首先,我们先来看看两个macro, #define LIST_HEAD(name) \ struct list_head name = { &name, &name } #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) 这两个macro在Kernel里也算蛮常出现的,是用来将list_head做初始化的,它的初始化就是将next和prev这两个字段设为跟结构的地址相同。所以,如果我们在程序里看到这样的程序,它的意思就是宣告一个list_head结构的变量hello,并将prev和next都设成hello的地址。 LIST_HEAD(hello) 因此,如果要检查这个list是否是空的,只要检查hello.next是否等于&hello就可以了。事实上,Linux也提供了一个叫list_empty()的函式来检查list是否为空的。 static __inline__ int list_empty(struct list_head *head) { return head->next == head; } 现在我们来介绍如何加入或删除list_head到上面的hello串行里。Linux提供二个函式来做这些事,分别是list_add()和lis_del()。这两个函式的定义都放在里,而且其程序代码也都很简单,只是单纯double linked list的串接和删除而已,因此我们不对它们做介绍。有关于这个结构,其实最重要的应该是它提供的这个macro。 #define list_entry(ptr, type, member) \ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) 图:http://linuxfab.cx/Columns/17/Image5.gif 我们现在来做个实验,相信各位会更容易了解这个macro的。请看一下下面这段程序代码。 struct HelloWorld { int x, y; struct list_head list; } hello; 假设int是4个byte。那么以下这一行会得到8,如图5所示 (unsigned long) (&((struct HelloWorld *)0)->list) 有的人会对这一行程序感到奇怪,(struct HelloWorld*)0不就是一个NULL的指针吗? 怎么可以用0->list去参考list这个字段呢? 难道不怕造成segmentation fault吗? 请注意一下,我们在0->list的前面还加上了一个&。如果没有&,那上面这一行就会segmentation fault了。如果你加上了&,那就没问题啰。Segmentation fault通常是去参考到不合法的内存地址内容所造成的,如果我们加上了&就表示我们没有要去参考这个不合法地址的内容,我们只是要那个字段的地址而已,因此,不会造成segmentation fault。其实,结构的配置在内存里是连续的。所以,如果我们去读取某个字段时,像&hello->list。会先取得hello变量的地址,再然后再计算HelloWorld结构里list字段所在的offset,再将hello的地址加上list字段的offset,求得list字段真正的地址。然后再去读list字段的内容。这是compiler帮我们做的。那我们现在就来看看上面那一行究竟是什么意思。首先,我们先把上面那一行想象成下面这个样子。 ptr = 0; (unsigned long) (&((struct HelloWorld *)ptr)->list) 这样是不是容易懂了吗,就是要取得&ptr->list的地址而已。所以,如果ptr是100的话,那会得到100+8=108。因为前面有二个int,每一个int是4个byte。经过转型,就得到了(unsigned long)型态的108。如果ptr是0的话,那同理,我们会得到0+8=8。也就是这个字段在HelloWorld结构里的offset。 现在,如果我们已经知道了list在HelloWorld结构中的offset,而且我们现在也知道hello这个变量里list的地址的话,那有没有办法得到hello本身的地址呢? 可以的,就像图6一样,如果我们知道list的地址,只要将list的地址减8就可以知道了hello的地址了嘛。 struct list_head *plist = &hello.list; printf( "&hello = %x\n", (char*)plist - (unsigned long) 8 )); 而这种方式就是list_head的用法,它是专门用来当作别的结构的字段,只要我们得到这个字段的位置和包含这个字段的结构是那一种,我们可以很轻易的算出包含此字段的结构地址,图6就是super block在使用list_head所得到的结果。只要我们知道s_list的地址,只要呼叫 list_entry( &sb1.s_list, struct super_block, s_list) 就可以得到其sb1这个super_block结构的地址。
[1] [2] 下一页
(出处:http://www.sheup.com)