不落辰

知不可乎骤得,托遗响于悲风

0%

操作系统-xv6-Interrupt

  • Interrupt中断概述
    • 驱动(管理设备的代码)架构:Handler + Bottom
    • interrupt Handler : 即 Bottom
    • 例子 : kernel读取用户字符传给shell,并显示在终端上
      • PLIC路由中断;UART传输字符
    • Interrupt的并发性
      • 发出Interrupt的设备与cpu并行
      • 中断会停止当前进程(user -> uservec / kernel -> kernelvec)
      • 驱动的top和bottom是并行的.(一个cpu上跑top , 一个cpu上跑bottom)

本篇有待复习、总结

问题 及 解释

  • interrupt是如何保存上下文以及将上下文重新加载的?(Interrupt也会进入uservec/kernelvec吗?会)

    • 和trap机制一样。都是用uservec或者kernelvec。
  • console显示 $应当是不需要中断,而console上显示用户输入的字符则需要中断?

    • console显示$ 就是驱动将$送入了uart,uart送入了console。
    • console显示用户输入字符,则是
      • shell 读取 用户输入的字符(这过程中涉及中断),顺带驱动将读入的字符送入uart,再送到console。
        • 所涉及的中断是,键盘键入字符给uart,也即,键盘上有字符输入需要kernel处理,也即键盘需要得到os关注,因此发出个中断。中断经由PLIC路由到os。kernel中的Interrupt Handler对此进行处理,
  • uart到底是个什么?

    • 应该是个芯片吧。
    • 我现在理解就是一个字符的转发站。一端是kernel code,一端是设备。在这两方之间进行字符的转发。
    • UART硬件可以看作一组映射到内存中的控制寄存器,对硬件的控制可以直接通过load和store特定内存来完成。UART内存映射地址开始于0x10000000或UART0(定义于memlayout.h)。每个控制寄存器的大小为1byte,偏移量定义于uart.c。
    • uart和网卡这些东西是什么关系?
  • 中断到底指的是什么?停止当前正在运行的进程?然后去处理来的字符?

    • 中断 是 设备希望得到 os关注时,所采取的手段。即向os发送一个中断,表示“我需要你处理一下”。不一定是设备产生了字符,也可能是其他事件。比如网卡接收了packet。
  • uart和cpu是并行的吗?

    • 对。
  • 剩下的问题请 搜索 “未解决问题” 或者 “QUES”

总结

  • 一句话说明:user在键盘上敲下的按键,是怎么使得console上显示出来敲入的字符的?
    • 先前shell 调用read … -> sys_read … -> (驱动的top)consoleread(相对于InputBuffer是个Consumer的角色), 此时 inputBuffer为空,sleep等待。
    • user按下按键,键盘发起中断,经PLIC路由达到一个CPU核。cpu核接收中断 保存上下文 跳转到相应的处理代码处,这里比方说是uservec -> usertrap,usertrap根据PLIC(plic_claim)提供的信息,得知是哪个设备发出了中断,进行相应处理。这里是uart中断,故进入(驱动的Bottom)(即Interrupt Handler)(同时相对于inputBuffer来说是一个Producer的角色)uartintr()。uartintr中,调用uartget()从uart的reg中读出字符c,然后调用consoleintr(c),其中1. 通过consputc(c)(最终到writeReg(c)将字符写入uart寄存器)来讲user键入的字符送入uart,然后uart负责送到console上;2. 将c放入驱动的inputBuffer,并唤醒之前由于inputBuffer为空而睡眠的consoleread。而后结束中断处理。层层返回,返回到usertrap,然后到了usertrapret,然后userretvec,继续shell。
    • 如图
  • 对于uart来说可以看成有两个驱动。
    • 驱动1:一个是top是consolewrite(负责往outputBuffer中放字符) , buffer是outputBuffer , bottom是uartintr中的uartstart()(其实并不是一个Interrupt Handler,而仅仅是一个操作设备的代码,负责outputBuffer中的字符送入uart,uart将字符送入console)
    • 驱动2:一个top是consoleread(负责从inputBuffer中读) , buffer是 inputBuffer,bottom是uartintr。(是个Interrupt Handler)(负责处理设备发出的中断,具体是读取uart送来的字符,放入inputBuffer,唤醒consoleread,并将字符echo到uart进而到console上)
      • 对于该驱动,其top和bottom是可以并行的。
      • 如一个cpu核在consoleread,另一个cpu核接收了uart发出的中断,故执行uartintr。
  • 关于我以为的uart,PLIC的作用。正确性未知,反正我这么理解的。有待验证。

