Linux 塊設備驅動 (1)

1. 背景

Sampleblk 是一個用於學習目的的 Linux 塊設備驅動項目。其中 day1 的源代碼實現了一個最簡的塊設備驅動,源代碼只有 200 多行。本文主要圍繞這些源代碼,討論 Linux 塊設備驅動開發的基本知識。html

開發 Linux 驅動須要作一系列的開發環境準備工做。Sampleblk 驅動是在 Linux 4.6.0 下開發和調試的。因爲在不一樣 Linux 內核版本的通用 block 層的 API 有很大變化,這個驅動在其它內核版本編譯可能會有問題。開發,編譯,調試內核模塊須要先準備內核開發環境,編譯內核源代碼。這些基礎的內容互聯網上隨處可得,本文再也不贅述。linux

此外,開發 Linux 設備驅動的經典書籍當屬 Device Drivers, Third Edition 簡稱 LDD3。該書籍是免費的,能夠自由下載並按照其規定的 License 從新分發。git

2. 模塊初始化和退出

Linux 驅動模塊的開發遵照 Linux 爲模塊開發者提供的基本框架和 API。LDD3 的 hello world 模塊提供了寫一個最簡內核模塊的例子。而 Sampleblk 塊驅動的模塊與之相似,實現了 Linux 內核模塊所必需的模塊初始化和退出函數,github

module_init(sampleblk_init);
module_exit(sampleblk_exit);

與 hello world 模塊不一樣的是,Sampleblk 驅動的初始化和退出函數要實現一個塊設備驅動程序所必需的基本功能。本節主要針對這部份內容作詳細說明。數組

2.1 sampleblk_init

概括起來,sampleblk_init 函數爲完成塊設備驅動的初始化,主要作了如下幾件事情,bash

2.1.1 塊設備註冊

調用 register_blkdev 完成 major number 的分配和註冊,函數原型以下,markdown

int register_blkdev(unsigned int major, const char *name);

Linux 內核爲塊設備驅動維護了一個全局哈希表 major_names這個哈希表的 bucket 是 [0..255] 的整數索引的指向 blk_major_name 的結構指針數組。數據結構

static struct blk_major_name {
    struct blk_major_name *next;
    int major;
    char name[16];
} *major_names[BLKDEV_MAJOR_HASH_SIZE];

register_blkdevmajor 參數不爲 0 時,其實現就嘗試在這個哈希表中尋找指定的 major 對應的 bucket 裏的空閒指針,分配一個新的blk_major_name,按照指定參數初始化 majorname。假如指定的 major 已經被別人佔用(指針非空),則表示 major 號衝突,反回錯誤。框架

major 參數爲 0 時,則由內核從 [1..255] 的整數範圍內分配一個未使用的反回給調用者。所以,雖然 Linux 內核的主設備號 (Major Number) 是 12 位的,不指定 major 時,仍舊從 [1..255] 範圍內分配。異步

Sampleblk 驅動經過指定 major 爲 0,讓內核爲其分配和註冊一個未使用的主設備號,其代碼以下,

sampleblk_major = register_blkdev(0, "sampleblk");
if (sampleblk_major < 0)
    return sampleblk_major;
2.1.2 驅動狀態數據結構的分配和初始化

一般,全部 Linux 內核驅動都會聲明一個數據結構來存儲驅動須要頻繁訪問的狀態信息。這裏,咱們爲 Sampleblk 驅動也聲明瞭一個,

struct sampleblk_dev {
    int minor;
    spinlock_t lock;
    struct request_queue *queue;
    struct gendisk *disk;
    ssize_t size;
    void *data;
};

爲了簡化實現和方便調試,Sampleblk 驅動暫時只支持一個 minor 設備號,而且能夠用如下全局變量訪問,

struct sampleblk_dev *sampleblk_dev = NULL;

下面的代碼分配了 sampleblk_dev 結構,而且給結構的成員作了初始化,

sampleblk_dev = kzalloc(sizeof(struct sampleblk_dev), GFP_KERNEL);
if (!sampleblk_dev) {
    rv = -ENOMEM;
    goto fail;
}

sampleblk_dev->size = sampleblk_sect_size * sampleblk_nsects;
sampleblk_dev->data = vmalloc(sampleblk_dev->size);
if (!sampleblk_dev->data) {
    rv = -ENOMEM;
    goto fail_dev;
}
sampleblk_dev->minor = minor;
2.1.3 Request Queue 初始化

