从零编写linux0.11 - 第十章 文件系统(二)

从零编写linux0.11 - 第十章 文件系统(二)

编程环境:Ubuntu 20.04、gcc-9.4.0

代码仓库:https://gitee.com/AprilSloan/linux0.11-project

linux0.11源码下载(不能直接编译,需进行修改)

本章目标

完善文件系统,提供文件的增删改等功能。能够更改文件以及创建删除文件和文件夹。

1.write - 更改普通文件

上一章中,write 函数已经能够写入字符设备中。这一节继续完善 write 函数,对普通文件进行修改。

buffer_head 中的 b_dirt 是用来表示 buffer_head 是否被修改过,同样,我们需要一个成员表示 inode 是否被修改过,同时还要记录修改的时间。m_inode 结构体变为如下形式。

struct m_inode {
    unsigned short i_mode;      // 文件的类型和属性(rwx)
    unsigned short i_uid;       // 文件所有者的用户id
    unsigned long i_size;       // 文件长度(字节)
    unsigned long i_mtime;      // 修改时间(从1970.1.1:0时算起,单位:秒)
    unsigned char i_gid;        // 文件所有者的组id
    unsigned char i_nlinks;     // 链接数
    unsigned short i_zone[9];   // 文件占用的逻辑块的块号
                                // zone[0]-zone[6]是直接块号
                                // zone[7]是一次间接块号
                                // zone[8]是二次间接块号
/* these are in memory also */
    unsigned long i_atime;      // 最近访问文件的时间
    unsigned long i_ctime;      // 修改时间
    unsigned short i_dev;       // 设备号
    unsigned short i_num;       // inode号
    unsigned short i_count;     // 引用计数
    unsigned char i_dirt;       // 是否修改过
};

write 函数的声明如下:

ssize_t write(int fd, const void *buf, size_t count);

更改普通文件的操作需要注意哪些问题?

  1. **是普通文件吗?**需要判断文件描述符所指代的文件是不是普通文件,是的话就进入下一步。

  2. **从哪里开始更改?**在调用 open 函数时,添加上 O_APPEND 标志代表从文件末尾开始添加内容,不然就是从 f_pos 的位置开始。

  3. **要为文件添加逻辑块吗?**假如,一个文件的大小为 1KB,此时要从文件末尾开始添加10个字节。这就需要为文件分配一个新的逻辑块,并将逻辑块号写入文件的 inode。

搞清楚问题后我们来看看更改普通文件的流程。

// read_write.c
int sys_write(unsigned int fd, char *buf, int count)
{
    ...
    if (S_ISCHR(inode->i_mode)) {   // 字符设备文件
        return rw_char(WRITE, inode->i_zone[0], buf, count, &file->f_pos);
    }
    if (S_ISREG(inode->i_mode)) {   // 普通文件
        return file_write(inode, file, buf, count);
    }
    
    printk("(Write)inode->i_mode=%06o\n", inode->i_mode);
    return -EINVAL;
}

第8-10行:判断文件是否是普通文件。

// file_dev.c
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;

    if (filp->f_flags & O_APPEND)   // 在文件末尾添加
        pos = inode->i_size;
    else                            // 从当前的位置更改
        pos = filp->f_pos;
    while (i < count) {
        // 找到文件读写指针所在的逻辑块
        block = create_block(inode, pos / BLOCK_SIZE);
        if (!block)
            break;
        bh = bread(inode->i_dev, block);
        if (!bh)
            break;
        c = pos % BLOCK_SIZE;   // 文件读写指针在逻辑块中的偏移
        p = c + bh->b_data;     // 文件读写指针在文件缓冲区的偏移
        bh->b_dirt = 1;
        c = BLOCK_SIZE - 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)) {
        filp->f_pos = pos;
        inode->i_ctime = CURRENT_TIME;
    }
    return (i ? i : -1);
}

第10-13行:解决了第二个问题。确定了更改的起始地点。

第16-18行:找到文件读写指针所在的逻辑块。如果 f_pos 的值为1024,那么我们需要修改的地方是在文件的第二个逻辑块上。此时,若文件只有一个逻辑块,那么我们需要为文件添加一个逻辑块。

第19-21行:将逻辑块读取到内存中。

第22-27行:计算要向逻辑块写入的字符数。假如,文件只有一个逻辑块,有1020个字节的内容。此时,向文件末尾添加10个字节,那么会向第一个逻辑块写入4个字节,创建第二个逻辑块,向第二个逻辑块写入6个字节。

第28-32行:如果文件的更改导致文件的大小发生变化,需要修改文件的大小。

第34-35行:复制 buf 中的数据到文件缓冲区中。

第36行:该文件缓冲区已无用,释放掉。

第14-37:如果写入的内容跨越多个逻辑块,就会多次循环。

第38-42行:修改 inode 的修改时间,如果读写标志不是 O_APPEND,就更改文件读写指针。

最后返回修改是否成功。

// inode.c
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)   // 7个直接 + 1个一级间接 + 1个二级间接
        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;
        bh = bread(inode->i_dev, inode->i_zone[7]);
        if (!bh)
            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;
    
    bh = bread(inode->i_dev, inode->i_zone[8]);
    if (!bh)
        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;
    
    bh = bread(inode->i_dev, i);
    if (!bh)
        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;
}

int create_block(struct m_inode *inode, int block)
{
    return _bmap(inode, block, 1);
}

create_block 调用了 _bmap 函数,参数 create 的值为1。

第13-20行:如果是前7个逻辑块,且文件没有该逻辑块,就为文件添加一个逻辑块,返回逻辑块号。

第23-43行:如果传入的是文件的第8-519个逻辑块,且文件没有一级间接逻辑块。要先为文件添加一个一级间接逻辑块,然后再添加一个用于存放文件数据的逻辑块,将逻辑块号写入一级间接逻辑块中,返回逻辑块号。

第46-78行:如果不是以上两种情况,且文件没有二级间接逻辑块,要先为文件添加一个二级间接逻辑块,然后再添加一个一级间接逻辑块,将逻辑块号写入二级间接逻辑块中,接着再添加一个用于存放文件数据的逻辑块,将逻辑块号写入一级间接逻辑块中,返回逻辑块号。

想要为文件添加逻辑块,我们得找到一个空闲的逻辑块,这需要遍历逻辑块位图。linux0.11 使用内联汇编来寻找位图中的第一个0(空闲逻辑块)所在的位置。

