在前文《理解Linux操作系統的塊設備》中我們從比較高層面(Hight Level)介紹了塊設備的原理和塊設備的特性。但是關於Linux操作系統塊設備的實現原理可能還一知半解。本文將進一步深入的分析Linux的塊設備,期望能讓大家更加深入的理解塊設備的實現細節

其實在Linux操作系統中可以非常方便的實現一個塊設備,或者說是塊設備驅動。在Linux中我們熟知的RAID、多路徑和Ceph的RBD等都是這樣一種塊設備。其特徵就是在操作系統的/dev目錄下面會創建一個文件。如圖1顯示的不同類型的塊設備,包含普通的SCSI塊設備和LVM邏輯卷塊設備,本質上都是塊設備,差異在於在不同的業務邏輯和名稱。

塊設備的實現原理

在Linux操作系統中,塊設備的實現其實十分簡單,但也十分複雜。簡單的是我們可以只用2個函數就可以創建一個塊設備驅動程序;複雜的地方是塊設備的匯流排和底層設備驅動的關係錯綜複雜,且塊設備驅動種類繁多。 我們先看一下如何創建一個塊設備,創建的方法很簡單,主要是調用Linux內核的2個函數,分別是alloc_disk和add_disk。alloc_disk用於分配一個gendisk結構體的實例,而後者則是將該結構體實例註冊到系統中。經過上述2步的操作,我們就可以在/dev目錄下看到一個塊設備。另外一個比較重要的地方是初始化gendisk結構體的請求隊列,這樣應用層有請求的時候會調用該隊列的常式進行處理關於創建塊設備的詳細實現代碼本文並不打算進行深入介紹,需要了解的同學可以閱讀《Linux設備驅動程序》這本書,目前最新的是第三版。這本書的第16章詳細的介紹了一個基於內存的塊設備驅動的實現細節,並且有配套源代碼。所謂基於內存的塊設備是指這個塊設備的數據存儲在內存中,而不是真正的諸如磁碟或者光碟的物理設備中。如下是本文從該書中截取的代碼片段,核心是上文提到的2個函數。

static void setup_device(struct sbull_dev* dev, int which)
{
memset(dev, 0, sizeof(struct sbull_dev));
dev->size = nsectors * hardsect_size;
dev->data = vmalloc(dev->size);
if (dev->data == NULL)
{
printk(KERN_NOTICE "vmalloc failed.
");
return;
}

spin_lock_init(&dev->lock);

/*初始化一個隊列函數,用於處理IO請求*/
dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
dev->queue->queuedata = dev;
blk_queue_logical_block_size(dev->queue, hardsect_size);

/*創建gendisk結構體,並初始化*/
dev->gd = alloc_disk(SBULL_MINORS);
dev->gd->major = sbull_major;
dev->gd->first_minor = which*SBULL_MINORS;
dev->gd->fops = &sbull_ops;
dev->gd->queue = dev->queue;
dev->gd->private_data = dev;

/*拼湊塊設備的名稱,為sbulla*/
snprintf( dev->gd->disk_name, 32, "sbull%c", (a + which));
set_capacity( dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE) );
add_disk(dev->gd); /*將塊設備添加到系統內核*/

return;
}

上述塊設備程序可以編譯成為一個內核模塊。通過insmod命令將內核模塊載入後,就可以在/dev目錄下看到一個名為sbulla的塊設備。

brw-rw---- 1 root disk 251, 0 Jun 16 09:13 /dev/sbulla

塊設備的複雜性在於其匯流排種類繁多,並且底層驅動類型也非常多。整個系統複雜的地方在於塊設備的初始化工作。一個設備上電後,如何關聯到Linux內核,並呈現給用戶就變得非常複雜。如圖2是本號之前介紹過的關於Linux匯流排相關內容的截圖。塊設備可能位於圖中匯流排的任意位置。

由於這部分內容本身非常複雜,因此本文暫時不對設備初始化工作相關的內容細節進行介紹。後續本號單獨介紹相關的內容。今天本文主要通過幾個實例,以比較直觀的方式介紹一下塊設備與底層驅動層面的相關內容。 以SCSI塊設備為例,雖然都是在/dev下面呈現一個名稱為sdX的塊設備,但底層驅動差異卻非常巨大。我們知道SCSI設備可以通過多種方式連接到主機端:

  • SAS或者SATA介面的磁碟
  • IP-SAN,通過乙太網方式連接存儲設備,在主機端呈現為普通磁碟
  • FC-SAN, 通過光纖方式連接到存儲設備,在主機端呈現為普通磁碟 可以看出,雖然都基於SCSI的塊設備,但底層的驅動差異卻是非常之大,因此初始化的流程自然也有很大的差異。這還都是SCSI塊設備,如果再將基於網路的塊設備(nbd或者rbd)和軟盤、光碟等塊設備考慮進來,那就更加複雜了。

Linux中形形色色的塊設備

