说明:
- 所有代码均在Ubuntu 18.04(内核版本
linux 5.4.0-52-generic
)上编译运行通过- 本文同步发表于官方博客——
www.linuxrevealed.com
类Unix操作系统有一套由顶级开发人员积累起来并形成的、用于模块化软件开发的文化规范和哲学方法。这种理念强调构建简单、短小精悍、清晰、模块化并且可扩展的代码,这些特性使得代码可以由其编写者之外的人也能轻松的维护和重复使用。阅读和学习Linux系统的源码,不止是要学习其实现机制,方便开发或者解bug,还需要在阅读代码过程中学习其代码实现的原理及优点,从而指导我们自己也编写出优秀的代码。同时,掌握这些思想还可以帮助我们快速理解代码的框架,在阅读代码的过程中,起到事半功倍的效果。下面我们来认识几点Linux开发背后的哲学。
一、 只提供机制,不提供策略
区分机制和策略是Unix设计背后隐含的最好思想之一。所谓的机制就是“程序能提供什么功能”,而所谓的策略就是“如何使用或者什么时候使用这些功能”。对于Linux操作系统来讲,它只提供了机制,并不提供策略。策略部分由运行于其上的应用程序来完成。
举个简单的例子来理解内核的"机制"和"策略"。比如,之前较早版本的Android手机上,都会有一个三色LED指示灯。当有未接来电、未读消息时,充电时,灯会有不同的状态。那么,对于LED驱动程序,它需要提供的机制可以包括:
1)LED灯亮或者灭(不同颜色值实际上对应的只是不同的LED设备而已);
2)LED灯以某个频率、某个颜色闪烁 (闪烁频率、颜色值由应用程序传入)。对于不同场景下,LED闪亮的策略可以有如下设计:
1)当有未接来电时
,调用红色LED驱动程序的接口,设置
闪烁频率为每秒一次;
2)当有未读消息时
,调用绿色LED驱动程序的接口,设置
闪烁频率为每秒一次;
3)当电池处于充电状态,且电池电量还未达到100%时
,调用黄色LED(实际上是同时控制红色LED和绿色LED)驱动程序的接口,设置
其亮度,使其常亮;
4)当电池处于充电状态,且电池电量已达到100%时
,调用绿色LED驱动程序的接口,设置
其亮度,使其常亮;
注:上文中当... ...时,设置... ...
这个动作就组成了一种策略,通俗来讲就是,在某个条件具备时,就执行某种特定的动作,这是由上层的应用程序来实现的。
结合上面的例子,驱动程序提供策略
,赋予操作系统可以做某事的功能,而应用程序提供机制
,调用操作系统提供的接口,在适当的时候去做某事。
为什么要把机制和策略分开?而Linux只提供机制呢?
这要从计算机说起,计算机是为用户服务的,它是用户实现某种需求或者完成某个任务的工具。而Linux是一个运行于计算机操作系统,操作系统的其中一个很重要的作用就是管理计算机的硬件资源,并提供接口给用户程序(应用程序)。而应用程序实际上就是代表用户来使用计算机的资源完成某项任务。那么,其中的从属关系自然就决定用户是主体,其意愿或想法需要通过应用程序调用操作系统接口来实现,因此,操作系统只需要提供机制,至于策略怎么实现,需要用户通过应用程序来决定。
结合上面的例子,之前的项目中有一个需求:修改Charger的驱动程序,使得充电时,点亮黄色LED灯,充满电后点亮绿色LED灯。这实际上就在driver中增加了亮灯的策略——操作系统根据是否在充电决定了黄灯亮还是绿灯亮的策略。那么,假设有未读信息时,灯应该以每秒一次的频率闪烁绿色(应用程序控制的策略)。这样的话,当充满电,而且又来未读信息时,灯应该怎么设置呢?
应用程序检测到有未读信息,会把灯设置成“绿灯每秒闪一次”,而驱动程序检测到此时已经充满电,根据它被赋予的策略——充电时,点亮黄色LED灯,充满电后点亮绿色LED灯,它会直接点亮绿灯。那么,此时灯就比较困惑了,到底是常亮呢?还是每秒闪一次呢?
因此,编写访问硬件的内核驱动程序代码时,不要在其中加入任何策略相关的代码。这样将机制与策略分离之后,可以保证不同的用户程序能够按照自己的需求使用驱动程序,而不出现不同用户之间策略的相互影响和干扰。
二、 分离和分层的思想
代码实现的越是短小精悍,其复用程度越高,出问题的几率就越小。这种思想实际上就是要求代码中所有的子结构(函数或者模块)都需要“高内聚低耦合”。这种思想形成了Linux代码中分层和分离。
分层思想实际上就是一个实现纵向的分为了不同的层次,每个层次使用下层提供的服务,并对这个层次的服务进程重新的组合和分发,再为其上层提供一个统一的接口,从而屏蔽掉下层的异构体。
分层思想在设备驱动的代码中尤为常见。比如我们即将要看到的在设备驱动模型中,kobject
,device
和platform_device
就是这种关系。简单来讲,kobject
抽象出了内核中一种对象,并提供了对这个对象操作的一些方法。这种对象是用来对包含它的对象做引用计数用的。而device
在使用的时候偏偏就需要对其引用计数,以便在不被使用的时候被释放。所以,device
结构就包含了kobject
结构,并且device
的大部分操作方法都调用了kobject
的提供的API。类似的,platform_device
是一种特殊的device
,所以,platform_device
包含了device
结构,并且,platform_device
相关的操作方法最终是由device
的提供的API来实现的。
当然,既然构造了kobject
这种特殊结构,它并不只是用来给device
结构使用的,内核中还有其他很多基础的结构其实都包含了kobject
结构,比如,device_node
,cdev
也会直接包含kobject
结构。类似的,device
这种特殊结构,并不只是用于platform_device
的,与platform_device
类似的真实的设备像i2c_client
,usb_device
都存在对应的结构体,并直接包含device
结构。
分离思想就是把同一类设备的可复用的代码抽象出来,单独写成了一个驱动框架,具体设备相关的代码会再构成一个驱动,并配合上述驱动框架完成真正的设备驱动功能。比如,在子模块提供一个core driver,用于实现某一类设备的通用的控制方法,在这些控制方法中,真正需要控制某个设备时,会通过函数指针调用由不同的驱动程序实现的相应的设备的实际控制方法。这样就把对某类设备通用的操作的代码和具体设备特定的代码分离开来,从而最大程度的实现代码的复用,并且提高了代码的可扩展性。类似这种分层化的子模块在Linux中有很多,比如led driver,pwm driver等,后续我们分析相关代码的时候再来体会。
模块的分离和分层的思想对于保证模块的低耦合和其高复用有很大作用,在后面博文中分析具体模块的代码时,我们会对其有深刻的体会的。
[^_^]:
设备驱动程序属于内核程序,因此一定要注意并发的处理。引起并发的原因可能有以下几种:
1)Linux通常运行着多个并发的进程,而这些进程可能同时使用某个驱动程序,从而导致某个驱动程序的并发;
2)中断处理程序的异步运行可能导致驱动程序的并发
3)一些异步运行的软件抽象(比如内核定时器)也有可能导致并发
4)SMP(对称多处理器)上,不同的CPU运行相同的程序也有可能导致并发
因此,对于驱动程序乃至内核中的其他模块在设计时,务必保证程序是可重入的,即它必须能够同时运行在多个上下文中。
三、 一切都是文件(Everything is a File)
在Linux系统上,有一个概念——“一切都是文件”。意思指对于Linux系统,所有的IO资源都是文件,包括文件、目录、硬盘、设备,甚至一些虚拟的设备,比如管道等都被认为是文件。这么做的好处是读写这些资源都可用open()/close()/write()/read()等函数进行处理。屏蔽了硬件的区别,所有设备都抽象成文件,提供统一的接口给用户。虽然类型各不相同,但是对其提供的却是同一套API。更进一步,对文件的操作也可以跨文件系统执行。
在Linux系统中有三类文件:普通文件、目录文件和特殊文件。
普通文件就是一般意义上我们所了解到的文件,比如打开计算机的某个文件夹之后,我们可以看到有保存日志的文本文件,存放照片的图像文件,存放烧写镜像的二进制文件等等,这些常规文件。
目录文件就是存放上述普通文件的目录,在Linux中它也被看作一个文件,也可以被读,写等。
最后一类,特殊文件,这类文件包括了代表硬盘之类的块设备文件,代表字符设备的字符设备文件,用于进程间通信管道的管道文件,网络通信使用到的socket文件等。后续的博客我们会一一接触到这些文件,并介绍这些文件的用法。
举个简单的例子,键盘在Linux系统中属于input类型的字符设备。那么,Linux也为连接到系统的键盘创建了一个对应的字符设备文件。执行
ls -l
命令可以看到当前系统中存在哪些input类的字符设备文件:$ ls -l /dev/input total 0 drwxr-xr-x 2 root root 60 3月 19 09:53 by-path crw-rw---- 1 root input 13, 64 3月 8 15:34 event0 crw-rw---- 1 root input 13, 65 3月 8 15:34 event1 crw-rw---- 1 root input 13, 66 3月 8 15:34 event2 crw-rw---- 1 root input 13, 67 3月 20 17:51 event3 crw-rw---- 1 root input 13, 63 3月 8 15:34 mice crw-rw---- 1 root input 13, 32 3月 20 17:51 mouse0 ...
这里的“
/dev/input/mouse0
”就是鼠标对应的字符设备文件。上层应用程序如果需要监听鼠标事件(比如,鼠标位移、点击事件),直接读这个文件就可以得到了。执行sudo cat /dev/input/mouse0
,然后,移动鼠标,则能够看到有数据输出。后面在介绍input子系统时,我们会详细分析这些数据。
Linux的设计哲学其实还有很多,有兴趣的话,大家可以参考文末的参考阅读。