5.6: 关于中断的问题

问题一: 进程相应键盘输入的处理流程

用户进程等待键盘输入,一般会使得当前进程让出cpu,等键盘输入一个按键,会触发中断用户进程从阻塞到参与调度状态,等到调度到时才处理中断并读取键盘输入是吗,这样键盘敲击是不是来回不断调用中断,还是基于缓冲buffer的机制,读取多少个字符才触发中断?

回答

用户进程等待键盘输入确实会经历一个阻塞和唤醒的过程,但具体的行为是基于 中断触发机制缓冲区机制相结合的

键盘输入的数据会首先被累积到内核的缓冲区buffer中, 当输入达到“换行符”或某些特定条件(如缓冲区满)时,才会通知用户进程可以读取。

中断触发机制

键盘输入触发中断 按键触发中断:每次按键(无论是单个字符还是特殊键)都会触发 UART 硬件中断。

中断处理:

  • 中断处理函数 uartintr() 负责从 UART 的 RHR 寄存器读取按键数据。

  • 读取的数据被存储到驱动程序维护的缓冲区中(例如,环形缓冲区)。

  • 特殊键(如退格键或 Control-U)在中断处理函数中直接处理。

  • 如果缓冲区中已经累积了一整行数据,uartintr()会调用wakeup()唤醒阻塞的用户进程。

  • 中断驱动写入:

每次用户按下键盘按键时,UART 设备触发中断。 中断处理函数(uartintr())负责从 UART 的 RHR 寄存器读取字符数据。 读取的字符数据就会被写入环形缓冲区。

以下是中断处理写入缓冲区的函数

// handle a uart interrupt, raised because input has
// arrived, or the uart is ready for more output, or
// both. called from devintr().
void
uartintr(void)
{
  // read and process incoming characters.
  while(1){
    int c = uartgetc();
    if(c == -1)
      break;
    consoleintr(c);
  }

  // send buffered characters.
  acquire(&uart_tx_lock);
  uartstart();
  release(&uart_tx_lock);
}

缓冲区机制

  • 键盘输入的数据会首先被累积到内核的缓冲区中

  • 当输入达到“换行符”或某些特定条件(如缓冲区满)时,才会通知用户进程可以读取。

具体解释如下

缓冲区的定义在console.c下,

struct {
  struct spinlock lock;
  
  // input
#define INPUT_BUF_SIZE 128
  char buf[INPUT_BUF_SIZE];
  uint r;  // Read index  读取位置(被用户进程消费的数据位置)
  uint w;  // Write index 写入位置(新输入的数据写入到此位置)
  uint e;  // Edit index 编辑位置(用于支持用户在输入时的编辑操作)
} cons;
  • 通过 cons.buf 的 r, w, 和 e 指针的配合,输入缓冲区实现了环形的读取和写入逻辑。

相关代码如consoleintr() 的目的是用于处理键盘中断,每次按键输入时调用。 数据被写入 cons.buf 缓冲区。

// the console input interrupt handler.
// uartintr() calls this for input character.
// do erase/kill processing, append to cons.buf,
// wake up consoleread() if a whole line has arrived.
//
void
consoleintr(int c)
{
  acquire(&cons.lock);

  switch(c){
  case C('P'):  // Print process list.
    procdump();
    break;
  case C('U'):  // Kill line.
    while(cons.e != cons.w &&
          cons.buf[(cons.e-1) % INPUT_BUF_SIZE] != '\n'){
      cons.e--;
      consputc(BACKSPACE);
    }
    break;
  case C('H'): // Backspace
  case '\x7f': // Delete key
    if(cons.e != cons.w){
      cons.e--;
      consputc(BACKSPACE);
    }
    break;
  default:
    if(c != 0 && cons.e-cons.r < INPUT_BUF_SIZE){
      c = (c == '\r') ? '\n' : c;

      // echo back to the user.
      consputc(c);

      // store for consumption by consoleread().
      cons.buf[cons.e++ % INPUT_BUF_SIZE] = c;

      if(c == '\n' || c == C('D') || cons.e-cons.r == INPUT_BUF_SIZE){
        // wake up consoleread() if a whole line (or end-of-file)
        // has arrived. 如果遇到换行符或缓冲区满,则更新写指针 cons.w 并唤醒用户进程
        cons.w = cons.e;
        wakeup(&cons.r);
      }
    }
    break;
  }
  
  release(&cons.lock);
}

结论

所以说结论是:每次按键都会触发中断,因此中断的触发频率与用户的键盘输入频率一致.由于输入是基于缓冲区的,用户进程并不是每次中断都被唤醒。只有当缓冲区中累积了一整行(或者满足一定条件)时,才唤醒用户进程处理。

中断程序能执行快速响应和回显,行缓冲存在的意义是什么?

  1. 提高系统性能 减少系统调用开销: 如果每输入一个字符就需要触发系统调用,将字符传递给应用程序,会频繁切换内核态和用户态,导致性能下降。行缓冲模式通过累积输入数据,等待用户按下回车键后一次性传递给应用程序,从而减少系统调用的次数。

缓解 I/O 开销: 输入设备通常速度较慢,逐字符处理会占用更多的 CPU 时间。行缓冲机制允许输入积累成块,提升 I/O 效率。

  1. PCB 中与 I/O 中断相关的字段 在 xv6 中,struct proc 包含了一些与 I/O 和中断处理相关的字段:

(1)state 字段 描述进程的状态: SLEEPING:进程在等待某个事件(如 I/O 完成)。 RUNNABLE:进程可以被调度运行。 当进程因等待 I/O 而被阻塞时,其状态会被设置为 SLEEPING。 (2)chan 字段 chan 是一个通用的指针,用于表示进程等待的事件。 如果进程因为 I/O 操作被阻塞,它会通过 chan 指向一个特定的资源或设备。 当对应的 I/O 中断完成后,内核会检查 chan 并唤醒相关的进程。 (3)tf 字段 struct trapframe *tf 保存了当前中断发生时的上下文,包括寄存器和程序计数器等信息。 在 I/O 中断处理中,tf 保存的上下文可以用于恢复进程的执行。 (4)文件描述符表 ofile[NOFILE] 是一个数组,存储了进程打开的文件。 如果 I/O 操作涉及文件,内核会更新这个表的信息。