[Linux] Linux標準塊設備驅動詳解:從原理到實現

Linux標準塊設備驅動詳解:從原理到實現

在Linux系統中,塊設備是存儲系統的核心組成部分,涵蓋了硬盤、固態硬盤(SSD)、U盤、SD卡等各類持久化存儲介質。與字符設備不同,塊設備以固定大小的“塊”為單位進行數據讀寫,支持隨機訪問,并通過復雜的I/O調度機制提升性能和設備壽命。本文將深入剖析Linux塊設備驅動的架構、核心數據結構、注冊流程及請求處理機制,并通過一個完整的基于內存的RAM磁盤驅動示例,幫助開發者掌握塊設備驅動開發的關鍵技術。

文章目錄

  • Linux標準塊設備驅動詳解:從原理到實現
    • 一、塊設備概述:理解I/O模型的本質差異
    • 二、核心數據結構解析
      • 1. `block_device_operations`:設備操作接口
      • 2. `gendisk`:磁盤設備的抽象
    • 三、驅動注冊與注銷流程詳解
      • 1. 注冊流程
      • 2. 注銷流程
    • 四、I/O請求處理機制
      • 1. 核心組件
      • 2. 多隊列(blk-mq)處理模式
        • 定義多隊列操作集
        • 請求處理函數示例
    • 五、完整示例:基于內存的RAM磁盤驅動
      • 編譯與測試
    • 六、關鍵要點總結
    • 結語


一、塊設備概述:理解I/O模型的本質差異

在Linux設備模型中,設備主要分為字符設備、塊設備和網絡設備三類。其中,塊設備(Block Device) 的顯著特征是:

  • 以塊為單位傳輸數據:通常以512字節或4KB為基本單位(扇區),即使應用層請求非對齊數據,內核也會自動進行填充和裁剪。
  • 支持隨機訪問:可以任意讀寫任意扇區,無需按順序操作。
  • 使用緩沖區緩存(Buffer Cache):內核通過Page Cache和Buffer Head機制緩存頻繁訪問的數據,減少對物理設備的直接訪問,提升性能并延長設備壽命(尤其是SSD)。
  • 依賴I/O調度器:內核提供多種I/O調度算法(如CFQ、Deadline、NOOP、BFQ),用于合并相鄰請求、優化請求順序,降低磁頭尋道時間或提升SSD的并行性。

與之對比,字符設備(如串口、鍵盤)通常以字節流方式工作,不經過塊層調度,也不支持隨機訪問。因此,塊設備驅動需要更復雜的軟件棧來處理請求的排隊、合并、調度和完成通知。


二、核心數據結構解析

Linux內核通過一組關鍵數據結構來抽象和管理塊設備。掌握這些結構是編寫塊設備驅動的基礎。

1. block_device_operations:設備操作接口

該結構體定義了用戶空間與塊設備交互的操作接口,類似于字符設備中的file_operations

struct block_device_operations {int (*open)(struct block_device *bdev, fmode_t mode);void (*release)(struct gendisk *disk, fmode_t mode);int (*ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);int (*compat_ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);int (*revalidate_disk)(struct gendisk *disk);int (*getgeo)(struct block_device *bdev, struct hd_geometry *geo);void (*swap_slot_free_notify)(struct block_device *, unsigned long);struct module *owner;
};
  • open / release:設備打開和關閉時的回調,用于初始化硬件或釋放資源。
  • ioctl:處理設備特定的控制命令,例如獲取磁盤幾何信息(CHS)、執行設備診斷等。
  • getgeo:返回磁盤的物理幾何參數(柱面、磁頭、扇區),主要用于兼容舊系統。
  • owner:指向所屬模塊,防止模塊在使用中被卸載。

注意:現代驅動中,openrelease通常為空,因為塊設備的打開由內核自動管理。


2. gendisk:磁盤設備的抽象

gendisk結構體代表一個完整的磁盤設備,包括主設備和所有分區。

struct gendisk {int major;                  // 主設備號int first_minor;            // 起始次設備號int minors;                 // 支持的分區數量(1表示無分區)char disk_name[32];         // 設備名稱,如 "myblk"struct block_device_operations *fops;  // 操作函數集struct request_queue *queue;          // 請求隊列sector_t capacity;          // 容量(以512字節扇區為單位)struct disk_part_tbl *part_tbl;       // 分區表struct hd_struct part0;     // 主設備信息// 其他成員...
};

