📄 ch16s03.html
字号:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>16.3. 请求处理-Linux设备驱动第三版(中文版)-开发频道-华星在线</title>
<meta name="description" content="驱动开发-开发频道-华星在线" />
<meta name="keywords" content="Linux设备驱动,中文版,第三版,ldd,linux device driver,驱动开发,电子版,程序设计,软件开发,开发频道" />
<meta name="author" content="华星在线 www.21cstar.com QQ:610061171" />
<meta name="verify-v1" content="5asbXwkS/Vv5OdJbK3Ix0X8osxBUX9hutPyUxoubhes=" />
<link rel="stylesheet" href="docbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.69.0">
<link rel="start" href="index.html" title="Linux 设备驱动 Edition 3">
<link rel="up" href="ch16.html" title="第 16 章 块驱动">
<link rel="prev" href="ch16s02.html" title="16.2. 块设备操作">
<link rel="next" href="ch16s04.html" title="16.4. 一些其他的细节">
</head>
<body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
<div class="navheader">
<table width="100%" summary="Navigation header">
<tr><th colspan="3" align="center">16.3. 请求处理</th></tr>
<tr>
<td width="20%" align="left">
<a accesskey="p" href="ch16s02.html">上一页</a> </td>
<th width="60%" align="center">第 16 章 块驱动</th>
<td width="20%" align="right"> <a accesskey="n" href="ch16s04.html">下一页</a>
</td>
</tr>
</table>
<hr>
</div>
<div class="sect1" lang="zh-cn">
<div class="titlepage"><div><div><h2 class="title" style="clear: both">
<a name="RequestProcessing.sect1"></a>16.3. 请求处理</h2></div></div></div>
<p>每个块驱动的核心是它的请求函数. 这个函数是真正做工作的地方 --或者至少开始的地方; 剩下的都是开销. 因此, 我们花不少时间来看在块驱动中的请求处理.</p>
<p>一个磁盘驱动的性能可能是系统整个性能的关键部分. 因此, 内核的块子系统编写时在性能上考虑了很多; 它做所有可能的事情来使你的驱动从它控制的设备上获得最多. 这是一个好事情, 其中它盲目地使能快速 I/O. 另一方面, 块子系统没必要在驱动 API 中曝露大量复杂性. 有可能编写一个非常简单的请求函数( 我们将很快见到 ), 但是如果你的驱动必须在一个高层次上操作复杂的硬件, 它可能是任何样子.</p>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="IntroductiontotherequestMethod.sect2"></a>16.3.1. 对请求方法的介绍</h3></div></div></div>
<p>块驱动的请求方法有下面的原型:</p>
<pre class="programlisting">
void request(request_queue_t *queue);
</pre>
<p>这个函数被调用, 无论何时内核认为你的驱动是时候处理对设备的读, 写, 或者其他操作. 请求函数在返回之前实际不需要完成所有的在队列中的请求; 实际上, 它可能不完成它们任何一个, 对大部分真实设备. 它必须, 但是, 驱动这些请求并且确保它们最终被驱动全部处理.</p>
<p>每个设备有一个请求队列. 这是因为实际的从和到磁盘的传输可能在远离内核请求它们时发生, 并且因为内核需要这个灵活性来调度每个传送, 在最好的时刻(将影响磁盘上邻近扇区的请求集合到一起, 例如). 并且这个请求函数, 你可能记得, 和一个请求队列相关, 当这个队列被创建时. 让我们回顾 sbull 如何创建它的队列: </p>
<pre class="programlisting">
dev->queue = blk_init_queue(sbull_request, &dev->lock);
</pre>
<p>这样, 当这个队列被创建时, 请求函数和它关联到一起. 我们还提供了一个自旋锁作为队列创建过程的一部分. 无论何时我们的请求函数被调用, 内核持有这个锁. 结果, 请求函数在原子上下文中运行; 它必须遵循所有的 5 章讨论过的原子代码的通用规则.</p>
<p>在你的请求函数持有锁时, 队列锁还阻止内核去排队任何对你的设备的其他请求. 在一些条件下, 你可能考虑在请求函数运行时丢弃这个锁. 如果你这样做, 但是, 你必须保证不存取请求队列, 或者任何其他的被这个锁保护的数据结构, 在这个锁不被持有时. 你必须重新请求这个锁, 在请求函数返回之前. </p>
<p>最后, 请求函数的启动(常常地)与任何用户空间进程之间是完全异步的. 你不能假设内核运行在发起当前请求的进程上下文. 你不知道由这个请求提供的 I/O 缓冲是否在内核或者用户空间. 因此任何类型的明确存取用户空间的操作都是错误的并且将肯定引起麻烦. 如你将见到的, 你的驱动需要知道的关于请求的所有事情, 都包含在通过请求队列传递给你的结构中.</p>
</div>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="ASimplerequestMethod.sect2"></a>16.3.2. 一个简单的请求方法</h3></div></div></div>
<p>sbull 例子驱动提供了几个不同的方法给请求处理. 缺省地, sbull 使用一个方法, 称为 sbull_request, 它打算作为一个最简单地请求方法的例子. 别忙, 它在这里:</p>
<pre class="programlisting">
static void sbull_request(request_queue_t *q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL) {
struct sbull_dev *dev = req->rq_disk->private_data;
if (! blk_fs_request(req)) {
printk (KERN_NOTICE "Skip non-fs request\n");
end_request(req, 0);
continue;
}
sbull_transfer(dev, req->sector, req->current_nr_sectors,
req->buffer, rq_data_dir(req));
end_request(req, 1);
}
}
</pre>
<p>这个函数介绍了 struct request 结构. 我们之后将详细检查 struct request; 现在, 只需说它表示一个我们要执行的块 I/O 请求.</p>
<p>内核提供函数 elv_next_request 来获得队列中第一个未完成的请求; 当没有请求要被处理时这个函数返回 NULL. 注意 elf_next 不从队列里去除请求. 如果你连续调用它 2 次, 它 2 次都返回同一个请求结构. 在这个简单的操作模式中, 请求只在它们完成时被剥离队列.</p>
<p>一个块请求队列可包含实际上不从磁盘和自磁盘移动块的请求. 这些请求可包括供应商特定的, 低层的诊断操作或者和特殊设备模式相关的指令, 例如给可记录介质的报文写模式. 大部分块驱动不知道如何处理这样的请求, 并且简单地失败它们; sbull 也以这种方式工作. 对 block_fs_request 的调用告诉我们是否我们在查看一个文件系统请求--一个一旦数据块的. 如果这个请求不是一个文件系统请求, 我们传递它到 end_request:</p>
<pre class="programlisting">
void end_request(struct request *req, int succeeded);
</pre>
<p>当我们处理了非文件系统请求, 之后我们传递 succeeded 为 0 来指示我们没有成功完成这个请求. 否则, 我们调用 sbull_transfer 来真正移动数据, 使用一套在请求结构中提供的成员:</p>
<div class="variablelist"><dl>
<dt><span class="term"><span>sector_t sector;</span></span></dt>
<dd><p>我们设备上起始扇区的索引. 记住这个扇区号, 象所有这样的在内核和驱动之间传递的数目, 是以 512-字节扇区来表示的. 如果你的硬件使用一个不同的扇区大小, 你需要相应地调整扇区. 例如, 如果硬件是 2048-字节的扇区, 你需要用 4 来除起始扇区号, 在安放它到对硬件的请求之前.</p></dd>
<dt><span class="term"><span>unsigned long nr_sectors;</span></span></dt>
<dd><p>要被传送的扇区(512-字节)数目.</p></dd>
<dt><span class="term"><span>char *buffer;</span></span></dt>
<dd><p>一个指向缓冲的指针, 数据应当被传送到或者从的缓冲. 这个指针是一个内核虚拟地址并且可被驱动直接解引用, 如果需要.</p></dd>
<dt><span class="term"><span>rq_data_dir(struct request *req);</span></span></dt>
<dd><p>这个宏从请求中抽取传送的方向; 一个 0 返回值表示从设备中读, 非 0 返回值表示写入设备.</p></dd>
</dl></div>
<p>有了这个信息, sbull 驱动可实现实际的数据传送, 使用一个简单的 memcpy 调用 -- 我们数据已经在内存, 毕竟. 进行这个拷贝操作的函数( sbull_transfer ) 也处理扇区大小的调整, 并确保我们没有拷贝超过我们的虚拟设备的尾.</p>
<pre class="programlisting">
static void sbull_transfer(struct sbull_dev *dev, unsigned long sector, unsigned long nsect, char *buffer, int write)
{
unsigned long offset = sector*KERNEL_SECTOR_SIZE;
unsigned long nbytes = nsect*KERNEL_SECTOR_SIZE;
if ((offset + nbytes) > dev->size)
{
printk (KERN_NOTICE "Beyond-end write (%ld %ld)\n", offset, nbytes);
return;
}
if (write)
memcpy(dev->data + offset, buffer, nbytes);
else
memcpy(buffer, dev->data + offset, nbytes);
}
</pre>
<p>用这个代码, sbull 实现了一个完整的, 简单的基于 RAM 的磁盘设备. 但是, 对于很多类型的设备, 它不是一个实际的驱动, 由于几个理由.</p>
<p>这些原因的第一个是 sbull 同步执行请求, 一次一个. 高性能的磁盘设备能够在同时有很多个请求停留; 磁盘的板上控制器因此可以优化的顺序(有人希望)执行它们. 如果我们只处理队列中的第一个请求, 我们在给定时间不能有多个请求被满足. 能够工作于多个请求要求对请求队列和请求结构的深入理解; 下面几节会帮助来建立这种理解.</p>
<p>但是, 有另外一个问题要考虑. 当系统进行大的传输, 包含多个在一起的磁盘扇区, 就获得最好的性能. 磁盘操作的最高开销常常是读写头的定位; 一旦这个完成, 实际上需要的读或者写数据的时间几乎可忽略. 设计和实现文件系统和虚拟内存子系统的开发者理解这点, 因此他们尽力在磁盘上连续地查找相关的数据, 并且在一次请求中传送尽可能多扇区. 块子系统也在这个方面起作用; 请求队列包含大量逻辑,目的是找到邻近的请求并且接合它们为更大的操作.</p>
<p>sbull 驱动, 但是, 采取所有这些工作并且简单地忽略它. 一次只有一个缓冲被传送, 意味着最大的单次传送几乎从不超过单个页的大小. 一个块驱动能做的比那个要好的多, 但是它需要一个对请求结构和bio结构的更深的理解, 请求是从它们建立的.</p>
<p>下面几节更深入地研究块层如何完成它的工作, 已经这些工作导致的数据结构.</p>
</div>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="RequestQueues.sect2"></a>16.3.3. 请求队列</h3></div></div></div>
<p>最简单的说, 一个块请求队列就是: 一个块 I/O 请求的队列. 如果你往下查看, 一个请求队列是一令人吃惊得复杂的数据结构. 幸运的是, 驱动不必担心大部分的复杂性.</p>
<p>请求队列跟踪等候的块I/O请求. 但是它们也在这些请求的创建中扮演重要角色. 请求队列存储参数, 来描述这个设备能够支持什么类型的请求: 它们的最大大小, 多少不同的段可进入一个请求, 硬件扇区大小, 对齐要求, 等等. 如果你的请求队列被正确配置了, 它应当从不交给你一个你的设备不能处理的请求.</p>
<p>请求队列还实现一个插入接口, 这个接口允许使用多 I/O 调度器(或者电梯). 一个 I/O 调度器的工作是提交 I/O 请求给你的驱动, 以最大化性能的方式. 为此, 大部分 I/O 调度器累积批量的 I/O 请求, 排列它们为递增(或递减)的块索引顺序, 并且以那个顺序提交请求给驱动. 磁头, 当给定一列排序的请求时, 从磁盘的一头到另一头工作, 非常象一个满载的电梯, 在一个方向移动直到所有它的"请求"(等待出去的人)已被满足. 2.6 内核包含一个"底线调度器", 它努力确保每个请求在预设的最大时间内被满足, 以及一个"预测调度器", 它实际上短暂停止设备, 在一个预想中的读请求之后, 这样另一个邻近的读将几乎是马上到达. 到本书为止, 缺省的调度器是预测调度器, 它看来有最好的交互的系统性能.</p>
<p>I/O 调度器还负责合并邻近的请求. 当一个新 I/O 请求被提交给调度器, 它在队列里搜寻包含邻近扇区的请求; 如果找到一个, 并且如果结果的请求不是太大, 这 2 个请求被合并.</p>
<p>请求队列有一个 struct request_queue 或者 request_queue_t 类型. 这个类型, 和许多操作它的函数, 定义在 <linux/blkdev.h>. 如果你对请求队列的实现感兴趣, 你可找到大部分代码在 drivers/block/ll_rw_block.c 和 elevator.c.</p>
<div class="sect3" lang="zh-cn">
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -