ffutop

前一篇已经描述对文件系统进行了宏观性的描述,这一篇,将以特定的文件读写操作为示例,串联对整个文件系统的基本操作。

首先先来看看平台相关的文件读写操作的 C 代码是怎样一个调用方式

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

int panic()
{
    fprintf(stderr, "%s (errno=%d)\n", strerror(errno), errno);
    return -1;
}

int main(int argc, char *argv[])
{
    /* 打开文件 frw.txt (以可读写 | 若不存在则新建的形式) */
    int fd = open("/root/frw.txt", O_RDWR | O_CREAT);
    if (fd == -1)
        return panic();

    /* 向文件写入 Hello World! 共计 12 个字符 */
    ssize_t wsize = write(fd, "Hello World!", 12);
    if (wsize == -1)
        return panic();

    /* 重定位文件读写指针 */
    off_t off = lseek(fd, 0, SEEK_SET);
    if (off == -1)
        return panic();

    char* buf = (char *) malloc(wsize);
    /* 读取文件内容 */
    ssize_t rsize = read(fd, buf, wsize);
    if (rsize == -1)
        return panic();

    printf("%s\n", buf);
    free(buf);
    /* 关闭文件 */
    int stat = close(fd);
    if (stat == -1)
        return panic();

    return 0;
}

高速缓冲区初始化

上一篇已经描述过了,文件系统的结构、包括数据,都是持久化地存储在存储设备中的。

但是,我们应该也隐约的了解另一个事实,文件读写操作并不会直接操作存储设备上的数据,而是先经过一个称之为高速缓冲的内存区域。

那么,高速缓冲是什么? 究竟承担什么工作? 先来看看它的初始化流程吧。

首先回到 main.c (内核代码的主函数)

void main(void) 
{
    ROOT_DEV = ORIG_ROOT_DEV;
    drive_info = DRIVE_INFO;
    memory_end = (1<<20) + (EXT_MEM_K<<10);
    memory_end &= 0xfffff000;
    if (memory_end > 16*1024*1024)
        memory_end = 16*1024*1024;
    if (memory_end > 12*1024*1024)
        buffer_memory_end = 4*1024*1024;
    else if (memory_end > 6*1024*1024)
        buffer_memory_end = 2*1024*1024;
    else
        buffer_memory_end = 1*1024*1024;
    main_memory_start = buffer_memory_end;
#ifdef RAMDISK
    main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
    mem_init(main_memory_start,memory_end);
    trap_init();
    blk_dev_init();
    chr_dev_init();
    tty_init();
    time_init();
    sched_init();                       // 第四篇已经讲过,负责任务调度模块的初始化
    buffer_init(buffer_memory_end);     // 本篇的起始,负责缓冲区的初始化
    hd_init();
    floppy_init();
    sti();
    move_to_user_mode();
    if (!fork()) {      /* we count on this going ok */
        init();
    }
    for(;;) pause();
}

buffer_init(buffer_memory_end); 用来初始化缓冲区。此处有几个原因:

  1. CPU 读写操作如果直接操作外存,速度上是一个极大的考验。毕竟内存已经较之 CPU 速度慢,外存的读写速度就更慢了。

  2. 解耦,其实读写操作并不仅仅发生在外存(块存储设备),同样的,字符设备等等也都会需要读写操作,增加中间层可以封装变化。

  3. 更多,个人了解有限...

struct buffer_head {
 char * b_data;
 unsigned long b_blocknr;
 unsigned short b_dev;
 unsigned char b_uptodate;
 unsigned char b_dirt;
 unsigned char b_count;
 unsigned char b_lock;
 struct task_struct * b_wait;
 struct buffer_head * b_prev;
 struct buffer_head * b_next;
 struct buffer_head * b_prev_free;
 struct buffer_head * b_next_free;
};

/* from fs/buffer.c */
void buffer_init(long buffer_end)
{
    struct buffer_head * h = start_buffer;
    void * b;
    int i;

    if (buffer_end == 1<<20)
        b = (void *) (640*1024);
    else
        b = (void *) buffer_end;
    while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
        h->b_dev = 0;
        h->b_dirt = 0;
        h->b_count = 0;
        h->b_lock = 0;
        h->b_uptodate = 0;
        h->b_wait = NULL;
        h->b_next = NULL;
        h->b_prev = NULL;
        h->b_data = (char *) b;
        h->b_prev_free = h-1;
        h->b_next_free = h+1;
        h++;
        NR_BUFFERS++;
        /* 跳过 640K ~ 1M 的显存和 BIOS RAM 部分 */
        if (b == (void *) 0x100000)
            b = (void *) 0xA0000;
    }
    h--;
    free_list = start_buffer;
    free_list->b_prev_free = h;
    h->b_next_free = free_list;
    for (i=0;i<NR_HASH;i++)
        hash_table[i]=NULL;
}