使用 blk_init_queue 初始化 Request Queue 須要先聲明一個所謂的策略 (Strategy) 回調和保護該 Request Queue 的自旋鎖。而後將該策略回調的函數指針和自旋鎖指針作爲參數傳遞給該函數。

在 Sampleblk 驅動裏,就是 sampleblk_request 函數和 sampleblk_dev->lock

spin_lock_init(&sampleblk_dev->lock);
sampleblk_dev->queue = blk_init_queue(sampleblk_request,
    &sampleblk_dev->lock);
if (!sampleblk_dev->queue) {
    rv = -ENOMEM;
    goto fail_data;
}

策略函數 sampleblk_request 用於執行塊設備的 read 和 write IO 操做,其主要的入口參數就是 Request Queue 結構:struct request_queue。關於策略函數的具體實現咱們稍後介紹。

當執行 blk_init_queue 時,其內部實現會作以下的處理,

  1. 從內存中分配一個 struct request_queue 結構。
  2. 初始化 struct request_queue 結構。對調用者來講,其中如下部分的初始化格外重要,
    • blk_init_queue 指定的策略函數指針會賦值給 struct request_queuerequest_fn 成員。
    • blk_init_queue 指定的自旋鎖指針會賦值給 struct request_queuequeue_lock 成員。
    • 與這個request_queue 關聯的 IO 調度器的初始化。

Linux 內核提供了多種分配和初始化 Request Queue 的方法,

  • blk_mq_init_queue 主要用於使用多隊列技術的塊設備驅動
  • blk_alloc_queueblk_queue_make_request 主要用於繞開內核支持的 IO 調度器的合併和排序,使用自定義的實現。
  • blk_init_queue 則使用內核支持的 IO 調度器,驅動只專一於策略函數的實現。

Sampleblk 驅動屬於第三種狀況。這裏再次強調一下:若是塊設備驅動須要使用標準的 IO 調度器對 IO 請求進行合併或者排序時,必需使用 blk_init_queue 來分配和初始化 Request Queue.

2.1.4 塊設備操做函數表初始化

Linux 的塊設備操做函數表 block_device_operations 定義在 include/linux/blkdev.h 文件中。塊設備驅動能夠經過定義這個操做函數表來實現對標準塊設備驅動操做函數的定製。
若是驅動沒有實現這個操做表定義的方法,Linux 塊設備層的代碼也會按照塊設備公共層的代碼缺省的行爲工做。

Sampleblk 驅動雖然聲明瞭本身的 open, release, ioctl 方法,但這些方法對應的驅動函數內都沒有作實質工做。所以實際的塊設備操做時的行爲是由塊設備公共層來實現的,

static const struct block_device_operations sampleblk_fops = {
    .owner = THIS_MODULE,
    .open = sampleblk_open,
    .release = sampleblk_release,
    .ioctl = sampleblk_ioctl,
};
2.1.5 磁盤建立和初始化

Linux 內核使用 struct gendisk 來抽象和表示一個磁盤。也就是說,塊設備驅動要支持正常的塊設備操做,必需分配和初始化一個 struct gendisk

首先,使用 alloc_disk 分配一個 struct gendisk

disk = alloc_disk(minor);
if (!disk) {
    rv = -ENOMEM;
    goto fail_queue;
}
sampleblk_dev->disk = disk;

而後,初始化 struct gendisk 的重要成員,尤爲是塊設備操做函數表,Rquest Queue,和容量設置。最終調用 add_disk 來讓磁盤在系統內可見,觸發磁盤熱插拔的 uevent。

disk->major = sampleblk_major;
disk->first_minor = minor;
disk->fops = &sampleblk_fops;
disk->private_data = sampleblk_dev;
disk->queue = sampleblk_dev->queue;
sprintf(disk->disk_name, "sampleblk%d", minor);
set_capacity(disk, sampleblk_nsects);
add_disk(disk);

2.2 sampleblk_exit

