1. 程式人生 > >我是如何學習寫一個作業系統(九):檔案系統

我是如何學習寫一個作業系統(九):檔案系統

前言

這個應該是這個系列的尾聲了,一個完整的作業系統可能最主要的也就是分成這幾大模組:程序管理、記憶體管理和檔案系統。計算機以程序為基本單位進行資源的排程和分配;而與使用者的互動,基本單位則是檔案

生磁碟

檔案正是對生磁碟的抽象

磁碟的組成

一個磁碟由多個盤面串聯而成,而一個盤面又被分為磁軌,磁軌又由扇區組成。

磁碟的訪問單元就是扇區,一個扇區為512位元組

磁碟的使用

  • CPU向磁碟的控制器發出一個指令
  • 控制器開始進行尋道、旋轉和傳輸
  • 最後完成後向CPU傳送一箇中斷

也就是向控制器傳送柱面磁頭扇區等資訊,然後等待迴應

盤塊

盤塊是對扇區的抽象

程式負責提高盤塊block,而磁碟驅動負責從block計算出cyl,head,sec(CHS),最後傳遞到磁碟控制器上

磁碟訪問時間 = 寫入控制器時間 + 尋道時間 + 旋轉時間 + 傳輸時間

其中主要的時間都在尋道時間和旋轉時間,所以對扇區抽象成盤塊就可以一次訪問多個扇區來節省磁碟的訪問時間

磁碟排程

既然有多程序交替執行,就有可能多個程序同時訪問相同磁碟的情況,就需要一個請求佇列來處理所有請求,就會涉及到排程演算法了

FCFS排程

FCFS是最公平最直觀的演算法,也就是按照佇列順序來訪問磁碟,但是效率也很低下,磁頭會在不規律的磁軌長途奔襲

SSTF排程

SSTF演算法就類似短作業優先演算法,先尋找更近距離的磁軌,但是SSTF演算法可能會產生飢餓問題,過長距離的磁軌可能一直得不到處理

SCAN排程

SCAN演算法就是SSTF演算法的改良版,也就是進行SSTF,但是在中途不會折返去尋找更短距離的磁軌,這樣就避免了飢餓問題

C-SCAN排程(電梯演算法)

把掃描限定在一個方向,當訪問到某個方向的最後一個磁軌時,磁軌返回磁碟相反方向磁軌的末端,並再次開始掃描。

檔案和檔案系統

檔案是對磁碟的第三層抽象,扇區和盤塊分別是前兩層抽象。之所以有檔案這層抽象是為了方便使用者的使用,在使用者的眼裡,磁碟上的資訊都可以看作是字元流。所以檔案的抽象九可以看作是字元流到盤塊集合的對映關係

檔案的邏輯結構

從檔案到盤塊的對映來看,一般有這幾種組織方式

  • 順序檔案

    記錄是定長的且按關鍵字順序排列。可以順序儲存或以連結串列形勢儲存,在訪問時需要順序搜尋檔案。順序檔案有以下兩種結構:

    1. 第一種是串結構,各記錄之間的順序與關鍵字無關。通常的辦法是由時間來決定,即按存入時間的先後排列,最先存入的記錄作為第一個記錄,其次存入的為第二個記錄,以此類推。

    2. 第二種是順序結構,指檔案中所有記錄按關鍵字順序排列。
      在對記錄進行批量操作時,即每次要讀或寫一大批記錄,對順序檔案的效率是所有邏輯檔案中最高的;此外,也只有順序檔案才能儲存在磁帶上,並能有效的工作。但順序檔案對查詢、修改、增加或刪除單個記錄的操作比較困難。

  • 索引檔案

    對於可變長記錄的檔案只能順序查詢,系統開銷較大,為此可以建立一張索引表以加快檢索速度,索引表本身是順序檔案。在記錄很多或是訪問要求高的檔案中,需要引入索引以提供有效的訪問,實際中,通過索引可以成百上千倍的提高訪問速度。

  • 索引順序表

    索引順序表是順序和索引兩種組織形勢的結合。索引順序檔案將順序檔案中所有記錄分為若干個組,為順序檔案建立一張索引表,在索引表中為每組中的第一個記錄建立一個索引項,其中含有該記錄的關鍵字值和指向該記錄的指標。

在實際的作業系統實現中,一般是採用多級索引

目錄和檔案系統

檔案系統或者說目錄是對磁碟的第四個抽象,也就是抽象了整個磁碟

作業系統為了實現檔案目錄,引入了稱為檔案控制塊的資料結構。

檔案控制塊。

檔案控制塊(FCB)是用來存放控制檔案需要的各種資訊的資料結構,以實現“按名存取”。FCB的有序集合稱為檔案目錄,一個FCB就是一個檔案目錄項。為了建立一個新檔案,系統將分配一個FCB並存放在檔案目錄中,稱為目錄項。