關鍵操作流程

  1. 分配:使用 alloc_disk(minors) 動態分配一個gendisk對象。
  2. 初始化:設置設備號、名稱、操作函數、請求隊列和容量。
  3. 設置容量:通過 set_capacity(disk, sectors) 指定設備總扇區數。例如,1MB內存磁盤對應:
    set_capacity(disk, (1 * 1024 * 1024) / 512); // = 2048 扇區
    
  4. 注冊:調用 add_disk(disk) 將設備注冊到內核,此后設備節點(如 /dev/myblk)將自動出現在/dev目錄下。

重要提示:一旦調用add_disk(),驅動必須確保設備可正常響應I/O請求,否則可能導致系統掛起。


三、驅動注冊與注銷流程詳解

塊設備驅動的生命周期管理涉及設備號分配、磁盤對象初始化和內核注冊。

1. 注冊流程

static dev_t dev_num;  // 設備號
static struct gendisk *disk;
static struct request_queue *queue;static int __init myblk_init(void)
{int ret;// 1. 動態分配設備號ret = register_blkdev(0, "myblk");if (ret <= 0) {printk(KERN_ERR "Failed to register block device\n");return -EIO;}dev_num = MKDEV(ret, 0);  // 主設備號由內核返回// 2. 分配并初始化gendiskdisk = alloc_disk(1);  // 支持1個分區if (!disk) {unregister_blkdev(MAJOR(dev_num), "myblk");return -ENOMEM;}disk->major = MAJOR(dev_num);disk->first_minor = 0;strcpy(disk->disk_name, "myblk");disk->fops = &my_blk_fops;           // 操作函數disk->queue = queue;                 // 請求隊列set_capacity(disk, 2048);            // 1MB容量// 3. 注冊到內核add_disk(disk);printk(KERN_INFO "myblk: Registered block device with major %d\n", MAJOR(dev_num));return 0;
}

2. 注銷流程

