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.

364 lines
21 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.

# 28 | 部门分类:如何表示设备类型与设备驱动?
你好我是LMOS。
小到公司,大到国家,都有各种下属部门,比如我们国家现在有教育部、科学技术部、外交部,财政部等,这些部门各自负责完成不同的职能工作,如教育部负责教育事业和语言文字工作,科学技术部负责推动解决经济社会发展的重大科技问题。
既然大道相通那我们的Cosmos中是否也是类似这样的结构呢
答案是肯定的,在前面的课中,我们搞定了内存管理和进程管理,它们是内核不可分隔的,但是计算机中还有各种类型的设备需要管理。
我们的Cosmos也会“成立各类部门”用于管理众多设备一个部门负责一类设备。具体要怎么管理设备呢你不妨带着这个问题正式开始今天的学习
这节课的代码,你可以从[这里](https://gitee.com/lmos/cosmos/tree/master/lesson28~29/Cosmos)下载。
## 计算机的结构
不知道你是否和我一样,经常把计算机的机箱打开,看看 CPU看看内存条看看显卡看看主板上的各种芯片。
其实,这些芯片并非独立存在,而是以总线为基础连接在一起的,各自完成自己的工作,又能互相打配合,共同实现用户要求的功能。
为了帮你理清它们的连接关系,我为你画了一幅图,如下所示。
![](https://static001.geekbang.org/resource/image/2f/4c/2f7697d94bee25d4c036eb4bca16ee4c.jpg?wh=4400x3305 "计算机结构示意图")
上图是一个典型的桌面系统,你先不用管是物理上怎么样连接的,逻辑上就是这样的。实际可能比图中有更多或者更少的总线。但是总线有层级关系,各种设备通过总线相连。这里我们只需要记住,计算机中有很多种类的设备,脑中有刚才这幅图就行了。
## 如何管理设备
在前面的课程中我们实现了管理内存和进程其实进程从正面看它是管理应用程序的反过来看它也是管理CPU的它能使CPU的使用率达到最高。
管理内存和管理CPU是操作系统最核心的部分但是这还不够因为计算机不止有CPU还有各种设备。
如果把计算机内部所有的设备和数据都描述成资源,操作系统内核无疑是这些资源的管理者。既然设备也是一种资源,如何高效管理它们,以便提供给应用进程使用和操作,就是操作系统内核的重要任务。
### 分权而治
一个国家之所以有那么多部门,就是要把管理工作分开,专权专职专责,对于操作系统也是一样。
现代计算机早已不限于只处理计算任务,它还可以呈现图像、音频,和远程计算机通信,储存大量数据,以及和用户交互。所以,计算机内部需要处理图像、音频、网络、储存、交互的设备。这从上面的图中也可以看得出来。
操作系统内核要控制这些设备,就要包含每个设备的控制代码。如果操作系统内核被设计为通用可移植的内核,那是相当可怕的。试想一下,这个世界上有如此多的设备,操作系统内核代码得多庞大,越庞大就越危险,因为其中一行代码有问题,整个操作系统就崩溃了。
可是仅仅只有这些问题吗?当然不是,我们还要考虑到后面这几点。
1.操作系统内核开发人员,不可能罗列世界上所有的设备,并为其写一套控制代码。
2.为了商业目的,有很多设备厂商并不愿意公开设备的编程细节。就算内核开发人员想为其写控制代码,实际也不可行。
3.如果设备更新换代,就要重写设备的控制代码,然后重新编译操作系统内核,这样的话操作很麻烦,操作系统内核开发人员和用户都可能受不了。
以上三点,足于证明这种方案根本不可取。
既然操作系统内核无法包含所有的设备控制代码,那就索性不包含,或者只包含最基本、最通用的设备控制代码。这样操作系统内核就可以非常通用,非常精巧。
但是要控制设备就必须要有设备的相关控制代码才行,所以我们要把设备控制代码独立出来,与操作系统内核分开、独立开发,设备控制代码可由设备厂商人员开发。
每个设备对应一个设备控制代码模块,操作系统内核要控制哪个设备,就加载相应的设备代码模块,以后不使用这个设备了,就可以删除对应的设备控制代码模块。
这种方式,给操作系统内核带来了**巨大的灵活性**。设备厂商在发布新设备时,只要随之发布一个与此相关的设备控制代码模块就行了。
### 设备分类
要想管理设备,先要对其分门别类,在开始分类之前,你不妨先思考一个问题:操作系统内核所感知的设备,一定要与物理设备一一对应吗?
举个例子储存设备其实不管它是机械硬盘还是TF卡或者是一个设备控制代码模块它向操作系统内核表明它是储存设备但它完全有可能分配一块内存空间来储存数据不必访问真正的储存设备。**所以,操作系统内核所感知的设备,并不需要和物理设备对应,这取决于设备控制代码自身的行为。**
操作系统内核所定义的设备可称为内核设备或者逻辑设备其实这只是对物理计算平台中几种类型设备的一种抽象。下面我们在cosmos/include/knlinc/krldevice\_t.h文件中对设备进行分类定义代码如下。
```
#define NOT_DEVICE 0 //不表示任何设备
#define BRIDGE_DEVICE 4 //总线桥接器设备
#define CPUCORE_DEVICE 5 //CPU设备CPU也是设备
#define RAMCONTER_DEVICE 6 //内存控制器设备
#define RAM_DEVICE 7 //内存设备
#define USBHOSTCONTER_DEVICE 8 //USB主控制设备
#define INTUPTCONTER_DEVICE 9 //中断控制器设备
#define DMA_DEVICE 10 //DMA设备
#define CLOCKPOWER_DEVICE 11 //时钟电源设备
#define LCDCONTER_DEVICE 12 //LCD控制器设备
#define NANDFLASH_DEVICE 13 //nandflash设备
#define CAMERA_DEVICE 14 //摄像头设备
#define UART_DEVICE 15 //串口设备
#define TIMER_DEVICE 16 //定时器设备
#define USB_DEVICE 17 //USB设备
#define WATCHDOG_DEVICE 18 //看门狗设备
#define RTC_DEVICE 22 //实时时钟设备
#define SD_DEVICE 25 //SD卡设备
#define AUDIO_DEVICE 26 //音频设备
#define TOUCH_DEVICE 27 //触控设备
#define NETWORK_DEVICE 28 //网络设备
#define VIR_DEVICE 29 //虚拟设备
#define FILESYS_DEVICE 30 //文件系统设备
#define SYSTICK_DEVICE 31 //系统TICK设备
#define UNKNOWN_DEVICE 32 //未知设备,也是设备
#define HD_DEVICE 33 //硬盘设备
```
上面定义的这些类型的设备都是Cosmos内核抽象出来的逻辑设备例如NETWORK\_DEVICE网络设备不管它是有线网卡还是无线网卡或者是设备控制代码虚拟出来的虚拟网卡。Cosmos内核都将认为它是一个网络设备这就是设备的抽象这样有利于我们灵活、简便管理设备。
## 设备驱动
刚才我们解决了设备分类,下面我来研究如何实现分权而治,就是把操作每个设备的相关代码独立出来,这种方式在业界有一个更专业的名字——**设备驱动程序**。同时在下面的内容中,我们将不区分设备驱动程序和驱动程序。
这种“分权而治”的方式,给操作系统内核带了灵活性、可扩展性……可是也带来了新的问题,有哪些问题呢?
首先是操作系统内核如何表示多个设备与驱动的存在?然后,还有如何组织多个设备和多个驱动程序的问题,最后我们还得考虑应该让驱动程序提供一些什么支持。下面我们分别解决这些问题。
### 设备
你能说说一个设备包含哪些信息吗无非是设备类型设备名称设备状态设备id设备的驱动程序等。
我们把这些信息归纳成一个数据结构,在操作系统内核建立这个数据结构的实例变量,这个设备数据结构的实例变量,一旦建立,就表示操作系统内核中存在一个逻辑设备了。
我们接下来就一起整理一下设备的信息,然后把它们变成一个数据结构,代码如下。
```
typedef struct s_DEVID
{
uint_t dev_mtype;//设备类型号
uint_t dev_stype; //设备子类型号
uint_t dev_nr; //设备序号
}devid_t;
typedef struct s_DEVICE
{
list_h_t dev_list;//设备链表
list_h_t dev_indrvlst; //设备在驱动程序数据结构中对应的挂载链表
list_h_t dev_intbllst; //设备在设备表数据结构中对应的挂载链表
spinlock_t dev_lock; //设备自旋锁
uint_t dev_count; //设备计数
sem_t dev_sem; //设备信号量
uint_t dev_stus; //设备状态
uint_t dev_flgs; //设备标志
devid_t dev_id; //设备ID
uint_t dev_intlnenr; //设备中断服务例程的个数
list_h_t dev_intserlst; //设备中断服务例程的链表
list_h_t dev_rqlist; //对设备的请求服务链表
uint_t dev_rqlnr; //对设备的请求服务个数
sem_t dev_waitints; //用于等待设备的信号量
struct s_DRIVER* dev_drv; //设备对应的驱动程序数据结构的指针
void* dev_attrb; //设备属性指针
void* dev_privdata; //设备私有数据指针
void* dev_userdata;//将来扩展所用
void* dev_extdata;//将来扩展所用
char_t* dev_name; //设备名
}device_t;
```
设备的信息比较多,大多是用于组织设备的。这里的**设备ID结构十分重要**,它表示设备的类型、设备号,子设备号是为了解决多个相同设备的,还有一个指向设备驱动程序的指针,这是用于访问设备时调用设备驱动程序的,只要有人建立了一个设备结构的实例变量,内核就能感知到一个设备存在了。
至于是谁建立了设备结构的实例变量,这个问题我们接着探索。
### 驱动
操作系统内核和应用程序都不会主动建立设备,那么谁来建立设备呢?当然是控制设备的代码,也就是我们常说的**驱动程序**。
那么驱动程序如何表示呢,换句话说,操作系统内核是如何感知到一个驱动程序的存在呢?
根据前面的经验我们还是要定义一个数据结构来表示一个驱动程序数据结构中应该包含驱动程序名驱动程序ID驱动程序所管理的设备最重要的是**完成功能设备相关功能的函数**,下面我们来定义它,代码如下。
```
typedef struct s_DRIVER
{
spinlock_t drv_lock; //保护驱动程序数据结构的自旋锁
list_h_t drv_list;//挂载驱动程序数据结构的链表
uint_t drv_stuts; //驱动程序的相关状态
uint_t drv_flg; //驱动程序的相关标志
uint_t drv_id; //驱动程序ID
uint_t drv_count; //驱动程序的计数器
sem_t drv_sem; //驱动程序的信号量
void* drv_safedsc; //驱动程序的安全体
void* drv_attrb; //LMOSEM内核要求的驱动程序属性体
void* drv_privdata; //驱动程序私有数据的指针
drivcallfun_t drv_dipfun[IOIF_CODE_MAX]; //驱动程序功能派发函数指针数组
list_h_t drv_alldevlist; //挂载驱动程序所管理的所有设备的链表
drventyexit_t drv_entry; //驱动程序的入口函数指针
drventyexit_t drv_exit; //驱动程序的退出函数指针
void* drv_userdata;//用于将来扩展
void* drv_extdata; //用于将来扩展
char_t* drv_name; //驱动程序的名字
}driver_t;
```
上述代码你应该很容易看懂。Cosmos内核每加载一个驱动程序模块就会自动分配一个驱动程序数据结构并且将其实例化。
而Cosmos内核在首次启动驱动程序时就会调用这个驱动程序的入口点函数在这个函数中驱动程序会分配一个设备数据结构并用相关的信息将其实例化比如填写正确的设备类型、设备ID号、设备名称等。
Cosmos内核负责建立驱动数据结构而驱动程序又建立了设备数据结构这一来二去就形成了一个驱动程序与Cosmos内核**“握手”**的动作。
### 设备驱动的组织
有了设备、驱动,我们下面探索一下怎么合理的组织好它们。
组织它们要解决的问题,就是在哪里安放驱动。然后我们还要想好怎么找到它们,下面我们用一个叫做**设备表**的数据结构,来组织这些驱动程序数据结构和设备数据结构。
这个结构我已经帮你定义好了,如下所示。
```
#define DEVICE_MAX 34
typedef struct s_DEVTLST
{
uint_t dtl_type;//设备类型
uint_t dtl_nr;//设备计数
list_h_t dtl_list;//挂载设备device_t结构的链表
}devtlst_t;
typedef struct s_DEVTABLE
{
list_h_t devt_list; //设备表自身的链表
spinlock_t devt_lock; //设备表自旋锁
list_h_t devt_devlist; //全局设备链表
list_h_t devt_drvlist; //全局驱动程序链表,驱动程序不需要分类,一个链表就行
uint_t devt_devnr; //全局设备计数
uint_t devt_drvnr; //全局驱动程序计数
devtlst_t devt_devclsl[DEVICE_MAX]; //分类存放设备数据结构的devtlst_t结构数组
}devtable_t;
```
在这段代码的devtable\_t结构中devtlst\_t是每个设备类型一个表示一类设备但每一类可能有多个设备所以在devtlst\_t结构中有一个设备计数和设备链表。而你可能想到Cosmos中肯定要定义一个devtable\_t结构的全局变量代码如下。
```
//在 cosmos/kernel/krlglobal.c文件中
KRL_DEFGLOB_VARIABLE(devtable_t,osdevtable);
//在 cosmos/kernel/krldevice.c文件中
void devtlst_t_init(devtlst_t *initp, uint_t dtype)
{
initp->dtl_type = dtype;//设置设备类型 initp->dtl_nr = 0;
list_init(&initp->dtl_list);
return;
}
void devtable_t_init(devtable_t *initp)
{
list_init(&initp->devt_list);
krlspinlock_init(&initp->devt_lock);
list_init(&initp->devt_devlist);
list_init(&initp->devt_drvlist);
initp->devt_devnr = 0;
initp->devt_drvnr = 0;
for (uint_t t = 0; t < DEVICE_MAX; t++)
{//初始化设备链表
devtlst_t_init(&initp->devt_devclsl[t], t);
}
return;
}
void init_krldevice()
{
devtable_t_init(&osdevtable);//初始化系统全局设备表
return;
}
//在 cosmos/kernel/krlinit.c文件中
void init_krl()
{
init_krlmm();
init_krldevice();
//记住一定要在初始化调度器之前,初始化设备表
init_krlsched();
init_krlcpuidle();
return;
}
```
上面的设备表的初始化代码已经写好了,如果你大脑中没有设备驱动组织图,可能脑子里还是有点乱,所以我来帮你画一幅图,如下所示。
![](https://static001.geekbang.org/resource/image/17/9f/17d232bd17b0c328yyed37bab98baa9f.jpg?wh=5008x4580 "设备表结构示意图")
上图看似复杂实则简单我帮你理一下重点首先devtable\_t结构中能找到所有的设备和驱动然后从设备能找到对应的驱动从驱动也能找到其管理的所有设备 ,最后就能实现一个驱动管理多个设备。
### 驱动程序功能
我们还有一个问题需要解决,那就是驱动程序,究竟要为操作系统内核提供哪些最基本的功能支持?
我们已经知道了,写驱动程序就是为了操控相应的设备,所以这得看大多数设备能完成什么功能了。现代计算机的设备无非就是可以输入数据、处理数据、输出数据,然后完成一些特殊的功能。
当然,现代计算机的设备很多,能耗是个严重的问题,所以操作系统内核应该能控制设备能耗。下面我来帮你归纳一下用来驱动程序的几种主要函数,如下。
```
//驱动程序入口和退出函数
drvstus_t device_entry(driver_t* drvp,uint_t val,void* p);
drvstus_t device_exit(driver_t* drvp,uint_t val,void* p);
//设备中断处理函数
drvstus_t device_handle(uint_t ift_nr,void* devp,void* sframe);
//打开、关闭设备函数
drvstus_t device_open(device_t* devp,void* iopack);
drvstus_t device_close(device_t* devp,void* iopack);
//读、写设备数据函数
drvstus_t device_read(device_t* devp,void* iopack);
drvstus_t device_write(device_t* devp,void* iopack);
//调整读写设备数据位置函数
drvstus_t device_lseek(device_t* devp,void* iopack);
//控制设备函数
drvstus_t device_ioctrl(device_t* devp,void* iopack);
//开启、停止设备函数
drvstus_t device_dev_start(device_t* devp,void* iopack);
drvstus_t device_dev_stop(device_t* devp,void* iopack);
//设置设备电源函数
drvstus_t device_set_powerstus(device_t* devp,void* iopack);
//枚举设备函数
drvstus_t device_enum_dev(device_t* devp,void* iopack);
//刷新设备缓存函数
drvstus_t device_flush(device_t* devp,void* iopack);
//设备关机函数
drvstus_t device_shutdown(device_t* devp,void* iopack);
```
如上所述,我们可以把每一个操作定义成一个函数,让驱动程序实现这些函数。函数名你可以随便写,但是函数的形式却不能改变,这是操作系统内核与驱动程序沟通的桥梁。当然有很多设备本身并不支持这么多操作,例如时钟设备,驱动程序就不必实现相应的操作。
那么这些函数如何和操作系统内核关联起来呢还记得driver\_t结构中那个函数指针数组吗如下所示。
```
#define IOIF_CODE_OPEN 0 //对应于open操作
#define IOIF_CODE_CLOSE 1 //对应于close操作
#define IOIF_CODE_READ 2 //对应于read操作
#define IOIF_CODE_WRITE 3 //对应于write操作
#define IOIF_CODE_LSEEK 4 //对应于lseek操作
#define IOIF_CODE_IOCTRL 5 //对应于ioctrl操作
#define IOIF_CODE_DEV_START 6 //对应于start操作
#define IOIF_CODE_DEV_STOP 7 //对应于stop操作
#define IOIF_CODE_SET_POWERSTUS 8 //对应于powerstus操作
#define IOIF_CODE_ENUM_DEV 9 //对应于enum操作
#define IOIF_CODE_FLUSH 10 //对应于flush操作
#define IOIF_CODE_SHUTDOWN 11 //对应于shutdown操作
#define IOIF_CODE_MAX 12 //最大功能码
//驱动程序分派函数指针类型
typedef drvstus_t (*drivcallfun_t)(device_t*,void*);
//驱动程序入口、退出函数指针类型
typedef drvstus_t (*drventyexit_t)(struct s_DRIVER*,uint_t,void*);
typedef struct s_DRIVER
{
//……
drivcallfun_t drv_dipfun[IOIF_CODE_MAX];//驱动程序分派函数指针数组。
list_h_t drv_alldevlist;//驱动所管理的所有设备。
drventyexit_t drv_entry;
drventyexit_t drv_exit;
//……
}driver_t;
```
看到这里你是不是明白了driver\_t结构中的drv\_dipfun函数指针数组正是存放上述那12个驱动程序函数的指针。这样操作系统内核就能通过driver\_t结构调用到对应的驱动程序函数操作对应的设备了。
## 重点回顾
现在,我们搞明白了一个典型计算机的结构,里面有很多设备,需要操作系统合理地管理,而操作系统通过加载驱动程序来管理和使用设备,并为此提供了一系列的机制,这也是我们这节课的重点。
1.计算机结构,我们通过了解一个典型的计算机系统结构,明白了设备的多样性。然后我们对设备做了抽象分类,采用分权而治的方式,让操作系统通过驱动程序来管理设备,同时又能保证操作系统和驱动程序分离,达到操作系统和设备解耦的目的。
2.归纳整理设备和设备驱动的信息,抽象两个对应的数据结构,这两个数据结构在内存中的实例变量就代表一个设备和对应的驱动。然后,我们通过设备表结构组织了驱动和设备的数据结构。
3.驱动程序最主要的工作是要操控设备但这些个操作设备的动作是操作系统调用的所以对驱动定义了必须要支持的12种标准方法并对应到函数这些函数的地址保存在驱动程序的数据结构中。
你可能在想,我们驱动程序是怎么加载的,设备又是怎么建立的呢?这是正是我们后面课程要解决的。不过你可以先开动脑筋,思考一下,提出你自己的见解,考虑一下这个问题的解决方案。
## 思考题
请你写出一个用来访问设备的接口函数,或者想一下访问一个设备需要什么参数。
欢迎你在留言区跟我交流互动,积极输出有助于更高效地理解这节课的内容。也欢迎你把这节课分享给同事、朋友。
我是LMOS。我们下节课见