• Linux字符设备驱动基础知识


    Linux字符设备驱动基础知识


      在Linux设备驱动中,字符设备驱动较为基础,字符设备即一个一个的字节,按照字节流进行读写操作的设备,读写数据具有一定的先后顺序,例如I2C、SPI、LCD等都属于字符设备。

      在Linux中一切皆文件,设备驱动加载成功后会在/dev目录下生成相应的文件,应用程序通过对这个名为、dev/xxx(xxx为具体设备驱动的名称)的文件进行相应操作即可实现对硬件的操作。

    cdev 结构体

      在字符设备驱动程序的管理核心是字符设备,内核为字符设备驱动抽象出了一个具体的数据结构 struct cdev 定义如下:

    \include\linux\cdev.h
    /*
     *  字符设备的内核抽象
     */
    struct cdev {
        struct kobject kobj;                /* 内嵌的内核(kobject)对象 */
        struct module *owner;               /* 所属模块在内核的对象指针 */
        const struct file_operations *ops;  /* 文件操作结构体,用于实现与硬件的一系列操作 */
        struct list_head list;              /* 用来将已经向内核注册的所有字符设备形成链接 */
        dev_t dev;                          /* 设备号 */
        unsigned int count;                 /* 隶属于同一设备号的次设备号的个数 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

      cdev结构体的dev_t成员定义设备号,为了方便管理,Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,共为32位,其中高12位为主设备号,低20位为次设备号,因此Linux系统中主设备号范围为0-4095。

      主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

      可使用以下宏对设备号进行操作:

    \include\linux\kdev_t.h
    #define MINORBITS	20
    #define MINORMASK	((1U << MINORBITS) - 1)
    
    #define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))       /* 获取主设备号 */
    #define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))        /* 获取次设备号 */
    #define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))            /* 使用主设备号和次设备号生成dev_t */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    操作cdev的相关函数

    \include\linux\cdev.h
    /*
     * 用于初始化cdev的成员,并建立cdev与file_operations之间的连接
     */
    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    {
    	memset(cdev, 0, sizeof *cdev);
    	INIT_LIST_HEAD(&cdev->list);
    	kobject_init(&cdev->kobj, &ktype_cdev_default);
    	cdev->ops = fops;       /* 将传入的文件操作结构体指针赋值给cdev的ops成员 */
    }
    /*
     * 动态申请一个cdev结构体内存
     */
    struct cdev *cdev_alloc(void)
    {
    	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
    	if (p) {
    		INIT_LIST_HEAD(&p->list);
    		kobject_init(&p->kobj, &ktype_cdev_dynamic);
    	}
    	return p;
    }
    
    /*
     * 释放一个cdev
     */
    void cdev_put(struct cdev *p)
    {
    	if (p) {
    		struct module *owner = p->owner;
    		kobject_put(&p->kobj);
    		module_put(owner);
    	}
    }
    
    /*
     * 向系统注册一个字符设备
     */
    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    {
    	int error;
    
    	p->dev = dev;
    	p->count = count;
    
    	error = kobj_map(cdev_map, dev, count, NULL,
    			 exact_match, exact_lock, p);
    	if (error)
    		return error;
    
    	kobject_get(p->kobj.parent);
    
    	return 0;
    }
    
    /*
     * 删除一个字符设备
     */
    void cdev_del(struct cdev *p)
    {
    	cdev_unmap(p->dev, p->count);
    	kobject_put(&p->kobj);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    分配和释放设备号

      在调用cdev_add()函数向内核注册字符设备之前,需要先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号。

    \fs\char_dev.c
    /*
     * 对已知起始设备的设备号申请设备号
     */
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    
    /*
     * 起始设备的设备号未知时,向内核动态申请未被占用的设备号
     */
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
    			const char *name);
    /*
     * 释放之前申请的设备号
     */
    void unregister_chrdev_region(dev_t from, unsigned count);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    file_operations 结构体

      file_operation是将Linux系统调用和驱动程序关联起来的关键数据结构。这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

    \include\linux\fs.h
    /*
     * 文件操作结构体,用于实现与硬件的一系列操作
     */
    struct file_operations {
    	struct module *owner;
    	loff_t (*llseek) (struct file *, loff_t, int);
    	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    	int (*iterate) (struct file *, struct dir_context *);
    	unsigned int (*poll) (struct file *, struct poll_table_struct *);
    	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    	int (*mmap) (struct file *, struct vm_area_struct *);
    	int (*mremap)(struct file *, struct vm_area_struct *);
    	int (*open) (struct inode *, struct file *);
    	int (*flush) (struct file *, fl_owner_t id);
    	int (*release) (struct inode *, struct file *);
    	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    	int (*aio_fsync) (struct kiocb *, int datasync);
    	int (*fasync) (int, struct file *, int);
    	int (*lock) (struct file *, int, struct file_lock *);
    	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    	int (*check_flags)(int);
    	int (*flock) (struct file *, int, struct file_lock *);
    	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    	int (*setlease)(struct file *, long, struct file_lock **, void **);
    	long (*fallocate)(struct file *file, int mode, loff_t offset,
    			  loff_t len);
    	void (*show_fdinfo)(struct seq_file *m, struct file *f);
    #ifndef CONFIG_MMU
    	unsigned (*mmap_capabilities)(struct file *);
    #endif
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    结构体常用成员的主要含义

    • struct module *owner

    功能:owner并不是一个操作,它是指向拥有这个结构的模块的指针,主要作用是在设备还在被使用时阻止其被卸载,一般为 THIS_MODULE。定义在<linux/module.h>中的宏。

    • loff_t (*llseek) (struct file *, loff_t, int)

    功能: 用于改变文件的当前读/写位置,并将新位置返回,出错时,返回一个负值。


    参数1: 指针指向进行读取目标文件的结构体;


    参数2: 文件定位的目标偏移量。


    参数3: 对文件定位的起始地址,这个值可以为文件开头(SEEK_SET)、当前位置(SEEK_CUR)、文件末尾(SEEK_END)。

    • ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)

    功能: 用于从设备文件中获取数据,成功时返回读取的字节数,出错时返回一个负值。(此操作为阻塞操作)


    参数1: 为进行读取信息的目标文件;


    参数2: 为对应放置信息的缓冲区(用户空间内存地址);


    参数3: 要读取的信息长度;


    参数4: 读取位置相对于文件开头的偏移,读取信息后,指针会移动读取信息的长度值的距离;

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

    功能: 用于向设备文件写入数据,成功时返回写入的字节数,若用户未实现此函数的回调函数,用户运行write()时将得到-EINVAL返回值。(此操作为阻塞操作)


    参数1: 为目标文件结构体指针;


    参数2: 为要写入文件的信息缓冲区;


    参数3: 为要写如信息的长度;


    参数4: 为当前的偏移位置,这个值通常是用来判断写文件是否越界;

    • unsigned int (*poll) (struct file *, struct poll_table_struct *)

    功能: 轮询函数,用于查询设备是否能够进行非阻塞读写,函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。


    参数1: 为文件对象结构指针;


    参数2: 轮询表指针。

    • long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)

    功能: 用于提供对设备的控制功能,与应用程序的ioctl函数对应。

    • long (*compat_ioctl) (struct file *, unsigned int, unsigned long)

    功能: 与unlocked_ioctl功能类似,区别在与64位系统会调用此函数,32位系统调用unlocked_ioctl。

    • int (*mmap) (struct file *, struct vm_area_struct *)

    功能: 用于将设备内存映射到进程空间(用户空间)。


    参数1: 为文件对象结构指针;


    参数2: 为进程地址空间。

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

    功能: 打开设备文件,与release文件对应。


    参数1: 为文件索引节点,文件索引节点只有一个,无论用户打开多少个文件,都只对应一个inode结构;


    参数2: 为文件对象结构体,只要打开一个文件,就对应一个file结构体,file结构体通常用于追踪文件在运行时的状态信息。

    • int (*release) (struct inode *, struct file *)

    功能: 用于释放(关闭)设备文件,常与应用程序的close函数对应。


    参数1: 同open;


    参数2: 同open。

    • int (*fasync) (int, struct file *, int)

    功能: 用于刷新待处理的数据,类似于将内存缓冲区的数据刷新至磁盘内,允许进程把所有的脏缓冲区刷新到磁盘。

    • int (*aio_fsync) (struct kiocb *, int datasync)

    功能: 与fasync类似,aio_fsync用于异步刷新。

    字符设备驱动开发

    设备驱动的加载与卸载

      linux设备驱动一般有两种方式运行,一种是将驱动跟内核一起编译,此时linux将会自动加载运行;另外一种是将驱动程序编译为模块(.ko文件)后,使用驱动加载命令 insmod 将相应的模块加载即可。

      在这里一般会使用两个宏

    \include\linux\init.h
    module_init(x)
    module_exit(x)
    
    • 1
    • 2
    • 3

       module_init 宏用于向linux内核注册一个模块的加载函数,参数x为需要注册的具体函数。在命令行调用 insmod 命令时,x将会被调用。

       module_exit 宏用于向linux内核卸载一个模块,参数为具体需要卸载的函数。在命令行调用 rmmod 命令时,x将会被调用。

    相关加载卸载命令

    • 加载驱动模块命令:
    insmod xxx.ko
    
    • 1

      insmod命令不能解决模块的依赖关系,即:a.ko依赖于b.ko模块,必须使用 insmod 命令加载b.ko模块,然后再加载a.ko模块。

    • 驱动卸载命令:
    rmmod xxx.ko
    modprobe -r xxx.ko
    
    • 1
    • 2

    字符设备注册注销

      对字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。同样,卸载驱动时也需要注销字符设备。相应函数如下:

    \include\linux\fs.h
    /*
     * 字符设备驱动注册
     */
    static inline int register_chrdev(unsigned int major, const char *name,
    				  const struct file_operations *fops)
    {
    	return __register_chrdev(major, 0, 256, name, fops);
    }
    
    /*
     * 字符设备驱动卸载
     */
    static inline void unregister_chrdev(unsigned int major, const char *name)
    {
    	__unregister_chrdev(major, 0, 256, name);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    当前设备被用掉的设备号查看命令 cat /proc/devices

    创建设备节点文件命令

      驱动加载成功之后需要在 /dev 目录下创建一个与之对应的设备节点文件,应用程序通过操作这个节点文件来完成对具体设备的操作。

    /* 创建chrdev设备节点文件,c表示字符设备,a为主设备号,与/proc/devices下的加载的设备号一致,b为次设备号 */
    mknod /dev/chrdev c a b
    
    • 1
    • 2

    分配和释放设备号

    动态分配设备号

    \fs\char_dev.c
    /*
     * 分配设备号
     */
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
    			const char *name)
    {
    	struct char_device_struct *cd;
    	cd = __register_chrdev_region(0, baseminor, count, name);
    	if (IS_ERR(cd))
    		return PTR_ERR(cd);
    	*dev = MKDEV(cd->major, cd->baseminor);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    创建设备

    \driver\base\core.c
    /* 
     * 创建设备
     */
    struct device *device_create(struct class *class, struct device *parent,
    			     dev_t devt, void *drvdata, const char *fmt, ...)
    {
    	va_list vargs;
    	struct device *dev;
    
    	va_start(vargs, fmt);
    	dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
    	va_end(vargs);
    	return dev;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    创建类

    \linux\devices.h
    /*
     * 创建类
     */
    #define class_create(owner, name)		\
    ({						\
    	static struct lock_class_key __key;	\
    	__class_create(owner, name, &__key);	\
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    参考

    《Linux设备驱动开发详解 基于最新的Linux4.0内核.pdf》

    《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf》

    🎃

    🎐

    🎨

  • 相关阅读:
    JVM虚拟机学习笔记之-5. 字节码执行引擎
    使用appscan定时批量扫描方法
    mock+封装axios+vuex分发
    为何你的算法总是比别人的慢?【21天算法系列】之顺序查找算法【Java 版】
    TypeScript高级类型
    Linux编程:获取时间戳
    Git 服务器搭建
    新功能&案例分享丨DolphinDB 与你相约上海,报名限时开放!
    膜拜大神,把微服务讲的又全又好的SpringCloud架构进阶
    设计模式之迭代器模式
  • 原文地址:https://blog.csdn.net/qq_41596356/article/details/125385456