Interrupt

Interrup 与 syscall区别

  • 中断场景:硬件希望得到操作系统的关注。

    • 网卡收到一个packet,网卡会生成一个中断
    • 用户按下键盘,键盘会产生一个中断。
  • 中断发生时,os需要做的是

    • 保存当前工作
    • 处理中断
      • 这里的保存和恢复工作,与之前学的系统调用使用的trap很像。
      • 所以系统调用,page fault,中断,都使用相同的机制。
    • 处理完成后,恢复之前的工作。
  • 中断Interrupt 和 系统调用syscall 的区别

    • asynchronous
      • Interrupt: 硬件发出Interrupt时,os的Interrupt handler与当前在cpu上运行的process没有任何关联。
      • syscall: syscall发生在process的上下文中。
    • concurrency
      • Interrupt: CPU和产生Interrupt的设备是真正并行的。我们必须管理这里的并行
      • 比如网卡自己独立的处理来自网络的packet,然后在某个时间点产生中断,但是同时,CPU也在运行。
    • program device
      • 我们这节课主要关注外部设备,例如网卡,UART,而这些设备需要被编程。

PLIC 与 CPU

  • PLIC : 是个cpu核旁边的硬件,用于管理来自于外设的中断
    • CPU是通过Platform Level Interrupt Control,简称PLIC来处理设备中断。
  • PLIC位置如图:在cpu核旁边
  • PLIC具体结构如下
    • 设备发出的中断到达PLIC时,PLIC会将中断路由到某个CPU核。如果所有cpu核都在处理中断,PLIC会保留中断直到有一个CPU核处理中断。
  • PLIC管理中断流程
    • PLIC会向CPU通知当前有一个待处理的中断。
    • 其中一个CPU核会claim接收中断,这样PLIC就不会把中断发给其他CPU处理。
    • CPU核处理完中断后,CPU会通知PLIC。
    • PLIC将不再保存中断信息。

驱动

什么是驱动

  • 驱动
    • 管理设备的代码称为驱动
    • 所有驱动都在内核
  • 下面介绍uart设备的驱动

典型架构

  • 典型架构:大部分驱动可分为两部分bottom和top

  • bottom : Interrupt Handler

    • bottom部分通常是Interrupt Handler。
    • 当PLIC将中断送至CPU,CPU接收该中断,然后CPU会调用相应的Interrupr Handler。
    • Interrupt Handler 就是对 设备发出的中断进行响应,也即在操作发出中断的设备或者其他响应设备。它也并不运行在任何特定process的context,只是处理中断。
  • top : 用户进程 / 内核的其他部分调用的接口

    • 对uart来说,top是read/write接口,这些接口可被更高级代码调用。
  • 驱动的典型架构如下

    • 通过buffer queue使得并行的cpu和设备解耦。
    • QUES1 : 这句话是想说明什么?老师的这句话是在说top部分不能直接从interrupt handler中读取数据,而是要从queue中读取的原因吧?
      • 通常对于Interrupt handler来说存在一些限制,因为它并没有运行在任何进程的context中,所以进程的page table并不知道该从哪个地址读写数据,也就无法直接从Interrupt handler读写数据。

  • Interrupt Handler中如何对设备进行操作、如何编程?

    • 通常来说,编程是通过memory mapped I/O完成的。在SiFive的手册中,设备地址出现在物理地址的特定区间内,这个区间由主板制造商决定。操作系统需要知道这些设备位于物理地址空间的具体位置,然后再通过普通的load/store指令对这些地址进行编程。load/store指令实际上的工作就是读写设备的控制寄存器

    • 例如,对网卡执行store指令时,CPU会修改网卡的某个控制寄存器,进而导致网卡发送一个packet。所以这里的load/store指令不会读写内存,而是会操作设备

中断开启

  • 开启中断,也即使得CPU可以接受来自设备的中断。
  • 我觉着大致可以分为三步
    • 令设备可以发出中断
    • 令PLIC可以接收并路由中断
    • 令CPU可以接受中断