缓冲块的所有关键信息都由 buffer_head 数据结构进行记录, 至于有多少个 buffer_head? 只能说能划分多少就划分多少。

比较直观的结构信息如下

main.c 中已经提前确认了内核程序占用内存的大小,缓冲区大小以及主内存大小。

在高速缓冲区的开始位置,都用来存储 buffer_head 的信息,与每个缓冲块(从缓冲区结束位置开始分配)一一对应,直到中间某个位置不足以分配缓冲块为止。

另外的信息,就是可以看到一个 hash_table 数据结构了。

应该都能够想象,每个缓存块与外存中的数据块相对应,通过设备号 + 数据块号进行唯一定位。但是,如何快速查找需要操作的数据块是否已经存在与高速缓存中了呢? 显然直接遍历查找是不太可靠的办法。采用开放地址法的哈希表能够协助快速定位缓冲块。

挂载文件系统

既然高速缓冲区都准备就绪了,那么文件系统是否已经挂载了呢? 很遗憾,知道 main() 调用 init() 函数之前,文件系统依然没有挂载,也就是说,CPU 仍然只能通过基本输入输出对存储设备进行相当初级的 IO 操作。

那么,什么时候才能去挂载根目录呢?

/* from init/main.c */
/* 由 main() 触发 */
void init(void)
{
    int pid,i;
    /* 这是比较重要的一环了,开始挂载的起始动作 */
    setup((void *) &drive_info);
    ...
}

setup 函数做了什么呢?这是一个内嵌汇编,主要做的就是触发系统调用 int 0x80

__inline__ int setup(void * BIOS) { 
    long __res; 
    __asm__ volatile (
            "int $0x80" 
            : "=a" (__res) 
            : "0" (0),"b" ((long)(BIOS))
    ); 
    if (__res >= 0) 
        return (int) __res; 
    errno = -__res; 
    return -1; 
}

其中看到给出的 EAX = 0, 查表(表在 include/linux/sys.h 里) 可以知道触发的是 sys_setup 函数(函数位于 kernel/blk_drv/hd.c)

/* 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;

    /* setup 只允许被调用一次 */
    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++) {
        /* 0x300 和 0x305 分别代表两个硬盘 */
        /* 读取每个硬盘的第一块数据 (1024B) */
        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("");
        }
        /* 读取分区表 (位于 引导扇区第 446 字节开始处 */
        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();           /* mount 根文件系统 */
    return (0);
}

终于到了挂载文件系统的时候了

mount_root 用来做初级的初始化工作。同时,上一节已经描述过了,存储设备的超级块是用了记录整个文件系统最重要的结构。

那么通过将超级块的信息读取到内存,也就可以将内存与外存的文件系统相互联系起来了。

下面这段代码最重要的内容就是 read_super() 函数了 .

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");
    /* 先初始化文件表,该版本操作系统限制最大同时打开 NR_FILE(64个) 文件 */
    for(i=0;i<NR_FILE;i++)
        /* f_count = 0 表明没有被引用 */
        file_table[i].f_count=0;
    /* 如果引导盘是软盘的话,提示插入根文件系统盘 */
    if (MAJOR(ROOT_DEV) == 2) {
        printk("Insert root floppy and press ENTER");
        wait_for_keypress();
    }
    /* 初始化内存超级块数据结构 (总共 8 个) */
    for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++) {
        p->s_dev = 0;
        p->s_lock = 0;
        p->s_wait = NULL;
    }
    /* Hint: 读取超级块的信息,挂载根文件系统重要的部分(代码请往下翻) */
    if (!(p=read_super(ROOT_DEV)))
        panic("Unable to mount root");
    /* 读取文件系统的 1 号i节点 (即该设备上文件系统的根节点) */
    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 指的是当前的任务(任务1),以后所有的任务都会由任务1或任务1的子任务进行派生,也就意味着 current->root 会一直复制过去
     * 到这里为止,应该认为根文件系统以及被挂载了。是不是跟被耍了一样?
     */
    current->pwd = mi;
    current->root = mi;
    free=0;
    i=p->s_nzones;
    /* 统计还有多少空闲数据块以及多少可用i节点 (附上一张启动过程中打印的信息) */
    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);
}

