Linux 字符驱动架构
如何往内核里添加一个字符驱动程序
分配设备号
前置:
* 设备号分为主设备号和次设备号.
* 主设备号是分配给设备驱动程序的唯一标识符,用于标识设备所属的驱动程序。它告诉内核在访问设备时应该调用哪个驱动程序来处理请求.
* 次设备号是与主设备号配合使用的较小标识符,用于区分同一主设备号下的不同设备实例
* 内核中设备号是一个`dev_t`类型的变量, 通过`MAJOR`和`MINOR`宏可以获取主设备号和次设备号.`MKDEV`宏可以将主设备号和次设备号合并成一个`dev_t`类型的变量.
申请设备号的方法有两种:
- 静态申请, 自己指定设备号
int register_chrdev_region(dev_t first, unsigned int count, char *name);
- 动态申请, 调用该函数后内核会分配一个未被使用的设备号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
无论使用哪种方法都在不需要的时候释放设备号
void unregister_chrdev_region(dev_t first, unsigned int count);
实现设备操作函数
在linux中一切皆文件, linux使用统一的接口来访问设备, 实现这些接口就是实现设备的驱动程序, 不支持的接口可以设置为NULL
.
接口的定义是struct file_operations
:
struct file_operations {
struct module *owner; // 指向该文件操作所属模块的指针
fop_flags_t fop_flags; // 文件操作标志
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 (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, unsigned int flags); // IO轮询函数
int (*iterate_shared) (struct file *, struct dir_context *); // 迭代共享文件函数
__poll_t (*poll) (struct file *, struct poll_table_struct *); // 文件轮询函数 (常用)
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // IO控制函数 (常用)
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); // 兼容IO控制函数
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 *); // 文件锁定函数
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); // 读取管道函数
void (*splice_eof)(struct file *file); // 管道结束函数
int (*setlease)(struct file *, int, struct file_lease **, 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 *); // 内存映射能力函数 (仅在无MMU配置下)
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); // 复制文件范围函数
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); // 重新映射文件范围函数
int (*fadvise)(struct file *, loff_t, loff_t, int); // 文件建议函数
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags); // io_uring命令函数
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *, unsigned int poll_flags); // io_uring命令轮询函数
};
ioctl
ioctl 可以支持各种cmd, 如何选择数字作为cmd是一个问题, cmd应该是唯一的, 避免向其他设备错误发送了命令却被执行了.
在 <linux/ioctl.h>
中定义了 cmd
的格式和一些宏来生成和解析cmd
. cmd
被分为四个部分:
type
(魔数,用于区分设备), number
(用于区分不同命令,在设备内唯一), size
(在后面会知道怎么用), direction
(数据传输的方向,读或写).
Documentation/ioctl-number.txt
中列了在内核中已经使用的魔数.
#define CMD1 _IO(XXX_IOC_MAGIC, 1) //没有参数的命令
#define CMD2 _IOR(XXX_IOC_MAGIC, 2, int) //读表示用户空间读, 内核空间写
#define CMD3 _IOW(XXX_IOC_MAGIC, 3, int) //写表示用户空间写, 内核空间读
ioctl 的实现常常是一个 switch 语句, 基于命令号.
通常还会用到 capable(int capability)
来检查用户是否有权限执行这个命令.常见的权限有
CAP_SYS_ADMIN | 一个捕获-全部的能力, 提供对许多系统管理操作的存取. |
CAP_SYS_MODULE | 加载或去除内核模块的能力 |
CAP_SYS_RAWIO | 进行 "raw" I/O 操作的能力. 例子包括存取设备端口或者直接和 USB 设备通讯. |
CAP_SYS_TTY_CONFIG | 进行 tty 配置任务的能力. |
poll
搜索系统调用定义小技巧: SYSCALL_DEFINE.*(poll
驱动的这些接口的用户是系统调用, 所以在驱动中如何去实现这些接口需要考虑系统调用需要什么.
驱动poll的客户
为了区分系统调用和file_operatar, 我会在系统调用前加上sys_前缀, 例如sys_poll.
poll的客户有sys_select
, sys_poll
, sys_epoll
.
sys_poll
的实现大致是: 首先会调用所以的驱动程序的poll,如果没有想要的事件,则会调用schedule_timeout
进行休眠, 直到等待事件产生或者超时被唤醒. 然后会再次调用所有的驱动程序的poll, 判断是否有事件发生或者超时.
(wake up 只会唤醒进程,不会移除wait queue里的内容,wait queue里的内容会在检查完事件后被移除, 并且poll里poll_wait不会添加重复的项目,所以poll_wait可以被多次调用)
所以poll需要把当前进程添加到等待队列中, 这样才能在合适的地方唤醒进程.
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
并且要返回当前的设备状态, 才能让系统调用知道是否有事件发生,状态定义如下:
- POLLIN 有数据可以读取。
- POLLRDNORM 等同于 POLLIN。
- POLLPRI 有紧急的数据需要读取
- POLLOUT 可以写数据。
- POLLNVAL 无效的请求。
- POLLHUP 指定的文件描述符挂起。
- POLLERR 指定的文件描述符发生错误。
fasync 异步通知
内核中使用cdev
管理驱动程序.
- 使用
cdev_alloc
函数分配一个cdev
结构体 - 使用
cdev_init(struct cdev *cdev, struct file_operations *fops)
函数初始化cdev
结构体, 把设备操作函数和cdev绑定. - 使用
cdev_add(struct cdev *cdev, dev_t dev, unsigned int count)
函数将cdev
添加到内核中, 和设备号绑定在一起, 一个cdev可以和多个设备号绑定在一起,这个函数会把dev - (dev+count-1)的设备号都绑定到这个cdev.
- 在不需要的时候,使用
cdev_del(struct cdev *cdev)
函数将cdev
从内核中删除, 使其失效.
使用模块技术加载进入内核.
创建设备节点(文件)
设备节点通常是在/dev
目录下, 通过mknod
命令创建, 也可以通过udev
自动创建.
mknod 命令格式: c 表示字符设备, b 表示块设备
mknod /dev/设备名 c 主设备号 次设备号
创建设备节点的时候会根据设备号把对应的cdev放在文件的inode中, 这样就能通过这个设备节点文件访问到对应的设备驱动程序.
使用动态分配设备号的一个问题就是不能提前知道设备号, 不能提前创建设备节点. 只能在分配通过/proc/devices
查看设备号, 然后创建设备节点.
下面是一个创建设备节点的脚本:
#!/bin/sh
module="scull"
device="scull"
mode="664"
# invoke insmod with all arguments we got
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod ./$module.ko $* || exit 1 # $*表示所以传入当前脚本的参数, || exit 1表示如果insmod执行失败,则exit 1 错误退出
# remove stale nodes
rm -f /dev/${device}[0-3]
major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices)
mknod -m ${mode} /dev/${device}0 c $major 0
mknod -m ${mode} /dev/${device}1 c $major 1
mknod -m ${mode} /dev/${device}2 c $major 2
mknod -m ${mode} /dev/${device}3 c $major 3
# give appropriate group/permissions, and change the group.
# Not all distributions have staff, some have "wheel" instead.
# group="staff"
# grep -q '^staff:' /etc/group || group="wheel"
# chgrp $group /dev/${device}[0-3]
# chmod $mode /dev/${device}[0-3]
使用udev/mdev自动创建设备节点
mdev是busybox中的一个工具, 嵌入式版本的udev, 一个用户程序,
用来自动创建设备节点.
快速参考
<linux/types.h> |
---|
dev_t 内核设备号类型 |
<linux/kdev_t.h> |
---|
MAJOR (dev_t dev); |
MINOR (dev_t dev); |
MKDEV (int major, int minor); |
<linux/fs.h> |
---|
file_operations |
alloc_chrdev_region (dev_t *dev, unsigned int firstminor, unsigned int count, char *name) |
register_chrdev_region (dev_t first, unsigned int count, char *name) |
unregister_chrdev_region (dev_t first, unsigned int count); |
iminor (inode) 通过inode获取次设备号 |
imajor (inode) |
<linux/cdev.h> |
---|
struct cdev *cdev_alloc (void); |
void cdev_init (struct cdev *dev, struct file_operations *fops); |
int cdev_add (struct cdev *dev, dev_t num, unsigned int count); |
void cdev_del (struct cdev *dev); |
<linux/kernel.h> |
---|
container_of (pointer, container_type, container_field); 通过filed找到container的地址 |
<linux/uaccess.h> |
---|
copy_to_user (void __user *to, const void *from, unsigned long n) |
copy_from_user (void *to, const void __user *from, unsigned long n) |