字符设备驱动框架讲解

哈哈哈哈我的号又活啦~~~距上一篇博客已经过去两年半了QAQ...

中间毕业之后参加工作,暂时脱离码农的世界近两年,全世界跑了跑,发现还是敲代码好玩哈哈哈哈啊哈哈~~

现在从事芯片底层开发,类似于微码...最近给新员工将东西,比较基础,也在这里贴出来,表示我又回到咱码农行列啦~~~

 

废话少说,先上图:

 

 

1、cdev结构体

struct cdev {

    struct kobject kobj;

    struct module *owner;

    struct file_operations *ops;

    struct list_head list;

    dev_t dev;

    unsigned int count;

};

简单介绍几个重要成员:

1.1 dev_t:设备号,为32位,其中12位为主设备号,20位为次设备号;

    使用宏MAJOR(dev_t dev)和MINOR(dev_t dev)来获取主设备号和次设备号;

    使用MKDEV(int major, int minor)来通过主设备号和次设备号生成dev_t;

1.2 file_operations:字符设备驱动提供给虚拟文件系统的接口函数;

 

Linux内核提供了一组函数来操作cdev结构体:

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

    struct cdev *cdev_alloc(void);

    void cdev_put(struct cdev *p);

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

    void cdev_del(struct cdev *);

cdev_init用于初始化cdev的成员,并建立cdev和file_operations之间的连接,所以我们通过ioctl能够准确调用到对应字符设备并进行其他操作;

cdev_alloc用于动态申请一个cdev内存;

cdev_add和cdev_del分别向系统添加和删除一个cdev,完成字符设备的注册和注销;因为cde_add通常发生在字符设备驱动模块加载函数中,cdev_del函数通常发生在字符设备驱动模块卸载函数中;

 

2、分配和释放设备号

    在调用cdev_add函数向系统注册字符设备之前,应该首先调用register_chrdev_region()或者alloc_chrdev_region()向系统申请设备号,Resister函数用于在已知起始设备的设备号的情况,而alloc用于设备号未知,向系统动态申请未被占用设备号的情况。alloc相对于register的优点是它会自动避开设备号重复的冲突。

    在cdev_del函数从系统注销字符设备之后,unregister_chrdev_region应该被调用用以释放原先申请的设备号;

 

3、file_operations结构体

    该结构体中的成员函数是字符设备驱动程序设计的主体内容,结构体目前已经比较庞大,它的定义这里就不细看了,简单介绍几个主要成员函数:

    read函数用来从设备中读取数据,成功是返回读取的字节数,出错时返回负值,这个与用户空间的ssize_t read和size_t fread对应。

    write函数用来从设备中发送数据,成功是返回写入的字节。如果此函数未被实现,当用户进行write操作的时候。将得到一个-EINVAL返回值,这个与用户空间的ssize_t write和size_t write对应。

    unlocked_ioctl提供设备相关控制命令的实现(非读写),当调用成功是,返回一个非负值。

    在内核空间与用户空间的界面处,内核检查用户空间缓冲区的合法性显得尤为重要,非法侵入者可以伪造一片内核空间的缓冲区地址传入系统调用的接口,让内核对这个evil指针指向的内核空间进行填充数据。

 

4、ioctl函数

    ioctl一般对应的是file_operations结构体的unlocked_ioctl成员函数,三个入参:filp,cmd和arg,其中filp指的是文件结构体指针,cmd是设备支持的命令,arg指的是涉及到用户数据的参数。

    重点看一下cmd的组成,Linux建议I/O控制命令由设备类型、序列号、方向和数据尺寸组成,如:

 

 设备类型  序列号  方向  数据尺寸
 8位 8位  2位  14位 

    设备类型字段为一个“幻数”,可以使0x0 ~ 0xff的值,新设备驱动定义的幻数要避免与已使用的冲突。

 

    命令码的序列号也是8位宽,这个通常是定义给某一驱动设备支持的命令,如:0x0 0x1 0x2...

    方向字段为2位,该字段表示数据的传送方向,可能的值为:_IOC_NONE(无数据传输),_IOC_READ(读),_IOC_WRITE(写)和_IOC_READ|_IOC_WRITE(双向),数据传送的方向是从应用程序的角度来看的。

    数据长度字段表示涉及的用户数据的大小,(用于用户数据的输入与输出)。

    内核定义了_IO(),_IOR(),_IOW()和_IOWR()这4个宏来辅助生成命令,定义如下:

    #define _IO(type, nr) _IOC(NONE, (type), (nr), 0)

    #define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), (_IOC_TYPECHECK(size)))

    #define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))

    #define _IOWR(type, nr, size)  _IOC(_IOC_READ | _IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))

    这几个宏的作用是根据传入的type, nr, size和宏名隐藏的方向来组合生成命令码。

    通过这些宏来声明设备ioctl命令的好处在于:避免以单纯的数字作为命令的时候容易出现不同设备使用相同命令而产生命令码污染;在通过ioctl进入内核之后可以通过_IOC_TYPE(), _IOC_SIZE(), _IOC_NR()对用户传入的命令的设备类型、用户数据尺寸和命令序列号等信息进行校验,及时拦截非法请求;

 

    关于文件结构体file的私有数据private_data的使用,后续再讲,实际上,在大多数Linux驱动中遵循一个潜规则:将文件的私有数据private_data指向设备结构体,再在访问函数中通过private_data来访问设备结构体,这种做法在某一类驱动不知包括一个设备,而是包括两个及其以上的设备的时候,采用private_data的优势就显现出来了。

 

OK 现在再回头看看刚才那张图,是不是理解了一些呢?多多对比已有驱动代码,就会更加深刻理解的,linux驱动万变不离其宗~~

 

posted @ 2019-12-24 15:27  001010  阅读(914)  评论(0编辑  收藏  举报