// bitmap.c
#define find_first_zero(addr) ({\
int __res;                      \
__asm__ __volatile__ (          \
    "cld\n"                     \
    "1:lodsl\n\t"               \
    "notl %%eax\n\t"            \
    "bsfl %%eax,%%edx\n\t"      \
    "je 2f\n\t"                 \
    "addl %%edx,%%ecx\n\t"      \
    "jmp 3f\n"                  \
    "2:addl $32,%%ecx\n\t"      \
    "cmpl $8192,%%ecx\n\t"      \
    "jl 1b\n"                   \
    "3:"                        \
    : "=c"(__res)               \
    : "c"(0), "S"(addr));       \
__res; })

__res 被放在 ecx 中,并被初始化为0。addr 是逻辑块位图的起始地址,被放在 esi 中。

cld会使 esi 在字符操作中自动递增。

lodsl从 esi 指向的地址把一个双字(4 字节)加载到 eax 中。因为执行了 cld,esi 会递增。

notl %%eax会将 eax 按位取反。

bsfl %%eax,%%edx会从低位开始扫描 eax,看是否有1的位,若有就在 edx 中保存该位的位号。

je 2f:如果第8行中,eax 的值为0,则跳转到第12行;否则,继续执行第10行指令。

addl %%edx,%%ecx计算出第一个0位在逻辑块中的位号。

2:addl $32,%%ecx:4字节有32位,ecx 保存已经遍历的位数。

cmpl $8192,%%ecx:一个逻辑块有1024字节,8192个位,将已遍历的位数与逻辑块的位数相比较。

jl 1b:如果已遍历的位数小于逻辑块的位数,说明还没遍历完逻辑块,继续循环查找;否则就结束了,并没有找到空闲的逻辑块。

在找到第一个0位后,我们将对应的逻辑块给文件,并把这个位置1,表示该逻辑块已被使用。这个操作也是用汇编实现的。

// bitmap.c
#define clear_block(addr)   \
__asm__ __volatile__(       \
    "cld\n\t"               \
    "rep\n\t"               \
    "stosl"                 \
    :: "a"(0), "c"(BLOCK_SIZE / 4), "D"((long)(addr)))

#define set_bit(nr, addr) ({            \
register int res;                       \
__asm__ __volatile__(                   \
    "btsl %2,%3\n\t"                    \
    "setb %%al"                         \
    : "=a" (res)                        \
    : "0"(0), "r"(nr), "m"(*(addr)));   \
res; })

clear_block 会将文件缓冲区的数据全部设置为0。分配的逻辑块可能还保留有以前的数据,我们要把这些数据全部清理掉。

在 eax 中放入0,在 ecx 中放入BLOCK_SIZE / 4,在 edi 中放入文件缓冲区的起始地址。

cld rep stosb会将 edi 指向的地址中填入 eax 的数据。edi 递增,ecx 递减。若ecx等于0,扫描结束。

set_bit 会将0位置1。nr 是位号,addr 是逻辑块位图的起始地址。

将 res 存入 eax 中并赋值0。

btsl %2,%3的作用是将从 addr 开始的第 nr 位设置为1。

setb %%al是把 al 的值置为1。

介绍了内联汇编代码后,就继续说说 new_block 函数。我们要对逻辑块位图操作,就要先找到超级块,我们在 read_super 中将逻辑块位图读入了内存中

