字符设备

字符设备:指只能一个byte一个byte读写的设备,不能随机读写数据,要按先后顺序。字符设备是面向流的设备,常见字符设备有鼠标、键盘、串口、终端、LED灯。

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

每个字符设备或块设备,都在/dev目录下有一个对应的设备文件。Linux APP可以通过这些设备文件(又称设备节点),来使用驱动程序操作字符设备和块设备。

字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系:

Linux驱动(Linux字符设备驱动)(1)

from Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析 | CSDN

Linux内核中,

Linux字符设备驱动中,

TIPS: register_chrdev 与 register_chrdev_region, alloc_chrdev_region有何区别?

register_chrdev 设备注册 设备号申请。register_chrdev_region和alloc_chrdev_region 设备号申请,设备注册由cdev_init cdev_add完成。register_chrdev() 支持一次注册一个设备,而且需要传入参数file_operations。默认写死注册的设备号范围0~255。释放字符设备时,使用unregister_chrdev()。但不必使用cdev_xxx系列操作。register_chrdev_region() 支持一次注册多个设备号,不需要传入参数file_operations,在cdev_init()中绑定cdev与file_operations。释放设备号时,使用unregister_chrdev_region。register_chrdev_region需要搭配cdev_xxx系列操作使用。alloc_chrdev_region() 与register_chrdev_region()的区别在于前者申请的设备号由系统决定,后者由调用者指定。

APP中访问设备驱动程序,


Linux字符设备驱动结构cdev结构体

Linux内核中,使用cdev结构体描述一个字符设备。cdev定义:

#include <linux/cdev.h> struct cdev { struct kobject kobj; /* 内嵌的kobject对象 */ struct module *owner; /* 所属模块 */ struct file_operations *ops; /* 文件操作结构体 */ struct list_head list; dev_t dev; /* 设备号 */ unsigned int count; /* 该设备关联的设备编号的数量 */ };

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

如何获取主次设备号,或dev_t?

MAJOR(dev_t dev); // 主设备号 MINOR(dev_t dev); // 次设备号

MKDEV(int major, int minor); // 生成dev_t, 包含主次设备号信息

这几个宏定义如下:

#include <linux/kdev_t.h> #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 高12bit为主设备号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 低20bit为次设备号 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

内核提供一组函数用于操作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 *);

1)cdev_init 初始化cdev成员,最重要的是建立cdev和file_operations之间的连接

源码:

/** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev, remembering @fops, making it ready to add to the * system with cdev_add(). */ void cdev_init(struct cdev *cdev, const struct file_operations *fops) { memset(cdev, 0, sizeof *cdev); /* 将整个结构体清零 */ INIT_LIST_HEAD(&cdev->list); /* 初始化list成员, 指向自身 */ kobject_init(&cdev->kobj, &ktype_cdev_default); /* 初始化kobj成员 */ cdev->ops = fops; /* 建立cdev和file_operations之间的连接 */ }

2)cdev_alloc 动态申请一个cdev内存

源码:

/** * cdev_alloc() - allocate a cdev structure * * Allocates and returns a cdev structure, or NULL on failure. */ struct cdev *cdev_alloc(void) { struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); /* 动态申请一个cdev内存, GFP_KERNEL: 无内存可用时可休眠 */ if (p) { INIT_LIST_HEAD(&p->list); /* 初始化list成员, 指向自身 */ kobject_init(&p->kobj, &ktype_cdev_dynamic); /* 初始化kobj成员 */ } return p; }

上面两个初始化函数,为何都没看到owner、dev、count 这3个成员的初始化?

对于owner成员,struct module类型对象,是内核对于一个模块的抽象。该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式初始化.owner = THIS_MODULE

对于dev和count成员,在cdev_add中才会赋值。

3)cdev_add 向内核添加一个cdev,完成字符设备的注册

这里需要提供参数dev(设备号)和count(该设备关联的设备编号的数量),直接赋值给cdev结构的dev和count成员。