重要要的部分,read_super(int dev),用于读取超级块的数据

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;
    /* 设备超级块不在内存中,就先找一个空闲的内存超级块 (总共维护 8 个超级块数据结构) */
    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);
    /* 通过 block read 读取设备第一个物理块 (每个物理块 1024 B) */
    if (!(bh = bread(dev,1))) {
        s->s_dev=0;
        free_super(s);
        return NULL;
    }
    /* 复制一份超级块的数据 */
    *((struct d_super_block *) s) =
        *((struct d_super_block *) bh->b_data);
    /* 释放缓冲区的数据 */
    brelse(bh);
    /* 验证魔数, 该版本操作系统只支持 1.0 版 Minix 文件系统,魔数 0x137F */
    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;
    /* 读取 i 节点位图块 */
    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;
    /* 与前面的 wait_on_super() 对应(解开lock标志) */
    free_super(s);
    return s;
}

是不是觉得也没什么,就这样根文件系统已经挂载了? 毫无实感是吧。

Extra: 普通挂载

既然讲过了根文件系统的挂载。那就顺带着讲讲普通文件系统的挂载吧。

相信从命令上来讲应该比较简单也比较熟悉吧。mount disk.img /mnt 也算是挂载到 /mnt 下了

但是,究竟是怎么实现的呢?

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;

    /** 
     * 省略大部分判断逻辑, 主要就是:
     * 1. 判断 dev_name 所属的设备号,读取该设备上的超级块
     * 2. 读取 dir_name (需要挂载到的位置),判定是否允许被挂载(比如根节点不允许挂其它设备)
     */
    ...

    /* 设置超级块的 mount 标志 */
    sb->s_imount=dir_i;
    /* 设置该 i 节点的 mount 标志 */
    dir_i->i_mount=1;
    dir_i->i_dirt=1;        /* NOTE! we don't iput(dir_i) */
    return 0;           /* we do that in umount */
}

文件读写

前面讲了这么多,终于到了最关心的部分了。当然,也并不是说前面的内容不重要,事实上,相当重要,只是都隐藏在了内核引导的过程中,且调用频度低,才导致了没有存在感。但是这恰恰才是支持文件读写的基石。

不多说废话,下面就要开始文件读写的内容。

打开文件

打开文件的函数原型是 int open(const char * filename, int flag, ...);

当然,此类系统调用最终的实现都是 int 0x80 , 明确一个调用号,然后就陷入内核态了。

内核态下调用的函数是: int sys_open(const char * filename,int flag,int mode)

来看看细节:

int sys_open(const char * filename,int flag,int mode)
{
    struct m_inode * inode;
    struct file * f;
    int i,fd;

    /*
     * current 是由内核数据段维护的当前任务的指针
     * umask 是指当前任务在新建文件时的默认掩码
     *   例如 Linux 默认是 022, 即新建文件被禁止了组用户与其它用户的写权限
     * 这里是先确定新建文件的权限
     */
    mode &= 0777 & ~current->umask;
    /*
     * 文件描述符,每个文件单独维护一套,以数字标记
     * 找一个空闲的文件描述符项
     */
    for(fd=0 ; fd<NR_OPEN ; fd++)
        if (!current->filp[fd])
            break;
    if (fd>=NR_OPEN)
        return -EINVAL;
    /*
     * 顾名思义,设置在调用 exec() 函数时主动关闭的文件
     * exec() 通常与 fork() 联用,fork() 负责复制一个任务,而 exec 负责替换新任务的代码和数据段(从而产生一个新的任务)
     * 这里的声明即是复位 fd 位置的标志,允许子任务也持有相同的文件描述符项
     */
    current->close_on_exec &= ~(1<<fd);
    f=0+file_table;
    /* 在文件表中找一项空闲的 */
    for (i=0 ; i<NR_FILE ; i++,f++)
        if (!f->f_count) break;
    if (i>=NR_FILE)
        return -EINVAL;
    /* 当前任务的文件描述符项指向文件表项, 同时文件表项的引用计数+1*/
    (current->filp[fd]=f)->f_count++;
    /* 调用 open_namei 打开文件,如果失败则释放刚才占用的文件结构,并返回错误码 */
    if ((i=open_namei(filename,flag,mode,&inode))<0) {
        current->filp[fd]=NULL;
        f->f_count=0;
        return i;
    }
    /* 
     * 对不同的文件进行不同的特殊处理, 毕竟有 "一切皆文件" 的口号嘛
     * 诸如字符设备等也都是文件
     */
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
    if (S_ISCHR(inode->i_mode))
        if (MAJOR(inode->i_zone[0])==4) {
            if (current->leader && current->tty<0) {
                current->tty = MINOR(inode->i_zone[0]);
                tty_table[current->tty].pgrp = current->pgrp;
            }
        } else if (MAJOR(inode->i_zone[0])==5)
            if (current->tty<0) {
                iput(inode);
                current->filp[fd]=NULL;
                f->f_count=0;
                return -EPERM;
            }
/* Likewise with block-devices: check for floppy_change */
    if (S_ISBLK(inode->i_mode))
        check_disk_change(inode->i_zone[0]);
    /* 初始化内存文件结构的各个参数 */
    f->f_mode = inode->i_mode;
    f->f_flags = flag;
    f->f_count = 1;
    f->f_inode = inode;
    f->f_pos = 0;
    return (fd);
}