// bitmap.c
int new_block(int dev)
{
    struct buffer_head *bh;
    struct super_block *sb;
    int i, j;

    sb = get_super(dev);
    if (!sb)
        panic("trying to get new block from nonexistant device");
    j = 8192;
    for (i = 0; i < 8; i++) {
        bh = sb->s_zmap[i];
        if (bh) {
            j = find_first_zero(bh->b_data);
            if (j < 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;
    bh = getblk(dev, j);
    if (!bh)
        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;
}

第8-10行:找到设备的超级块。

第11-21行:找到逻辑块位图中的第一个0位。

第22-23行:找到0位对应的逻辑块号。但这里找到的不是真正的逻辑块号,毕竟 set_bit 最多会把 j 设置成8191,而文件系统中可能不止8192个逻辑块。

第25-27行:计算出真正的逻辑块号。逻辑块位图可能不止一个(虽然我们的文件系统只有一个),如果我们是在第二个逻辑块位图中找到0位,就需要加上第一个逻辑块位图的位数。数据区的0号逻辑块对应的是文件系统的 s_firstdatazone - 1 号逻辑块。

第28-36行:读取这个逻辑块,并清理旧数据。

最后返回这个空闲的逻辑块块号。

write 系统调用的部分完成了,是不是可以开始测试了?

先等等,我们先实现 lseek 系统调用。为什么还要写这个系统调用?

我发现,当调用 write 后,文件读写指针发生了改变,再调用 read 就读不了修改的内容。思来想去还是觉得得把 lseek 完成了。

//fs.h
#define IS_SEEKABLE(x) ((x) >= 1 && (x) <= 3)

// unistd.h
#define SEEK_SET    0   // 设置文件读写指针
#define SEEK_CUR    1   // 从当前文件读写指针做偏移
#define SEEK_END    2   // 从文件末尾做偏移

// read_write.c
int sys_lseek(unsigned int fd, off_t offset, int origin)
{
    struct file *file;
    int tmp;

    if (fd >= NR_OPEN)
        return -EBADF;
    file = current->filp[fd];
    if (!(file) || !(file->f_inode) || !IS_SEEKABLE(MAJOR(file->f_inode->i_dev)))
        return -EBADF;
    switch (origin) {
        case 0:
            if (offset < 0)
                return -EINVAL;
            file->f_pos = offset;
            break;
        case 1:
            if (file->f_pos + offset < 0)
                return -EINVAL;
            file->f_pos += offset;
            break;
        case 2:
            tmp = file->f_inode->i_size + offset;
            if (tmp < 0)
                return -EINVAL;
            file->f_pos = tmp;
            break;
        default:
            return -EINVAL;
    }
    return file->f_pos;
}

第15-18行:检查传入的参数是否有问题,文件所属设备是否支持 lseek。只有内存、软盘和硬盘才支持 lseek。

第21-25行:SEEK_SET 用于设置文件的读写指针。

第26-30行:SEEK_CUR 用来将文件读写指针加上 offset。

第31-36行:SEEK_END 用来将文件读写指针设置为文件末尾加上 offset。

最后更改 init 函数,做测试。

void init(void)
{
    int fd;
    static char buf[75] = {0};
    setup();
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    fd = open("/usr/root/hello.c", O_RDWR, 0);
    if (fd < 0) {
        printf("open file failed!\n");
        while (1);
    }
    write(fd, "#include ", 18);
    lseek(fd, 0, SEEK_SET);
    read(fd, buf, 74);
    printf("%s\n", buf);
    close(fd);
    while (1);
}

结果如下,可以看到,读取的内容确实发生了变化。如果我们把第14-15行代码注释掉,再运行一次,就会发现文件没有被修改。这是因为我们都只是对文件缓冲区进行修改,并没有将这些数据同步到软盘上。这就是我们下一节的内容了。

从零编写linux0.11 - 第十章 文件系统(二)_第1张图片

2.sync - 同步软盘数据

这一节实现 sync 系统调用,手动地同步文件内容。那为什么不弄成自动同步文件内容呢?好问题,自动同步我没整出来。

同步文件数据需要注意那些事情?

  1. 同步 inode 数据。在之前,我们只会修改 inode 结构体,并没有把结构体数据写回文件缓冲区。

  2. 同步文件缓冲区数据。把文件缓冲区数据写回软盘。

  3. inode 成员的互斥访问。之前,我们可以让多个进程同时修改一个 inode 结构体的值,但这是不对的,这会导致 inode 数据出现问题。

要互斥访问 inode,我们得设置与锁相关的变量。

struct m_inode {
    unsigned short i_mode;      // 文件的类型和属性(rwx)
    unsigned short i_uid;       // 文件所有者的用户id
    unsigned long i_size;       // 文件长度(字节)
    unsigned long i_mtime;      // 修改时间(从1970.1.1:0时算起,单位:秒)
    unsigned char i_gid;        // 文件所有者的组id
    unsigned char i_nlinks;     // 链接数
    unsigned short i_zone[9];   // 文件占用的逻辑块的块号
                                // zone[0]-zone[6]是直接块号
                                // zone[7]是一次间接块号
                                // zone[8]是二次间接块号
/* these are in memory also */
    struct task_struct *i_wait; // 等待访问inode的进程
    unsigned long i_atime;      // 最近访问文件的时间
    unsigned long i_ctime;      // 修改时间
    unsigned short i_dev;       // 设备号
    unsigned short i_num;       // inode号
    unsigned short i_count;     // 引用计数
    unsigned char i_lock;       // 互斥锁
    unsigned char i_dirt;       // 是否修改过
};

用 i_lock 记录 inode 是否被上锁,如果被上锁,就用 i_wait 记录当前进程的 pcb。当 inode 解锁时,通过 i_wait 唤醒进程。

// buffer.c
int sys_sync(void)
{
    int i;
    struct buffer_head *bh;

    sync_inodes();
    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++) {
        wait_on_buffer(bh); // 等待其他进程读取逻辑块结束
        if (bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    return 0;
}

可以看到,第8-13行代码会遍历所有的文件缓冲区,如果文件缓冲区被更改过,就将文件缓冲区写回软盘。只有在读取逻辑块时才会对文件缓冲区上锁,使用 wait_on_buffer 是为了等待读取结束。

sync_inodes 函数用于同步 inode 数据,这个操作之后才能同步文件缓冲区数据。

// inode.c
void sync_inodes(void)
{
    int i;
    struct m_inode *inode;

    inode = inode_table;
    for (i = 0; i < NR_INODE; i++, inode++) {
        wait_on_inode(inode);
        if (inode->i_dirt)
            write_inode(inode);
    }
}

这个函数会遍历所有读入内存的 inode,如果 inode 被修改过,就将它同步到文件缓冲区中。同样,wait_on_inode 会等待其他进程修改 inode。

// 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;
    }
    sb = get_super(inode->i_dev);
    if (!sb) {
        panic("trying to write inode without device");
    }
    block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
            (inode->i_num - 1) / INODES_PER_BLOCK;
    bh = bread(inode->i_dev, block);
    if (!bh) {
        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);
}

write_inode 会把 inode 数据写到文件缓冲区中,这时不能让其他进程修改 inode 的数据,使用 lock_inode 对 inode 上锁,修改完成后,使用 unlock_inode 解锁。

第9-12行:如果 inode 没有设置脏位或设备号为0,这个 inode 出现问题,无需同步。

第13-22行:获取设备超级块,找到 inode 所在的逻辑块号,并将该逻辑块读取到内存中。

第23-28行:同步 inode 数据到文件缓冲区,设置文件缓冲区的脏位,清除 inode 的脏位。

// inode.c
static inline void wait_on_inode(struct m_inode *inode)
{
    cli();
    while (inode->i_lock)
        sleep_on(&inode->i_wait);
    sti();
}

static inline void lock_inode(struct m_inode *inode)
{
    cli();
    while (inode->i_lock)
        sleep_on(&inode->i_wait);
    inode->i_lock = 1;
    sti();
}

static inline void unlock_inode(struct m_inode *inode)
{
    inode->i_lock = 0;
    wake_up(&inode->i_wait);
}

这三个函数与 wait_on_buffer、lock_buffer 和 unlock_buffer 相似。

当我们从内存读取 inode 数据或是将 inode 数据写入内存时,都需要上锁,防止数据被破环。

// inode.c
static void read_inode(struct m_inode *inode)
{
    struct super_block *sb;
    struct buffer_head *bh;
    int block;

    lock_inode(inode);		// 上锁
    sb = get_super(inode->i_dev);
    if (!sb)
        panic("trying to read inode without dev");
    block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks + (inode->i_num - 1) / INODES_PER_BLOCK;  // 计算inode所在的逻辑块
    bh = bread(inode->i_dev, block);
    if (!bh)
        panic("unable to read i-node block");
    __asm__("cld"::);
    *(struct d_inode *)inode = ((struct d_inode *)bh->b_data)[(inode->i_num - 1) % INODES_PER_BLOCK];
    brelse(bh);
    unlock_inode(inode);	// 解锁
}

当释放 inode 的时候,可以将 inode 同步到文件缓冲区中。

// inode.c
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_count > 1) {
        inode->i_count--;
        return;
    }
    if (inode->i_dirt) {
        write_inode(inode);
    }
    inode->i_count--;
}

