• 操作系统:设备I/O 如何表示设备类型与设备驱动?


    image

    计算机的结构

    计算机结构示意图:
    image

    主板上的各种芯片并非独立存在,而是以总线为基础连接在一起的,各自完成自己的工作,又能相互打配合,共同实现用户要求的功能。

    如何管理设备

    前面的学习中宏,实现了管理内存和进程,其实进程从正面看它是管理应用程序的,反过来看它也是管理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;
    }
    

    上面的设备表的初始化代码已经写好了,设备表结构示意图如下:
    image
    首先 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 种标准方法,并对应到函数,这些函数的地址保存在驱动程序的数据结构中。

  • 相关阅读:
    jquery 学习笔记
    session
    六、线程中断机制
    二、CompletableFuture(一)基础概念
    四、常见的锁
    五、synchronized细节
    三、CompletableFuture(二)常见用法
    七、等待唤醒的三种方式
    序列化 和 反序列化
    Trigger
  • 原文地址:https://www.cnblogs.com/whiteBear/p/16361835.html
Copyright © 2020-2023  润新知