6.S081-7中断(键盘输入+屏幕输出) - Interrupts

6.S081-7中断(输入输出) - Interrupts

本节课的主要内容:

  • console中的提示符“$ ”是如何显示出来的;

  • 如果你在键盘输入“ls”,这些字符是怎么最终在console中显示出来的。

仔细越读以下内容,将会对理解接下来的课程有帮助:

  • 缩写:本节中IRQ = Interupt ReQuest(中断)。

  • UART0:是一个硬件设备,连接了console和keyboard,操作系统通过UART和这两个设备进行交互。console就是接收一个字符,然后够输出到屏幕上的设备。

  • 因为只有一个UART设备,一个buffer只针对一个UART设备,而这个buffer会被所有的CPU核共享,这样运行在多个CPU核上的多个程序可以同时向Console打印输出,而驱动中是通过锁来确保多个CPU核上的程序串行的向Console打印输出。

1. 复习虚拟内存(上节课知识点)

Linux环境下使用top命令查看,可以得到如下信息👇(图为课题组服务器)

image-20220817185144116

从图中的这一行👇可以看出,总内存占有量:11512011, 空闲的内存只有571384(大部分都被占用了used + buff/cache)。

KiB Mem : 11512011+total,   571384 free, 10159904+used, 12949688 buff/cache

解释一下buff/cache: 因为我们想要操作系统的物理内存尽可能高的利用率,因此我们将闲置的内存当作buff / cache。

以上是一个非常常见的场景,大部分操作系统运行时几乎没有任何空闲的内存。这意味着,如果应用程序或者内核需要使用新的内存,那么我们需要丢弃一些已有的内容。 —— 当当前进程需要比目前系统中free的内容还要多的时候:需要首先从used 或者 buff/ cache中撤回一部分已经被占用的内存。

VIRT是虚拟内存大小(理论上已经分配的页),RES是分配的真实值(实际上物理页大小)——可以看出RES远小于VIRT。所以,我们上节课讨论的基于虚拟内存和page fault提供的非常酷的功能在这都有使用,比如说demand paging。

2. 硬件触发中断 + 驱动管理(接口)

中断的场景:硬件想要得到OS的关注。—— 形成中断后,OS需要保护现场,处理中断,恢复现场。

比如网卡收到package,键盘被按下…

中断和系统调用的区别?
  • asynchronous —— 中断并不是由当前运行的任何一个进程触发的,而是硬件触发的。
  • concurrency—— CPU和引发中断的设备是真正的并行(各自在运行)
  • program device——需要被编程——比如网卡,UART等——需要处理专门的寄存器,信号转换,缓存等 —— 这个其实就是本科的《汇编与微机原理》的驱动程序(管理设备的代码)

简单描述一下驱动程序:分为top和bottom两个部分。

  • top是用户进程,或者内核其他部分调用的接口,有read/write接口。
  • bottom 是Interrupt handler (CPU调用Interrupt handler来处理中断)。

image

这里关于具体的实现,本科的微机原理十分详细,不过我已经忘了大半。。。。需要注意的是,这里的设备使用的是独立的地址空间,因此不能直接让进程操作虚拟内存页进行数据操作,需要通过Memory-mapped IO,来进行。

3. 设置中断(通过设置寄存器,让CPU为可以接受中断的状态)

RISC-V有许多与中断相关的寄存器:

  • SIE(Supervisor Interrupt Enable)寄存器。这个寄存器中有一个bit(E)专门针对例如UART的外部设备的中断;有一个bit(S)专门针对软件中断,软件中断可能由一个CPU核触发给另一个CPU核;还有一个bit(T)专门针对定时器中断。我们这节课只关注外部设备的中断。
  • SSTATUS(Supervisor Status)寄存器。这个寄存器中有一个bit来打开或者关闭中断。每一个CPU核都有独立的SIE和SSTATUS寄存器,除了通过SIE寄存器来单独控制特定的中断,还可以通过SSTATUS寄存器中的一个bit来控制所有的中断。
  • SIP(Supervisor Interrupt Pending)寄存器。当发生中断时,处理器可以通过查看这个寄存器知道当前是什么类型的中断。
  • SCAUSE寄存器,这个寄存器我们之前看过很多次。它会表明当前状态的原因是中断。
  • STVEC寄存器,它会保存当trap,page fault或者中断发生时,CPU运行的用户程序的程序计数器,这样才能在稍后恢复程序的运行。