如果没有其他进程在使用 inode,而且 inode 被修改过,那么我们就将 inode 数据同步到文件缓冲区中。

在 get_empty_inode 中也可以将 inode 同步到文件缓冲区中。

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);
        if (inode->i_dirt)
            write_inode(inode);
    } while (inode->i_count);
    memset(inode, 0, sizeof(*inode));
    inode->i_count = 1;
    return inode;
}

添加了第15行,当 i_count、i_dirt、i_lock 都为0时,才能把 inode 分配出去。

第26-28行也是新添加的,在使用 write_inode 之前使用 wait_on_inode 等待。write_inode 里面明明有 lock_inode,为什么要使用 wait_on_inode 呢?而且,i_wait 只能保存一个进程的 pcb,如果多个进程同时执行 wait_on_inode,一些进程不就死锁了吗?改成链表应该会更好。这个代码有点迷,暂时先这样吧,以后有问题再改,但愿不会是那种不可复现的问题。

最后修改一下 main.c。

// main.c
inline _syscall0(int, sync)
void init(void)
{
    int fd;
    static char buf[75] = {0};
    setup();
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    fd = open("/usr/root/hello.c", O_RDWR, 0);
    if (fd < 0) {
        printf("open file failed!\n");
        while (1);
    }
    write(fd, "#include ", 18);
    lseek(fd, 0, SEEK_SET);
    read(fd, buf, 74);
    printf("%s\n", buf);
    close(fd);
    sync();
    while (1);
}

运行看看结果。把第16和17行代码注释掉再跑一次,也会是如下的结果,文件确实同步到软盘中。

从零编写linux0.11 - 第十章 文件系统(二)_第2张图片

文件系统软盘被修改后,我一般是用 vscode 中 git 的放弃更改选项来复原文件系统,这样就可以持续使用了。

从零编写linux0.11 - 第十章 文件系统(二)_第3张图片

3.creat - 创建普通文件

创建普通文件的系统调用是 creat,它是通过 sys_open 实现的。

int sys_creat(const char *pathname, int mode)
{
    return sys_open(pathname, O_CREAT | O_TRUNC, mode);
}

传入 O_CREAT 代表如果没有该文件,就创建这个文件。

接下来的工作主要还是修改 sys_open 的流程。

  1. **找到文件所在目录的 inode。**这一步与之前一样。

  2. **通过目录的 inode 找到逻辑块,查找是否存在该文件。**如果找到了,那就和之前一样。

  3. 如果目录逻辑块中没找到该文件,就获取一个空闲的 inode 和逻辑块,并把 inode 号和文件名写入目录的逻辑块中,把逻辑块号写到 inode 中。

// namei.c
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;

    dir = dir_namei(pathname, &namelen, &basename);
    if (!dir)
        return -ENOENT;
    if (!namelen) {
        iput(dir);
        return -EISDIR;
    }
    
    bh = find_entry(&dir, basename, namelen, &de);
    if (!bh) {
        if (!(flag & O_CREAT)) {
            iput(dir);
            return -ENOENT; // No such file or directory
        }
        if (!permission(dir, MAY_WRITE)) {
            iput(dir);
            return -EACCES;	// Permission denied
        }
        inode = new_inode(dir->i_dev);
        if (!inode) {
            iput(dir);
            return -ENOSPC;	// No space left on device
        }
        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;
    }
    ...
}

O_ACCMODE 的值是3,O_WRONLY 的值是1,O_RDWR 的值是2。flag & O_ACCMODE是在判断对文件是否有写操作。如果 flag 中同时有 O_TRUNC 标志位以及 O_RDWR、O_WRONLY 这两个标志位的其中一个,就会把文件长度截断为0。

第10-11行:如果 flag 有 O_TRUNC 标志位,没有 O_RDWR、O_WRONLY 标志位,就给 flag 设置上 O_WRONLY 标志位。这样,我们通过 open(path, O_TRUNC, 0) 就能截断文件(将文件长度变为0)。

mode 参数只在创建文件时有用,是对新文件赋予的访问权限,umask 是文件创建权限屏蔽位。第12行代码会屏蔽掉一些权限。第13行代码指定文件类型为普通文件。

第24-54行:如果没有找到该文件,看看是否需要创建该文件。

第25-28行:如果 flag 没有 O_CREAT 标志位,不需要创建文件,返回错误号。

第29-32行:如果我们对目录没有写权限,就无法创建文件。

第33-37行:在 inode 位图上找到第一个0位,并找到对应的空闲 inode,还要在内存中找到一个空闲的 inode 用于存放软盘上的 inode 的数据。如果没有找到空闲的 inode,说明内存或软盘的 inode 已经用完了。

第38-40行:设置文件的用户,文件类型和对文件的访问权限。设置脏位,以标识该 inode 需要写到文件缓冲区中。

第41-48行:将文件名和 inode 号写到目录中。

// bitmap.c
struct m_inode *new_inode(int dev)
{
    struct m_inode *inode;
    struct super_block *sb;
    struct buffer_head *bh;
    int i, j;

