XV6学习(8)中断和设备驱动
驱动是操作系统中用于管理特定设备的代码:驱动控制设备硬件,通知硬件执行操作,处理中断,与等待该设备IO的进程进行交互。
当设备需要与操作系统进行交互时,就会产生中断(陷阱的一种),之后内核的陷阱处理代码就会识别中断设备并调用对应的驱动处理程序。在XV6这一步发生在trap.c的devintr中。
大部分设备驱动在两个上下文中执行代码:顶层部分运行在进程的内核线程中,底层部分在中断处理时执行。顶层部分通过系统调用如read和write来调用,这一部分代码会请求硬件开始一个操作的执行(如请求硬盘读取块);之后就会进入等待状态等待操作的完成。当设备完成操作后,就会触发一个中断,驱动的中断处理程序,即底层部分就会判断完成的操作,唤醒对应的正在等待的进程,之后通知硬件执行下一个操作。
代码:控制台输入
控制台的驱动程序console.c是一个驱动结构的简单抽象。控制台驱动通过UART(Universal asynchronous receiver-transmitter,通用异步收发传输器)串口读取用户输入的字符。驱动程序一次会累积一行的输入,并处理特定的字符如退格和ctrl-u。用户进程通过read系统调用来获取一行输入。
驱动调用的UART硬件是由QEMU模拟的16550芯片,一个16550芯片可以管理一条连接到终端或其他电脑的RS232串行链路。在QEMU中,其连接到键盘和显示器。
UART硬件可以看作一组映射到内存中的控制寄存器,对硬件的控制可以直接通过load和store特定内存来完成。UART内存映射地址开始于0x10000000或UART0(定义于memlayout.h)。每个控制寄存器的大小为1byte,偏移量定义于uart.c。
XV6main函数中的consoleinit对UART设备进行初始化,设置UART设备每接收一个字节的输入就产生一个接收中断,每当完成一个字节输出的发送时就产生一个传输完成中断。
XV6 shell通过init.c中打开的文件描述符对控制台进行读取。read系统调用将会调用consoleread函数,该函数等待输入的到达(通过中断)并保持在cons.buf中,拷贝其到用户空间,当一整行接收完成后返回到用户进程中。如果没有一整行输入到达,read进程就会在sleep调用中等待。
当用户输入一个字符,UART设备就会产生一个中断,激活XV6的陷阱处理程序。陷阱处理程序将会调用devintr,读取scause判断是否为外部设备产生的中断。之后通过PLIC(平台级中断控制器)判断中断设备,如果是UART设备,就会调用uartintr函数。
uartinit从UART设备中读取所有输入字符,并将其交给consoleintr处理,此函数不会等待字符的输入,因为未来的输入会产生新的中断。consoleintr将输入保持在buffer中直到一整行到达,同时对一些特殊符号进行处理。当一整行到达后,就会唤醒一个正在等待的consoleread。
当consoleread被唤醒时,buffer中就保存了完整的一行输入,此时就可以将其拷贝到用户空间并返回。
代码:控制台输出
write系统调用对控制台的写入最终会调用uartputc函数,设备会维护输出缓冲uart_tx_buf,因此写进程不需要等待UART完成发送。uartputc将字符加入缓冲区后,调用uartstart函数开始传输之后返回,该函数唯一的等待情况是缓冲区已满。
每当UART发送一个字节后,就会产生一次中断。uartintr函数会调用uartstart函数判断传输是否完成,未完成就开始传输下一个缓冲的字符。因此,当进程写入多个字符时,第一个字节会通过uartputc调用uartstart进行传输,之后的字节将会被uartintr调用的uartstart进行传输。
对于设备活动和进程活动,常用的解耦方式是通过缓冲和中断。控制台驱动可以处理输入即使没有进程在等待读取,一个随后到来的read会读取到输入。类似的,进程可以进行输出而不需要等待设备响应。解耦可以允许进程并行执行设备IO从而提高性能,尤其是当设备速度很慢或需要立即进行响应(如输入一个字符)。这种思想也被称作I/O并行。
驱动中的并行
在consoleread和consoleintr中会调用acquire函数。这些调用会申请一个锁,用于在并行访问中保护驱动的数据结构。在这里有三种并行风险:两个不同CPU上的进程同时调用consoleread;当CPU在执行consoleread函数时硬件触发了一个中断;当consoleread在执行时,硬件在其他CPU上触发了一个中断。
在并行中需要关注的另一个点是一个进程可能会等待设备的输入,但是中断信号在另一个进程运行时产生,因此中断处理程序是必须上下文无关的(不允许考虑中断时的进程或代码)。例如中断处理程序不能安全地在当前进程地页表上调用copyout函数。中断处理程序应该仅执行上下文无关的工作(如拷贝输入到缓冲区),之后唤醒顶层部分来处理剩余工作。
定时器中断
XV6通过定时器中断来维护时钟以及进行进程切换;在usertrap和kerneltrap中的yield函数会执行进程切换。定时器中断会由RISC-V CPU内部的时钟硬件产生。XV6对此时钟硬件进行编程以定期中断每个CPU。
RISC-V要求定时器中断必须在机器模式下执行,而不是在监管模式下执行。RISC-V的机器模式在无分页环境下执行,并且具有一系列单独的控制寄存器,因此在机器模式下运行普通的 xv6 内核代码是不切实际的。所有XV6的定时器中断处理程序是和陷阱机制完全分开的。
start.c中的代码执行于机器模式中,main函数之前,在timerinit函数中对定时器中断进行了设置:对CLINT硬件编程使其在一定时间后产生一次中断;设置scratch区域(类似于trapframe),帮助定时器中断处理程序保存寄存器和CLINT寄存器的地址。最后函数会设置mtvec为timervec函数地址并开启定时器中断。
定时器中断会在任何时候发生,内核在执行关键操作时也无法禁用定时器中断。因此定时器中断处理程序必须保证不会干扰被中断的内核代码执行。处理程序最基本的策略就是产生一个软件中断之后立即返回。产生的软件中断就可以通过通用的陷阱机制进行处理,并且可以进行关闭。软件中断的处理程序在devintr函数中。
机器模式的时钟中断向量为timervec,该函数保存了三个寄存器在start函数准备的scratch区域中,通知CLINT下一个中断的时刻,通过csrw sip, a1(a1为2)指令触发一个软件中断,最后恢复寄存器并返回。
真实操作系统
XV6运行设备和时钟中断在内核执行时产生。定时器中断在中断处理程序中强制线程切换,即使是在内核态执行中。这个功能可以使得内核线程公平地获取CPU时间片,尤其是当内核线程耗费大量时间进行计算而不返回用户态。但是,这使得内核代码需要考虑到其可能会被暂停并在一段时间后再另一个CPU上恢复,而这给XV6带来了一定的复杂性。如果设备中断和定时器中断只在用户代码执行时运行触发,内核可以变得更加简单。
在许多操作系统中,驱动程序的代码量远远大于内核本身。要支持所有设备在计算机上运行是十分繁杂的工作:有大量设备需要支持,设备有很多特性,设备间的协议十分复杂并且缺少文档。
UART设备通过读取控制寄存器一次接收一个字节数据,这种模式称为程序I/O(programmed I/O),因为数据移动由软件驱动。这种方式十分简单但是在高速设备上是十分缓慢的。高速设备通常通过DMA方式来进行数据传输。DMA设备硬件可以直接对内存进行读写,现代硬盘和网络设备就是通过这种方式进行的。DMA设备驱动会在内存中准备数据,之后通过一次控制寄存器的写入告诉设备对准备好的数据进行处理。
当设备需要在无法预知但不太频繁的时间上需要进行处理时,中断是有效的。但是中断有很大的CPU开销。因此高速设备会使用一些技巧来减少中断次数。一个技巧就是对一整批的输入或输出请求发起一次中断。另一个是驱动完全禁用中断,转为定时查询设备是否需要处理,这种技术被称为轮询(polling)。如果设备操作执行非常频繁,那么轮询是有意义的,反之如果设备大部分时间都是空闲的,那么轮询会浪费CPU时间。一些驱动会根据设备负载自动切换轮询和中断。
UART驱动先拷贝输入数据到内核缓冲区,之后再拷贝到用户空间。这在低数据传输率的情况下是有效的,但是对于高速设备,两次拷贝会显著地降低性能。一些操作系统可以直接将数据在用户态缓冲区和设备硬件之间移动,通常是通过DMA。
XV6学习(8)中断和设备驱动的更多相关文章
- Linux嵌入式学习-烟雾传感器驱动-字符设备驱动-按键驱动
MQ-2烟雾气敏传感器模块在X210v3开发板上的驱动. 现在需要一个MQ-2烟雾气敏传感器模块的驱动.其检测烟雾超过一定的标准后,会返回一个不同的电平,和按键驱动差不多. 但是在编写驱动的时候,需要 ...
- 蜕变成蝶~Linux设备驱动之中断与定时器
“我叮咛你的 你说 不会遗忘 你告诉我的 我也全部珍藏 对于我们来说 记忆是飘不落的日子 永远不会发黄 相聚的时候 总是很短 期待的时候 总是很长 岁月的溪水边 捡拾起多少闪亮的诗行 如果你要想念我 ...
- Hasen的linux设备驱动开发学习之旅--时钟
/** * Author:hasen * 參考 :<linux设备驱动开发具体解释> * 简单介绍:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:时钟 * Date ...
- 嵌入式Linux驱动学习之路(二十)USB设备驱动
USB在接入系统的时候,以0的设备ID和主机通信,然后由主机为其分配新的ID. 在主机端,D+和D-都是下拉接地的.而设备端的D-接上拉时,表明此设备为高速设备:12M/s. D+接上拉时则是全速设备 ...
- 字符设备驱动之Led驱动学习记录
一.概述 Linux内核就是由各种驱动组成的,内核源码中大约有85%的各种渠道程序的代码.一般来说,编写Linux设备驱动大致流程如下: 1.查看原理图,数据手册,了解设备的操作方法. 2.在内核中找 ...
- linux设备驱动归纳总结(六):3.中断的上半部和下半部——tasklet【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-100005.html linux设备驱动归纳总结(六):3.中断的上半部和下半部——tasklet x ...
- linux设备驱动归纳总结(六):2.分享中断号【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-90837.html xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
- linux设备驱动归纳总结(六):1.中断的实现【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-90740.html linux设备驱动归纳总结(六):1.中断的实现 xxxxxxxxxxxxxxxx ...
- linux字符设备驱动学习笔记(一):简单的字符设备驱动
最近在鼓捣lnux字符设备驱动,在网上搜集的各种关于linux设备驱动的代码和注释,要么是针对2.4的,要么是错误百出,根本就不能运行成功,真希望大家在发博客的时候能认真核对下代码的正确性,特别是要把 ...
随机推荐
- [LeetCode]223. Rectangle Area矩形面积
/* 像是一道数据分析题 思路就是两个矩形面积之和减去叠加面积之和 */ public int computeArea(int A, int B, int C, int D, int E, int F ...
- [LeetCode]实现数学计算
乘方 思路是:pow(x,n) = pow(x,n/2)*pow(x,n-n/2) 递归实现 public double myPow(double x, int n) { if (n==0) retu ...
- mysql数据库连接java
1 1.创建配置文件jdbc.properties 2 3 jdbc.driver=com.mysql.jdbc.Driver 4 jdbc.url=jdbc:mysql://localhost:33 ...
- Fragment学习
利用Fragment可以动态的加载页面,减少Activity的数量. 便于开发 类似与html中FragmentSet一样 嵌套在一起,使每个页面为独立的 代码如下: package com.exam ...
- Spring项目出现--Error:java: Compilation failed: internal java compiler error
错误现象 使用Idea导入新项目或升级idea或新建项目时会出现以下异常信息: Error:java: Compilation failed: internal java compiler error ...
- 使用mono-repo实现跨项目组件共享
本文会分享一个我在实际工作中遇到的案例,从最开始的需求分析到项目搭建,以及最后落地的架构的整个过程.最终实现的效果是使用mono-repo实现了跨项目的组件共享.在本文中你可以看到: 从接到需求到深入 ...
- MyArray框架搭建与实现
#include<iostream> using namespace std; template<class T> class MyArray { public: //构造函数 ...
- 设计模式之SOLID原则
介绍 设计模式中的SOLID原则,分别是单一原则.开闭原则.里氏替换原则.接口隔离原则.依赖倒置原则.前辈们总结出来的,遵循五大原则可以使程序解决紧耦合,更加健壮. SRP 单一责任原则 OCP 开放 ...
- i5 11300h和R5 5600H 的区别 哪个好
酷睿i5-11300H配置为4个内核及8个线程,具备8MB的L3缓存和5MB的L2缓存,基础频率3.10GHz最高睿频4.40GHz.Intel的显卡将集成Xe GPU内核.至于TDP,i5-1130 ...
- ATM_tests
ATM取款机练习程序 一.程序分析 自顶向下.逐步细化 按照程序执行的流程,将程序分解为若干个功能相对独立的函数(方法),每个函数(方法)负责某一功能,然后根据程序执行的流程,将函数(方法)组装(调用 ...