這是個 sampleblk_init 的逆過程,

  • 刪除磁盤

    del_gendiskadd_disk 的逆過程,讓磁盤在系統中再也不可見,觸發熱插拔 uevent。

    del_gendisk(sampleblk_dev->disk);

  • 中止並釋放塊設備 IO 請求隊列

    blk_cleanup_queueblk_init_queue 的逆過程,但其在釋放 struct request_queue 以前,要把待處理的 IO 請求都處理掉。

    blk_cleanup_queue(sampleblk_dev->queue);

    blk_cleanup_queue 把全部 IO 請求所有處理完時,會標記這個隊列立刻要被釋放,這樣能夠阻止 blk_run_queue 繼續調用塊驅動的策略函數,繼續執行 IO 請求。Linux 3.8 以前,內核在 blk_run_queueblk_cleanup_queue 同時執行時有嚴重 bug。最近在一個有磁盤 IO 時的 Surprise Remove 的壓力測試中發現了這個 bug (老實說,有些驚訝,這個 bug 存在這麼久一直沒人發現)。

  • 釋放磁盤

    put_diskalloc_disk 的逆過程。這裏 gendisk 對應的 kobject 引用計數變爲零,完全釋放掉 gendisk

    put_disk(sampleblk_dev->disk);

  • 釋放數據區

    vfreevmalloc 的逆過程。

    vfree(sampleblk_dev->data);

  • 釋放驅動全局數據結構。

    freekzalloc 的逆過程。

    kfree(sampleblk_dev);

  • 註銷塊設備。

    unregister_blkdevregister_blkdev 的逆過程。

    unregister_blkdev(sampleblk_major, 「sampleblk」);

3. 策略函數實現

理解塊設備驅動的策略函數實現,必需先對 Linux IO 棧的關鍵數據結構有所瞭解。

3.1 struct request_queue

塊設備驅動待處理的 IO 請求隊列結構。若是該隊列是利用blk_init_queue 分配和初始化的,則該隊裏內的 IO 請求( struct request )須要通過 IO 調度器的處理(排序或合併),由 blk_queue_bio 觸發。

當塊設備策略驅動函數被調用時,request 是經過其 queuelist 成員連接在 struct request_queuequeue_head 鏈表裏的。一個 IO 申請隊列上會有不少個 request 結構。

3.2 struct bio

一個 bio 邏輯上表明瞭上層某個任務對通用塊設備層發起的 IO 請求。來自不一樣應用,不一樣上下文的,不一樣線程的 IO 請求在塊設備驅動層被封裝成不一樣的 bio 數據結構。

同一個 bio 結構的數據是由塊設備上從起始扇區開始的物理連續扇區組成的。因爲在塊設備上連續的物理扇區在內存中沒法保證是物理內存連續的,所以纔有了段 (Segment)的概念。在 Segment 內部的塊設備的扇區是物理內存連續的,但 Segment 之間卻不能保證物理內存的連續性。Segment 長度不會超過內存頁大小,並且老是扇區大小的整數倍。

下圖清晰的展示了扇區 (Sector),塊 (Block) 和段 (Segment) 在內存頁 (Page) 內部的佈局,以及它們之間的關係(注:圖截取自 Understand Linux Kernel 第三版,版權歸原做者全部),

Segment block sector layout in a page

所以,一個 Segment 能夠用 [page, offset, len] 來惟一肯定。一個 bio 結構能夠包含多個 Segment。而 bio 結構經過指向 Segment 的指針數組來表示了這種一對多關係。

struct bio 中,成員 bi_io_vec 就是前文所述的「指向 Segment 的指針數組」 的基地址,而每一個數組的元素就是指向 struct bio_vec 的指針。

struct bio {

    [...snipped..]

    struct bio_vec      *bi_io_vec; /* the actual vec list */

    [...snipped..]
}

struct bio_vec 就是描述一個 Segment 的數據結構,

struct bio_vec {
    struct page *bv_page;       /* Segment 所在的物理頁的 struct page 結構指針 */
    unsigned int    bv_len;     /* Segment 長度,扇區整數倍 */
    unsigned int    bv_offset;  /* Segment 在物理頁內起始的偏移地址 */
};

struct bio 中的另外一個成員 bi_vcnt 用來描述這個 bio 裏有多少個 Segment,即指針數組的元素個數。一個 bio 最多包含的 Segment/Page 數是由以下內核宏定義決定的,

#define BIO_MAX_PAGES       256

多個 bio 結構能夠經過成員 bi_next 連接成一個鏈表。bio 鏈表能夠是某個作 IO 的任務 task_struct 成員 bio_list 所維護的一個鏈表。也能夠是某個 struct request 所屬的一個鏈表(下節內容)。

下圖展示了 bio 結構經過 bi_next 連接組成的鏈表。其中的每一個 bio 結構和 Segment/Page 存在一對多關係 (注:圖截取自 Professional Linux Kernel Architecture,版權歸原做者全部),

bio list and page vectors

3.3 struct request

一個 request 邏輯上表明瞭塊設備驅動層收到的 IO 請求。該 IO 請求的數據在塊設備上是從起始扇區開始的物理連續扇區組成的。

struct request 裏能夠包含不少個 struct bio,主要是經過 bio 結構的 bi_next 連接成一個鏈表。這個鏈表的第一個 bio 結構,則由 struct requestbio 成員指向。
而鏈表的尾部則由 biotail 成員指向。

通用塊設備層接收到的來自不一樣線程的 bio 後,一般根據狀況選擇以下兩種方案之一,

  • bio 合併入已有的 request

    blk_queue_bio 會調用 IO 調度器作 IO 的合併 (merge)。多個 bio 可能所以被合併到同一個 request 結構裏,組成一個 request 結構內部的 bio 結構鏈表。因爲每一個 bio 結構都來自不一樣的任務,所以 IO 請求合併只能在 request 結構層面經過鏈表插入排序完成,原有的 bio 結構內部不會被修改。

  • 分配新的 request

    若是 bio 不能被合併到已有的 request 裏,通用塊設備層就會爲這個 bio 構造一個新 request 而後插入到 IO 調度器內部的隊列裏。待上層任務經過 blk_finish_plug 來觸發 blk_run_queue 動做,塊設備驅動的策略函數 request_fn 會觸發 IO 調度器的排序操做,將 request 排序插入塊設備驅動的 IO 請求隊列。

不論以上哪一種狀況,通用塊設備的代碼將會調用塊驅動程序註冊在 request_queuerequest_fn 回調,這個回調裏最終會將合併或者排序後的 request 交由驅動的底層函數來作 IO 操做。

3.4 策略函數 request_fn

如前所述,當塊設備驅動使用 blk_run_queue 來分配和初始化 request_queue 時,這個函數也須要驅動指定自定義的策略函數 request_fn 和所需的自旋鎖 queue_lock。驅動實現本身的 request_fn 時,須要瞭解以下特色,

  • 當通用塊層代碼調用 request_fn 時,內核已經拿了這個 request_queuequeue_lock。所以,此時的上下文是 atomic 上下文。在驅動的策略函數退出 queue_lock 以前,須要遵照內核在 atomic 上下文的約束條件。

  • 進入驅動策略函數時,通用塊設備層代碼可能會同時訪問 request_queue。爲了減小在 request_queuequeue_lock 上的鎖競爭, 塊驅動策略函數應該儘早退出 queue_lock,而後在策略函數返回前從新拿到鎖。

  • 策略函數是異步執行的,不處在用戶態進程所對應的內核上下文。所以實現時不能假設策略函數運行在用戶進程的內核上下文中。

Sampleblk 的策略函數是 sampleblk_request,經過 blk_init_queue 註冊到了 request_queuerequest_fn 成員上。

static void sampleblk_request(struct request_queue *q)
{
    struct request *rq = NULL;
    int rv = 0;
    uint64_t pos = 0;
    ssize_t size = 0;
    struct bio_vec bvec;
    struct req_iterator iter;
    void *kaddr = NULL;

    while ((rq = blk_fetch_request(q)) != NULL) {
        spin_unlock_irq(q->queue_lock);

        if (rq->cmd_type != REQ_TYPE_FS) {
            rv = -EIO;
            goto skip;
        }

        BUG_ON(sampleblk_dev != rq->rq_disk->private_data);

        pos = blk_rq_pos(rq) * sampleblk_sect_size;
        size = blk_rq_bytes(rq);
        if ((pos + size > sampleblk_dev->size)) {
            pr_crit("sampleblk: Beyond-end write (%llu %zx)\n", pos, size);
            rv = -EIO;
            goto skip;
        }

        rq_for_each_segment(bvec, rq, iter) {
            kaddr = kmap(bvec.bv_page);

            rv = sampleblk_handle_io(sampleblk_dev,
                pos, bvec.bv_len, kaddr + bvec.bv_offset, rq_data_dir(rq));
            if (rv < 0)
                goto skip;

            pos += bvec.bv_len;
            kunmap(bvec.bv_page);
        }
skip:

        blk_end_request_all(rq, rv);

        spin_lock_irq(q->queue_lock);
    }
}