塊設備的種類非常多,我們今天就介紹幾種比較典型的塊設備。 SCSI磁碟 最為典型的當然是SCSI磁碟了,SCSI磁碟通過SAS、SATA介面或者HBA卡連接到伺服器的主板,在操作系統內部呈現為一個磁碟設備。熟悉Linux操作系統的同學大概都清楚,對於SCSI磁碟在Linux系統內部是以sd為開頭的名稱。在本文圖1中,下半部分的塊設備就是SCSI磁碟。 SCSI磁碟的具體實現在文件sd.c(driver/scsi/sd.c)中,在該文件中的sd_probe函數中通過調用alloc_disk和add_disk創建了SCSI磁碟塊設備(代碼太長,本文就不貼了)。這裡面另外一個比較重要的地方是初始化了通用塊數據結構的請求隊列。完成上述初始化後,用戶層面就可以訪問該磁碟,並且請求會轉發到這裡註冊的隊列中進行處理。 網路塊設備 另外一個比較典型的塊設備是網路塊設備(Network Block Device),這種塊設備通過網路將一個遠程的文件或者塊設備映射為本地的一個塊設備。另外,Ceph中的塊存儲內核客戶端(RBD)也屬於此類設備,只是Ceph的後端實現是基於一個分散式存儲集羣,更加複雜而已。

網路塊設備最大的特點是建立了一個從服務端到客戶端的設備映射,相對於SCSI來說這種映射又非常簡單。我們以NBD為例瞭解一下基本原理。NBD本身是一個CS(Client-Server)架構的程序,在服務端可以將一個文件/或者磁碟映射為出來(命令為: nbd-server 12345 itworld123.txt,其中12345為埠號)。這一點其實非常類似NFS對目錄的映射,差異在於NBD在客戶端映射為一個磁碟,而NFS在客戶端映射為一個目錄樹。 塊設備的初始化依然是通過上述2個函數完成的,但這裡的核心是初始化的請求隊列常式do_nbd_request。該函數是NBD塊設備的核心,其將一個塊請求轉換為一個網路請求,並發送給NBD服務端進行處理。網路請求的協議非常簡單,通過如圖4所示的一個結構體進行標識。

DRBD 關於DRBD本號在前面的文章中有過介紹,全稱為Distributed Relicated Block Device(也就是,分散式複製塊設備)。DRBD可以理解為一個基於網路的RAID1,也就是其塊設備在2臺伺服器上同時存在,並且有配對關係。當請求寫入其中一個塊設備的時候,DRBD會通過內部的邏輯將數據複製到另外一個伺服器上的塊設備。通過這種方式增加了塊設備的可用性,當其中一臺伺服器宕機時,另外一臺伺服器仍然可以對我提供服務。

同NBD類似,DRBD的與其它塊設備差異的地方在於其隊列處理常式,在DRBD中該常式為drbd_make_request,各位可以自行分析一下該常式的具體實現。 除了上面介紹的塊設備類型外,還有LVM和多路徑等等很多類型的塊設備。由於篇幅有限,本文暫時不做介紹,後續專門進行介紹。

塊設備的請求處理

前文我們介紹中提到了塊設備的偽文件系統,並且知道偽文件系統最終會調用通用塊層的generic_perform_write函數。本文將接著分析一下該函數的具體實現,這樣大家就對上文中提到的請求隊裏的常式有了更加深入的瞭解。下面是該函數的具體代碼,本文刪除了一些非關鍵部分的代碼,保留了核心代碼。

void generic_make_request(struct bio *bio)
{
struct bio_list bio_list_on_stack;
bio_list_init(&bio_list_on_stack);
current->bio_list = &bio_list_on_stack;
do {
/*獲取請求隊列 */
struct request_queue *q = bdev_get_queue(bio->bi_bdev);
/*通過請求隊列的常式進行處理 */
q->make_request_fn(q, bio);
bio = bio_list_pop(current->bio_list);
} while (bio);
current->bio_list = NULL; /* deactivate */
}

可以看到請求到通用塊層後會調用請求隊列的make_request_fn函數指針,而該函數最終調用我們在創建塊設備時註冊的常式。兩者並非同一個函數,這裡需要注意一點,關於這部分內容我們後續詳細介紹。因為這裡比較複雜,關於通用塊層IO調度的內容都在這裡。 通過上面的描述我們對IO請求的處理更加深入了一層,也就是從用戶層面到偽文件系統層面,現在到通用塊層的請求隊列中了。當然,最後是到我們註冊的常式中進行處理。各種不同類型塊設備的差異就在這裡,不同的類型塊設備的處理邏輯有所不同。對於SCSI設備就是通過SCSI協議發送到Target端進行處理,而對於NBD設備則是通過網路發送到服務端進行處理。 好了,今天先到這,後續我們在介紹塊設備中最為核心的特性---磁碟IO調度。


推薦閱讀:
相關文章