接下来我们看看XV6是如何对其他寄存器进行编程,使得CPU处于一个能接受中断的状态。

看一下console的$是如何输出的👇

start.c中的start函数(entry.S jumps here in machine mode on stack0.),其中的w_XXX代表的是写XXX寄存器命令。

// entry.S jumps here in machine mode on stack0.
void
start()
{
  // set M Previous Privilege mode to Supervisor, for mret.
  unsigned long x = r_mstatus();
  x &= ~MSTATUS_MPP_MASK;
  x |= MSTATUS_MPP_S;
  w_mstatus(x);

  // set M Exception Program Counter to main, for mret.
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

  // disable paging for now.
  w_satp(0);

  // delegate all interrupts and exceptions to supervisor mode.
  w_medeleg(0xffff);
  w_mideleg(0xffff);

  // ask for clock interrupts.
  timerinit();

  // keep each CPU's hartid in its tp register, for cpuid().
  int id = r_mhartid();
  w_tp(id);

  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

这里将所有的中断都设置在Supervisor mode,然后设置SIE寄存器来接收External,软件和定时器中断,之后初始化定时器。

接下来我们看一下main函数中是如何处理External中断👇(很明显,第一行就是consoleinit();,紧接着就是printfinit();

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    procinit();      // process table
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache
    iinit();         // inode cache
    fileinit();      // file table
    virtio_disk_init(minor(ROOTDEV)); // emulated hard disk
    userinit();      // first user process
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        
}

看一下consoleinit(void); —— 这里我们先不管锁🔒的内容。所以第一条函数是uartinit();

void
consoleinit(void)
{
  initlock(&cons.lock, "cons");

  uartinit();

  // connect read and write system calls
  // to consoleread and consolewrite.
  devsw[CONSOLE].read = consoleread;
  devsw[CONSOLE].write = consolewrite;
}

继续看uartinit();👇,它首先关闭中断,设置波特率,设置字符长度为8bit,重置FIFO,最后再重新打开中断。

void
uartinit(void)
{
  // disable interrupts.
  WriteReg(IER, 0x00);

  // special mode to set baud rate.
  WriteReg(LCR, 0x80);

  // LSB for baud rate of 38.4K.
  WriteReg(0, 0x03);

  // MSB for baud rate of 38.4K.
  WriteReg(1, 0x00);

  // leave set-baud mode,
  // and set word length to 8 bits, no parity.
  WriteReg(LCR, 0x03);

  // reset and enable FIFOs.
  WriteReg(FCR, 0x07);

  // enable receive interrupts.
  WriteReg(IER, 0x01);
}

以上就是uartinit(),运行完这个函数之后,原则上UART就可以生成中断了。但是因为我们还没有对PLIC编程,所以中断不能被CPU感知。最终,在main函数中,需要调用plicinit()。下图是plicinit()

void
plicinit(void)
{
  // set desired IRQ priorities non-zero (otherwise disabled).
  *(uint32*)(PLIC + UART0_IRQ*4) = 1;
  *(uint32*)(PLIC + VIRTIO0_IRQ*4) = 1;
}

上面代码的第一行设置了PLIC可以接收UART的中断,第二行是可以接受磁盘IO的中断。

**解释PLIC:**所有的设备都连接到处理器上,处理器上是通过Platform Level Interrupt Control,简称PLIC来处理设备中断。**PLIC会管理来自于外设的中断。**PLIC与外设一样,也占用了一个I/O地址(0xC000_0000)。

具体流程如下👇

  • PLIC会通知当前有一个待处理的中断;

  • 其中一个CPU核会Claim接收中断,这样PLIC就不会把中断发给其他的CPU处理;

  • CPU核处理完中断之后,CPU会通知PLIC;

  • PLIC将不再保存中断的信息。

注意在main中👇——plicinit();之后就是plicinithart();

plicinit();      // set up interrupt controller
plicinithart();  // ask PLIC for device interrupts

plicinithart(); 如下👇(plicinit是由0号CPU运行,之后,每个CPU的核都需要调用plicinithart函数表明对于哪些外设中断感兴趣。)

void
plicinithart(void)
{
  int hart = cpuid();
  
  // set uart's enable bit for this hart's S-mode. 
  *(uint32*)PLIC_SENABLE(hart)= (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);

  // set this hart's S-mode priority threshold to 0.
  *(uint32*)PLIC_SPRIORITY(hart) = 0;
}

所以在plicinithart()中,每个CPU的核都表明自己对来自于UART和VIRTIO的中断感兴趣。因为我们忽略中断的优先级,所以我们将优先级设置为0。

到目前为止,我们有了生成中断的外部设备,我们有了PLIC可以传递中断到单个的CPU。但是CPU自己还没有设置好接收中断,因为我们还没有设置好SSTATUS寄存器。在main函数的最后,程序调用了scheduler函数👇可以看到,该函数主要是运行进程,但是进程运行前,会先执行iter_on();

// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();

    int found = 0;
    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->scheduler, &p->context);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
    if(found == 0){
      intr_on();
      asm volatile("wfi");
    }
  }
}

