• Linux驱动开发:内核模块和字符设备驱动


    目录

    内核模块

    内核模块的概念

    内核模块程序的一般形式

    内核模块的相关工具

    内核模块参数

    内核模块和普通应用程序区别

    字符设备驱动

    字符设备驱动基础

    字符设备驱动框架


    内核模块

    linux是 宏内核(单内核)的操作系统的典型代表,它和微内核(典型代表window操作系统)的最大区别在于所有的内核功能都能被整体编译在一起,形成一个单独的内核镜像文件,显著的优点就是效率非常高,内核中各功能模块的交互通过直接的函数调用来进行,而微内核只实现内核中相当关键和核心的一部分,其他功能模块被单独编译,功能模块之间的交互需通过微内核提供的通信机制来建立。

    但对于linux这类宏内核,缺点很明显,如果要增加,删除,修改内核的某个功能,不得不重新编译整个内核,然后重启整个系统。为了弥补这一缺点,Linux引入了内核模块。

    内核模块的概念

    内核模块就是被单独编译的一段代码,它可以在需要的时候动态的加载到内核,从而动态的增加内核的功能,不需要的时候可以动态卸载,从而减少内核的功能,并节约一部分内存,加载或卸载都不需要重启整个系统,所有在进行驱动开发的时候非常好用。内核模块这一特点也有助于减少内核镜像文件体积,自然也减少了内核占用的内存空间。

    内核模块程序的一般形式

    1. //1.头文件
    2. #include <linux/init.h>
    3. #include <linux/module.h>
    4. //2.模块初始化
    5. static int init_module(void)
    6. {
    7. printk("module init\n");
    8. return 0;
    9. }
    10. //3.模块清除
    11. static void cleanup_module(void)
    12. {
    13. printk("cleanup module\n");
    14. }
    15. //4.注册
    16. module_init(init_module);
    17. module_exit(cleanup_module);
    18. //5.信息
    19. MODULE_LICENSE("GPL");

    模块初始化函数一般在模块被动态加载到内核时调用,返回0表示初始化成功,通常返回一个负值表示失败,一般在模块初始化中还会对某些对象进行初始化,比如对内存进行分配对驱动进行注册等等。模块的清除函数在模块从内核中卸载时被调用。

    MODULE_LICENSE("GPL")

    MODULE_LICENSE是一个宏,里面的参数是一个字符串,代表相应的许可证协议(GPL协议是将linux内核源码进行任意的修改和再发布的同时必须将修改后的源码发布,因为linux是一个开源项目)

    内核模块的相关工具

    1.模块加载

    insmod mymod.ko

    insmod:加载指定目录下的一个.ko文件到内核

    modprobe:自动加载模块到内核;前提条件是模块要执行安装操作,运行前最好运行depmod来更新模块依赖信息(不需指定路径和后缀)

    2.模块信息

    modinfo:查看模块信息

    3.模块卸载

    rmmod:将模块从内核中卸载

    内核模块参数

    模块的初始化函数在模块被加载时调用,但是该函数不接受参数,但如果我们想在加载时对模块的行为进行控制,比如我们编写了一个串口驱动,想要在加载时波特率由命令行参数设定,这种方式我们叫做模块参数。

    在模块的加载过程中中,加载程序会得到命令行参数,并转换成相应类型的值,然后赋值给对应变量,这个过程发生在调用模块初始化函数之前。(bool,invbool,charp字符串指针short,int,long,ushort,uint,ulong)

    module_param(变量名,变量类型,权限)

    module_param(变量名,数组元素类型,数组元素个数的指针(可选),权限)

    1. #include <linux/init.h>
    2. #include <linux/module.h>
    3. int val =1;
    4. int val2 = 2;
    5. module_param(val,int,0000);
    6. module_param(val2,int,0000);
    7. static int mod_init(void)
    8. {
    9. printk("mod_init val=%d,val2=%d\n",val,val2);
    10. return 0;
    11. }
    12. static void mod_exit(void)
    13. {
    14. printk("mod_exit\n");
    15. ;
    16. }
    17. module_init(mod_init);
    18. module_exit(mod_exit);
    19. MODULE_LICENSE("GPL");

    内核模块和普通应用程序区别

    1.内核模块运行在内核空间;应用程序运行在用户空间。

    2.内核模块是被动的被调用,应用程序是顺序执行。

    3.内核模块处于c函数库之下,不能调用C库函数,应用程序可以任意调用

    4.内核模块产生非法访问可能导致整个系统崩溃,而应用程序通常只影响自己。

    5.内核模块的并发更多,而应用程序一般只考虑多线程多进程

    6.内核空间的调用链上只有4kb或8kb的栈,相对于应用程序很小,如果需要较大内存空间需要动态分配。

    7.printk不支持浮点数打印

    字符设备驱动

    linux系统中根据驱动程序实现的模型框架将设备的驱动分为:字符设备驱动,块设备驱动,网络设备驱动。

    字符设备驱动基础

    在linux系统中,一切皆文件,应用程序要对设备进行访问,最终就会转化为对文件的访问,这样可以统一对上层的接口。设备文件通常在/dev目录下

    ls -l /dev  //可以看到目录下的设备文件

    属性中第一个字符表示文件属性:b为块设备,c为字符设备,设备文件会比普通文件多出两个数字,这两个数字分别是主设备号和次设备号。这两个号是设备在内核中的身份和标志,是内核区分不同设备的唯一信息,通常内核用主设备号区分一类设备,次设备号区分同一类设备的不同个体或不同分区。

    设备文件(设备节点)在linux系统中往往是自动创建的,但我们也可以通过mknod命令来手动创建,但是设备号不能冲突。

    1. mknod /dev/myled c(设备属性) 253(主设备号) 0(次设备号)
    2. cat /proc/devices //查看设备信息

    字符设备驱动框架

    要实现一个字符设备驱动,需要先构造一个cdev对象,并让cdev同设备号和设备的操作方法集合相关联,然后将该cdev结构对象添加到内核的cdev_map散列表中。首先我们需要在驱动中注册设备号,有静态注册和动态注册两种方法,为了防止设备冲突注册失败,我们一般选择动态注册。

    下面我们列出相关函数:

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

    //动态注册

    //dev:主设备号

    //baseminor:次设备号,第一个次设备号

    //count : 设备个数

    //name:设备名

    //返回值:成功返回0,失败返回错误码(负值)

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

    //字符设备初始化

    //cdev: cdev设备

    //fops: 字符设备操作集

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

    //注册字符设备到内核

    //p: cdev设备

    //dev: 设备号

    //count:个数

    //返回值:失败返回负值

    void unregister_chrdev_region(dev_t from, unsigned count)

    //释放设备号

    void cdev_del(struct cdev *p);

    //从内核中删除

    //p: cdev设备

    上述函数顺序也是我们在模块加载和卸载的顺序,先分配设备号,然后将字符设备初始化,再将字符设备注册到内核中。

    我们再介绍几个宏

    MAJOR:从设备号中取出主设备号

    MINOR:从设备号中取出次设备号

    描述字符设备的结构体:

    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. };
    1. //字符设备操作集
    2. //一般会根据实际情况,实现对应的操作
    3. struct file_operations {
    4.   struct module *owner;  
    5.   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
    6.   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);   
    7.   int (*mmap) (struct file *, struct vm_area_struct *);  
    8.   long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);  
    9.   int (*open) (struct inode *, struct file *);      /* 打开设备 */
    10.   int (*release) (struct inode *, struct file *);   /* 关闭设备 */
    11.   int (*flush) (struct file *, fl_owner_t id);  
    12.   loff_t (*llseek) (struct file *, loff_t, int);  
    13.   int (*fasync) (int, struct file *, int);  
    14.   unsigned int (*poll) (struct file *, struct poll_table_struct *);  
    15.   ...
    16. };

    我们看一段示例代码:

    1. //1.头文件
    2. #include
    3. #include
    4. #include
    5. #include
    6. #define LED_ON 1
    7. #define LED_OFF 2
    8. //2.模块加载与卸载
    9. static dev_t devnum;//设备号
    10. static char *name = "myled"; //设备名
    11. static struct cdev mycdev; //字符设备
    12. long led_ioctl(struct file *pf, unsigned int cmd, unsigned long args);
    13. static struct file_operations myfops={
    14. .unlocked_ioctl = led_ioctl,
    15. };
    16. static int mod_init(void)
    17. {
    18. //与内核相关
    19. int ret;
    20. //1.分配设备号
    21. ret = alloc_chrdev_region(&devnum, 0, 1, name);
    22. if(ret!=0){
    23. return ret;
    24. }
    25. //2.初始化字符设备cdev
    26. cdev_init(&mycdev,&myfops);
    27. //3.注册到内核
    28. ret = cdev_add(&mycdev,devnum,1);
    29. if(ret<0){
    30. return ret;
    31. }
    32. printk("register success %d,%d\n",MAJOR(devnum),MINOR(devnum));
    33. //与硬件相关
    34. return 0;
    35. }
    36. static void mod_exit(void)
    37. {
    38. //1.从内核中删除cdev
    39. cdev_del(&mycdev);
    40. //2.释放设备号
    41. unregister_chrdev_region(devnum,1);
    42. }
    43. /*********IO操作***********/
    44. long led_ioctl(struct file *pf, unsigned int cmd, unsigned long args)
    45. {
    46. printk("led_ioctl:%d\n",cmd);
    47. switch (cmd)
    48. {
    49. case LED_ON: printk("do_led_on\n");break;
    50. case LED_OFF: printk("do_led_off\n");break;
    51. default: break;
    52. }
    53. return 0;
    54. }
    55. //3.注册
    56. module_init(mod_init);
    57. module_exit(mod_exit);
    58. //4.模块信息(许可证)
    59. MODULE_LICENSE("GPL");

  • 相关阅读:
    windows.h
    数据结构和算法之基于链表的栈和队列、多文件和模块分层设计、函数指针
    吴恩达《机器学习》8-1->8-2:非线性假设、神经元和大脑
    【Java 设计模式】创建者模式 之抽象工厂模式
    LeetCode-795. 区间子数组个数【双指针】
    3D Slicer学习记录(0)--利用OpenIGTLink实现数据发送接收
    疫情之下的裁员浪潮,7点建议帮你斩获心仪offer
    学习Ajax需要了解的一些概念
    昨天阅读量800多
    Vue.js+SpringBoot开发生活废品回收系统
  • 原文地址:https://blog.csdn.net/m0_70983574/article/details/126545482