先根据读内核源码一书的理解,做如下记录:
1.内存缓冲区是用户和硬盘文件进行交互的必经之地。当进行读硬盘时,先依据dev、块号等信息获取缓冲块(这是由bread-->getblk),然后调用ll_rw_block函数创造读请求,然后调用hd_out函数读取后放入缓冲区内,中断返回。
2.超级块(super_block)主要用于存放整个文件系统的信息,当创建新的硬盘逻辑块时会调用new_block,该函数会先读取超级块(get_super(dev))的信息,看看是否满足条件、设置位图等等。
一:
什么是文件系统,详见:http://zh.wikipedia.org/zh/%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F
其实一句话就是管理这块文件的机制(组织方式,数据结构之类...)
Linux系统中存在很多的文件系统,例如常见的ext2,ext3,ext4,sysfs,rootfs,proc...很多很多。。。我们知道每个文件系统是独立的,有自己的组织方法,操作方法。那么对于用户来说,不可能所有的文件系统都了解,那么怎么做到让用户透明的去处理文件呢?例如:我想写文件,那就直接read就OK,不管你是什么文件系统,具体怎么去读!OK,这里就需要引入虚拟文件系统。
所以虚拟文件系统就是:对于一个system,可以存在多个“实际的文件系统”,例如:ext2,ext3,fat32,ntfs...例如我现在有多个分区,对于每一个分区我们知道可以是不同的“实际文件系统”,例如现在三个磁盘分区分别是:ext2,ext3,fat32,那么每个“实际的文件系统”的操作和数据结构什么肯定不一样,那么,用户怎么能透明使用它们呢?那么这个时候就需要VFS作为中间一层!用户直接和VFS打交道。例如read,write,那么映射到VFS中就是sys_read,sys_write,那么VFS可以根据你操作的是哪个“实际文件系统”(哪个分区)来进行不同的实际的操作!那么这个技术也是很熟悉的“钩子结构”(此名称不知道是否合理,自己一直这样叫了)技术来处理的。其实就是VFS中提供一个抽象的struct结构体,然后对于每一个具体的文件系统要把自己的字段和函数填充进去,这样就解决了异构问题。
如图:
二:
Linux虚拟文件系统四大对象:
1)超级块(super block)
2)索引节点(inode)
3)目录项(dentry)
4)文件对象(file)
=> 超级块:一个超级块对应一个文件系统(已经安装的文件系统类型如ext2,此处是实际的文件系统哦,不是VFS)。之前我们已经说了文件系统用于管理这些文件的数据格式和操作之类的,系统文件有系统文件自己的文件系统,同时对于不同的磁盘分区也有可以是不同的文件系统。那么一个超级块对于一个独立的文件系统。保存文件系统的类型、大小、状态等等。
(“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block)
既然我们知道对于不同的文件系统有不同的super_block,那么对于不同的super_block的操作肯定也是不同的,所以我们在下面的super_block结构中可以看到上面说的抽象的struct结构(例如下面的:struct super_operations):
(linux内核2.4.37)
- "font-size:14px;">struct super_block {
- 746 struct list_head s_list;
- 747 kdev_t s_dev;
- 748 unsigned long s_blocksize;
- 749 unsigned char s_blocksize_bits;
- 750 unsigned char s_dirt;
- 751 unsigned long long s_maxbytes;
- 752 struct file_system_type *s_type;
- 753 struct super_operations *s_op;
- 754 struct dquot_operations *dq_op;
- 755 struct quotactl_ops *s_qcop;
- 756 unsigned long s_flags;
- 757 unsigned long s_magic;
- 758 struct dentry *s_root;
- 759 struct rw_semaphore s_umount;
- 760 struct semaphore s_lock;
- 761 int s_count;
- 762 atomic_t s_active;
- 763
- 764 struct list_head s_dirty;
- 765 struct list_head s_locked_inodes;
- 766 struct list_head s_files;
- 767
- 768 struct block_device *s_bdev;
- 769 struct list_head s_instances;
- 770 struct quota_info s_dquot;
- 771
- 772 union {
- 773 struct minix_sb_info minix_sb;
- 774 struct ext2_sb_info ext2_sb;
- 775 struct ext3_sb_info ext3_sb;
- 776 struct hpfs_sb_info hpfs_sb;
- 777 struct ntfs_sb_info ntfs_sb;
- 778 struct msdos_sb_info msdos_sb;
- 779 struct isofs_sb_info isofs_sb;
- 780 struct nfs_sb_info nfs_sb;
- 781 struct sysv_sb_info sysv_sb;
- 782 struct affs_sb_info affs_sb;
- 783 struct ufs_sb_info ufs_sb;
- 784 struct efs_sb_info efs_sb;
- 785 struct shmem_sb_info shmem_sb;
- 786 struct romfs_sb_info romfs_sb;
- 787 struct smb_sb_info smbfs_sb;
- 788 struct hfs_sb_info hfs_sb;
- 789 struct adfs_sb_info adfs_sb;
- 790 struct qnx4_sb_info qnx4_sb;
- 791 struct reiserfs_sb_info reiserfs_sb;
- 792 struct bfs_sb_info bfs_sb;
- 793 struct udf_sb_info udf_sb;
- 794 struct ncp_sb_info ncpfs_sb;
- 795 struct usbdev_sb_info usbdevfs_sb;
- 796 struct jffs2_sb_info jffs2_sb;
- 797 struct cramfs_sb_info cramfs_sb;
- 798 void *generic_sbp;
- 799 } u;
- 800
-
-
-
- 804 struct semaphore s_vfs_rename_sem;
- 805
- 806
-
-
-
-
-
-
- 813 struct semaphore s_nfsd_free_path_sem;
- 814 };
解释字段:
s_list:指向超级块链表的指针,这个struct list_head是很熟悉的结构了,里面其实就是用于连接关系的prev和next字段。
内核中的结构处理都是有讲究的(内核协议栈中也说过),内核单独使用一个简单的结构体将所有的super_block都链接起来,但是这个结构不是super_block本身,因为本身数据结构太大,效率不高,所有仅仅使用
struct
{
list_head prev;
list_head next;
}
这样的结构来将super_block中的s_list链接起来,那么遍历到s_list之后,直接读取super_block这么长的一个内存块,就可以将这个
super_block直接读进来!这样就很快捷方便!这也是为什么s_list必须放在第一个字段的原因。
s_dev:包含该具体文件系统的块设备标识符。例如,对于 /dev/hda1,其设备标识符为 0x301
s_blocksize:文件系统中数据块大小,以字节单位
s_blocksize_bits:上面的size大小占用位数,例如512字节就是9 bits
s_dirt:脏位,标识是否超级块被修改
s_maxbytes:允许的最大的文件大小(字节数)
struct file_system_type *s_type:文件系统类型(也就是当前这个文件系统属于哪个类型?ext2还是fat32)
要区分“文件系统”和“文件系统类型”不一样!一个文件系统类型下可以包括很多文件系统即很多的super_block,后面会说!
struct super_operations *s_op:指向某个特定的具体文件系统的用于超级块操作的函数集合
struct dquot_operations *dq_op:指向某个特定的具体文件系统用于限额操作的函数集合
struct quotactl_ops *s_qcop:用于配置磁盘限额的的方法,处理来自用户空间的请求
s_flags:安装标识
s_magic:区别于其他文件系统的标识
s_root:指向该具体文件系统安装目录的目录项
s_umount:对超级块读写时进行同步
s_lock:锁标志位,若置该位,则其它进程不能对该超级块操作
s_count:对超级块的使用计数
s_active:引用计数
s_dirty:已修改的索引节点inode形成的链表,一个文件系统中有很多的inode,有些inode节点的内容会被修改,那么会先被记录,然后写回磁盘。
s_locked_inodes:要进行同步的索引节点形成的链表
s_files:所有的已经打开文件的链表,这个file和实实在在的进程相关的
s_bdev:指向文件系统被安装的块设备
u:u 联合体域包括属于具体文件系统的超级块信息
s_instances:具体的意义后来会说的!(同一类型的文件系统通过这个子墩将所有的super_block连接起来)
s_dquot:磁盘限额相关选项
=>索引节点inode:保存的其实是实际的数据的一些信息,这些信息称为“元数据”(也就是对文件属性的描述)。例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,指向存储该内容的磁盘区块的指针,文件分类等等。
( 注意数据分成:元数据+数据本身 )
同时注意:inode有两种,一种是VFS的inode,一种是具体文件系统的inode。前者在内存中,后者在磁盘中。所以每次其实是将磁盘中的inode调进填充内存中的inode,这样才是算使用了磁盘文件inode。
注意inode怎样生成的:每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定(现代OS可以动态变化),一般每2KB就设置一个inode。一般文件系统中很少有文件小于2KB的,所以预定按照2KB分,一般inode是用不完的。所以inode在文件系统安装的时候会有一个默认数量,后期会根据实际的需要发生变化。
注意inode号:inode号是唯一的,表示不同的文件。其实在Linux内部的时候,访问文件都是通过inode号来进行的,所谓文件名仅仅是给用户容易使用的。当我们打开一个文件的时候,首先,系统找到这个文件名对应的inode号;然后,通过inode号,得到inode信息,最后,由inode找到文件数据所在的block,现在可以处理文件数据了。
inode和文件的关系:当创建一个文件的时候,就给文件分配了一个inode。一个inode只对应一个实际文件,一个文件也会只有一个inode。inodes最大数量就是文件的最大数量。
维基上说的也比较详细:维基-inode
- "font-size:14px;">440 struct inode {
- 441 struct list_head i_hash;
- 442 struct list_head i_list;
- 443 struct list_head i_dentry;
- 444
- 445 struct list_head i_dirty_buffers;
- 446 struct list_head i_dirty_data_buffers;
- 447
- 448 unsigned long i_ino;
- 449 atomic_t i_count;
- 450 kdev_t i_dev;
- 451 umode_t i_mode;
- 452 unsigned int i_nlink;
- 453 uid_t i_uid;
- 454 gid_t i_gid;
- 455 kdev_t i_rdev;
- 456 loff_t i_size;
- 457 time_t i_atime;
- 458 time_t i_mtime;
- 459 time_t i_ctime;
- 460 unsigned int i_blkbits;
- 461 unsigned long i_blksize;
- 462 unsigned long i_blocks;
- 463 unsigned long i_version;
- 464 unsigned short i_bytes;
- 465 struct semaphore i_sem;
- 466 struct rw_semaphore i_alloc_sem;
- 467 struct semaphore i_zombie;
- 468 struct inode_operations *i_op;
- 469 struct file_operations *i_fop;
- 470 struct super_block *i_sb;
- 471 wait_queue_head_t i_wait;
- 472 struct file_lock *i_flock;
- 473 struct address_space *i_mapping;
- 474 struct address_space i_data;
- 475 struct dquot *i_dquot[MAXQUOTAS];
- 476
- 477 struct list_head i_devices;
- 478 struct pipe_inode_info *i_pipe;
- 479 struct block_device *i_bdev;
- 480 struct char_device *i_cdev;
- 481
- 482 unsigned long i_dnotify_mask;
- 483 struct dnotify_struct *i_dnotify;
- 484
- 485 unsigned long i_state;
- 486
- 487 unsigned int i_flags;
- 488 unsigned char i_sock;
- 489
- 490 atomic_t i_writecount;
- 491 unsigned int i_attr_flags;
- 492 __u32 i_generation;
- 493 union {
- 494 struct minix_inode_info minix_i;
- 495 struct ext2_inode_info ext2_i;
- 496 struct ext3_inode_info ext3_i;
- 497 struct hpfs_inode_info hpfs_i;
- 498 struct ntfs_inode_info ntfs_i;
- 499 struct msdos_inode_info msdos_i;
- 500 struct umsdos_inode_info umsdos_i;
- 501 struct iso_inode_info isofs_i;
- 502 struct nfs_inode_info nfs_i;
- 503 struct sysv_inode_info sysv_i;
- 504 struct affs_inode_info affs_i;
- 505 struct ufs_inode_info ufs_i;
- 506 struct efs_inode_info efs_i;
- 507 struct romfs_inode_info romfs_i;
- 508 struct shmem_inode_info shmem_i;
- 509 struct coda_inode_info coda_i;
- 510 struct smb_inode_info smbfs_i;
- 511 struct hfs_inode_info hfs_i;
- 512 struct adfs_inode_info adfs_i;
- 513 struct qnx4_inode_info qnx4_i;
- 514 struct reiserfs_inode_info reiserfs_i;
- 515 struct bfs_inode_info bfs_i;
- 516 struct udf_inode_info udf_i;
- 517 struct ncp_inode_info ncpfs_i;
- 518 struct proc_inode_info proc_i;
- 519 struct socket socket_i;
- 520 struct usbdev_inode_info usbdev_i;
- 521 struct jffs2_inode_info jffs2_i;
- 522 void *generic_ip;
- 523 } u;
- 524 };
解释一些字段:
i_hash:指向hash链表指针,用于inode的hash表,下面会说
i_list:指向索引节点链表指针,用于inode之间的连接,下面会说
i_dentry:指向目录项链表指针,注意一个inodes可以对应多个dentry,因为一个实际的文件可能被链接到其他的文件,那么就会有另一个dentry,这个链表就是将所有的与本inode有关的dentry都连在一起。
i_dirty_buffers和i_dirty_data_buffers:脏数据缓冲区
i_ino:索引节点号,每个inode都是唯一的
i_count:引用计数
i_dev:如果inode代表设备,那么就是设备号
i_mode:文件的类型和访问权限
i_nlink:与该节点建立链接的文件数(硬链接数)
i_uid:文件拥有者标号
i_gid:文件所在组标号
i_rdev:实际的设备标识
注意i_dev和i_rdev之间区别:如果是普通的文件,例如磁盘文件,存储在某块磁盘上,那么i_dev代表的就是保存这个文件的磁盘号,但是如果此处是特殊文件例如就是磁盘本身(因为所有的设备也看做文件处理),那么i_rdev就代表这个磁盘实际的磁盘号。
i_size:inode所代表的的文件的大小,以字节为单位
i_atime:文件最后一次访问时间
i_mtime:文件最后一次修改时间
i_ctime:inode最后一次修改时间
i_blkbits:块大小,字节单位
i_blksize:块大小,bit单位
i_blocks:文件所占块数
i_version:版本号
i_bytes:文件中最后一个块的字节数
i_sem:指向用于同步操作的信号量结构
i_alloc_sem:保护inode上的IO操作不被另一个打断
i_zombie:僵尸inode信号量
i_op:索引节点操作
i_fop:文件操作
i_sb:inode所属文件系统的超级块指针
i_wait:指向索引节点等待队列指针
i_flock:文件锁链表
注意下面:address_space不是代表某个地址空间,而是用于描述页高速缓存中的页面的。一个文件对应一个address_space,一个address_space和一个偏移量可以确定一个页高速缓存中的页面。
i_mapping:表示向谁请求页面
i_data:表示被inode读写的页面
i_dquot:inode的磁盘限额
关于磁盘限额:在多任务环境下,对于每个用户的磁盘使用限制是必须的,起到一个公平性作用。
磁盘限额分为两种:block限额和inode限额,而且对于一个特文件系统来说,使用的限额机制都是一样的,所以限额的操作函数
放在super_block中就OK!
i_devices:设备链表。共用同一个驱动程序的设备形成的链表。
i_pipe:指向管道文件(如果文件是管道文件时使用)
i_bdev:指向块设备文件指针(如果文件是块设备文件时使用)
i_cdev:指向字符设备文件指针(如果文件是字符设备时使用)
i_dnotify_mask:目录通知事件掩码
i_dnotify:用于目录通知
i_state:索引节点的状态标识:I_NEW,I_LOCK,I_FREEING
i_flags:索引节点的安装标识
i_sock:如果是套接字文件则为True
i_write_count:记录多少进程以刻写模式打开此文件
i_attr_flags:文件创建标识
i_generation:保留
u:具体的inode信息
注意管理inode的四个链表:
inode_unused:将目前还没有使用的inode链接起来(通过i_list域链接)
inode_in_use:目前正在使用的inode链接起来(通过i_list域链接)
super_block中的s_dirty:将所有修改过的inode链接起来,这个字段在super_block中(通过i_list域链接起来)
inode_hashtable:注意为了加快inode的查找效率,将正在使用的inode和脏inode也会放在inode_hashtable这样一个hash结构中,
但是,不同的inode的hash值可能相等,所以将hash值相等的这些inode通过这个i_hash字段连接起来。
=>目录项:目录项是描述文件的逻辑属性,只存在于内存中,并没有实际对应的磁盘上的描述,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计。注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。例如:open一个文件/home/xxx/yyy.txt,那么/、home、xxx、yyy.txt都是一个目录项,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。
注意:目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。
- "font-size:14px;"> 67 struct dentry {
- 68 atomic_t d_count;
- 69 unsigned int d_flags;
- 70 struct inode * d_inode;
- 71 struct dentry * d_parent;
- 72 struct list_head d_hash;
- 73 struct list_head d_lru;
- 74 struct list_head d_child;
- 75 struct list_head d_subdirs;
- 76 struct list_head d_alias;
- 77 int d_mounted;
- 78 struct qstr d_name;
- 79 unsigned long d_time;
- 80 struct dentry_operations *d_op;
- 81 struct super_block * d_sb;
- 82 unsigned long d_vfs_flags;
- 83 void * d_fsdata;
- 84 unsigned char d_iname[DNAME_INLINE_LEN];
- 85 };
解释一些字段:
d_count:引用计数
d_flags:目录项缓存标识,可取DCACHE_UNUSED、DCACHE_REFERENCED等
d_inode:与该目录项关联的inode
d_parent:父目录的目录项
d_hash:内核使用dentry_hashtable对dentry进行管理,dentry_hashtable是由list_head组成的链表,一个dentry创建之后,就通过
d_hash链接进入对应的hash值的链表中。
d_lru:最近未使用的目录项的链表
d_child:目录项通过这个加入到父目录的d_subdirs中
d_subdirs:本目录的所有孩子目录链表头
d_alias:一个有效的dentry必然与一个inode关联,但是一个inode可以对应多个dentry,因为一个文件可以被链接到其他文件,所以,这个dentry就是通过这个字段链接到属于自己的inode结构中的i_dentry链表中的。(inode中讲过)
d_mounted:安装在该目录的文件系统的数量!注意一个文件目录下可以有不同的文件系统!
d_name:目录项名称
d_time:重新变为有效的时间!注意只要操作成功这个dentry就是有效的,否则无效。
d_op:目录项操作
d_sb:这个目录项所属的文件系统的超级块
d_vfs_flags:一些标志
d_fsdata:文件系统私有数据
d_iname:存放短的文件名
一些解释:一个有效的dentry结构必定有一个inode结构,这是因为一个目录项要么代表着一个文件,要么代表着一个目录,而目录实际上也是文件。所以,只要dentry结构是有效的,则其指针d_inode必定指向一个inode结构。但是inode却可以对应多个
dentry,上面已经说过两次了。
注意:整个结构其实就是一棵树。
=>文件对象:注意文件对象描述的是进程已经打开的文件。因为一个文件可以被多个进程打开,所以一个文件可以存在多个文件对象。但是由于文件是唯一的,那么inode就是唯一的,目录项也是定的!
进程其实是通过文件描述符来操作文件的,注意每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。一般情况下打开文件后,打开位置都是从0开始,除非一些特殊情况。Linux用file结构体来保存打开的文件的位置,所以file称为打开的文件描述。这个需要好好理解一下!file结构形成一个双链表,称为系统打开文件表。
- "font-size:14px;">565 struct file {
- 566 struct list_head f_list;
- 567 struct dentry *f_dentry;
- 568 struct vfsmount *f_vfsmnt;
- 569 struct file_operations *f_op;
- 570 atomic_t f_count;
- 571 unsigned int f_flags;
- 572 mode_t f_mode;
- 573 loff_t f_pos;
- 574 unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
- 575 struct fown_struct f_owner;
- 576 unsigned int f_uid, f_gid;
- 577 int f_error;
- 578
- 579 size_t f_maxcount;
- 580 unsigned long f_version;
- 581
- 582
- 583 void *private_data;
- 584
- 585
- 586 struct kiobuf *f_iobuf;
- 587 long f_iobuf_lock;
- 588 };
解释一些字段:
f_list:所有的打开的文件形成的链表!注意一个文件系统所有的打开的文件都通过这个链接到super_block中的s_files链表中!
f_dentry:与该文件相关的dentry
f_vfsmnt:该文件在这个文件系统中的安装点
f_op:文件操作,当进程打开文件的时候,这个文件的关联inode中的i_fop文件操作会初始化这个f_op字段
f_count:引用计数
f_flags:打开文件时候指定的标识
f_mode:文件的访问模式
f_pos:目前文件的相对开头的偏移
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin:预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数
f_owner:记录一个进程ID,以及当某些事发送的时候发送给该ID进程的信号
f_uid:用户ID
f_gid:组ID
f_error:写操作错误码
f_version:版本号,当f_pos改变时候,version递增
private_data:私有数据( 文件系统和驱动程序使用 )
重点解释一些重要字段:
首先,f_flags、f_mode和f_pos代表的是这个进程当前操作这个文件的控制信息。这个非常重要,因为对于一个文件,可以被多个进程同时打开,那么对于每个进程来说,操作这个文件是异步的,所以这个三个字段就很重要了。
第二:对于引用计数f_count,当我们关闭一个进程的某一个文件描述符时候,其实并不是真正的关闭文件,仅仅是将f_count减一,当f_count=0时候,才会真的去关闭它。对于dup,fork这些操作来说,都会使得f_count增加,具体的细节,以后再说。
第三:f_op也是很重要的!是涉及到所有的文件的操作结构体。例如:用户使用read,最终都会调用file_operations中的读操作,而file_operations结构体是对于不同的文件系统不一定相同。里面一个重要的操作函数式release函数,当用户执行close时候,其实在内核中是执行release函数,这个函数仅仅将f_count减一,这也就解释了上面说的,用户close一个文件其实是将f_count减一。只有引用计数减到0才关闭文件。
注意:对于“正在使用”和“未使用”的文件对象分别使用一个双向链表进行管理。
注意上面的file只是对一个文件而言,对于一个进程(用户)来说,可以同时处理多个文件,所以需要另一个结构来管理所有的files!
即:用户打开文件表--->files_struct
- "font-size:14px;">172 struct files_struct {
- 173 atomic_t count;
- 174 rwlock_t file_lock;
- 175 int max_fds;
- 176 int max_fdset;
- 177 int next_fd;
- 178 struct file ** fd;
- 179 fd_set *close_on_exec;
- 180 fd_set *open_fds;
- 181 fd_set close_on_exec_init;
- 182 fd_set open_fds_init;
- 183 struct file * fd_array[NR_OPEN_DEFAULT];
- 184 };
解释一些字段:
count:引用计数
file_lock:锁,保护下面的字段
max_fds:当前文件对象的最大的数量
max_fdset:文件描述符最大数
next_fd:已分配的最大的文件描述符+1
fd:指向文件对象指针数组的指针,一般就是指向最后一个字段fd_arrray,当文件数超过NR_OPEN_DEFAULT时候,就会重新分配一个数组,然后指向这个新的数组指针!
close_on_exec:执行exec()时候需要关闭的文件描述符
open_fds:指向打开的文件描述符的指针
close_on_exec_init:执行exec()时候需要关闭的文件描述符初始化值
open_fds_init:文件描述符初值集合
fd_array:文件对象指针的初始化数组
注意上面的file和files_struct记录的是与进程相关的文件的信息,但是对于进程本身来说,自身的一些信息用什么表示,这里就涉及到fs_struct结构体。
- "font-size:14px;"> 5 struct fs_struct {
- 6 atomic_t count;
- 7 rwlock_t lock;
- 8 int umask;
- 9 struct dentry * root, * pwd, * altroot;
- 10 struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
- 11 };
解释一些字段:
count:引用计数
lock:保护锁
umask:打开文件时候默认的文件访问权限
root:进程的根目录
pwd:进程当前的执行目录
altroot:用户设置的替换根目录
注意:实际运行时,这三个目录不一定都在同一个文件系统中。例如,进程的根目录通常是安装于“/”节点上的ext文件系统,而当前工作目录可能是安装于/etc的一个文件系统,替换根目录也可以不同文件系统中。
rootmnt,pwdmnt,altrootmnt:对应于上面三个的安装点。
基本的概念和基本的结构总结完了,后面会总结看看这些之间的关系。
(内核2.4.37)
一、首先,看看磁盘,超级块,inode节点在物理上整体的分布情况:
(图示来自:www.daoluan.net)
对于一个分区,对应一个文件系统,一个文件系统其实本质上还是磁盘的一部分,所以可以包括多个柱面。对于柱面上的数据,主要看看inode节点位图、block位图,i节点,数据块。inode节点位图是为了记录inode节点的使用情况,之前的违章中已经说过,inode节点在文件系统安装的时候,会初始化所有的inode节点,那么形成的位图表示使用or没使用的大表。对于block块也是一样的,记录数据块使用情况。
对于inode节点来说,每个文件都会对应一个inode节点,目录项也会对应一个inode节点。对于一个文件来说,只对应一个inode节点,但是一个文件可以有多个数据块,因为一个比较大的文件,一个数据块根本存放不了。所以inode中记录多个文件数据块的信息。
对于目录块来说,主要是为了索引而存在,所以里面的内容主要是inode节点号和文件名,其实就是一个映射表形式的东西。
二、
上一篇中对于VFS有一个简单的介绍与理解,我们知道,与用户打交道的是VFS,然后VFS与底层真正的文件系统交流。那我们知道在一个VFS下面,允许存在很多的“文件系统类型”,例如ext2,ext3,ext4,sysfs,proc等等。这些类型是可以共存的,同时,对于每一个类型来说,可以存在多个文件系统实体,例如:在一个目录下有多个子目录,子目录之间的文件系统类型可以不一样,也可以有部分是一样的类型,借用windows中的例子说就是,D盘和E盘可以都是NTFS类型文件系统,也可以是不一样的文件类型系统。
在Linux中,系统有一个全局变量叫做file_systems,这个变量用来管理所有的“文件系统类型”链表。也就是所有的文件系统类型都必须注册到(链接到)这个链表中,才可以被使用。如果是自己的文件系统,只要符合VFS的标准,也是可以注册进去的。最终形成一个单链表形式结构。
而对于一个文件系统类型,使用file_system_type结构表示:
- "font-size:14px;">995 struct file_system_type {
- 996 const char *name;
- 997 int fs_flags;
- 998 struct super_block *(*read_super) (struct super_block *, void *, int);
- 999 struct module *owner;
- 1000 struct file_system_type * next;
- 1001 struct list_head fs_supers;
- 1002 };
字段意思:
name:文件系统类型名称,如ext2。
flags:安装文件类型标志,在fs.h中有定义。
read_super:各种文件系统读入其“超级块”的函数指针,不同的文件系统之间可能不一样,因此读入函数也不一样。
owner:如果这个文件系统是通过一个可安装模块实现的,那么这个指针指向这个模块。
next:这个就是链接到下一个“文件类型”的指针。
fs_supers:属于相同的文件系统类型的所有的super_blocks构成一个双向链表。在超级块中有一个s_instance就是连接这个双链表的连接点。(超级块在上一篇有介绍)
1、
那么根据上面的解释,一个大的框架图如下:
同时,在内核中,有一个全局的变量super_blocks用于将所有的suoer_block连接在一起,形成一个双向链表,这样就会发现s_list字段就有意义了!
如图:
2、我们在之前也提过,文件系统最终还是要和进程一起协作的,任何对于文件的操作都是基于进程的。由操作系统的知识我们知道,对于进程来说,管理进程的叫“进程控制块PCB”,这个在内核中的结构为:task_struct,这个是很复杂的一个结构体,对于进程来说,可以有自己的操作的文件,那么进程的文件的信息,也是包含在这个结构中:
- 283 struct task_struct {
- ... ...
- 391
- 392 struct fs_struct *fs;
- 393
- 394 struct files_struct *files;
- ... ...
- }
代码中的两个字段就是涉及进程的文件的字段。每个进程在PCB中保存着一份文件描述符表,文件描述符就是这个表的索引(数组的下标),每个表项都有一个指向已打开文件的指针。
代码中第一个字段fs:代码本进程自身的文件系统的信息。例如进程本身的根目录,挂载点,当前目录等信息。
代码中第二个字段files:保存着本进程涉及的所有的文件的信息的指针。files_struct结构之前已经说过:files_struct
里面有两个重要字段:
- 172 struct files_struct {
- ... ...
- 178 struct file ** fd;
- ... ...
- 183 struct file * fd_array[NR_OPEN_DEFAULT];
- 184 };
fd就是涉及到的所有的文件的数组指针!一般情况下fd就是fd_array,但是如果打开的文件超过NR_OPEN_DEFAULT,那么就会重新分配新的数组,然后fd指向新的数组。
对于一个文件数组来说例如:fd[],所谓“文件描述符”其实就是这个数组的下标!例如:默认0就是标准输入文件描述符,1是标准输出,2是标准错误。对于用户来说操作的是这个“文件描述符”,但是对于内核来说,“文件描述符”仅仅是为了找到对应的文件而已!然后所有的在内核中的操作,都是使用实际文件的file指针进行的!关于file结构体在上一篇也说了(之前files_struct链接)。延伸一下:我们在写C语言程序的时候会遇到两个函数,open和fopen。对于前者,返回的就是一个“文件描述符”,即那个文件数组的下标,对于fopen,返回的是一个FILE的指针,这里面其实除了“文件描述符”之外,还包括IO缓冲这些信息。文件指针FILE*更上层,FILE指针将文件描述符和缓冲区封装在一起。
OK,那么用户进程打开一个文件的具体的过程是什么呢?下面分析总结一下:首先我们知道用户使用open返回一个“文件描述符”(具体怎么获得,之后再说),然后在进程PCB中,即task_struct文件数组中找到对应“文件描述符”(数组下标)的文件(file)指针,在file结构体中,f_dentry记录了这个文件的完整目录项,一般在内存中会有dentry的缓存,通过这个我们可以找到文件的inode。对于一个dentry来说,也是有自己的inode,目录名称之类信息。总之通过dentry,可以找到最终文件的inode,找到inode之后,就可以定位到具体的文件数据在磁盘上的位置了!
对于上面的过程,整体的一张图如下:
3、多个进程和多个文件之间的关系:
对于一个进程来说,可以打开多个文件,也可以多个进程打开一个文件,对于文件来说,不变的永远是自己的inode节点,变化的仅仅是和进程直接关系的file结构。可以看一下下面的大图:
对于dup和fork函数来说,前者是复制一个文件描述符,后者是复制进程,同时相关的文件信息也会被复制。
一、对于Dup
之前已经知道,对于一个进程来说,有一个files_struct来管理所有的相关文件,最终的反应形式其实就是一个文件数组而已:
所谓文件描述符就是数组下标。Dup函数总是从数组第一个元素开始扫描,获取第一个可用的文件描述符(也就是没有关联实际文件的fd),这就是所谓:dup总是使用最小的文件描述符。理解了原理就简单了。
一个Dup操作之后,变成什么情况呢?看下面的图:
例如:dup(fd[y]),从开头找到第一个可以的文件描述符(所谓最小描述符),现在可以发现fd[x]和fd[y]同时指向file!这个千万注意,两个文件描述符指向同一个文件和指向同一个file不一样,指向同一个文件可以是不同的file,但是inode永远唯一,但是此处,fd指向同一个file,那么任意一个操作,另一个一定是同步的!
常见例子:一般来说,初始化的时候,进程都拥有默认的三个文件描述符默认代表,标准输入,标准输出,标准错误。但是这不是硬性规定,你可以自己改呀!例如下面的代码:
close(0);
dup(fd[x]);/* 这是一个普通文件的文件描述符 */
这之后,你会发现,0号文件描述符关联上了这个文件(0是最小的文件描述符,所以肯定会被dup选中!)。
2、对于fork函数:
父进程fork之后,子进程和父进程共享父进程打开的文件,那么使用图示表现为:
父子进程有相同的文件fd,并且对应的fd指向相同的file。
常见例子:父子进程使用管道通信时候。在父进程中创建一个pair_pipe,也就是创建一个可以通信的两个文件,一个口用于写,一个用于读。那么fork之后,子进程中复制上面信息,也拥有和父进程相同的pair_pipe,其实指向的就是同一个文件,如下图:
现在关闭父进程的pair_pipe[0],关闭子进程的pair_pipe[1],那么父子进程分别使用pair_pipe[0]和pair_pipe[1]进行通信!
具体的通信图示如下:
在文件系统中,有三大缓冲为了提升效率:inode缓冲区、dentry缓冲区、块缓冲。
(内核:2.4.37)
一、inode缓冲区
为了加快对索引节点的索引,引入inode缓冲区,下面我们看Linux/fs/inode.c代码。inode缓冲区代码
1、一些数据结构:
之前已经说过,有多个链表用于管理inode节点:
- "font-size:14px;">59 static LIST_HEAD(inode_in_use);
- 60 static LIST_HEAD(inode_unused);
- 61 static LIST_HEAD(inode_unused_pagecache);
- 62 static struct list_head *inode_hashtable;
- 63 static LIST_HEAD(anon_hash_chain);
inode_in_use:正在使用的inode,即有效的inode,i_count > 0且i_nlink > 0。
inode_unused:有效的节点,但是还没有使用,处于空闲状态。(数据不在pagecache中)。
inode_unused_pagecache:同上。(数据在pagecache中)。
inode_hashtable:用于inode在hash表中,提高查找效率。
anon_hash_chain:用于超级块是空的的inodes。例如:sock_alloc()函数, 通过调用fs/inode.c中get_empty_inode()创建的套接字是一个匿名索引节点,这个节点就加入到了anon_hash_chain链表。
dirty:用于保存超级块中的所有的已经修改的inodes。
- "font-size:14px;"> 76 struct inodes_stat_t inodes_stat;
- 77
- 78 static kmem_cache_t * inode_cachep;
上面的两个字段:
inodes_stat:记录inodes节点的状态。
inode_cachep:对inodes对象的缓存块。
2、基本初始化:初始化inode哈希表头和slab内存缓存块
索引节点高速缓存的初始化是由inode_init()实现的,现在看看下面代码:
- "font-size:14px;">1296
-
-
- 1299 void __init inode_init(unsigned long mempages)
- 1300 {
- 1301 struct list_head *head;
- 1302 unsigned long order;
- 1303 unsigned int nr_hash;
- 1304 int i;
- 1305
- 1306 mempages >>= (14 - PAGE_SHIFT);
- 1307 mempages *= sizeof(struct list_head);
- 1308 for (order = 0; ((1UL << order) << PAGE_SHIFT) < mempages; order++)
- 1309 ;
- 1310
- 1311 do {
- 1312 unsigned long tmp;
- 1313
- 1314 nr_hash = (1UL << order) * PAGE_SIZE /
- 1315 sizeof(struct list_head);
- 1316 i_hash_mask = (nr_hash - 1);
- 1317
- 1318 tmp = nr_hash;
- 1319 i_hash_shift = 0;
- 1320 while ((tmp >>= 1UL) != 0UL)
- 1321 i_hash_shift++;
- 1322
- 1323 inode_hashtable = (struct list_head *)
- 1324 __get_free_pages(GFP_ATOMIC, order);
- 1325 } while (inode_hashtable == NULL && --order >= 0);
- 1326
- 1327 printk(KERN_INFO "Inode cache hash table entries: %d (order: %ld, %ld bytes)\n",
- 1328 nr_hash, order, (PAGE_SIZE << order));
- 1329
- 1330 if (!inode_hashtable)
- 1331 panic("Failed to allocate inode hash table\n");
- 1332
- 1333 head = inode_hashtable;
- 1334 i = nr_hash;
- 1335 do {
- 1336 INIT_LIST_HEAD(head);
- 1337 head++;
- 1338 i--;
- 1339 } while (i);
- 1340
- 1341
- 1342 inode_cachep = kmem_cache_create("inode_cache", sizeof(struct inode),
- 1343 0, SLAB_HWCACHE_ALIGN, init_once,
- 1344 NULL);
- 1345 if (!inode_cachep)
- 1346 panic("cannot create inode slab cache");
- 1347
- 1348 unused_inodes_flush_task.routine = try_to_sync_unused_inodes;
- 1349 }
- 1350
注意上面的逻辑,说明两个问题:
1). 第一初始化inode_hashtable作为链表的头。
2). 初始化inode的slab缓存,也就是说,如果我需要分配一个inode缓存在内存中,那么都从这个inode_cachep中分配一个inode内存节点。然后统一加入到这个inode_hashtable中进行管理!也就是所谓的创建inode slab分配器缓存。
下面看看具体的缓存的分配过程:
先看init_once函数:
- "font-size:14px;">169 static void init_once(void * foo, kmem_cache_t * cachep, unsigned long flags)
- 170 {
- 171 struct inode * inode = (struct inode *) foo;
- 172
- 173 if ((flags & (SLAB_CTOR_VERIFY|SLAB_CTOR_CONSTRUCTOR)) ==
- 174 SLAB_CTOR_CONSTRUCTOR)
- 175 inode_init_once(inode);
- 176 }
注意:在上面的kmem_cache_create函数中,执行的顺序是:
---> kmem_cache_create(里面重要的一步是cachep->ctor = ctor; cachep->dtor = dtor;)
---> kmem_cache_alloc
---> __kmem_cache_alloc
---> kmem_cache_grow(里面一个重要设置是:ctor_flags = SLAB_CTOR_CONSTRUCTOR;)
---> kmem_cache_init_objs:里面会执行cachep->ctor(objp, cachep, ctor_flags);
这样最终就跳转到上面的init_once函数中了!在init函数中执行的是inode_init_once函数:
- "font-size:14px;">141
-
-
-
-
- 146 void inode_init_once(struct inode *inode)
- 147 {
- 148 memset(inode, 0, sizeof(*inode));
- 149 __inode_init_once(inode);
- 150 }
再看__inode_init_once函数:
- "font-size:14px;">152 void __inode_init_once(struct inode *inode)
- 153 {
- 154 init_waitqueue_head(&inode->i_wait);
- 155 INIT_LIST_HEAD(&inode->i_hash);
- 156 INIT_LIST_HEAD(&inode->i_data.clean_pages);
- 157 INIT_LIST_HEAD(&inode->i_data.dirty_pages);
- 158 INIT_LIST_HEAD(&inode->i_data.locked_pages);
- 159 INIT_LIST_HEAD(&inode->i_dentry);
- 160 INIT_LIST_HEAD(&inode->i_dirty_buffers);
- 161 INIT_LIST_HEAD(&inode->i_dirty_data_buffers);
- 162 INIT_LIST_HEAD(&inode->i_devices);
- 163 sema_init(&inode->i_sem, 1);
- 164 sema_init(&inode->i_zombie, 1);
- 165 init_rwsem(&inode->i_alloc_sem);
- 166 spin_lock_init(&inode->i_data.i_shared_lock);
- 167 }
3、注意知道现在我们主要说了上面的两个基本的问题(红字部分),但是这只是一个框架而已,对于具体的一个文件系统来说怎么个流程,下面需要看看!
我们以最常见的ext2作为说明:
现在一个ext2类型的文件系统想要创建一个inode,那么执行:ext2_new_inode函数
- "font-size:14px;">314 struct inode * ext2_new_inode (const struct inode * dir, int mode)
- 315 {
- 316 struct super_block * sb;
- 317 struct buffer_head * bh;
- 318 struct buffer_head * bh2;
- 319 int group, i;
- 320 ino_t ino;
- 321 struct inode * inode;
- 322 struct ext2_group_desc * desc;
- 323 struct ext2_super_block * es;
- 324 int err;
- 325
- 326 sb = dir->i_sb;
- 327 inode = new_inode(sb);
- 328 if (!inode)
- 329 return ERR_PTR(-ENOMEM);
- 330
- 331 lock_super (sb);
- 332 es = sb->u.ext2_sb.s_es;
- 333 repeat:
- 334 if (S_ISDIR(mode))
- 335 group = find_group_dir(sb, dir->u.ext2_i.i_block_group);
- 336 else
- 337 group = find_group_other(sb, dir->u.ext2_i.i_block_group);
- 338
- 339 err = -ENOSPC;
- 340 if (group == -1)
- 341 goto fail;
- 342
- 343 err = -EIO;
- 344 bh = load_inode_bitmap (sb, group);
- 345 if (IS_ERR(bh))
- 346 goto fail2;
- 347
- 348 i = ext2_find_first_zero_bit ((unsigned long *) bh->b_data,
- 349 EXT2_INODES_PER_GROUP(sb));
- 350 if (i >= EXT2_INODES_PER_GROUP(sb))
- 351 goto bad_count;
- 352 ext2_set_bit (i, bh->b_data);
- 353
- 354 mark_buffer_dirty(bh);
- 355 if (sb->s_flags & MS_SYNCHRONOUS) {
- 356 ll_rw_block (WRITE, 1, &bh);
- 357 wait_on_buffer (bh);
- 358 }
- 359
- 360 ino = group * EXT2_INODES_PER_GROUP(sb) + i + 1;
- 361 if (ino < EXT2_FIRST_INO(sb) || ino > le32_to_cpu(es->s_inodes_count)) {
- 362 ext2_error (sb, "ext2_new_inode",
- 363 "reserved inode or inode > inodes count - "
- 364 "block_group = %d,inode=%ld", group, ino);
- 365 err = -EIO;
- 366 goto fail2;
- 367 }
- 368
- 369 es->s_free_inodes_count =
- 370 cpu_to_le32(le32_to_cpu(es->s_free_inodes_count) - 1);
- 371 mark_buffer_dirty(sb->u.ext2_sb.s_sbh);
- 372 sb->s_dirt = 1;
- 373 inode->i_uid = current->fsuid;
- 374 if (test_opt (sb, GRPID))
- 375 inode->i_gid = dir->i_gid;
- 376 else if (dir->i_mode & S_ISGID) {
- 377 inode->i_gid = dir->i_gid;
- 378 if (S_ISDIR(mode))
- 379 mode |= S_ISGID;
- 380 } else
- 381 inode->i_gid = current->fsgid;
- 382 inode->i_mode = mode;
- 383
- 384 inode->i_ino = ino;
- 385 inode->i_blksize = PAGE_SIZE;
- 386 inode->i_blocks = 0;
- 387 inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
- 388 inode->u.ext2_i.i_state = EXT2_STATE_NEW;
- 389 inode->u.ext2_i.i_flags = dir->u.ext2_i.i_flags & ~EXT2_BTREE_FL;
- 390 if (S_ISLNK(mode))
- 391 inode->u.ext2_i.i_flags &= ~(EXT2_IMMUTABLE_FL|EXT2_APPEND_FL);
- 392 inode->u.ext2_i.i_block_group = group;
- 393 ext2_set_inode_flags(inode);
- 394 insert_inode_hash(inode);
- 395 inode->i_generation = event++;
- 396 mark_inode_dirty(inode);
- 397
- 398 unlock_super (sb);
- 399 if(DQUOT_ALLOC_INODE(inode)) {
- 400 DQUOT_DROP(inode);
- 401 inode->i_flags |= S_NOQUOTA;
- 402 inode->i_nlink = 0;
- 403 iput(inode);
- 404 return ERR_PTR(-EDQUOT);
- 405 }
- 406 ext2_debug ("allocating inode %lu\n", inode->i_ino);
- 407 return inode;
- 408
- 409 fail2:
- 410 desc = ext2_get_group_desc (sb, group, &bh2);
- 411 desc->bg_free_inodes_count =
- 412 cpu_to_le16(le16_to_cpu(desc->bg_free_inodes_count) + 1);
- 413 if (S_ISDIR(mode))
- 414 desc->bg_used_dirs_count =
- 415 cpu_to_le16(le16_to_cpu(desc->bg_used_dirs_count) - 1);
- 416 mark_buffer_dirty(bh2);
- 417 fail:
- 418 unlock_super(sb);
- 419 make_bad_inode(inode);
- 420 iput(inode);
- 421 return ERR_PTR(err);
- 422
- 423 bad_count:
- 424 ext2_error (sb, "ext2_new_inode",
- 425 "Free inodes count corrupted in group %d",
- 426 group);
- 427
- 428 err = -ENOSPC;
- 429 if (sb->s_flags & MS_RDONLY)
- 430 goto fail;
- 431
- 432 desc = ext2_get_group_desc (sb, group, &bh2);
- 433 desc->bg_free_inodes_count = 0;
- 434 mark_buffer_dirty(bh2);
- 435 goto repeat;
- 436 }
这个函数比较复杂,但是我们主要看327行和394行,就是创建一个inode内存节点,然后将这个inode插入inode_hashtable中!
这个函数具体的解释不再看了,现在主要从这两个函数入手:
1). fs/inode.c中的new_inode函数,创建一个inode内存节点:
- "font-size:14px;">964 struct inode * new_inode(struct super_block *sb)
- 965 {
- 966 static unsigned long last_ino;
- 967 struct inode * inode;
- 968
- 969 spin_lock_prefetch(&inode_lock);
- 970
- 971 inode = alloc_inode(sb);
- 972 if (inode) {
- 973 spin_lock(&inode_lock);
- 974 inodes_stat.nr_inodes++;
- 975 list_add(&inode->i_list, &inode_in_use);
- 976 inode->i_ino = ++last_ino;
- 977 inode->i_state = 0;
- 978 spin_unlock(&inode_lock);
- 979 }
- 980 return inode;
- 981 }
看看这个alloc_inode函数:
- "font-size:14px;"> 80 static struct inode *alloc_inode(struct super_block *sb)
- 81 {
- 82 static struct address_space_operations empty_aops;
- 83 static struct inode_operations empty_iops;
- 84 static struct file_operations empty_fops;
- 85 struct inode *inode;
- 86
- 87 if (sb->s_op->alloc_inode)
- 88 inode = sb->s_op->alloc_inode(sb);
- 89 else {
- 90 inode = (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL);
- 91
- 92 if (inode)
- 93 memset(&inode->u, 0, sizeof(inode->u));
- 94 }
- 95
- 96 if (inode) {
- 97 struct address_space * const mapping = &inode->i_data;
- 98
- 99 inode->i_sb = sb;
- 100 inode->i_dev = sb->s_dev;
- 101 inode->i_blkbits = sb->s_blocksize_bits;
- 102 inode->i_flags = 0;
- 103 atomic_set(&inode->i_count, 1);
- 104 inode->i_sock = 0;
- 105 inode->i_op = &empty_iops;
- 106 inode->i_fop = &empty_fops;
- 107 inode->i_nlink = 1;
- 108 atomic_set(&inode->i_writecount, 0);
- 109 inode->i_size = 0;
- 110 inode->i_blocks = 0;
- 111 inode->i_bytes = 0;
- 112 inode->i_generation = 0;
- 113 memset(&inode->i_dquot, 0, sizeof(inode->i_dquot));
- 114 inode->i_pipe = NULL;
- 115 inode->i_bdev = NULL;
- 116 inode->i_cdev = NULL;
- 117
- 118 mapping->a_ops = &empty_aops;
- 119 mapping->host = inode;
- 120 mapping->gfp_mask = GFP_HIGHUSER;
- 121 inode->i_mapping = mapping;
- 122 }
- 123 return inode;
- 124 }
我们主要看87行和90行!看了注释也就明白了!第一种是文件系统也就是这个超级快提供了分配函数,那么就这个文件系统按照自己的意愿去分配,如果没有,那么就是要用这个通用的分配函数inode = (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL);这个函数其实很简单,其实就是在我们已经初始化好的这个inode_cache中分配一个inode内存块出来。
2). fs/inode.c中的insert_inode_hash函数,将新的分配的inode插入到inode_hashtable中:
- "font-size:14px;">1166 void insert_inode_hash(struct inode *inode)
- 1167 {
- 1168 struct list_head *head = &anon_hash_chain;
- 1169 if (inode->i_sb)
- 1170 head = inode_hashtable + hash(inode->i_sb, inode->i_ino);
- 1171 spin_lock(&inode_lock);
- 1172 list_add(&inode->i_hash, head);
- 1173 spin_unlock(&inode_lock);
- 1174 }
注意这个hash表其实就可以看做是一个数组链表组合体,如图所示:
head = inode_hashtable + hash(inode->i_sb, inode->i_ino);这一行就是通过这个hash函数算出hash值,找到这个inode应该放在哪一列。譬如定位到第三列,那么第三列中的都是hash值相同的inode。然后所有的这列inode都是构成双向链表的。注意inode中的i_hash字段就做这个事的!!list_add(&inode->i_hash, head);函数就是将hash值相同的inode构成双向链表。
看一下这个具体的hash函数(inode.c中):
- "font-size:14px;">1043 static inline unsigned long hash(struct super_block *sb, unsigned long i_ino)
- 1044 {
- 1045 unsigned long tmp = i_ino + ((unsigned long) sb / L1_CACHE_BYTES);
- 1046 tmp = tmp + (tmp >> I_HASHBITS);
- 1047 return tmp & I_HASHMASK;
- 1048 }
OK,上面的具体的inode创建和加入的流程基本清楚了。具体创建的过程是涉及到内存这一块的,不多说了。
4. 下面看看给一个怎么去找到一个inode,涉及ilookup函数:
- "font-size:14px;">1102 struct inode *ilookup(struct super_block *sb, unsigned long ino)
- 1103 {
- 1104 struct list_head * head = inode_hashtable + hash(sb,ino);
- 1105 struct inode * inode;
- 1106
- 1107 spin_lock(&inode_lock);
- 1108 inode = find_inode(sb, ino, head, NULL, NULL);
- 1109 if (inode) {
- 1110 __iget(inode);
- 1111 spin_unlock(&inode_lock);
- 1112 wait_on_inode(inode);
- 1113 return inode;
- 1114 }
- 1115 spin_unlock(&inode_lock);
- 1116
- 1117 return inode;
- 1118 }
这个函数其实比较简单了,首先还是获得这个inode的hash值定位,然后开始finde_inode:
- "font-size:14px;">929 static struct inode * find_inode(struct super_block * sb, unsigned long ino, struct list_head *head, find_inode_t find_actor, void *opaque)
- 930 {
- 931 struct list_head *tmp;
- 932 struct inode * inode;
- 933
- 934 repeat:
- 935 tmp = head;
- 936 for (;;) {
- 937 tmp = tmp->next;
- 938 inode = NULL;
- 939 if (tmp == head)
- 940 break;
- 941 inode = list_entry(tmp, struct inode, i_hash);
- 942 if (inode->i_ino != ino)
- 943 continue;
- 944 if (inode->i_sb != sb)
- 945 continue;
- 946 if (find_actor && !find_actor(inode, ino, opaque))
- 947 continue;
- 948 if (inode->i_state & (I_FREEING|I_CLEAR)) {
- 949 __wait_on_freeing_inode(inode);
- 950 goto repeat;
- 951 }
- 952 break;
- 953 }
- 954 return inode;
- 955 }
上面函数最核心的本质不就是双向链表的查找么,OK。
最后:关于inode怎么工作的,将会在后面的分析ext2代码中在详细研究。
在文件系统中,有三大缓冲为了提升效率:inode缓冲区、dentry缓冲区、块缓冲。
(内核:2.4.37)
二、块buffer缓冲区
0、整体来说,Linux 文件缓冲区分为page cache和buffer cache,每一个 page cache 包含若干 buffer cache。
》 内存管理系统和 VFS 只与 page cache 交互,内存管理系统负责维护每项 page cache 的分配和回收,同时在使用“内存映射”方式访问时负责建立映射。
》 VFS 负责 page cache 与用户空间的数据交换。
》 而具体文件系统则一般只与 buffer cache 交互,它们负责在存储设备和 buffer cache 之间交换数据,具体的文件系统直接操作的就是disk部分,而具体的怎么被包装被用户使用是VFS的责任(VFS将buffer cache包装成page给用户)。
》 每一个page有N个buffer cache,struct buffer_head结构体中一个字段b_this_page就是将一个page中的buffer cache连接起来的结构
看一下这个结构:/include/linux/mm.h,对于struct buffer_head下面再看。
看到下面167行代码就懂了~
- typedef struct page {
- 156 struct list_head list;
- 157 struct address_space *mapping;
- 158 unsigned long index;
- 159 struct page *next_hash;
-
- 161 atomic_t count;
- 162 unsigned long flags;
-
- 164 struct list_head lru;
-
- 166 struct page **pprev_hash;
- 167 struct buffer_head * buffers;
- 168
- 169
-
-
-
-
-
-
-
-
-
- 179 #if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)
- 180 void *virtual;
-
- 182 #endif
- 183 } mem_map_t;
关系图如下:
1、对于具体的Linux文件系统,会以block(磁盘块)的形式组织文件,为了减少对物理块设备的访问,在文件以块的形式调入内存后,使用块高速缓存进行管理。每个缓冲区由两部分组成,第一部分称为缓冲区首部,用数据结构buffer_head表示,第二部分是真正的存储的数据。由于缓冲区首部不与数据区域相连,数据区域独立存储。因而在缓冲区首部中,有一个指向数据的指针和一个缓冲区长度的字段。
Ps:内核同样有几种不同的链表来管理buffer cache,在fs/buffer.c中定义:
static struct buffer_head **hash_table;
static struct buffer_head *lru_list[NR_LIST];
static struct buffer_head * unused_list;
下面我们具体看看这个结构体struct buffer_head,这个版本中这个结构体在fs.h中,后面的一些版本,在buffer_head.h中。
- 246 struct buffer_head {
- 247
- 248 struct buffer_head *b_next;
- 249 unsigned long b_blocknr;
- 250 unsigned short b_size;
- 251 unsigned short b_list;
- 252 kdev_t b_dev;
- 253
- 254 atomic_t b_count;
- 255 kdev_t b_rdev;
- 256 unsigned long b_state;
- 257 unsigned long b_flushtime;
- 258
- 259 struct buffer_head *b_next_free;
- 260 struct buffer_head *b_prev_free;
- 261 struct buffer_head *b_this_page;
- 262 struct buffer_head *b_reqnext;
- 263
- 264 struct buffer_head **b_pprev;
- 265 char * b_data;
- 266 struct page *b_page;
- 267 void (*b_end_io)(struct buffer_head *bh, int uptodate);
- 268 void *b_private;
- 269
- 270 unsigned long b_rsector;
- 271 wait_queue_head_t b_wait;
- 272
- 273 struct list_head b_inode_buffers;
- 274 };
解释一些上面的字段:
b_next:用于链接到块缓冲区的hash表
b_blocknr:本block的块号
b_size:block的大小
b_list:表示当前的这个buffer在那个链表中
b_dev:虚拟设备标识
b_count:引用计数(几个人在使用这个buffer)
b_rdev:真实设备标识
b_state:状态位图,如下:
- 212
- 213 enum bh_state_bits {
- 214 BH_Uptodate,
- 215 BH_Dirty,
- 216 BH_Lock,
- 217 BH_Req,
- 218 BH_Mapped,
- 219 BH_New,
- 220 BH_Async,
- 221 BH_Wait_IO,
- 222 BH_Launder,
- 223 BH_Attached,
- 224 BH_JBD,
- 225 BH_Sync,
- 226 BH_Delay,
- 227
- 228 BH_PrivateStart,
-
-
- 231 };
- 232
b_flushtime:脏buffer需要被写入的时间
b_next_free:指向lru链表中next元素
b_prev_free:指向链表上一个元素
b_this_page:连接到同一个page中的那个链表
b_reqnext:请求队列
b_pprev:hash队列双向链表
data:指向数据块的指针
b_page:这个buffer映射的页面
b_end_io:IO结束时候执行函数
b_private:保留
b_rsector:缓冲区在磁盘上的实际位置
b_inode_buffers:inode脏缓冲区循环链表
3、关于VFS怎么去管理几个buffer cache的链表,如下:
》 hash表:用于管理包含有效数据的buffer,在定位buffer的时候很快捷。哈希索引值由数据块号以及其所在的设备标识号计算(散列)得到。
关于这段hash代码如下:
- "font-size:14px;">539
-
-
- 542 #define _hashfn(dev,block) \
- 543 ((((dev)<<(bh_hash_shift - 6)) ^ ((dev)<<(bh_hash_shift - 9))) ^ \
- 544 (((block)<<(bh_hash_shift - 6)) ^ ((block) >> 13) ^ \
- 545 ((block) << (bh_hash_shift - 12))))
下面简单的看一下流程:
当我们在一个具有的文件系统中,当我们需要读取一块数据的时候,需要调用bread函数(面包?ヾ(。`Д´。),应该是buffer read的缩写吧。。。)。
如下:
- 1181
-
-
-
-
-
-
-
- 1189 struct buffer_head * bread(kdev_t dev, int block, int size)
- 1190 {
- 1191 struct buffer_head * bh;
- 1192
- 1193 bh = getblk(dev, block, size);
- 1194 if (buffer_uptodate(bh))
- 1195 return bh;
- 1196 set_bit(BH_Sync, &bh->b_state);
- 1197 ll_rw_block(READ, 1, &bh);
- 1198 wait_on_buffer(bh);
- 1199 if (buffer_uptodate(bh))
- 1200 return bh;
- 1201 brelse(bh);
- 1202 return NULL;
- 1203 }
对于上面函数的分析,基本上分成两个步骤,
第一:通过dev号+block号找到相应的buffer,使用函数getblk,如下:
- 1013 struct buffer_head * getblk(kdev_t dev, int block, int size)
- 1014 {
- 1015 for (;;) {
- 1016 struct buffer_head * bh;
- 1017
- 1018 bh = get_hash_table(dev, block, size);
- 1019 if (bh) {
- 1020 touch_buffer(bh);
- 1021 return bh;
- 1022 }
- 1023
- 1024 if (!grow_buffers(dev, block, size))
- 1025 free_more_memory();
- 1026 }
- 1027 }
简单看一下这个查找buffer函数:get_hash_table
- 628 struct buffer_head * get_hash_table(kdev_t dev, int block, int size)
- 629 {
- 630 struct buffer_head *bh, **p = &hash(dev, block);
- 631
- 632 read_lock(&hash_table_lock);
- 633
- 634 for (;;) {
- 635 bh = *p;
- 636 if (!bh)
- 637 break;
- 638 p = &bh->b_next;
- 639 if (bh->b_blocknr != block)
- 640 continue;
- 641 if (bh->b_size != size)
- 642 continue;
- 643 if (bh->b_dev != dev)
- 644 continue;
- 645 get_bh(bh);
- 646 break;
- 647 }
- 648
- 649 read_unlock(&hash_table_lock);
- 650 return bh;
- 651 }
如果没找到对应的buffer,那么使用grow_buffers函数增加一个新的buffer,看函数:
- 2596
-
-
-
- 2600 static int grow_buffers(kdev_t dev, unsigned long block, int size)
- 2601 {
- 2602 struct page * page;
- 2603 struct block_device *bdev;
- 2604 unsigned long index;
- 2605 int sizebits;
- 2606
- 2607
- 2608 if (size & (get_hardsect_size(dev)-1))
- 2609 BUG();
- 2610
- 2611 if (size < 512 || size > PAGE_SIZE)
- 2612 BUG();
- 2613
- 2614 sizebits = -1;
- 2615 do {
- 2616 sizebits++;
- 2617 } while ((size << sizebits) < PAGE_SIZE);
- 2618
- 2619 index = block >> sizebits;
- 2620 block = index << sizebits;
- 2621
- 2622 bdev = bdget(kdev_t_to_nr(dev));
- 2623 if (!bdev) {
- 2624 printk("No block device for %s\n", kdevname(dev));
- 2625 BUG();
- 2626 }
- 2627
- 2628
- 2629 page = grow_dev_page(bdev, index, size);
- 2630
- 2631
- 2632 atomic_dec(&bdev->bd_count);
- 2633 if (!page)
- 2634 return 0;
- 2635
- 2636
- 2637 hash_page_buffers(page, dev, block, size);
- 2638 UnlockPage(page);
- 2639 page_cache_release(page);
- 2640
- 2641
- 2642 atomic_inc(&buffermem_pages);
- 2643 return 1;
- 2644 }
- 2645
这个函数就是增加一个新的buffer,首先由grow_dev_page创建一个缓冲区包含这个block,然后将这个buffer链接到这个全局的hash缓冲区中使用函数hash_page_buffers。具体的代码很简单,不单看了。
第二:如果没有找到需要的buffer,那么执行底层读取函数ll_rw_block将数据从磁盘去读进来,这个函数在/source/drivers/block/ll_rw_blk.c中,具体的代码不看了。
OK,至此,寻找一个我们需要的buffer就结束了。
》 LRU链表
对于每一种不同缓冲区都会使用一个LRU来管理未使用的有效缓冲区
Ps:缓冲区类型如下:
- 1152 #define BUF_CLEAN 0
- 1153 #define BUF_LOCKED 1
- 1154 #define BUF_DIRTY 2
分别是:未使用的干净的缓冲区;正在等待写入的缓冲区;脏缓冲区,还没有被写回磁盘。
这个三种链表怎么得到的呢,看代码也知道是吻合的,看LRU的声明:static struct buffer_head *lru_list[NR_LIST];
再看:#define NR_LIST 3,OK
当我们需要寻找一块buffer的时候,如果发现buffer在缓冲区中,且在LRU链表中,那么从LRU表中删除。
结合上面的一个hash链表,基本过程就是:
首先呢在hash表中寻找,如果找到,那就OK,如果没有找到,那么需要分配新的buffer,如果分到,那么加载数据进来,继续...如果没有足够的空间分配,那么, 需要将LRU中一个取出(LRU链首元素),先看是否置了“脏”位,如已置,则将它的内容写回磁盘。然后清空内容,将它分配给新的数据块。
在缓冲区使用完了后,将它的b_count域减1,如果b_count变为0,则将它放在某个LRU链尾,表示该缓冲区已可以重新利用。
unused_list 用于辅助就不多说了~~~
在文件系统中,有三大缓冲为了提升效率:inode缓冲区、dentry缓冲区、块缓冲。
(内核:2.4.37)
为什么这个缓冲区会存在,不好意思,我说了废话,当然和前面一样的,为了提升效率,例如我们写一个.c的helloworld文件,简单的过程是编辑,编译,执行。。。那么这个过程都是需要找到所在的文件位置的,如果每次都从根开始找并且还有构造相应的目录项对象,是很费时的,所以将目录项一般也都是缓存起来的~~~
Ps:dentry结构
- 67 struct dentry {
- 68 atomic_t d_count;
- 69 unsigned int d_flags;
- 70 struct inode * d_inode;
- 71 struct dentry * d_parent;
- 72 struct list_head d_hash;
- 73 struct list_head d_lru;
- 74 struct list_head d_child;
- 75 struct list_head d_subdirs;
- 76 struct list_head d_alias;
- 77 int d_mounted;
- 78 struct qstr d_name;
- 79 unsigned long d_time;
- 80 struct dentry_operations *d_op;
- 81 struct super_block * d_sb;
- 82 unsigned long d_vfs_flags;
- 83 void * d_fsdata;
- 84 unsigned char d_iname[DNAME_INLINE_LEN];
- 85 };
和前面的一样,这个也涉及到几个相应的链表来管理,那么看看/fs/dcache.c中哪些链表被定义了。
- 52 static struct list_head *dentry_hashtable;
- 53 static LIST_HEAD(dentry_unused);
哈希链表 :从中能够快速获取与给定的文件名和目录名对应的目录项对象。
“未使用”链表:所有未使用 目录项对象都存放在一个LRU的双向链表。LRU链表的首元素和尾元素的地址存放在变量dentry_unused中的next 域和prev域中。目录项对象的d_lru域包含的指针指向该链表中相邻目录的对象。
简单的看一下dcache初始化过程:
- 1181 static void __init dcache_init(unsigned long mempages)
- 1182 {
- 1183 struct list_head *d;
- 1184 unsigned long order;
- 1185 unsigned int nr_hash;
- 1186 int i;
- 1187
- 1188
-
-
-
-
-
-
-
- 1196 dentry_cache = kmem_cache_create("dentry_cache",
- 1197 sizeof(struct dentry),
- 1198 0,
- 1199 SLAB_HWCACHE_ALIGN,
- 1200 NULL, NULL);
- 1201 if (!dentry_cache)
- 1202 panic("Cannot create dentry cache");
- 1203
- 1204 #if PAGE_SHIFT < 13
- 1205 mempages >>= (13 - PAGE_SHIFT);
- 1206 #endif
- 1207 mempages *= sizeof(struct list_head);
- 1208 for (order = 0; ((1UL << order) << PAGE_SHIFT) < mempages; order++)
- 1209 ;
- 1210
- 1211 do {
- 1212 unsigned long tmp;
- 1213
- 1214 nr_hash = (1UL << order) * PAGE_SIZE /
- 1215 sizeof(struct list_head);
- 1216 d_hash_mask = (nr_hash - 1);
- 1217
- 1218 tmp = nr_hash;
- 1219 d_hash_shift = 0;
- 1220 while ((tmp >>= 1UL) != 0UL)
- 1221 d_hash_shift++;
- 1222
- 1223 dentry_hashtable = (struct list_head *)
- 1224 __get_free_pages(GFP_ATOMIC, order);
- 1225 } while (dentry_hashtable == NULL && --order >= 0);
- 1226
- 1227 printk(KERN_INFO "Dentry cache hash table entries: %d (order: %ld, %ld bytes)\n",
- 1228 nr_hash, order, (PAGE_SIZE << order));
- 1229
- 1230 if (!dentry_hashtable)
- 1231 panic("Failed to allocate dcache hash table\n");
- 1232
- 1233 d = dentry_hashtable;
- 1234 i = nr_hash;
- 1235 do {
- 1236 INIT_LIST_HEAD(d);
- 1237 d++;
- 1238 i--;
- 1239 } while (i);
- 1240 }
上面代码就是相当于分配cache空间,并将hash表什么的都初始化了~~~
下面看一下怎么分配一个目录项对象,涉及函数d_alloc:
- 580
-
-
-
-
-
-
-
-
- 589
- 590 struct dentry * d_alloc(struct dentry * parent, const struct qstr *name)
- 591 {
- 592 char * str;
- 593 struct dentry *dentry;
- 594
- 595 dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
- 596 if (!dentry)
- 597 return NULL;
- 598
- 599 if (name->len > DNAME_INLINE_LEN-1) {
- 600 str = kmalloc(NAME_ALLOC_LEN(name->len), GFP_KERNEL);
- 601 if (!str) {
- 602 kmem_cache_free(dentry_cache, dentry);
- 603 return NULL;
- 604 }
- 605 } else
- 606 str = dentry->d_iname;
- 607
- 608 memcpy(str, name->name, name->len);
- 609 str[name->len] = 0;
- 610
- 611 atomic_set(&dentry->d_count, 1);
- 612 dentry->d_vfs_flags = 0;
- 613 dentry->d_flags = 0;
- 614 dentry->d_inode = NULL;
- 615 dentry->d_parent = NULL;
- 616 dentry->d_sb = NULL;
- 617 dentry->d_name.name = str;
- 618 dentry->d_name.len = name->len;
- 619 dentry->d_name.hash = name->hash;
- 620 dentry->d_op = NULL;
- 621 dentry->d_fsdata = NULL;
- 622 dentry->d_mounted = 0;
- 623 INIT_LIST_HEAD(&dentry->d_hash);
- 624 INIT_LIST_HEAD(&dentry->d_lru);
- 625 INIT_LIST_HEAD(&dentry->d_subdirs);
- 626 INIT_LIST_HEAD(&dentry->d_alias);
- 627 if (parent) {
- 628 dentry->d_parent = dget(parent);
- 629 dentry->d_sb = parent->d_sb;
- 630 } else
- 631 INIT_LIST_HEAD(&dentry->d_child);
- 632
- 633 spin_lock(&dcache_lock);
- 634 if (parent)
- 635 list_add(&dentry->d_child, &parent->d_subdirs);
- 636 dentry_stat.nr_dentry++;
- 637 spin_unlock(&dcache_lock);
- 638
- 639 return dentry;
- 640 }
- 641
下面看看怎么去寻找一个目录,涉及函数d_lookup:
- 698
-
-
-
-
-
-
-
-
-
- 708
- 709 struct dentry * d_lookup(struct dentry * parent, struct qstr * name)
- 710 {
- 711 unsigned int len = name->len;
- 712 unsigned int hash = name->hash;
- 713 const unsigned char *str = name->name;
- 714 struct list_head *head = d_hash(parent,hash);
- 715 struct list_head *tmp;
- 716
- 717 spin_lock(&dcache_lock);
- 718 tmp = head->next;
- 719 for (;;) {
- 720 struct dentry * dentry = list_entry(tmp, struct dentry, d_hash);
- 721 if (tmp == head)
- 722 break;
- 723 tmp = tmp->next;
- 724 if (dentry->d_name.hash != hash)
- 725 continue;
- 726 if (dentry->d_parent != parent)
- 727 continue;
- 728 if (parent->d_op && parent->d_op->d_compare) {
- 729 if (parent->d_op->d_compare(parent, &dentry->d_name, name))
- 730 continue;
- 731 } else {
- 732 if (dentry->d_name.len != len)
- 733 continue;
- 734 if (memcmp(dentry->d_name.name, str, len))
- 735 continue;
- 736 }
- 737 __dget_locked(dentry);
- 738 dentry->d_vfs_flags |= DCACHE_REFERENCED;
- 739 spin_unlock(&dcache_lock);
- 740 return dentry;
- 741 }
- 742 spin_unlock(&dcache_lock);
- 743 return NULL;
- 744 }
这篇文章是由databasecolumn的几个数据库大牛写的,简要的介绍了MapReduce以及将其与现代数据库管理系统进行了对比,并指出了一些不足之处。本文纯属学习性翻译,从多方面来了解MapReduce,不代表完全赞同原文的观点。请读者也辩证的看。
一月八号,一个数据库专栏的读者询问我们关于对新的分布式数据库研究成果的意见。我们在这结合MapReduce谈谈我们的看法。现在是讨论这个问题的不错的时机,因为最近媒体上到处充斥着新的革命所谓“云计算”的信息。这种模式需要利用大量的(低端)处理器并行工作来解决计算问题。实际上,这建议利用大量的低端处理器来构建数据中心,而不是利用数目少的多的高端服务器来构建。
举例来说,IBM和Google已经宣布计划用1000台处理器构建的集群提供给部分大学,传授学生们如何使用MapReduce工具在这些集群上编程。加利福尼亚大学伯克利分校甚至打算开设使用MapReduce框架编程的课程。我们对MapReduce支持者大肆炒作它如何如何能够开发更加具有扩展性,以及数据密集型程序感到震惊。MapReduce可能在某些特定类型的通用计算上是个不错的想法,但是对于数据库社区来说:
1. 从大规模数据应用程序模型来说是一个巨大的倒退。
2. 不是一个最优实现,因为它使用蛮力来代替索引。
3. 一点都不新奇,它只是实现了一个特定的25年前就有的众所周知的技术。
4. 失去了大部分目前数据库管理系统的特性。
5. 不能兼容所有目前数据库管理系统用户已经依赖的工具。
首先我们将简要的讨论下MapReduce到底是什么,然后我们将就上面5点进行更深层次的讨论。
MapReduce是什么?
MapReduce基础出发点是很易懂的。它由称为map和reduce的两部分用户程序组成,然后利用框架在计算机集群上面根据需求运行多个程序实例来处理各个子任务,然后再对结果进行归并。
Map程序从输入流中读取一组“记录”,然后对记录进行需要的过滤或者转换,然后输出一组记录(key,data)。当map程序生成输出记录时,一个分割方法将记录划分为M个不相交的块并赋予一个键值。这个分割方法一般是一个hash函数,只要这个决定性的函数能够满足就行。当一个块被填充后,它将写入磁盘,map程序结束的时候每个块都将输出M个文件。
通常情况下,将有多个map的程序实例运行在计算机集群的不同的节点上。每个map实例都将由MapReduce调度程序分配一个不重复的输入文件来独立执行。如果有N个节点参与map程序执行,那么N个节点中的每个节点都将有M个文件存储在各自的磁盘上,也就是说,总共将有N*M个文件。Fi,j, 1 ≤ i ≤ N, 1 ≤ j ≤ M.
其中有个值得注意的关键点是每个map实例都必须使用一个相同的hash方法。这样,所有的拥有相同hash值的输出记录才会写入相应的输出文件。
MapReduce的第二个阶段就是执行M个reduce的程序实例。Rj, 1 ≤ j ≤ M.每个reduce实例Rj的输入文件由文件 Fi,j组成,1 ≤ i ≤ N。还有一个值得注意的是:所有从map阶段输出的拥有相同hash值的记录,无论是哪个map实例生成的,都将由一个相同的reduce实例处理。在map-reduce框架收集整理之后,所有的输入记录都将根据它们的键值(key)编组然后提供给reduce程序。跟map程序一样,reduce程序也可以做任意的计算。所以,你可以对输入的记录做任何你想要的事情。举例来说,可能会对记录的别的字段进行一些附加的计算。每个reduce实例都可以将记录写入输出文件,只要是MapReduce计算所需要的结果。
用SQL来做类比,map象聚合(aggregate)查询中的group-by子句。Reduce则类似计算group-by起来的行的聚合函数(例如求平均等)。
现在我们基于这个计算模型来讨论上面提到的五点:
1. MapReduce是一个数据库存取的退步
做为一个数据处理模型,MapReduce呈现出了一个巨大的退步。数据库社区从IBM在1968年第一次发布IMS以来的四十年中学到了以下三个经验:
* 结构描述是好的。
* 将结构描述从程序中分离是好的
* 高阶的访问语言是好的
MapReduce没有吸引上面三个经验中的任何一个,而且还退步到了现在数据库管理系统发明前的60年代。
数据库管理系统社区学习到的关于最重要的结构描述就是:记录的字段和它的数据类型都记录在存储系统中。更重要的是,数据库管理系统的运行时可以保证所有的记录都遵守结构描述。这是避免将垃圾数据添加到数据集中的最好的方法。MapReduce没有这样的方法,也没有避免将垃圾数据添加到数据集中的控制。一个毁坏的数据集可以悄无声息的破坏整个使用这个数据集的MapReduce程序。
将数据描述与程序分离也很关键。如果开发者想在一个数据集上开发一个新的程序,他必须先去了解记录结构。在现代数据库管理系统中,结构描述存储在系统目录中,而且可以被用户用SQL查询来了解它的结构。与此相反的是,如果数据描述不存在,或者隐藏在程序之中,开发者要了解这个数据结构必须通过检查原有的代码。这个工作不仅仅是非常沉闷的,而且开发者必须先找到这个程序的源代码。如果没有相应的结构描述存在,后面的这个沉闷的问题将在所有的MapReduce程序中存在。
在1970年数据库管理系统社区,关系型数据库支持者和数据系统语言协会(Codasyl)支持者进行了一场“剧烈的辩论”。其中一个最大的争议是数据库管理系统的访问程序以何种方式访问:
* 用统计来获取你想要的数据(关系型的观点)
* 提供一个算法来进行数据访问(Codasyl的观点)
争论的结果已经是古代史了,但是整个世界都看到了高阶语言的价值以及关系型系统的胜利。以高阶语言的形式编程更加容易编写,易于修改,而且方便一个新来者的理解。Codasyl被批判为“以汇编语言的形式来对数据库管理系统进行访问”。MapReduce程序员有点类似Codasyl程序员。他们用低阶的语言来处理低阶记录。没有人提倡回归汇编语言,类似的,不应该强制任何人用MapReduce来编程。
MapReduce提倡者可能会反对说他们的数据集没有数据描述的假设。那么我们解除这个断言。在从输入数据记录中提取一个key时,map方法在每个输入记录中至少依赖一个存在的数据字段。相同Reduce方法的持有者从接受到的处理数据中计算一些值。
基于Google的BigTable或者Hadoop的Hbase来编写MapReduce程序并不会真正重大的改变这种状况。在相同的表中使用一种自描述元组格式(行号,列名,值)确实可以拥有不同的架构。但是,BigTable和Hbase并没有提供逻辑上的独立。以视图机制举例来说,视图有一个明显的简化作用,当逻辑数据描述(logic schema)改变后,仍保持程序运行。
2. MapReduce是一个粗燥的实现
所有现在数据库管理系统使用hash或者B-tree来索引加快对数据的访问。如果一个用户在查找一个记录集的子记录集(比如雇员中谁的薪水在10000或者谁在鞋生产部门),那么他可以使用索引来有效的缩减查找范围。另外,还提供了一个查询优化器来决定到底是使用索引还是进行一个残忍野蛮的顺序查询。
MapReduce没有索引,理所当然的只能使用蛮力来作为处理选项。而不管索引在当前情况下是否是一个最好的访问机制。
一个值得争论的是,MapReduce提出的自动的在计算机集群中提供并行计算的价值。其实这个特性在1980年时代就被数据库管理系统研究社区研究过了,多个原型被提出来,比如Gamma,Bubba和Grace。商业化的利用这些思想在系统则在80年代末期,比如Teradata。
概括起来说,在前20年已经出现了高性能,商业化的,面向网格计算机群的SQL引擎(带结构描述和索引)。MapReduce跟这些系统相比并没有那么好。
MapReduce同时存在很多底层的实现问题,特别是数据交换和数据斜交的情况。
一个因素是MapReduce支持者好像没有注意到关于数据斜交的问题。就像在“平行数据库系统:未来的高性能数据库系统”中提到的,数据斜交是构建成功高扩展性并行查询系统的巨大障碍。这个问题重现在map阶段,当拥有相同键的数据拥有大幅度差异的时候。这个差异,反过来导致某些reduce实例花费比其它实例更长甚至常很多的时间来运行。结果就是计算的运行时间由速度最慢的那个reduce实例决定。平行数据库社区已经广泛的研究了这个问题并且拥有了成熟的,MapReduce社区可能愿意采纳的解决方案。
还有第二个严重的性能问题被MapReduce支持者掩盖了。回忆N个map实例中的每个实例都将生成M个输出文件。每个都分发给不同的reduce实例。这些文件都被写入本地硬盘以备map实例使用。如果N是1000,M是500,那么在map阶段将生成500000个本地文件。当reduce阶段开始,500个reduce实例必须读取1000个输入文件,必须使用类似FTP的协议将每个输入文件从各个map实例运行的节点中获取(pull)过来。在100秒内所有reduce实例将同时的运行起来,不可避免的会发生两个或者更多个reduce实例企图并行的从同一个map节点中获取输入文件,包括大量的磁盘搜索,当超过因子20时,将极大的降低磁盘的有效传输率。这就是为什么并行数据库系统不实现分割文件,而使用推(push to sockets)来代替拉(pull)。因为MapReduce通过实现分割文件来获得优秀的容错性,不好说如果MapReduce框架修改成使用推(push)模型是否会成功。
鉴于实验评估,我们严重的怀疑MapReduce在大规模应用中会表现的很好。MapReduce的实现者还需要好好的研究过去25年来并行数据库管理系统的研究文献。
3. MapReduce并不新奇
MapReduce社区看起来感觉他们发现了一个全新的处理大数据集的模型。实际上,MapReduce所使用的技术至少是20年前的。将大数据集划分为小数据集的思想是在Kitsuregawa首次提出的“Application of Hash to Data Base Machine and Its Architecture”的基础上发展出来的一个新的连接算法。在“Multiprocessor Hash-Based Join Algorithms”中,Gerber演示了如何将Kitsuregawa的技术扩展到使用联合分区表,分区执行以及基于hash的分割来连接并行的无共享集群。DeWitt演示了如何采用这些技术来执行有group by子句以及没有group by子句的并行聚合。DeWitt和Gray描述了并行数据库系统以及他们如何处理查询。Shatdal和Naughton探索了并行聚合的替代策略。
Teradata已经出售利用这些技术构建的数据库管理系统20多年了,而这些技术正是MapReduce一伙声称的发明的技术。
当然MapReduce提倡者将毫无疑问的声称他们编写的MapReduce函数实现他们的软件与使用并行SQL实现有多么大的不同,我们必须提醒他们,POSTGRES已经在80年代中期就支持了用户自定义函数以及用户自定义聚合。本质上来说,从1995年Illustra引擎开始算,所有现代数据库系统都提供了类似的功能很长一段时间了。
4. MapReduce失去了很多特性
所有下面的特性都被现在的数据库管理系统提供了,而MapReduce没有:
* 批量导入 —— 将输入数据转化成想要的格式并加载到数据库中
* 索引 —— 如上文所述
* 更新 —— 改变数据集中的数据
* 事务 —— 支持并行更新以及从失败的更新中恢复
* 完善的约束 —— 防止垃圾数据添加到数据集
* 完善的引用 —— 类似FK,防止垃圾数据的存在
* 视图 —— 底层逻辑数据描述可以改变但不需要重写程序
简单的说来,MapReduce只提供了现在数据库管理系统的函数性功能。
5. MapReduce与现有的数据库管理系统工具不兼容
一个现代的SQL数据库管理系统都拥有如下可用的工具:
* 报表 —— (比如水晶报表) 将数据友好的展示给人
* 商业智能工具 —— (比如Business Objects or Cognos)允许在数据仓库中进行特定查询
* 数据挖掘工具 —— (比如Oracle Data Mining)允许用户在大数据集中发现数据规律
* 复制工具 —— 允许用户在不同的数据库中进行复制传输
* 数据库设计工具 —— 帮助用户构建数据库
MapReduce不能使用这些工具,同时它也没有自己的工具。直到它能与SQL兼容或者有人编写了这些工具,MapReduce仍然在端到端的任务中显得十分困难。
总结
看到一个庞大的社区参与到设计和实现可扩展的查询处理技术中是值得振奋的。但是,我们认为他们不应该忽视超过40年的数据库技术的经验。——特别是拥有巨大优势的数据描述模型以及物理数据与逻辑数据互相独立;描述性的查询语言(比如SQL);提供设计,实现,以及维护程序。此外,计算机科学社区不应该孤立起来而应该多读读别的社区的技术文献。我们鼓励好好的研究下近25年来的并行数据库管理系统的技术文献。最后,在MapReduce达到现代数据库管理系统之前,还有很多的没实现的特性以及必须的工具需要添加。
我们完全的理解利用数据库来解决他们的问题是可以的。数据库社区意识到数据库系统解决他们的问题使用起来操作过于“复杂”。数据库社区也从MapReduce提供的优秀的容错机制中学到了不少的有价值的东西。最后我们注意到一些数据库的研究者开始探索以MapReduce框架为基础来构建可扩展的数据库系统,yahoo!研究中心的Pig项目就是其中一个努力的结果。
其他的代码暂时就不看了,以后总结。。。