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_IODO_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来表示已经完成了这个函数的执行。除此之外,还需要填写ioreqio_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来实现。

更多驱动程序分析

《ahci磁盘驱动分析》

《keyboard键盘驱动分析》

中断管理

在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);

在这个键盘驱动例子中,flagsIRQF_DISABLED,表示在键盘中断函数处理期间需要关闭中断,datadevext,那么就可以在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);

在这个驱动案例中,flagsIRQF_SHARED,表示这是一个共享中断,datadriver驱动本身。

在中断处理函数中虽然没有用到data这个变量,但是必须传入一个地址,来表明它不同于其它驱动,这是共享中断必须的。

ahci_handler中,如果中断被处理了,就返回IRQ_HANDLED,没有被处理就返回IRQ_NEXTONE,表示需要其它驱动处理中断。

Copyright © BookOS-developers 2021 all right reserved,powered by Gitbook修订时间: 2021-06-16

results matching ""

    No results matching ""