其中iter_on()定义如下👇,它只做一件事:设置SSTATUS寄存器,打开中断标志位。

// enable device interrupts
static inline void
intr_on()
{
  w_sstatus(r_sstatus() | SSTATUS_SIE);
}

4. $的输出——UART驱动的top部分

接下来我想看一下如何从Shell程序输出提示符“$ ”到Console。首先我们看init.c中的main函数,这是系统启动后运行的第一个进程。

  • 运行在用户空间 —— 启动的第一个进程,进程号为1,init进程,然后它会fork出来进程号为2的shell进程 —— 这些都是CSAPP + 本课程前面的内容了,不再详细描述。

这里真正关注的是,首先打开了一个叫做console的设备+通过mknod创建了console设备(其实就是把驱动挂载到内核中——本科嵌入式的内容。)

因为这里的open是第一次打开的文件,因此文件描述符fd = 0,然后通过dup创建了fd = 1, fd = 2 (也就是说,其实0,1,2都是代表console文件(其实是console设备,但是通过挂载技术,OS让设备可以将设备虚拟成文件来操作))。

char *argv[] = { "sh", 0 };

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", 1, 1);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    printf("init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf("init: fork failed\n");
      exit(1);
    }
    if(pid == 0){
      exec("sh", argv);
      printf("init: exec sh failed\n");
      exit(1);
    }
    while((wpid=wait(0)) >= 0 && wpid != pid){
      //printf("zombie!\n");
    }
  }
}

再来看shell程序是做了什么👇

  • 首先确认0,1,2是否都打开了
  • 运行gtcmd —— 可以看到这里输出了$

尽管Console背后是UART设备,但是从应用程序来看,它就像是一个普通的文件。Shell程序只是向文件描述符2写了数据,它并不知道文件描述符2对应的是什么。在Unix系统中,设备是由文件表示。

int
getcmd(char *buf, int nbuf)
{
  write(2, "$ ", 2);
  memset(buf, 0, nbuf);
  gets(buf, nbuf);
  if(buf[0] == 0) // EOF
    return -1;
  return 0;
}

int
main(void)
{
  static char buf[100];
  int fd;

  // Ensure that three file descriptors are open.
  while((fd = open("console", O_RDWR)) >= 0){
    if(fd >= 3){
      close(fd);
      break;
    }
  }

  // Read and run input commands.
  while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Chdir must be called by the parent, not the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(2, "cannot cd %s\n", buf+3);
      continue;
    }
    if(fork1() == 0)
      runcmd(parsecmd(buf));
    wait(0);
  }
  exit(0);
}

继续来看write(2, "$ ", 2); 前面的实验(6.S081 附加Lab1 用户执行系统调用的过程(Trap))中,已经看过系统调用的过程,它是调用了sys_write实现的👇

uint64
sys_write(void)
{
  struct file *f;
  int n;
  uint64 p;

  if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)
    return -1;

  return filewrite(f, p, n);
}

这个函数中首先对参数做了检查,然后又调用了filewrite函数。filewrite函数位于file.c文件中👇

// Write to file f.
// addr is a user virtual address.
int
filewrite(struct file *f, uint64 addr, int n)
{
  int r, ret = 0;

  if(f->writable == 0)
    return -1;

  if(f->type == FD_PIPE){
    ret = pipewrite(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
      return -1;
    ret = devsw[f->major].write(f, 1, addr, n);
  } else if(f->type == FD_INODE){
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;

      begin_op(f->ip->dev);
      ilock(f->ip);
      if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
        f->off += r;
      iunlock(f->ip);
      end_op(f->ip->dev);

      if(r < 0)
        break;
      if(r != n1)
        panic("short filewrite");
      i += r;
    }
    ret = (i == n ? n : -1);
  } else {
    panic("filewrite");
  }

  return ret;
}

显然是执行了这一部分👇——针对不同的devs,我们有不同的write函数,这里是console,因此对应的是consolewrite函数。

else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
        return -1;
    ret = devsw[f->major].write(f, 1, addr, n);
}
// map major device number to device functions.
struct devsw {
  int (*read)(struct file *, int, uint64, int);
  int (*write)(struct file *, int, uint64, int);
};

consolewrite对应的是UART驱动的top部分,这里的either_copyin的意思是copy (either from kernel or user——depending on src),也就是从某个空间取得一个字符,赋值到c。——然后运行consputs(c);

//
// user write()s to the console go here.
//
int
consolewrite(struct file *f, int user_src, uint64 src, int n)
{
  int i;

  acquire(&cons.lock);
  for(i = 0; i < n; i++){
    char c;
    if(either_copyin(&c, user_src, src+i, 1) == -1)
      break;
    consputc(c);
  }
  release(&cons.lock);

  return n;
}

consputs(c);如下👇,它首先判断是否panicked然后处理特出字符(backspace等),然后调用了uartput(c)来实现普通字符的输出。

//
// send one character to the uart.
//
void
consputc(int c)
{
  extern volatile int panicked; // from printf.c

  if(panicked){
    for(;;)
      ;
  }

  if(c == BACKSPACE){
    // if the user typed backspace, overwrite with a space.
    uartputc('\b'); uartputc(' '); uartputc('\b');
  } else {
    uartputc(c);
  }
}

uartputc如下👇(这里已经是驱动的top部分了),这里很像一个生产者消费者模型,即producer生产内容,放到buffer里面(buffer是一个循环的buffer),首先判断队列是否满了,如果是的话,需要等待(生产者消费者的多线程,阻塞),如果buffer不是满的,就将c写入队列,并更新写指针,之后调用uartstart();

// 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().
void
uartputc(int c)
{
  acquire(&uart_tx_lock);

  if(panicked){
    for(;;)
      ;
  }

  while(1){
    if(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
      // buffer is full.
      // wait for uartstart() to open up space in the buffer.
      sleep(&uart_tx_r, &uart_tx_lock);
    } else {
      uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c;
      uart_tx_w += 1;
      uartstart();
      release(&uart_tx_lock);
      return;
    }
  }
}

