输入输出系统
我们知道计算机由CPU、主板、硬盘、内存、鼠标、键盘、显示器、网卡等部件组成。 这里鼠标、键盘等是输入设备, 硬盘、显示器等是输出设备。网卡既是输入设备也算输出设备。这么多不同的设备,统一由输入输出系统来管理。
用设备控制器屏蔽设备差异
计算机的是操作系统, 操作系统的核心是内核。进程管理、内存管理、文件系统管理、输入输出系统的管理都是内核的功能。输入输出管理,需要跟不同的设备打交道,但是设备多种多样,内核只能提供一个统一的抽象接口来管理设备,CPU 并不直接和设备打交道,它们中间有一个叫作设备控制器(Device Control Unit)的组件,例如硬盘有磁盘控制器、USB 有 USB 控制器、显示器有视频控制器等。
控制器其实有点儿像一台小电脑。它有它的芯片,类似小 CPU,执行自己的逻辑。它也有它的寄存器。这样 CPU 就可以通过写这些寄存器,对控制器下发指令,通过读这些寄存器,查看控制器对于设备的操作状态。CPU 对于寄存器的读写,可比直接控制硬件,要标准和轻松很多。
输入输出设备我们大致可以分为两类:块设备(Block Device)和字符设备(Character Device)
- 块设备将信息存储在固定大小的块中,每个块都有自己的地址。硬盘就是常见的块设备。
- 字符设备发送或接收的是字节流。而不用考虑任何块结构,没有办法寻址。鼠标就是常见的字符设备。
由于块设备传输的数据量比较大,控制器里往往会有缓冲区。CPU 写入缓冲区的数据攒够一部分,才会发给设备。CPU 读取的数据,也需要在缓冲区攒够一部分,才拷贝到内存。
每个控制寄存器被分配一个 I/O 端口,我们可以通过特殊的汇编指令(例如 in/out 类似的指令)操作这些寄存器。数据缓冲区,可内存映射 I/O,可以分配一段内存空间给它,就像读写内存一样读写数据缓冲区。
设备完成操作后,CPU有两种方式可以获取通知。 第一种, 就是轮询寄存器的状态位。 第二种,设备通过中断通知CPU处理。
为了响应中断,我们一般会有一个硬件的中断控制器,当设备完成任务后触发中断到中断控制器,中断控制器就通知 CPU,一个中断产生了,CPU 需要停下当前手里的事情来处理中断。
中断有两种,一种软中断,例如代码调用 INT 指令触发,一种是硬件中断,就是硬件通过中断控制器触发的。
有的设备需要读取或者写入大量数据。这种类型的设备需要支持 DMA 功能,也就是说,允许设备在 CPU 不参与的情况下,能够自行完成对内存的读写。
CPU 只需要对 DMA 控制器下指令,说它想读取多少数据,放在内存的某个地方就可以了,接下来 DMA 控制器会发指令给磁盘控制器,读取磁盘上的数据到指定的内存位置,传输完毕之后,DMA 控制器发中断通知 CPU 指令完成,CPU 就可以直接用内存里面现成的数据了。
用驱动程序屏蔽设备控制器差异
每种设备的控制器的寄存器、缓冲区等使用模式,指令都不同,操作系统需要通过驱动程序对接不同的设备控制器。设备驱动程序中是一些面向特殊设备控制器的代码。不同的设备不同。但是对于操作系统其它部分的代码而言,设备驱动程序应该有统一的接口。就像下面图中的一样,不同的设备驱动程序,可以以同样的方式接入操作系统,而操作系统的其它部分的代码,也可以无视不同设备的区别,以同样的接口调用设备驱动程序。
驱动程序就像一个适配层, 向上对操作系统提供统一的接口,向下对接具体的设备控制器。所有设备驱动程序都要,按照同样的规则,实现同样的方法。
设备驱动程序是用来对接设备控制器的,中断处理也在设备驱动里面完成。
中断的触发最终会到达 CPU,会中断操作系统当前运行的程序,所以操作系统也要有一个统一的流程来处理中断,使得不同设备的中断使用统一的流程。
一个设备驱动程序初始化的时候,要先注册一个该设备的中断处理函数。中断的时候,触发的函数是 do_IRQ。这个函数是中断处理的统一入口。在这个函数里面,我们可以找到设备驱动程序注册的中断处理函数 Handler,然后执行它进行中断处理。
对于块设备来讲,在驱动程序之上,文件系统之下,还需要一层通用设备层。这一层通用块层,将与块设备相关的通用逻辑放在这一层,维护与设备无关的块的大小,然后通用块层下面对接各种各样的驱动程序。
用文件系统接口屏蔽驱动程序的差异
从硬件设备到设备控制器,到驱动程序,到通用块层,到文件系统,层层屏蔽不同的设备的差别,最终到这里涉及对用户使用接口,也要统一。操作设备,都是基于文件系统的接口,也要有一个统一的标准。
首先要统一的是设备名称。所有设备都在 /dev/ 文件夹下面创建一个特殊的设备文件。这个设备特殊文件也有 inode,但是它不关联到硬盘或任何其他存储介质上的数据,而是建立了与某个设备驱动程序的连接。对于设备文件,ls 出来的内容和我们原来讲过的稍有不同。
1 | cd /dev |
主设备号相同,表示它们使用同样的字符设备驱动。
Linux 的驱动程序已经被写成和操作系统有标准接口的代码,可以看成一个标准的内核模块。在 Linux 里面,安装驱动程序,其实就是加载一个内核模块。可以通过lsmod查看。
1 |
|
如果没有安装过相应的驱动,可以通过 insmod 安装内核模块。内核模块的后缀一般是 ko。
linux使用sysfs文件系统管理设备,在/sys目录下面,可以查找到设备的信息。
在 /sys 路径下有下列的文件夹:
- /sys/devices 是内核对系统中所有设备的分层次的表示;
- /sys/dev 目录下一个 char 文件夹,一个 block 文件夹,分别维护一个按字符设备和块设备的主次号码 (major:minor) 链接到真实的设备 (/sys/devices 下) 的符号链接文件;
- /sys/block 是系统中当前所有的块设备;
- /sys/module 有系统中所有模块的信息。
有了 sysfs 以后,我们还需要一个守护进程 udev。当一个设备新插入系统的时候,内核会检测到这个设备,并会创建一个内核对象 kobject 。 这个对象通过 sysfs 文件系统展现到用户层,同时内核还向用户空间发送一个热插拔消息。udevd 会监听这些消息,在 /dev 中创建对应的文件。
有些任务只使用读写很难完成,例如检查特定于设备的功能和属性,超出了通用文件系统的限制。所以,对于设备来讲,还有一种接口称为 ioctl,表示输入输出控制接口,是用于配置和修改特定设备属性的通用接口。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。