n continuation of the previous text 第10章:中断处理-14: Interrupt-Driven I/O, let's GO ahead.
A Write-Buffering Example
We have mentioned the shortprint driver a couple of times; now it is time to actually take a look. This module implements a very simple, output-oriented driver for the parallel port; it is sufficient, however, to enable the printing of files. If you chose to test this driver out, however, remember that you must pass the printer a file in a format it understands; not all printers respond well when given a stream of arbitrary data.
我们已经多次提到 shortprint 驱动,现在是时候深入了解它了。这个模块为并行端口实现了一个非常简单的、面向输出的驱动;不过,它足以支持文件打印功能。如果你选择测试这个驱动,请注意必须向打印机发送它能识别的格式文件 —— 并非所有打印机都能正常处理任意数据流。
The shortprint driver maintains a one-page circular output buffer. When a user-space process writes data to the device, that data is fed into the buffer, but the write method does not actually perform any I/O. Instead, the core of shortp_write looks like this:
shortprint 驱动维护一个一页大小的循环输出缓冲区。当用户空间进程向该设备写入数据时,数据会被送入缓冲区,但 write 方法并不实际执行任何 I/O 操作。相反,shortp_write 的核心逻辑如下:
while (written < count) {
/* 等待缓冲区有可用空间 */
space = shortp_out_space();
if (space <= 0) {
// 若缓冲区满,进入等待队列,可被信号中断
if (wait_event_interruptible(shortp_out_queue,
(space = shortp_out_space()) > 0))
goto out; // 被信号中断,退出循环
}
/* 将数据写入缓冲区 */
// 确保写入数据量不超过用户请求的剩余长度
if ((space + written) > count)
space = count - written;
// 从用户空间复制数据到内核缓冲区
if (copy_from_user((char *) shortp_out_head, buf, space)) {
up(&shortp_out_sem); // 释放信号量
return -EFAULT; // 复制失败,返回错误
}
shortp_incr_out_bp(&shortp_out_head, space); // 更新缓冲区写指针
buf += space; // 移动用户空间数据指针
written += space; // 更新已写入字节数
/* 若当前无输出在进行,启动输出流程 */
spin_lock_irqsave(&shortp_out_lock, flags); // 加锁保护共享变量
if (!shortp_output_active)
shortp_start_output(); // 启动数据输出到硬件
spin_unlock_irqrestore(&shortp_out_lock, flags); // 解锁并恢复中断状态
}
out:
*f_pos += written; // 更新文件偏移量
A semaphore (shortp_out_sem) controls access to the circular buffer; shortp_write obtains that semaphore just prior to the code fragment above. While holding the semaphore, it attempts to feed data into the circular buffer. The function shortp_out_space returns the amount of contiguous space available (so there is no need to worry about buffer wraps); if that amount is 0, the driver waits until some space is freed. It then copies as much data as it can into the buffer.
一个信号量(shortp_out_sem)用于控制对循环缓冲区的访问;shortp_write 在进入上述代码片段前会先获取该信号量。持有信号量期间,它会尝试将数据写入循环缓冲区。shortp_out_space 函数返回缓冲区中连续的可用空间大小(因此无需担心缓冲区环绕问题);若返回值为 0,驱动会等待直到有空间释放,之后将尽可能多的数据复制到缓冲区中。
Once there is data to output, shortp_write must ensure that the data is written to the device. The actual writing is done by way of a workqueue function; shortp_write must kick that function off if it is not already running. After obtaining a separate spinlockthat controls access to variables used on the consumer side of the output buffer (including shortp_output_active), it calls shortp_start_output if need be. Then it’s just a matter of noting how much data was “written” to the buffer and returning.
The function that starts the output process looks like the following:
一旦有数据需要输出,shortp_write 必须确保数据被写入设备。实际的写入操作通过工作队列函数完成;如果该函数尚未运行,shortp_write 必须启动它。在获取一个独立的自旋锁(用于控制对输出缓冲区 “消费者端” 变量的访问,包括 shortp_output_active)后,会根据需要调用 shortp_start_output。之后只需记录 “写入” 到缓冲区的数据量并返回即可。启动输出过程的函数如下:
static void shortp_start_output(void)
{
if (shortp_output_active) /* Should never happen */
return;
/* Set up our 'missed interrupt' timer */
shortp_output_active = 1;
shortp_timer.expires = jiffies + TIMEOUT;
add_timer(&shortp_timer);
/* And get the process going. */
queue_work(shortp_workqueue, &shortp_work);
}
The reality of dealing with hardware is that you can, occasionally, lose an interrupt from the device. When this happens, you really do not want your driver to stop forevermore until the system is rebooted; that is not a user-friendly way of doing things. It is far better to realize that an interrupt has been missed, pickup the pieces, and go on. To that end, shortprint sets a kernel timer whenever it outputs data to the device. If the timer expires, we may have missed an interrupt. We lookat the timer function shortly, but, for the moment, let’s stickwith the main output functionality. That is implemented in our workqueue function, which, as you can see above, is scheduled here. The core of that function looks like the following:
与硬件交互的实际情况是,偶尔可能会丢失设备的中断。这种情况下,绝不能让驱动一直停滞到系统重启(这不是用户友好的设计)。更好的做法是检测到中断丢失后,恢复状态并继续工作。为此,shortprint 驱动在向设备输出数据时,都会设置一个内核计时器。如果计时器到期,说明可能丢失了中断。我们稍后再看计时器函数,现在先聚焦核心输出功能 —— 这部分由上面代码中调度的工作队列函数实现,其核心逻辑如下:
spin_lock_irqsave(&shortp_out_lock, flags); // 加锁保护共享变量
/* 所有数据都已写入设备? */
if (shortp_out_head == shortp_out_tail) { /* 缓冲区为空 */
shortp_output_active = 0; // 标记输出结束
wake_up_interruptible(&shortp_empty_queue); // 唤醒等待缓冲区为空的进程
del_timer(&shortp_timer); // 删除计时器
}
/* 还有数据,继续写入一个字节 */
else
shortp_do_write(); // 向硬件写入一个字节
/* 若有进程等待,考虑唤醒它们 */
if (((PAGE_SIZE + shortp_out_tail - shortp_out_head) % PAGE_SIZE) > SP_MIN_SPACE)
{
wake_up_interruptible(&shortp_out_queue); // 唤醒等待缓冲区空间的写进程
}
spin_unlock_irqrestore(&shortp_out_lock, flags); // 解锁并恢复中断状态
Since we are dealing with the output side’s shared variables, we must obtain the spinlock. Then we look to see whether there is any more data to send out; if not, we note that output is no longer active, delete the timer, and wake up anybody who might have been waiting for the queue to become completely empty (this sort of wait is done when the device is closed). If, instead, there remains data to write, we call shortp_do_write to actually send a byte to the hardware.
Then, since we may have freed space in the output buffer, we consider waking up any processes waiting to add more data to that buffer. We do not perform that wakeup unconditionally, however; instead, we wait until a minimum amount of space is available. There is no point in awakening a writer every time we take one byte out of the buffer; the cost of awakening the process, scheduling it to run, and putting it backto sleep is too high for that. Instead, we should wait until that process is able to move a substantial amount of data into the buffer at once. This technique is common in buffering, interrupt-driven drivers.
For completeness, here is the code that actually writes the data to the port:
由于涉及输出端的共享变量,必须先获取自旋锁。之后检查是否还有数据需要发送:如果没有,就标记输出结束、删除计时器,并唤醒所有等待缓冲区为空的进程(这种等待通常发生在设备关闭时);如果还有数据,则调用 shortp_do_write 向硬件实际发送一个字节。
此外,由于可能释放了输出缓冲区的空间,需要考虑唤醒等待写入数据的进程。但不会无条件唤醒,而是等待缓冲区有最小可用空间时才操作 —— 每次从缓冲区取出一个字节就唤醒写进程毫无意义,进程唤醒、调度运行再睡眠的开销太大。只有当进程能一次性写入大量数据时才唤醒,这种技巧在缓冲型、中断驱动的驱动中很常见。为完整起见,以下是实际向端口写入数据的代码:
static void shortp_do_write(void)
{
unsigned char cr = inb(shortp_base + SP_CONTROL); // 读取控制寄存器值
/* 操作进行中,重置计时器 */
mod_timer(&shortp_timer, jiffies + TIMEOUT); // 更新计时器超时时间
/* 向设备发送一个字节(通过选通信号确认) */
outb_p(*shortp_out_tail, shortp_base + SP_DATA); // 写入数据寄存器
shortp_incr_out_bp(&shortp_out_tail, 1); // 更新缓冲区读指针
if (shortp_delay)
udelay(shortp_delay); // 可选延时(适配慢速设备)
outb_p(cr | SP_CR_STROBE, shortp_base + SP_CONTROL); // 置位选通信号(通知设备取数)
if (shortp_delay)
udelay(shortp_delay);
outb_p(cr & ~SP_CR_STROBE, shortp_base + SP_CONTROL); // 复位选通信号
}
Here, we reset the timer to reflect the fact that we have made some progress, strobe the byte out to the device, and update the circular buffer pointer.
The workqueue function does not resubmit itself directly, so only a single byte will be written to the device. At some point, the printer will, in its slow way, consume the byte and become ready for the next one; it will then interrupt the processor. The interrupt handler used in shortprint is short and simple:
这里会重置计时器(表示操作有进展),通过选通信号向设备发送一个字节,并更新循环缓冲区的读指针。
工作队列函数不会直接重新提交自身,因此每次仅向设备写入一个字节。之后,打印机(速度较慢)会处理完这个字节并准备好接收下一个,随后触发处理器中断。shortprint 驱动的中断处理程序简洁明了:
static irqreturn_t shortp_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
if (! shortp_output_active)
return IRQ_NONE;
/* Remember the time, and farm off the rest to the workqueue function */
do_gettimeofday(&shortp_tv);
queue_work(shortp_workqueue, &shortp_work);
return IRQ_HANDLED;
}
Since the parallel port does not require an explicit interrupt acknowledgment, all the interrupt handler really needs to do is to tell the kernel to run the workqueue function again.
由于并行端口不需要显式确认中断,中断处理程序的核心工作就是通知内核再次运行工作队列函数。
What if the interrupt never comes? The driver code that we have seen thus far would simply come to a halt. To keep that from happening, we set a timer back a few pages ago. The function that is executed when that timer expires is:
如果中断始终没有到来,前面的驱动代码会完全停滞。为避免这种情况,我们之前设置了计时器。计时器到期时执行的函数如下:
static void shortp_timeout(unsigned long unused)
{
unsigned long flags;
unsigned char status;
if (!shortp_output_active) // 若没有输出在进行,直接返回
return;
spin_lock_irqsave(&shortp_out_lock, flags); // 加锁保护
status = inb(shortp_base + SP_STATUS); // 读取状态寄存器
/* 若打印机仍忙,仅重置计时器 */
if ((status & SP_SR_BUSY) == 0 || (status & SP_SR_ACK)) {
shortp_timer.expires = jiffies + TIMEOUT; // 重新设定超时时间
add_timer(&shortp_timer); // 重新注册计时器
spin_unlock_irqrestore(&shortp_out_lock, flags);
return;
}
/* 否则,说明中断丢失 */
spin_unlock_irqrestore(&shortp_out_lock, flags);
shortp_interrupt(shortp_irq, NULL, NULL); // 手动调用中断处理程序
}
If no output is supposed to be active, the timer function simply returns; this keeps the timer from resubmitting itself when things are being shut down. Then, after taking the lock, we query the status of the port; if it claims to be busy, it simply hasn’t gotten around to interrupting us yet, so we reset the timer and return. Printers can, at times, take a very long time to make themselves ready; consider the printer that runs out of paper while everybody is gone over a long weekend. In such situations, there is nothing to do other than to wait patiently until something changes.
If, however, the printer claims to be ready, we must have missed its interrupt. In that case, we simply invoke our interrupt handler manually to get the output process moving again.
如果当前没有输出在进行,计时器函数直接返回(避免在关闭设备时重复提交)。之后获取锁并查询端口状态:如果打印机显示 “忙”,说明只是尚未触发中断,只需重置计时器并返回即可。打印机有时可能需要很长时间才能准备好(比如长周末期间没人时打印机缺纸),这种情况下只能耐心等待状态变化。
但如果打印机显示 “就绪”,则说明肯定丢失了中断。此时直接手动调用中断处理程序,恢复输出流程。
补充说明:
-
计时器的核心作用:作为中断的 “兜底机制”,避免因硬件故障或中断丢失导致驱动卡死,提升驱动的鲁棒性。
-
工作队列的设计逻辑:每次仅处理一个字节,通过中断触发下一次处理,契合打印机 “低速、逐字节” 的输出特性,避免占用过多 CPU 资源。
-
唤醒策略的优化:等待 “最小可用空间” 再唤醒写进程,平衡了 “及时响应” 和 “降低开销”,是缓冲驱动的经典优化手段。
-
读操作的类比实现:
shortprint不支持端口读取(仅返回中断时间信息),但中断驱动的读操作逻辑类似 —— 设备数据先读入驱动缓冲区,积累到一定量、满足读请求或超时后,再复制到用户空间。
10万+

被折叠的 条评论
为什么被折叠?