uartstart();如下👇uartstart就是通知设备执行操作。首先是检查当前设备是否空闲,如果空闲的话,我们会从buffer中读出数据,然后将数据写入到THR(Transmission Holding Register)发送寄存器。这里相当于告诉设备,我这里有一个字节需要你来发送。一旦数据送到了设备,系统调用会返回,用户应用程序Shell就可以继续执行。这里从内核返回到用户空间的机制与之前课程中的trap机制是一样的。

// 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;
    }
    
    int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
    uart_tx_r += 1;
    
    // maybe uartputc() is waiting for space in the buffer.
    wakeup(&uart_tx_r);
    
    WriteReg(THR, c);
  }
}

与此同时,UART设备会将数据送出。在某个时间点,我们会收到中断,因为我们之前设置了要处理UART设备中断。接下来我们看一下,当发生中断时,实际会发生什么。

其实,我看了一下19年的OS源码,内容是这样的👇(没有uartstart,也没有锁,直接进行写入操作(不过只能写入单字符))——上面我们讲的内容是先放到缓存里,再去操作,因此会有可能需要生产者消费者模型,而这里要更加简单,因为是单字符操作。

// write one output character to the UART.
void
uartputc(int c)
{
  // wait for Transmit Holding Empty to be set in LSR.
  while((ReadReg(LSR) & (1 << 5)) == 0)
    ;
  WriteReg(THR, c);
}

5. $的输出——UART驱动的bottom部分

如果发生了中断,RISC-V会如何操作?

  • 首先,会清除SIE寄存器相应的bit,这样可以阻止CPU核被其他中断打扰,该CPU核可以专心处理当前中断。处理完成之后,可以再次恢复SIE寄存器相应的bit。

  • 之后,会设置SEPC寄存器为当前的程序计数器。我们假设Shell正在用户空间运行,突然来了一个中断,那么当前Shell的程序计数器会被保存。

  • 之后,要保存当前的mode。在我们的例子里面,因为当前运行的是Shell程序,所以会记录user mode。

  • 再将mode设置为Supervisor mode。

  • 最后将程序计数器的值设置成STVEC的值。(注,STVEC用来保存trap处理程序的地址,详见lec06)在XV6中,STVEC保存的要么是uservec或者kernelvec函数的地址,具体取决于发生中断时程序运行是在用户空间还是内核空间。在我们的例子中,Shell运行在用户空间,所以STVEC保存的是uservec函数的地址。而从之前的课程我们可以知道uservec函数会调用usertrap函数。所以最终,我们在usertrap函数中。我们这节课不会介绍trap过程中的拷贝,恢复过程,因为在之前的课程中已经详细的介绍过了。

看一下trap.c中的usertrap如何处理中断👇——通过调用devintr()函数判断是否是外部中断(或者是软件中断)

  } else if((which_dev = devintr()) != 0) {
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    printf("page down:%d\n",PGROUNDDOWN(r_stval()));
    p->killed = 1;
  }

devintr()如下👇,首先调用plic_claim();

// 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();

    if(irq == UART0_IRQ){
      uartintr();
    } else if(irq == VIRTIO0_IRQ || irq == VIRTIO1_IRQ ){
      virtio_disk_intr(irq - VIRTIO0_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;
  }
}

plic_claim函数位于plic.c文件中。在这个函数中,当前CPU核会告知PLIC,自己要处理中断,PLIC_SCLAIM会将中断号返回,对于UART来说,返回的中断号是10。

// ask the PLIC what interrupt we should serve.
int
plic_claim(void)
{
  int hart = cpuid();
  //int irq = *(uint32*)(PLIC + 0x201004);
  int irq = *(uint32*)PLIC_SCLAIM(hart);
  return irq;
}

此外,从devintr()可以看到👇,即,如果是UART0触发的中断,那么会调用uartintr(); (应该是中断处理程序)

if(irq == UART0_IRQ) {
   uartintr();
}

