• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Linux:字符设备驱动框架

武飞扬头像
风间琉璃•
帮助3

目录

一、驱动介绍

1.内核模块

2.日志级别

3.模块符号的导出

4.内核模块参数

二、字符设备驱动(一)

1.模块加载

2.注册字符设备驱动

3.内存映射

三、字符设备驱动(二)

1.模块加载

2.申请设备号

3.注册字符设备

4.自动创建设备节点

 5.文件私有数据

总结


前言

学新通

一、驱动介绍

Linux驱动属于内核的一部分,学习驱动开发时将驱动设计为内核模块,内核模块是一种可以在系统运行时加载和卸载的机制。

内核编程的注意事项

1.不能使用C标准库和C标准头文件

2.使用GNU C

3.没有内存保护机制

4.不能处理浮点运算

5.注意并发互斥和可移植性问题 

1.内核模块

Linux 驱动有两种运行方式,第一种是将驱动编译进 Linux 内核中,当 Linux 内核启动的时就会自动运行驱动程序。第二种是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用insmod或者modprobe命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块。

模块的加载和卸载注册函数如下

  1.  
    #include <linux/init.h>
  2.  
    #include <linux/module.h> //需要包含头文件
  3.  
     
  4.  
    module_init(xxx_init); //注册模块加载函数
  5.  
    module_exit(xxx_exit); //注册模块卸载函数

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用insmod命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用

字符设备驱动模块加载和卸载模板如下所示:

  1.  
    #include <linux/init.h>
  2.  
    #include <linux/module.h>
  3.  
     
  4.  
    /* 驱动入口函数 */
  5.  
    static int __init xxx_init(void)
  6.  
    {
  7.  
    /* 入口函数具体内容 */
  8.  
     
  9.  
    return 0;
  10.  
    }
  11.  
     
  12.  
     
  13.  
    /* 驱动出口函数 */
  14.  
    static void __exit xxx_exit(void)
  15.  
    {
  16.  
    /* 出口函数具体内容 */
  17.  
    }
  18.  
     
  19.  
    /* 将上面两个函数指定为驱动的入口和出口函数 */
  20.  
    module_init(xxx_init);
  21.  
    module_exit(xxx_exit)
  22.  
     
  23.  
    MODULE_LICENSE("GPL");//GPL模块许可证
学新通

注:在编写模块时必须加上模块许可证防止内核被污染,造成某些功能无法使用

 驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块: insmod和 modprobe

insmod drv.ko  //加载驱动模块

rmmod  drv.ko   //卸载驱动模块

modprobe  drv.ko   //加载或者卸载驱动模块

 区别:insmod 命令不能解决模块的依赖关系,但是 modprobe 不会存在这个问题, modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中。比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。

modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用modprobe 命令来加载驱动。 modprobe 命令默认会去/lib/modules/<kernel-version>目录中查找模块。

同时 modprobe 命令也可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,推荐使用 rmmod 命令
 

2.日志级别

printk在内核中用来记录日志信息的函数,只能在内核源码范围内使用。和printf非常相似。printk函数主要做两件事情:①将信息记录到log中 调用控制台驱动来将信息输出

  1.  
    #define KERN_EMERG "<0>" /*系统不可用*/
  2.  
    #define KERN_ALERT "<1>" /*必须立即处理的错误信息*/
  3.  
    #define KERN_CRIT "<2>" /*严重错误信息*/
  4.  
    #define KERN_ERR "<3>" /*错误信息*/
  5.  
    #define KERN_WARNING "<4>" /*警告信息*/
  6.  
    #define KERN_NOTICE "<5>" /*需要注意的信息*/
  7.  
    #define KERN_INFO "<6>" /*一般信息*/
  8.  
    #define KERN_DEBUG "<7>" /*调试信息*/
printk(KERN_DEBUG"debug\r\n");

printk打印的内容是否显示取决于日志级别,只有当printk的日志级别高于内核默认打印级别时才打印(打印日志级别数值小于内核默认打印级别)。数字越小,优先级越高

若是printk不提供打印级别使用默认打印级别,可以通过查看/proc/sys/kernel/printk来查看,第二个数字就是printk的默认打印级别

