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.

457 lines
23 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.

# 30 | 部门响应设备如何处理内核I/O包
你好我是LMOS。
在上一课中,我们实现了建立设备的接口,这相当于制定了部门的相关法规,只要遵守这些法规就能建立一个部门。当然,建立了一个部门,是为了干活的,吃空饷可不行。
其实一个部门的职责不难确定它应该能对上级下发的任务作出响应并完成相关工作而这对应到设备就是如何处理内核的I/O包这节课我们就来解决这个问题。
首先我们需要搞清楚什么是I/O包然后实现内核向设备发送I/O包的工作。最后我还会带你一起来完成一个驱动实例用于处理I/O包这样你就能真正理解这里的来龙去脉了。
好,让我们开始今天的学习吧!代码你可以从[这里](https://gitee.com/lmos/cosmos/tree/master/lesson30/Cosmos)下载。
## 什么是I/O包
就像你要给部门下达任务时,需要准备材料报表之类的东西。同样,内核要求设备做什么事情,完成什么功能,必须要告诉设备的驱动程序。
内核要求设备完成任务,无非是调用设备的驱动程序函数,把完成任务的细节用参数的形式传递给设备的驱动程序。
由于参数很多而且各种操作所需的参数又不相同所以我们就想到了更高效的管理方法也就是把各种操作所需的各种参数封装在一个数据结构中称为I/O包这样就可以统一驱动程序功能函数的形式了。
思路理清以后,现在我们来设计这个数据结构,如下所示。
```
typedef struct s_OBJNODE
{
spinlock_t on_lock; //自旋锁
list_h_t on_list; //链表
sem_t on_complesem; //完成信号量
uint_t on_flgs; //标志
uint_t on_stus; //状态
sint_t on_opercode; //操作码
uint_t on_objtype; //对象类型
void* on_objadr; //对象地址
uint_t on_acsflgs; //访问设备、文件标志
uint_t on_acsstus; //访问设备、文件状态
uint_t on_currops; //对应于读写数据的当前位置
uint_t on_len; //对应于读写数据的长度
uint_t on_ioctrd; //IO控制码
buf_t on_buf; //对应于读写数据的缓冲区
uint_t on_bufcurops; //对应于读写数据的缓冲区的当前位置
size_t on_bufsz; //对应于读写数据的缓冲区的大小
uint_t on_count; //对应于对象节点的计数
void* on_safedsc; //对应于对象节点的安全描述符
void* on_fname; //对应于访问数据文件的名称
void* on_finode; //对应于访问数据文件的结点
void* on_extp; //用于扩展
}objnode_t;
```
现在你可能还无法从objnode\_t这个名字看出它跟I/O包的关系。但你从刚才的代码里可以看出objnode\_t的数据结构中包括了各个驱动程序功能函数的所有参数。
等我们后面讲到API接口时你会发现objnode\_t结构不单是完成了I/O包传递参数的功能它在整个I/O生命周期中都起着重要的作用。这里为了好理解我们就暂且把objnode\_t结构当作I/O包来看。
### 创建和删除I/O包
刚才我们已经定义了I/O包也就是objnode\_t结构但若是要使用它就必须先把它建立好。
根据以往的经验你应该已经猜到了这里创建I/O包就是在内存中建立objnode\_t结构的实例变量并初始化它。由于这是一个全新的模块所以我们要先在cosmos/kernel/目录下建立一个新的krlobjnode.c文件在这个文件中写代码如下所示。
```
//建立objnode_t结构
objnode_t *krlnew_objnode()
{
objnode_t *ondp = (objnode_t *)krlnew((size_t)sizeof(objnode_t));//分配objnode_t结构的内存空间
if (ondp == NULL)
{
return NULL;
}
objnode_t_init(ondp);//初始化objnode_t结构
return ondp;
}
//删除objnode_t结构
bool_t krldel_objnode(objnode_t *onodep)
{
if (krldelete((adr_t)onodep, (size_t)sizeof(objnode_t)) == FALSE)//删除objnode_t结构的内存空间
{
hal_sysdie("krldel_objnode err");
return FALSE;
}
return TRUE;
}
```
上述代码非常简单主要完成了建立、删除objnode\_t结构这两件事其实说白了就是分配和释放objnode\_t结构的内存空间。
这里再一次体现了**内存管理组件在操作系统内核之中的重要性**objnode\_t\_init函数会初始化objnode\_t结构中的字段因为其中有自旋锁、链表、信号量而这些结构并不能简单地初始为0否则可以直接使用memset之类的函数把那个内存空间清零就行了。
## 向设备发送I/O包
现在我们假定在上层接口函数中已经建立了一个I/O包即objnode\_t结构并且把操作码、操作对象和相关的参数信息填写到了objnode\_t结构之中。那么下一步就需要把这个I/O发送给具体设备的驱动程序以便驱动程序完成具体工作。
我们需要定义实现一个函数,专门用于完成这个功能,它标志着一个设备驱动程序开始运行,经它之后内核就实际的控制权交给驱动程序,由驱动程序代表内核操控设备。
下面我们就来写好这个函数不过这个函数属于驱动模型函数所以要在krldevice.c文件中实现这个函数。代码如下所示。
```
//发送设备IO
drvstus_t krldev_io(objnode_t *nodep)
{
//获取设备对象
device_t *devp = (device_t *)(nodep->on_objadr);
if ((nodep->on_objtype != OBJN_TY_DEV && nodep->on_objtype != OBJN_TY_FIL) || nodep->on_objadr == NULL)
{//检查操作对象类型是不是文件或者设备,对象地址是不是为空
return DFCERRSTUS;
}
if (nodep->on_opercode < 0 || nodep->on_opercode >= IOIF_CODE_MAX)
{//检查IO操作码是不是合乎要求
return DFCERRSTUS;
}
return krldev_call_driver(devp, nodep->on_opercode, 0, 0, NULL, nodep);//调用设备驱动
}
//调用设备驱动
drvstus_t krldev_call_driver(device_t *devp, uint_t iocode, uint_t val1, uint_t val2, void *p1, void *p2)
{
driver_t *drvp = NULL;
if (devp == NULL || iocode >= IOIF_CODE_MAX)
{//检查设备和IO操作码
return DFCERRSTUS;
}
drvp = devp->dev_drv;
if (drvp == NULL)//检查设备是否有驱动程序
{
return DFCERRSTUS;
}
//用IO操作码为索引调用驱动程序功能分派函数数组中的函数
return drvp->drv_dipfun[iocode](devp, p2);
}
```
krldev\_io函数只接受一个参数也就是objnode\_t结构的指针。它会首先检查objnode\_t结构中的IO操作码是不是合乎要求的还要检查被操作的对象即设备是不是为空然后调用krldev\_call\_driver函数。
这个krldev\_call\_driver函数会再次确认传递进来的设备和IO操作码然后重点检查设备有没有驱动程序。这一切检查通过之后我们就用IO操作码为索引调用驱动程序功能分派函数数组中的函数并把设备和objnode\_t结构传递进去。有没有觉得眼熟没错这正是我们[前面课程](https://time.geekbang.org/column/article/394875)中对驱动程序的设计。
好了现在一个设备的驱动程序就能正式开始工作开始响应处理内核发来的I/O包了。可是我们还没有驱动呢所以下面我们就去实现一个驱动程序。
## 驱动程序实例
现在我们一起来实现一个真实而且简单的设备驱动程序就是systick设备驱动它是我们Cosmos系统的心跳systick设备的主要功能和作用是每隔 1ms产生一个中断相当于一个定时器每次时间到达就产生一个中断向系统报告又过了1ms相当于千分之一秒即每秒钟内产生1000次中断。
对于现代CPU的速度来说这个中断频率不算太快。x86平台上有没有这样的定时器呢当然有其中8254就是一个古老且常用的定时器对它进行编程设定它就可以周期的产生定时器中断。
这里我们就以8254定时器为基础实现Cosmos系统的systick设备。我们先从systick设备驱动程序的整体框架入手然后建立systick设备最后一步一步实现systick设备驱动程序。
### systick设备驱动程序的整体框架
在前面的课程中我们已经了解了在Cosmos系统下一个设备驱动程序的基本框架但是我们没有深入具体化。
所以这里我会带你从全局好好了解一个真实的设备它的驱动程序应该至少有哪些函数。由于这是个驱动程序我们需要在cosmos/drivers/目录下建立一个drvtick.c文件在drvtick.c文件中写入以下代码如下所示。
```
//驱动程序入口和退出函数
drvstus_t systick_entry(driver_t *drvp, uint_t val, void *p)
{
return DFCERRSTUS;
}
drvstus_t systick_exit(driver_t *drvp, uint_t val, void *p)
{
return DFCERRSTUS;
}
//设备中断处理函数
drvstus_t systick_handle(uint_t ift_nr, void *devp, void *sframe)
{
return DFCEERSTUS;
}
//打开、关闭设备函数
drvstus_t systick_open(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
drvstus_t systick_close(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//读、写设备数据函数
drvstus_t systick_read(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
drvstus_t systick_write(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//调整读写设备数据位置函数
drvstus_t systick_lseek(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//控制设备函数
drvstus_t systick_ioctrl(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//开启、停止设备函数
drvstus_t systick_dev_start(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
drvstus_t systick_dev_stop(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//设置设备电源函数
drvstus_t systick_set_powerstus(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//枚举设备函数
drvstus_t systick_enum_dev(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//刷新设备缓存函数
drvstus_t systick_flush(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
//设备关机函数
drvstus_t systick_shutdown(device_t *devp, void *iopack)
{
return DFCERRSTUS;
}
```
以上就是一个驱动程序必不可少的函数,**在各个函数可以返回一个错误状态,而不做任何实际工作,但是必须要有这个函数。**这样在内核发来任何设备功能请求时,驱动程序才能给予适当的响应。这样,一个驱动程序的整体框架就确定了。
写好了驱动程序的整体框架,我们这个驱动就完成了一半。下面我们来一步一步来实现它。
### systick设备驱动程序的入口
我们先来写好systick设备驱动程序的入口函数。那这个函数用来做什么呢其实我们在上一节课就详细讨论过无非是建立设备向内核注册设备安装中断回调函数等操作所以这里不再赘述。
我们直接写出这个函数,如下所示。
```
drvstus_t systick_entry(driver_t* drvp,uint_t val,void* p)
{
if(drvp==NULL) //drvp是内核传递进来的参数不能为NULL
{
return DFCERRSTUS;
}
device_t* devp=new_device_dsc();//建立设备描述符结构的变量实例
if(devp==NULL)//不能失败
{
return DFCERRSTUS;
}
systick_set_driver(drvp);
systick_set_device(devp,drvp);//驱动程序的功能函数设置到driver_t结构中的drv_dipfun数组中
if(krldev_add_driver(devp,drvp)==DFCERRSTUS)//将设备挂载到驱动中
{
if(del_device_dsc(devp)==DFCERRSTUS)//注意释放资源。
{
return DFCERRSTUS;
}
return DFCERRSTUS;
}
if(krlnew_device(devp)==DFCERRSTUS)//向内核注册设备
{
if(del_device_dsc(devp)==DFCERRSTUS)//注意释放资源
{
return DFCERRSTUS;
}
return DFCERRSTUS;
}
//安装中断回调函数systick_handle
if(krlnew_devhandle(devp,systick_handle,20)==DFCERRSTUS)
{
return DFCERRSTUS; //注意释放资源。
}
init_8254();//初始化物理设备
if(krlenable_intline(0x20)==DFCERRSTUS)
{
return DFCERRSTUS;
}
return DFCOKSTUS;
}
```
你可能非常熟悉这部分代码,没错,这正是上节课中,我们的那个驱动程序入口函数的实例。
不过在上节课里,我们主要是要展示一个驱动程序入口函数的流程。这里却是要投入工作的真实设备驱动。
最后的**krlenable\_intline函数**它的主要功能是开启一个中断源上的中断。而init\_8254函数则是为了初始化8254它就是一个古老且常用的定时器。这两个函数非常简单我已经帮写好了。
但是这样还不够,有了驱动程序入口函数,驱动程序并不会自动运行。根据前面我们的设计,需要把这个驱动程序入口函数放入驱动表中。
下面我们就把这个systick\_entry函数放到驱动表里代码如下所示。
```
//cosmos/kernel/krlglobal.c
KRL_DEFGLOB_VARIABLE(drventyexit_t,osdrvetytabl)[]={systick_entry,NULL};
```
有了刚才这步操作之后Cosmos在启动的时候就会执行初始驱动初始化init\_krldriver函数接着这个函数就会启动运行systick设备驱动程序入口函数。我们的systick\_entry函数一旦执行就会建立systick设备不断的产生时钟中断。
### 配置设备和驱动
在驱动程序入口函数中,除了那些标准的流程之外,我们还要对设备和驱动进行适当的配置,就是设置一些标志、状态、名称、驱动功能派发函数等等。有了这些信息,设备才能加入到驱动程序中,然后注册到内核,这样才能被内核所识别。
好,让我们先来实现设置驱动程序的函数,它主要设置设备驱动程序的名称、功能派发函数,代码如下。
```
void systick_set_driver(driver_t *drvp)
{
//设置驱动程序功能派发函数
drvp->drv_dipfun[IOIF_CODE_OPEN] = systick_open;
drvp->drv_dipfun[IOIF_CODE_CLOSE] = systick_close;
drvp->drv_dipfun[IOIF_CODE_READ] = systick_read;
drvp->drv_dipfun[IOIF_CODE_WRITE] = systick_write;
drvp->drv_dipfun[IOIF_CODE_LSEEK] = systick_lseek;
drvp->drv_dipfun[IOIF_CODE_IOCTRL] = systick_ioctrl;
drvp->drv_dipfun[IOIF_CODE_DEV_START] = systick_dev_start;
drvp->drv_dipfun[IOIF_CODE_DEV_STOP] = systick_dev_stop;
drvp->drv_dipfun[IOIF_CODE_SET_POWERSTUS] = systick_set_powerstus;
drvp->drv_dipfun[IOIF_CODE_ENUM_DEV] = systick_enum_dev;
drvp->drv_dipfun[IOIF_CODE_FLUSH] = systick_flush;
drvp->drv_dipfun[IOIF_CODE_SHUTDOWN] = systick_shutdown;
drvp->drv_name = "systick0drv";//设置驱动程序名称
return;
}
```
上述代码的功能并不复杂我一说你就能领会。systick\_set\_driver函数无非就是将12个驱动功能函数的地址分别设置到driver\_t结构的drv\_dipfun数组中。其中驱动功能函数在该数组中的元素位置正好与IO操作码一一对应当内核用IO操作码调用驱动时就是调用了这个数据中的函数。最后我们将驱动程序的名称设置为systick0drv。
新建的设备也需要配置相关的信息才能工作,比如需要指定设备,设备状态与标志,设备类型、设备名称这些信息。尤其要注意的是,设备类型非常重要,内核正是通过类型来区分各种设备的,下面我们写个函数,完成这些功能,代码如下所示。
```
void systick_set_device(device_t *devp, driver_t *drvp)
{
devp->dev_flgs = DEVFLG_SHARE;//设备可共享访问
devp->dev_stus = DEVSTS_NORML;//设备正常状态
devp->dev_id.dev_mtype = SYSTICK_DEVICE;//设备主类型
devp->dev_id.dev_stype = 0;//设备子类型
devp->dev_id.dev_nr = 0; //设备号
devp->dev_name = "systick0";//设置设备名称
return;
}
```
上述代码中systick\_set\_device函数需要两个参数但是第二个参数暂时没起作用而第一个参数其实是一个device\_t结构的指针在systick\_entry函数中调用new\_device\_dsc函数的时候就会返回这个指针。后面我们会把设备加载到内核中那时这个指针指向的设备才会被注册。
### 打开与关闭设备
其实对于systick这样设备主要功能是定时中断还不能支持读、写、控制、刷新、电源相关的功能就算内核对systick设备发起了这样的I/O包systick设备驱动程序相关的功能函数也只能返回一个错误码表示不支持这样的功能请求。
但是,打开与关闭设备这样的功能还是应该要实现。下面我们就来实现这两个功能请求函数,代码如下所示。
```
//打开设备
drvstus_t systick_open(device_t *devp, void *iopack)
{
krldev_inc_devcount(devp);//增加设备计数
return DFCOKSTUS;//返回成功完成的状态
}
//关闭设备
drvstus_t systick_close(device_t *devp, void *iopack)
{
krldev_dec_devcount(devp);//减少设备计数
return DFCOKSTUS;//返回成功完成的状态
}
```
这样打开与关闭设备的功能就实现了只是简单地增加与减少设备的引用计数然后返回成功完成的状态就行了。而增加与减少设备的引用计数是为了统计有多少个进程打开了这个设备当设备引用计数为0时就说明没有进程使用该设备。
### systick设备中断回调函数
对于systick设备来说重要的并不是打开、关闭读写等操作而是systick设备产生的中断以及在中断回调函数中执行的操作即周期性的执行系统中的某些动作比如更新系统时间比如控制一个进程占用CPU的运行时间等这些操作都需要在systick设备中断回调函数中执行。
按照前面的设计systick设备每秒钟产生1000次中断那么1秒钟就会调用1000次这个中断回调函数这里我们只要写出这个函数就行了因为安装中断回调函数的思路我们在前面的课程中已经说过了可以回顾上节课现在我们直接实现这个中断函数代码可以像后面这样写。
```
drvstus_t systick_handle(uint_t ift_nr, void *devp, void *sframe)
{
kprint("systick_handle run devname:%s intptnr:%d\n", ((device_t *)devp)->dev_name, ift_nr);
return DFCOKSTUS;
}
```
这个中断回调函数,暂时什么也没干,就输出一条信息,让我们知道它运行了,为了直观观察它运行了,我们要对内核层初始化函数修改一下,禁止进程运行,以免进程输出的信息打扰我们观察结果,修改的代码如下所示。
```
void init_krl()
{
init_krlmm();
init_krldevice();//初始化设备
init_krldriver();//初始化驱动程序
init_krlsched();
//init_krlcpuidle();禁止进程运行
STI();//打开CPU响应中断的能力
die(0);//进入死循环
return;
}
```
下面我们打开终端切到Cosmos目录下执行make vboxtest指令如果不出意外我们将会中看到如下界面。
![](https://static001.geekbang.org/resource/image/84/e9/84c7837a89eb56b2863c8b30eb1217e9.jpg?wh=1044x921 "测试中断回调函数")
上图中的信息,会不断地滚动出现,信息中包含设备名称和中断号,这标志着我们中断回调函数的运行正确无误。
当然,如果我们费了这么功夫搞了中断回调函数,就只是为了输出信息,那也太不划算了,我们当然有更重要的事情要做,你还记得之前讲过的进程知识吗?这里我再帮你理一理思路。
我们在每个进程中都要主动调用进程调度器函数否则进程就会永远霸占CPU永远运行下去。这是因为我们没有定时器可以周期性地检查进程运行了多长时间如果进程的运行时间超过了就应该强制调度让别的进程开始运行。
更新进程运行时间的代码,我已经帮你写好了,你只需要在这个中断回调函数中调用就好了,代码如下所示。
```
drvstus_t systick_handle(uint_t ift_nr, void *devp, void *sframe)
{
krlthd_inc_tick(krlsched_retn_currthread());//更新当前进程的tick
return DFCOKSTUS;
}
```
这里的krlthd\_inc\_tick函数需要一个进程指针的参数而krlsched\_retn\_currthread函数是返回当前正在运行进程的指针。在krlthd\_inc\_tick函数中对进程的tick值加1如果大于20也就是20 毫秒就重新置0并进行调度。
下面我们把内核层初始化函数恢复到原样重新打开终端切到cosmos目录下执行make vboxtest指令我们就将会看到如下界面。
![](https://static001.geekbang.org/resource/image/75/47/75647910ba0529e6c80ba127bbe9c247.jpg?wh=1044x921 "测试进程运行时间更新")
我们可以看到进程A、进程B还有调度器交替输出的信息。这已经证明我们更新进程运行时间检查其时间是否用完并进行调度的代码逻辑都是完全正确的恭喜你走到了这一步
至此我们的systick驱动程序就实现了它非常简单但却包含了一个驱动程序完整实现。同时这个过程也一步步验证了我们对驱动模型的设计是正确的。
## 重点回顾
又到课程的结尾到此为止我们了解了实现一个驱动程序完整过程虽然我们只是驱动了一个定时器设备使之周期性的产生定时中断。在定时器设备的中断回调函数中我们调用了更新进程时间的函数达到了这样的目的在进程运行超时的情况下内核有能力夺回CPU调度别的进程运行。
现在我来为你梳理一下重点。
1.为了搞清楚设备如何处理I/O包我们了解了什么是I/O包写好了处理建立、删除I/O包的代码。
2.要使设备完成相应的功能内核就必须向设备驱动发送相应的I/O包在I/O包提供相应IO操作码和适当的参数。所以我们动手实现了向设备发送I/O包并调用设备驱动程序的机制。
3.一切准备就绪之后我们建立了systick驱动程序实例这是一个完整的驱动程序它支持打开关闭和周期性产生中断的功能请求。通过这个实例让我们了解了一个真实设备驱动的实现以及它处理内核I/O包的过程。
你可能对这样简单的驱动程序不够满意,也不能肯定我们的驱动模型是不是能适应大多数场景,请不要着急,在后面讲到文件系统时,我们会实现一个更为复杂的驱动程序。
## 思考题
请你想一想为什么没有systick设备这样周期性的产生中断进程就有可能霸占CPU呢
欢迎你在留言区跟我交流互动,也欢迎你把这节课分享给身边的同事、朋友,一起实践驱动程序的实例。
我是LMOS我们下节课见