📄 ch16s03.html
字号:
<div class="titlepage"><div><div><h4 class="title">
<a name="Queuecreationanddeletion.sect3"></a>16.3.3.1. 队列的创建和删除</h4></div></div></div>
<p>如同我们在我们的例子代码中见到的, 一个请求队列是一个动态的数据结构, 它必须被块 I/O 子系统创建. 这个创建和初始化一个队列的函数是:</p>
<pre class="programlisting">
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
</pre>
<p>当然, 参数是, 这个队列的请求函数和一个控制对队列存取的自旋锁. 这个函数分配内存(实际上, 不少内存)并且可能失败因为这个; 你应当一直检查返回值, 在试图使用这个队列之前.</p>
<p>作为初始化一个请求队列的一部分, 你可设置成员 queuedata(它是一个 void * 指针 )为任何你喜欢的值. 这个成员是请求队列的对于我们在其他结构中见到的 private_data 的对等体.</p>
<p>为返回一个请求队列给系统(在模块卸载时间, 通常), 调用 blk_cleanup_queue:</p>
<pre class="programlisting">
void blk_cleanup_queue(request_queue_t *);
</pre>
<p>这个调用后, 你的驱动从给定的队列中不再看到请求,并且不应当再次引用它.</p>
</div>
<div class="sect3" lang="zh-cn">
<div class="titlepage"><div><div><h4 class="title">
<a name="Queueingfunctions.sect3"></a>16.3.3.2. 排队函数</h4></div></div></div>
<p>有非常少的函数来操作队列中的请求 -- 至少, 考虑到驱动. 你必须持有队列锁, 在你调用这些函数之前.</p>
<p>返回要处理的下一个请求的函数是 elv_next_request:</p>
<pre class="programlisting">
struct request *elv_next_request(request_queue_t *queue);
</pre>
<p>我们已经在简单的 sbull 例子中见到这个函数. 它返回一个指向下一个要处理的请求的指针(由 I/O 调度器所决定的)或者 NULL 如果没有请求要处理. elv_next_request 留这个请求在队列上, 但是标识它为活动的; 这个标识阻止了 I/O 调度器试图合并其他的请求到这些你开始执行的.</p>
<p>为实际上从一个队列中去除一个请求, 使用 blkdev_dequeue_request:</p>
<pre class="programlisting">
void blkdev_dequeue_request(struct request *req);
</pre>
<p>如果你的驱动同时从同一个队列中操作多个请求, 它必须以这样的方式将它们解出队列.</p>
<p>如果你由于同样的理由需要放置一个出列请求回到队列中, 你可以调用:</p>
<pre class="programlisting">
void elv_requeue_request(request_queue_t *queue, struct request *req);
</pre>
</div>
<div class="sect3" lang="zh-cn">
<div class="titlepage"><div><div><h4 class="title">
<a name="Queuecontrolfunctions.sect3"></a>16.3.3.3. 队列控制函数</h4></div></div></div>
<p>块层输出了一套函数, 可被驱动用来控制一个请求队列如何操作. 这些函数包括:</p>
<div class="variablelist"><dl>
<dt><span class="term"><span>void blk_stop_queue(request_queue_t *queue);</span></span></dt>
<dd></dd>
<dt><span class="term"><span>void blk_start_queue(request_queue_t *queue);</span></span></dt>
<dd><p>如果你的设备已到到达一个状态, 它不能处理等候的命令, 你可调用 blk_stop_queue 来告知块层. 在这个调用之后, 你的请求函数将不被调用直到你调用 blk_start_queue. 不用说, 你不应当忘记重启队列, 当你的设备可处理更多请求时. 队列锁必须被持有当调用任何一个这些函数时.</p></dd>
<dt><span class="term"><span>void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);</span></span></dt>
<dd><p>告知内核你的设备可进行 DMA 的最高物理地址的函数. 如果一个请求包含一个超出这个限制的内存引用, 一个反弹缓冲将被用来给这个操作; 当然, 这是一个进行块 I/O 的昂贵方式, 并且应当尽量避免. 你可在这个参数中提供任何可能的值, 或者使用预先定义的符号 BLK_BOUNCE_HIGH(使用反弹缓冲给高内存页), BLK_BOUNCE_ISA (驱动只可 DMA 到 16MB 的 ISA 区), 或者BLK_BOUCE_ANY(驱动可进行 DMA 到任何地址). 缺省值是 BLK_BOUNCE_HIGH.</p></dd>
<dt><span class="term"><span>void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);</span></span></dt>
<dd></dd>
<dt><span class="term"><span>void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max);</span></span></dt>
<dd></dd>
<dt><span class="term"><span>void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);</span></span></dt>
<dd></dd>
<dt><span class="term"><span>void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);</span></span></dt>
<dd><p>设置参数的函数, 这些参数描述可被设备满足的请求. blk_queue_max 可用来以扇区方式设置任一请求的最大的大小; 缺省是 255. blk_queue_max_phys_segments 和 blk_queue_max_hw_segments 都控制多少物理段(系统内存中不相邻的区)可包含在一个请求中. 使用 blk_queue_max_phys_segments 来说你的驱动准备处理多少段; 例如, 这可能是一个静态分配的散布表的大小. blk_queue_max_hw_segments, 相反, 是设备可处理的最多的段数. 这 2 个参数缺省都是 128. 最后, blk_queue_max_segment_size 告知内核任一个请求的段可能是多大字节; 缺省是 65,536 字节.</p></dd>
<dt><span class="term"><span>blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);</span></span></dt>
<dd><p>一些设备无法处理跨越一个特殊大小内存边界的请求; 如果你的设备是其中之一, 使用这个函数来告知内核这个边界. 例如, 如果你的设备处理跨 4-MB 边界的请求有困难, 传递一个 0x3fffff 掩码. 缺省的掩码是 0xffffffff.</p></dd>
<dt><span class="term"><span>void blk_queue_dma_alignment(request_queue_t *queue, int mask);</span></span></dt>
<dd><p>告知内核关于你的设备施加于 DMA 传送的内存对齐限制的函数. 所有的请求被创建有给定的对齐, 并且请求的长度也匹配这个对齐. 缺省的掩码是 0x1ff, 它导致所有的请求被对齐到 512-字节边界. </p></dd>
<dt><span class="term"><span>void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);</span></span></dt>
<dd><p>告知内核你的设备的硬件扇区大小. 所有由内核产生的请求是这个大小的倍数并且被正确对齐. 所有的在块层和驱动之间的通讯继续以 512-字节扇区来表达, 但是.</p></dd>
</dl></div>
</div>
</div>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="TheAnatomyofaRequest.sect2"></a>16.3.4. 请求的分析</h3></div></div></div>
<p>在我们的简单例子里, 我们遇到了这个请求结构. 但是, 我们未曾接触这个复杂的数据结构. 在本节, 我们看, 详细地, 块 I/O 请求在 Linux 内核中如何被表示.</p>
<p>每个请求结构代表一个块 I/O 请求, 尽管它可能是由几个独立的请求在更高层次合并而成. 对任何特殊的请求而传送的扇区可能分布在整个主内存, 尽管它们常常对应块设备中的多个连续的扇区. 这个请求被表示为多个段, 每个对应一个内存中的缓冲. 内核可能合并多个涉及磁盘上邻近扇区的请求, 但是它从不合并在单个请求结构中的读和写操作. 内核还确保不合并请求, 如果结果会破坏任何的在前面章节中描述的请求队列限制.</p>
<p>基本上, 一个请求结构被实现为一个 bio 结构的链表, 结合一些维护信息来使驱动可以跟踪它的位置, 当它在完成这个请求中. 这个 bio 结构是一个块 I/O 请求移植的低级描述; 我们现在看看它.</p>
<div class="sect3" lang="zh-cn">
<div class="titlepage"><div><div><h4 class="title">
<a name="Thebiostructure.sect3"></a>16.3.4.1. bio 结构</h4></div></div></div>
<p>当内核, 以一个文件系统的形式, 虚拟文件子系统, 或者一个系统调用, 决定一组块必须传送到或从一个块 I/O 设备; 它装配一个 bio 结构来描述那个操作. 那个结构接着被递给这个块 I/O 代码, 这个代码合并它到一个存在的请求结构, 或者, 如果需要, 创建一个新的. 这个 bio 结构包含一个块驱动需要来进行请求的任何东西, 而不必涉及使这个请求启动的用户空间进程.</p>
<p>bio 结构, 在 <linux/bio.h> 中定义, 包含许多成员对驱动作者是有用的:</p>
<div class="variablelist"><dl>
<dt><span class="term"><span>sector_t bi_sector;</span></span></dt>
<dd><p>这个 bio 要被传送的第一个(512字节)扇区.</p></dd>
<dt><span class="term"><span>unsigned int bi_size;</span></span></dt>
<dd><p>被传送的数据大小, 以字节计. 相反, 常常更易使用 bio_sectors(bio), 一个给定以扇区计的大小的宏.</p></dd>
<dt><span class="term"><span>unsigned long bi_flags;</span></span></dt>
<dd><p>一组描述 bio 的标志; 最低有效位被置位如果这是一个写请求(尽管宏 bio_data_dir(bio)应当用来代替直接加锁这个标志).</p></dd>
<dt><span class="term"><span>unsigned short bio_phys_segments;</span></span></dt>
<dd></dd>
<dt><span class="term"><span>unsigned short bio_hw_segments;</span></span></dt>
<dd><p>包含在这个 BIO 中的物理段的数目, 和在 DMA 映射完成后被硬件看到的段数目, 分别地.</p></dd>
</dl></div>
<p>一个 bio 的核心, 但是, 是一个称为 bi_io_vec 的数组, 它由下列结构组成:</p>
<pre class="programlisting">
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
</pre>
<p>图 <a href="ch16s03.html#ldd3-16-1.fig" title="图 16.1. bio 结构">bio 结构</a>显示了这些结构如何结合在一起. 如同你所见到的, 在一个块 I/O 请求被转换为一个 bio 结构后, 它已被分为单独的物理内存页. 所有的一个驱动需要做的事情是步进全部这个结构数组(它们有 bi_vcnt 个), 和在每个页内传递数据(但是只 len 字节, 从 offset 开始).</p>
<div class="figure">
<a name="ldd3-16-1.fig"></a><p class="title"><b>图 16.1. bio 结构</b></p>
<div><img src="images/ldd3-16-1.png" alt="bio 结构"></div>
</div>
<p>直接使用 bi_io_vec 数组不被推荐, 为了内核开发者可以在以后改变 bio 结构而不会引起破坏. 为此, 一组宏被提供来简化使用 bio 结构. 开始的地方是 bio_for_each_segment, 它简单地循环 bi_io_vec 数组中每个未被处理的项. 这个宏应当如下用: </p>
<pre class="programlisting">
int segno;
struct bio_vec *bvec;
bio_for_each_segment(bvec, bio, segno) {
/* Do something with this segment
}
</pre>
<p>在这个循环中, bvec 指向当前的 bio_vec 项, 并且 segno 是当前的段号. 这些值可被用来设置 DMA 发送器(一个使用 blk_rq_map_sg 的替代方法在"块请求和 DMA"一节中描述). 如果你需要直接存取页, 你应当首先确保一个正确的内核虚拟地址存在; 为此, 你可使用:</p>
<pre class="programlisting">
char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
void __bio_kunmap_atomic(char *buffer, enum km_type type);
</pre>
<p>这个底层的函数允许你直接映射在一个给定的 bio_vec 中找到的缓冲, 由索引 i 所指定的. 一个原子的 kmap 被创建; 调用者必须提供合适的来使用的槽位(如同在 15 章的"内存映射和 struct page"一节中描述的).</p>
<p>块层还维护一组位于 bio 结构的指针来跟踪请求处理的当前状态. 几个宏来提供对这个状态的存取:</p>
<div class="variablelist"><dl>
<dt><span class="term"><span>struct page *bio_page(struct bio *bio);</span></span></dt>
<dd><p>返回一个指向页结构的指针, 表示下一个被传送的页.</p></dd>
<dt><span class="term"><span>int bio_offset(struct bio *bio);</span></span></dt>
<dd><p>返回页内的被传送的数据的偏移.</p></dd>
<dt><span class="term"><span>int bio_cur_sectors(struct bio *bio);</span></span></dt>
<dd><p>返回要被传送出当前页的扇区数.</p></dd>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -