You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 31 | 输入与输出:如何建立售前售后生态体系?
到这一节,操作系统作为一家外包公司,里面最核心的职能部门差不多都凑齐了。我们有了项目管理部门(进程管理),有为了维护项目执行期间数据的会议室管理部门(内存管理),有项目执行完毕后归档的档案库管理部门(文件系统)。
这一节,我们来规划一下这家公司的售前售后生态体系(输入输出系统)。这里你需要注意“生态”两个字,我们不仅仅是招聘一些售前和售后员工,而是应该建立一套体系让供应商,让渠道帮着我们卖,形成一个生态。
计算机系统的输入和输出系统都有哪些呢我们能举出来的例如键盘、鼠标、显示器、网卡、硬盘、打印机、CD/DVD等等多种多样。这样当然方便用户使用了但是对于操作系统来讲却是一件复杂的事情因为这么多设备形状、用法、功能都不一样怎么才能统一管理起来呢
## 用设备控制器屏蔽设备差异
这有点像一家公司要做To B的生意发现客户多种多样众口难调不同的地域不一样不同的行业不一样。如果你不懂某个地方的规矩根本卖不出去东西如果你不懂某个具体行业的使用场景也无法满足客户的需求。怎么办呢一般公司采取的策略就是建立生态设置很多代理商让各个地区和各个行业的代理商帮你屏蔽这些差异化。你和代理商之间只要进行简单的标准产品交付就可以了。
计算机系统也是这样的CPU并不直接和设备打交道它们中间有一个叫作**设备控制器**Device Control Unit的组件例如硬盘有磁盘控制器、USB有USB控制器、显示器有视频控制器等。这些控制器就像代理商一样它们知道如何应对硬盘、鼠标、键盘、显示器的行为。
如果你是一家大公司你的代理商往往是小公司。控制器其实有点儿像一台小电脑。它有它的芯片类似小CPU执行自己的逻辑。它也有它的寄存器。这样CPU就可以通过写这些寄存器对控制器下发指令通过读这些寄存器查看控制器对于设备的操作状态。
CPU对于寄存器的读写可比直接控制硬件要标准和轻松很多。这就相当于你和代理商的标准产品交付。
输入输出设备我们大致可以分为两类:**块设备**Block Device和**字符设备**Character Device
* 块设备将信息存储在固定大小的块中,每个块都有自己的地址。硬盘就是常见的块设备。
* 字符设备发送或接收的是字节流。而不用考虑任何块结构,没有办法寻址。鼠标就是常见的字符设备。
由于块设备传输的数据量比较大控制器里往往会有缓冲区。CPU写入缓冲区的数据攒够一部分才会发给设备。CPU读取的数据也需要在缓冲区攒够一部分才拷贝到内存。
这个也比较好理解,代理商我们也可以分成两种。一种是**集成商模式**,也就是说没有客户的时候,代理商不会在你这里采购产品,每次它遇到一个客户的时候,会带上你,共同应标。你出标准产品,地域的和行业的差异,它来搞定。这有点儿像字符设备。另外一种是**代购代销模式**,也就是说从你这里批量采购一批产品,然后没卖完之前,基本就不会找你了。这有点儿像块设备。
CPU如何同控制器的寄存器和数据缓冲区进行通信呢
* 每个控制寄存器被分配一个I/O端口我们可以通过特殊的汇编指令例如in/out类似的指令操作这些寄存器。
* 数据缓冲区可内存映射I/O可以分配一段内存空间给它就像读写内存一样读写数据缓冲区。如果你去看内存空间的话有一个原来我们没有讲过的区域ioremap就是做这个的。
这有点儿像,如果你要给你的代理商下一个任务,或者询问订单的状态,直接打电话联系他们的负责人就可以了。如果你需要和代理商做大量的交互,共同讨论应标方案,那电话说太麻烦了,你可以把代理商拉到你们公司来,你们直接在一个会议室里面出方案。
对于CPU来讲这些外部设备都有自己的大脑可以自行处理一些事情但是有个问题是当你给设备发了一个指令让它读取一些数据它读完的时候怎么通知你呢
控制器的寄存器一般会有状态标志位,可以通过检测状态标志位,来确定输入或者输出操作是否完成。第一种方式就是**轮询等待**,就是一直查,一直查,直到完成。当然这种方式很不好,于是我们有了第二种方式,就是可以通过**中断**的方式,通知操作系统输入输出操作已经完成。
为了响应中断我们一般会有一个硬件的中断控制器当设备完成任务后触发中断到中断控制器中断控制器就通知CPU一个中断产生了CPU需要停下当前手里的事情来处理中断。
这就像代理商有了新客户客户有了新需求客户交付完毕等事件都需要有一种机制通知你们公司在哪里呢当然是在办事大厅呀。如果你问不对呀办事大厅不是处理系统调用的么还记得32位系统调用是通过INT产生软中断触发的么这就统一起来了中断有两种一种**软中断**例如代码调用INT指令触发一种是**硬件中断**,就是硬件通过中断控制器触发的。所以将中断作为办事大厅的一项服务,没有什么问题。
![](https://static001.geekbang.org/resource/image/5d/55/5d9290f08847685d65bc3edd88242855.jpg)
有的设备需要读取或者写入大量数据。如果所有过程都让CPU协调的话就需要占用CPU大量的时间比方说磁盘就是这样的。这种类型的设备需要支持DMA功能也就是说允许设备在CPU不参与的情况下能够自行完成对内存的读写。实现DMA机制需要有个DMA控制器帮你的CPU来做协调就像下面这个图中显示的一样。
CPU只需要对DMA控制器下指令说它想读取多少数据放在内存的某个地方就可以了接下来DMA控制器会发指令给磁盘控制器读取磁盘上的数据到指定的内存位置传输完毕之后DMA控制器发中断通知CPU指令完成CPU就可以直接用内存里面现成的数据了。还记得咱们讲内存的时候有个DMA区域就是这个作用。
DMA有点儿像一些比较大的代理商不但能够帮你代购代销而且自己有能力售前、售后和技术支持实施部署都能自己搞定。
![](https://static001.geekbang.org/resource/image/1e/35/1ef05750bc9ff87a3330104802965335.jpeg)
## 用驱动程序屏蔽设备控制器差异
虽然代理商机制能够帮我们屏蔽很多设备的细节,但是从上面的描述我们可以看出,由于每种设备的控制器的寄存器、缓冲区等使用模式,指令都不同,所以对于操作系统这家公司来讲,需要有个部门专门对接代理商,向其他部门屏蔽代理商的差异,类似公司的渠道管理部门。
那什么才是操作系统的渠道管理部门呢?就是用来对接各个设备控制器的设备驱动程序。
这里需要注意的是,设备控制器不属于操作系统的一部分,但是设备驱动程序属于操作系统的一部分。操作系统的内核代码可以像调用本地代码一样调用驱动程序的代码,而驱动程序的代码需要发出特殊的面向设备控制器的指令,才能操作设备控制器。
设备驱动程序中是一些面向特殊设备控制器的代码。不同的设备不同。但是对于操作系统其它部分的代码而言,设备驱动程序应该有统一的接口。就像下面图中的一样,不同的设备驱动程序,可以以同样的方式接入操作系统,而操作系统的其它部分的代码,也可以无视不同设备的区别,以同样的接口调用设备驱动程序。
接下来两节,我们会讲字符设备驱动程序和块设备驱动程序的模型,从那里我们也可以看出,所有设备驱动程序都要,按照同样的规则,实现同样的方法。
![](https://static001.geekbang.org/resource/image/7b/68/7bf96d3c8e3a82cdac9c7629b81fa368.png)
上面咱们说了,设备做完了事情要通过中断来通知操作系统。那操作系统就需要有一个地方处理这个中断,既然设备驱动程序是用来对接设备控制器的,中断处理也应该在设备驱动里面完成。
然而中断的触发最终会到达CPU会中断操作系统当前运行的程序所以操作系统也要有一个统一的流程来处理中断使得不同设备的中断使用统一的流程。
一般的流程是一个设备驱动程序初始化的时候要先注册一个该设备的中断处理函数。咱们讲进程切换的时候说过中断返回的那一刻是进程切换的时机。不知道你还记不记得中断的时候触发的函数是do\_IRQ。这个函数是中断处理的统一入口。在这个函数里面我们可以找到设备驱动程序注册的中断处理函数Handler然后执行它进行中断处理。
![](https://static001.geekbang.org/resource/image/aa/c0/aa9d074d9819f0eb513e11014a5772c0.jpg)
另外对于块设备来讲在驱动程序之上文件系统之下还需要一层通用设备层。比如咱们上一章讲的文件系统里面的逻辑和磁盘设备没有什么关系可以说是通用的逻辑。在写文件的最底层我们看到了BIO字眼的函数但是好像和设备驱动也没有什么关系。是的因为块设备类型非常多而Linux操作系统里面一切是文件。我们也不想文件系统以下就直接对接各种各样的块设备驱动程序这样会使得文件系统的复杂度非常高。所以我们在中间加了一层通用块层将与块设备相关的通用逻辑放在这一层维护与设备无关的块的大小然后通用块层下面对接各种各样的驱动程序。
![](https://static001.geekbang.org/resource/image/3c/73/3c506edf93b15341da3db658e9970773.jpg)
## 用文件系统接口屏蔽驱动程序的差异
上面我们从硬件设备到设备控制器,到驱动程序,到通用块层,到文件系统,层层屏蔽不同的设备的差别,最终到这里涉及对用户使用接口,也要统一。
虽然我们操作设备,都是基于文件系统的接口,也要有一个统一的标准。
首先要统一的是设备名称。所有设备都在/dev/文件夹下面创建一个特殊的设备文件。这个设备特殊文件也有inode但是它不关联到硬盘或任何其他存储介质上的数据而是建立了与某个设备驱动程序的连接。
硬盘设备这里有一点绕。假设是/dev/sdb这是一个设备文件。这个文件本身和硬盘上的文件系统没有任何关系。这个设备本身也不对应硬盘上的任何一个文件/dev/sdb其实是在一个特殊的文件系统devtmpfs中。但是当我们将/dev/sdb格式化成一个文件系统ext4的时候就会将它mount到一个路径下面。例如在/mnt/sdb下面。这个时候/dev/sdb还是一个设备文件在特殊文件系统devtmpfs中而/mnt/sdb下面的文件才是在ext4文件系统中只不过这个设备是在/dev/sdb设备上的。
这里我们只关心设备文件当我们用ls -l在/dev下面执行的时候就会有这样的结果。
```
# ls -l
crw------- 1 root root 5, 1 Dec 14 19:53 console
crw-r----- 1 root kmem 1, 1 Dec 14 19:53 mem
crw-rw-rw- 1 root root 1, 3 Dec 14 19:53 null
crw-r----- 1 root kmem 1, 4 Dec 14 19:53 port
crw-rw-rw- 1 root root 1, 8 Dec 14 19:53 random
crw--w---- 1 root tty 4, 0 Dec 14 19:53 tty0
crw--w---- 1 root tty 4, 1 Dec 14 19:53 tty1
crw-rw-rw- 1 root root 1, 9 Dec 14 19:53 urandom
brw-rw---- 1 root disk 253, 0 Dec 31 19:18 vda
brw-rw---- 1 root disk 253, 1 Dec 31 19:19 vda1
brw-rw---- 1 root disk 253, 16 Dec 14 19:53 vdb
brw-rw---- 1 root disk 253, 32 Jan 2 11:24 vdc
crw-rw-rw- 1 root root 1, 5 Dec 14 19:53 zero
```
对于设备文件ls出来的内容和我们原来讲过的稍有不同。
首先是第一位字符。如果是字符设备文件则以c开头如果是块设备文件则以b开头。其次是这里面的两个号一个是主设备号一个是次设备号。主设备号定位设备驱动程序次设备号作为参数传给启动程序选择相应的单元。
从上面的列表我们可以看出来mem、null、random、urandom、zero都是用同样的主设备号1也就是它们使用同样的字符设备驱动而vda、vda1、vdb、vdc也是同样的主设备号也就是它们使用同样的块设备驱动。
有了设备文件我们就可以使用对于文件的操作命令和API来操作文件了。例如使用cat命令可以读取/dev/random 和/dev/urandom的数据流可以用od命令转换为十六进制后查看。
```
cat /dev/urandom | od -x
```
这里还是要明确一下,如果用文件的操作作用于/dev/sdb的话会无法操作文件系统上的文件操作的这个设备。
如果Linux操作系统新添加了一个设备应该做哪些事情呢就像咱们使用Windows的时候如果新添加了一种设备首先要看这个设备有没有相应的驱动。如果没有就需要安装一个驱动等驱动安装好了设备就在Windows的设备列表中显示出来了。
在Linux上面如果一个新的设备从来没有加载过驱动也需要安装驱动。Linux的驱动程序已经被写成和操作系统有标准接口的代码可以看成一个标准的内核模块。在Linux里面安装驱动程序其实就是加载一个内核模块。
我们可以用命令lsmod查看有没有加载过相应的内核模块。这个列表很长我这里列举了其中一部分。可以看到这里面有网络和文件系统的驱动。
```
# lsmod
Module Size Used by
iptable_filter 12810 1
bridge 146976 1 br_netfilter
vfat 17461 0
fat 65950 1 vfat
ext4 571716 1
cirrus 24383 1
crct10dif_pclmul 14307 0
crct10dif_common 12595 1 crct10dif_pclmul
```
如果没有安装过相应的驱动可以通过insmod安装内核模块。内核模块的后缀一般是ko。
例如我们要加载openvswitch的驱动就要通过下面的命令
```
insmod openvswitch.ko
```
一旦有了驱动我们就可以通过命令mknod在/dev文件夹下面创建设备文件就像下面这样
```
mknod filename type major minor
```
其中filename就是/dev下面的设备名称type就是c为字符设备b为块设备major就是主设备号minor就是次设备号。一旦执行了这个命令新创建的设备文件就和上面加载过的驱动关联起来这个时候就可以通过操作设备文件来操作驱动程序从而操作设备。
你可能会问人家Windows都说插上设备后一旦安装了驱动就直接在设备列表中出来了你这里怎么还要人来执行命令创建呀能不能智能一点
当然可以,这里就要用到另一个管理设备的文件系统,也就是/sys路径下面的sysfs文件系统。它把实际连接到系统上的设备和总线组成了一个分层的文件系统。这个文件系统是当前系统上实际的设备数的真实反映。
在/sys路径下有下列的文件夹
* /sys/devices是内核对系统中所有设备的分层次的表示
* /sys/dev目录下一个char文件夹一个block文件夹分别维护一个按字符设备和块设备的主次号码(major:minor)链接到真实的设备(/sys/devices下)的符号链接文件;
* /sys/block是系统中当前所有的块设备
* /sys/module有系统中所有模块的信息。
有了sysfs以后我们还需要一个守护进程udev。当一个设备新插入系统的时候内核会检测到这个设备并会创建一个内核对象kobject 。 这个对象通过sysfs文件系统展现到用户层同时内核还向用户空间发送一个热插拔消息。udevd会监听这些消息在/dev中创建对应的文件。
![](https://static001.geekbang.org/resource/image/62/90/6234738aac8d5897449e1a541d557090.jpg)
有了文件系统接口之后我们不但可以通过文件系统的命令行操作设备也可以通过程序调用read、write函数像读写文件一样操作设备。但是有些任务只使用读写很难完成例如检查特定于设备的功能和属性超出了通用文件系统的限制。所以对于设备来讲还有一种接口称为ioctl表示输入输出控制接口是用于配置和修改特定设备属性的通用接口这个我们后面几节会详细说。
## 总结时刻
这一节,我们讲了输入与输出设备的管理,内容比较多。输入输出设备就像管理代理商一样。因为代理商复杂多变,代理商管理也同样复杂多变,需要层层屏蔽差异化的部分,给上层提供标准化的部分,最终到用户态,给用户提供了基于文件系统的统一的接口。
![](https://static001.geekbang.org/resource/image/80/7f/80e152fe768e3cb4c84be62ad8d6d07f.jpg)
## 课堂练习
如果你手头的Linux是一台物理机试着插进一块U盘看文件系统中设备的变化。如果你没有Linux物理机可以使用公有云的云主机添加一块硬盘看文件系统中设备的变化。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)