策略函數 sampleblk_request 的實現邏輯以下,

  1. 使用 blk_fetch_request 循環獲取隊列中每個待處理 request
    內核函數 blk_fetch_request 能夠返回 struct request_queuequeue_head 隊列的第一個 request 的指針。而後再調用 blk_dequeue_request 從隊列裏摘除這個 request
  2. 每拿到一個 request,當即退出鎖 queue_lock,但處理完每一個 request,須要再次得到 queue_lock
  3. REQ_TYPE_FS 用來檢查是不是一個來自文件系統的 request。本驅動不支持非文件系統 request
  4. blk_rq_pos 能夠返回 request 的起始扇區號,而 blk_rq_bytes 返回整個 request 的字節數,應該是扇區的整數倍。
  5. rq_for_each_segment 這個宏定義用來循環迭代遍歷一個 request 裏的每個 Segment: 即 struct bio_vec。注意,每一個 Segment 即 bio_vec 都是以 blk_rq_pos 爲起始扇區,物理扇區連續的的。Segment 之間只是物理內存不保證連續而已。
  6. 每個 struct bio_vec 均可以利用 kmap 來得到這個 Segment 所在頁的虛擬地址。利用 bv_offsetbv_len 能夠進一步知道這個 segment 的確切頁內偏移和具體長度。
  7. rq_data_dir 能夠獲知這個 request 的請求是 read 仍是 write。
  8. 處理完畢該 request 以後,必需調用 blk_end_request_all 讓塊通用層代碼作後續處理。

驅動函數 sampleblk_handle_io 把一個 request的每一個 segment 都作一次驅動層面的 IO 操做。調用該驅動函數前,起始扇區地址 pos長度 bv_len, 起始扇區虛擬內存地址 kaddr + bvec.bv_offset,和 read/write 都作爲參數準備好。因爲 Sampleblk 驅動只是一個 ramdisk 驅動,所以,每一個 segment 的 IO 操做都是 memcpy 來實現的,

/*
 * Do an I/O operation for each segment
 */
static int sampleblk_handle_io(struct sampleblk_dev *sampleblk_dev,
        uint64_t pos, ssize_t size, void *buffer, int write)
{
    if (write)
        memcpy(sampleblk_dev->data + pos, buffer, size);
    else
        memcpy(buffer, sampleblk_dev->data + pos, size);

    return 0;
}

4. 試驗

4.1 編譯和加載

  • 首先,須要下載內核源代碼,編譯和安裝內核,用新內核啓動。

    因爲本驅動是在 Linux 4.6.0 上開發和調試的,並且塊設備驅動內核函數不一樣內核版本變更很大,最好去下載 Linux mainline 源代碼,而後 git checkout 到版本 4.6.0 上編譯內核。編譯和安裝內核的具體步驟網上有不少介紹,這裏請讀者自行解決。

  • 編譯好內核後,在內核目錄,編譯驅動模塊。

    $ make M=/ws/lktm/drivers/block/sampleblk/day1

  • 驅動編譯成功,加載內核模塊

    $ sudo insmod /ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko

  • 驅動加載成功後,使用 crash 工具,能夠查看 struct smapleblk_dev 的內容,

    crash7> mod -s sampleblk /home/yango/ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko
    MODULE NAME SIZE OBJECT FILE
    ffffffffa03bb580 sampleblk 2681 /home/yango/ws/lktm/drivers/block/sampleblk/day1/sampleblk.ko

    crash7> p *sampleblk_dev
    $4 = {
    minor = 1,
    lock = {
    {
    rlock = {
    raw_lock = {
    val = {
    counter = 0
    }
    }
    }
    }
    },
    queue = 0xffff880034ef9200,
    disk = 0xffff880000887000,
    size = 524288,
    data = 0xffffc90001a5c000
    }

注:關於 Linux Crash 的使用,請參考延伸閱讀。

4.2 模塊引用問題解決

問題:把驅動的 sampleblk_request 函數實現所有刪除,從新編譯和加載內核模塊。而後用 rmmod 卸載模塊,卸載會失敗, 內核報告模塊正在被使用。

使用 strace 能夠觀察到 /sys/module/sampleblk/refcnt 非零,即模塊正在被使用。