    inode = get_empty_inode();  // 找到内存中空闲的 inode
    if (!inode)
        return NULL;
    sb = get_super(dev);
    if (!sb) {
        panic("new_inode with unknown device");
    }
    j = 8192;
    for (i = 0; i < 8; i++) {
        bh = sb->s_imap[i];
        if (bh) {
            j = find_first_zero(bh->b_data);
            if (j < 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;
}

new_inode 和 new_block 的逻辑有些类似。

第9-11行:找到内存中空闲的 inode。

第12-15行:获取设备的超级块。

第16-28行:找到第一个0位,如果没找到,说明软盘上的 inode 已经用完了。

第29-31行:把0位置1,表示已经被使用了。

第32-40行:设置文件缓冲区的脏位,该缓冲区需要写回软盘。初始化 inode 的成员。

// namei.c
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;

    if (namelen > NAME_LEN)
        namelen = NAME_LEN;
    if (!namelen)
        return NULL;
    block = dir->i_zone[0];
    if (!block)
        return NULL;
    bh = bread(dir->i_dev, block);
    if (!bh)
        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;
            bh = bread(dir->i_dev, block);
            if (!bh) {
                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;
}

// fs.h
#define DIR_ENTRIES_PER_BLOCK ((BLOCK_SIZE) / (sizeof(struct dir_entry)))

在 add_entry 中,我们必须考虑一种情况:如果目录只有一个逻辑块,有64个文件(一个逻辑块可以存放64个目录项),那么再添加一个文件的话,就需要为目录添加一个逻辑块,在新的逻辑块中写入文件名和 inode 号。

第11-14行:文件长度过长会被截断,文件长度为0就直接返回 NULL。

第15-20行:找到目录的第一个逻辑块,并将它读取到内存中。

第21-53行:遍历目录的逻辑块,找到未被使用的目录项,将文件名写到目录项中。如果所有的目录项都被使用,也不能分配新的逻辑块,就会执行到第54行。

第24-36行:如果遍历一个逻辑块仍未找到空闲的目录项,就读取下一个逻辑块。如果遍历目录的所有逻辑块仍没有找到,就为目录分配一个新的逻辑块。

第37-42行:如果已经遍历所有的目录项,我们会添加一个新的目录项,目录的大小会因此发生改变。目录大小发生改变,就要设置目录 inode 的脏位,需要将数据同步到文件缓冲区中。假如目录中有三个文件,如果删除其中一个,目录的大小不会改变。之后在目录中创建一个新的文件,会把被删除的文件的目录项作为新文件的目录项,目录的大小也不会改变。

第43-50行:目录项未被使用或原本的文件被删除的情况下,目录项的 inode 号为0,我们就在这个目录项中写入文件名。

最后来创建一个文件吧。

void init(void)
{
    int fd;
    static char buf[75] = {0};
    setup();
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    fd = open("/usr/root/main.c", O_RDWR | O_CREAT, 0);
    if (fd < 0) {
        printf("create file main.c failed!\n");
        while (1);
    }
    write(fd, "Hello World!", 12);
    lseek(fd, 0, SEEK_SET);
    read(fd, buf, 12);
    printf("file content: %s\n", buf);
    close(fd);
    sync();
    while (1);
}

虽然可以使用 creat 来创建文件,不过一般都是用 open 来创建文件。在 /usr/root/ 下创建一个名为 main.c 的文件,并向其中写入"Hello World!"。结果如下:

从零编写linux0.11 - 第十章 文件系统(二)_第4张图片

将上面的代码改成下面的样子,再次运行,结果相同。这说明我们确实创建了一个新文件。

void init(void)
{
    int fd;
    static char buf[75] = {0};
    setup();
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    fd = open("/usr/root/main.c", O_RDWR, 0);
    if (fd < 0) {
        printf("create file main.c failed!\n");
        while (1);
    }
    read(fd, buf, 12);
    printf("file content: %s\n", buf);
    close(fd);
    sync();
    while (1);
}

4.unlink - 删除文件

删除文件的系统调用是 unlink。要删除文件,我们应该做哪些工作呢?

  1. **将目录的目录项中文件 inode 号清零。**我们无需把文件名清零。
  2. **链接数为0时,将文件 inode 对应的 inode 位图中的位清零。**之后创建文件就可以使用这个 inode。
  3. **链接数为0时,将文件逻辑块对应的逻辑块位图中的位清零。**之后创建和扩大文件就可以使用这些逻辑块。

链接数代表有多少个目录项指向文件。文件的链接数至少为1,目录的链接数至少为2,链接数为0表示文件被删除。

// namei.c
int sys_unlink(const char *name)
{
    const char *basename;
    int namelen;
    struct m_inode *dir, *inode;
    struct buffer_head *bh;
    struct dir_entry *de;

    dir = dir_namei(name, &namelen, &basename);
    if (!dir)
        return -ENOENT; // No such file or directory
    if (!namelen) {     // 文件名长度不能为0
        iput(dir);
        return -ENOENT;
    }
    if (!permission(dir, MAY_WRITE)) {
        iput(dir);
        return -EPERM;  // Operation not permitted
    }
    bh = find_entry(&dir, basename, namelen, &de);
    if (!bh) {
        iput(dir);
        return -ENOENT;
    }
    inode = iget(dir->i_dev, de->inode);
    if (!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;
}

第10-12行:找到文件所在目录的 inode。

第17-20行:如果用户对目录没有写权限,不能删除文件。

第21-25行:找到文件对应的目录项,bh 是文件缓冲区,对应了目录项所在的逻辑块。

第26-31行:读取文件的 inode。

第32-39行:如果设置了S_ISVTX,则除非是所有者或者超级管理员,其他人无法删除或重命名文件夹及下面的文件。

第40-45行:unlink 只能删除文件,不能删除目录,删除目录要使用 rmdir 系统调用。

第46-50行:i_nlinks 如果为0表示文件已经被删除。我们不可能对已经删除的文件再删除一次,这明显是出现了什么问题。为了方便继续操作,将 i_nlinks 设置为1。

第51-53行:将目录项的 inode 设置为0,表示这个目录项已经空闲。因为对文件缓冲区做了修改,需要设置文件缓冲区的脏位。之后不会再使用该文件缓冲区,将其释放掉。

第54-57行:删除普通文件时,i_nlinks 减1。当 i_nlinks 为0时,我们需要将文件逻辑块对应的逻辑块位图中的位清零。这个操作放在 iput 进行。

// inode.c
void iput(struct m_inode *inode)
{
    ...
    if (!inode->i_nlinks) {
        truncate(inode);
        free_inode(inode);
        return;
    }
    if (inode->i_dirt) {
        write_inode(inode);
    }
    inode->i_count--;
}

truncate 会将文件逻辑块对应的逻辑块位图中的位清零。

free_inode 会将文件 inode 对应的 inode 位图中的位清零。

// bitmap.c
void free_inode(struct m_inode *inode)
{
    struct super_block *sb;
    struct buffer_head *bh;

    if (!inode)
        return;
    if (!inode->i_dev) {
        memset(inode, 0, sizeof(*inode));
        return;
    }
    if (inode->i_count > 1) {
        printk("trying to free inode with count=%d\n", inode->i_count);
        panic("free_inode");
    }
    if (inode->i_nlinks)
        panic("trying to free inode with links");
    sb = get_super(inode->i_dev);
    if (!sb)
        panic("trying to free inode on nonexistent device");
    if (inode->i_num < 1 || inode->i_num > sb->s_ninodes)
        panic("trying to free inode 0 or nonexistant inode");
    bh = sb->s_imap[inode->i_num >> 13];
    if (!bh) {
        panic("nonexistent imap in superblock");
    }
    if (clear_bit(inode->i_num & 8191, bh->b_data))
        printk("free_inode: bit already cleared.\n\r");
    bh->b_dirt = 1;
    memset(inode, 0, sizeof(*inode));
}

第13-16行:i_count 的值应该为1,不然在 iput 中就不会执行到 free_inode。

第17-18行:i_nlinks 的值应该为0,不然在 iput 中就不会执行到 free_inode。

第22-27行:检验 inode 号的取值范围是否正确。读取位图的逻辑块。

第28-29行:将 inode 对应的位清零。如果这个位原本就是零,那就有问题了。

第30行:因为修改了位图,而位图在文件缓冲区上,需要设置文件缓冲区的脏位。

第31行:将 inode 清空,一边反复使用。

// bitmap.c
#define clear_bit(nr, addr) ({          \
register int res;                       \
__asm__ __volatile__(                   \
    "btrl %2,%3\n\t"                    \
    "setnb %%al"                        \
    : "=a"(res)                         \
    : "0"(0), "r"(nr), "m"(*(addr)));   \
res; })

将 res 存放在 eax 中,并初始化为0。

btrl %2,%3将 addr 地址开始的第 nr 个位的值存储到 EFLAGS.CF 中,然后把该位置零。

setnb %%al用于根据 EFLAGS.CF 设置 al。如果 CF=1 则 al=0,否则 al=1。

// truncate.c
void truncate(struct m_inode *inode)
{
    int i;

    if (!(S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)))
        return;
    for (i = 0; i < 7; i++) // 直接逻辑块
        if (inode->i_zone[i]) {
            free_block(inode->i_dev, inode->i_zone[i]);
            inode->i_zone[i] = 0;
        }
    free_ind(inode->i_dev, inode->i_zone[7]);   // 一级间接逻辑块
    free_dind(inode->i_dev, inode->i_zone[8]);  // 二级间接逻辑块
    inode->i_zone[7] = inode->i_zone[8] = 0;
    inode->i_size = 0;  // 文件长度设置为0
    inode->i_dirt = 1;
    inode->i_mtime = inode->i_ctime = CURRENT_TIME;
}

对于直接逻辑块,我们可以利用 free_block 快速地将位图的位清零,但是对于一级间接逻辑块和二级间接逻辑块,我们需要进行遍历,把逻辑块对应的位一个一个清零。

// truncate.c
static void free_ind(int dev, int block)
{
    struct buffer_head *bh;
    unsigned short *p;
    int i;

    if (!block)
        return;
    bh = bread(dev, block);
    if (bh) {
        p = (unsigned short *)bh->b_data;
        for (i = 0; i < 512; i++, p++)
            if (*p)
                free_block(dev, *p);
        brelse(bh);
    }
    free_block(dev, block);
}

对于一级间接逻辑块,需要读取逻辑块,一个逻辑块号占2个字节,一个逻辑块中有512个逻辑块号,需要把所有的逻辑块的位都置为0。最后还要把一级间接逻辑块对应的位置为0。

// truncate.c
static void free_dind(int dev, int block)
{
    struct buffer_head *bh;
    unsigned short *p;
    int i;

    if (!block)
        return;
    bh = bread(dev, block);
    if (bh) {
        p = (unsigned short *)bh->b_data;
        for (i = 0; i < 512; i++, p++)
            if (*p)
                free_ind(dev, *p);
        brelse(bh);
    }
    free_block(dev, block);
}

对于二级间接逻辑块,需要调用 free_ind 将一级间接逻辑块及以下的逻辑块位置零,最后把二级间接逻辑块对应的位置为0。

// bitmap.c
void free_block(int dev, int block)
{
    struct super_block *sb;
    struct buffer_head *bh;

    sb = get_super(dev);
    if (!sb) {
        panic("trying to free block on nonexistent device");
    }
    if (block < sb->s_firstdatazone || block >= sb->s_nzones) {
        panic("trying to free block not in datazone");
    }
    bh = get_hash_table(dev, block);
    if (bh) {
        if (bh->b_count != 1) {
            printk("trying to free block (%04x:%d), count=%d\n",
                   dev, block, bh->b_count);
            return;
        }
        bh->b_dirt = 0;
        bh->b_uptodate = 0;
        brelse(bh);
    }
    block -= sb->s_firstdatazone - 1;
    if (clear_bit(block & 8191, sb->s_zmap[block / 8192]->b_data)) {
        printk("block (%04x:%d) ", dev, block + sb->s_firstdatazone - 1);
        panic("free_block: bit already cleared");
    }
    sb->s_zmap[block / 8192]->b_dirt = 1;
}

第11-13行:检查 block 参数是否合法。

第14行:如果逻辑块没有被读到文件缓冲区,get_hash_table 会返回 NULL;相反,则会返回一个地址。

第15-24行:如果逻辑块被读到文件缓冲区,复原 b_dirt 和 b_uptodate,表示文件缓冲区中无数据,最后释放 bh。

第25-29行:计算逻辑块在位图中的位置,清零该位。

第30行:因为修改了逻辑块位图,设置文件缓冲区的脏位。

最后修改 init 函数,进行测试。

// main.c
void init(void)
{
    int fd;
    setup();
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    unlink("/usr/root/hello.c");
    fd = open("/usr/root/hello.c", O_RDWR, 0);
    if (fd < 0) {
        if (errno & ENOENT)
            printf("No such file or directory\n");
    }
    sync();
    while (1);
}

第一次运行后,把第9行注释掉再次运行,可以得到一样的结果,说明我们确实删掉了 hello.c。

从零编写linux0.11 - 第十章 文件系统(二)_第5张图片

5.mkdir,rmdir - 增删目录

这一节是对目录的操作。增删目录与增删文件的操作差不多。

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;
    // 读取目标目录的上一级目录的inode
    dir = dir_namei(pathname, &namelen, &basename);
    if (!dir)
        return -ENOENT;
    if (!namelen) {	// 目录名长度不能为0
        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; // File exists
    }
    inode = new_inode(dir->i_dev);	// 为目录找到一个空闲inode
    if (!inode) {
        iput(dir);
        return -ENOSPC;
    }
    inode->i_mtime = inode->i_atime = CURRENT_TIME;
    inode->i_zone[0] = new_block(inode->i_dev);	// 为目录分配逻辑块
    if (!inode->i_zone[0]) {
        iput(dir);
        inode->i_nlinks--;
        iput(inode);
        return -ENOSPC;
    }
    dir_block = bread(inode->i_dev, inode->i_zone[0]);	// 将逻辑块读入内存
    if (!dir_block) {
        iput(dir);
        free_block(inode->i_dev, inode->i_zone[0]);
        inode->i_nlinks--;
        iput(inode);
        return -ERROR;
    }
    inode->i_size = 32;
    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;
}