学新通

 而/proc/sys/kernel/printk中的第一个数字就是内核默认打印级别,可以通过uboot的环境变量bootargs传递内核默认打印级别。在uboot的bootargs中加入loglevel=X指令

学新通

 学新通

 内核的默认打印级别修改为8。

3.模块符号的导出

 模块导出符号可以将模块中的变量/函数导出,供内核其他代码/模块使用。 内核中提供了相应的宏来实现模块的导出:

EXPORT_SYMBOL -------------- 使用无限制

EXPORT_SYMBOL_GPL ---------- 只有遵循GPL协议的代码才可以使用

需注意的是, 如果一个模块使用了另一个模块的 变量/函数,该模块依赖于另一个模块,加载模块时必须先加载依赖的模块,如果一个模块被内核使用,该模块不得卸载。 

4.内核模块参数

内核的模块参数不但可以在编写代码时设置其的值, 还可以在加载模块时设置其的值,甚至可以再加载模块后修改其的值。在模块中声明一些变量,使用以下语法将这些变量设置为模块参数:

module_param(模块参数名,模块参数类型,访问权限);

module_param_array(数组模块参数名,数组元素类型,NULL,访问权限);

在代码其他地方使用模块参数和使用普通变量没有区别。加载模块时可以通过 "模块参数名=值" 的方式来修改模块参数的值。当模块加载成功后,访问权限非0的模块参数就会出现在以下路径下 /sys/module/模块名/parameters  存在和模块参数名相同的文件,这些文件的权限来自于模块参数的权限,内容来自于模块参数的值。同时也可以通过修改文件中保存的数据来修改对应模块参数。

二、字符设备驱动(一)

 驱动是沟通底层硬件和上层应用的桥梁,访问设备文件通过文件系统IO,在用户层访问设备文件和访问普通文件没有区别。 

1.模块加载

模块加载和卸载模板:

  1.  
    #include <linux/init.h>
  2.  
    #include <linux/module.h>
  3.  
     
  4.  
    /* 驱动入口函数 */
  5.  
    static int __init xxx_init(void)
  6.  
    {
  7.  
    /* 入口函数具体内容 */
  8.  
     
  9.  
    return 0;
  10.  
    }
  11.  
     
  12.  
     
  13.  
    /* 驱动出口函数 */
  14.  
    static void __exit xxx_exit(void)
  15.  
    {
  16.  
    /* 出口函数具体内容 */
  17.  
    }
  18.  
     
  19.  
    /* 将上面两个函数指定为驱动的入口和出口函数 */
  20.  
    module_init(xxx_init);
  21.  
    module_exit(xxx_exit)
  22.  
     
  23.  
    MODULE_LICENSE("GPL");//GPL模块许可证
学新通

2.注册字符设备驱动

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。