$ strace rmmod sampleblk
execve("/usr/sbin/rmmod", ["rmmod", "sampleblk"], [/* 26 vars */]) = 0

................[snipped]..........................

openat(AT_FDCWD, "/sys/module/sampleblk/holders", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 2 entries */, 32768)     = 48
getdents(3, /* 0 entries */, 32768)     = 0
close(3)                                = 0
open("/sys/module/sampleblk/refcnt", O_RDONLY|O_CLOEXEC) = 3    /* 顯示引用數爲 3 */
read(3, "1\n", 31)                      = 2
read(3, "", 29)                         = 0
close(3)                                = 0
write(2, "rmmod: ERROR: Module sampleblk i"..., 41rmmod: ERROR: Module sampleblk is in use
) = 41
exit_group(1)                           = ?
+++ exited with 1 +++

若是用 lsmod 命令查看,能夠看到模塊的引用計數確實是 3,但沒有顯示引用者的名字。通常狀況下,只有內核模塊間的相互引用纔有引用模塊的名字,因此沒有引用者的名字,那麼引用者來自用戶空間的進程。

那麼,到底是誰在使用 sampleblk 這個剛剛加載的驅動呢?利用 module:module_get tracepoint,就能夠獲得答案了。從新啓動內核,在加載模塊前,運行 tpoint 命令。而後,再運行 insmod 來加載模塊。

$ sudo ./tpoint module:module_get
Tracing module:module_get. Ctrl-C to end.

   systemd-udevd-2986  [000] ....   196.382796: module_get: sampleblk call_site=get_disk refcnt=2
   systemd-udevd-2986  [000] ....   196.383071: module_get: sampleblk call_site=get_disk refcnt=3

能夠看到,原來是 systemd 的 udevd 進程在使用 sampleblk 設備。若是熟悉 udevd 的人可能就會當即恍然大悟,由於 udevd 負責偵聽系統中全部設備的熱插拔事件,並負責根據預約義規則來對新設備執行一系列操做。而 sampleblk 驅動在調用 add_disk 時,kobject 層的代碼會向用戶態的 udevd 發送熱插拔的 uevent,所以 udevd 會打開塊設備,作相關的操做。
利用 crash 命令,能夠很容易找到是哪一個進程在打開 sampleblk 設備,

crash> foreach files -R /dev/sampleblk
PID: 4084   TASK: ffff88000684d700  CPU: 0   COMMAND: "systemd-udevd"
ROOT: /    CWD: /
 FD       FILE            DENTRY           INODE       TYPE PATH
  8 ffff88000691ad00 ffff88001ffc0600 ffff8800391ada08 BLK  /dev/sampleblk1
  9 ffff880006918e00 ffff88001ffc0600 ffff8800391ada08 BLK  /dev/sampleblk1

因爲 sampleblk_request 函數實現被刪除,則 udevd 發送的 IO 操做沒法被 sampleblk 設備驅動完成,所以 udevd 陷入到長期的阻塞等待中,直到超時返回錯誤,釋放設備。上述分析能夠從系統的消息日誌中被證明,

messages:Apr 23 03:11:51 localhost systemd-udevd: worker [2466] /devices/virtual/block/sampleblk1 is taking a long time
messages:Apr 23 03:12:02 localhost systemd-udevd: worker [2466] /devices/virtual/block/sampleblk1 timeout; kill it
messages:Apr 23 03:12:02 localhost systemd-udevd: seq 4313 '/devices/virtual/block/sampleblk1' killed

注:tpoint 是一個基於 ftrace 的開源的 bash 腳本工具,能夠直接下載運行使用。它是 Brendan Gregg 在 github 上的開源項目,前文已經給出了項目的連接。

從新把刪除的 sampleblk_request 函數源碼加回去,則這個問題就不會存在。由於 udevd 能夠很快結束對 sampleblk 設備的訪問。

4.3 建立文件系統

雖然 Sampleblk 塊驅動只有 200 行源碼,但已經能夠看成 ramdisk 來使用,在其上能夠建立文件系統,

$ sudo mkfs.ext4 /dev/sampleblk1

文件系統建立成功後,mount 文件系統,並建立一個空文件 a。能夠看到,均可以正常運行。

$sudo mount /dev/sampleblk1 /mnt
$touch a

至此,sampleblk 作爲 ramdisk 的最基本功能已經實驗完畢。

5. 延伸閱讀

相關文章
相關標籤/搜索