1.1
當我們在應用程序中編寫write系統調用,向磁盤中寫入數據時,寫入請求會先調用底層寫函數,將請求先寫入內存中的頁高速緩存(page cache)中,寫入成功則立刻返回,真正的寫入磁盤操作會延遲執行。Page cache是硬盤在內存中的一個緩存,是linux內核所使用的主要磁盤高速緩存,在絕大多數情況下,內核在讀寫磁盤時都引用page cache(極少數應用會繞過頁高速緩存,如數據庫軟件)。
當把page cache中的一頁數據寫到塊設備之前,內核首先檢查對應的頁是否已經在高速緩存中,如果不在,就要先在其中增加一個新項,並用要寫到磁盤中的數據填充該項。I/O數據的傳送並不是馬上開始,而是要延遲幾秒後纔對磁盤進行更新,從而使進程有機會對要寫入磁盤的數據做進一步的修改(也就是內核進行延遲寫操作)。
當內核以文件系統、虛擬內存子系統或者系統調用的形式決定從塊I/O設備輸入、輸出塊數據時,它將再結合一個bio結構,用來描述這個操作。該結構被傳遞給 I/O代碼,代碼會把它合併到一個已經存在的request結構中,或者根據需要,再創建一個新的request結構。bio結構包含了驅動程序執行請求的全部信息,而不必與初始化這個請求的用戶空間的進程相關聯。
內核中塊設備的I/O操作基本容器由bio結構體表示,定義在<linux/bio.h>中,該結構體代表了正在現場的(活動的)以片段(segment)鏈表形式組織的塊I/O操作。一個片段是一小塊連續的內存緩衝區。這樣的好處就是不需要保證單個緩衝區一定要連續。所以通過片段來描述緩衝區,即使一個緩衝區分散在內存的多個位置上,bio結構體也 能對內核保證I/O操作的執行,這樣的就叫做聚散I/O(scatter/gather).
bio爲通用層的主要數據結構,既描述了磁盤的位置,又描述了內存的位置,是上層內核vfs與下層驅動的連接紐帶。
struct bio {
sector_t bi_sector;//該bio結構所要傳輸的第一個(512字節)扇區:磁盤的位置
struct bio *bi_next; //請求鏈表
struct block_device *bi_bdev;//相關的塊設備
unsigned long bi_flags//狀態和命令標誌
unsigned long bi_rw; //讀寫
unsigned short bi_vcnt;//bio_vesc偏移的個數
unsigned short bi_idx; //bi_io_vec的當前索引
unsigned short bi_phys_segments;//結合後的片段數目
unsigned short bi_hw_segments;//重映射後的片段數目
unsigned int bi_size; //I/O計數
unsigned int bi_hw_front_size;//第一個可合併的段大小;
unsigned int bi_hw_back_size;//最後一個可合併的段大小
unsigned int bi_max_vecs; //bio_vecs數目上限
struct bio_vec *bi_io_vec; //bio_vec鏈表:內存的位置
bio_end_io_t *bi_end_io;//I/O完成方法
atomic_t bi_cnt; //使用計數
void *bi_private; //擁有者的私有方法
bio_destructor_t *bi_destructor; //銷燬方法
};
文件系統需要寫到硬盤的數據保存在page cache裏面,那麼這個過程又是怎麼和dma建立關係的呢?
DMA寫磁盤過程概述:
若硬盤支持DMA,並且在操作系統中打開了DMA,則每次讀寫磁盤,都會涉及到DMA操作。雖然文件系統對硬盤的I/O請求不是連續的,數據所在的物理內存頁也是不連續的,但是操作系統會將這些不連續的內存頁組合到一起,再啓用DMA操作(啓用DMA的過程開銷較大,需要設置一系列寄存器),那麼這些數據就能夠一次傳輸完成,這樣也就能高效的傳輸數據。內核中有個物理設備描述符表(physical region descriptor table,PRDT),要進行數據的傳輸必需將相應的物理頁以及物理頁內數據長度填充到PRDT裏面。,PRDT結構如下:
Figure 1說明:
每個PRDT大小爲8字節,0-3字節說明物理頁的內存地址,4-5字節說明內存區域的數量,以字節爲單位,全零表示64K大小。最後一個字節的最後一位表示PRDT表的結束。
scsi層的scsi_init_io函數把bio封裝,然後將其映射給DMA的scatterlist結構體,該結構體即PRDT中的一項,(內核中dma_desc_array對應PRDT),用來指向每個內存塊。剩餘工作就是設置DMA寄存器,然後發送,我們後面將詳細分析該部分代碼。
以下是write系統調用內核態處理函數的路徑:
經過一系列處理,write系統調用處理結束後,若需要寫磁盤數據最終會經過以下路徑:
scsi_scan_target(scsi掃面函數)——》__scsi_scan_target ——》scsi_sequential_lun_scan ——》scsi_probe_and_add_lun ——>scsi_alloc_sdev ——》scsi_alloc_queue(scsi分配隊列),從這裏分開,一條路徑是設置DMA併發送命令到DMA控制器(路徑一),另一條是初始化函數路徑(路徑二)。
路徑一:scsi_request_fn——>scsi_dispatch_cmd——》scsi_log_send——》(.queuecommand =ata_scsi_queuecmd,)ata_scsi_queuecmd——》__ata_scsi_queuecmd——》ata_scsi_translate——》ata_qc_issue——》ata_bmdma_qc_issue——》(bfin_bmdma_setup:設置DMA寄存器/ bfin_bmdma_start:開始DMA)
路徑二:scsi_prep_fn——>scsi_setup_blk_pc_cmnd ——》scsi_init_io ——》scsi_init_sgtable ——》blk_rq_map_sg(該函數的參數request這個結構體封裝了bio結構體).
以下主要分析bfin_bmdma_setup和bfin_bmdma_start函數,即DMA操作過程:
(1) 軟件準備好一個PRD Table放在內存中,每個8字節,對齊到4字節邊界。
(2) 軟件把PRD table的起始地址設置好,同時通過設置讀/寫控制位設置數據和傳輸方向,清除狀態寄存器中的中斷位和錯誤位。
(3) 軟件發出DMA傳送指令到disk設備。
(4) 向總線控制器IDE命令寄存器的對應通道中寫入1,使能總線控制器。
(5) DMA從IDE設備中請求控制器傳送數據到/從內存中
(6) 傳送結束,IDE設備發出中斷
(7) 接收到中斷後,軟件設置命令寄存器的開始/結束位,然後先後讀控制器狀態、驅動狀態,進而確定是否傳送成功。
代碼如下:
/**
* bfin_bmdma_setup- Set up IDE DMA transaction
* @qc:Info associated with this ATA transaction.
*
* Note:Original code is ata_bmdma_setup().
*/
static void bfin_bmdma_setup(struct ata_queued_cmd *qc)
{
struct ata_port *ap = qc->ap;
/*下面結構體就是scatterlist 結構體的封裝,其指向所有scatterlist 在內存中的位置,對應以上步驟(2)*/
struct dma_desc_array *dma_desc_cpu =(struct dma_desc_array *)ap->bmdma_prd;
void __iomem *base = (void __iomem*)ap->ioaddr.ctl_addr;
/*DMA配置*/
unsigned short config = DMAFLOW_ARRAY |NDSIZE_5 | RESTART | WDSIZE_16 | DMAEN;
struct scatterlist *sg;
unsigned int si;
unsigned int channel;
unsigned int dir;
unsigned int size = 0;
dev_dbg(qc->ap->dev, "inatapi dma setup\n");
/* Program the ATA_CTRL register withdir */
/*設置ATA控制寄存器,與DMA控制寄存器無關*/
if (qc->tf.flags &ATA_TFLAG_WRITE) {
channel = CH_ATAPI_TX;
dir = DMA_TO_DEVICE;
} else {
channel = CH_ATAPI_RX;
dir = DMA_FROM_DEVICE;
config |= WNR;
}
dma_map_sg(ap->dev,qc->sg, qc->n_elem, dir);
/* fill the ATAPI DMA controller *//挨個填寫sg結構體,sg結構體用來指向每個要傳輸的內存塊,對應以上步驟(1)
for_each_sg(qc->sg, sg,qc->n_elem, si) {
dma_desc_cpu[si].start_addr =sg_dma_address(sg);
dma_desc_cpu[si].cfg = config;
dma_desc_cpu[si].x_count =sg_dma_len(sg) >> 1;
dma_desc_cpu[si].x_modify = 2;
size += sg_dma_len(sg);
}
/* Set the last descriptor to stop mode*/
dma_desc_cpu[qc->n_elem - 1].cfg&= ~(DMAFLOW | NDSIZE);
flush_dcache_range((unsignedint)dma_desc_cpu,
(unsigned int)dma_desc_cpu +
qc->n_elem *sizeof(struct dma_desc_array));
/* Enable ATA DMA operation*/
//設定從bmdma_prd_dma指//定的內存位置處獲取scatterlist結構體
set_dma_curr_desc_addr(channel, (unsigned long*)ap->bmdma_prd_dma);
//初始化DMA操作
set_dma_x_count(channel, 0);
set_dma_x_modify(channel, 0);
set_dma_config(channel, config);
SSYNC();
/* Send ATA DMA command */
/*這裏要注意,雖然是發送DMA命令,但是真正的DMA操作還沒開始;
*該函數中有設置各種ATA設備寄存器,並等待設置結束後返回
*/
bfin_exec_command(ap, &qc->tf);
//根據初始化時設置的IO操作方向,確定DMA方向,對應步驟(2)
if (qc->tf.flags &ATA_TFLAG_WRITE) {
/*set ATA DMA write direction */
ATAPI_SET_CONTROL(base,(ATAPI_GET_CONTROL(base)
| XFER_DIR));
} else {
/* set ATA DMA read direction*/
ATAPI_SET_CONTROL(base,(ATAPI_GET_CONTROL(base)
& ~XFER_DIR));
}
/* Reset all transfer count */
ATAPI_SET_CONTROL(base,ATAPI_GET_CONTROL(base) | TFRCNT_RST);
/* Set ATAPI state machine contorl interminate sequence */
ATAPI_SET_CONTROL(base,ATAPI_GET_CONTROL(base) | END_ON_TERM);
/* Set transfer length to the totalsize of sg buffers */
ATAPI_SET_XFER_LEN(base, size >>1);
/**
* bfin_bmdma_start- Start an IDE DMA transaction
* @qc:Info associated with this ATA transaction.
*
* Note:Original code is ata_bmdma_start().
*/ static void bfin_bmdma_start(structata_queued_cmd *qc)
{
struct ata_port *ap = qc->ap;
void __iomem *base = (void __iomem*)ap->ioaddr.ctl_addr;
dev_dbg(qc->ap->dev, "inatapi dma start\n");
if (!(ap->udma_mask ||ap->mwdma_mask))
return;
/* start ATAPI transfer*/
if (ap->udma_mask)
ATAPI_SET_CONTROL(base, ATAPI_GET_CONTROL(base)
| ULTRA_START);
else
ATAPI_SET_CONTROL(base,ATAPI_GET_CONTROL(base)
| MULTI_START);
}