在整个打开文件的过程中,除了真正去文件系统查找文件的时候用到了文件名,其它时候,都将是以文件描述符进行交互的。

再看看更细节的方面,毕竟就目前来说,我们跳过了最重要的一环 open_namei ,从而看似整个流程都简单了很多很多。

通常我们在编码过程中都是通过绝对地址/相对地址来唯一定位一个文件。因此,就必然存在逐级寻找文件的过程

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");
    /* 判定当前路径i节点是否有效 */
    if (!current->pwd || !current->pwd->i_count)
        panic("No cwd inode");
    /* 
     * 这里的 get_fs_byte(..) 是宏定义,fs 指的是 FS 段寄存器
     * Linux 内核将 DS, ES 用于内核数据段, 用 FS 指向局部描述符表的当前任务数据段
     * 这里可以简单理解成取字符数组的第一个字节
     */
    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;
    }
}

/*
 *  dir_namei()
 *
 * 处理路径 pathname, 处理成i节点表示的最终一级目录+目录下文件名(也可能pathname表示的就是目录)
 */
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;
}

/*
 *  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;
    /* 如果给的 pathname 是一个目录 */
    if (!namelen) {         /* special case: '/usr/' etc */
        if (!(flag & (O_ACCMODE|O_CREAT|O_TRUNC))) {
            *res_inode=dir;
            return 0;
        }
        iput(dir);
        return -EISDIR;
    }
    /* 找到目录对应的i节点的数据块 */
    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;
}

文件写入

接下来就要进行文件写入的流程了

如何陷入内核态函数就不再细说了,相信看过这么多系统调用之后,也能知道基本上系统调用在内核态对应的函数都是以 sys_ 形式出现的

int sys_write(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    /* 非法 fd , 抛异常 */
    if (fd>=NR_OPEN || count <0 || !(file=current->filp[fd]))
        return -EINVAL;
    /* count = 0,无需写入数据 */
    if (!count)
        return 0;
    inode=file->f_inode;
    /* 针对不同的i节点类型,有不同的写入函数 */
    if (inode->i_pipe)
        return (file->f_mode&2)?write_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode->i_mode))
        return rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos);
    if (S_ISBLK(inode->i_mode))
        return block_write(inode->i_zone[0],&file->f_pos,buf,count);
    if (S_ISREG(inode->i_mode))
        return file_write(inode,file,buf,count);
    printk("(Write)inode->i_mode=%06o\n\r",inode->i_mode);
    return -EINVAL;
}

看看对于常规文件是怎么操作的吧。

