Minix 1.0文件系统的实现

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)。

    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才会被删除,否则删除的只是目录项。同时硬链接不能是目录。硬链接的文件类型是一个普通文件,而软链接是一个符号链接文件。

    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]中的数据。这里先给出一些设备号:

设备主设备号次设备号设备号
硬盘一300x300
硬盘一主分区一310x301
硬盘一主分区二320x302
硬盘一主分区三330x303
硬盘一主分区四340x304
硬盘二350x305
硬盘二主分区一360x306
硬盘二主分区二370x307
硬盘二主分区三380x308
硬盘二主分区四390x309
串口一410x401
串口二420x402
tty500x500

=<<8+

    所以第一块硬盘的设备号是0x300,第二块硬盘的设备号是0x305。通过它们的次设备号刚好可以对应hd[次设备号],找到起始物理扇区号和扇区数。 (dev, block)转换为LBA公式:
LBA=block×2+hd[MINOR(dev)].start_sect

    但是内核中使用的并不是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源码下载

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值