前置知识

  • 前置知识:RISCV中 与中断相关的寄存器
    • $SIE (Supervisor Interrupt Enable)
      • 有3个bit。
        • bit(E) : 代表UART外部设备的中断。
        • bit(S) : 代表软件中断
        • bit(T) : 代表定时器中断
    • $SSTATUS (Supervisor Statue)
      • 有1个bit 负责 打开、关闭中断。
      • $SIE负责单独控制特定的中断;$SSTAUS 负责控制所有中断
      • 每个CPU核都有独立的$SIE $SSTATUS。
    • $SIP (Supervisor Interrupt Pending)
      • 此时发生的中断类型
    • $SCAUSE
      • 表明当前进入kernel的原因是中断
    • $STVEC
      • trap / page fault / Interrupt发生时,此时运行的user process的程序计数器。

系统一开始是如何开启中断的

  • entry.S -> start.c : void start()

    • 所有的中断都设置在Supervisor mode
    • 设置$SIE来接收External,软件和定时器中断
    • 初始化定时器
  • start.C -> main.c : int main(){}

    • consoleinit:设置好uart芯片,使其可以产哼中断
    • plicinit,plicinithart : 使得PLIC可以接受相应中断,并路由到cpu。
    • scheduler -> inrt_on : 设置$SSTATUS,使得CPU可以接收中断。
    • 至此,中断被完全打开,cpu可以接受来自设备的中断。

前置知识:发生中断时如何进入相应C code

  • 发生中断时,进入kernel C Code之前的处理:(所有都是这样)
      1. 清除$SIE
      • 防止CPU核被其他中断打扰。CPU核完成当前中断后,再恢复$SIE
      1. 保存当前PC到$SEPC
      1. 保存当前mode
      1. 设置mode为Supervisor mode
      1. PC设置为$STVEC。即跳转到相应的处理代码处。
      • user态时发生的中断即跳转到uservec,kernel处发生的中断即跳转到kernelvec。
      • uservec -> usertrap ; kernelvec -> kerneltrap

console如何显示 $

当没发生中断时

  • 打印$流程
    • init.c -> sh.c -> getcmd -> fprintf($) -> write(fd, &c, 1); -> sys_write -> filewrite -> devsw[CONSOLE].write -> consolewrite -> uartputc(c) -> uartstart()
  • 该驱动要将字符传送到uart最后传送到console上,需要有个缓冲区。这里是个循环buffer。我称之为outputBuffer (queue)吧。

    • code如下
    • uart_tx_r 指向当前要从buffer中读走的字符
    • uart_tx_w 指向当前要写入字符到buffer中的位置。
      1
      2
      3
      4
      5
      6
      // the transmit output buffer.
      struct spinlock uart_tx_lock;
      #define UART_TX_BUF_SIZE 32
      char uart_tx_buf[UART_TX_BUF_SIZE];
      int uart_tx_w; // write next to uart_tx_buf[uart_tx_w++]
      int uart_tx_r; // read next from uart_tx_buf[uar_tx_r++]

  • TOP : consolewrite -> void uartputc(int c)

      1. 将c写入给buffer queue。
      1. 从buffer中取出char给uart,通知uart处理char。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        // add a character to the output buffer and tell the
        // UART to start sending if it isn't already.
        // blocks if the output buffer is full.
        // because it may block, it can't be called
        // from interrupts; it's only suitable for use
        // by write().
        // 将字符C写入驱动的buffer queue中。
        void
        uartputc(int c)
        {
        acquire(&uart_tx_lock);
        if(panicked){
        for(;;)
        ;
        }
        while(1){
        // 循环buffer已经满了 则sleep,将CPU让给其他进程。
        if(((uart_tx_w + 1) % UART_TX_BUF_SIZE) == uart_tx_r){
        // buffer is full.
        // wait for uartstart() to open up space in the buffer.
        sleep(&uart_tx_r, &uart_tx_lock);
        } else {
        // 将char c 写入buffer,更新w指针。
        uart_tx_buf[uart_tx_w] = c;
        uart_tx_w = (uart_tx_w + 1) % UART_TX_BUF_SIZE;
        // 通知uart进行操作
        uartstart();
        release(&uart_tx_lock);
        return;
        }
        }
        }
  • BOTTOM : uartintr -> consoleintr ,uartstart()

  • 这里直接调用了其BOTTOM中的一部分:uartstart,并不是为了相应设备发出的中断,只是为了操作下设备。

      1. 从uart_tx_buf中取出一个char
      • int c = uart_tx_buf[uart_tx_r++];
      1. 将该char送入uart.
      • WriteReg(THR, c);
      • 也即,告诉uart,我这里有一个字节需要你来发送。
      1. 数据送到uart后,系统调用即返回,user shell继续执行.
      1. 与此同时,uart将数据送出,送到指定设备(console?)。
    • 如果没发生中断的话,这里就是正常输出了$吧

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      // if the UART is idle, and a character is waiting
      // in the transmit buffer, send it.
      // caller must hold uart_tx_lock.
      // called from both the top- and bottom-half.
      // 通知设备执行操作
      void
      uartstart()
      {
      while(1){
      if(uart_tx_w == uart_tx_r){
      // transmit buffer is empty.
      return;
      }

      if((ReadReg(LSR) & LSR_TX_IDLE) == 0){
      // the UART transmit holding register is full,
      // so we cannot give it another byte.
      // it will interrupt when it's ready for a new byte.
      return;
      }
      // 从uart_tx_buf中取出一个char,将该char送入uart
      int c = uart_tx_buf[uart_tx_r];
      uart_tx_r = (uart_tx_r + 1) % UART_TX_BUF_SIZE;

      // maybe uartputc() is waiting for space in the buffer.
      // 唤醒等待空闲空间的uartputc()函数
      wakeup(&uart_tx_r);

      // 将char写入uart的寄存器中
      // 也即,告诉uart,我这里有一个字节需要你来发送。
      // 数据送到uart后,系统调用即返回,user shell继续执行
      // 与此同时,uart将数据送出,送到指定设备(应该是console)。
      WriteReg(THR, c);
      }
      }

