描述接下来的章节中, 用何种逻辑与顺序介绍各个子模块
/dev目录下有很多设备节点.
ls -l /dev
凡是第一行开头字母是’c’的, 都叫字符设备节点.
例如/dev/console就是一个字符设备节点.
Linux下一切皆文件, 字符设备节点也可以看成一个文件, 对于文件(例如一个txt文档), 你会做哪些操作? 打开它(open), 看看里面有什么东西(read), 写进去一点东西(write), 保存关闭它(close).
所以对字符设备节点, 你也可以open/read/write/close.
与字符设备类似的还有一个块设备, ls -l /dev , 第一行是’b’的都是块设备. 后面会有文章专门分析块设备, 这里想说的是什么情况下会用字符设备, 或者说字符设备的特点是什么:
? 字符设备:是指只能一个字节一个字节读写的设备, 不能随机读取设备内存中的某一数据, 读取数据需要按照先后数据.
字符设备是面向流的设备, 常见的字符设备有鼠标、键盘、串口、控制台和LED设备等
? 块设备:是指可以从设备的任意位置读取一定长度数据的设备. 块设备包括硬盘、磁盘、U盘和SD卡等
ls -l /dev/console
crw------- 1 root root 5, 1 Sep 14 16:32 /dev/console
/dev/console: 设备节点
5: 主设备号
1: 次设备号
设备号由主+次设备号组成, 后面会详解介绍设备号. 设备号与设备节点是一一对应的. 我们可以用mknod手动创建设备节点, 若需创建设备节点, 必须知道主次设备号
设备模型一文中介绍过VFS: 虚拟文件系统, 它对内核进行抽象, 提供统一的文件系统接口给到用户空间(open/read/write/close等), 以实现Linux中一切皆文件的妙思.
struct file
代表一个打开的文件.
系统中每个打开的文件在内核空间都有一个关联的struct file
它由内核在打开文件时创建, 在文件关闭后释放.其成员loff_t f_pos 表示文件读写位置
struct inode
用来记录文件的物理上的信息.
因此, 它和代表打开文件的file结构是不同的. 一个文件可以对应多个file结构, 但只有一个inode结构.
其成员dev_t i_rdev表示设备号
struct cdev
在Linux内核中, 用cdev抽象一个字符设备, 也就是说你要创建一个字符设备, 就是创建一个cdev.
struct cdev中包括一个结构体: struct file_operations.
struct file_operations: 一个函数指针的集合(open/read/write/close), 定义能在设备上进行的操作. 结构中的成员指向驱动中的函数, 这些函数实现一些与驱动相关的特定功能, 对于不支持的操作保留为NULL. 聪明的你应该猜到了, 用户空间的动作最终会映射到这些函数上, 例如在用户空间针对某个字符设备节点调用open, 最终会导致该字符设备的file_operations->open函数被执行
注意, 前面我们说过设备号与设备节点一一对应, 但是设备号或者说设备节点与cdev却是N:1的关系, N >= 1.
也就是说, 一个设备节点可能对应一个cdev; 也可能多个设备节点对应同一个cdev, 这些设备节点往往有相同的主设备号, 但是次设备号不同. 在后一种情况下, 当你对这些设备节点调用open函数时, 会导致同一个cdev的file_operations->open被调用, 我们可以在file_operations->open里面获取次设备号, 以区分不同的设备节点. misc核心代码就是用的这套逻辑.
在查看下一幅图片和阅读接下来的章节之前, 建议你先扫一眼6.1《编写字符设备驱动的一般步骤》. 好在脑子里对编写字符设备驱动有个大致印象.
前面我们大致了解了字符设备驱动中的主要概念.
接来下我们详细分析一下字符设备驱动中主要的数据结构.
dev_t头文件: include/linux/types.h
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
从代码上看, dev_t就是一个uint型整数, 32bit.
这32bit中, 高12位代存放主设备号, 低20位存放次设备号.
不过在内核代码中, 千万不要手动去操作这个整数的高12位或低20位. Linux内核提供了几个宏来操作dev_t:
头文件: 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))
创建设备号: MKDEV
如果已知主/次设备ID, 可以用 MKDEV(int major, int minor); 来生成一个设备号.
读取主/次设备号: MAJOR/MINOR
如果已知一个设备号dev_t:
MAJOR(dev_t dev); 可以获取到主设备号
MINOR(dev_t dev); 可以获取到次设备号
前面我们说过, 内核中cdev数据结构就代表字符设备.
头文件: include/linux/cdev.h
Comment |
|
struct kobject kobj; |
cdev也是一个kobject, 具备kobject的所有特性, 但是它不对应sysfs下的某个目录, 原因是设备驱动核心代码并没有用kobject_add把这个kobj添加到内核. kobj在cdev中的唯一作用就是引用计数的功能. |
struct module *owner; |
所属的模块, 通常是THIS_MODULE |
const struct file_operations *ops; |
open/read/write/close等操作函数集合 const表明核心代码只会使用它, 不能修改它 |
struct list_head list; |
链表头 一个设备文件在物理上对应一个inode (后面会介绍) 多个设备文件可以对应同一个设备驱动(cdev) cdev->list下挂载着所有的inode (通过inode->i_devices) |
dev_t dev; |
设备号 |
unsigned int count; |
dev定义的是(主设备号+起始次设备号), count表明从起始次设备号开始, 有多少个次设备 |
这个结构体是字符设备当中最重要的结构体之一, file_operations 结构体中的成员函数指针是字符设备驱动程序设计的主体内容, 这些函数在应用程序进行Linux 的 open()、read()、write()、close()、seek()、ioctl()等系统调用时会最终被调用.
不是所有的函数指针都需要初始化, 对于不需要实现的功能, 其函数指针可以为NULL. 不过内核对于这些NULL函数的处理方式不都是一样的.
下面很多函数的参数列表中都有__user, 表示后面的地址是用户空间的地址, 内核空间不能直接访问. __user在编译时并没有任何特殊效果, 它就像代码的注释一样.
下面的介绍不用逐行阅读, 大致了解即可, 在后续用到对应的功能时可以查阅此表
file 结构代表一个打开的文件.
它的特点是一个文件可以对应多个file结构.
它由内核再open时创建, 并传递给在该文件上操作的所有函数, 直到最后close函数, 在文件的所有实例都被关闭之后, 内核才释放这个数据结构
头文件: include/linux/fs.h
Note that a file has nothing to do with the FILE pointers of user-space programs. A FILE is defined in the C library and never appears in kernel code. A struct file, on the other hand, is a kernel structure that never appears in user programs
file结构体应该在介绍文件系统时详细讨论, 这里只简单介绍一些与字符设备驱动相关的内容:
struct file |
Comment |
fmode_tf_mode; |
读写权限 The file mode identifies the file as either readable or writable (or both), by means of the bits FMODE_READ and FMODE_WRITE You might want to check this field for read/write permission in your open or ioctl function, but you don‘t need to check permissions for read and write, because the kernel checks before invoking your method An attempt to read or write when the file has not been opened for that type of access is rejected without the driver even knowing about it |
loff_t f_pos |
The current reading or writing position loff_t is a 64-bit value on all platforms (long long in gcc terminology) The driver can read this value if it needs to know the current position in the file but should not normally change it read and write should update a position using the pointer they receive as the last argument instead of acting on filp->f_pos directly The one exception to this rule is in the llseek method, the purpose of which is to change the file position |
unsigned int f_flags |
These are the file flags, such as O_RDONLY, O_NONBLOCK, and O_SYNC A driver should check the O_NONBLOCK flag to see if nonblocking operation has been requested; the other flags are seldom used In particular, read/write permission should be checked using f_mode rather than f_flags |
struct file_operations *f_op |
对字符设备驱动而言, 当用户调用open系统调用时, 内核就会把cdev->file_operations赋值给这里的f_op The value in file->f_op is never saved by the kernel for later reference, 意思就是说f_op具体指向哪个operations, 是可以被更改的. 这个特性很有用, misc核心系统(drivers/char/misc.c)就用到了这个特性, 看看misc_open函数的代码, 你应该会明白. 这个特性类似于C++中的函数重载机制 |
void *private_data |
The open system call sets this pointer to NULL before calling the open method for the driver You are free to make its own use of the field or to ignore it; you can use the field to point to allocated data, but then you must remember to free that memory in the release method before the file structure is destroyed by the kernel 这个元素在驱动代码中用得非常多, 一般的情况是, open的时候我们在这个指针存放一些东西, 然后在read/write等函数中我们就可以通过file->private_data访问到这些东西. |
struct dentry *f_dentry |
The directory entry (dentry) structure associated with the file |
内核用inode 结构在内部表示文件, 它是实实在在的表示物理硬件上的某一个文件, 且一个文件仅有一个inode与之对应, 同样它有二个比较重要的成员:
头文件: include/linux/fs.h
struct inode |
Comment |
…… |
|
dev_t i_rdev |
如果inode代表一个设备文件, 这个域存放设备号 unsigned int imajor (struct inode *inode); 用于从inode中获取主设备号 unsigned int iminor (struct inode *inode); 用于从inode中获取次设备号 |
struct cdev *i_cdev |
struct cdev is the kernel‘s internal structure that represents char devices this field contains a pointer to that structure when the inode refers to a char device file |
struct list_headi_devices; |
用于把inode挂载到cdev->list链表下 |
…… |
|
在建立一个字符设备之前, 驱动首先要做的就是申请一个或者多个设备号. 设备号可以通过静态或动态的方式申请.
头文件: include/linux/fs.h
实现文件: fs/char_dev.c
参数列表: (dev_t from, unsigned count, const char *name)
? from: 主设备号已知, 次设备号一般情况是0, 两者组成from
? count: 所请求的连续设备号的个数
? name: 与此设备号范围关联的设备名称. 它会出现在/proc/devices中.
? 静态申请设备号: 针对已知主设备号的情况, 用于像内核申请from开始, count个连续的设备号. 如果申请失败, 该API返回非0值.
? 一般我们编写驱动的时候, 如果已经知道它的主设备号, 比如要创建一个video设备, 那它的主设备号就是81. 有可能某个硬件设备已经使用了81对应的某个次设备号了, 所以我们需要先向驱动查询是否还剩余count个次设备号
参数列表: (dev_t *dev, unsigned baseminor, unsigned count, const char *name)
? *dev : 仅用于输出, 成功完成调用后将保存已经分配范围的第一个编号
? baseminor: 要使用的被请求的第一个次设备号, 一般情况下是0
? count: 所请求的连续设备号的个数
? name: 与此设备号范围关联的设备名称. 它会出现在/proc/devices中
? 动态申请设备号: 针对未知主设备号的情况
? 一部分主设备号已经静态地分配给了大部分常见设备<Documentation/devices.txt>
? 如果我们不是明确的知道驱动应该使用的主设备号, 最好使用动态分配的方式, 不要随意指定一个空闲的主设备号
? 缺点
? 由于分配的主设备号不能预先得知, 无法预先创建设备节点(/dev/xxx), 不过这也不是问题, 后面我们会介绍如何创建设备节点
注销占用的设备号.
参数列表: (dev_t from, unsigned count)
? from: 主/次设备号已知
? count: 要注销的设备号个数
设备节点的创建方式大体上可以分为两种:
自动创建
自动创建设备节点依赖Linux设备模型, 还记得struct device结构体中有一个元素叫devt. 当你得到设备号之后(不管是静态还是动态申请的), 把这个设备号赋值给device->devt, 然后调用device_add把这个device添加到内核, device_add会自动创建设备节点(通过devtmpfs或者udev). 详解逻辑请参考设备模型一文中device_add函数的分析
手动创建
在已知设备号的情况下(静态申请的你应该知道, 动态申请的你可以通过name在/proc/devices中查到), 可以用个mknod命令手动创建设备节点
头文件: include/linux/cdev.h
实现文件: fs/char_dev.c
cdev API |
Comment |
struct cdev *cdev_alloc(void) |
分配一个cdev结构体空间, 注意调用完该API之后, 不需要再调用cdev_init, 直接对得到的cdev进行操作, 比如给cdev->ops赋值 |
void cdev_init(struct cdev *, const struct file_operations *) |
当你用kzalloc手动分配了一个cdev空间之后, 需要调用该API初始化cdev结构体空间, 注意第2个参数是file_operations |
int cdev_add(struct cdev *, dev_t, unsigned) |
当你用cdev_alloc 或者 cdev_init准备好一个字符设备之后, 调用该API把字符设备添加到内核 |
void cdev_del(struct cdev *) |
从内核中移除一个字符设备, 该函数会调用kobject_put减少cdev->kobj的引用计数, 当计数到0时, 会通过该kobj的ktype释放资源. 我们在设备模型一文中介绍过kobject的这个机制 |
void cdev_put(struct cdev *p) |
与cdev_get对应, 不过cdev_get是fs/char_dev.c中的一个static函数. 一般cdev_add之后用cdev_del cdev_get之后用cdev_put |
懒人API
头文件: include/linux/fs.h
实现文件: fs/char_dev.c
该API主要用来分配一个cdev结构体空间, 并做简单初始化, 逻辑很简单:
? 调用kzalloc分配cdev
? 初始化cdev->list链表头, 如果不记得这个链表头的作用, 回头看看数据结构中的描述
? 调用kobject_init初始化cdev->kobj, 指定该kobj的ktype为ktype_cdev_dynamic, 这个ktype是在fs/char_dev.c中定义的, 目的只有一个: 当cdev->kobj引用计数到0时, 释放空间.
该API用于初始化一个手动分配(不是用cdev_alloc)的cdev结构体空间, 逻辑很简单:
? 用memset清理该cdev结构体空间
? 初始化cdev->list链表头
? 调用kobject_init初始化cdev->kobj, 指定该kobj的ktype为ktype_cdev_default, 这个ktype也是在fs/char_dev.c中定义的 (注意与cdev_alloc中的ktype不是同一个), 目的也是当cdev->kobj引用计数到0时, 释放空间.
? 给cdev->ops赋值 : cdev->ops = fops;
cdev_add是把一个字符设备添加到内核.
添加到内核是个什么意思呢? 其实就是相当于内核建立了一个箱子, 每当有一个字符设备通过cdev_add添加到内核时, 就把这个cdev加到箱子里面去, 在需要查找某个字符设备的时候, 就从这个箱子里面获取.
综合来看, 这个箱子就有两方面的功能, 一是可以方便的插入一个字符设备, 二是可以方便的查找到字符设备. 插入用链表比较方便, 而为了快速查找, 用数组更加方便. Hash算法就是结合了链表和数组的优点, 所以内核的这个箱子就是用Hash算法实现的. 关于Hash算法, 可以查看我们的编程基础这篇文档(建议先理解一下Hash算法中的拉链法, 再来理解下面的内容就很容易了).
这个箱子的名字叫做cdev_map, 是fs/char_dev.c中定义的一个static struct kobj_map *类型的指针. cdev_map的初始化是在void __init chrdev_init(void)函数中.
简单理解一下kobj_map这个结构体:
struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;
} *probes[255];
struct mutex *lock;
};
? probes就是一个Hash表, probe相当于Hash里面的一个元素(Entry), Entry有3个主要属性:
? key : probes[255]表明, key的范围为 0 - 255, key值是设备号(dev).
? next : 就是probe结构体中的next指针, 用于链接相似key值的probe.
? value : 除去next, probe结构体中的其他变量都是value
? lock, 互斥锁, 用于互斥对Hash表的访问
cdev_add(struct cdev *p, dev_t dev, unsigned count)的逻辑如下:
? p->dev = dev : 将设备号赋值给cdev结构体
? p->count = count : 将次设备号的个数赋值给cdev结构体
? 调用kobj_map, 把该字符设备加入到cdev_map这个Hash表中. Key值就是设备号(主+次设备号), 用( 设备号%255 )决定挂载到哪个数组元素上.
? 调用kobject_get(p->kobj.parent), 增加kobj.parent的引用计数
注意: cdev_add中并没有调用kobject_add来把cdev->kobj添加到内核, 所以/sys/下不存在与字符设备对应的目录. kobject在此的唯一作用是引用计数.
cdev_del是从内核中移除一个字符设备. 逻辑如下:
? 调用cdev_unmap从cdev_map这个Hash表中移除字符设备.
? 调用kobject_put减少cdev->kobj的引用计数. 当引用计数减为0时, 就会调用对应的ktype->release(ktype_cdev_dynamic或者ktype_cdev_default)来释放资源. 释放资源的具体代码就不分析了, 可以自行阅读
该函数用于分配设备号.
register_chrdev_region , alloc_chrdev_region, 包括下文的register_chrdev都会调用该函数.
内核中所有的设备号都记录在chrdevs这个Hash表中. chrdevs是在fs/char_dev.c中定义的, 如下:
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
? chrdevs代表Hash表, char_device_struct代表Hash表中的一个元素(Entry), Entry的3个主要属性:
? key : key值的范围是(0 - CHRDEV_MAJOR_HASH_SIZE), key值是主设备号. 注意哦, 只是主设备号, 每一个char_device_struct对应一个<主设备号(char_device_struct->major)+次设备号范围>, char_device_struct->baseminor代表该主设备号下次设备号的起始值, char_device_struct->minorct表示次设备的个数.
? next : char_device_struct结构体中的next指针
? value : 除了前面已经介绍的major, baseminor, minorct;
name代表与该主设备对应的名称, 会出现在/proc/devices中, name是调用者传过来的参数;
cdev代表与这个(主设备号+次设备号范围)对应的字符设备.
所谓分配设备号的意思就是, 当调用者给定了一个major和minor范围后, 该函数就会在chrdevs Hash表中查询(major + minor范围)是否被他人占用, 如果没有, 就会创建一个char_device_struct结构体, 然后把它加入到Hash表中, 并把该结构体返回给调用者.
register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
? 直接调用__register_chrdev(major, 0, 256, name, fops) : major是主设备号, 0是次设备号起始值, 256是次设备号个数
? 调用__register_chrdev_region检查上诉256个设备号有没有被占用.
如果设备号没有被占用, 该函数会返回一个 struct char_device_struct 类型的元素cd
? 调用cdev_alloc分配一个cdev结构体
? 调用cdev_add把该cdev添加到内核
? 将这个cdev的指针存储在cd->cdev中, 存储起来的目的是为了unregister_chrdev时能找到并移除这个cdev.
void unregister_chrdev(unsigned int major, const char *name)
? 直接调用__unregister_chrdev(major, 0, 256, name)
? 调用__unregister_chrdev_region, 查找chrdevs Hash表中是否存在主设备号为major, 次设备范围从0 - 256的字符设备. 如果存在的话, 则从Hash表中移除这些设备号, 以便其他人可以再次申请, 同时返回找到的struct char_device_struct结构体(cd)
? 从cd中获取cdev, 调用cdev_del移植这个cdev
? 调用kfree(cd)释放资源
编写字符设备驱动最主要的工作就是填充file_operations结构体.
下面我们来看看最主要的几个函数该如何编写吧.
原型: int (*open)(struct inode *inode, struct file *filp);
调用时机: 用户空间使用open系统调用时
目的: open函数最主要的目的是做一些初始化动作, 例如分配memory, 初始化硬件等等, 为后续其他操作做准备. 一般来说, open里面要做如下事情:
? Check for device-specific errors (such as device-not-ready or similar hardware problems)
? Initialize the device if it is being opened for the first time
? Update the filp->f_op pointer, if necessary
这个地方有必要解释一下, 当我们向内核注册一个字符设备时(即struct cdev), 我们必须指定cdev->file_operations. 用户空间open某个字符设备节点时, 会生成一个struct file *filp, 并把cdev->file_operations赋值给filp->f_op
不过在这里, 如果有必要, 可以修改file->f_op, 相当于指定一套新的file_operations. misc的核心代码里面就是这么做的.
? Allocate and fill any data structure to be put in filp->private_data
可以把某些数据存储在filp->private_data, 这样后续的read/write等操作都可以通过filp->private_data获取到这些数据.
原型: int (*release) (struct inode *, struct file *)
目的: release相当于open的反函数, 一般做如下事情:
? Deallocate anything that open allocated in filp->private_data
? Shut down the device on last close
你有没有想过, 如果用户空间open的次数少于close的次数, 会发生什么情况?
例如: the dup and fork system calls create copies of open files without calling open; each of those copies is then closed at program termination.
答案也很简单, 不是所有的close系统调用都会导致release函数的执行.
The kernel keeps a counter of how many times a file structure is being used. Neither fork nor dup creates a new file structure (only open does that); they just increment the counter in the existing structure. The close system call executes the release method only when the counter for the file structure drops to 0
这样, 就保证了一次open对应一次release.
但是, 每一次close系统调用, 都会导致ops-> flush函数被执行. 不过驱动程序中很少用到flush.
还有一个问题要注意, 就算用户空间的代码不显示的调用close, 系统也会在进程exit时自动调用close.
read/write所做的事情非常类似, write是从用户空间获取数据, read是把数据送到用户空间. 因此可以把它俩一起介绍.
原型: 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)
调用时机: 用户空间使用read/write, pread/pwrite, fread/fwrite, 系统调用时
参数解释
? filp: file pointer
? buff: 指向用户空间的一个buffer, 用于内核空间与用户空间交换数据.
注意, 内核空间不可直接访问这个buffer, 你必须使用copy_to_user/copy_from_user, 这两个函数的头文件是include/linux/uaccess.h.
copy_to_user/copy_from_user的返回值代表还剩多少字节的数据需要拷贝
另外, 这两个函数可能会休眠, 所以不能用在不允许休眠的上下文中, 例如spinlock, 中断处理函数等等. 我们在并发与竞态一文中详细列举了哪些上下文中不允许休眠
? count代表有多少字节的数据需要传输
? offp代表从哪里开始传输数据, 当驱动代码成功传输了N个字节的数据之后, 需要更新offp, 例如*offp = *offp + N;
内核系统会根据offp的值来更新filp->f_pos (一个例外是pread/pwrite, 如果用户空间使用这两个系统调用, 则内核系统不会更新filp->f_pos的值). 注意read/write函数中不要直接修改filp->f_pos.
? 关于read/write的返回值
? 返回负数, 代表出错.
如果传输过程中出错了, 则应该先返回已经传输完成的字节数. 等到下次系统调用时, 在返回错误值.
read/write中, 我们经常返回形如 - EBUSY这样的错误代码, 不过用户空间看到的错误始终是-1, 如果想获取详细的错误信息, 需要访问errno变量.
? 返回0, 对于read, 代表end-of-file; 对于write, 代表no-more-space. 此时可以用阻塞机制(blocking I/O), 我们会在后面详解介绍
? 返回 > 0, 代表实际传输完成的字节数.
如果返回值等于count, 则代表用户要求传输的字节数都传输完毕了.
如果返回值小于count, 则代表只完成了部分数据传输, 此时应用程序需要重新调用read/write. 如果应用程序使用的是库函数fread/fwrite, 则这两个库函数会自动重调read/write, 直到完成所有数据的传输.
除了read/write之外, 大部分驱动程序还需要具备另外一种能力, 即通过设备驱动程序执行各种类型的硬件控制. ioctl就是用来干这个事的.
用户空间中的原型: int ioctl(int fd, unsigned long cmd, ...)
最后的参数…不是代表可变数目的参数哦, 而是代表可选参数. 有的cmd后面不需要跟参数, 有的需要一个整型参数, 有的需要一个指针参数. …只是为了防止编译器进行类型检查.
驱动程序中的原型: long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)
long (*compat_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)
参数解释:
? file: 与传递给open那个file参数一样, 是同一个东西
? cmd: 命令编号, 用于区分不同的命令. 理论上讲, 从0开始编号也是可以的, 不过最好不要这样做, 内核提供了专用的方法来给cmd编号, 后文《ioctl cmd》会详细介绍.
? arg: 无论用户空间的是指针还是整数值, 它都以unsigned long的形式传递给驱动程序. 如果是整数值, 直接使用就可以了; 但如果是指针, 就要注意了, 这里指针指向的是用户空间的地址. 你可能已经想到了, 可以用copy_from_user/copy_to_user, 这两个函数是可以的, 但是因为ioctl调用通常涉及到小的数据项, 因此可以通过其它更有效的方式来操作. 后文《ioctl arg》会详细介绍.
? 返回值: 如果驱动程序中收到了一个不支持的cmd, 则可以返回-EINVAL或者-ENOTTY. 一般的, 会选择前者.
再多说一点, ioctl主要用于控制硬件做某项操作, 你有没有想过这里面的权限如何控制? 比如不是所有的用户都有权限执行格式化操作.
根据UNIX系统的传统, 特权操作仅限于超级用户账号, 这种特权要么全有, 要么全无 ---- 超级用户几乎可以做任何事情, 而所有其他用户则受到严格的限制.
Linux内核提供了一个更为灵活的系统, 称为权能(capability). 基于权能的系统抛弃了那种要么全有要么全无的特权分配方式, 而是把特权操作划分为独立的组, 这样某个特定的用户或程序就可以被授权执行某一指定的特权操作. 后文《ioctl capability》会详细介绍内核代码是如何实现权能的.
为了方便程序员创建唯一的ioctl命令号, 每一个命令号被分为多个位字段.
旧版本的内核使用一个16位整数代表一个cmd, 分为2个字段: 高8位是与设备相关的”幻”数, 用于区分不同的设备; 低8位是一个序列号码, 在设备内唯一.
新版本的内核用一个32位的整数代表一个cmd, 分为4个字段: type, number, direction, size.
旧的方式就不在介绍了, 下面我们详细了解下新方式中4个字段的细节.
头文件: include/uapi/linux/ioctl.h
驱动代码中应该用#include <linux/ioctl.h>的方式引用此头文件, 以便使用新的编号方式. 该头文件最终会引用include/uapi/asm-generic/ioctl.h. 后者定义了新编号方式的细节.
字段 |
Comment |
32位整数中所处的位置 |
type |
幻数, 用于区分不同的设备 Documentation/ioctl/ioctl-number.txt中定义了已经被占用的幻数, 如果你想在你的代码中使用一个新的幻数, 记得查询此文件 |
此字段有8位宽 #define _IOC_TYPEBITS8 处在第[8 - 15]的位置 |
number |
编号, 用于区分不同的命令 |
此字段有8位宽 #define _IOC_NRBITS8 处于第[0 - 7]的位置 |
direction |
如果相关命令涉及到数据的传输, 该字段定义数据传输的方向, 方向是从应用程序的角度看的 _IOC_NONE : 没有数据传输 _IOC_WRITE : 写数据(应用往驱动中写入数据) _IOC_READ : 读数据(应用从驱动中读取数据) _IOC_WRITE | _IOC_READ |
默认情况下此字段有2位宽, 不过不同的体系架构中可以override #ifndef _IOC_DIRBITS # define _IOC_DIRBITS2 #endif 默认情况下, 处于[30 - 31]的位置 |
size |
所需传输的用户数据的大小 内核系统本身不会去检查这字段 驱动程序中可以利用这个字段来辅助检查用户空间丢过来的数据是否正确 不过驱动程序也可以不管这个字段 |
默认情况下此字段有14位宽, 不过不同的体系架构中可以override #ifndef _IOC_SIZEBITS # define _IOC_SIZEBITS14 #endif 默认情况下, 处于[16 - 30]的位置 |
|
|
|
API
头文件: include/uapi/linux/ioctl.h
上述头文件会最终引用include/uapi/asm-generic/ioctl.h, 后者定义了一些宏, 用于创建一个cmd或者从cmd中分解出各个字段
API ioctl-cmd |
Comment |
_IO(type,nr) |
创建一个cmd 幻数: type 命令编号: nr 数据方向: 无数据传输 数据大小: 0 |
_IOR(type,nr,size) |
创建一个cmd 幻数: type 命令编号: nr 数据方向: 应用从驱动中读取数据 数据大小: sizeof(size) |
_IOW(type,nr,size) |
幻数: type 命令编号: nr 数据方向: 应用往驱动中写入数据 数据大小: sizeof(size) |
_IOWR(type,nr,size) |
创建一个cmd 幻数: type 命令编号: nr 数据方向: 读/写数据 数据大小: sizeof(size) |
|
|
_IOC_DIR(nr) |
从cmd中获取方向位 |
_IOC_TYPE(nr) |
从cmd中获取幻数 |
_IOC_NR(nr) |
从cmd中获取命令编号 |
_IOC_SIZE(nr) |
从cmd中获取数据大小 |
Predefined Commands
include/uapi/asm-generic/ioctls.h中有很多预定义的cmd, 内核系统会使用这些cmd.
如果你自己创建的一个cmd恰好与系统预定义的一模一样的话, 那你的驱动代码永远都不可能收到这个cmd, 因为它被内核系统截获了.
前面我们提到, ioctl的参数可能是整型, 也可能是指针.
如果是整型, 那么很简单, 直接使用就可以了.
如果是指针, 就需要注意该指针是指向用户空间的, 内核不能直接访问.
copy_from_user / copy_to_user虽然可以用来与用户空间交换数据, 但一般arg数据量较少, copy_xxx_user在这种情况下效率不高.
因此我们介绍一种新的方式.
头文件: include/linux/uaccess.h
上述头文件最终会引用 arch/arm/include/asm/uaccess.h
总体来讲, 就是先用一个API检查下地址的有效性, 然后在用针对少量数据优化过的API完成数据传输.
API
API uaccess |
Comment |
access_ok(type, addr, size) |
这是一个宏 第一个参数是VERIFY_READ或者VERIFY_WRITE, 取决于要执行的动作时读取还是写入用户空间内存区(注意read还是write是从驱动的角度来看的哦) addr参数是一个用户空间地址 size是字节数 返回值 1表示成功; 0表示失败, 如果返回失败, 驱动程序通常要返回-EFAULT给调用者
access_ok最主要的作用是检查给定地址是否为当前进程的一个用户空间地址, 而非内核空间地址 |
__put_user(x,ptr) |
这是一个宏 代表将数据x写入ptr所指向的地址. ptr必须是指向用户空间的一个地址, 因此, 在调用此API之前, 必须用access_ok对ptr指向的地址进行检查 传输的数据大小依赖于ptr的参数类型(用sizeof来确定), 例如ptr是一个字符指针, 就传递1个字节, 2 4 8字节情况类似. __put_user只支持 1 2 4 8这几种字节大小的数据传输, 该API是专门针对这种少量数据优化过的
如果数据传输成功, 返回0 |
put_user(x,p) |
这是一个宏 与上个API类似 不同之处在于, 该API不需要提前用access_ok检查p所指向的地址, 它会自己去检查 |
__get_user(x,ptr) |
这是一个宏 代表从ptr指向的地址拷贝一个数据到x中 与__put_user类似, 也需要提前调用access_ok. |
get_user(x,p) |
宏, 不在赘述 |
前文提到过, Linux内核提供了一个更为灵活的权限管理机制, 称为权能(capability). 基于权能的系统抛弃了那种要么全有要么全无的特权分配方式, 而是把特权操作划分为独立的组, 这样某个特定的用户或程序就可以被授权执行某一指定的特权操作, 同时又没有执行其它不想关操作的能力.
内核专门为管理权能导出了两个系统调用capget和capset, 用户空间的程序也可以使用这两个系统调用来管理权能.
在内核系统中, 权能相关的代码如下:
头文件: include/linux/capability.h
实现文件: kernel/capability.c
上述头文件会引用include/uapi/linux/capability.h, 后者定义了内核系统中支持哪几种capability, 并对每种capability的意义都做了注释, 这里就不一一说明了, 有兴趣可以自行阅读代码.
内核驱动代码中, 在执行某项特殊操作之前(例如格式化磁盘), 需要检查要求执行此项操作的用户态进程是否有相应的权限.
内核代码中权限检查需要引用头文件<linux/sched.h>, 对应的API是int capable(int capability).
该API会检查当前进程是否有对应的权限.
举个例子以便理解吧.
假设驱动代码在执行某项特殊操作时, 需要用户态进程有CAP_SYS_ADMIN (uapi/linux/capability.h中定义的)的权限, 则应做如下检查:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
用户态进程则需要通过capset来设置自己的权限, 例如让自己具有CAP_SYS_ADMIN的权限, 以便进行特殊操作.
很多时候, 我们可以直接通过read/write接口来控制硬件. 此时, read/write传输的不仅仅是数据, 还有命令.
例如console, 当我们往/dev/console中写入一个特殊字符时, 可以改变console的背景颜色, 或者调整光标位置, 原因是console主要用来显示ASCII码中的字符, 数字等. ASCII码中的其他特殊字符就可用做控制.
在例如一个机器人, 它只能前后左右移动, 往这个机器人中写入或者读取数据其实是没有实际意义的, 这种情况下, 我们就可以用write来控制机器人的行为, 例如写入1代表向前走, 写入2代表向后走…
直接使用read/write控制硬件的一个好处是, 我们可以直接用echo等命令, 而不需要专门编写一个应用程序去调用ioctl. 而且The controlling program can live on a different computer from the controlled device, 你不可能在某台机器上给另外一台机器发送ioctl命令.
采用这种方式的重点在于, 代表命令的数据不能出现在正常的数据流中, 否则就会引起混乱.
在本文前面《主要数据结构》一章中我们介绍过file这个数据结构.
当用户空间调用open打开一个文件时, 内核空间就会生成一个对应的struct file.
file->f_flags里面有个标志, 叫O_NONBLOCK.
O_NONBLOCK是在include/uapi/asm-generic/fcntl.h中定义的:
#ifndef O_NONBLOCK
#define O_NONBLOCK 00004000
#endif
一般的, 我们只需要引用头文件include/linux/fs.h, fs.h中会引用include/linux/fcntl.h, fcntl.h会最终引用include/uapi/asm-generic/fcntl.h
默认情况下, 用户空间调用open时, file->f_flags的O_NONBLOCK位为0; 除非用户程序以”非阻塞IO的方式”open一个文件, O_NONBLOCK位才为1.
那么f_flags中的这个位代表什么意思呢? 假设用户空间想要从驱动的buffer中read一个数据, 但此时buffer是空的: 默认情况下, read操作就会阻塞, 也就是等到buffer中有数据了在继续执行; 但是如果用户程序在open设备文件的时候, 显示的要求” 非阻塞”, 则此时read操作应该立即返回.
驱动程序的file_operations中, 只有open, read, write 3个函数受file->f_flags中O_NONBLOCK位的影响. 换句话说, 我们在编写驱动程序的open, read, write函数时, 需要检查O_NONBLOCK位, 以确定我们是否应该阻塞. 一般用 if (file->f_flags & O_NONBLOCK) 这种形式来判断是否需要阻塞.
如前所述, 默认情况下, 在条件不满足时, 驱动程序是需要阻塞的哦. 内核系统也提供了一套机制(wait_event)来处理阻塞问题. 这里我们就不详细介绍这个机制了, 如需要了解, 可以阅读另一篇文章《并发_竞态_阻塞》.
使用非阻塞I/O的应用程序也经常使用poll, select, epoll系统调用.
poll, select和epoll的本质是一样的: 都允许进程决定是否可以对一个或者多个打开的文件做非阻塞的读取或者写入; 这些调用会阻塞进程, 直到给定的文件描述符集合中任何一个可读取或者写入.
因此, 它们经常用于那些要使用多个输入或者输出流而又不会阻塞于其中任何一个流的应用程序中.
简而言之, 就是某个用户空间进程打开了多个文件, 事先调用poll, select, epoll看看这些文件是否都准备好了数据, 如果有任何一个准备好了, 就立马调用read读取数据. write操作也是同样的逻辑.
这种情况相当于把本该存在于read中的阻塞, 拿出来放在read之前.
对于上述系统调用, 驱动代码中需要实现对应的file_operations->poll函数.
关于如何在驱动中编写poll函数的细节, 这里就不细说了, 参考http://www.makelinux.net/ldd3/chp-6-sect-3
用户空间可以通过多种方式获取数据, 例如:
默认情况下, 会以阻塞方式read/write数据;
也可以用非阻塞方式read/write数据;
还可以用poll/select/epoll的方式查询是否可以获取数据了.
想象这样一种情况, 假设用户空间有个进程在做一个运算, 这个运算要耗费很长时间, 运算里面用到了底层驱动中的一些数据. 在运算的过程中, 如果底层驱动数据发送变化时, 用户空间的进程要及时获取数据并重新运算.
这种情况下, 上述几种获取数据的方式都不太适合.
我们可以用一种新的方式, 异步通知机制.
用户空间进程可以监听底层驱动, 当驱动数据变化时, 驱动代码可以通知用户进程, 用户进程收到通知后在进行处理.
关于如何实现异步通知机制, 这里就不说细节了, 参考: http://www.makelinux.net/ldd3/chp-6-sect-4
用户空间可以通过系统调用lseek或者llseek来改变文件的当前读取/写入位置, 即修改(filp->f_pos).
在内核驱动代码中, 我们需要实现对应的file_operations->llseek函数, 以配合用户空间的系统调用.
还记得吗? 我们在前文《主要数据结构》中, 介绍file数据结构时, 说明了file->f_pos的意义, 并说明了驱动代码中只有llseek能够直接修改file_fpos; read/write函数则不能直接修改.
关于如何驱动代码中实现llseek的细节, 这里就不细说了, 参考: http://www.makelinux.net/ldd3/chp-6-sect-5
时间关系, 就不详细讲解这部分内容了, 如需了解详情, 可以查看http://www.makelinux.net/ldd3/chp-6-sect-6
大致上来说, 访问控制包括:
? 只允许1个进程打开设备文件
? 只允许1个用户的多个进程打开设备文件
默认情况下, 多个用户的多个进程都可以同时打开设备文件.
第一步: 编写相应的函数, 以填充file_operations结构体
第二步: 获取一个设备号: 不管是静态还是动态的方式.
第三步: 构建一个cdev结构体: 不管是用cdev_alloc自动分配, 还是手动定义然后用cdev_init初始化. 并将之前定义的file_operations赋值给cdev->ops.
第四部: 调用cdev_add(cdev, num)注册一个字符设备. 第一个参数是之前准备好的cdev结构体; 第二个参数是之前获取到的设备号.
上述步骤就是编写字符设备驱动的基本要求.
当然, 如果你想在用户空间访问此设备驱动对应的设备节点(/dev/xxx), 则还需通过手动或者自动的方式创建设备节点.
设计这样一个Demo, 编写一个字符设备驱动, 并自动创建设备节点. 用户空间能够通过echo往设备节点写入一个数据, 然后用cat从设备节点读出之前写入的那个数据.
https://gitlab.com/study-linux/char_drivers
练习: 上述Demo设计的很简单:
你还可以在上述Demo的基础上实现ioctl以便用户空间能往驱动中发送cmd;
或者在驱动中创建一个环形缓冲区, write函数往环形缓冲区写入数据, read函数从缓冲区读取数据, 当条件不满足时, bloking read/write函数 <建议你先阅读另外一篇文章: 并发, 竞态, 阻塞>;
你还可以实现poll函数, 以便用户空间可以使用poll/select等系统调用. 或者你也可以实现异步通知机制, 或者练习访问控制.
原文:https://www.cnblogs.com/jliuxin/p/14129356.html