• 企业400电话
  • 网络优化推广
  • AI电话机器人
  • 呼叫中心
  • 全 部 栏 目

    网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    Linux内核设备驱动之字符设备驱动笔记整理
    POST TIME:2021-10-16 17:44
    /********************
     * 字符设备驱动
     ********************/

    (1)字符设备驱动介绍

    字符设备是指那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动。

    此类驱动适合于大多数简单的硬件设备。比如并口打印机,我们通过在/dev下建立一个设备文件(如/dev/printer)来访问它。

    用户应用程序用标准的open函数打开dev/printer,然后用write向文件中写入数据,用read从里面读数据。

    调用流程:

    所谓驱动,就是提供最后的write函数,通过访问打印机硬件的寄存器直接和打印机对话

    (2)主设备号和次设备号

    a.设备编号介绍

    对字符设备的访问是通过文件系统内的设备文件进行的。这些文件位于/dev。用"ls -l"查看。

    设备通过设备号来标识。设备号分两部分,主设备号和次设备号。

    通常,主设备号标示设备对应的驱动程序,linux允许多个驱动共用一个主设备号;

    而次设备号用于确定设备文件所指的设备。

    在内核中,用dev_t类型<linux/types.h>保存设备编号。

    2.4内核中采用16位设备号(8位主,8位从),而2.6采用32位,12位主,20位从。

    在驱动中访问设备号应该用<linux/kdev_t.h>中定义的宏。

    获取设备号:

    b.分配和释放设备编号

    在建立一个字符设备前,驱动需要先获得设备编号。

    分配:

    #include <linux/fs.h>
    int register_chrdev_region(dev_t first, unsigned int count, char *name);
    //first:要分配的设备编号范围的起始值(次设备号常设为0)
    //count: 所请求的连续编号范围
    //name: 和编号关联的设备名称(见/proc/devices)

    也可以要求内核动态分配:

    int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
    //firstminor: 通常为0
    //*dev: 存放内核返回的设备号

    释放:

    void unregister_chrdev_region(dev_t first, unsigned int count);
    //在模块的清除函数中调用

    在Documentation/devices.txt中可以找到内核已经分配的设备号。

    c.建立设备文件

    当设备驱动模块向系统申请了主设备号和次设备号,并且已经通过insmod加载到内核中后,我们就可以通过在/dev下创建设备文件来访问这个设备了。

    字符设备的创建:$>mknod /dev/mychar c major minor

    我们在驱动中常常采用动态分配主次设备号的方法,这样不会和系统中已有的设备号冲突。

    动态分配时,/dev下的设备文件也需要通过分析/proc/devices动态建立。

    见char_load和char_unload脚本。

    (3)字符设备的基本数据结构

    和字符设备驱动关系最紧密的3个基本的数据结构是:file, file_oepeations和inode

    a.file_operations数据结构

    结构中包含了若干函数指针。这些函数就是实际和硬件打交道的函数。

    用户空间调用的open,write等函数最终会调用这里面的指针所指向的函数。每个打开的文件和一组函数关联。

    见<linux/fs.h>和驱动书的p54

    2.6内核结构的初始化:

    struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .llseek = my_llseek,
    .read = my_read,
    .write = my_write,
    .ioctl = my_ioctl,
    .open = my_open,
    .release = my_release,
    }

    2.4内核结构的初始化:

    struct file_operations my_fops = {
    owner: THIS_MODULE,
    llseek: my_llseek,
    ...
    }

    b.file结构<linux/fs.h>

    file是一个内核结构体,实际上和用户open文件后返回的文件描述符fd对应。

    file结构代表一个打开的文件,系统中每个打开的文件在内核空间都有一个对应的file结构。

    它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数,在文件的所有实例都被关闭后,内核会释放这个结构。

    用户空间进程fork一个新进程后,新老进程会共享打开的文件描述符fd,这个操作不会在内核空间创建新的file结构,只会增加已创建file结构的计数。

    见<linux/fs.h>

    mode_t f_mode;  通过FMODE_READ和FMODE_WRITE标示文件是否可读或可写。

    loff_t f_pos;  当前的读写位置,loff_t为64位

    unsigned int f_flags;  文件标志,如O_RDONLY, O_NONBLOCK, O_SYNC。标志都定义在<linux/fcntl.h>

    struct file_operations *f_op;  与文件相关的操作。内核在执行open时对这个指针赋值。可以在驱动的open方法中根据次设备号赋予不同的f_op

    void *private;  通常将表示硬件设备的结构体赋给private.

    struct dentry *f_dentry;  文件对应的目录项(dentry)结构。可通过filp->f_dentry->d_inode访问索引节点。

    file中其他的内容和驱动关系不大。

    c.inode结构

    内核用inode结构表示一个实际的文件,可以是一个普通的文件,也可以是一个设备文件。

    每个文件只有一个inode结构,而和文件描述符对应的file结构可以有多个(多次进行open调用)。这些file都指向同一个inode。

    inode定义在<linux/fs.h>

    dev_t i_rdev;  对于表示设备文件的inode结构,i_rdev里包含了真正的设备编号

    struct cdev *i_cdev  cdev是表示字符设备的内核的内部结构。当inode表示一个字符设备时,i_cdev指向内核中的struct cdev.

    其他结构和设备驱动关系不大。

    用如下宏从inode获取设备号:

    (4)字符设备的注册

    内核内部使用struct cdev结构来表示一个字符设备。

    我们的驱动要把自己的cdev注册到内核中去。见 <linux/cdev.h>

    a.通常在设备的结构中加入cdev

    struct scull_dev{
    ...
    struct cdev cdev; /* 字符设备结构 */
    }

    b.初始化

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

    c.设定cdev中的内容

    d.向内核添加设定好的cdev

    int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
    //num: 设备对应的第一个编号
    //count: 和设备关联的设备编号的数量,常取1
    //一旦cdev_add返回,内核就认为设备可以使用了,所以要在调用之前完成设备的硬件初始化。

    (5)老式的注册函数

    2.4中的老式注册函数仍然在驱动函数中大量存在,但新的代码不应该使用这些代码。

    注册:

    int register_chrdev(unsigned int major,
      const char *name,
      struct file_operations *fops);
    //为给定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构

    注销:

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

    (6)open和release

    a.open

    在驱动的open方法中完成设备的初始化工作,open完成后,硬件就可以使用,用户程序可以通过write等访问设备,open的工作有:

    open原型;

    int (*open) (struct inode *inode, struct file *filp);
    //在open中通过inode获得dev指针,并将其赋给file->private_data
    //struct scull_dev *dev;
    //dev = contain_of(inode->i_cdev, struct scull_dev, cdev);
    //filp->private_data = dev;
    //(如果dev是静态分配的,则在open或write等方法中可以直接访问dev,但如果dev是在module_init时动态分配的,则只能通过上面的方法获得其指针)

    b.release

    并不是每个close调用都会引起对release方法的调用,只有当file的计数器归零时,才会调用release,从而释放dev结构)

    (7)read和write

    read和write的工作是从用户空间拷贝数据到内核,或是将内核数据拷贝到用户空间。其原型为:

    ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
    ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
    //buff: 用户空间的缓冲区指针
    //offp: 用户在文件中进行存取操作的位置
    //在read和write中,拷贝完数据后,应该更新offp,并将实际完成的拷贝字节数返回。

    (8)和用户空间交换数据

    read和write中的__user *buff 是用户空间的指针,内核不能直接引用其中的内容(也就是不能直接对buff进行取值操作),需要通过内核提供的函数进行数据拷贝。其原因是:

    内核和用户空间交换数据的函数见<asm/uaccess.h>

    如:

    1. unsigned long copy_to_user(
          void __user *to, 
          const void *from, 
          unsigned long count);
    //向用户空间拷贝数据

    2. unsigned long copy_from_user(
          void *to, 
          const void __user *from, 
          unsigned long count);
    //从用户空间获得数据

    3. int put_user(datum, ptr)
    //向用户空间拷贝数据。字节数由sizeof(*ptr)决定
    //返回值为0成功,为负错误。

    4. int get_user(local, ptr);
    //从用户空间获得数据。字节数由sizeof(*ptr)决定
    //返回值和local都是从用户空间获得的数据

    任何访问用户空间的函数都必须是可睡眠的,这些函数需要可重入。

    copy_to_user等函数如果返回值不等于0,则read或write应向用户空间返回-EFAULT

    主设备号用来表示设备驱动, 次设备号表示使用该驱动的设备

    在内核dev_t 表示设备号, 设备号由主设备号和次设备号组成

    #include <linux/kdev_t.h>
    #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //根据设备号获取主设备号
    #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //获取次设备号
    #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))     //根据指定的主设备和次设备号生成设备号
    #include <linux/fs.h> 
    //静态:申请指定的设备号, from指设备号, count指使用该驱动有多少个设备(次设备号), 设备名 
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    //name的长度不能超过64字节 
    //动态申请设备号, 由内核分配没有使用的主设备号, 分配好的设备存在dev, baseminor指次设备号从多少开始, count指设备数, name设备名 
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
    const char *name)
    //释放设备号, from指设备号, count指设备数
    void unregister_chrdev_region(dev_t from, unsigned count)
    //cat /proc/devices 可查看设备使用情况
    在内核源码的documentations/devices.txt可查看设备号的静态分配情况
    ///内核里使用struct cdev来描述一个字符设备驱动 
    #include <linux/cdev.h>
    struct cdev {
    struct kobject kobj;    //内核用于管理字符设备驱动 
    struct module *owner;   //通常设为THIS_MODULE, 用于防止驱动在使用中时卸载驱动模块
    const struct file_operations *ops; //怎样操作(vfs)
    struct list_head list;   //因多个设备可以使用同一个驱动, 用链表来记录
    dev_t dev;         //设备号
    unsigned int count;    //设备数
    };

    ////////字符设备驱动//////////

    1. 申请设备号

    2. 定义一个cdev的设备驱动对象

    struct cdev mycdev; 
    //定义一个file_operations的文件操作对象
    struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = 读函数
    ....
    };

    3. 把fops对象与mycdev关联起来

    cdev_init(&mycdev, &fops); //mycdev.ops = &fops;
    mycdev.owner = THIS_MODULE; 

    4. 把设备驱动加入内核里, 并指定该驱动对应的设备号

    cdev_add(&mycdev, 设备号, 次设备号的个数);

    5. 卸载模块时, 要把设备驱动从内核里移除, 并把设备号反注册

    cdev_del(&mycdev);
    ///////////创建设备文件
    mknod /dev/设备文件名 c 主设备号 次设备号
    ////////inode节点对象描述一个文件/设备文件, 包括权限,设备号等信息
    struct inode {
    ...
    dev_t i_rdev;   //设备文件对应的设备号
    struct cdev *i_cdev; //指向对应的设备驱动对象的地址
    ...
    };
    ////file对象描述文件描述符, 在文件打开时创建, 关闭时销毁
    struct file {
    ...
    const struct file_operations *f_op; //对应的文件操作对象的地址
    unsigned int f_flags; //文件打开的标志
    fmode_t f_mode; //权限
    loff_t f_pos;  //文件描述符的偏移
    struct fown_struct f_owner; //属于哪个进程
    unsigned int f_uid, f_gid; 
    void *private_data; //给驱动程序员使用
    ...
    };

    通file里的成员f_path.dentry->d_inode->i_rdev可以获取到设备文件的设备号

    ///错误码在<asm/errno.h> ////

    /////////struct file_operations ////

    inode表示应用程序打开的文件的节点对象,  file表示打开文件获取到的文件描述符

    成功返回0, 失败返回错误码

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

    buf指向用户进程里的缓冲区, len表示buf的大小(由用户调用read时传进来的)

    off表示fl文件描述符的操作偏移, 返回值为实际给用户的数据字节数.

    ssize_t (*read) (struct file *fl, char __user *buf, size_t len, loff_t *off);

    用户进程把数据给驱动

    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    to指用户进程的缓冲区, from指驱动里装数据的缓冲区, n多少字节, 返回值是0

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

    to指驱动的...   from用户...    n多少字节, ....

    static inline unsigned long __must_check copy_to_user(void __user *to, const
    void *from, unsigned long n)
    {
    if (access_ok(VERIFY_WRITE, to, n))
    n = __copy_to_user(to, from, n);
    return n; //返回值为剩下多少字节没拷贝
    }
    extern inline long copy_from_user(void *to, const void __user *from, long n)
    ///////////
    ///动态申请内存, 并清零. size为申请多大(不要超过128K),
    //flags为标志(常为GFP_KERNEL). 成功返回地址, 失败返回NULL
    // GFP_ATOMIC, 使用系统的内存紧急池
    void *kmalloc(size_t size, gfp_t flags);//申请后要内存要清零
    void *kzalloc(size_t size, gfp_t flags); //申请出来的内存已清零
    void kfree(const void *objp); //回收kmalloc/kzalloc的内存
    void *vmalloc(unsigned long size); //申请大内存空间
    void vfree(const void *addr); //回收vmalloc的内存
    // kmalloc申请出来的内存是物理地址连续的, vmalloc不一定是连续的
    ///// container_of(ptr, type, member) type包括member成员的结构体,
    //ptr是type类型 结构体的member成员的地址.
    //此宏根据结构体成员的地址获取结构体变量的首地址
    #define container_of(ptr, type, member) ({ \
    
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \
    
    (type *)( (char *)__mptr - offsetof(type,member) );})
    #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
     15 typedef struct led_dev_t {
     16     dev_t mydevid;
     17     unsigned int *rLEDCON;
     18     unsigned int *rLEDDAT;
     19     struct cdev mycdev;
     20 }LED_DEV;
     LED_DEV myled;
     //ind->i_cdev是指向myled.mycdev成员的地址
     //结构体变量myled首地址可由container_of(ind->i_cdev, LED_DEV, mycdev)获取;

    /////// 自动创建设备文件 ////

    #include <linux/device.h>

    1.  

    struct class *cl; 
    cl = class_create(owner, name) ; //owner指属于哪个模块, name类名
    //创建出来后可以查看 /sys/class/类名
    void class_destroy(struct class *cls); //用于销毁创建出来的类

    2. 创建设备文件

    struct device *device_create(struct class *cls, struct device *parent,
      dev_t devt, void *drvdata,
      const char *fmt, ...)
      __attribute__((format(printf, 5, 6)));
    device_create(所属的类, NULL, 设备号, NULL, "mydev%d", 88); //在/dev/目录下产生名字为mydev88的设备文件
    void device_destroy(struct class *cls, dev_t devt); //用于销毁创建出来的设备文件
    ////////
    int register_chrdev(unsigned int major, const char *name,
      const struct file_operations *fops) ; //注册设备号并创建驱动对象
    void unregister_chrdev(unsigned int major, const char *name); //反注册设备号并删除驱动对象
    static inline int register_chrdev(unsigned int major, const char *name,
     const struct file_operations *fops)
    {
    return __register_chrdev(major, 0, 256, name, fops);
    }
    int __register_chrdev(unsigned int major, unsigned int baseminor,
       unsigned int count, const char *name,
       const struct file_operations *fops)
    {
    struct char_device_struct *cd;
    struct cdev *cdev;
    int err = -ENOMEM;
    cd = __register_chrdev_region(major, baseminor, count, name);
    if (IS_ERR(cd))
    return PTR_ERR(cd);
    cdev = cdev_alloc();
    if (!cdev)
    goto out2;
    cdev->owner = fops->owner;
    cdev->ops = fops;
    kobject_set_name(&cdev->kobj, "%s", name);
    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
    if (err)
    goto out;
    cd->cdev = cdev;
    return major ? 0 : cd->major;
    out:
    kobject_put(&cdev->kobj);
    out2:
    kfree(__unregister_chrdev_region(cd->major, baseminor, count));
    return err;
    }

    总结

    以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。如果你想了解更多相关内容请查看下面相关链接

    上一篇:Linux内核设备驱动之虚拟文件系统笔记整理
    下一篇:Linux内核设备驱动之内核的时间管理笔记整理
  • 相关文章
  • 

    关于我们 | 付款方式 | 荣誉资质 | 业务提交 | 代理合作


    © 2016-2020 巨人网络通讯

    时间:9:00-21:00 (节假日不休)

    地址:江苏信息产业基地11号楼四层

    《增值电信业务经营许可证》 苏B2-20120278

    X

    截屏,微信识别二维码

    微信号:veteran88

    (点击微信号复制,添加好友)

     打开微信