第10-11行:只有超级用户能够创建目录,这很奇怪,因为我们平时不会使用sudo mkdir来创建目录,用户应该也能够创建目录才对。

第20-23行:需要对上一级目录有写权限。因为创建目录会更改上一级目录的目录项,必须有写权限。

第23-28行:如果找到了目标目录,bh 就是文件缓冲区的地址,这就说明系统中已经存在这个文件,我们不需要再创建了。

第50-56行:为什么要把文件大小设置成32字节呢?我们需要向刚创建的目录添加两个目录项,一个目录项16字节,两个就是32字节了。这两个目录项,一个是".“,代表当前目录,一个是”…",代表上一级目录。

第57行:i_nlinks 表示有多个目录项指向该文件。目录中有一个"."目录项指向自己,上一级目录有一个目录项指向该目录,所以是两个。

第64-71行:将目录添加到上一级目录的目录项中。如果添加失败,需要将分配的 inode 和逻辑块都重新置为空闲状态。

第72-75行:目录项添加成功后,对上一级目录做处理。

// namei.c
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;
	dir = dir_namei(name, &namelen, &basename);
	if (!dir)
		return -ENOENT;
	if (!namelen) {	// 目录名长度不能为0
		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;
	}
	inode = iget(dir->i_dev, de->inode);	// 找到目录的inode
	if (!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) {	// 不能使用"rmdir ."的形式删除目录
		iput(inode);
		iput(dir);
		brelse(bh);
		return -EPERM;
	}
	if (!S_ISDIR(inode->i_mode)) {	// 必须是目录
		iput(inode);
		iput(dir);
		brelse(bh);
		return -ENOTDIR;		// Not a directory
	}
	if (!empty_dir(inode)) {	// 目录中不能有文件
		iput(inode);
		iput(dir);
		brelse(bh);
		return -ENOTEMPTY;		// Directory not empty
	}
	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;
}