字符设备的注册和注销函数原型:

  1.  
    static inline int register_chrdev(unsigned int major, const char *name,
  2.  
    const struct file_operations *fops)
  3.  
     
  4.  
    static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备,需要传入主设备号,设备名称和指向设备操作函数集合变量。这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行
 

  1.  
    /*
  2.  
    * @description : 打开设备
  3.  
    * @param - inode : 传递给驱动的inode
  4.  
    * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
  5.  
    * 一般在open的时候将private_data指向设备结构体。
  6.  
    * @return : 0 成功;其他 失败
  7.  
    */
  8.  
    static int led_open(struct inode *inode, struct file *filp)
  9.  
    {
  10.  
    return 0;
  11.  
    }
  12.  
     
  13.  
    /*
  14.  
    * @description : 从设备读取数据
  15.  
    * @param - filp : 要打开的设备文件(文件描述符)
  16.  
    * @param - buf : 返回给用户空间的数据缓冲区
  17.  
    * @param - cnt : 要读取的数据长度
  18.  
    * @param - offt : 相对于文件首地址的偏移
  19.  
    * @return : 读取的字节数,如果为负值,表示读取失败
  20.  
    */
  21.  
    static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
  22.  
    {
  23.  
    return 0;
  24.  
    }
  25.  
     
  26.  
    /*
  27.  
    * @description : 向设备写数据
  28.  
    * @param - filp : 设备文件,表示打开的文件描述符
  29.  
    * @param - buf : 要写给设备写入的数据
  30.  
    * @param - cnt : 要写入的数据长度
  31.  
    * @param - offt : 相对于文件首地址的偏移
  32.  
    * @return : 写入的字节数,如果为负值,表示写入失败
  33.  
    */
  34.  
    static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
  35.  
    {
  36.  
    return 0;
  37.  
    }
  38.  
     
  39.  
    /*
  40.  
    * @description : 关闭/释放设备
  41.  
    * @param - filp : 要关闭的设备文件(文件描述符)
  42.  
    * @return : 0 成功;其他 失败
  43.  
    */
  44.  
    static int led_release(struct inode *inode, struct file *filp)
  45.  
    {
  46.  
    return 0;
  47.  
    }
  48.  
     
  49.  
    //设备操作函数
  50.  
    static struct file_operations led_fops = {
  51.  
    .owner = THIS_MODULE,
  52.  
    .open = led_open,
  53.  
    .read = led_read,
  54.  
    .write = led_write,
  55.  
    .release = led_release,
  56.  
    };
  57.  
     
  58.  
    //驱动入口函数
  59.  
    static int __init led_init(void)
  60.  
    {
  61.  
    int retvalue = 0;
  62.  
     
  63.  
    /*注册字符设备驱动 */
  64.  
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
  65.  
    if(retvalue < 0){
  66.  
    printk("register chrdev failed!\r\n");
  67.  
    return -EIO;
  68.  
    }
  69.  
    return 0;
  70.  
    }
  71.  
    //驱动出口函数
  72.  
    static void __exit led_exit(void)
  73.  
    {
  74.  
     
  75.  
    /* 注销字符设备驱动 */
  76.  
    unregister_chrdev(LED_MAJOR, LED_NAME);
  77.  
    }
  78.  
     
  79.  
    module_init(led_init);
  80.  
    module_exit(led_exit);
  81.  
    MODULE_LICENSE("GPL");
学新通

3.内存映射

在Linux中不能直接访问寄存器,要想要操作寄存器需要完成物理地址到虚拟空间的映射。

  1.  
    #include <linux/io.h>
  2.  
    #include <mach/platform.h>
  3.  
     
  4.  
    #define ioremap(cookie,size) __arm_ioremap((cookie), (size),
  5.  
    MT_DEVICE)
  6.  
     
  7.  
    void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
  8.  
    unsigned int mtype)
  9.  
    {
  10.  
    return arch_ioremap_caller(phys_addr, size, mtype,
  11.  
    __builtin_return_address(0));
  12.  
    }

 返回值: __iomem 类型的指针,指向映射后的虚拟空间首地址

建立映射:映射的虚拟地址 = ioremap(IO内存起始地址,映射长度);一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。

void iounmap (volatile void __iomem *addr)
  1.  
    /* 寄存器物理地址 */
  2.  
    #define CCM_CCGR1_BASE (0X020C406C)
  3.  
     
  4.  
    /* 映射后的寄存器虚拟地址指针 */
  5.  
    static void __iomem *IMX6U_CCM_CCGR1;
  6.  
     
  7.  
    /* 寄存器地址映射 */
  8.  
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
  9.  
    if(IS_ERR_OR_NULL(IMX6U_CCM_CCGR1))
  10.  
    {
  11.  
    //...
  12.  
    }

解除映射:

  1.  
    void iounmap (volatile void __iomem *addr)
  2.  
     
  3.  
    iounmap(IMX6U_CCM_CCGR1);

三、字符设备驱动(二)

前面的字符设备驱动框架比较简单,不灵活。不仅需要设定主设备号,在测试时还需要手动创建设备文件。新字符设备驱动框架刚好能解决这两个大问题。

1.模块加载

这一步和前面是一样的。

2.申请设备号

前面设备号的申请是开发者检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的设备号给驱动。这样很不方便。Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号。

动态申请设备号:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

dev:保存申请到的设备号。
baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0
count: 要申请的设备号数量。
name:设备名字。

静态申请设备号