FCB主要包含以下資訊:

  1. 基本資訊,如檔名、檔案的物理位置、檔案的邏輯結構、檔案的物理結構等。
  2. 存取控制資訊,如檔案的存取許可權等。
  3. 使用資訊,如檔案建立時間、修改時間等。

檔案目錄樹

在多級目錄下一般對磁碟就可以抽象為

  • FCB陣列

    FCB陣列就是將所有盤塊的FCB資訊都集中到一個數組中

  • 資料盤塊集合

    在每個資料盤塊裡都包含一些目錄項用來找到子目錄,目錄項也就是檔名+對應的FCB的“地址”,也就是去之前的FCB陣列中找到相應的FCB

在磁碟進行格式化的時候,會存放一些資訊用來知道一些磁碟資訊和找到根目錄

  • inode點陣圖: 哪些inode空閒,哪些被佔用

  • 超級塊:記錄兩個點陣圖有多大等資訊

  • 盤塊點陣圖: 哪些盤塊是空閒的,硬碟大小不同這個點陣圖的大小也不同

檔案的實現

在我之前實現的FragileOS裡檔案系統非常簡陋,基本沒有什麼好說的。這其實也是為什麼之前把這個系列改了一個方向來結合的看Linux0.11的程式碼。所以來看一下Linux0.11裡是怎麼使用和實現檔案系統的,

讀取檔案

  • 這是讀取檔案的系統呼叫
  • 函式首先對引數有效性進行判斷
  • 之後對檔案的型別進行判斷
  • 如果是目錄檔案或者是常規檔案就執行讀取操作
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節點和檔案結構,讀取檔案中資料。
  • 首先判斷引數的有效性
  • 之後迴圈的呼叫bread來讀取資料
  • 之後複製chars位元組到使用者緩衝區buf中
  • 最後是修改該i節點的訪問時間為當前時間和返回讀取的位元組數
int file_read(struct m_inode * inode, struct file * filp, char * buf, int count)
{
    int left,chars,nr;
    struct buffer_head * bh;

    if ((left=count)<=0)
        return 0;
    while (left) {
        if ((nr = bmap(inode,(filp->f_pos)/BLOCK_SIZE))) {
            if (!(bh=bread(inode->i_dev,nr)))
                break;
        } else
            bh = NULL;
        nr = filp->f_pos % BLOCK_SIZE;
        chars = MIN( BLOCK_SIZE-nr , left );
        filp->f_pos += chars;
        left -= chars;
        if (bh) {
            char * p = nr + bh->b_data;
            while (chars-->0)
                put_fs_byte(*(p++),buf++);
            brelse(bh);
        } else {
            while (chars-->0)
                put_fs_byte(0,buf++);
        }
    }
    inode->i_atime = CURRENT_TIME;
    return (count-left)?(count-left):-ERROR;
}

檔案寫入

  • 根據i節點和檔案結構資訊,將使用者資料寫入檔案中
  • 首先確定資料寫入檔案的位置
  • 然後算出對應的盤塊
  • 然後使用者緩衝區buf中複製c個位元組到告訴緩衝塊中p指向的開始位置處
  • 最後是修改該i節點的訪問時間為當前時間和返回讀取的位元組數
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;

/*
 * ok, append may not work when many processes are writing at the same time
 * but so what. That way leads to madness anyway.
 */
    if (filp->f_flags & O_APPEND)
        pos = inode->i_size;
    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;
        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);
}

檔案目錄的實現

開啟建立檔案

  • 首先對引數進行處理,然後搜尋程序結構中檔案結構指標陣列一個空閒的檔案控制代碼
  • 接著為開啟檔案在檔案表中尋找一個空閒結構項
  • 然後呼叫函式open_namei()執行開啟操作,若返回值小於0,則說明出錯,就釋放剛申請到的檔案結構
  • 然後為不同的檔案型別做一些特殊的處理
  • 最後初始化開啟檔案的檔案結構,然後返回檔案控制代碼
int sys_open(const char * filename,int flag,int mode)
{
    struct m_inode * inode;
    struct file * f;
    int i,fd;

    mode &= 0777 & ~current->umask;
    for(fd=0 ; fd<NR_OPEN ; fd++)
        if (!current->filp[fd])
            break;
    if (fd>=NR_OPEN)
        return -EINVAL;
    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;
    (current->filp[fd]=f)->f_count++;
    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);
}

解析目錄

  • 先根據當前路徑的第一個字元來判斷當前路徑是絕對路徑還是相對路徑
  • 然後進迴圈處理過程,分割每個目錄名
  • 得到這個目錄名後,呼叫查詢目錄項函式find_entry()在當前處理的目錄中尋找指定名稱的目錄
  • 如果找到這個目錄,就設定一些資訊,然後繼續以該目錄項為當前目錄繼續迴圈處理路徑名中的下一目錄名部分(或檔名)
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;
    }
}