Linux 字符驱动架构

如何往内核里添加一个字符驱动程序

分配设备号

前置:

* 设备号分为主设备号和次设备号.
* 主设备号是分配给设备驱动程序的唯一标识符,用于标识设备所属的驱动程序。它告诉内核在访问设备时应该调用哪个驱动程序来处理请求.
* 次设备号是与主设备号配合使用的较小标识符,用于区分同一主设备号下的不同设备实例
* 内核中设备号是一个`dev_t`类型的变量, 通过`MAJOR`和`MINOR`宏可以获取主设备号和次设备号.`MKDEV`宏可以将主设备号和次设备号合并成一个`dev_t`类型的变量.

申请设备号的方法有两种:

  1. 静态申请, 自己指定设备号
int register_chrdev_region(dev_t first, unsigned int count, char *name);
  1. 动态申请, 调用该函数后内核会分配一个未被使用的设备号
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管理驱动程序.

  1. 使用cdev_alloc函数分配一个cdev结构体
  2. 使用cdev_init(struct cdev *cdev, struct file_operations *fops)函数初始化cdev结构体, 把设备操作函数和cdev绑定.
  3. 使用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)
posted @ 2025-02-28 21:36  天刚刚破晓  阅读(22)  评论(0)    收藏  举报