int register_chrdev_region(dev_t from, unsigned count, const char *name);

from - 要申请的起始设备号     

count - 设备号个数     

name - 设备号在内核中的名称

返回0申请成功,否则失败    

注销字符设备之后要释放掉设备号,设备号释放函数

void unregister_chrdev_region(dev_t from, unsigned count)

from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。

 新字符设备驱动模板:

  1.  
     
  2.  
    //创建设备号
  3.  
    if (newchrled.major) //定义了设备号就静态申请
  4.  
    {
  5.  
    newchrled.devid = MKDEV(newchrled.major, 0);
  6.  
    register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
  7.  
    }
  8.  
    else //没有定义设备号就动态申请
  9.  
    {
  10.  
     
  11.  
    alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);//申请设备号
  12.  
    newchrled.major = MAJOR(newchrled.devid); //获取分配号的主设备号
  13.  
    newchrled.minor = MINOR(newchrled.devid); // 获取分配号的次设备号
  14.  
    }

3.注册字符设备

在 Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中的定义如下

  1.  
    struct cdev {
  2.  
    struct kobject kobj;
  3.  
    struct module *owner;
  4.  
    const struct file_operations *ops;//操作函数集合
  5.  
    struct list_head list;
  6.  
    dev_t dev;//设备号
  7.  
    unsigned int count;
  8.  
    };

在 cdev 中有两个重要的成员变量:ops 和 dev,字符设备文件操作函数集合file_operations 以及设备号 dev_t
 

向Linux内核添加字符设备

①初始化cdev结构体变量

定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化, cdev_init 函数原型如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

 参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。

  1.  
    struct cdev testcdev;
  2.  
     
  3.  
    //设备操作函数
  4.  
    static struct file_operations test_fops = {
  5.  
    .owner = THIS_MODULE,
  6.  
    //其他具体的初始项
  7.  
    };
  8.  
     
  9.  
    testcdev.owner = THIS_MODULE;
  10.  
     
  11.  
    //初始化 cdev 结构体变量
  12.  
    cdev_init(&testcdev, &test_fops);

② 将设备添加到内核

 cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

p - 要添加的cdev结构     

dev - 绑定的起始设备号     

count - 设备号个数

cdev_add(&testcdev, devid, 1); //添加字符设备

 将cdev添加到内核同时绑定设备号。

其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev函数完成者两步操作。卸载也是一样的。

卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备

  1.  
    void cdev_del(struct cdev *p);
  2.  
     
  3.  
    cdev_del(&testcdev); //删除 cdev


4.自动创建设备节点

上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自在/dev 目录下创建对应的设备文件

在 Linux 下通过 udev 来实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。具体关于mdev和udev可以参考前面的笔记。


自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添
加自动创建设备节点相关代码。

①首先要创建一个 class 类,定义在文件include/linux/device.h 里面。

struct class *class_create(struct module *owner, const char *name);

class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。设备类名对应 /sys/class 目录的子目录名返回值是个指向结构体 class 的指针,也就是创建的类

卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy

void class_destroy(struct class *cls); // cls要删除的类

②创建设备:

创建好类以后还不能实现自动创建设备节点,还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。

  1.  
    struct device *device_create(struct class *class,
  2.  
    struct device *parent,
  3.  
    dev_t devt,
  4.  
    void *drvdata,
  5.  
    const char *fmt, ...)

 简化后

struct device *device_create(设备类指针, 父设备指针,设备号, 额外数据, 设备文件名);

成功会在 /dev 目录下生成设备文件。

卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy

void device_destroy(struct class *class, dev_t devt);

参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号

小结:

a.创建设备类 ---------------------- class_create 

struct class *class_create(struct module *owner, const char *name);

//创建成功会生产该路径: /sys/class/设备类名

b.创建设备文件(设备节点) ------------------ device_create

struct device *device_create(设备类指针, 父设备指针,设备号, 额外数据, 设备文件名);

//成功会在 /dev 目录下生成设备文件

销毁设备类 --------------------- class_destroy

