第10章:中断处理-15: A Write-Buffering Example

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.

如果当前没有输出在进行,计时器函数直接返回(避免在关闭设备时重复提交)。之后获取锁并查询端口状态:如果打印机显示 “忙”,说明只是尚未触发中断,只需重置计时器并返回即可。打印机有时可能需要很长时间才能准备好(比如长周末期间没人时打印机缺纸),这种情况下只能耐心等待状态变化。

但如果打印机显示 “就绪”,则说明肯定丢失了中断。此时直接手动调用中断处理程序,恢复输出流程。

补充说明:

  1. 计时器的核心作用​​​​​​​​​​​​​​​​​​​​​作为中断的 “兜底机制”,避免因硬件故障或中断丢失导致驱动卡死,提升驱动的鲁棒性。

  2. 工作队列的设计逻辑​​​​​​​每次仅处理一个字节,通过中断触发下一次处理,契合打印机 “低速、逐字节” 的输出特性,避免占用过多 CPU 资源。

  3. 唤醒策略的优化​​​​​​​​​​​​​等待 “最小可用空间” 再唤醒写进程,平衡了 “及时响应” 和 “降低开销”,是缓冲驱动的经典优化手段。

  4. 读操作的类比实现shortprint 不支持端口读取(仅返回中断时间信息),但中断驱动的读操作逻辑类似 —— 设备数据先读入驱动缓冲区,积累到一定量、满足读请求或超时后,再复制到用户空间。

### ESP32-S3 Print to Stdout and Stderr Examples In the context of programming an ESP32-S3, directing output specifically to `stdout` or `stderr` can be achieved through custom logging functions that utilize file descriptors associated with these streams. Given that `stdout` operates under line buffering by default while `stderr` is unbuffered[^1], it's important to understand how this behavior impacts debugging and monitoring applications running on microcontrollers like the ESP32-S3. For demonstration purposes, consider implementing a simple example using C++ within the Arduino IDE framework for ESP32 devices: #### Example Code Demonstrating Output to Stdout and Stderr ```cpp #include "Arduino.h" void setup() { Serial.begin(115200); // Redirecting stdout and stderr to serial port. setvbuf(stdout, NULL, _IONBF, 0); // Disable buffering for stdout printf("This message goes to stdout\n"); fprintf(stderr, "This error message goes directly to stderr without waiting.\n"); } void loop() { delay(1000); } ``` The above code snippet initializes communication at a baud rate suitable for observing outputs via USB-to-serial connection. By disabling buffering for `stdout`, messages sent there will behave similarly to those directed towards `stderr`: they are immediately flushed out upon execution of the respective print statements. Additionally, when working in environments where Python might interact with such hardware (for instance, during model training phases as mentioned), ensuring proper handling of standard input/output streams becomes crucial especially around package management tasks involving tools like `pip`. However, direct interaction between Python scripts and ESP32-S3 concerning I/O redirection would typically occur over UART interfaces rather than modifying internal C/C++ program behaviors[^2]. --related questions-- 1. How does one modify buffer settings for different types of streams in embedded systems? 2. What methods exist for enhancing debug information visibility from ESP32-S3 projects? 3. Can you explain more about setting up virtual environments specific to ESP32 development workflows? 4. In what scenarios should developers prefer using `fprintf` over `printf` in their programs targeting microcontroller units?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值