字符设备驱动及模型

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

 

字符设备:不能随机访问,一字节一字节的读写数据,常见的设备有鼠标、键盘、串口、控制台和LED设备等。

块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

网络设备:。。。

 

 

驱动的静态加载和动态加载:

区别:

1 编译选项不同:Y编译到内核映像上,M生成驱动文件.ko,在文件系统上

2 存在位置不同:静态一般是编译进内核映像文件中,动态在文件系统上。

3 加载的时机不同:静态一般是随内核启动的加载,而动态一般是在文件系统完后用insmod加载

 

动态加载的优势:热插拔   调试  开机优化(驱动的加载是要花时间的)

 

 

常用驱动设备命令:

lsmod: 查看系统中所有已经被加载了的所有的模块以及模块间的依赖关系

lsmod 命令实际上读取并分析/proc/modules 文件——cat /proc/modules

rmmod: 卸载驱动    rmmod xxx——不加后缀

modinfo:获得模块的信息,通过modinfo xx.ko 来查看模块的信息.

insmod或modprobe :加载驱动(modprobe无需加后缀)

 

使用 modprobe 命令加载的模块,若以“modprobe -r filename”的方式卸载
卸载其依赖的模块

查看已经加载的驱动模块的信息:

lsmod   能够显示驱动的大小以及被谁使用  

cat /proc/modules   能够显示驱动模块大小、在内核空间中的地址

cat /proc/devices    只显示驱动的主设备号,且是分类显示 

/sys/modules         下面存在对应的驱动的目录,目录下包含驱动的分段信息等等。

 

mknod:创建设备节点(字符设备和块设备文件节点)

mknod 设备节点名 主设备号 次设备号-----将设备节点(应用层)与设备号(内核层)建立联系。

主设备号用来标识与设备文件相连的驱动程序。次设备号用来辨别操作的是哪个设备。

简单的说就是:主设备号用来反映设备类型,次设备号用来区分同类型的设备。

 

cdev 结构体的 dev_t 成员定义了设备号,为 32 位,其中高 12 位为主设备号,低20 位为次设备号。

根据主设备号和次设备号生成设备号:MKDEV(int major,int minor);

从设备号获取主设备号:MAJOR(dev_t dev);

从设备号获取次设备号:MINOR(dev_t dev);

 

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

字符驱动框架:

 

问:应用程序open如何找到驱动程序的open函数

答:应用程序的open通过C库的open函数,通过系统调用的system_open函数,进而通过swi val指令,进入内核,通过一定的办法来找到驱动程序的open函数。

问:通过什么样的方法来找到驱动程序的open函数

答:通过一个注册函数+设备节点————这里的注册函数就是下面字符设备驱动模型的实现。

问:应用程序一般是由main函数开始执行,那么驱动程序一般是先执行什么?

答:通过一个宏,指定驱动程序的入口函数,当装载驱动时就会执行入口函数。

例如:module_init(xxx_init);  //用于修饰入口函数,

自然地,驱动程序的出口函数,则是在卸载驱动时就会执行出口函数。

例如:module_exit(xxx_exit);  //用于修饰出口函数

 

Linux 内核模块加载函数一般_ _init 标识声明 

Linux 内核模块卸载函数一般以_ _exit 标识声明 

 

 

字符设备驱动模型:

 

cdev介绍

cdev是一个结构体,里面的成员来共同帮助我们注册驱动到内核中,表达字符设备的,将这个struct cdev结构体进行填充,主要填充的内容就是

 

 

file_operations这个结构体变量,让cdev中的ops成员的值为file_operations结构体变量的值。这个结构体会被cdev_add函数向内核注册,可以用很多函数来操作他.

如:

cdev_alloc:让内核为这个结构体分配内存的

cdev_init:将struct cdev类型的结构体变量和file_operations结构体进行绑定的

cdev_add:向内核里面添加一个驱动,注册驱动

cdev_del:从内核中注销掉一个驱动。注销驱动

 

 

Linux内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,它的定义如下:
    static struct char_device_struct
    {
        struct char_device_struct *next;    // 指向散列冲突链表中的下一个元素的指针
        unsigned    int major;              // 主设备号
        unsigned    int baseminor;          // 起始次设备号
        int minorct;                        // 设备编号的范围大小
        char    name[64];                   // 处理该设备编号范围内的设备驱动的名称
        struct file_operations *fops;      
        struct cdev *cdev;                  // 指向字符设备驱动程序描述符的指针
    }*chrdevs[CHRDEV_MAJOR_HASH_SIZE];

 

 

 

 

1 驱动初始化

    1. 分配设备号(cdev)

内核提供的了三个函数来分配设备号:这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev()。我们使用其中一种就可以。

 

 

(1)register_chrdev 比较老的内核注册的形式,早期的驱动——同一类字符设备(即主设备号相同),会在内核中连续注册了256(分析内核代码中可知),也就是所以的此设备号都会被占用,而在大多数情况下都不会用到这么多次设备号,所以会造成极大的资源浪费。

(2)register_chrdev_region/alloc_chrdev_region + cdev  新的驱动形式

区别:register_chrdev()函数是老版本里面的设备号注册函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。register_chrdev_region以及alloc_chrdev_region就是将上述函数的静态和动态注册设备号进行了拆分的强化

           

 

 

int register_chrdev (unsigned int major, const  char *name, struct file_operations*fops); 

major:要注册的设备号,参数如果等于0,则表示采用系统动态分配的主设备号。

name:驱动名称。

fops:file_operation操作集合,后面有介绍。

 

register_chrdev_region(dev_t first,unsigned int count,char *name)
(1)first :要分配的设备编号范围的初始值这组连续设备号(count个)起始设备号(设备号包含的次设备号常为0)

  1. count:连续编号范围.是这组设备号的大小(也是次设备号的个数)
    (3)name:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称

(4)返回值:如果分配成功进行, register_chrdev_region 的返回值是 0

出错的情况下, 返回一个负的错误码.

 

/*功能:申请使用从first开始的count 个设备号(主设备号不变,次设备号增加)*/

注:要事先知道主次设备号,要先查看cat /proc/devices去查看没有使用的。

 

alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)——来让内核自动给我们分配设备号

(1)让内核给我们自动分配一个设备号,其存放于第一个参数dev中。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号 次设备号。 

(2)第二个参数:次设备号的基准,从第几个次设备号开始分配。第一个次设备号的号数常为0.

(3)第三个参数:次设备号的个数。

(4)第四个参数:驱动的名字。

(5)  返回值:成功:0      失败:负数(绝对值是错误码)

/*功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。*/

 

释放:(3驱动注销有讲)

void unregister_chrdev(unsigned int major,const char *name);

void unregister_chrdev_region(dev_t from,unsigned count);

 

 

1.2初始化cdev

初始化cdev:有两种方式定义初始化方式:

 

方式1:

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

cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接。如果追踪这个函数的实现可以发现,其实这个函数就是将fops赋给cdev->ops。

 

 

方式2:

返回值:成功 cdev 对象首地址    失败:NULL

 

该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即: .ops=xxx_ops).

 

如果我们定义了一个全局的 static struct cdev *pcdev; 我们就可以用 pcdev = cdev_alloc()来给这个pcdev分配堆内存空间cdev_del(pcdev)时会释放掉这个申请到的堆内存,cdev_del函数内部是能知道你的struct cdev定义的对象是用的堆内存还是栈内存还是数据段内存的。这个函数cdev_del调用时,会先去看你有没有使用堆内存,如果有用到的话,会先去释放掉你用的堆内存,然后在注销掉你这个设备驱动,防止内存泄漏。

 

注意:

1如果struct cdev要用堆内存一定要用内核提供的这个cdev_alloc去分配堆内存,因为内部会做记录,这样在cdev_del注销掉这个驱动的时候,才会去释放掉那段堆内存。

2两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。

 

 

1.3注册cdev

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

cdev_add()函数向系统添加一个 cdev,完成字符设备的注册。

cdev:需要注册的cdev

dev:第一个设备号 dev

count:和该设备关联的设备编号的数量。

返回值:成功:0      失败:负数(绝对值是错误码)

 

 

所谓注册设备就是告诉内核该结构的信息。在驱动中一旦调用这个函数成功,则我们的设备就“活”了,内核就能调用它的操作,因此我们的驱动在没准备好操作集(file_operations)之前,不要调用这个函数。

注销:

cdev_del:从内核中注销掉一个驱动。

 

 

1.4硬件初始化

硬件初始化主要是硬件资源的申请与配置。。。。。。

 

2实现设备的操作

file_operations结构体成员函数有很多个,下面就选几个常见的来展示:

int(*open)(struct inode *, struct file*);

 

ssize_t(*read)(struct file *, char __user*, size_t, loff_t*); /*用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值*/

 

ssize_t(*write)(struct file *, const char__user *, size_t, loff_t*);/*向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值*/

 

int(*release)(struct inode *, struct file*); /*关闭*/

。。。。。。。。。。。。。。。。。

 

_IOC_NR, _IOC_TYPE, _IOC_SIZE, _IOC_DIR......

_IO,_IOR,_IOW,_IOWR宏........

静态映射与动态映射原理分析.......

 

 

 

补充:

内核空间和用户空间是无法直接传递数据的,内核为驱动程序提供在内核空间和用户空间传递数据的方法:

用户空间-->内核空间

  1. copy_from_user函数
    unsigned long copy_from_user(void *to, const void *from, unsigned long n);
    to:目标地址(内核空间)
    from:源地址(用户空间)
    n:将要拷贝数据的字节数
    返回:成功返回0,失败返回没有拷贝成功的数据字节数
    (2)get_user宏
    int get_user(data, ptr);--------Get a simple variable from user space
    data:Variable to store result.

ptr:Source address, in user space.
返回:成功返回0,失败返回非0

+++++++++++++++++++++++++++++++++++++++++++++++++

 

内核空间-->用户空间
(1)copy_to_user函数
unsigned long copy_to_user(void *to, const void *from, unsigned long n)
to:目标地址(用户空间)
from:源地址(内核空间)
n:将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数
(2)put_user宏:
int put_user(data, prt)-----------Write a simple value into user space
data:Value to copy to user space.
ptr:Destination address, in user space.
返回:成功返回0, 失败返回非0

 

注:get_user和put_user这两个使用的数据只能是char,int ,long等简单的数据。如结构体,枚举等的数据则不能胜任。

 

 

 

3 驱动注销

3.1设备注销

void cdev_del(struct cdev *dev);

 

3.2 设备号注销

void unregister_chrdev(unsigned int major,const char *name);

-- 老版本字符设备注销函数,对应于register_chrdev() 

major   主设备号

name  设备文件

 

 

void unregister_chrdev_region(dev_t from, unsigned count);

eg:unregister_chrdev_region(MKDEV(major, 0), 1);

对应register_chrdev_region()、alloc_chrdev_region()

 

 

4.

//重要

module_init(first_drv_init);  //用于修饰入口函数

module_exit(first_drv_exit);  //用于修饰出口函数

 

//不重要,只是一些信息的注册

MODULE_AUTHOR("LWJ");

MODULE_DESCRIPTION("Just for Demon");

MODULE_LICENSE("GPL");  //遵循GPL协议

 

补充:

  1. 问:在应用程序中使用open("/dev/xxx",O_RDWR);是如何找到对应的驱动设备文件?

   答:关键在于设备节点的创建,其设备节点的创建方法如下一小节“创建设备节点”

eg:

 

 

 

6.创建设备节点

方法一:利用mknod命令手动创建设备节点。
方法二:自动创建设备节点:利用udev(mdev)设备文件系统来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。而在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。然后udev跟据class_create和device_create生成的信息创建设备节点。

原理:
        1 内核中定义了struct class结构体,它对应一个类。
        2 先调用class_create()函数,可以用它来创建一个类,这个类将存放于sys/class下面(查看#ls /sys/class).
        3 再调用device_create()函数,从而在/dev目录下创建相应的设备节点(查看#ls /dev)。
        4 卸载模块对应的函数是 device_destroy 和 class_destroy()

注:2.6 以后的版本使用device_create(),之前的版本使用的class_device_create()

 

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

功能:在/sys/class目录下创建一个目录,目录名是name指定的

参数:

struct module *owner - THIS_MODULE

const char *name - 设备名

返回值:

  成功:class指针

  失败: - bool IS_ERR(const void *ptr)  判断是否出错

  long PTR_ERR(const void *ptr)  转换错误码

 

void class_destroy(struct class *cls);

功能:删除class指针指向的目录

参数:struct class *cls - class指针

 

 

struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);    

功能:

在class指针指向的目录下再创建一个目录,目录名由const char *fmt, ...指出、并导出设备信息(dev_t)

参数:

struct class *cls - class指针

struct device *parent - 父对象,父设备,没有NULL

dev_t devt - 设备号

void *drvdata - 驱动私有数据

const char *fmt, ... - fmt是目录名字符串格式,...就是不定参数

返回值:成功 - device指针

    失败 - bool IS_ERR(const void *ptr)  判断是否出错

long PTR_ERR(const void *ptr)   转换错误码

 

 

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

功能:删除device_create创建的目录

参数:struct class *cls - class指针   注意:不是struct device *

   dev_t devt - 设备号

 

 

注:

cls = class_create(THIS_MODULE, "myclass");

test_device = device_create(cls,NULL,devno,NULL,"hello");

相当于mknod /dev/hello c主设备号 次设备号

 

 

下面可以看几个class几个名字的对应关系:

 

其他的信息:

 

 

 

7.在用户空间动态申请内存用的函数是 malloc(),这个函数在各种操作系统上的使用是一致的,对应的用户空间内存释放函数是 free()。注意:动态申请的内存使用完后必须要释放,否则会造成内存泄漏,如果内存泄漏发生在内核空间,则会造成系统崩溃。 
那么,在内核空间中如何申请内存呢?一般我们会用到 kmalloc()、kzalloc()、vmalloc() 等,下面我们介绍一下这些函数的使用以及它们之间的区别。

kmalloc:

函数原型:

void *kmalloc(size_t size, gfp_t flags);

kmalloc() 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。 

较常用的 flags(分配内存的方法):

GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;

GFP_KERNEL —— 正常分配内存;

GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。

flags 的参考用法: 
|– 进程上下文,可以睡眠 GFP_KERNEL 
|– 进程上下文,不可以睡眠 GFP_ATOMIC 
| |– 中断处理程序 GFP_ATOMIC 
| |– 软中断 GFP_ATOMIC 
| |– Tasklet GFP_ATOMIC 
|– 用于DMA的内存,可以睡眠 GFP_DMA | GFP_KERNEL 
|– 用于DMA的内存,不可以睡眠 GFP_DMA |GFP_ATOMIC 

对应的内存释放函数为:

void kfree(const void *objp);

kzalloc:

kzalloc() 函数与 kmalloc() 非常相似,参数及返回值是一样的,可以说是前者是后者的一个变种,因为 kzalloc() 实际上只是额外附加了__GFP_ZERO 标志。所以它除了申请内核内存外,还会对申请到的内存内容清零。

kzalloc() 对应的内存释放函数也是 kfree()。

vmalloc:

函数原型:

void *vmalloc(unsigned long size);

vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。

对应的内存释放函数为:

void vfree(const void *addr);

注意:vmalloc() 和 vfree() 可以睡眠,因此不能从中断上下文调用。

kmalloc()、kzalloc()、vmalloc() 的共同特点是:

用于申请内核空间的内存;

内存以字节为单位进行分配;

所分配的内存虚拟地址上连续;

kmalloc()、kzalloc()、vmalloc() 的区别是:

kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)

kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;

kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;

kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;

kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;

一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。例如,当模块被动态加载到内核当中时,就把模块装载到由 vmalloc() 分配的内存上。

 

函数devm_kzalloc()和kzalloc()一样都是内核内存分配函数,但是devm_kzalloc()是跟设备(装置)有关的,当设备(装置)被拆卸或者驱动(驱动程序)卸载(空载)时,内存会被自动释放。另外,当内存不在使用时,可以使用函数devm_kfree()释放。

而kzalloc()则需要手动释放(使用kfree()),但如果工程师检查不仔细,则有可能造成内存泄漏

 

8.linux驱动: 如何向模块传递参数, module_param和module_param_array

(1)module_param(name, type, perm);
name 既是用户看到的参数名,又是模块内接受参数的变量;
type 表示参数的数据类型,是下列之一:byte, short, ushort, int, uint, long, ulong, charp, bool, invbool;
perm 指定了在sysfs中相应文件的访问权限。访问权限与linux文件访问权限相同的方式管理,如0644,或使用stat.h中的宏如S_IRUGO表示。
0    表示完全关闭在sysfs中相对应的项。
#define S_IRUSR    00400 文件所有者可读
#define S_IWUSR    00200 文件所有者可写
#define S_IXUSR    00100 文件所有者可执行
#define S_IRGRP    00040 与文件所有者同组的用户可读
#define S_IWGRP    00020
#define S_IXGRP    00010
 #define S_IROTH    00004 与文件所有者不同组的用户可读
 #define S_IWOTH    00002
#define S_IXOTH    00001
            

这些宏不会声明变量,因此在使用宏之前,必须声明变量,典型地用法如下:
static unsigned int int_var = 0;
module_param(int_var, uint, S_IRUGO);
     insmod xxxx.ko int_var=x

    
  (2)传递多个参数可以通过宏 module_param_array(para , type , &n_para , perm) 实现。
para:既是外部模块的参数名又是程序内部的变量名

type是数据类型,perm是sysfs的访问权限。

指针nump指向一个整数,其值表示有多少个参数存放在数组para中。
para:参数数组; 数组的大小才是决定能输入多少个参数的决定因素.n_para:参数个数; 这个变量其实无决定性作用;只要para数组大小够大,在插入模块的时候,输入的参数个数会改变n_para的值,最终传递数组元素个数存在n_para中.

典型地用法如下:
static int para[MAX_FISH];
static int n_para;
module_param_array(para , int , &n_para , S_IRUGO); 

 

 

 

错误补充:

这种错误只要把'class_device_create'改成'device_create','class_device_destroy'改成'device_destroy'一般就可以正确通过编译了。这主要是由内核版本决定的。

 

 

 

 

 

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值