/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */ 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); /* 将cdev放入cdev_map中 */ if (error) return error; kobject_get(p->kobj.parent); /* 增加引用计数 */ return 0; }

4)cdev_del 从内核删除一个cdev

/** * cdev_del() - remove a cdev from the system * @p: the cdev structure to be removed * * cdev_del() removes @p from the system, possibly freeing the structure * itself. */ void cdev_del(struct cdev *p) { cdev_unmap(p->dev, p->count); /* 将dev从cdev_map中擦除 */ kobject_put(&p->kobj); /* 减少引用计数 */ } static void cdev_unmap(dev_t dev, unsigned count) { kobj_unmap(cdev_map, dev, count); /* 将dev从cdev_map中擦除 */ }

分配、释放设备号分配设备号

调用cdev_add()向系统注册字符设备前,应先申请设备号。分配设备号有2种方法:

1)静态申请:register_chrdev_region

register_chrdev_region() 用于已知起始设备号的情况,向系统静态申请设备号(范围)。

要申请的设备号范围:[from, from count)。

有些设备号已被Linux内核开发者分配掉了,具体分配内容可查看Documentation/devices.txt。

/** * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include * the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * * Return value is zero on success, a negative error code on failure. */ int register_chrdev_region(dev_t from, unsigned count, const char *name) { struct char_device_struct *cd; dev_t to = from count; dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n) 1, 0); if (next > to) next = to; cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name); if (IS_ERR(cd)) goto fail; } return 0; fail: /* 出错回滚 */ to = n; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n) 1, 0); kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); } return PTR_ERR(cd); }

2)动态申请:alloc_chrdev_regionalloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。得到的设备号会放入第一个参数dev中。alloc_chrdev_region相比register_chrdev_region,优点:alloc_chrdev_region会自动避开设备号重复的冲突。

/** * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. */ 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; }

检查:注册设备成功后,会在/proc/devices 添加字符设备名称。因此,可以利用insmod命令加载设备驱动后,观察/proc/devices值,判断是否注册了设备。

# cat /proc/devices

释放设备号

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

从系统反注册设备号,范围:[from, from count)

/** * unregister_chrdev_region() - unregister a range of device numbers * @from: the first in the range of numbers to unregister * @count: the number of device numbers to unregister * * This function will unregister a range of @count device numbers, * starting with @from. The caller should normally be the one who * allocated those numbers in the first place... */ void unregister_chrdev_region(dev_t from, unsigned count) { dev_t to = from count; dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n) 1, 0); /* 下一个设备号dev_t */ if (next > to) next = to; kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); /* 反注册单个设备号, 并释放空间 */ } }

file_operations结构体

file_operations 是设备驱动程序与APP交互的接口,其成员函数是字符设备驱动程序设计的主体,实际会在APP调用open/write/read/close等系统调用时被内核调用。

file_operations结构体定义:

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 *); int (*iterate_shared) (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 (*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 (*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 ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64); ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64); };

主要成员:

read和write返回0,暗示end-of-line(EOF)。


字符设备驱动的组成

Linux中,字符设备驱动组成:字符设备驱动模块加载、卸载函数,字符设备驱动的file_operations结构体的成员函数。

字符设备驱动模块的加载、卸载函数

加载函数应该实现:1)设备号的申请;2)cdev的注册。

卸载函数应该实现:1)设备号的释放;2)cdev的注销。

典型的设备结构体、模块加载函数、卸载函数代码形式:

/* 设备结构体 struct xxx_dev_t = { struct cdev cdev; ... } xxx_dev; */ /* 设备驱动模块加载函数 */ static init __init xxx_init(void) { ... cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev */ xxx_dev.cdev.owner = THIS_MODULE; /* 获得字符设备号 */ if (xxx_major) { register_chrdev_region(xxx_dev_no, 1, DEV_NAME); } else { alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME); } ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备 */ ... } /* 设备驱动模块卸载函数 */ static void __exit xxx_exit(void) { unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号 */ cdev_del(&xxx_dev.cdev); /* 注销设备 */ }

