I/O设备管理
简单来说就是驱动程序开发,xbook2的驱动框架来源于windowsNT内核,它是以driver object为一个驱动程序,以device object为一个驱动程序上的设备。典型的案例就是,一个tty驱动程序,对应着多个tty设备,tty0,tty1,tty2...tty7。本章将讲解如何在xbook2中开发驱动程序,这应该是操作系统内核中比较开放的一个模块了。
驱动框架
一、重要结构体介绍
在xbook2种,每个驱动都需要有一个驱动对象,来存放这个驱动的相关信息,如驱动的入口,功能函数等。
/* 驱动对象 */
typedef struct _driver_object
{
unsigned int flags; /* 驱动标志 */
list_t list; /* 驱动程序构成一个链表 */
list_t device_list; /* 驱动下的设备构成的链表 */
struct drver_extension *drver_extension; /* 驱动扩展 */
string_t name; /* 名字 */
/* 驱动控制函数 */
driver_func_t driver_enter;
driver_func_t driver_exit;
/* 驱动派遣函数 */
driver_dispatch_t dispatch_function[MAX_IOREQ_FUNCTION_NR];
spinlock_t device_lock; /* 设备锁 */
} driver_object_t;
device_list
记录者这个驱动对应的所有设备对象,设备对象描述了每一个具体的设备。
/* 设备对象 */
typedef struct _device_object
{
list_t list; /* 设备在驱动中的链表 */
device_type_t type; /* 设备类型 */
struct _driver_object *driver; /* 设备所在的驱动 */
void *device_extension; /* 设备扩展,自定义 */
unsigned int flags; /* 设备标志 */
atomic_t reference; /* 引用计数,管理设备打开情况 */
io_request_t *cur_ioreq; /* 当前正在处理的io请求 */
string_t name; /* 名字 */
uint16_t mtime; /* 设备修改时的时间 */
uint16_t mdate; /* 设备修改时的日期 */
struct {
spinlock_t spinlock; /* 设备自旋锁 */
mutexlock_t mutexlock; /* 设备互斥锁 */
} lock;
unsigned long reserved; /* 预留 */
} device_object_t;
type
记录了设备的类型,目前的设备类型如下:
typedef enum _device_type {
DEVICE_TYPE_ANY, /* 任意设备 */
DEVICE_TYPE_BEEP, /* 蜂鸣器设备 */
DEVICE_TYPE_DISK, /* 磁盘设备 */
DEVICE_TYPE_KEYBOARD, /* 键盘设备 */
DEVICE_TYPE_MOUSE, /* 鼠标设备 */
DEVICE_TYPE_NULL, /* 空设备 */
DEVICE_TYPE_PORT, /* 端口设备 */
DEVICE_TYPE_SERIAL_PORT, /* 串口设备 */
DEVICE_TYPE_PARALLEL_PORT, /* 并口设备 */
DEVICE_TYPE_PHYSIC_NETCARD, /* 物理网卡设备 */
DEVICE_TYPE_PRINTER, /* 打印机设备 */
DEVICE_TYPE_SCANNER, /* 扫描仪设备 */
DEVICE_TYPE_SCREEN, /* 屏幕设备 */
DEVICE_TYPE_SOUND, /* 声音设备 */
DEVICE_TYPE_STREAM, /* 流设备 */
DEVICE_TYPE_UNKNOWN, /* 未知设备 */
DEVICE_TYPE_VIDEO, /* 视频设备 */
DEVICE_TYPE_VIRTUAL_DISK, /* 虚拟磁盘设备 */
DEVICE_TYPE_VIRTUAL_CHAR, /* 虚拟字符设备 */
DEVICE_TYPE_WAVE_IN, /* 声音输入设备 */
DEVICE_TYPE_WAVE_OUT, /* 声音输出设备 */
DEVICE_TYPE_8042_PORT, /* 8042端口设备 */
DEVICE_TYPE_NETWORK, /* 网络设备 */
DEVICE_TYPE_BUS_EXTERNDER, /* BUS总线扩展设备 */
DEVICE_TYPE_ACPI, /* ACPI设备 */
DEVICE_TYPE_VIEW, /* 视图设备 */
MAX_DEVICE_TYPE_NR
} device_type_t;
当我们开发驱动时,我们需要为创建的设备指定一个设备类型。上层应用需要根据设备类型来查找某种类型设备的使用情况。
还有一个重要的成员device_extension
,这个是每个设备的扩展指针,也就是每个设备都可以指向一个私有结构体,用该结构体存放数据,这个在后面驱动开发时非常有用。
二、驱动程序结构
这里,给出一个最基础的程序结构:
#include <xbook/bitops.h>
#include <xbook/driver.h>
#include <xbook/debug.h>
#include <xbook/task.h>
#include <string.h>
#include <stdio.h>
#define DRV_NAME "xxx"
#define DRV_VERSION "0.1"
#define DEV_NAME "xxx"
// #define DEBUG_DRV
static iostatus_t xxx_enter(driver_object_t *driver)
{
iostatus_t status;
device_object_t *devobj;
/* 创建设备 */
status = io_create_device(driver, 0, DEV_NAME, DEVICE_TYPE_XXX, &devobj);
if (status != IO_SUCCESS) {
keprint(PRINT_ERR "zero_enter: create device failed!\n");
return status;
}
return status;
}
static iostatus_t xxx_exit(driver_object_t *driver)
{
/* 遍历所有对象 */
device_object_t *devobj, *next;
/* 由于涉及到要释放devobj,所以需要使用safe版本 */
list_for_each_owner_safe (devobj, next, &driver->device_list, list) {
io_delete_device(devobj); /* 删除每一个设备 */
}
string_del(&driver->name); /* 删除驱动名 */
return IO_SUCCESS;
}
iostatus_t xxx_driver_func(driver_object_t *driver)
{
iostatus_t status = IO_SUCCESS;
/* 绑定驱动信息 */
driver->driver_enter = xxx_enter;
driver->driver_exit = xxx_exit;
driver->dispatch_function[IOREQ_READ] = xxx_read;
/* 初始化驱动名字 */
string_new(&driver->name, DRV_NAME, DRIVER_NAME_LEN);
#ifdef DEBUG_DRV
keprint(PRINT_DEBUG "zero_driver_func: driver name=%s\n",
driver->name.text);
#endif
return status;
}
static __init void xxx_driver_entry(void)
{
if (driver_object_create(xxx_driver_func) < 0) {
keprint(PRINT_ERR "[driver]: %s create driver failed!\n", __func__);
}
}
driver_initcall(xxx_driver_entry);
驱动程序分析
null驱动程序分析
这里,我们以null驱动为例,讲解xbook2驱动程序的结构,这样使开发者在开发自己的驱动程序时变得轻松。
xbook2/src/drivers/char/zero.c
#include <xbook/debug.h>
#include <xbook/bitops.h>
#include <string.h>
#include <xbook/driver.h>
#include <xbook/task.h>
#include <xbook/virmem.h>
#include <arch/io.h>
#include <arch/interrupt.h>
#include <sys/ioctl.h>
#include <stdio.h>
#define DRV_NAME "virt-zero"
#define DRV_VERSION "0.1"
#define DEV_NAME "zero"
// #define DEBUG_DRV
iostatus_t zero_read(device_object_t *device, io_request_t *ioreq)
{
iostatus_t status = IO_SUCCESS;
#ifdef DEBUG_DRV
keprint(PRINT_DEBUG "zero_read: data:\n");
#endif
int len = ioreq->parame.read.length;
unsigned char *data = (unsigned char *) ioreq->user_buffer;
while (len-- > 0) {
*data = 0;
data++;
}
ioreq->io_status.infomation = ioreq->parame.read.length; /* 读取永远是0 */
ioreq->io_status.status = status;
io_complete_request(ioreq);
return status;
}
static iostatus_t zero_enter(driver_object_t *driver)
{
iostatus_t status;
device_object_t *devobj;
/* 初始化一些其它内容 */
status = io_create_device(driver, 0, DEV_NAME, DEVICE_TYPE_VIRTUAL_CHAR, &devobj);
if (status != IO_SUCCESS) {
keprint(PRINT_ERR "zero_enter: create device failed!\n");
return status;
}
/* neighter io mode */
devobj->flags = 0;
return status;
}
static iostatus_t zero_exit(driver_object_t *driver)
{
/* 遍历所有对象 */
device_object_t *devobj, *next;
/* 由于涉及到要释放devobj,所以需要使用safe版本 */
list_for_each_owner_safe (devobj, next, &driver->device_list, list) {
io_delete_device(devobj); /* 删除每一个设备 */
}
string_del(&driver->name); /* 删除驱动名 */
return IO_SUCCESS;
}
iostatus_t zero_driver_func(driver_object_t *driver)
{
iostatus_t status = IO_SUCCESS;
/* 绑定驱动信息 */
driver->driver_enter = zero_enter;
driver->driver_exit = zero_exit;
driver->dispatch_function[IOREQ_READ] = zero_read;
/* 初始化驱动名字 */
string_new(&driver->name, DRV_NAME, DRIVER_NAME_LEN);
#ifdef DEBUG_DRV
keprint(PRINT_DEBUG "zero_driver_func: driver name=%s\n",
driver->name.text);
#endif
return status;
}
static __init void zero_driver_entry(void)
{
if (driver_object_create(zero_driver_func) < 0) {
keprint(PRINT_ERR "[driver]: %s create driver failed!\n", __func__);
}
}
driver_initcall(zero_driver_entry);
我们从底部网上分析,最开始有一个driver_initcall
,参数是zero_driver_entry
,表示在初始化驱动的时候来调用这个函数,那么这个驱动程序就会被初始化了。
在zero_driver_entry
里面,调用了driver_object_create
函数来创建一个驱动对象,传入一个参数,表示这个驱动对象初始化的时候会去调用的函数。
在那个函数种,需要填写这个驱动对象的一些信息,比如驱动的进入driver_enter
和退出driver_exit
,表示开始执行和退出执行时调用的函数。开始执行函数zero_enter
,当系统重启或者关闭时,就会调用退出函数zero_exit
。
dispatch_function
派遣函数的意思就是当驱动需要执行某个功能的时候,就会调用对应的函数,在null驱动中,我们设置了IOREQ_READ
操作,也就是读操作,这里绑定了zero_read
,那么当驱动发生读取的时候,就会去调用zero_read
这个函数进行数据读取。
派遣函数支持:
/* io请求函数表 */
enum _io_request_function {
IOREQ_OPEN, /* 设备打开派遣索引 */
IOREQ_CLOSE, /* 设备关闭派遣索引 */
IOREQ_READ, /* 设备读取派遣索引 */
IOREQ_WRITE, /* 设备写入派遣索引 */
IOREQ_DEVCTL, /* 设备控制派遣索引 */
IOREQ_MMAP, /* 设备内存映射派遣索引 */
IOREQ_FASTIO, /* 设备快速IO派遣索引 */
IOREQ_FASTREAD, /* 设备快速读取派遣索引 */
IOREQ_FASTWRITE, /* 设备快速写入派遣索引 */
MAX_IOREQ_FUNCTION_NR
};
如果需要支持某个操作,那么就绑定对应的派遣函数即可,这个可以在其它驱动中体现。
最后通过string_new
为驱动创建了一个驱动名字,一般我们都通过DRV_NAME
宏来指定驱动的名字。
zero_enter
函数中是真正进去驱动初始化时执行的内容,在这里,我们通过io_create_device
创建设备对象,并且对设备对象进行初始化。
iostatus_t io_create_device(
driver_object_t *driver, /* 驱动指针 */
unsigned long device_extension_size, /* 设备扩展大小 */
char *device_name, /* 设备名字 */
device_type_t type, /* 设备类型 */
device_object_t **device /* 创建完成后的设备指针 */
);
调用如果device_extension_size
传入0,那么就不会为设备对象预留扩展区域,创建后的device_object.device_extension
就是NULL
,不然,就是指向了扩展区的地址。
注意device
这个参数是双指针,那么,需要传入一个指向指针的地址,因此有device_object_t *devobj;
生命一个变量,然后传进去时,使用的是&devobj
,如果创建成功,devobj
就指向了创建的设备的地址。
devobj->flags
是设备的标志,该标志支持的值如下:
enum _device_object_flags {
DO_BUFFERED_IO = (1 << 0), /* 缓冲区IO */
DO_DIRECT_IO = (1 << 1), /* 直接内存IO */
DO_DISPENSE = (1 << 2), /* 分发位 */
};
我们只是使到了DO_BUFFERED_IO
和DO_DIRECT_IO
。下面,详细解释一下这两个标志的意思。不过,在认识在这之前,我们还需要认识一个结构体,那就是io_request_t
。
/* 输入输出请求 */
typedef struct _io_request
{
list_t list; /* 队列链表 */
unsigned int flags; /* 标志 */
struct _mdl *mdl_address; /* 内存描述列表地址 */
void *system_buffer; /* 系统缓冲区 */
void *user_buffer; /* 用户缓冲区 */
struct _device_object *devobj; /* 设备对象 */
io_parame_t parame; /* 参数 */
io_status_block_t io_status; /* 状态块 */
} io_request_t;
当我们执行派遣函数时,就会传入设备和io请求。
/* 派遣函数定义 */
typedef iostatus_t (*driver_dispatch_t)(device_object_t *device, io_request_t *ioreq);
此时,我们还需要关注一个结构体,io_parame_t
,定义如下:
typedef struct _io_parame {
union
{
struct {
unsigned int flags;
char *devname;
} open;
struct {
unsigned long length;
unsigned long offset;
} read;
struct {
unsigned long length;
unsigned long offset;
} write;
struct {
unsigned int code;
unsigned long arg;
} devctl;
struct {
int flags;
size_t length;
} mmap;
};
} io_parame_t;
它的内容是一个联合体,在联合里面有open,read,write,devctl,mmap
,也就是当执行这些对应派遣函数时,可以通过对应的参数获取到参数值。比如,在null
驱动中,调用zero_read
派遣函数的时候,ioreq
对应的parame
可以通过ioreq->parame.read
来取值,比如这里通过ioreq->parame.read.length
获取了要读取数据的长度。如果是xxx_write
,那么就是通过ioreq->parame.write.length
来获取要写入数据的长度。
那么devobj->flags
和这个io_request_t
有什么关系呢?接下来就可以进行描述了。
当我们进行read或者write时,就会有读写缓冲区,而devobj->flags
就记录着这个缓冲区怎么传递。
devobj->flags | 描述 |
---|---|
0 | 这是采用DO_NEITHER_IO模式,意思是既不是DO_BUFFERED_IO也不是DO_DIRECT_IO。在这个模式下面,当进行读写时,传入的缓冲区使用ioreq->user_buffer,表示直接使用用户的缓冲区地址,而不做额外处理。 |
DO_BUFFERED_IO | 这是采用缓冲区IO模式,意思是,进行读写的时候,会做一个buffer拷贝。当进行write写入的时候,传过来的缓冲区是在内核中分配的一个缓冲区,然后这个缓冲区会先将用户的buffer内容拷贝进去,然后将这个内核缓冲区传递过来,此时使用的是ioreq->system_buffer。这么做的原因是为了满足有中断产生的驱动,产生后进行数据读写拷贝。比如现在是在dd,这个程序执行了write系统调用,往某个磁盘写入数据,写入第一个扇区后产生了中断,然后再次写入第二个扇区,如果产生中断时当前调度的程序时init程序,那么如果直接从用户的地址读取数据,此时是没有dd数据的地址的,那么此时进行数据读取就会产生错误。解决方法就是,在内核分配一个缓冲区,因为内核缓冲区是所有进程共享的,于是,就算是产生中断了,要求写入下一个扇区,那么数据是在内核中,此时也是可以正常获取数据的。 |
DO_DIRECT_IO | 这是采取直接IO模式,在这个模式下,是对DO_BUFFERED_IO的优化。因为DO_BUFFERED_IO需要分配一个内核缓冲区,并做一个数据复制,因此会比较消耗性能。于是采用直接IO模式,将用户缓冲区的地址直接映射到内核的虚拟地址中,那么就可以直接通过这个地址来访问用户的数据了,不需要复制数据,并且在执行完后,会解除映射。但是这种模式映射和解除映射也需要花费一定时间,适合读写数据比较大的时候使用。数据可以通过ioreq->mdl_address获取,不过注意,这个地址是一个结构体,还需要进一步从里面获取具体地址。 |
在实际的使用中,我们常用的是0和DO_BUFFERED_IO
模式。在null驱动中,devobj->flags
的值是0,表示DO_NEITHER_IO
模式。
在zero_read
函数中,我们需要有一个返回值,用的是iostatus_t
来表示操作执行的状态。
#define IO_SUCCESS 0 /* 成功 */
#define IO_FAILED (1 << 0) /* 失败 */
通常我们使用这个IO宏表示成功和失败的值。
除此之外,还需要在每个函数中调用完成请求函数io_complete_request
来表示已经完成了这个函数的执行。除此之外,还需要填写ioreq
的io_status
,表示本次执行的状况。ioreq->io_status.status
则是执行状态,和返回值保持一致,ioreq->io_status.infomation
保存执行后的信息,比如在zero_read
中,这个信息的值就是成功读取的数据量。
现在差不多整个驱动的内容都已经讲完了。
最后再讲一下zero_exit
,这个函数做的就是退出驱动的事情。因为所有设备是通过链表的形式挂在driver下面的,所以需要通过一个链表循环来获取所有设备,并通过io_delete_device
函数将设备从驱动中删除。
void io_delete_device(
device_object_t *device /* 要设备的对象 */
);
最后,还需要删除驱动的名字,通过string_del
来实现。
更多驱动程序分析
中断管理
在xbook2的中断管理中,我们只需要了解其接口的使用即可,如果想要了解其实现原理,可以自行研究。
中断接口
你需要引入的头文件是<xbook/hardirq.h>
中断接口分为注册中断,注销中断,以及捕捉中断。
int irq_register(irqno_t irq, /* 中断irq号 */
irq_handler_t handler, /* 中断处理函数 */
unsigned long flags, /* 中断标志 */
char *irqname, /* 中断名字 */
char *devname, /* 设备名字 */
void *data); /* 中断私有数据 */
当需要注册一个中断时,需要传入中断号irq
来表明是哪个中断。传入handler
来执行中断处理,这个是一个函数指针。
typedef int (*irq_handler_t)(irqno_t, void *);
#define IRQ_HANDLED 0
#define IRQ_NEXTONE -1
这是中断处理函数的指针类型,我们需要定义对应的中断处理函数如下:
int xxx_handler(irqno_t no, void *data)
{
xxx
return IRQ_HANDLED; /* 已经处理则需要返回HANDLED */
}
IRQ_HANDLED
表示中断被成功处理,而IRQ_NEXTONE
表示本驱动不识别这个中断,希望下一个驱动能够处理这个中断,这是在共享中断中使用的。
flags标志,可以对这个中断进行一定的设置,其值如下:
#define IRQF_DISABLED 0x01
#define IRQF_SHARED 0x02
当有IRQF_DISABLED
标志时,表示处理中断函数过程中会关闭中断,当有IRQF_SHARED
时,表示这是一个共享中断,那么多个驱动都可以使用这个中断irq
。如果是IRQF_SHARED
,那么data
一定不能为NULL
。
注册成功后,返回0,失败返回-1。
当需要注销一个中断时,就可以使用irq_unregister
来注销中断。
int irq_unregister(irqno_t irq, void *data);
irq
是中断号,data
是注册时传入的数据。因为可能是共享中断,所以data
用来识别不同的驱动。
PS/2键盘中断案例
static int keyboard_handler(irqno_t irq, void *data)
{
device_extension_t *ext = (device_extension_t *) data;
...xxx...
return 0;
}
irq_register(devext->irq, keyboard_handler, IRQF_DISABLED, "IRQ1", DRV_NAME, (void *) devext);
在这个键盘驱动例子中,flags
是IRQF_DISABLED
,表示在键盘中断函数处理期间需要关闭中断,data
是devext
,那么就可以在keyboard_handler
中通过data
传入,从而可以解析出对应的接口体,来满足我们的使用。
AHCI磁盘驱动中断案例
static int ahci_handler(irqno_t irq, void *data)
{
int intrhandled = IRQ_NEXTONE;
int i;
for(i=0;i<32;i++) {
if(hba_mem->interrupt_status & (1 << i)) {
dbgprint("ahci: interrupt %d occur!\n", i);
hba_mem->ports[i].interrupt_status = ~0;
hba_mem->interrupt_status = (1 << i);
ahci_flush_commands((struct hba_port *)&hba_mem->ports[i]);
intrhandled = IRQ_HANDLED;
}
}
return intrhandled;
}
irq_register(ahci_int, ahci_handler, IRQF_SHARED, "ahci", "ahci driver", (void *)driver);
在这个驱动案例中,flags
是IRQF_SHARED
,表示这是一个共享中断,data
为driver
驱动本身。
在中断处理函数中虽然没有用到data
这个变量,但是必须传入一个地址,来表明它不同于其它驱动,这是共享中断必须的。
在ahci_handler
中,如果中断被处理了,就返回IRQ_HANDLED
,没有被处理就返回IRQ_NEXTONE
,表示需要其它驱动处理中断。