<audio id="audio" title="31 | 输入与输出：如何建立售前售后生态体系？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/41/959cc496dd14b1e75283c5876ed1db41.mp3"></audio>

到这一节，操作系统作为一家外包公司，里面最核心的职能部门差不多都凑齐了。我们有了项目管理部门（进程管理），有为了维护项目执行期间数据的会议室管理部门（内存管理），有项目执行完毕后归档的档案库管理部门（文件系统）。

这一节，我们来规划一下这家公司的售前售后生态体系（输入输出系统）。这里你需要注意“生态”两个字，我们不仅仅是招聘一些售前和售后员工，而是应该建立一套体系让供应商，让渠道帮着我们卖，形成一个生态。

计算机系统的输入和输出系统都有哪些呢？我们能举出来的，例如键盘、鼠标、显示器、网卡、硬盘、打印机、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指令触发，一种是**硬件中断**，就是硬件通过中断控制器触发的。所以将中断作为办事大厅的一项服务，没有什么问题。

<img src="https://static001.geekbang.org/resource/image/5d/55/5d9290f08847685d65bc3edd88242855.jpg" alt="">

有的设备需要读取或者写入大量数据。如果所有过程都让CPU协调的话，就需要占用CPU大量的时间，比方说，磁盘就是这样的。这种类型的设备需要支持DMA功能，也就是说，允许设备在CPU不参与的情况下，能够自行完成对内存的读写。实现DMA机制需要有个DMA控制器帮你的CPU来做协调，就像下面这个图中显示的一样。

CPU只需要对DMA控制器下指令，说它想读取多少数据，放在内存的某个地方就可以了，接下来DMA控制器会发指令给磁盘控制器，读取磁盘上的数据到指定的内存位置，传输完毕之后，DMA控制器发中断通知CPU指令完成，CPU就可以直接用内存里面现成的数据了。还记得咱们讲内存的时候，有个DMA区域，就是这个作用。

DMA有点儿像一些比较大的代理商，不但能够帮你代购代销，而且自己有能力售前、售后和技术支持，实施部署都能自己搞定。

<img src="https://static001.geekbang.org/resource/image/1e/35/1ef05750bc9ff87a3330104802965335.jpeg" alt="">

## 用驱动程序屏蔽设备控制器差异

虽然代理商机制能够帮我们屏蔽很多设备的细节，但是从上面的描述我们可以看出，由于每种设备的控制器的寄存器、缓冲区等使用模式，指令都不同，所以对于操作系统这家公司来讲，需要有个部门专门对接代理商，向其他部门屏蔽代理商的差异，类似公司的渠道管理部门。

那什么才是操作系统的渠道管理部门呢？就是用来对接各个设备控制器的设备驱动程序。

这里需要注意的是，设备控制器不属于操作系统的一部分，但是设备驱动程序属于操作系统的一部分。操作系统的内核代码可以像调用本地代码一样调用驱动程序的代码，而驱动程序的代码需要发出特殊的面向设备控制器的指令，才能操作设备控制器。

设备驱动程序中是一些面向特殊设备控制器的代码。不同的设备不同。但是对于操作系统其它部分的代码而言，设备驱动程序应该有统一的接口。就像下面图中的一样，不同的设备驱动程序，可以以同样的方式接入操作系统，而操作系统的其它部分的代码，也可以无视不同设备的区别，以同样的接口调用设备驱动程序。

接下来两节，我们会讲字符设备驱动程序和块设备驱动程序的模型，从那里我们也可以看出，所有设备驱动程序都要，按照同样的规则，实现同样的方法。

<img src="https://static001.geekbang.org/resource/image/7b/68/7bf96d3c8e3a82cdac9c7629b81fa368.png" alt="">

上面咱们说了，设备做完了事情要通过中断来通知操作系统。那操作系统就需要有一个地方处理这个中断，既然设备驱动程序是用来对接设备控制器的，中断处理也应该在设备驱动里面完成。

然而中断的触发最终会到达CPU，会中断操作系统当前运行的程序，所以操作系统也要有一个统一的流程来处理中断，使得不同设备的中断使用统一的流程。

一般的流程是，一个设备驱动程序初始化的时候，要先注册一个该设备的中断处理函数。咱们讲进程切换的时候说过，中断返回的那一刻是进程切换的时机。不知道你还记不记得，中断的时候，触发的函数是do_IRQ。这个函数是中断处理的统一入口。在这个函数里面，我们可以找到设备驱动程序注册的中断处理函数Handler，然后执行它进行中断处理。

<img src="https://static001.geekbang.org/resource/image/aa/c0/aa9d074d9819f0eb513e11014a5772c0.jpg" alt="">

另外，对于块设备来讲，在驱动程序之上，文件系统之下，还需要一层通用设备层。比如咱们上一章讲的文件系统，里面的逻辑和磁盘设备没有什么关系，可以说是通用的逻辑。在写文件的最底层，我们看到了BIO字眼的函数，但是好像和设备驱动也没有什么关系。是的，因为块设备类型非常多，而Linux操作系统里面一切是文件。我们也不想文件系统以下，就直接对接各种各样的块设备驱动程序，这样会使得文件系统的复杂度非常高。所以，我们在中间加了一层通用块层，将与块设备相关的通用逻辑放在这一层，维护与设备无关的块的大小，然后通用块层下面对接各种各样的驱动程序。

<img src="https://static001.geekbang.org/resource/image/3c/73/3c506edf93b15341da3db658e9970773.jpg" alt="">

## 用文件系统接口屏蔽驱动程序的差异

上面我们从硬件设备到设备控制器，到驱动程序，到通用块层，到文件系统，层层屏蔽不同的设备的差别，最终到这里涉及对用户使用接口，也要统一。

虽然我们操作设备，都是基于文件系统的接口，也要有一个统一的标准。

首先要统一的是设备名称。所有设备都在/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中创建对应的文件。

<img src="https://static001.geekbang.org/resource/image/62/90/6234738aac8d5897449e1a541d557090.jpg" alt="">

有了文件系统接口之后，我们不但可以通过文件系统的命令行操作设备，也可以通过程序，调用read、write函数，像读写文件一样操作设备。但是有些任务只使用读写很难完成，例如检查特定于设备的功能和属性，超出了通用文件系统的限制。所以，对于设备来讲，还有一种接口称为ioctl，表示输入输出控制接口，是用于配置和修改特定设备属性的通用接口，这个我们后面几节会详细说。

## 总结时刻

这一节，我们讲了输入与输出设备的管理，内容比较多。输入输出设备就像管理代理商一样。因为代理商复杂多变，代理商管理也同样复杂多变，需要层层屏蔽差异化的部分，给上层提供标准化的部分，最终到用户态，给用户提供了基于文件系统的统一的接口。

<img src="https://static001.geekbang.org/resource/image/80/7f/80e152fe768e3cb4c84be62ad8d6d07f.jpg" alt="">

## 课堂练习

如果你手头的Linux是一台物理机，试着插进一块U盘，看文件系统中设备的变化。如果你没有Linux物理机，可以使用公有云的云主机，添加一块硬盘，看文件系统中设备的变化。

欢迎留言和我分享你的疑惑和见解 ，也欢迎可以收藏本节内容，反复研读。你也可以把今天的内容分享给你的朋友，和他一起学习和进步。

<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