销毁设备文件 ------------------- device_destroy

  1.  
    struct class *class; /* 类 */
  2.  
    struct device *device; /* 设备 */
  3.  
    dev_t devid; /* 设备号 */
  4.  
     
  5.  
    /* 驱动入口函数 */
  6.  
    static int __init led_init(void)
  7.  
    {
  8.  
    ...
  9.  
    /* 创建类 */
  10.  
    class = class_create(THIS_MODULE, "xxx");
  11.  
    /* 创建设备 */
  12.  
    device = device_create(class, NULL, devid, NULL, "xxx");
  13.  
    ...
  14.  
    return 0;
  15.  
    }
  16.  
     
  17.  
    /* 驱动出口函数 */
  18.  
    static void __exit led_exit(void)
  19.  
    {
  20.  
    ...
  21.  
    /* 删除设备 */
  22.  
    device_destroy(newchrled.class, newchrled.devid);
  23.  
    /* 删除类 */
  24.  
    class_destroy(newchrled.class);
  25.  
    ...
  26.  
    }
  27.  
     
  28.  
    module_init(led_init);
  29.  
    module_exit(led_exit);
学新通

 5.文件私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),在编写驱动的时候可以将这些属性全部写成变量的形式,但对于一个设备的所有属性信息最好将其做成一个结构体,编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中

  1.  
    /* newchrled设备结构体 */
  2.  
    struct newchrled_dev{
  3.  
    dev_t devid; /* 设备号 */
  4.  
    struct cdev cdev; /* cdev */
  5.  
    struct class *class; /* 类 */
  6.  
    struct device *device; /* 设备 */
  7.  
    int major; /* 主设备号 */
  8.  
    int minor; /* 次设备号 */
  9.  
    };
  10.  
     
  11.  
    struct newchrled_dev newchrled; /* led设备 */
  12.  
     
  13.  
    /*
  14.  
    * @description : 打开设备
  15.  
    * @param - inode : 传递给驱动的inode
  16.  
    * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
  17.  
    * 一般在open的时候将private_data指向设备结构体。
  18.  
    * @return : 0 成功;其他 失败
  19.  
    */
  20.  
    static int led_open(struct inode *inode, struct file *filp)
  21.  
    {
  22.  
    filp->private_data = &newchrled; /* 设置私有数据 */
  23.  
    return 0;
  24.  
    }
  25.  
     
  26.  
    static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
  27.  
    {
  28.  
    struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
  29.  
    return 0;
  30.  
    }
学新通

在 open 函数里面设置好私有数据后,在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体

6.应用层和内核层传递数据

应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助下面的这两个函数
 

static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

用户空间-->内核空间
学新通

copy_from_user(void *to, const void __user *from, unsigned long n)

to:目标地址(内核空间)

from:源地址(用户空间)

n:将要拷贝数据的字节数

返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数

 内核空间-->用户空间

学新通

copy_to_user(void __user *to, const void *from, unsigned long n)

 to:目标地址(用户空间)

 from:源地址(内核空间)

 n:将要拷贝数据的字节数

 返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数

总结

Linux系统将设备分为3类:字符设备、块设备、网络设备。

学新通

Linux中所有的设备文件在/dev目录下,内核中有很多的字符设备驱动,这些字符设备驱动和字符设备文件匹配的方式是通过设备号

学新通在应用层调用open函数打开设备文件,对于上层open调用到内核时会发生一次软中断中断号是0X80,从用户空间进入到内核空间。

学新通

open会调用到system_call(内核函数),system_call会根据设备文件中的设备名,去找出要操作的设备号。

然后调到虚拟文件VFS (为了上层调用到确切的硬件统一化),调用VFS里的sys_open,sys_open会找到在驱动链表(管理所有设备的驱动)里面,根据主设备号和次设备号找到字符设备驱动,然后驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能。

学新通

大致流程:
学新通

补充:

1.在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。

2.在linux操作系统中,每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。

3.在linux操作系统中, 每个驱动程序都有一个设备号

4.在linux操作系统中,每打开一次文件,Linux操作系统会在VFS层分配一个struct file结构体来描述打开的文件。

大致驱动原理:

(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。

(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口

(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中

学新通

(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。

其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作
 

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhehie
系列文章
更多 icon
同类精品
更多 icon
继续加载