图文解析设备在内核中的注册流程

流程总览

【文章福利】小编推荐自己的Linux内核源码交流群:【 869634926】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦

设备的注册流程

在电脑上插入一个 USB 鼠标时,操作系统会作出怎样的反应呢?
整个过程可以分为5步:

  • 操作系统会收到一个中断。
  • USB 总线驱动的中断处理程序会执行。
  • 调用操作系统内核相关的服务,查找 USB 鼠标对应的驱动程序。
  • 操作系统加载驱动程序。
  • 驱动程序开始执行,向操作系统内核注册一个鼠标设备。这就是一般操作系统加载驱动的粗略过程。对于安装在主板上的设备,操作系统会枚举设备信息,然后加载驱动程序,让驱动程序创建并注册相应的设备。当然,你还可以手动加载驱动程序。

为了简单起见,我们的 Cosmos 不会这样复杂,暂时也不支持设备热拨插功能。我们让 Cosmos 自动加载驱动,在驱动中向 Cosmos 注册相应的设备,这样就可以大大降低问题的复杂度

整个流程,可参考下图:

上图中,完整展示了 Cosmos 自动加载驱动的整个流程,Cosmos 在初始化驱动时会扫描整个驱动表,然后加载表中每个驱动,分别调用各个驱动的入口函数,最后在驱动中建立设备并向内核注册。

驱动程序表

为了简化问题,便于你理解,我们把驱动程序和内核链接到一起,省略了加载驱动程序的过程,因为加载程序不仅仅是把驱动程序放在内存中就可以了,还要进行程序链接相关的操作,这个操作极其复杂,我们先不在这里研究,感兴趣的话你可以自行拓展。

既然我们把内核和驱动程序链接在了一起,就需要有个机制让内核知道驱动程序的存在。这个机制就是驱动程序表,它可以这样设计。

//cosmos/kernel/krlglobal.c
KRL_DEFGLOB_VARIABLE(drventyexit_t,osdrvetytabl)[]={NULL};

drventyexit_t类型,在上一讲了解过了,它就是一个函数指针类型,这里就是定义了一个函数指针数组,而这个函数指针数组中放的就是驱动程序的入口函数,而内核只需要扫描这个函数指针数组,就可以调用到每个驱动程序了。

有了这个函数指针数组,接着我们还需要写好这个驱动程序的初始化函数,代码如下。

void init_krldriver()
{
    //遍历驱动程序表中的每个驱动程序入口函数
    for (uint_t ei = 0; osdrvetytabl[ei] != NULL; ei++)
    {    //运行一个驱动程序入口
        if (krlrun_driverentry(osdrvetytabl[ei]) == DFCERRSTUS)
        {
            hal_sysdie("init driver err");
        }
    }
    return;
}
void init_krl()
{
    init_krlmm();
    init_krldevice();
    init_krldriver();
    //……
    return;
}

始化驱动的代码就写好了。init_krldriver 函数主要的工作就是遍历驱动程序表中的每个驱动程序入口,并把它作为参数传给 krlrun_driverentry 函数。

有了 init_krldriver 函数,还要在 init_krl 函数中调用它,主要调用上述代码中的调用顺序,请注意,一定要先初始化设备表,然后才能初始化驱动程序,否则在驱动程序中建立的设备和驱动就无处安放了。

运行驱动程序

我们使用驱动程序表,虽然省略了加载驱动程序的步骤,但是驱动程序必须要运行,才能工作。接下来我们就详细看看运行驱动程序的全过程。

调用驱动程序入口函数

首先来解决怎么调用驱动程序入口函数。直接调用驱动程序入口函数是不行的,要先给它准备一个重要的参数,也就是驱动描述符指针

为进一步理解,来写一个函数描述内核加载驱动的过程,后面代码中 drvp 就是一个驱动描述符指针。

drvstus_t krlrun_driverentry(drventyexit_t drventry)
{
    driver_t *drvp = new_driver_dsc();//建立driver_t实例变量
    if (drvp == NULL)
    {
        return DFCERRSTUS;
    }
    if (drventry(drvp, 0, NULL) == DFCERRSTUS)//运行驱动程序入口函数
    {
        return DFCERRSTUS;
    }
    if (krldriver_add_system(drvp) == DFCERRSTUS)//把驱动程序加入系统
    {
        return DFCERRSTUS;
    }
    return DFCOKSTUS;
}

先调用了 一个 new_driver_dsc 函数,用来建立一个 driver_t 结构实例变量.

然后就是调用传递进来的函数指针,并且把 drvp 作为参数传送进去。接着再进入驱动程序中运行,最后,当驱动程序入口函数返回的时候,就会把这个驱动程序加入到我们 Cosmos 系统中了。

一个驱动程序入口函数的例子

一个驱动程序要能够被操作系统调用,产生实际作用,那么这个驱动程序入口函数,就至少有一套标准流程要走,否则只需要返回一个 DFCOKSTUS 就行了,DFCOKSTUS 是个宏,表示成功的状态。