uartintr();程序如下👇,因为这里我们还没有输入内容,因此没有所以代码会直接运行到uartstart函数,这个函数会将Shell存储在buffer中的任意字符送出。实际上在提示符“ ”之后, S h e l l 还会输出一个空格字符, w r i t e 系统调用可以在 U A R T 发送提示符“ ”之后,Shell还会输出一个空格字符,write系统调用可以在UART发送提示符“ 之后,Shell还会输出一个空格字符,write系统调用可以在UART发送提示符”的同时,并发的将空格字符写入到buffer中。所以UART的发送中断触发时,可以发现在buffer中还有一个空格字符,之后会将这个空格字符送出。

这样,驱动的top部分和bottom部分就解耦开了。

// read one input character from the UART.
// return -1 if none is waiting.
int
uartgetc(void)
{
  if(ReadReg(LSR) & 0x01){
    // input data is ready.
    return ReadReg(RHR);
  } else {
    return -1;
  }
}

// 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();
  release(&uart_tx_lock);
}

6. 中断的并发(生产者-消费者模型)

我们之所以需要锁是因为有多个CPU核,但是却只有一个Console。

驱动的top和bottom部分可以并行的运行。所以一个CPU核可以执行uartputc函数,而另个一CPU核可以执行uartintr函数,我们需要确保它们是串行执行的,而锁确保了这一点。

学生提问:那是不是意味着,某个时间,其他所有的CPU核都需要等待某一个CPU核的处理?

Frans教授:这里并不是死锁。其他的CPU核还是可以在等待的时候运行别的进程。

这里主要讲的是生产者 - 消费者模型(上面的uart的buffer是一个环形队列),关于这部分,可以看前面OS的代码(uartstartuartputsc),但是这里会可能会更加详细,并且易于理解:我的博客 C++ 实现生产者和消费者(并发)

这里注意:

学生提问:对于uartputc中的sleep,它怎么知道应该让Shell去sleep?

Frans教授: sleep会将当前在运行的进程存放于sleep数据中 (我猜应该指的就是阻塞队列中)。它传入的参数是需要等待的信号,在这个例子中传入的是uart_tx_r的地址。在uartstart函数中**,一旦buffer中有了空间,会调用与sleep对应的函数wakeup,传入的也是uart_tx_r的地址。任何等待在这个地址的进程都会被唤醒。**有时候这种机制被称为conditional synchronization。

7. ls —— UART读取键盘输入

在UART的另一侧,会有类似的事情发生,有时Shell会调用read从键盘中读取字符。 在read系统调用的底层,会调用fileread函数。在这个函数中,如果读取的文件类型是设备,会调用相应设备的read函数。