当发生中断时

  • 发生中断时,进入kernel C Code之前的处理:(所有都是这样)

    • 如上所述。
  • 发生中断时,进入kernel C Code之后的流程

    • 举例 ,当在我们向Console输出字符时,发生中断。
    • shell运行在user态,故跳转到usertrap。
    • usertrap -> devintr -> uartintr -> consoleintr , uartstart
    • usertrap
      1
      2
      3
      4
      5
      6
      7
      if(){
      ...
      }else if((which_dev = devintr()) != 0){
      // ok
      }else{
      ...
      }
    • int devintr()
      • 获取中断号 : int irq = plic_claim();
      • 根据中断号判断是哪个设备的中断,进行相应处理。我们的例子中会进入uartintr()
        • if(irq == UART0_IRQ) uartintr();
        • if(irq == VIRTIO0_IRQ) virtio_disk_intr();
        • if(irq) printf(“unexpected interrupt irq=%d\n”, irq);
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          37
          38
          39
          40
          41
          42
          43
          44
          // check if it's an external interrupt or software interrupt,
          // and handle it.
          // returns 2 if timer interrupt,
          // 1 if other device,
          // 0 if not recognized.
          int
          devintr()
          {
          uint64 scause = r_scause();

          if((scause & 0x8000000000000000L) &&
          (scause & 0xff) == 9){
          // this is a supervisor external interrupt, via PLIC.
          // irq indicates which device interrupted.
          // 获取中断号
          int irq = plic_claim();
          // 鉴定为uart发出中断
          if(irq == UART0_IRQ){
          uartintr();
          } else if(irq == VIRTIO0_IRQ){
          virtio_disk_intr();
          } else if(irq){
          printf("unexpected interrupt irq=%d\n", irq);
          }
          // the PLIC allows each device to raise at most one
          // interrupt at a time; tell the PLIC the device is
          // now allowed to interrupt again.
          if(irq)
          plic_complete(irq);
          return 1;
          } else if(scause == 0x8000000000000001L){
          // software interrupt from a machine-mode timer interrupt,
          // forwarded by timervec in kernelvec.S.
          if(cpuid() == 0){
          clockintr();
          }
          // acknowledge the software interrupt by clearing
          // the SSIP bit in sip.
          w_sip(r_sip() & ~2);
          return 2;
          } else {
          return 0;
          }
          }
    • 驱动的BOTTOM Interrupt Handler: void uartintr()
      • consoleintr()
      • uartstart()
        • 将shell存储在驱动中的buffer queue中的字符取出,送到uart的寄存器
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          // handle a uart interrupt, raised because input has
          // arrived, or the uart is ready for more output, or
          // both. called from trap.c.
          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(); // 将buffer queue中的char送入uart的寄存器。uart将char送到console
          release(&uart_tx_lock);
          }
        • 实际上在提示符“$”之后,Shell还会输出一个空格字符,write系统调用可以在UART发送提示符“$”的同时,并发的将空格字符写入到buffer中。所以UART的发送中断触发时,可以发现在buffer中还有一个空格字符,之后会将这个空格字符送出。??还不太理解。
  • 由上述可见,驱动的top部分和bottom部分解耦开。(通过buffer queue)
    • top : consolewrite->uartput(c)
    • bottom : void uartintr() : consoleintr() , uartstart()