rmdir 和 mkdir 的前一部分是一样的。

第34-40行:如果设置了S_ISVTX,则除非是所有者或者超级管理员,其他人无法删除或重命名文件夹及下面的文件。

第47-52行:不能使用"rmdir ."的形式删除当前目录,但是可以通过"rmdir …/dir"的形式删除当前目录。

第65-66行:当目录为空时,指向该目录的目录项只有两个。

// namei.c
static int empty_dir(struct m_inode *inode)
{
    int nr, block;
    int len;
    struct buffer_head *bh;
    struct dir_entry *de;

    len = inode->i_size / sizeof(struct dir_entry);
    if (len < 2 || !inode->i_zone[0]) {
        printk("warning - bad directory on dev %04x\n", inode->i_dev);
        return 0;
    }
    bh = bread(inode->i_dev, inode->i_zone[0]);
    if (!bh) {
        printk("warning - bad directory on dev %04x\n", inode->i_dev);
        return 0;
    }
    de = (struct dir_entry *)bh->b_data;
    if (de[0].inode != inode->i_num || !de[1].inode ||
            strcmp(".", de[0].name) || strcmp("..", de[1].name)) {
        printk("warning - bad directory on dev %04x\n", inode->i_dev);
        return 0;
    }
    nr = 2;
    de += 2;
    while (nr < len) {
        if ((void *)de >= (void *)(bh->b_data + BLOCK_SIZE)) {
            brelse(bh);
            block = bmap(inode, nr / DIR_ENTRIES_PER_BLOCK);
            if (!block) {
                nr += DIR_ENTRIES_PER_BLOCK;
                continue;
            }
            bh = bread(inode->i_dev, block);
            if (!bh)
                return 0;
            de = (struct dir_entry *)bh->b_data;
        }
        if (de->inode) {
            brelse(bh);
            return 0;
        }
        de++;
        nr++;
    }
    brelse(bh);
    return 1;
}

第19-24行:第一个目录项是当前目录,第一个目录项 inode 号必须与当前目录的 inode 号相同,文件名是".“。第二个目录项是上一级目录,第二个目录项的 inode 号不能为0,文件名是”…"。有一个不对,就报错并返回。

第25-26行:我们从第3个目录项开始检查目录中是否存在文件。

第28-39行:当遍历完一个逻辑块后,使用 bmap 找到下一个逻辑块,并读入内存,继续循环检查。

这一节只需要看懂这三个函数,其他函数之前都讲过了。

最后修改 init 函数。

// main.c
inline _syscall2(int, mkdir, const char *, _path, mode_t, mode)
inline _syscall1(int, rmdir, const char *, pathname)
void init(void)
{
    int res;
    setup();
    open("/dev/tty0", O_RDWR, 0);
    dup(0);
    dup(0);
    res = mkdir("/sixsixsix", 0666);
    if (res < 0) {
        printf("mkdir failed!\n");
    }
    rmdir("/sixsixsix");
    sync();
    while (1);
}

运行结果为:

从零编写linux0.11 - 第十章 文件系统(二)_第6张图片

那怎么判断创建、删除目录是否成功呢?使用 bless 或 ghex 打开 rootimage,搜索 sixsixsix,如下所示。我们确实在根目录创建了 sixsixsix 目录。它的 inode 号为0,说明这个目录已经被删除了。

从零编写linux0.11 - 第十章 文件系统(二)_第7张图片

6.完善文件系统

最后来修复系统存在的 bug。

首先是 find_entry。find_entry 会在目录逻辑块中查找文件名。我们之前的代码只会查找第一个逻辑块,而不是查找目录的所有逻辑块。