int file_write(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    off_t pos;      /* 偏移量 */
    int block,c;
    struct buffer_head * bh;
    char * p;
    int i=0;

    /* 如果是 Append 模式,把偏移量重置到文件末尾 */
    if (filp->f_flags & O_APPEND) 
        pos = inode->i_size;
    /* 否则就使用当前文件数据结构持有的偏移量 */
    /*
        附上数据结构  file 的内容 
        struct file {
            unsigned short f_mode;
            unsigned short f_flags;
            unsigned short f_count;
            struct m_inode * f_inode;
            off_t f_pos;    每个打开的文件都将持有当前的偏移值
        };
     */
    else
        pos = filp->f_pos;
    /* 逐字符向缓冲区写入数据 */
    while (i<count) {
        /* 最开始当然是创建在磁盘上占用一个数据块了 (如果文件对应的块不存在的话) */
        if (!(block = create_block(inode,pos/BLOCK_SIZE)))
            break;
        /* 根据数据块获得相应的缓冲块 */
        if (!(bh=bread(inode->i_dev,block)))
            break;
        /* 在缓冲块中的偏移量 */
        c = pos % BLOCK_SIZE;
        /* 定位到具体的缓冲区的内存地址 */
        p = c + bh->b_data;
        bh->b_dirt = 1;
        /* 当前这个缓冲块还有多少字节可写 */
        c = BLOCK_SIZE-c;
        /* 如果需要写入的数据量少于 c */
        if (c > count-i) c = count-i;
        /* 添加偏移量计数, 更新数据结构中维护的值 */
        pos += c;
        if (pos > inode->i_size) {
            inode->i_size = pos;
            inode->i_dirt = 1;
        }
        i += c;
        /* 向缓冲块逐字节写入数据 */
        while (c-->0)
            *(p++) = get_fs_byte(buf++);
        /* 释放对缓冲块的占用,当然,在释放前会完成缓冲块<->外存数据块的同步 */
        brelse(bh);
    }
    inode->i_mtime = CURRENT_TIME;
    if (!(filp->f_flags & O_APPEND)) {
        /* 非 APPEND 模式,更新文件读写指针(偏移量); APPEND 模式是使用 inode->i_size ,所有就不需要在这里更新了 */
        filp->f_pos = pos;
        inode->i_ctime = CURRENT_TIME;
    }
    return (i?i:-1);
}

是不是觉得也没有什么太高大上的操作。确实如此,更多关于缓存块与文件系统数据块的同步都已经被包装到 bread(), brelse() 中了。

不过,暂时无需细究。总之到此为止,先要有一个基础的观念: 所有与外存储器(这里也包括控制台等)进行数据交互都必须经过缓冲区

缓冲区封装了对外存储器的全部操作,而提供给 CPU 更高效的 I/O 操作,当然,也更为简单快捷

文件读取

至于文件读取,也基本类似了,所以也就不再深入描述。

当然,要注意的就是,在本篇开始的部分提供的例程中,write & read 中插入了 off_t off = lseek(fd, 0, SEEK_SET); 这样的代码。

原因应该也能够想到,学习 sys_write(..) 的时候我们已经看到,任务对同一个文件在内存中维护了一个文件读写偏移量。因此,要读取刚才写入的内容,就不得不先改动这个读写偏移量了

int sys_read(unsigned int fd,char * buf,int count)
{
    struct file * file;
    struct m_inode * inode;

    if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
        return -EINVAL;
    if (!count)
        return 0;
    verify_area(buf,count);
    inode = file->f_inode;
    if (inode->i_pipe)
        return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
    if (S_ISCHR(inode->i_mode))
        return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
    if (S_ISBLK(inode->i_mode))
        return block_read(inode->i_zone[0],&file->f_pos,buf,count);
    if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
        if (count+file->f_pos > inode->i_size)
            count = inode->i_size - file->f_pos;
        if (count<=0)
            return 0;
        return file_read(inode,file,buf,count);
    }
    printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
    return -EINVAL;
}

小结

这篇对文件系统的代码层面的描述,仅仅只是捡了一个相当有限的片面 (常规文件读写)。从文件系统的挂载开始提供了一个读写的完整流程介绍(当然,很多细节是缺失的,不过不要着急)。

虽然平常都能够了解到一个比较模糊的前提,文件读写需要利用缓冲区,但是究竟什么是缓存区,如何使用都不会有太多的概念。本篇最大的重点,就是首先请读者们建立起一个基础性的对缓冲区的了解。事实上,这个中介在 I/O 中扮演了相当重要的角色。而且内存也为其提供了相当大的一份空间,差不多有 1/4 了。

跨了差不多半个多月来写,上下文的承接可能有些生硬了,甚至不一致了... 尴尬...

  __                    __                  
 / _| __ _ _ __   __ _ / _| ___ _ __   __ _ 
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, |
                 |___/                |___/