shell 如何 读取用户键入的 字符 并 传给shell程序 / 并 显示在终端上

  • 类似于上面的驱动要将字符传送到uart最后传送到console上。那个驱动结构中有一个循环缓冲区。我就称之为outputBuffer queue吧。
  • 同理,这里要将keyboard传给uart上的字符读到驱动中,驱动中也有一个循环缓冲区。我就称之为inputBuffer queue吧
    • code如下,原理和那个outputBuffer一样。
      1
      2
      3
      4
      5
      6
      7
      // input
      #define INPUT_BUF 128
      char buf[INPUT_BUF];
      uint r; // Read index
      uint w; // Write index
      uint e; // Edit index
      } cons;
  • 如图

shell 从console中读取用户输入

  • 这一部分还没发生中断。
  • 函数流程: (sh.c)gets -> read -> fileread -> devsw[f->major].read -> consoleread(int user_dst, uint64 dst, int n) ->阻塞的读完指定bytes
  • TOP : consoleread 核心逻辑如下:
    • 是驱动架构中的TOP
    • 读取buffer queue中的字符,遇到’\n’或者读完n bytes之后就返回到用户sh.c
    • 当buffer queue中没有字符时,sleep等待。等待被唤醒。即等待bottom的InterruptHandler处理键盘的中断,将字符放入buffer queue。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      //
      // user read()s from the console go here.
      // copy (up to) a whole input line to dst.
      // user_dist indicates whether dst is a user
      // or kernel address.
      //
      int consoleread(int user_dst, uint64 dst, int n)
      {
      acquire(&cons.lock);
      while(n > 0)
      {
      // wait until interrupt handler has put some input into cons.buffer.
      // 这不就是条件变量配合锁使用。
      while(cons.r == cons.w)
      {
      // 等待buffer中有可读数据
      sleep(&cons.r, &cons.lock);
      }
      c = cons.buf[cons.r++ % INPUT_BUF];
      // copy the input byte to the user-space buffer.
      cbuf = c;
      // 将字符c传给user层地址空间
      either_copyout(user_dst, dst, &cbuf, 1);

      dst++;--n;
      if(c == '\n')
      {
      // a whole line has arrived, return to
      // the user-level read().
      break;
      }
      }
      release(&cons.lock);
      return target - n;
      }

用户键入键盘,触发中断

  • 用户键入键盘,触发中断,kernel的uartintr作为InterrupHandler进行处理。
  • usertrap -> devintr -> BOTTOM : uartintr -> uartgetc , consoleintr , uartstart。
  • BOTTOM Interrupt Handler: uartintr
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // handle a uart interrupt, raised because input has
    // arrived, or the uart is ready for more output, or
    // both. called from trap.c.
    void
    uartintr(void)
    {
    // 其实我觉着这read 和 send两部分可以分成两个函数吧。。。。
    // read and process incoming characters.
    // 这里操作的buffer是inputBuffer(对于shell来说) 是一个为了从uart读入字符给上层的buffer
    while(1){
    int c = uartgetc();
    if(c == -1)
    break;
    consoleintr(c);
    }
    // send buffered characters.
    // 这里操作的buffer是outputBuffer(对于shell来说) 是一个为了从将字符从上层送入uart的buffer。
    acquire(&uart_tx_lock);
    uartstart();
    release(&uart_tx_lock);
    }
    • uartgetc : 从uart中读取1个字符
    • consoleintr : 将从uart读取的字符填充到buffer queue中,并唤醒consoleread。可以看出consoleintr是个producer的角色。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      void consoleintr(int c)
      {
      acquire(&cons.lock);
      switch(c){
      // ....
      default:
      if(c != 0 && cons.e-cons.r < INPUT_BUF){
      c = (c == '\r') ? '\n' : c;
      // echo back to the user.
      consputc(c);
      // store for consumption by consoleread().
      cons.buf[cons.e++ % INPUT_BUF] = c;
      // 读取了一行 / 全部读取完了。
      // 那么 唤醒等待条件变量cons.r的consoleread
      if(c == '\n' || c == C('D') || cons.e == cons.r+INPUT_BUF){
      // wake up consoleread() if a whole line (or end-of-file)
      // has arrived.
      cons.w = cons.e;
      wakeup(&cons.r);
      }
      }
      }
      }
    • uartstart() : 感觉在这里没啥用处。或者说跟上面读取的键盘的字符没啥关系。因为uartstart把output buffer queue中的字符给uart。让其送到console。和我们上述读取键盘的字符时所用的buf不是一个buf。
  • 之后Consumer consoleread被唤醒。从input buffer queue 中读取字符到user态。返回到user层。