// namei.c
static struct buffer_head *find_entry(struct m_inode **dir,
    const char *name, int namelen, struct dir_entry **res_dir)
{
    ...
    i = 0;
    de = (struct dir_entry *)bh->b_data;
    while (i < entries) {
        if ((char *)de >= BLOCK_SIZE + bh->b_data) {
            brelse(bh);
            bh = NULL;
            block = bmap(*dir, i / DIR_ENTRIES_PER_BLOCK);
            if (!block) {
                i += DIR_ENTRIES_PER_BLOCK;
                continue;
            }
            bh = bread((*dir)->i_dev, block);
            if (!bh) {
                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;
}

这次添加了第9-23行代码。第9行是在判断是否遍历完一个逻辑块,如果是,就读取目录的下一个逻辑块。bmap 函数会依次遍历目录的直接逻辑块和间接逻辑块,直至遍历完所有的目录项。

第二处在 open_namei 函数中。

int open_namei(const char *pathname, int flag, int mode, struct m_inode **res_inode)
{
    ...
    inode->i_atime = CURRENT_TIME;
    if (flag & O_TRUNC)
        truncate(inode);
    *res_inode = inode;
    return 0;
}

如果使用了 O_TRUNC 标志,调用 open 函数打开文件时会将文件原本的内容全部丢弃,文件大小变为 0。这就需要用到 truncate 函数,我们之前在删除文件的时候也用到了这个函数。它会把文件逻辑块对应逻辑块位图的位清零,并把 inode 的 i_size 设置为0。

如果文件缓冲区被用了怎么办?我们只能等待其他进程释放文件缓冲区,但是,选择等哪个文件缓冲区还是有讲究的。我们根据 b_dirt 和 b_lock 两个参数判断等待哪个文件缓冲区。

  1. 如果既没有设置脏位也没有上锁就好了,这种情况下进程很可能马上就释放文件缓冲区,等它准没错。
  2. 如果没有这种,就只能等上锁的,因为这种要么正在读取软盘,要么正在写回软盘,等待的时间虽然有点长,但是还是可以忍受的。
  3. 如果没有以上两种,那就只能等设置脏位的了,它必定需要写回软盘后才能给我们操作,等待的时间要比之前两种就得多。
  4. 最后就是既设置脏位,又被上锁的,简直是 buff 叠满,等待时间最长。

下面的 BADNESS 就体现了这一点,BADNESS 越小越好。

// buffer.c
#define BADNESS(bh) (((bh)->b_dirt << 1) + (bh)->b_lock)
struct buffer_head *getblk(int dev, int block)
{
    struct buffer_head *tmp, *bh;

repeat:
    bh = get_hash_table(dev, block);
    if (bh)
        return bh;
    tmp = free_list;
    do {
        if (tmp->b_count)
            continue;
        if (!bh || BADNESS(tmp) < BADNESS(bh)) {
            bh = tmp;
            if (!BADNESS(tmp))
                break;
        }
        tmp = tmp->b_next_free;
    } while (tmp != free_list);
    if (!bh) {
        sleep_on(&buffer_wait);
        goto repeat;
    }
    wait_on_buffer(bh);
    if (bh->b_count)
        goto repeat;
    while (bh->b_dirt) {
        sync_dev(bh->b_dev);
        wait_on_buffer(bh);
        if (bh->b_count)
            goto repeat;
    }
    if (find_buffer(dev, block))
        goto repeat;
    bh->b_count = 1;    // 读取一个逻辑块
    bh->b_dirt = 0;     // 数据还未被修改
    bh->b_uptodate = 0; // 还未加载数据
    remove_from_queues(bh); // 从链表中移除
    bh->b_dev = dev;
    bh->b_blocknr = block;
    insert_into_queues(bh); // 插入到链表末尾
    return bh;
}

如果 get_hash_table 返回 NULL,执行到第15行时,!bh判断为真后就不会判断逻辑或后面的语句,而是直接运行第16行代码。如果这个文件缓冲区既没有设置脏位也没有上锁,就跳出 while 循环。否则我们需要遍历所有的文件缓冲区,找到一个 BADNESS 最小的文件缓冲区。

出了 while 循环后,文件缓冲区未上锁的情况下才能继续执行,如果 b_count 不为0,说明有进程还在使用这个文件缓冲区,我们需要重新找一个。如果文件缓冲区设置了脏位,sync_dev 会将数据写回软盘,等待写回完成后,如果有进程在使用文件缓冲区,还是需要重新找一个。运行到第35行,如果逻辑块已经被读入到内存中,那又要重新选。咦?既然读到了内存中,直接用不就行了,为什么要重选?因为这是其他进程读入内存的,多个进程同时对同一个缓冲区进行更改,容易造成数据错乱。

当运行到37行时,我们终于找到了一个未上锁,未被更改,未被使用的文件缓冲区。

// buffer.c
int sync_dev(int dev)
{
    int i;
    struct buffer_head *bh;

    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++) {
        if (bh->b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev && bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    sync_inodes();
    bh = start_buffer;
    for (i = 0; i < NR_BUFFERS; i++, bh++) {
        if (bh->b_dev != dev)
            continue;
        wait_on_buffer(bh);
        if (bh->b_dev == dev && bh->b_dirt)
            ll_rw_block(WRITE, bh);
    }
    return 0;
}

这个函数中,首先将所有设置脏位的文件缓冲区写回软盘,再将 inode 同步到文件缓冲区,最后将文件缓冲区写回软盘。那为什么不干脆先把 inode 同步到文件缓冲区,再将文件缓冲区写回软盘?

这里执行两次同步操作是为了提高内核效率。我不太懂为什么能提高效率。让我动手写的话,我估计会把第7-14行删掉。

可以看到,只有当文件缓冲区用完的情况下,才会自动把文件缓冲区的数据同步到软盘。这功能有点鸡肋。

本章小节

我的博客写得不怎么好,有些东西自己解释不通,就只是讲解代码,而不是更具有启发式的理解。比如 wait_on_inode 这个函数,我只讲了它应该出现在哪里,但我解释不了它为什么在那里,其他地方为什么不需要用这个函数。

下一章是可执行文件加载,a.out 格式已经被淘汰了,与其学这个,不如学学 elf 格式的可执行文件。我准备把 linux1.2 的 execve 代码搬到 linux0.11 中,不知道多久能弄好。系统需要提供一个的静态链接库以供编译,这部分可以把 glibc 搬过来。另外还需要一些可执行文件,比如 sh,ls 等等,这部分可以用 busybox 的代码。

你可能感兴趣的:(linux0.11,操作系统,操作系统,linux)