字符设备驱动的file_operations结构体的成员函数

file_operations的成员函数是字符设备驱动跟内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。大多数字符设备驱动会实现read()/write/ioctl()。

典型字符设备驱动代码形式:

/* 读设备 * filp: 文件结构指针 * buf: 用户空间内存地址, 在内核空间不能直接读写 * count: 要读的字节数 * f_pos: 读的位置相对于文件开头的偏移 */ ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { ... copy_to_user(buf, ..., ...); /* 将数据从内核空间拷贝到用户空间 */ ... } /* 写设备 */ ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { ... copy_from_user(.... buf, ...); /* 将数据从用户空间拷贝到内核空间 */ ... } /* ioctl函数 */ long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { ... switch(cmd) { case XXX_CMD1: ... break; case XXX_CMD2: ... break; default: /* 不支持的命令 */ return -ENOTTY; } return 0; }

copy_to_user和copy_from_user

注:用户空间不能直接访问内核空间的内存,所以要借助copy_to_user()将数据从内核空间拷贝到用户空间;同样地,内核空间不能直接访问用户空间的内存,所以借助copy_from_user()将数据从用户空间拷贝到内核空间。

#include <linux/uaccess.h> /* 用户 -> 内核 */ unsigned long copy_from_user(void *to, const void __user *from, unsigned long n); /* 内核 -> 用户 */ unsigned long copy_to_user(void __user *to, const void *from, unsigned long n); 注:函数返回不能被复制的字节数。如果完全复制成功,返回0;如果失败,返回负值。

其源码如下:

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) { if (likely(access_ok(VERIFY_READ, from, n))) /* 检查地址的合法性, from起始地址, 长度n */ n = __copy_from_user(to, from, n); /* 数据拷贝, 但不做地址合法性检查 */ else memset(to, 0, n); return n; } unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) { if (likely(access_ok(VERIFY_WRITE, to, n))) /* 检查地址的合法性, to起始地址, 长度n */ n = __copy_to_user(to, from, n); /* 数据拷贝, 但不做地址合法性检查 */ return n; }

likely:是宏定义,常用于编译器优化,告诉编译器分支大概率会发生。access_ok(type, addr, size):内核空间可以访问用户空间的缓冲区,但访问之前需要用access_ok检查其合法性,以确定传入的缓冲区地址的确术语用户空间。

如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user()。

int val; /* 内核空间变量 */ ... get_user(val, (int* ) arg); /* 用户 -> 内核, arg 是用户空间地址 */ ... put_user(val, (int* ) arg); /* 内核 -> 用户, arg 是用户空间地址 */

copy_from_user函数中的__user宏是什么?该宏表明背后的指针指向用户空间,实际上更多地充当了代码注释的功能。

#ifdef __CHECKER__ # define __user __attribute__((noderef, address_space(1))) #else # define __user #endif

put_user和get_user

put_user(), get_user() 也有另外一个版本:__put_user(), __get_user()。区别在于__put_user()不用access_ok()检查地址的合法性,而put_user()会。通常,在调用__put_user()之前,会手动检查用户空间缓冲区。get_user()和__get_user() 关系类似。

I/O控制函数unlocked_ioctl

I/O控制函数的cmd参数为事先定义的I/O控制命令,arg为对应于命令的参数。例如,对于串行设备,如果SET_BAUDRATE是设置波特率的命令,那arg就应该是波特率值。

字符设备驱动文件操作file_operations

字符设备驱动文件操作,通过定义file_operations实例,并将具体设备驱动函数赋值给file_operations成员来完成。

struct file_operations xxx_fops = { .owner = THIS_MODULE, .read = xxx_read, .write = xxx_write, .unlocked_ioctl = xxx_ioctl, };

通过模块加载函数中调用cdev_init(&xxx_dev.cdev, &xxx_fops) 为cdev和fops建立连接。

,