learn-tech/专栏/计算机基础实战课/32IO管理:Linux如何管理多个外设?.md
2024-10-16 10:18:29 +08:00

18 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        32 IO管理Linux如何管理多个外设
                        你好我是LMOS。

在上一节课中我们通过对IO Cache的学习知道了IO Cache缓存了IO设备的数据这些数据经过IO 调度器送给块层进而发送给IO设备。

今天我们再往下一层探索以Linux为例看看Linux是如何管理多个IO外设的。我们先从例子出发了解一下设备在Linux中的与众不同然后看看设备分类及接口分析一下应用开发人员应该如何使用它们最后我会带你一起实现一个设备加深理解。

这节课的配套代码,你可以从这里下载。话不多说,我们开始吧。

文件和外设的关系

用几十行代码在Linux上读写一个文件我们都很熟悉吧。若是不熟悉百度、谷歌都可以让我们熟悉。

我们今天要写的这个小例子就是从读取一个文件开始的。想要读取文件首先得知道文件在哪里也就是需要知道文件路径名知道了文件路径名再进行“三步走”就可以打开它、读取它、关闭它。一句话open、read、close一气呵成。

那么这个文件是什么呢,路径名如下所示:

"/dev/input/event3"

看了路径名我们知道enent3文件在根目录下dev目录的input目录之下。从名称上看这好像与设备、输入、事件有关系我这里先卖个关子看完后面的讲解你自然就知道答案了。

我们先来搞清楚读取这个文件能得到什么数据,读取该文件得到的不是一个字符流,而是由时间、类型、码值以及状态数据封装成的一个结构。每读取一次,就能得到一个这样的结构,该结构如下所示:

