Minix 1.0文件系统的实现
一、概述
在sys_open中,我们并没有讨论如何将文件路径名转化为具体的inode,它使用的是open_namei这个函数实现的。这里面涉及到文件系统的实现。本文将从这个函数出发,说明如何将文件路径名转化为具体的inode,以及普通文件和目录是怎么实现的,在硬盘中是怎么存储的。
二、解析文件路径
2.1 通过路径名获得对应的节点open_namei
open_namei位于fs/namei.c(p281,第337行)
/*
* open_namei()
*
* namei for open - this is in fact almost the whole open-routine.
*/
int open_namei(const char * pathname, int flag, int mode,
struct m_inode ** res_inode)
{
const char * basename;
int inr,dev,namelen;
struct m_inode * dir, *inode;
struct buffer_head * bh;
struct dir_entry * de;
if ((flag & O_TRUNC) && !(flag & O_ACCMODE))
flag |= O_WRONLY;
mode &= 0777 & ~current->umask;
mode |= I_REGULAR;
if (!(dir = dir_namei(pathname,&namelen,&basename)))
return -ENOENT;
if (!namelen) { /* special case: '/usr/' etc */
if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) {
*res_inode=dir;
return 0;
}
iput(dir);
return -EISDIR;
}
bh = find_entry(&dir,basename,namelen,&de);
if (!bh) {
if (!(flag & O_CREAT)) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EACCES;
}
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC;
}
inode->i_uid = current->euid;
inode->i_mode = mode;
inode->i_dirt = 1;
bh = add_entry(dir,basename,namelen,&de);
if (!bh) {
inode->i_nlinks--;
iput(inode);
iput(dir);
return -ENOSPC;
}
de->inode = inode->i_num;
bh->b_dirt = 1;
brelse(bh);
iput(dir);
*res_inode = inode;
return 0;
}
inr = de->inode;
dev = dir->i_dev;
brelse(bh);
iput(dir);
if (flag & O_EXCL)
return -EEXIST;
if (!(inode=iget(dev,inr)))
return -EACCES;
if ((S_ISDIR(inode->i_mode) && (flag & O_ACCMODE)) ||
!permission(inode,ACC_MODE(flag))) {
iput(inode);
return -EPERM;
}
inode->i_atime = CURRENT_TIME;
if (flag & O_TRUNC)
truncate(inode);
*res_inode = inode;
return 0;
}
参数flag表示文件打开标志,如追加标志等。mode表示创建文件时使用的访问权限,要与umask取反进行与操作。res_inode用于返回pathname映射到的inode。
首先用dir_namei找到父目录节点dir,以及文件名,文件名的长度。如果文件名为空,查看flag标志,确定是否返回父目录,这时打开的是一个目录。接着用find_entry在父目录dir下查找文件名basename这个目录项是否存在,不存在则看flag是否有O_CREAT标志,是否有写权限,是否创建该目录项(add_entry)和创建inode(new_inode)。sys_open只能创建普通文件,且权限由umask和mode决定。再利用目录项的inode节点号和目录所在的设备号,找到该pathname对应的inode(iget)。还要判断该inode是否为目录,flag是否设置了访问标志,是否可以访问。最后更新访问时间为当前时间。如果设置了截断标志,则将整个节点的内容截断(不管是什么类型的文件)。设置res_inode。
与该函数功能相似的还有namei(pathname)。但这个函数在文件不存在时不能进行创建,因为没有传递flag和mode标志,直接返回找到的inode。
可以发现,上述函数的核心是dir_namei和find_entry。
2.2 获得父目录节点和文件名dir_namei
这个函数也位于namei.c中。
/*
* dir_namei()
*
* dir_namei() returns the inode of the directory of the
* specified name, and the name within that directory.
*/
static struct m_inode * dir_namei(const char * pathname,
int * namelen, const char ** name)
{
char c;
const char * basename;
struct m_inode * dir;
if (!(dir = get_dir(pathname)))
return NULL;
basename = pathname;
while ((c=get_fs_byte(pathname++)))
if (c=='/')
basename=pathname;
*namelen = pathname-basename-1;
*name = basename;
return dir;
}
这个函数使用get_dir来获得父目录的inode。然后通过遍历pathname找到最后一个/后面的文件名,作为basename。最后返回得到的父节点dir。
2.3 获取父目录节点get_dir
接着看下get_dir。
/*
* get_dir()
*
* Getdir traverses the pathname until it hits the topmost directory.
* It returns NULL on failure.
*/
static struct m_inode * get_dir(const char * pathname)
{
char c;
const char * thisname;
struct m_inode * inode;
struct buffer_head * bh;
int namelen,inr,idev;
struct dir_entry * de;
if (!current->root || !current->root->i_count)
panic("No root inode");
if (!current->pwd || !current->pwd->i_count)
panic("No cwd inode");
if ((c=get_fs_byte(pathname))=='/') {
inode = current->root;
pathname++;
} else if (c)
inode = current->pwd;
else
return NULL; /* empty name is bad */
inode->i_count++;
while (1) {
thisname = pathname;
if (!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
iput(inode);
return NULL;
}
for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
/* nothing */ ;
if (!c)
return inode;
if (!(bh = find_entry(&inode,thisname,namelen,&de))) {
iput(inode);
return NULL;
}
inr = de->inode;
idev = inode->i_dev;
brelse(bh);
iput(inode);
if (!(inode = iget(idev,inr)))
return NULL;
}
}
get_dir首先根据pathname是否以/开头,决定以当前进程的根目录还是以当前目录为起点,解析pathname。然后不断地寻找pathname的/前面的文件名,并在inode中寻找该目录项(find_entry),利用该目录项的节点号,找到对应的inode(iget)。循环是以zhaodao空字符结束的。由于最后的文件名没有/,这里不对其进行查找,所以返回的是父目录的inode。必须保证路径名有效,也就是每个/前面的文件名必须存在节点,且为目录,当前进程(用户)有进入(执行)权限,否则会返回NULL。
2.4 从目录文件查找目录项find_entry
/*
* find_entry()
*
* finds an entry in the specified directory with the wanted name. It
* returns the cache buffer in which the entry was found, and the entry
* itself (as a parameter - res_dir). It does NOT read the inode of the
* entry - you'll have to do that yourself if you want to.
*
* This also takes care of the few special cases due to '..'-traversal
* over a pseudo-root and a mount point.
*/
static struct buffer_head * find_entry(struct m_inode ** dir,
const char * name, int namelen, struct dir_entry ** res_dir)
{
int entries;
int block,i;
struct buffer_head * bh;
struct dir_entry * de;
struct super_block * sb;
#ifdef NO_TRUNCATE
if (namelen > NAME_LEN)
return NULL;
#else
if (namelen > NAME_LEN)
namelen = NAME_LEN;
#endif
entries = (*dir)->i_size / (sizeof (struct dir_entry));
*res_dir = NULL;
if (!namelen)
return NULL;
/* check for '..', as we might have to do some "magic" for it */
if (namelen==2 && get_fs_byte(name)=='.' && get_fs_byte(name+1)=='.') {
/* '..' in a pseudo-root results in a faked '.' (just change namelen) */
if ((*dir) == current->root)
namelen=1;
else if ((*dir)->i_num == ROOT_INO) {
/* '..' over a mount-point results in 'dir' being exchanged for the mounted
directory-inode. NOTE! We set mounted, so that we can iput the new dir */
sb=get_super((*dir)->i_dev);
if (sb->s_imount) {
iput(*dir);
(*dir)=sb->s_imount;
(*dir)->i_count++;
}
}
}
if (!(block = (*dir)->i_zone[0]))
return NULL;
if (!(bh = bread((*dir)->i_dev,block)))
return NULL;
i = 0;
de = (struct dir_entry *) bh->b_data;
while (i < entries) {
if ((char *)de >= BLOCK_SIZE+bh->b_data) {
brelse(bh);
bh = NULL;
if (!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
!(bh = bread((*dir)->i_dev,block))) {
i += DIR_ENTRIES_PER_BLOCK;
continue;
}
de = (struct dir_entry *) bh->b_data;
}
if (match(namelen,name,de)) {
*res_dir = de;
return bh;
}
de++;
i++;
}
brelse(bh);
return NULL;
}
首先判断查找的目录项是否为..,如果是,若dir是当前进程的根目录,则直接设置为.,因为不可以再往上走了。否则若当前dir是根inode,则必须将dir换为该设备的挂载节点,这样才能..回到根文件系统对应的上一级节点。接着根据目录的大小,遍历所有的目录项。由于目录项包含文件名,跟name进行匹配(match),如果匹配,则返回目录项,以及目录项所在的缓冲区。这里在寻找目录的数据时,使用了bmap,完成第几个目录项在哪个逻辑数据块的映射,然后将该逻辑块读到缓冲区中,从而获取到全部是目录项的数据块。
2.5 将文件第几块数据映射到磁盘逻辑块号bmap
bmap位于fs/inode.c(p260,第140行)
int bmap(struct m_inode * inode,int block)
{
return _bmap(inode,block,0);
}
int create_block(struct m_inode * inode, int block)
{
return _bmap(inode,block,1);
}
_bmap位于bmap的上面:
static int _bmap(struct m_inode * inode,int block,int create)
{
struct buffer_head * bh;
int i;
if (block<0)
panic("_bmap: block<0");
if (block >= 7+512+512*512)
panic("_bmap: block>big");
if (block<7) {
if (create && !inode->i_zone[block])
if ((inode->i_zone[block]=new_block(inode->i_dev))) {
inode->i_ctime=CURRENT_TIME;
inode->i_dirt=1;
}
return inode->i_zone[block];
}
block -= 7;
if (block<512) {
if (create && !inode->i_zone[7])
if ((inode->i_zone[7]=new_block(inode->i_dev))) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
if (!inode->i_zone[7])
return 0;
if (!(bh = bread(inode->i_dev,inode->i_zone[7])))
return 0;
i = ((unsigned short *) (bh->b_data))[block];
if (create && !i)
if ((i=new_block(inode->i_dev))) {
((unsigned short *) (bh->b_data))[block]=i;
bh->b_dirt=1;
}
brelse(bh);
return i;
}
block -= 512;
if (create && !inode->i_zone[8])
if ((inode->i_zone[8]=new_block(inode->i_dev))) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
if (!inode->i_zone[8])
return 0;
if (!(bh=bread(inode->i_dev,inode->i_zone[8])))
return 0;
i = ((unsigned short *)bh->b_data)[block>>9];
if (create && !i)
if ((i=new_block(inode->i_dev))) {
((unsigned short *) (bh->b_data))[block>>9]=i;
bh->b_dirt=1;
}
brelse(bh);
if (!i)
return 0;
if (!(bh=bread(inode->i_dev,i)))
return 0;
i = ((unsigned short *)bh->b_data)[block&511];
if (create && !i)
if ((i=new_block(inode->i_dev))) {
((unsigned short *) (bh->b_data))[block&511]=i;
bh->b_dirt=1;
}
brelse(bh);
return i;
}
参数block是文件的第几个数据块,范围是0 ~ 7 + 512 + 512 * 512 - 1。而create表示block不存在时,是否要创建新的盘数据块。显然bmap是不创建模式。而create_block是创建模式,找不到则创建。返回的磁盘逻辑块号0表示不存在。
这个函数根据block的范围找到对应的逻辑块号。如果是直接块,0 - 6,则可以直接得到逻辑块号。如果是一级索引,也就是引申处一块数据块,再来存储磁盘逻辑块号,从这个一级索引查找。如果是二级索引,也就是1 + 512 中后面的512块是二级索引,这512块保存的都是磁盘逻辑块号(可能为0)。在这查找过程中,如果create = 0,如果某一个索引或寻找的数据块不存在,则返回0。如果create = 1,则索引和数据块都会进行创建和分配。
至此,我们可以看一下inode是如何保存数据的,这些数据是如何分布在磁盘上的,是怎么找到的。首先,看下inode的结构定义:
z在include/linux/fs.h(p396,第93行)
struct d_inode {
unsigned short i_mode;
unsigned short i_uid;
unsigned long i_size;
unsigned long i_time;
unsigned char i_gid;
unsigned char i_nlinks;
unsigned short i_zone[9];
};
struct m_inode {
unsigned short i_mode;
unsigned short i_uid;
unsigned long i_size;
unsigned long i_mtime;
unsigned char i_gid;
unsigned char i_nlinks;
unsigned short i_zone[9];
/* these are in memory also */
struct task_struct * i_wait;
unsigned long i_atime;
unsigned long i_ctime;
unsigned short i_dev;
unsigned short i_num;
unsigned short i_count;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_mount;
unsigned char i_seek;
unsigned char i_update;
};
inode记录了文件的元数据。其中d_inode表示硬盘上的inode,是存储在硬盘上的结构,而m_inode表示在内存中的inode,这个inode在d_inode的基础上多添加了一些属性,用来记录包括访问时间、改变时间、设备号、inode号、引用计数等。
i_mode记录文件类型和权限、i_uid记录文件属主、i_size记录文件大小、i_mtime记录文件内容修改时间、i_gid记录文件属组、i_nlinks记录硬链接数(多少个目录项指向该inode)、i_zone[9]记录了inode的逻辑数据块号。其中i_zone[0-6]记录的是直接块号、i_znoe[7]记录的是一级索引块号、i_zone[8]记录的是二级间接块号。
所以,这里使用了索引来记录数据块的位置,每个数据块号占两个字节,能够唯一地定位到磁盘数据块。在用户看来文件的数据是连续的,但在内核看来,数据是一块一块存储的,分布在磁盘上不同位置,不一定连续。_bmap完成的便是这两种观点的转换,用户要的文件的某个偏移的数据,转换为在第几块数据中,然后内核从inode的索引中查找,找到该索引后,看该位置是否部位零,不为零说明已经分配了磁盘数据块,_bmap把该磁盘数据块号返回。然后通过bread(dev, block)读取到磁盘的该数据块的数据到缓冲区中。对于目录来说,它的数据块里面的数据是目录项,每一项保存的是inode节点号和文件名,结构定义在include/linux/fs.h(第157行)。
struct dir_entry {
unsigned short inode;
char name[NAME_LEN];
};
通过目录项中的节点号,可以读取到其对应的inode,这时我们就找到了该目录下的一个文件。有多个目录项项的inode号相同,则意味着同一个inode有多个硬链接,该inode有不同的文件名,本质上修改的内容是一致的。
所以,解析路径名时,从一个目录中不断找到文件名的目录项(bmap和find_entry),然后找到该目录项的节点号,从而找到具体的inode(iget(dev, inum))。文件路径解析是不断重复上述过程,直到找到具体的inode为止。
三、创建文件的基本函数
在open_namei查找inode的过程中,如果最后文件名不存在,可以根据flag是否有O_CREAT置位创建inode(new_inode)并添加到父目录的目录项(add_entry)中。这其实是创建一个文件的过程。这里先介绍用于创建文件的基本函数,下一节讲如何使用这些函数创建文件。
3.1 从磁盘分配一个节点new_inode
new_inode位于fs/bitmap.c(p254,第136行)
struct m_inode * new_inode(int dev)
{
struct m_inode * inode;
struct super_block * sb;
struct buffer_head * bh;
int i,j;
if (!(inode=get_empty_inode()))
return NULL;
if (!(sb = get_super(dev)))
panic("new_inode with unknown device");
j = 8192;
for (i=0 ; i<8 ; i++)
if ((bh=sb->s_imap[i]))
if ((j=find_first_zero(bh->b_data))<8192)
break;
if (!bh || j >= 8192 || j+i*8192 > sb->s_ninodes) {
iput(inode);
return NULL;
}
if (set_bit(j,bh->b_data))
panic("new_inode: bit already set");
bh->b_dirt = 1;
inode->i_count=1;
inode->i_nlinks=1;
inode->i_dev=dev;
inode->i_uid=current->euid;
inode->i_gid=current->egid;
inode->i_dirt=1;
inode->i_num = j + i*8192;
inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
return inode;
}
这个函数首先获取一个空的inode(get_empty_inode),然后获取该设备的超级块。任何文件系统使用前必须进行挂载,挂载时会读进其超级块和inode位图和数据块位图,这些缓冲块会一直存在,直到卸载为止。从该设备的8个inode位图块中查找是否有未置位的比特。有则找到空的inode。然后对该比特置位。并把对应的位图缓冲块脏位置位。这时候就可以填写inode了。刚创建的inode的属主和属组是由当前进程的有效用户ID和有效组ID确定的。硬链接数是一(待会会添加目录来指向该节点),三个时间是当前系统时间(1970.1.1 00:00:00开始算起的秒数)。inode的脏位也进行了置位,待会会进行写盘(write_inode),注意这里并未设置该节点的类型和权限(inode->i_mode),留给上层函数设置。另外,还对部分内存使用的inode数据项进行了设置,如i_num = j + i * 8192,这一项很重要,它说明了如何定位到磁盘中的inode。
3.2 从inode_table获取一个空闲节点get_empty_inode
这里,我们来看下get_empty_inode这个函数,位于fs/inode.c(p261,第194行):
struct m_inode * get_empty_inode(void)
{
struct m_inode * inode;
static struct m_inode * last_inode = inode_table;
int i;
do {
inode = NULL;
for (i = NR_INODE; i ; i--) {
if (++last_inode >= inode_table + NR_INODE)
last_inode = inode_table;
if (!last_inode->i_count) {
inode = last_inode;
if (!inode->i_dirt && !inode->i_lock)
break;
}
}
if (!inode) {
for (i=0 ; i<NR_INODE ; i++)
printk("%04x: %6d\t",inode_table[i].i_dev,
inode_table[i].i_num);
panic("No free inodes in mem");
}
wait_on_inode(inode);
while (inode->i_dirt) {
write_inode(inode);
wait_on_inode(inode);
}
} while (inode->i_count);
memset(inode,0,sizeof(*inode));
inode->i_count = 1;
return inode;
}
这个函数主要从内核数组inode_table(共有32项)寻找一个干净的节点,即没被使用(i_count = 0),没被上锁(i_lock = 0),不脏(i_dirt = 0,脏时需要写到缓冲块中)。这里最重要的是没被使用,然后才是没被锁住,最后要保证脏节点写盘。如果全部被使用,则会死机。返回的inode只有i_count = 1,其他数据项都为0。
3.3 将inode写盘write_inode
接着再来看write_inode(位于fs/inode.c):
static void write_inode(struct m_inode * inode)
{
struct super_block * sb;
struct buffer_head * bh;
int block;
lock_inode(inode);
if (!inode->i_dirt || !inode->i_dev) {
unlock_inode(inode);
return;
}
if (!(sb=get_super(inode->i_dev)))
panic("trying to write inode without device");
block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
(inode->i_num-1)/INODES_PER_BLOCK;
if (!(bh=bread(inode->i_dev,block)))
panic("unable to read i-node block");
((struct d_inode *)bh->b_data)
[(inode->i_num-1)%INODES_PER_BLOCK] =
*(struct d_inode *)inode;
bh->b_dirt=1;
inode->i_dirt=0;
brelse(bh);
unlock_inode(inode);
}
这个函数首先对inode上锁,因为接下来要对其进行操作。首先找到该inode所在设备的超级块,然后利用超级块记录的inode位图和数据块位图的个数算出该inode在哪个磁盘块上,然后读取该磁盘块到高速缓冲区,最后将该inode的磁盘部分写到高速缓冲区中,置缓冲块脏标志,而inode脏位复位。缓冲块引用计数置零。解锁该 inode。
从对inode的定位可以看出磁盘中各个数据结构的分布如下:
3.4 添加目录项add_entry
add_entry位于fs/namei.c(p276,第165行)
/*
* add_entry()
*
* adds a file entry to the specified directory, using the same
* semantics as find_entry(). It returns NULL if it failed.
*
* NOTE!! The inode part of 'de' is left at 0 - which means you
* may not sleep between calling this and putting something into
* the entry, as someone else might have used it while you slept.
*/
static struct buffer_head * add_entry(struct m_inode * dir,
const char * name, int namelen, struct dir_entry ** res_dir)
{
int block,i;
struct buffer_head * bh;
struct dir_entry * de;
*res_dir = NULL;
#ifdef NO_TRUNCATE
if (namelen > NAME_LEN)
return NULL;
#else
if (namelen > NAME_LEN)
namelen = NAME_LEN;
#endif
if (!namelen)
return NULL;
if (!(block = dir->i_zone[0]))
return NULL;
if (!(bh = bread(dir->i_dev,block)))
return NULL;
i = 0;
de = (struct dir_entry *) bh->b_data;
while (1) {
if ((char *)de >= BLOCK_SIZE+bh->b_data) {
brelse(bh);
bh = NULL;
block = create_block(dir,i/DIR_ENTRIES_PER_BLOCK);
if (!block)
return NULL;
if (!(bh = bread(dir->i_dev,block))) {
i += DIR_ENTRIES_PER_BLOCK;
continue;
}
de = (struct dir_entry *) bh->b_data;
}
if (i*sizeof(struct dir_entry) >= dir->i_size) {
de->inode=0;
dir->i_size = (i+1)*sizeof(struct dir_entry);
dir->i_dirt = 1;
dir->i_ctime = CURRENT_TIME;
}
if (!de->inode) {
dir->i_mtime = CURRENT_TIME;
for (i=0; i < NAME_LEN ; i++)
de->name[i]=(i<namelen)?get_fs_byte(name+i):0;
bh->b_dirt = 1;
*res_dir = de;
return bh;
}
de++;
i++;
}
brelse(bh);
return NULL;
}
这个函数与find_entry的逻辑相同,但是它在寻找目录的目录项时用的是create_block,也就是说当数据块不存在的时候,会自动创建(可能要把索引块要创建了),分配更多的空间。另外,一个目录的第一项数据块一定是被分配了的,如果没有分配则是错误的,因为目录至少有.和..两个目录项。一个文件的大小不是看分配了多少数据块,而是看使用了多少目录项。一旦找到一个空目录项(目录项的节点号为零),则把文件名从用户空间拷贝进去。这过程中可能遍历了整个目录的所有目录项都没找到,则目录的大小要增大一项,目录节点的改变时间要改为当前时间。如果在整个目录里面有空项,则直接填充,不用改变目录的大小。这个函数并没有填充目录项的节点号,由上层函数填充,所以返回了目录项*res_dir和所在缓冲块(标记脏)。
3.5 分配一个磁盘块new_block
在file_write或者add_entry这些写函数里面,都有可能添加新的内容到文件中,这时会用到create_block这个函数。因为当索引块或者数据块不存在时,必须分配一个新的磁盘块。这是使用new_block实现的,从这个函数同样可以看出文件系统的结构。
nnew_block位于fs/bitmap.c(p253,第75行)
int new_block(int dev)
{
struct buffer_head * bh;
struct super_block * sb;
int i,j;
if (!(sb = get_super(dev)))
panic("trying to get new block from nonexistant device");
j = 8192;
for (i=0 ; i<8 ; i++)
if ((bh=sb->s_zmap[i]))
if ((j=find_first_zero(bh->b_data))<8192)
break;
if (i>=8 || !bh || j>=8192)
return 0;
if (set_bit(j,bh->b_data))
panic("new_block: bit already set");
bh->b_dirt = 1;
j += i*8192 + sb->s_firstdatazone-1;
if (j >= sb->s_nzones)
return 0;
if (!(bh=getblk(dev,j)))
panic("new_block: cannot get block");
if (bh->b_count != 1)
panic("new block: count is != 1");
clear_block(bh->b_data);
bh->b_uptodate = 1;
bh->b_dirt = 1;
brelse(bh);
return j;
}
这个函数的逻辑与new_inode类似。先获取所在设备的超级块(get_super),然后从其块位图中查看未置位的空比特,将其置位,将所在的位图块脏位置位。然后定位到具体的磁盘块,获取该块一个缓冲块,整块清零,脏位置位,有效位置位,返回磁盘逻辑块号。可知,这个缓冲块会写盘,而且部分是零,而且是可读的。
3.6 获取一个节点iget
iget位于fs/inode.c(p262,第244行)。
struct m_inode * iget(int dev,int nr)
{
struct m_inode * inode, * empty;
if (!dev)
panic("iget with dev==0");
empty = get_empty_inode();
inode = inode_table;
while (inode < NR_INODE+inode_table) {
if (inode->i_dev != dev || inode->i_num != nr) {
inode++;
continue;
}
wait_on_inode(inode);
if (inode->i_dev != dev || inode->i_num != nr) {
inode = inode_table;
continue;
}
inode->i_count++;
if (inode->i_mount) {
int i;
for (i = 0 ; i<NR_SUPER ; i++)
if (super_block[i].s_imount==inode)
break;
if (i >= NR_SUPER) {
printk("Mounted inode hasn't got sb\n");
if (empty)
iput(empty);
return inode;
}
iput(inode);
dev = super_block[i].s_dev;
nr = ROOT_INO;
inode = inode_table;
continue;
}
if (empty)
iput(empty);
return inode;
}
if (!empty)
return (NULL);
inode=empty;
inode->i_dev = dev;
inode->i_num = nr;
read_inode(inode);
return inode;
}
这个函数首先从内核数组inode_table中找(dev, nr)是否已经存在,不存在的话将该inode从硬盘读进来(read_inode)并返回。存在的话要等待解锁,然后引用数加一,返回(未处理脏位)。如果是挂载节点,则换成被挂载文件系统的根节点返回。
为了能够访问挂载的文件系统,必须在挂载文件系统时将挂载节点读进内核数组中而不释放,这样在访问挂载文件系统的文件时,才能将挂载点转化为被挂载文件系统的根节点,在解析文件路径时(get_dir),顺利进入到被挂载文件系统的目录下。
3.7 从磁盘读取节点read_inode
其中read_inode位于iget的下方,利用inode节点号和超级块,将其读进缓存,然后从缓存复制到inode。
static void read_inode(struct m_inode * inode)
{
struct super_block * sb;
struct buffer_head * bh;
int block;
lock_inode(inode);
if (!(sb=get_super(inode->i_dev)))
panic("trying to read inode without dev");
block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
(inode->i_num-1)/INODES_PER_BLOCK;
if (!(bh=bread(inode->i_dev,block)))
panic("unable to read i-node block");
__asm__ volatile ("cld");
*(struct d_inode *)inode =
((struct d_inode *)bh->b_data)
[(inode->i_num-1)%INODES_PER_BLOCK];
brelse(bh);
unlock_inode(inode);
}
四、创建多种类型的文件
Linux文件包含普通文件、目录、字符设备文件、块设备文件等。
文件类型定义在include/const.h(p356)
#ifndef _CONST_H
#define _CONST_H
#define BUFFER_END 0x200000
#define I_TYPE 0170000
#define I_DIRECTORY 0040000
#define I_REGULAR 0100000
#define I_BLOCK_SPECIAL 0060000
#define I_CHAR_SPECIAL 0020000
#define I_NAMED_PIPE 0010000
#define I_SET_UID_BIT 0004000
#define I_SET_GID_BIT 0002000
#endif
4.1 创建一个普通文件sys_creat
在fs/open.c(p311,第187行)定义了sys_creat的用于创建普通文件的系统调用。该函数利用sys_open来创建文件。
int sys_creat(const char * pathname, int mode)
{
return sys_open(pathname, O_CREAT | O_TRUNC, mode);
}
4.2 创建一个设备文件sys_mknod
在fs/namei.c(p283,第412行)定义了用于创建设备文件的sys_mknod系统调用。
int sys_mknod(const char * filename, int mode, int dev)
{
const char * basename;
int namelen;
struct m_inode * dir, * inode;
struct buffer_head * bh;
struct dir_entry * de;
if (!suser())
return -EPERM;
if (!(dir = dir_namei(filename,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EPERM;
}
bh = find_entry(&dir,basename,namelen,&de);
if (bh) {
brelse(bh);
iput(dir);
return -EEXIST;
}
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC;
}
inode->i_mode = mode;
if (S_ISBLK(mode) || S_ISCHR(mode))
inode->i_zone[0] = dev;
inode->i_mtime = inode->i_atime = CURRENT_TIME;
inode->i_dirt = 1;
bh = add_entry(dir,basename,namelen,&de);
if (!bh) {
iput(dir);
inode->i_nlinks=0;
iput(inode);
return -ENOSPC;
}
de->inode = inode->i_num;
bh->b_dirt = 1;
iput(dir);
iput(inode);
brelse(bh);
return 0;
}
与其他创建文件的逻辑类似,先判断是否已经存在该文件名,存在出错。要在当前目录下有写权限。创建的节点的硬连接数是一(new_inode),并添加目录项(添加失败会删除新创建的节点)。如果mode是设备文件标志,则将dev参数作为第一个数据块号,这是设备文件的本质。设备文件与普通文件和目录的不同在于,它的第一个直接块项保存的是设备号,其他8项全部不用,因为它的数据存在一个设备中,它将整个设备看出一个文件。当要读取设备文件时,获取其保存的设备号,然后进行读取。可能设备文件在根文件系统上,但是其真实对应的设备是另一项。可以参考字符设备文件的机制Linux 0.11 字符设备的使用和块设备文件的实现机制 Linux 0.11 块设备文件的使用。通常设备文件保存在/dev/下,如/dev/sda1,/dev/tty0等。另外对于块设备文件,它作为一个设备存在的标志,如果其上存在Linux可以识别的文件系统,一般要将其进行挂载,才能使用其上的文件。但只读取一个设备文件,不用对该设备进行挂载,因为读取设备文件不会涉及到超级块等文件系统的概念,读取的不是其上的文件,只需保证设备存在且有驱动程序。
4.3 创建一个目录sys_mkdir
sys_mkdir位于fs/namei.c(p284,第463行)
int sys_mkdir(const char * pathname, int mode)
{
const char * basename;
int namelen;
struct m_inode * dir, * inode;
struct buffer_head * bh, *dir_block;
struct dir_entry * de;
if (!suser())
return -EPERM;
if (!(dir = dir_namei(pathname,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EPERM;
}
bh = find_entry(&dir,basename,namelen,&de);
if (bh) {
brelse(bh);
iput(dir);
return -EEXIST;
}
inode = new_inode(dir->i_dev);
if (!inode) {
iput(dir);
return -ENOSPC;
}
inode->i_size = 32;
inode->i_dirt = 1;
inode->i_mtime = inode->i_atime = CURRENT_TIME;
if (!(inode->i_zone[0]=new_block(inode->i_dev))) {
iput(dir);
inode->i_nlinks--;
iput(inode);
return -ENOSPC;
}
inode->i_dirt = 1;
if (!(dir_block=bread(inode->i_dev,inode->i_zone[0]))) {
iput(dir);
free_block(inode->i_dev,inode->i_zone[0]);
inode->i_nlinks--;
iput(inode);
return -ERROR;
}
de = (struct dir_entry *) dir_block->b_data;
de->inode=inode->i_num;
strcpy(de->name,".");
de++;
de->inode = dir->i_num;
strcpy(de->name,"..");
inode->i_nlinks = 2;
dir_block->b_dirt = 1;
brelse(dir_block);
inode->i_mode = I_DIRECTORY | (mode & 0777 & ~current->umask);
inode->i_dirt = 1;
bh = add_entry(dir,basename,namelen,&de);
if (!bh) {
iput(dir);
free_block(inode->i_dev,inode->i_zone[0]);
inode->i_nlinks=0;
iput(inode);
return -ENOSPC;
}
de->inode = inode->i_num;
bh->b_dirt = 1;
dir->i_nlinks++;
dir->i_dirt = 1;
iput(dir);
iput(inode);
brelse(bh);
return 0;
}
可以看出,最终创建的目录的链接数是2,而且刚创建的目录有.和..两个目录项,第一块数据一定已经分配。大小为32,即两个目录项的大小:文件名占用14个字节,inode号占用2个字节,多余的补零。同时父目录的硬链接数加一。
4.4 创建一个匿名管道sys_pipe
在fs/pipe.c(p298,第71行)定义了sys_pipe系统调用。
int sys_pipe(unsigned long * fildes)
{
struct m_inode * inode;
struct file * f[2];
int fd[2];
int i,j;
j=0;
for(i=0;j<2 && i<NR_FILE;i++)
if (!file_table[i].f_count)
(f[j++]=i+file_table)->f_count++;
if (j==1)
f[0]->f_count=0;
if (j<2)
return -1;
j=0;
for(i=0;j<2 && i<NR_OPEN;i++)
if (!current->filp[i]) {
current->filp[ fd[j]=i ] = f[j];
j++;
}
if (j==1)
current->filp[fd[0]]=NULL;
if (j<2) {
f[0]->f_count=f[1]->f_count=0;
return -1;
}
if (!(inode=get_pipe_inode())) {
current->filp[fd[0]] =
current->filp[fd[1]] = NULL;
f[0]->f_count = f[1]->f_count = 0;
return -1;
}
f[0]->f_inode = f[1]->f_inode = inode;
f[0]->f_pos = f[1]->f_pos = 0;
f[0]->f_mode = 1; /* read */
f[1]->f_mode = 2; /* write */
put_fs_long(fd[0],0+fildes);
put_fs_long(fd[1],1+fildes);
return 0;
}
首先从全局文件指针数组找出两个没有使用的指针项,并与当前进程没有使用的指针项关联。最后通过get_pipe_inode获取一页内存的inode,并将两个文件句柄通过传进来的用户数组返回,第一项用于读,第二项用于写。
get_pipe_inode位于fs/inode.c(p262,第228行)。
struct m_inode * get_pipe_inode(void)
{
struct m_inode * inode;
if (!(inode = get_empty_inode()))
return NULL;
if (!(inode->i_size=get_free_page())) {
inode->i_count = 0;
return NULL;
}
inode->i_count = 2; /* sum of readers/writers */
PIPE_HEAD(*inode) = PIPE_TAIL(*inode) = 0;
inode->i_pipe = 1;
return inode;
}
可以看到,写者和读者通信的数据保存在get_free_page得到的一页内存中,而不是在磁盘上,该inode与任何文件路径没有关联,与文件系统无关。i_pipe置位,作为匿名管道使用,用完即释放该页内存(iput中调用free_page)。另外,由于两个文件句柄是特定于当前进程的,所以只能在fork出来的进程有效,即匿名管道只适用于有亲属关系的进程。同时通信数据不能太大,只有一页内存(4KB)。
4.5为已存在的文件添加硬链接sys_link
sys_link位于fs/namei.c(p291,第721行)。
int sys_link(const char * oldname, const char * newname)
{
struct dir_entry * de;
struct m_inode * oldinode, * dir;
struct buffer_head * bh;
const char * basename;
int namelen;
oldinode=namei(oldname);
if (!oldinode)
return -ENOENT;
if (S_ISDIR(oldinode->i_mode)) {
iput(oldinode);
return -EPERM;
}
dir = dir_namei(newname,&namelen,&basename);
if (!dir) {
iput(oldinode);
return -EACCES;
}
if (!namelen) {
iput(oldinode);
iput(dir);
return -EPERM;
}
if (dir->i_dev != oldinode->i_dev) {
iput(dir);
iput(oldinode);
return -EXDEV;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
iput(oldinode);
return -EACCES;
}
bh = find_entry(&dir,basename,namelen,&de);
if (bh) {
brelse(bh);
iput(dir);
iput(oldinode);
return -EEXIST;
}
bh = add_entry(dir,basename,namelen,&de);
if (!bh) {
iput(dir);
iput(oldinode);
return -ENOSPC;
}
de->inode = oldinode->i_num;
bh->b_dirt = 1;
brelse(bh);
iput(dir);
oldinode->i_nlinks++;
oldinode->i_ctime = CURRENT_TIME;
oldinode->i_dirt = 1;
iput(oldinode);
return 0;
}
首先获取被链接的文件的oldinode,该文件不能是目录。同时新创建的文件必须在同一个文件系统(设备)中。最后添加一个新的目录项,填写oldinode的节点号,oldinode的链接数加一。所以硬连接没有创建新的inode,只是添加一个目录项,不能跨越文件系统,被链接的文件不能是目录。
五、删除文件
Linux文件系统中有一个硬链接的概念,表示有多少个目录项指向该节点。但是硬链接不能跨越文件系统,因为目录项保存的是一个两个字节的节点号,并没有指定节点设备号,默认为所在目录的设备号。硬链接没有增加新的节点,对应的是同一份文件数据,而软链接会增加新的节点。只有当文件的链接数为零时,该inode才会被删除,否则删除的只是目录项。同时硬链接不能是目录。硬链接的文件类型是一个普通文件,而软链接是一个符号链接文件。
5.1 解除非目录的硬链接sys_unlink
sys_unlink位于fs/namei.c(p291,第721行)。
int sys_unlink(const char * name)
{
const char * basename;
int namelen;
struct m_inode * dir, * inode;
struct buffer_head * bh;
struct dir_entry * de;
if (!(dir = dir_namei(name,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EPERM;
}
bh = find_entry(&dir,basename,namelen,&de);
if (!bh) {
iput(dir);
return -ENOENT;
}
if (!(inode = iget(dir->i_dev, de->inode))) {
iput(dir);
brelse(bh);
return -ENOENT;
}
if ((dir->i_mode & S_ISVTX) && !suser() &&
current->euid != inode->i_uid &&
current->euid != dir->i_uid) {
iput(dir);
iput(inode);
brelse(bh);
return -EPERM;
}
if (S_ISDIR(inode->i_mode)) {
iput(inode);
iput(dir);
brelse(bh);
return -EPERM;
}
if (!inode->i_nlinks) {
printk("Deleting nonexistent file (%04x:%d), %d\n",
inode->i_dev,inode->i_num,inode->i_nlinks);
inode->i_nlinks=1;
}
de->inode = 0;
bh->b_dirt = 1;
brelse(bh);
inode->i_nlinks--;
inode->i_dirt = 1;
inode->i_ctime = CURRENT_TIME;
iput(inode);
iput(dir);
return 0;
}
sys_unlink要求父路径必须有进入权限,而当前所在目录必须有写权限,才能删除一个文件。同时这个inode必须不是一个目录。最后删除目录项,也就是没有了文件名,将硬链接数减一,更新inode修改时间。
这里还要注意另外一种权限,也就是当文件所在目录的粘贴位置位时,用户必须是超级用户或者是文件的属主或者是目录的属主,才能删除这个文件,否则不能删除。
5.2 释放节点iput
那么,当硬链接数为零时,文件的内容会被删除掉,在哪里看出来的呢?这是在iput函数中实现的。iput不仅会释放引用,在最后一次释放时写脏节点,还会清空文件数据占用的磁盘空间,将磁盘上的inode复位!
void iput(struct m_inode * inode)
{
if (!inode)
return;
wait_on_inode(inode);
if (!inode->i_count)
panic("iput: trying to free free inode");
if (inode->i_pipe) {
wake_up(&inode->i_wait);
if (--inode->i_count)
return;
free_page(inode->i_size);
inode->i_count=0;
inode->i_dirt=0;
inode->i_pipe=0;
return;
}
if (!inode->i_dev) {
inode->i_count--;
return;
}
if (S_ISBLK(inode->i_mode)) {
sync_dev(inode->i_zone[0]);
wait_on_inode(inode);
}
repeat:
if (inode->i_count>1) {
inode->i_count--;
return;
}
if (!inode->i_nlinks) {
truncate(inode);
free_inode(inode);
return;
}
if (inode->i_dirt) {
write_inode(inode); /* we can sleep - so do again */
wait_on_inode(inode);
goto repeat;
}
inode->i_count--;
return;
}
这个函数首先等待inode解锁,对于匿名管道文件当引用计数为零时则直接释放所占用的一页内存(匿名管道与文件系统无关,在内存中实现,用完即消失,不是一种文件类型)。对于一个块设备文件,会将其所有在内存中的inode和缓冲块同步到内存中。当有多个进程在使用这个inode时,就算inode的硬链接数已经为零,也不会写盘,而是等最后一个使用的进程释放占用的磁盘数据块和复位inode位图再将其彻底删除。在多个进程操作的情形下也不会写脏inode。
释放所有占用的磁盘数据块的函数是truncate(只释放有分配磁盘块的常规文件和目录),复位inode位图则是free_inode。
5.3 删除一个空目录sys_rmdir
int sys_rmdir(const char * name)
{
const char * basename;
int namelen;
struct m_inode * dir, * inode;
struct buffer_head * bh;
struct dir_entry * de;
if (!suser())
return -EPERM;
if (!(dir = dir_namei(name,&namelen,&basename)))
return -ENOENT;
if (!namelen) {
iput(dir);
return -ENOENT;
}
if (!permission(dir,MAY_WRITE)) {
iput(dir);
return -EPERM;
}
bh = find_entry(&dir,basename,namelen,&de);
if (!bh) {
iput(dir);
return -ENOENT;
}
if (!(inode = iget(dir->i_dev, de->inode))) {
iput(dir);
brelse(bh);
return -EPERM;
}
if ((dir->i_mode & S_ISVTX) && current->euid &&
inode->i_uid != current->euid) {
iput(dir);
iput(inode);
brelse(bh);
return -EPERM;
}
if (inode->i_dev != dir->i_dev || inode->i_count>1) {
iput(dir);
iput(inode);
brelse(bh);
return -EPERM;
}
if (inode == dir) { /* we may not delete ".", but "../dir" is ok */
iput(inode);
iput(dir);
brelse(bh);
return -EPERM;
}
if (!S_ISDIR(inode->i_mode)) {
iput(inode);
iput(dir);
brelse(bh);
return -ENOTDIR;
}
if (!empty_dir(inode)) {
iput(inode);
iput(dir);
brelse(bh);
return -ENOTEMPTY;
}
if (inode->i_nlinks != 2)
printk("empty directory has nlink!=2 (%d)",inode->i_nlinks);
de->inode = 0;
bh->b_dirt = 1;
brelse(bh);
inode->i_nlinks=0;
inode->i_dirt=1;
dir->i_nlinks--;
dir->i_ctime = dir->i_mtime = CURRENT_TIME;
dir->i_dirt=1;
iput(dir);
iput(inode);
return 0;
}
这个函数删除一个空目录,要求是超级用户。如果在父目录设置了粘贴位,则当前用户必须是属主,否则不能删除。空目录必须包含两项.和..,大小为32,除此之外不再含有其他目录项,硬链接数是2,否则不能删除。最后置该节点的硬链接数为零(在iget中删除),父目录的链接数减一(删除目录中..指向父目录,但删除后不存在了),目录项的inode号清零(删除目录项)。
六、使用多个文件系统
6.1 硬盘分区表
在系统启动时,完成各种硬件初始化后,会移动到用户态下运行,并fork处一个init进程。init进程的第一个工作是使用setup系统调用挂载根文件系统,之后才能使用各种文件,包括从文件系统打开/dev/tty0字符设备,使用tty终端打印信息printf(打印内核信息是printk,但这个函数在内核里面可以直接使用)。具体启动过程可以参考博客Linux 0.11内核的启动过程。
sys_setup位于kernel/blkdrv/hd.c(p140,第71行)
/* This may be used only once, enforced by 'static int callable' */
int sys_setup(void * BIOS)
{
static int callable = 1;
int i,drive;
unsigned char cmos_disks;
struct partition *p;
struct buffer_head * bh;
if (!callable)
return -1;
callable = 0;
#ifndef HD_TYPE
for (drive=0 ; drive<2 ; drive++) {
hd_info[drive].cyl = *(unsigned short *) BIOS;
hd_info[drive].head = *(unsigned char *) (2+BIOS);
hd_info[drive].wpcom = *(unsigned short *) (5+BIOS);
hd_info[drive].ctl = *(unsigned char *) (8+BIOS);
hd_info[drive].lzone = *(unsigned short *) (12+BIOS);
hd_info[drive].sect = *(unsigned char *) (14+BIOS);
BIOS += 16;
}
if (hd_info[1].cyl)
NR_HD=2;
else
NR_HD=1;
#endif
for (i=0 ; i<NR_HD ; i++) {
hd[i*5].start_sect = 0;
hd[i*5].nr_sects = hd_info[i].head*
hd_info[i].sect*hd_info[i].cyl;
}
/*
We querry CMOS about hard disks : it could be that
we have a SCSI/ESDI/etc controller that is BIOS
compatable with ST-506, and thus showing up in our
BIOS table, but not register compatable, and therefore
not present in CMOS.
Furthurmore, we will assume that our ST-506 drives
<if any> are the primary drives in the system, and
the ones reflected as drive 1 or 2.
The first drive is stored in the high nibble of CMOS
byte 0x12, the second in the low nibble. This will be
either a 4 bit drive type or 0xf indicating use byte 0x19
for an 8 bit type, drive 1, 0x1a for drive 2 in CMOS.
Needless to say, a non-zero value means we have
an AT controller hard disk for that drive.
*/
if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)
if (cmos_disks & 0x0f)
NR_HD = 2;
else
NR_HD = 1;
else
NR_HD = 0;
for (i = NR_HD ; i < 2 ; i++) {
hd[i*5].start_sect = 0;
hd[i*5].nr_sects = 0;
}
for (drive=0 ; drive<NR_HD ; drive++) {
if (!(bh = bread(0x300 + drive*5,0))) {
printk("Unable to read partition table of drive %d\n\r",
drive);
panic("");
}
if (bh->b_data[510] != 0x55 || (unsigned char)
bh->b_data[511] != 0xAA) {
printk("Bad partition table on drive %d\n\r",drive);
panic("");
}
p = 0x1BE + (void *)bh->b_data;
for (i=1;i<5;i++,p++) {
hd[i+5*drive].start_sect = p->start_sect;
hd[i+5*drive].nr_sects = p->nr_sects;
}
brelse(bh);
}
if (NR_HD)
printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
rd_load();
mount_root();
return (0);
}
这个函数在没有定义HD_TYPE宏的情况下,会利用从BIOS获取的两个硬盘的参数初始化hd_info。包括硬盘的磁头数、磁道的扇区数、柱面数、硬盘控制字等。一块硬盘的扇区数是这样计算的:
第一个硬盘的扇区总数保存在hd[0]中,第二个保存在hd[5]中。其中NR_HD表示硬盘的块数,但最多只有两块。接着,读取每块硬盘的MBR,即第一个扇区,该扇区的最后两个字节必须是0x55和0xAA,否则将判定硬盘损坏。另外MBR的第446(即0x1BE,从零开始)个字节开始有4个硬盘分区记录,记录的是硬盘的四个主分区的位置。
在include/linux/hdreg.h中定义了分区表项的数据结构。
struct partition {
unsigned char boot_ind; /* 0x80 - active (unused) */
unsigned char head; /* ? */
unsigned char sector; /* ? */
unsigned char cyl; /* ? */
unsigned char sys_ind; /* ? */
unsigned char end_head; /* ? */
unsigned char end_sector; /* ? */
unsigned char end_cyl; /* ? */
unsigned int start_sect; /* starting sector counting from 0 */
unsigned int nr_sects; /* nr of sectors in partition */
};
对于这个数据结构,最后两项比较有用,都是4个字节的长度。第一个是该分区的起始物理扇区号,这个是从零开始算起的,LBA,第二个是该分区的扇区数。至此,可以知道hd[1 - 4]保存的是第一块硬盘的四个分区的起始物理分区号和分区数,hd[6-9]保存的是第二块硬盘的四个分区的起始物理分区号和分区数。另外,第一块硬盘的扇区总数保存在hd[0]中,第二块保存在hd[5]中,起始扇区都是零。
对于硬盘而言,它在读取数据时是不区分分区的,它利用的都是绝对的物理扇区号。然而四个分区对应这四个设备,我们经常调用bread(dev, block)来读取磁盘数据,内核是如何得到最终的绝对物理分区的呢?对于bread而言它只知道block是这个设备上的第几块数据,要定位具体的扇区,还得靠设备号dev,找到hd[0-9]中的数据。这里先给出一些设备号:
设备 | 主设备号 | 次设备号 | 设备号 |
---|---|---|---|
硬盘一 | 3 | 0 | 0x300 |
硬盘一主分区一 | 3 | 1 | 0x301 |
硬盘一主分区二 | 3 | 2 | 0x302 |
硬盘一主分区三 | 3 | 3 | 0x303 |
硬盘一主分区四 | 3 | 4 | 0x304 |
硬盘二 | 3 | 5 | 0x305 |
硬盘二主分区一 | 3 | 6 | 0x306 |
硬盘二主分区二 | 3 | 7 | 0x307 |
硬盘二主分区三 | 3 | 8 | 0x308 |
硬盘二主分区四 | 3 | 9 | 0x309 |
串口一 | 4 | 1 | 0x401 |
串口二 | 4 | 2 | 0x402 |
tty | 5 | 0 | 0x500 |
所以第一块硬盘的设备号是0x300,第二块硬盘的设备号是0x305。通过它们的次设备号刚好可以对应hd[次设备号],找到起始物理扇区号和扇区数。 (dev, block)转换为LBA公式:
但是内核中使用的并不是LBA,而是使用CHS,来获取硬盘数据的。这里给出一个计算的例子:
假设一块硬盘的每个柱面有6个磁头,每个磁道有8个扇区,寻找第500个扇区对应的逻辑区块地址。
一个柱面有48个扇区。所以柱面号为10:
500÷48=10...20
多出的20个扇区不够一个柱面,而每个磁道有8个扇区,可以算得所在磁头:
20÷8=2...4
磁头号为2。
多出的4为扇区号。但CHS的扇区号是从1开始的,所以扇区号为5。
可以从make_request和do_hd_request这两个函数得到这个公式。具体见Linux 0.11 块设备文件的使用第四、五部分的内容。
6.2 挂载根文件系统mount_root
在sys_setup的最后调用了mount_root(),挂载了根文件系统。mount_root在fs/super.c(p271,第242行)。
void mount_root(void)
{
int i,free;
struct super_block * p;
struct m_inode * mi;
if (32 != sizeof (struct d_inode))
panic("bad i-node size");
for(i=0;i<NR_FILE;i++)
file_table[i].f_count=0;
if (MAJOR(ROOT_DEV) == 2) {
printk("Insert root floppy and press ENTER");
wait_for_keypress();
}
for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++) {
p->s_dev = 0;
p->s_lock = 0;
p->s_wait = NULL;
}
if (!(p=read_super(ROOT_DEV)))
panic("Unable to mount root");
if (!(mi=iget(ROOT_DEV,ROOT_INO)))
panic("Unable to read root i-node");
mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not 1 */
p->s_isup = p->s_imount = mi;
current->pwd = mi;
current->root = mi;
free=0;
i=p->s_nzones;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))
free++;
printk("%d/%d free blocks\n\r",free,p->s_nzones);
free=0;
i=p->s_ninodes+1;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))
free++;
printk("%d/%d free inodes\n\r",free,p->s_ninodes);
}
- 要点
- 确保d_inode的大小为32字节。
- 初始化全局文件表file_table。
- 如果根文件系统在软盘,则提示插入软盘,按任意键继续。
- 初始化8个超级块。
- 读取根文件系统的超级块。
- 获取根文件系统的第一个inode。
- 查看磁盘数据块位图,打印出剩余的block个数。
- 查看inode位图,打印出剩余的inode个数。
- 注意
- 根文件系统的inode的i_mount没有置位,但是超级块的s_isup却设置了inode。在iget中假设已经读进挂载点,会把挂载点转换为设备系统的根节点返回。在find_entry中查找..时,如果是在根节点中查找,如果该根节点所在的超级块的s_isup有设置则将该根节点转换为对应的挂载点再查找。
- 在挂载文件系统时,必须将其超级块、inode位图、block位图读进内存。
static struct super_block * read_super(int dev)
{
struct super_block * s;
struct buffer_head * bh;
int i,block;
if (!dev)
return NULL;
check_disk_change(dev);
if ((s = get_super(dev)))
return s;
for (s = 0+super_block ;; s++) {
if (s >= NR_SUPER+super_block)
return NULL;
if (!s->s_dev)
break;
}
s->s_dev = dev;
s->s_isup = NULL;
s->s_imount = NULL;
s->s_time = 0;
s->s_rd_only = 0;
s->s_dirt = 0;
lock_super(s);
if (!(bh = bread(dev,1))) {
s->s_dev=0;
free_super(s);
return NULL;
}
__asm__ volatile ("cld");
*((struct d_super_block *) s) =
*((struct d_super_block *) bh->b_data);
brelse(bh);
if (s->s_magic != SUPER_MAGIC) {
s->s_dev = 0;
free_super(s);
return NULL;
}
for (i=0;i<I_MAP_SLOTS;i++)
s->s_imap[i] = NULL;
for (i=0;i<Z_MAP_SLOTS;i++)
s->s_zmap[i] = NULL;
block=2;
for (i=0 ; i < s->s_imap_blocks ; i++)
if ((s->s_imap[i]=bread(dev,block)))
block++;
else
break;
for (i=0 ; i < s->s_zmap_blocks ; i++)
if ((s->s_zmap[i]=bread(dev,block)))
block++;
else
break;
if (block != 2+s->s_imap_blocks+s->s_zmap_blocks) {
for(i=0;i<I_MAP_SLOTS;i++)
brelse(s->s_imap[i]);
for(i=0;i<Z_MAP_SLOTS;i++)
brelse(s->s_zmap[i]);
s->s_dev=0;
free_super(s);
return NULL;
}
s->s_imap[0]->b_data[0] |= 1;
s->s_zmap[0]->b_data[0] |= 1;
free_super(s);
return s;
}
首先看下super_block的结构(include/linux/fs.h):
struct super_block {
unsigned short s_ninodes;
unsigned short s_nzones;
unsigned short s_imap_blocks;
unsigned short s_zmap_blocks;
unsigned short s_firstdatazone;
unsigned short s_log_zone_size;
unsigned long s_max_size;
unsigned short s_magic;
/* These are only in memory */
struct buffer_head * s_imap[8];
struct buffer_head * s_zmap[8];
unsigned short s_dev;
struct m_inode * s_isup;
struct m_inode * s_imount;
unsigned long s_time;
struct task_struct * s_wait;
unsigned char s_lock;
unsigned char s_rd_only;
unsigned char s_dirt;
};
struct d_super_block {
unsigned short s_ninodes;
unsigned short s_nzones;
unsigned short s_imap_blocks;
unsigned short s_zmap_blocks;
unsigned short s_firstdatazone;
unsigned short s_log_zone_size;
unsigned long s_max_size;
unsigned short s_magic;
};
分别是inode个数、block个数、inode位图块数、block位图块数、第一个数据块所在块号、log2(数据块数)、文件的最大大小、文件系统魔数。和inode一样,剩余的项都是只在内存存在,挂载信息不会写到磁盘中,即磁盘上不存在,包括s_imount,所以系统启动时必须挂载根文件系统,以及其他设备的文件系统。在现代的Linux中,一般写在/etc/fstab中,在init进程中进行挂载。
Minix 1.0的文件系统只支持它自身,其他类型的文件系统不支持,因为它会判断魔数是否为SUPER_MAGIC。read_super会读进超级块和两个位图块。0号inode和0号block不使用。
6.3 挂载其他设备的文件系统sys_mount
int sys_mount(char * dev_name, char * dir_name, int rw_flag)
{
struct m_inode * dev_i, * dir_i;
struct super_block * sb;
int dev;
if (!(dev_i=namei(dev_name)))
return -ENOENT;
dev = dev_i->i_zone[0];
if (!S_ISBLK(dev_i->i_mode)) {
iput(dev_i);
return -EPERM;
}
iput(dev_i);
if (!(dir_i=namei(dir_name)))
return -ENOENT;
if (dir_i->i_count != 1 || dir_i->i_num == ROOT_INO) {
iput(dir_i);
return -EBUSY;
}
if (!S_ISDIR(dir_i->i_mode)) {
iput(dir_i);
return -EPERM;
}
if (!(sb=read_super(dev))) {
iput(dir_i);
return -EBUSY;
}
if (sb->s_imount) {
iput(dir_i);
return -EBUSY;
}
if (dir_i->i_mount) {
iput(dir_i);
return -EPERM;
}
sb->s_imount=dir_i;
dir_i->i_mount=1;
dir_i->i_dirt=1; /* NOTE! we don't iput(dir_i) */
return 0; /* we do that in umount */
}
- 注意
- dev_name是设备文件名,dir_name是挂载点,要求是目录。rw_flag是文件系统的读写属性,未使用。
- 只能挂载块设备,且挂载点不能是根节点。
- 设备只能挂载一次,一个inode只能挂载一个设备。挂载点必须是目录。注意与硬链接比较。
- 不能用iput释放挂载点。因为iget默认挂载点已经读进inode_table数组了。
6.4 卸载文件系统sys_umount
int sys_umount(char * dev_name)
{
struct m_inode * inode;
struct super_block * sb;
int dev;
if (!(inode=namei(dev_name)))
return -ENOENT;
dev = inode->i_zone[0];
if (!S_ISBLK(inode->i_mode)) {
iput(inode);
return -ENOTBLK;
}
iput(inode);
if (dev==ROOT_DEV)
return -EBUSY;
if (!(sb=get_super(dev)) || !(sb->s_imount))
return -ENOENT;
if (!sb->s_imount->i_mount)
printk("Mounted inode has i_mount=0\n");
for (inode=inode_table+0 ; inode<inode_table+NR_INODE ; inode++)
if (inode->i_dev==dev && inode->i_count)
return -EBUSY;
sb->s_imount->i_mount=0;
iput(sb->s_imount);
sb->s_imount = NULL;
iput(sb->s_isup);
sb->s_isup = NULL;
put_super(dev);
sync_dev(dev);
return 0;
}
- 注意
- 这里利用设备文件名卸载。
- 不能卸载根文件系统。
- 不能卸载没有挂载的设备。
- 该设备必须没有inode在使用,即不繁忙。
- 释放超级块,同步该设备的inode和block。
从挂载和卸载文件的过程可以看出,整个系统至少有一个根文件系统,且不能卸载。所有文件系统都是在根文件系统上建立起来的,只有一棵目录树,不同于Windows的多个盘符多棵目录树。所以在规划硬盘时必须给/分配分区。被挂载的文件系统还可以有挂载点,可以再挂载文件系统。拔出设备时,必须将设备卸载,将内存缓冲块写盘,否则可能造成数据丢失,甚至损坏文件系统。
七、总结
在目录项中保存的是从1开始的节点号,这可以从new_inode得到:位图中j + i * 8192。但是read_inode和write_inode必须再利用启动块和超级块和位图块数来定位到具体的磁盘逻辑块。而数据块保存的是可以直接使用的磁盘逻辑块号,这可以从new_block得到:j + i*8192 + sb->s_firstdatazone-1。根文件系统是建立缓冲块上面的,而且调用的是bread(dev, block)。
部分流程图如下:
参考书籍
Linux 内核完全注释 赵炯
Linux 0.11源码下载