这个标准流程就是,首先要建立建立一个设备描述符,接着把驱动程序的功能函数设置到 driver_t 结构中的 drv_dipfun 数组中,并将设备挂载到驱动上,然后要向内核注册设备,最后驱动程序初始化自己的物理设备,安装中断回调函数。

看一个驱动程序的实际例子,代码如下。

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_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(20)==DFCERRSTUS)
    { 
        return DFCERRSTUS;
    }
    return DFCOKSTUS;
}

上述代码是一个真实设备驱动程序入口函数的标准流程,这是一个例子,不能运行,是一个驱动程序框架,这个例子告诉我们,操作系统内核要为驱动程序开发者提供哪些功能接口函数,这在很多通用操作系统上叫作驱动模型

设备与驱动的联系

上面的例子只是演示流程的,我们并没有写好供驱动程序开发者使用的接口函数,我们这就来写好这些接口函数。

要写的第一个接口就是将设备挂载到驱动上,让设备和驱动产生联系,确保驱动能找到设备,设备能找到驱动。代码如下所示。

drvstus_t krldev_add_driver(device_t *devp, driver_t *drvp)
{
    list_h_t *lst;
    device_t *fdevp;
    //遍历这个驱动上所有设备
    list_for_each(lst, &drvp->drv_alldevlist)
    {
        fdevp = list_entry(lst, device_t, dev_indrvlst);
        //比较设备ID有相同的则返回错误
        if (krlcmp_devid(&devp->dev_id, &fdevp->dev_id) == TRUE)
        {
            return DFCERRSTUS;
        }
    }
    //将设备挂载到驱动上
    list_add(&devp->dev_indrvlst, &drvp->drv_alldevlist);
    devp->dev_drv = drvp;//让设备中dev_drv字段指向管理自己的驱动
    return DFCOKSTUS;
}

由于我们的设计一个驱动程序可以管理多个设备,所以在上述代码中,要遍历驱动设备链表中的所有设备,看看有没有设备 ID 冲突。如果没有就把这个设备载入这个驱动中,并把设备中的相关字段指向这个管理自己的驱动,这样设备和驱动就联系起来了

向内核注册设备

一个设备要想被内核感知,最终供用户使用,就要先向内核注册,这个注册过程应该由内核来实现并提供接口,在这个注册设备的过程中,内核会通过设备的类型和 ID,把用来表示设备的 device_t 结构挂载到设备表中。下面我们来写好这部分代码,如下所示。

drvstus_t krlnew_device(device_t *devp)
{
    device_t *findevp;
    drvstus_t rets = DFCERRSTUS;
    cpuflg_t cpufg;
    list_h_t *lstp;
    devtable_t *dtbp = &osdevtable;
    uint_t devmty = devp->dev_id.dev_mtype;
    if (devp->dev_drv == NULL)//没有驱动的设备不行
    {
        return DFCERRSTUS;
    }
    krlspinlock_cli(&dtbp->devt_lock, &cpufg);//加锁
    //遍历设备类型链表上的所有设备
    list_for_each(lstp, &dtbp->devt_devclsl[devmty].dtl_list)
    {
        findevp = list_entry(lstp, device_t, dev_intbllst);
        //不能有设备ID相同的设备,如果有则出错
        if (krlcmp_devid(&devp->dev_id, &findevp->dev_id) == TRUE)
        {
            rets = DFCERRSTUS;
            goto return_step;
        }
    }
    //先把设备加入设备表的全局设备链表
    list_add(&devp->dev_intbllst, &dtbp->devt_devclsl[devmty].dtl_list);
    //将设备加入对应设备类型的链表中
    list_add(&devp->dev_list, &dtbp->devt_devlist);
    dtbp->devt_devclsl[devmty].dtl_nr++;//设备计数加一
    dtbp->devt_devnr++;//总的设备数加一
    rets = DFCOKSTUS;
return_step:
    krlspinunlock_sti(&dtbp->devt_lock, &cpufg);//解锁
    return rets;
}

上述代码中,主要是检查在设备表中有没有设备 ID 冲突,如果没有的话就加入设备类型链表和全局设备链表中,最后对其计数器变量加一。完成了这些操作之后,我们在操作设备时,通过设备 ID 就可以找到对应的设备了。

安装中断回调函数

设备很多时候必须要和 CPU 进行通信,这是通过中断的形式进行的,例如,当硬盘的数据读取成功、当网卡又来了数据、或者定时器的时间已经过期,这时候这些设备就会发出中断信号,中断信号会被中断控制器接受,然后发送给 CPU 请求内核关注。

收到中断信号后,CPU 就会开始处理中断,转而调用中断处理框架函数,最后会调用设备驱动程序提供的中断回调函数,对该设备发出的中断进行具体处理。

既然中断回调函数是驱动程序提供的,我们内核就要提供相应的接口用于安装中断回调函数,使得驱动程序开发者专注于设备本身,不用分心去了解内核的中断框架。

来实现这个安装中断回调函数的接口函数,代码如下所示。