struct input_event { struct timeval time; //时间 __u16 type; __u16 code; __s32 value; };

这个结构看上去好像是某个事件的信息,或者产生的数据。- 现在我们知道了读什么文件,也知道了读取该文件能得到什么样的内容,接下来我们立刻编写代码练练手。让我们写代码来实现读写”/dev/input/event3”文件如下所示

#define KB_DEVICE_FILE "/dev/input/event3"

int main(int argc, char *argv[]) { int fd = -1, ret = -1; struct input_event in; char *kbstatestr[] = {"弹起", "按下"}; char *kbsyn[] = {"开始", "键盘", "结束"}; //第一步:打开文件 fd = open(KB_DEVICE_FILE, O_RDONLY); if (fd < 0) { perror("打开文件失败"); return -1; } while (1) { //第二步读取一个event事件包 ret = read(fd, &in, sizeof(struct input_event)); if (ret != sizeof(struct input_event)) { perror("读取文件失败"); break; } //第三步解析event包 if (in.type == 1) { printf("------------------------------------\n"); printf("状态:%s 类型:%s 码:%d 时间:%ld\n", kbstatestr[in.value], kbsyn[in.type], in.code, in.time.tv_usec); if (in.code == 46) { break; } } } //第四步:关闭文件 close(fd); return 0; }

上述代码逻辑很简单,首先打开了/dev/input/event3这个文件然后在一个循环中反复读取该文件并打印出数据读取错误和码值等于46时就跳出循环最后关闭该文件程序退出。

接下来就是测试环节。我们用VSCode打开对应的工程目录编译一下然后运行。效果如下所示

你按下键盘上一个键,终端中它就会输出一行,松开键又会输出一行,输出“按下”或“弹起”的状态、键盘码以及按下弹起所花费的时间,这些数据精确地反映了键盘按键动作。

一个文件就能反映键盘的动作和数据,难道不奇怪吗?你是不是猛然醒悟了,原来/dev/input/event3这个文件就代表键盘这个文件是特殊的设备文件访问这种文件就是访问IO硬件设备。其实dev目录下全部的文件都是设备文件不知道你的脑海中是不是浮现出了熟悉的Linux设计哲学——一切都是文件。

你可以在dev目录下找到系统的所有设备它们都是以文件的形式存在的。从这种角度看这里的文件是抽象的是一种资源对象的标识。从上面的例子我们也可以看出设备的操作完全符合文件的操作方式。设备输入、输出数据的操作对应了文件的读写设备的启动或者停止则对应文件的打开或关闭。

说到这,你可能要反对我了:设备的操作不只是输入输出数据,还有设置设备功能、配置设备电源等操作么?例如设置声卡音量、设置处理器进入待机状态以减少功耗等等。

可是你别忘了文件还有一个操作——ioctrl通过它来给设备发送命令或者设置相关功能。这样一个设备的所有操作就和文件对上了。不过可不要想着用这种方案干坏事哦比如获取别人输入的敏感信息。

设备分类

设想一下,你需要管理你家里的日常用品,你通常会怎么做?你是不是首先会对这些物品进行分类。你可能会按物品的功能用途分类,也可能按物品归属于哪位家庭成员来分类。

对于Linux这个计算机大总管也是如此什么设备有什么功能、是用来做什么的、有多少个这种类型的设备、它们接入系统的方式是什么……这些信息Linux都需要了解得非常清楚才可以。

在了解Linux如何对设备进行分类之前我们应该先了解一下常规情况下系统中都有哪些设备。我为你画了一幅图如下所示

上图是一个典型的计算机系统,你先不管物理机器的结构和形式,逻辑上就是这样的。实际情况可能比图中有更多或者更少的总线和设备。

各种设备通过总线相连。这里我们只需要记住计算机中有很多的设备Linux 会把这些设备分成几类,分别是:网络设备、块设备、字符设备、杂项设备以及伪设备。具体情况你可以参考我后面梳理的示意图:

我们先来看看网络设备。网络设备在Linux上被抽象成一个接口设备相当于网线插口任何网络通信都要经过网络接口。接口就是能与其他主机交换数据的设备像是电子信号从网口流到另一个网口一样。

Linux使用一套传输数据包的函数来与网络设备驱动程序通信它们与字符设备和块设备或者文件的read()和write()接口不同所以网络设备在Linux中是一个独特的存在。

一般情况下接口对应于物理网卡但也可能是纯软件实现的比如输入ifconfig命令查看网口时会输出一个eth0、一个lo等信息lo就是网络回环loopback接口。Linux会给每个网络接口分配一个唯一的名字比如eth0、eth1等方便其它软件访问这些接口但这个名字在文件系统中并没有对应的文件名。

然后我们来看看块设备块设备这种设备类型也是Linux下的一个大类。块设备的特点是能按一块一块的方式传输数据而且能随机访问设备中的任一地址具体是通过/dev目录下的文件系统节点来访问。常见的块设备包括硬盘、flash、ssd、U盘、SD卡等。

块设备上通常能够安装文件系统即能被格式化。比如你的机器上有一块硬盘硬盘上有4个分区。那么在Linux系统中的表现就是这样的

这些设备文件可以像访问普通文件一样使用你只要计算好硬盘地址就能把数据写入到硬盘扇区中。比方说我们可以用cat /dev/sda1 > sda1.bk 命令,对硬盘的分区一进行备份。

然后我们来看看字符设备。字符设备也是Linux下的一个基础类设备比如键盘、鼠标串口声卡等都属于字符设备。字符设备是顺序访问的不能随机访问它只能像是访问字符数据字节流一样被访问只有在设备响应后才能读到相应信息这些功能由设备驱动程序保证和维护。

字符设备的驱动程序通常要实现打开、关闭、读取和写入回调函数供Linux使用。Linux会将应用程序中的调用转发给设备驱动程序的回调函数。字符设备的对应的文件名都在/dev目录下每一个文件对应一个字符设备或者块设备。

我们在/dev目录下可以使用ls -l命令查看详细信息第一个字母为“c”的即为字符设备文件第一个字母为“b”的即为块设备文件。

最后我们说说杂项设备和伪设备它们都是基于字符设备实现的本质上是属于字符设备。而伪设备则与其它设备不同它不对应物理硬件只是通过软件实现了一些功能比如读取random设备能产生一个随机数再比如把数据写入null设备数据会有去无回直接被丢弃还有通过读取kmsg设备获取内核输出的信息。

现在我们已经搞清楚了Linux是根据设备传输数据大小和传输方式来对设备进行分类的下面我们就可以亲手去创造一个设备了。

创造一个设备

一个再普通不过的计算机系统中也有种类繁多的设备。每种设备都有自己的编程控制方式所以Linux内核才用分而治之的方法把控制设备代码独立出来形成内核驱动程序模块。

这些驱动程序模块由驱动开发人员或设备厂商开发会按照Linux内核的规则来编写并提供相应接口供Linux内核调用。这些模块既能和Linux内核静态链接在一起也能动态加载到Linux内核这样就实现了Linux内核和众多的设备驱动的解耦。

你可能已经想到了一个驱动程序既可以是Linux内核的一个功能模块也能代表或者表示一个设备是否存在。

我们不妨再思考一个问题Linux内核所感知的设备一定要与物理设备一一对应吗

我们拿储存设备来举例,其实不管它是机械硬盘,还是 TF 卡或者是一个设备驱动程序它都可以向Linux内核表明它是储存设备。但是它完全有可能申请一块内存空间来储存数据不必访问真正的储存设备。所以Linux内核所感知的设备并不需要和物理设备对应这取决于驱动程序自身的行为。

现在我们就知道了创造一个设备等同于编写一个对应驱动程序。Linux内核只是和驱动程序交互而不需要系统中有真实存在的物理设备只要驱动程序告诉Linux内核是什么设备就行。

明白了驱动程序的原理我们这就来写一个驱动程序。先从Linux内核模块框架开始吧代码如下所示

#include <linux/module.h> #include <linux/init.h> //开始初始化函数 static int __init miscdrv_init(void) { printk(KERN_EMERG "INIT misc dev\n"); return 0; } //退出函数 static void __exit miscdrv_exit(void) { printk(KERN_EMERG "EXIT,misc\n"); }

module_init(miscdrv_init); module_exit(miscdrv_exit); //版权信息和作者 MODULE_LICENSE("GPL"); MODULE_AUTHOR("LMOS");

你看不到20行代码就构成了一个Linux内核模块。

从这个例子我们可以发现一个内核模块必须要具备两个函数一个是开始初始化函数在内核模块加载到Linux内核之后。首先就会调用该函数它的作用通常是创造设备另一个是退出函数内核模块退出到Linux内核之前首先就会调用该函数用于释放系统资源。

有了Linux内核模块之后我们现在还不能调用它这是因为我们没有创造设备对应用程序而言是无法使用的。那么怎么创建一个设备呢

Linux内核的驱动框架为我们提供了接口和方法只需要按照接口标准调用它就行了。这里我们需要创造一个杂项设备就需要调用misc_register函数。我们只要给这个函数提供一个杂项设备结构体作为参数就能在Linux内核中创造并注册一个杂项设备。

代码如下所示:

#define DEV_NAME "miscdevtest" //文件操作方法结构体 static const struct file_operations misc_fops = { .read = misc_read, //读回调函数 .write = misc_write, //写回调函数 .release = misc_release, //关闭回调函数 .open = misc_open, //打开回调函数 }; //杂项设备结构体 static struct miscdevice misc_dev = { .fops = &misc_fops, //设备文件操作方法 .minor = 255, //次设备号 .name = DEV_NAME, //设备名/dev/下的设备节点名 };

static int __init miscdrv_init(void) { misc_register(&misc_dev);//创造杂项设备 printk(KERN_EMERG "INIT misc dev\n"); return 0; }

对照这段代码我们看到Linux用一个miscdevice结构体表示一个杂项设备其实它内部包含了用于表示字符设备的cdev结构体所以杂项设备就是字符设备。

其实miscdevice结构体还有很多成员不过那些我们不用处理只需要设置以下三个成员就行了一是设备文件操作方法结构它是一些函数指针二是次设备号我们设置成最大值即255让系统自动处理三是设备名称就是在dev目录下的文件名。

完成上述操作最后只要在Linux内核模块的初始化miscdrv_init函数中调用misc_register函数就行了。

这里比较重要的是文件操作方法结构体中的回调函数它们是完成设备功能的主要函数应用程序对设备文件的打开、关闭、读、写等操作都会被Linux内核分发调用到这些函数。

举例来说在打开函数中你可以让设备加电工作起来而在读、写函数中你可以向设备传输数据。Linux内核并不在意你在这些函数做了什么也不在乎这些操作是不是直接作用于物理设备Linux内核只在乎是否有这些函数或者这些函数的执行状态是什么。

下面我们就来写好这些函数,如下所示:

//读回调函数 static ssize_t misc_read (struct file *pfile, char __user *buff, size_t size, loff_t *off) { printk(KERN_EMERG "line:%d,%s is call\n", LINE, FUNCTION); return 0; } //写回调函数 static ssize_t misc_write(struct file *pfile, const char __user *buff, size_t size, loff_t *off) { printk(KERN_EMERG "line:%d,%s is call\n", LINE, FUNCTION); return 0; } //打开回调函数 static int misc_open(struct inode *pinode, struct file *pfile) { printk(KERN_EMERG "line:%d,%s is call\n", LINE, FUNCTION); return 0; } //关闭回调函数 static int misc_release(struct inode *pinode, struct file *pfile) { printk(KERN_EMERG "line:%d,%s is call\n", LINE, FUNCTION); return 0; }

上述各种操作的回调函数非常简单都只调用了printk函数打印内核log这些log信息可以在/dev/kmsg设备文件中读取。

为了测试这个设备能否正常工作,我们还要写个应用程序对其访问,即对其进行打开、读、写、关闭这些操作,代码如下所示:

#define DEV_NAME "/dev/miscdevtest" int main(void) { char buf[] = {0, 0, 0, 0}; int i = 0; int fd; //打开设备文件 O_RDWR, O_RDONLY, O_WRONLY, fd = open(DEV_NAME, O_RDWR); if (fd < 0) { printf("打开 :%s 失败!\n", DEV_NAME); } //写数据到设备 write(fd, buf, 4); //从设备读取数据 read(fd, buf, 4); //关闭设备 可以不调用,程序关闭时系统自动调用 close(fd); return 0; }

我替你把所有的代码都准备好了可以从课程配套代码获取我们在工程目录下make一下就可以编译好了。成功编译后你会得到一个miscdrv.ko这是编译好的Linux内核模块文件还有一个是App文件这个是应用程序。

我们在测试之前先打开一个终端在其中输入sudo cat /dev/kmsg以便观察结果。然后再打开一个终端在其中输入sudo insmod miscdrv.ko把miscdrv.ko这个Linux内核模块安装加载到系统中。加载好了我们输入sudo app就可以看结果了如下图所示

通过截图,我们看到右边终端通过读取/dev/kmsg设备输出了正确的结果这说明我们的设备工作正常。只不过我们这个设备没有完成任何功能也没有对应真正的物理设备但是却真实地反映了设备的工作流程。

到这里我们已经理解了Linux管理设备的核心机制贯彻一切皆文件的思想Linux内核会在相应目录下建立特殊的文件节点用文件的形式表示一个设备。而内核操控设备的方式实质上就是把文件操作转发给对应的设备驱动程序回调函数来处理。

重点回顾

今天的课程就要结束了,现在我们一起来回顾一下今天的重点。

首先我们从一个例子开始写下了一个读取文件的应用程序。运行之后我们一按下键盘应用程序就能获取键盘数据这证明了我们读取的文件是一个设备间接地证明了Linux以文件的方式管理设备操作设备与操作文件相同。

然后我们一起探讨了Linux设备类型还分析了不同设备的特性。Linux按照设备的工作方式和数据传输类型对市面上的各种设备做了分类分成了字符设备、块设备、网络设备、杂项设备和伪设备。

最后我们创造了一个杂项设备了解了Linux如何感知设备、又是如何让应用程序访问到设备的。我们发现Linux用文件节点关联了Linux内核驱动程序模块为了操控设备内核会转发应用程序对文件的操作以此来调用驱动程序中的回调函数。

这就是Linux管理多个IO设备的方式但是Linux驱动模型远比今天课程所介绍的复杂得多其中还有支持总线和支持设备热拔插的机制。如果你想详细了解Linux驱动模型的实现可以阅读我的上一季课程《操作系统实战 45 讲》中的第二十八节课到三十一节课。

思考题

请问Linux网络通信的接口是什么

期待你在留言区聊聊你的学习收获或者提出疑问,如果觉得这节课还不错,别忘了分享给身边更多的朋友。