以下三幅圖片為磁盤結構的簡圖:
磁盤的中間有一個主軸,它可以帶動盤片轉動,讓所有的盤片都圍繞著主軸旋轉。有些磁盤的盤片上會有兩個盤面,即上下兩個盤面。
盤面被劃分成許多個狹窄的同心圓環,每一個同心圓環都被稱為一個磁道,磁道的編號是由外向內的,即最外圈的為0號磁道。 將盤面看成一個圓形,可以在盤面上劃分出多個相同的扇形,分配到每一個磁道上的弧段被稱為扇區。可以看出每一個磁道上的扇區數是相同的。注意上圖右半邊的注解。 每一個磁道上的扇區數目相同,且每個扇區的大小都為512字節,但外圈的扇區占用的物理面積更大,因此外圈的磁道上的扇區存在更多的浪費。
盤片旁邊為機械磁臂,用于移動磁頭。上圖中用藍色標出的圓柱體為一個柱面,例如所有盤面的0磁道可以組成一個柱面。只要往磁盤控制器中寫柱面(cylinder)、磁頭(head)、扇區(sector),就可以使用磁盤了。
扇區號的排列會影響磁盤的訪問速度,Linux0.11使用的磁盤的扇區號排列方式如下:
從圖中可以看出:扇區號是從一個柱面最上面的一個磁道從上往下開始排列,若一個磁道上有7個扇區,最上面磁道的扇區排列為0,1,2,3,4,5,6;7號扇區在下面一個磁道,且位于0號扇區的正下方。下一個柱面的扇區號從上一個柱面結束的扇區號+1開始排列,即扇區號的排列是連續的。這樣數據的讀寫就變為了按柱面進行的,而不是按盤面進行。
磁盤容量 = 盤面數 × 柱面數 × 扇區數 × 512字節; //柱面數也可以理解為一個盤面上的磁道數
磁盤訪問時間 = 寫入控制器時間 + 尋道時間 + 旋轉延遲時間 + 傳輸時間
硬盤讀取數據時,讀寫磁頭沿徑向移動,移到要讀取的扇區所在磁道的上方,這段時間稱為尋道時間(seek time)。因讀寫磁頭的起始位置與目標位置之間的距離不同,尋道時間也不同。磁頭到達指定磁道后,然后通過盤片的旋轉,使得要讀取的扇區轉到讀寫磁頭的下方,這段時間稱為旋轉延遲時間(rotational latencytime)。然后再讀寫數據,讀寫數據也需要時間,這段時間稱為傳輸時間(transfer time)
直接通過三維參數(柱面、磁頭、扇區)使用磁盤是非常麻煩的。從扇區號到盤塊號,是對磁盤使用的第一層抽象,本章節主要介紹如何通過盤塊號來使用磁盤,其中2.1節介紹盤塊號如何轉換為三維參數,2.2節分析Linux0.11中生磁盤的使用過程。
扇區大小固定,但操作系統可以每次讀/寫連續的幾個扇區,這幾個連續的扇區可以組成一個盤塊。為方便理解如何通過盤塊號來訪問磁盤,假定一個盤塊的大小為一個扇區,盤塊號排列方式與扇區號一樣(即盤塊號與圖1.4的扇區號排列一致)。根據這種排列方式,就可以根據盤塊號計算出要訪問的三維參數(C,H,S):
// 根據要訪問的三維參數可以計算出對應的盤塊號
block = C * (Heads * Sectors) + H * Sectors + S;
// 也可以通過盤塊號計算出要訪問的三維參數
S = block % Sectors; // 注意向磁盤控制器內寫入的扇區號是真實磁盤的物理扇區號,不是上圖中的扇區號
C = block / ( Heads * Sectors);
H = (block % ( Heads * Sectors)) / Sectors;
其中 C,H,S 表示訪問磁盤所需要的三維參數;block 表示盤塊號;Heads 為常量,表示磁盤的磁頭數(也就是盤面數);Sectors 為常量,表示一個磁道上的扇區數;若一個盤塊對應n個扇區,計算方式也與之類似。
盤塊大小的分配也會影響到磁盤使用的效率:盤塊越大,讀寫的速度會越快,但碎片也會越大(這些碎片無法使用);盤塊越小讀寫速度會越慢,但碎片會越小。
假設磁頭的初始位置是100號磁道,有多個進程先后陸續的請求訪問55,58,39,18,90,160,150,38,184號磁道
根據進程請求訪問磁盤的先后順序進行調度
按照FCFS的規則,按照請求到達的順序,磁頭需要一次移動到55,58,39,90,160,150,38,184號磁道
磁頭總共移動的磁道個數為45+3+19+21+72+70+10+112+146=498
平均尋道長度為498/9=55.3個磁道
最短尋道時間優先,其要求訪問的磁道與當前磁頭所在的磁道距離最近,以便每次的尋道時間最短,但這種調度算法卻不能保證平均尋道時間最短
假設磁頭的初始位置是100號磁道,有多個進程先后陸續的請求訪問55,58,39,18,90,160,150,38,184號磁道
按照SSTF的規則,請求到達的
磁頭總共移動了(100-18)+(184-18)=248個磁道
平均尋道長度為248/9=27.5個磁道
當磁頭正在由里向外移動時,SCAN算法所選擇的下一個訪問對象應是其欲訪問的磁道,既在當前磁道之外,又是距離最近的。這樣由里向外地訪問,直至再無更外的磁道需要訪問時,才將磁臂換向,由外向里移動。也叫電梯算法。
磁頭總共移動了(184-100)+(184-18)=250個磁道
平均尋道長度為250/9=27.8個磁道
為了減少SCAN算法造成的某些進程的請求被嚴重推遲,CSCAN算法規定磁頭單向移動。
磁頭總共移動了(184-100)+(184-18)+(90-18)=322個磁道
平均尋道長度為322/9=35.8個磁道
本節主要介紹 Linux0.11 中生磁盤的使用過程。本節內容只是對生磁盤的使用過程進行了粗糙的分析,主要是為了了解生磁盤的大致使用過程,如對 請求項隊列的數據結構:struct buffer_head struct request struct blk_dev_struct三個結構體是怎么配合工作的等等, 這些細節部分都沒有進行說明,對于這些細節建議參考《Linux內核完全剖析——基于0.12內核》。
生磁盤的使用過程簡圖如下:
下面根據 Linux0.11 的程序對生磁盤的使用過程進行分析。
(1)進程向緩沖區管理程序提出讀寫磁盤申請。緩沖區管理程序做出一個磁盤請求,在將請求加入請求隊列中,最后讓進程進入睡眠等待狀態
/*創建請求項,并加入到請求隊列中*/
/*major為主設備號,rw為命令(讀/寫),bh為存放數據的緩沖區頭指針*/
static void make_request(int major,int rw, struct buffer_head * bh)
{
struct request * req;
int rw_ahead;
......
/* fill up the request-info, and add it to the queue */
/*前面主要是在尋找空閑請求項,并將請求項插入到請求項隊列的指定位置*/
/*接下來就是做出請求項,從下面這段程序可以看出 bh 中所包含的信息*/
req->dev = bh->b_dev; /*數據源的主設備號*/
req->cmd = rw;
req->errors=0;
/*bh->b_blocknr 應該就是圖1.4中說的扇區號,而req->sector就是盤塊號。
從這里可以看出一個盤塊的大小為2個扇區(2*512Bety)*/
req->sector = bh->b_blocknr<<1;
req->nr_sectors = 2; /*本次請求的扇區數*/
req->buffer = bh->b_data;/*將請求項的緩沖區指針指向需要讀寫的數據緩沖區*/
req->waiting = NULL;
req->bh = bh;
req->next = NULL;
add_request(major+blk_dev,req);/*將請求項加入隊列*/
}
在將請求項插入請求隊列時,為了讓磁盤使用的更加高效,這里采用了電梯算法將請求項插入請求隊列。
/*本函數是將已經做好的請求項(req),加入到請求隊列(dev)中*/
static void add_request(struct blk_dev_struct * dev, struct request * req)
{
struct request * tmp;
req->next = NULL;
cli();/*這段代碼要互斥,因此關中斷*/
if (req->bh)
req->bh->b_dirt = 0;
if (!(tmp = dev->current_request)) {/*將tmp指向請求隊列的隊首*/
/*若當前設備的請求項列表為空則設置 req 為當前請求項,并立即調用設備請求項處理函數*/
dev->current_request = req;
sti();
(dev->request_fn)();/*設備請求項處理函數指針,若當前請求讀寫的為硬盤,則它是 do_hd_request() */
return;
}
/*如果當前設備的請求項列表不為空則將 req 插入請求隊列中*/
/*下面這個for循環將利用 電梯算法 將req插到dev的合適位置*/
for ( ; tmp->next ; tmp=tmp->next)/*從前往后掃描整個請求隊列*/
if ((IN_ORDER(tmp,req) || /*IN_ORDER應該是要比較tem的扇區號是否小于req的扇區號的,不過這里簡化了,比較的是柱面號*/
!IN_ORDER(tmp,tmp->next)) &&
IN_ORDER(req,tmp->next))
break;
req->next=tmp->next;
tmp->next=req;
sti();/*開中斷*/
}
將請求項插入請求隊列后就會讓進程進入睡眠等待狀態,不過我還沒找到相關代碼,找到后在補充這一段。
(2) 根據(1)中算出的盤塊號計算出要訪問的三維參數(扇區號,柱面,磁頭)并寫入磁盤控制器
從(1)中可以看出,對于硬盤的請求項,設備請求項處理函數為 do_hd_request() 。
/*本函數執行磁盤讀寫請求操作*/
/*該函數首先根據請求項中的設備號和盤塊號等信息計算出要訪問的磁盤三維參數。
然后根據請求項中的讀寫命令,向磁盤控制器發出相應的讀寫命令。*/
void do_hd_request(void)
{
int i,r = 0;
unsigned int block,dev;
unsigned int sec,head,cyl;
unsigned int nsect;
INIT_REQUEST;
dev = MINOR(CURRENT->dev);
block = CURRENT->sector;/*CURRENT->sector為盤塊號*/
if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
end_request(0);
goto repeat;
}
block += hd[dev].start_sect;/*start_sect 是分區在磁盤中的起始物理(絕對)扇區,這個和磁盤分區有關,先不管它*/
dev /= 5;
/*下面這段內嵌匯編將根據盤塊號算出cyl, head, sec(CHS)*/
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect));
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head));
sec++;
nsect = CURRENT->nr_sectors;/*nr_sectors 是分區中的扇區總數*/
......
if (CURRENT->cmd == WRITE) {
/*發送寫磁盤命令。write_intr 中斷調用函數,當前中斷為寫操作時被設置成中斷過程中調用的 C 函數。
磁盤完成寫盤命令后會向CPU發送中斷請求信號,于是在磁盤控制器完成寫操作后會立刻調用該函數*/
hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
......
} else if (CURRENT->cmd == READ) {
/*發送讀磁盤命令。read_intr 的用法與 write_intr 類似*/
hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
} else
panic("unknown hd-command");
}
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
unsigned int head,unsigned int cyl,unsigned int cmd,
void (*intr_addr)(void))
{
register int port asm("dx");
......
do_hd = intr_addr; /*do_hd = intr_addr 在磁盤中斷處理函數(hd_interrupt:)中被調用*/
outb_p(hd_info[drive].ctl,HD_CMD); /*向磁盤控制器中輸出控制字節*/
port=HD_DATA;
outb_p(hd_info[drive].wpcom>>2,++port);
outb_p(nsect,++port); //參數,讀寫扇區總數
outb_p(sect,++port); //參數,起始扇區
outb_p(cyl,++port); //參數,柱面號低8位
outb_p(cyl>>8,++port); //參數,柱面號高8位
outb_p(0xA0|(drive<<4)|head,++port); //參數,驅動器號加磁頭號
outb(cmd,++port); //命令,磁盤控制命令
}
outb_p() 會執行一段匯編代碼, 里面很重要的一句 :outb %%al,%%dx,就是向磁盤端口寫數據。
(3)磁盤中斷請求處理
當磁盤處理完成或發生錯誤是就會發出中斷信號,此時CPU響應中斷請求,并調用磁盤中斷處理程序:hd_interrupt:
hd_interrupt:
......
1: jmp 1f
1: xorl %edx,%edx
xchgl do_hd,%edx #do_hd是一個函數指針,被賦值為 write_intr() 或 read_intr(),
#看一下前面提到的 hd_out() 調用過程就會明白了。這里將edx設置為 do_hd
testl %edx,%edx
jne 1f
movl $unexpected_hd_interrupt,%edx
1: outb %al,$0x20
call *%edx # "interesting" way of handling intr.
# 調用 “edx” 函數
......
iret
(4)磁盤處理完成產生中斷,CPU處理中斷并將磁盤返回數據加入緩沖區,最后喚醒進程
read_intr() 函數會將磁盤控制器中的數據復制到請求項指定的緩沖區中。在執行read_intr() 時會調用函數 end_request(1), 該函數會將進程喚醒。write_intr() 的處理過程與 read_intr() 類似。
到此如何通過盤塊使用磁盤就分析完畢了。不過用盤塊號來使用磁盤是相當麻煩的,程序員忍忍也就算了,用戶怎能受得了如此折磨。下一章將介紹如何通過文件來使用磁盤。
圖解