// Read from file f.
// addr is a user virtual address.
int
fileread(struct file *f, uint64 addr, int n)
{
  int r = 0;

  if(f->readable == 0)
    return -1;

  if(f->type == FD_PIPE){
    r = piperead(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
      return -1;
    r = devsw[f->major].read(f, 1, addr, n);
  } else if(f->type == FD_INODE){
    ilock(f->ip);
    if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
      f->off += r;
    iunlock(f->ip);
  } else {
    panic("fileread");
  }

  return r;
}

这里显然调用的是consoleread函数,该函数源码👇

//
// 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(struct file *f, int user_dst, uint64 dst, int n)
{
  uint target;
  int c;
  char cbuf;

  target = n;
  acquire(&cons.lock);
  while(n > 0){
    // wait until interrupt handler has put some
    // input into cons.buffer.
    while(cons.r == cons.w){
      if(myproc()->killed){
        release(&cons.lock);
        return -1;
      }
      sleep(&cons.r, &cons.lock);
    }

    c = cons.buf[cons.r++ % INPUT_BUF];

    if(c == C('D')){  // end-of-file
      if(n < target){
        // Save ^D for next time, to make sure
        // caller gets a 0-byte result.
        cons.r--;
      }
      break;
    }

    // copy the input byte to the user-space buffer.
    cbuf = c;
    if(either_copyout(user_dst, dst, &cbuf, 1) == -1)
      break;

    dst++;
    --n;

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

  return target - n;
}

这里与UART类似,也有一个buffer,包含了128个字符。其他的基本一样,也有producer和consumser。但是在这个场景下Shell变成了consumser,因为Shell是从buffer中读取数据。而键盘是producer,它将数据写入到buffer中。

struct {
  struct spinlock lock;
  
  // input
#define INPUT_BUF 128
  char buf[INPUT_BUF];
  uint r;  // Read index
  uint w;  // Write index
  uint e;  // Edit index
} cons;

从consoleread函数中可以看出,当读指针和写指针一样时,说明buffer为空,进程会sleep。所以Shell在打印完“$ ”之后,如果键盘没有输入,Shell进程会sleep,直到键盘有一个字符输入。所以在某个时间点,假设用户通过键盘输入了“l”,这会导致“l”被发送到主板上的UART芯片,产生中断之后再被PLIC路由到某个CPU核,之后会触发devintr函数,devintr可以发现这是一个UART中断,然后通过uartgetc函数获取到相应的字符,之后再将字符传递给consoleintr函数。

默认情况下,字符会通过consputc,输出到console上给用户查看。之后,字符被存放在buffer中。在遇到换行符的时候,唤醒之前sleep的进程,也就是Shell,再从buffer中将数据读出。

所以这里也是通过buffer将consumer和producer之间解耦,这样它们才能按照自己的速度,独立的并行运行。如果某一个运行的过快了,那么buffer要么是满的要么是空的,consumer和producer其中一个会sleep并等待另一个追上来。

8. 中断的演化(略)

对于现在的CPU来说,中断速度太慢了 —— 如果一个高速产生中断那么中断处理速度将会成为一个瓶颈,比如千兆网卡。

这个网卡收到了大量的小包,网卡每秒可以生成1.5Mpps,这意味着每一个微秒,CPU都需要处理一个中断,这就超过了CPU的处理能力。那么当网卡收到大量包,并且处理器不能处理这么多中断的时候该怎么办呢?

据。而键盘是producer,它将数据写入到buffer中。

struct {
  struct spinlock lock;
  
  // input
#define INPUT_BUF 128
  char buf[INPUT_BUF];
  uint r;  // Read index
  uint w;  // Write index
  uint e;  // Edit index
} cons;

从consoleread函数中可以看出,当读指针和写指针一样时,说明buffer为空,进程会sleep。所以Shell在打印完“$ ”之后,如果键盘没有输入,Shell进程会sleep,直到键盘有一个字符输入。所以在某个时间点,假设用户通过键盘输入了“l”,这会导致“l”被发送到主板上的UART芯片,产生中断之后再被PLIC路由到某个CPU核,之后会触发devintr函数,devintr可以发现这是一个UART中断,然后通过uartgetc函数获取到相应的字符,之后再将字符传递给consoleintr函数。

默认情况下,字符会通过consputc,输出到console上给用户查看。之后,字符被存放在buffer中。在遇到换行符的时候,唤醒之前sleep的进程,也就是Shell,再从buffer中将数据读出。

所以这里也是通过buffer将consumer和producer之间解耦,这样它们才能按照自己的速度,独立的并行运行。如果某一个运行的过快了,那么buffer要么是满的要么是空的,consumer和producer其中一个会sleep并等待另一个追上来。

8. 中断的演化(略)

对于现在的CPU来说,中断速度太慢了 —— 如果一个高速产生中断那么中断处理速度将会成为一个瓶颈,比如千兆网卡。

这个网卡收到了大量的小包,网卡每秒可以生成1.5Mpps,这意味着每一个微秒,CPU都需要处理一个中断,这就超过了CPU的处理能力。那么当网卡收到大量包,并且处理器不能处理这么多中断的时候该怎么办呢?

这里的解决方法就是使用polling。除了依赖Interrupt,CPU可以一直读取外设的控制寄存器,来检查是否有数据。对于UART来说,我们可以一直读取RHR寄存器,来检查是否有数据。现在,CPU不停的在轮询设备,直到设备有了数据。

<think>好的,用户让我把之前画的系统框图翻译成中文。先看一下原来的内容,是ASCII字符画的模块结构图,里面有STM32F103和ESP32-S3两个主控模块,它们之间通过UART通信,然后各自连接到Home Assistant和云服务。 用户可能是在写论文或者做项目文档时需要这部分的中文版本,方便展示或说明系统架构。不过之前已经提供了详细的分模块解析,现在可能希望更直观的结构图翻译。但直接用文本翻译的话,保持原来的ASCII格式可能比较困难,因为中文和英文字符宽度不同,排版会乱。所以可能需要调整结构,或者用文字描述每个部分的中文名称。 另外,用户可能希望了解各个模块之间的交互关系,比如UART通信的具体作用,WiFi和MQTT的传输流程。需要确认是否需要更详细的解释,还是单纯翻译框图中的英文部分。比如“Sensors Data”翻译成“传感器数据”,“PWM Control”是“PWM控制”,“GPIO Interrupts”是“GPIO外部中断”等。 还要注意箭头符号的方向,确保翻译后的结构图逻辑正确。比如“UART”的双向通信用“<--->”表示,可能需要保持符号不变,或者用中文箭头替代。不过通常技术文档中保持符号更清晰,所以可能保留符号,只翻译模块内的文字。 最后,用户可能没有明确说明是否需要保持ASCII图的格式,但根据之前的回答,用户可能希望得到一个结构清晰的文字描述,而不是严格复制原图。因此,可能需要用中文重新描述整个系统结构,说明各模块的功能和连接方式,确保信息准确传达。</think>以下是对该系统架构图的中文翻译与解析: --- ### **系统架构图中文描述** ``` +-------------------+ +-----------------+ | STM32F103主控器 | | ESP32-S3协处理器| |-------------------| |-----------------| | 传感器数据采集 | UART| WiFi通信 | | PWM控制输出 |<--->| 摄像头视频流 | | GPIO外部中断管理 | | MQTT客户端 | +-------------------+ +-----------------+ ↓ ↓ +-------------------+ +-----------------+ | Home Assistant |<-----| 云服务平台 | | 移动端APP | | 通知服务 | +-------------------+ +-----------------+ ``` --- ### **关键连接与功能说明** 1. **STM32与ESP32通信** - **物理接口**:UART串口(如USART1_TX/RX引脚对接) - **数据流**: - STM32 → ESP32:传感器报警状态、控制指令(ASCII协议或自定义二进制协议) - ESP32 → STM32:APP下发的阈值修改、舵机控制命令 2. **ESP32云端交互** - **WiFi + MQTT**:通过内置WiFi模块连接路由器,使用MQTT协议向HomeAssistant推送数据(JSON格式) - **摄像头流媒体**:RTSP/HLS协议传输实时视频,陌生人识别结果通过MQTT附加到视频元数据 3. **移动端联动** - **HomeAssistant集成**:APP显示传感器数据、报警历史、实时视频及远程关窗/消警操作 - **推送服务**:云平台触发短信/APP通知(可集成第三方如Firebase、极光推送) --- ### **技术细节补充** - **UART配置示例**(STM32端): ```c // 波特率115200,8位数据,无校验,1位停止位 USART_InitTypeDef uart; uart.BaudRate = 115200; uart.WordLength = USART_WordLength_8b; uart.StopBits = USART_StopBits_1; uart.Parity = USART_Parity_No; USART_Init(USART1, &uart); ``` - **MQTT主题设计**: - 发布主题:`home/security/sensors` - 订阅主题:`home/security/control` 如需进一步细化某个模块的实现逻辑,欢迎随时提问!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值