磁盘DMA过程分析

摘要:
DMA磁盘写入过程概述:如果硬盘支持DMA,并且操作系统中启用了DMA,则每次读取或写入磁盘时都会涉及DMA操作。尽管文件系统对硬盘的I/O请求不是连续的,数据所在的物理内存页也是不连续的,但操作系统将组合这些不连续的内存页并启用DMA操作,从而可以一次性传输数据,这也可以有效地传输数据。剩下的工作是设置DMA寄存器,然后发送它。稍后我们将详细分析这部分代码。软件向磁盘设备发送DMA传输指令。

当我们在应用程序中编写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 voidbfin_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);

}

免责声明:文章转载自《磁盘DMA过程分析》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇java8学习之Collectors工厂类源码分析与实战微信公众号的文章爬取有三种方式下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

YUV422与RGB互相转换

  YUV422与RGB互相转换(经验证在IPNC与PC上都可以) 前一段时间在DM8168中进行颜色空间的转换,在网上找了些程序,自己也根据网上的改了下,由于能力问题,实在是不好意思说做了好几天才弄出来, 主要是因为YUV<—>RGB有各种各样的转换公式。在多次的实验修改后,终于找到了对的公式,共享出来,以便需要的人选择。 在监控系统中大多采...

C++获取时间函数

https://github.com/yangzijianGit/Time #include <Windows.h> #include <time.h> #include <winbase.h> unsigned long long GetCurrentTimeMsec() { #ifdef _WIN32 st...

RK3288 GMAC整理

一、源文件 源码路径:drivers etethernet ockchipgmac 源码阅读顺序: 二、重要探针函数stmmac_dvr_probe 1. alloc_etherdev 申请网卡设备和私有数据。 struct net_device *ndev = NULL; struct stmmac_priv *priv; ndev = alloc_e...

STM32CubeMx——串口使用DMA收发数据

用到的是DMA发送数据,DMA接收,在中断回调里发送出去。 一.代码生成 1.按以前的方法设置好时钟和调试方式,这里就不多说了。 2.设置串口1。 3.在DMASetting里点击Add添加USART1_TX,Mode有两种模式,一种是普通模式,使用一次发送语句就发一次,另一种是循环模式,使用一次发送会一直发送。这里发送我选择普通模式,接收选择循环模式...

十天学会单片机Day1点亮数码管(数码管、外部中断、定时器中断)

1.引脚定义 P3口各引脚第二功能定义 标号 引脚 第二功能 说明 P3.0 10 RXD 串行输入口 P3.1 11 TXD 串行输出口 P3.2 12 INT0(上划线) 外部中断0 P3.3 13 INT1(上划线) 外部中断1 P3.4 14 T0 定时器/计数器0 外部输入端 P3.5 15 T1 定时器/计数器1...

linux网络编程-socket(1)

 上面是对应的IpV4的地址结构: sin_len整个结构的大小 sin_family协议族,对应Tcp固定为AF_INET,除了tcp协议外还支持unix域协议等 sin_port socket通信的端口 sin_addr是一个无符号的32位的网络字节地址 上面的结构体仅仅支持IPv4地址协议,如果支持其他协议咱办了,后面引入了通用协议的地址协议 在...