static void __exit myblk_exit(void)
{if (disk) {del_gendisk(disk);           // 從內核移除設備put_disk(disk);              // 釋放gendisk}if (queue) {blk_cleanup_queue(queue);    // 清理請求隊列}unregister_blkdev(MAJOR(dev_num), "myblk");  // 釋放設備號
}

注意del_gendisk()會阻止新的I/O請求進入,但不會等待正在進行的請求完成。因此,驅動應確保在調用此函數前所有請求已處理完畢。


四、I/O請求處理機制

塊設備驅動的核心任務是處理來自文件系統的I/O請求。現代Linux內核采用多隊列(Multi-Queue, blk-mq) 架構以提升多核系統的并發性能。

1. 核心組件

  • request_queue:請求隊列,由blk_mq_init_queue()創建,管理所有待處理的I/O請求。
  • bio(Block I/O)結構體:描述一個I/O操作的基本單元,包含:
    • bi_sector:起始邏輯扇區號
    • bi_size:數據長度(字節)
    • bi_io_vec:指向bio_vec數組,描述分散/聚集(scatter-gather)的內存頁
    • bi_end_io:完成回調函數

2. 多隊列(blk-mq)處理模式

傳統請求隊列使用request_fn處理合并后的請求,而blk-mq直接處理bio,簡化了驅動邏輯。

定義多隊列操作集
static struct blk_mq_ops my_mq_ops = {.queue_rq = my_queue_rq,      // 核心請求處理函數.complete = my_complete_rq,   // 可選:完成回調
};
請求處理函數示例
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{struct request *req = bd->rq;struct bio *bio;sector_t sector = blk_rq_pos(req);unsigned int nr_bytes = blk_rq_bytes(req);// 遍歷所有bio(支持合并請求)__rq_for_each_bio(bio, req) {void *data = bio_data(bio);unsigned int len = bio->bi_iter.bi_size;if (bio_data_dir(bio) == READ) {// 模擬讀操作:從模擬存儲區復制數據memcpy(data, disk_data + sector * 512, len);} else {// 模擬寫操作memcpy(disk_data + sector * 512, data, len);}sector += len >> 9;  // 轉換為扇區數(512B/sector)}// 標記請求完成blk_mq_end_request(req, BLK_STS_OK);return BLK_STS_OK;
}

說明blk_mq_end_request()會自動調用bio的完成回調并釋放資源。


五、完整示例:基于內存的RAM磁盤驅動

以下是一個可編譯加載的完整RAM磁盤驅動,模擬一個1MB的塊設備。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/vmalloc.h>#define DEV_NAME        "myramdisk"
#define DISK_SIZE       (1 * 1024 * 1024)  // 1MBstatic dev_t dev_num;
static struct request_queue *queue;
static struct gendisk *disk;
static unsigned char *disk_data;// 請求處理函數
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd)
{struct request *req = bd->rq;struct bio *bio;sector_t sector = blk_rq_pos(req);__rq_for_each_bio(bio, req) {void *data = bio_data(bio);unsigned int len = bio->bi_iter.bi_size;if (sector + (len >> 9) > DISK_SIZE / 512) {return BLK_STS_IOERR;  // 越界檢查}if (bio_data_dir(bio) == READ) {memcpy(data, disk_data + sector * 512, len);} else {memcpy(disk_data + sector * 512, data, len);}sector += len >> 9;}blk_mq_end_request(req, BLK_STS_OK);return BLK_STS_OK;
}// 多隊列操作集
static struct blk_mq_ops my_mq_ops = {.queue_rq = my_queue_rq,
};// 模塊初始化
static int __init myramdisk_init(void)
{int ret;// 1. 分配設備號ret = register_blkdev(0, DEV_NAME);if (ret < 0) return ret;dev_num = MKDEV(ret, 0);// 2. 分配模擬存儲空間disk_data = vmalloc(DISK_SIZE);if (!disk_data) {unregister_blkdev(MAJOR(dev_num), DEV_NAME);return -ENOMEM;}memset(disk_data, 0, DISK_SIZE);// 3. 初始化請求隊列queue = blk_mq_init_sq_queue(&tag_set, &my_mq_ops, 0, BLK_MQ_F_SHOULD_MERGE);if (IS_ERR(queue)) {vfree(disk_data);unregister_blkdev(MAJOR(dev_num), DEV_NAME);return PTR_ERR(queue);}// 4. 分配并初始化gendiskdisk = alloc_disk(1);if (!disk) {blk_cleanup_queue(queue);vfree(disk_data);unregister_blkdev(MAJOR(dev_num), DEV_NAME);return -ENOMEM;}disk->major = MAJOR(dev_num);disk->first_minor = 0;strcpy(disk->disk_name, DEV_NAME);disk->fops = &my_fops;disk->queue = queue;set_capacity(disk, DISK_SIZE / 512);disk->private_data = NULL;// 5. 注冊設備add_disk(disk);printk(KERN_INFO "%s: RAM disk initialized (%d MB)\n", DEV_NAME, DISK_SIZE >> 20);return 0;
}// 模塊退出
static void __exit myramdisk_exit(void)
{if (disk) {del_gendisk(disk);put_disk(disk);}if (queue) {blk_cleanup_queue(queue);}if (disk_data) {vfree(disk_data);}unregister_blkdev(MAJOR(dev_num), DEV_NAME);printk(KERN_INFO "%s: unloaded\n", DEV_NAME);
}module_init(myramdisk_init);
module_exit(myramdisk_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple RAM block device driver");

編譯與測試

  1. 編譯模塊

    make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
    
  2. 加載模塊

    sudo insmod myramdisk.ko
    
  3. 驗證設備

    ls /dev/myramdisk
    dmesg | tail
    
  4. 格式化并掛載

    sudo mkfs.ext4 /dev/myramdisk
    sudo mkdir /mnt/ramdisk
    sudo mount /dev/myramdisk /mnt/ramdisk
    

六、關鍵要點總結

  1. 設備號管理:使用register_blkdev(0, ...)實現主設備號動態分配,避免沖突。
  2. 多隊列優先:現代驅動應使用blk-mq架構,直接處理bio,提高并發性能。
  3. 內存分配:大容量設備應使用vmalloc而非kmalloc,避免內存碎片。
  4. 錯誤處理:在queue_rq中進行邊界檢查,返回適當的blk_status_t
  5. 生命周期同步:確保del_gendisk()調用前無活躍I/O,防止內存訪問錯誤。
  6. 性能優化:合理配置隊列深度、硬件上下文數,啟用I/O調度器(如Deadline用于SSD)。

結語

Linux塊設備驅動是連接上層文件系統與底層存儲硬件的橋梁。通過理解gendiskrequest_queuebio等核心結構,掌握blk-mq請求處理機制,開發者可以構建高效、穩定的存儲驅動。本文的RAM磁盤示例為學習和調試提供了基礎框架,實際開發中可將其擴展為支持真實硬件(如PCIe SSD、NAND控制器)的復雜驅動。

更多細節可參考內核源碼樹中的drivers/block/目錄,如brd.c(RAM磁盤)、null_blk.c(空設備)等經典實現。


研究學習不易,點贊易。
工作生活不易,收藏易,點收藏不迷茫 :)


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/95767.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/95767.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/95767.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

什么是壓力測試,有哪些方法

壓力測試&#xff08;Stress Testing&#xff09;是性能測試的一種&#xff0c;旨在評估系統在極端負載條件下的表現&#xff0c;驗證其穩定性、可靠性和容錯能力。通過模擬超出正常范圍的并發用戶、數據量或請求頻率&#xff0c;發現系統在高負載下的瓶頸&#xff08;如內存泄…

lua腳本在redis中執行是否是原子性?

lua腳本在redis中執行是否是原子性&#xff1f;以及是否會阻塞其他腳本的執行【客戶端的請求】&#xff1f;先解答第二個問題:是的&#xff0c;保持原子執行。這也是redis中支持lua腳本執行的原因。Lua 腳本在 Redis 中是以原子方式執行的&#xff0c;在 Redis 服務器執行EVAL命…

DeepSeek文獻太多太雜?一招制勝:學術論文檢索的“核心公式”與提問藝術

如果我們想要完成一次學術論文檢索&#xff0c;那我們可以把它想象成一次精準的“學術尋寶”。你不是在漫無目的地閑逛&#xff0c;而是一名裝備精良的“學術尋寶獵人”&#xff0c;你的目標是找到深藏在浩瀚文獻海洋中的“珍寶”&#xff08;高價值論文&#xff09;。1 你的尋…

Linux內存管理章節一:深入淺出Linux內存管理:從物理內存到ARM32的用戶與內核空間

引言 如果說操作系統是計算機的心臟&#xff0c;那么內存管理就是它的靈魂脈絡。它默默地工作在Linux內核的最底層&#xff0c;卻決定著整個系統的穩定性、安全性和性能。今天&#xff0c;我們將撥開迷霧&#xff0c;深入探索Linux內存管理的核心概念&#xff0c;并結合熟悉的A…

ECMAScript (5)ES6前端開發核心:國際化與格式化、內存管理與性能

好的&#xff0c;我將根據【國際化與格式化】和【內存管理與性能】這兩個主題&#xff0c;為你生成詳細的課件內容&#xff0c;涵蓋概念、應用和實例。 &#x1f4d7; 前端開發核心&#xff1a;國際化與格式化、內存管理與性能 1. 國際化與格式化 (Internationalization & …

3D 可視化數字孿生運維管理平臺:構建 “虛實協同” 的智慧運維新范式

3D 可視化數字孿生運維管理平臺通過 “物理空間數字化建模 實時數據動態映射 智能分析決策”&#xff0c;將建筑、園區、工業設施等物理實體 1:1 復刻為虛擬孿生體&#xff0c;打破傳統運維 “信息割裂、依賴經驗、響應滯后” 的痛點&#xff0c;實現從 “被動搶修” 到 “主…

DP-觀察者模式代碼詳解

觀察者模式&#xff1a; 定義一系列對象之間的一對多關系&#xff1b;當一個對象改變狀態&#xff0c;它的依賴都會被通知。 主要由主題&#xff08;Subject&#xff09;和觀察者&#xff08;Observer&#xff09;組成。 代碼實現 package com.designpatterns.observer;/*** 定…

1983:ARPANET向互聯網的轉變

一、ARPANET早期1969年誕生的ARPANET最初還算不上互聯網&#xff0c;不過在ARPANET構建之初就已經考慮了分組交換&#xff1a;1970年代的ARPANET:其實這個時候我就有疑問&#xff0c;TCP/IP是1983年1月1日更新到ARPANET的&#xff0c;但是1970年代的ARPANET已經連接全美的重要單…

自動化運維-ansible中的變量運用

自動化運維-ansible中的變量運用 一、變量命名規則 組成&#xff1a;字母、數字、下劃線。必須以字母開頭。 合法: app_port, web_1, varA非法: 2_var (以數字開頭), my-var (包含其他字符), _private (以下劃線開頭) 避免使用內置關鍵字&#xff1a;例如 hosts, tasks, name…

深入學習并發編程中的volatile

volatile 的作用 保證變量的內存可見性禁止指令重排序1.保證此變量對所有的線程的可見性&#xff0c;當一個線程修改了這個變量的值&#xff0c;volatile 保證了新值能立即同步到主內存&#xff0c;其它線程每次使用前立即從主內存刷新。 但普通變量做不到這點&#xff0c;普通…

使用Java獲取本地PDF文件并解析數據

獲取本地文件夾下的PDF文件要獲取本地文件夾下的PDF文件&#xff0c;可以使用Java的File類和FilenameFilter接口。以下是一個示例代碼片段&#xff1a;import java.io.File; import java.io.FilenameFilter;public class PDFFileFinder {public static void main(String[] args…

吳恩達機器學習補充:決策樹和隨機森林

數據集&#xff1a;通過網盤分享的文件&#xff1a;sonar-all-data.csv 鏈接: https://pan.baidu.com/s/1D3vbcnd6j424iAwssYzDeQ?pwd12gr 提取碼: 12gr 學習來源&#xff1a;https://github.com/cabin-w/MLBeginnerHub 文末有完整代碼&#xff0c;由于這里的代碼和之前的按…

Shell腳本一鍵監控平臺到期時間并釘釘告警推送指定人

1. 監控需求客戶側有很多平臺需要定期授權&#xff0c;授權后管理后臺才可正常登錄&#xff0c;為避免授權到期&#xff0c;現撰寫腳本自動化監控平臺授權到期時間&#xff0c;在到期前15天釘釘或其他媒介提醒。2. 監控方案2.1 收集平臺信息梳理需要監控的平臺地址信息&#xf…

華為HCIE數通含金量所剩無幾?考試難度加大?

最近網上很火的一個梗——法拉利老了還是法拉利&#xff0c;這句話套在華為HCIE數通身上同樣適用&#xff0c;華為認證中的華為數通和云計算兩大巨頭充斥著大家的視野里面&#xff0c;也更加廣為人知&#xff0c;但隨著時代的發展&#xff0c;華為認證體系的調整&#xff0c;大…

#數據結構----2.1線性表

在數據結構的學習中&#xff0c;線性表是最基礎、最核心的結構之一 —— 它是后續棧、隊列、鏈表等復雜結構的 “基石”。今天從 “是什么”&#xff08;定義&#xff09;到 “怎么用”&#xff08;基本操作&#xff09;&#xff0c;徹底搞懂線性表的核心邏輯。 一、先明確&…

2508C++,skia動畫

gif動畫原理 先了解一下gif動畫的原理: gif動畫由一系列靜態圖像(或叫幀)組成.這些圖像按特定的順序排列,每一幀都代表動畫中的一個瞬間,幀圖像是支持透明的. 每兩幀之間有指定的時間間隔(一般小于60毫秒),gif播放器每渲染一幀靜態圖像后,即等待此時間間隔,依此邏輯不斷循環渲染…

AI + 機器人:當大語言模型賦予機械 “思考能力”,未來工廠將迎來怎樣變革?

一、引言1.1 未來工廠變革背景與趨勢在科技飛速發展的當下&#xff0c;全球制造業正站在變革的十字路口。隨著消費者需求日益多樣化、市場競爭愈發激烈&#xff0c;傳統工廠模式的弊端逐漸顯現。生產效率低下、難以適應個性化定制需求、設備維護成本高昂且缺乏前瞻性等問題&…

pinia狀態管理的作用和意義

1. 什么是狀態管理 狀態管理就是統一管理應用中的數據&#xff0c;讓數據在多個組件之間共享和同步。 // 沒有狀態管理 - 數據分散在各個組件中 // 組件A const user ref({ name: 張三, age: 25 })// 組件B const user ref({ name: 張三, age: 25 }) // 重復定義// 組件C c…

十四、STM32-----低功耗

一、電源框圖VDDA 供電區域&#xff0c;主要是 ADC 電源以及參考電壓&#xff0c;STM32 的 ADC 模塊配備獨立的供電方 式&#xff0c;使用了 VDDA 引腳作為輸入&#xff0c;使用 VSSA 引腳作為獨立地連接&#xff0c;VREF 引腳為提供給 ADC 的 參考電壓。電壓調節器是 STM32 的…

一篇文章帶你徹底搞懂 JVM 垃圾收集器

垃圾收集器是 JVM 內存管理的執行引擎&#xff0c;負責自動回收無用的對象內存。其設計核心是 權衡&#xff1a;主要是吞吐量和停頓時間之間的權衡。沒有“最好”的收集器&#xff0c;只有“最適合”特定場景的收集器。一、核心基礎&#xff1a;分代收集模型主流 HotSpot JVM 采…