Inetrrupt的并发

  • 设备与CPU是并行运行的

    • 如CPU将字符送入UART的reg之后就会返回Shell,而与此同时,UART正在并行的将字符传送给Console。此时Shell可能正在并行的调用write,向buffer queue中加入字符。
  • 中断会停止当前运行的程序

    • 对于user代码,无妨,因为中断会导致跳转到uservec trampoline,进行和syscall trap时一样的对用户上下文的保存。
    • QUES2:对于kernel,不成?我不理解。不是也有kernelvec吗
  • 驱动的top和bottom部分是并行运行的

    • 例如,Shell会在传输完提示符“$”之后再调用write系统调用传输空格字符,代码会走到UART驱动的top部分(注,uartputc函数),将空格写入到buffer中;但是同时在另一个CPU核,可能会收到来自于UART的中断,进而执行UART驱动的bottom部分,查看相同的buffer。如图
    • 所以一个驱动的top和bottom部分可以并行的在不同的CPU上运行。这里我们通过lock来管理并行。因为这里有共享的数据,我们想要buffer在一个时间只被一个CPU核所操作
      • input/outputbuffer queue存在于内存中,并且只有一份,所以,所有的CPU核都并行的与这一份数据交互。所以我们才需要lock。

Interrupt的演进

  • 直接摘mit

概括
由于发生中断时,cpu要经过多步才能处理中断数据。因此,如果设备高速的产生中断,那么cpu将很难及时处理。因此,现在产生中断之前,硬件会执行大量操作以减轻cpu负担。因此硬件现在更复杂。

最后我想介绍一下Interrupt在最近几十年的演进。当Unix刚被开发出来的时候,Interrupt处理还是很快的。这使得硬件可以很简单,当外设有数据需要处理时,硬件可以中断CPU的执行,并让CPU处理硬件的数据。
而现在,中断相对处理器来说变慢了。从前面的介绍可以看出来这一点,需要很多步骤才能真正的处理中断数据。如果一个设备在高速的产生中断,处理器将会很难跟上。?所以如果查看现在的设备,可以发现,现在的设备相比之前做了更多的工作。所以在产生中断之前,设备上会执行大量的操作,这样可以减轻CPU的处理负担。所以现在硬件变得更加复杂。

概括
对于高性能设备,会很高速的产生中断,cpu不能即使处理。
因此,引入polling。通过轮询设备来获取数据(可以经常拿到数据),节省了中断的代价,而不是只通过interrupt。
对于高性能网卡,如果大量包传入,那么会使用polling,而非interrupt。

如果你有一个高性能的设备,例如你有一个千兆网卡,这个网卡收到了大量的小包,网卡每秒可以生成1.5Mpps,这意味着每一个微秒,CPU都需要处理一个中断,这就超过了CPU的处理能力。那么当网卡收到大量包,并且处理器不能处理这么多中断**的时候该怎么办呢?
这里的解决方法就是使用polling。除了依赖Interrupt,CPU可以一直读取外设的控制寄存器,来检查是否有数据。对于UART来说,我们可以一直读取RHR寄存器,来检查是否有数据。现在,CPU不停的在轮询设备,直到设备有了数据。
这种方法浪费了CPU cycles,当我们在使用CPU不停的检查寄存器的内容时,我们并没有用CPU来运行任何程序。在我们之前的例子中,如果没有数据,内核会让Shell进程sleep,这样可以运行另一个进程。
所以,对于一个慢设备,你肯定不想一直轮询它来得到数据。我们会在没有数据的时候切换出来运行一些其他程序。但是如果是一个快设备,那么Interrupt的overhead也会很高,那么我们在polling设备的时候,是经常能拿到数据的,这样可以节省进出中断的代价。
所以对于一个高性能的网卡,如果有大量的包要传入,那么应该用polling。对于一些精心设计的驱动,它们会在polling和Interrupt之间动态切换(注,也就是网卡的NAPI)。