//中断回调函数类型
typedef drvstus_t (*intflthandle_t)(uint_t ift_nr,void* device,void* sframe);
//安装中断回调函数接口
drvstus_t krlnew_devhandle(device_t *devp, intflthandle_t handle, uint_t phyiline)
{
    //调用内核层中断框架接口函数
    intserdsc_t *sdp = krladd_irqhandle(devp, handle, phyiline);
    if (sdp == NULL)
    {
        return DFCERRSTUS;
    }
    cpuflg_t cpufg;
    krlspinlock_cli(&devp->dev_lock, &cpufg);
    //将中断服务描述符结构挂入这个设备结构中
    list_add(&sdp->s_indevlst, &devp->dev_intserlst);
    devp->dev_intlnenr++;
    krlspinunlock_sti(&devp->dev_lock, &cpufg);
    return DFCOKSTUS;
}

上述代码中,krlnew_devhandle 函数有三个参数,分别是安装中断回调函数的设备,驱动程序提供的中断回调函数,还有一个是设备在中断控制器中断线的号码。

krlnew_devhandle 函数中一开始就会调用内核层的中断框架接口,你发现了么?这个接口还没写呢,所以我们马上就去写好它,但是我们不应该在 krldevice.c 文件中写,而是要在 cosmos/kernel/ 目录下建立一个 krlintupt.c 文件,在这个文件模块中写,代码如下所示。

typedef struct s_INTSERDSC{    
    list_h_t    s_list;        //在中断异常描述符中的链表
    list_h_t    s_indevlst;    //在设备描述描述符中的链表
    u32_t       s_flg;         //标志
    intfltdsc_t* s_intfltp;    //指向中断异常描述符 
    void*       s_device;      //指向设备描述符
    uint_t      s_indx;        //中断回调函数运行计数
    intflthandle_t s_handle;   //中断处理的回调函数指针
}intserdsc_t;

intserdsc_t *krladd_irqhandle(void *device, intflthandle_t handle, uint_t phyiline)
{    //根据设备中断线返回对应中断异常描述符
    intfltdsc_t *intp = hal_retn_intfltdsc(phyiline);
    if (intp == NULL)
    {
        return NULL;
    }
    intserdsc_t *serdscp = (intserdsc_t *)krlnew(sizeof(intserdsc_t));//建立一个intserdsc_t结构体实例变量
    if (serdscp == NULL)
    {
        return NULL;
    }
    //初始化intserdsc_t结构体实例变量,并把设备指针和回调函数放入其中
    intserdsc_t_init(serdscp, 0, intp, device, handle);
    //把intserdsc_t结构体实例变量挂载到中断异常描述符结构中
    if (hal_add_ihandle(intp, serdscp) == FALSE)
    {
        if (krldelete((adr_t)serdscp, sizeof(intserdsc_t)) == FALSE)
        {
            hal_sysdie("krladd_irqhandle ERR");
        }
        return NULL;
    }
    return serdscp;
}

krladd_irqhandle 函数,它的主要工作是创建了一个 intserdsc_t 结构,用来保存设备和其驱动程序提供的中断回调函数。同时,我想提醒你,通过 intserdsc_t 结构也让中断处理框架和设备驱动联系起来了。
具体来说就是,中断处理框架既能找到对应的 intserdsc_t 结构,又能从 intserdsc_t 结构中得到中断回调函数和对应的设备描述符,从而调用中断回调函数,进行具体设备的中断处理。

驱动加入内核

当操作系统内核调用了驱动程序入口函数,驱动程序入口函数就会进行一系列操作,包括建立设备,安装中断回调函数等等,再之后就会返回到操作系统内核。
接下来,操作系统内核会根据返回状态,决定是否将该驱动程序加入到操作系统内核中。你可以这样理解,所谓将驱动程序加入到操作系统内核,无非就是将 driver_t 结构的实例变量挂载到设备表中

写这个实现挂载功能的函数,如下所示。

drvstus_t krldriver_add_system(driver_t *drvp)
{
    cpuflg_t cpufg;
    devtable_t *dtbp = &osdevtable;//设备表
    krlspinlock_cli(&dtbp->devt_lock, &cpufg);//加锁
    list_add(&drvp->drv_list, &dtbp->devt_drvlist);//挂载
    dtbp->devt_drvnr++;//增加驱动程序计数
    krlspinunlock_sti(&dtbp->devt_lock, &cpufg);//解锁
    return DFCOKSTUS;
}

由于驱动程序不需要分门别类,所以我们只把它挂载到设备表中一个全局驱动程序链表上就行了,最后简单地增加一下驱动程序计数变量,用来表明有多少个驱动程序。

小结

一个驱动程序开始是由内核加载运行,然后调用由内核提供的接口建立设备,最后向内核注册设备和驱动,完成驱动和内核的握手动作。现在我们来梳理一下这节课的重点。
首先我们一开始从全局出发,了解了设备的建立流程。然后为了简化内核加载驱动程序的复杂性,我们设计了一个驱动程序表。

最后,按照驱动程序的开发流程,我们给驱动程序开发者提供了一系列接口,它们是建立注册设备、设备加入驱动、安装中断回调函数,驱动加入到系统等,这些共同构成了一个最简化的驱动模型。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340