kernel——字符设备驱动
字符设备驱动的框架
设备节点:inode,类型为字符设备,记录设备号
设备号:内核确定驱动的唯一编号
cdev:字符驱动对象
框架代码
驱动
#include <linux/module.h>
#include <linux/file.h>
#include <linux/rtc.h>
static ssize_t rtc_read (struct file *fp, char __user *buf, size_t sz, loff_t *pos)
{
printk("%s\n", __func__);
return 0;
}
static ssize_t rtc_write (struct file *fp, const char __user *buf, size_t sz, loff_t *pos)
{
printk("%s\n", __func__);
return 0;
}
static int rtc_open (struct inode *pinode, struct file *fp)
{
printk("%s\n", __func__);
return 0;
}
static int rtc_release (struct inode *pinode, struct file *fp)
{
printk("%s\n", __func__);
return 0;
}
static const struct file_operations rtc_fops = {
.owner = THIS_MODULE,
.read = rtc_read,
.write = rtc_write,
.open = rtc_open,
.release = rtc_release,
};
static int __init rtc_init(void)
{
if (register_chrdev(222, "rtc-demo", &rtc_fops) < 0) {
printk("failed to register_chrdev\n");
return -1;
}
return 0;
}
static void __exit rtc_exit(void)
{
}
module_init(rtc_init);
module_exit(rtc_exit);
MODULE_LICENSE("GPL");
测试应用
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct rtc_time {
unsigned int year;
unsigned int mon;
unsigned int day;
unsigned int hour;
unsigned int min;
unsigned int sec;
};
int main()
{
int fd;
struct rtc_time tm;
if ((fd = open("/dev/rtc-demo", O_RDWR)) < 0) {
perror("open");
return -1;
}
if (read(fd, &tm, sizeof(tm)) < sizeof(tm)) {
perror("read");
return -1;
}
printf("%d:%d:%d\n", tm.hour, tm.min, tm.sec);
close(fd);
return 0;
}
运行前构造inode
mknod /dev/rtc-demo c 222 0
register_chrdev 分析
注册 字符设备的关键是将 cdev 放到一个容器中,待后续使用。
注册设备驱动需要的操作
- 申请设备号
- 构建 cdev 结构
- 使用设备号作为键,将cdev放到 kobj_map 容器中。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
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;
};
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1; // 次设备号获得创建设备数量
unsigned index = MAJOR(dev); // 使用 主设备号做哈希键
unsigned i;
struct probe *p;
if (n > 255)
n = 255;
p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
if (p == NULL)
return -ENOMEM;
for (i = 0; i < n; i++, p++) { // 每个设备的probe复制
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data; // 这里data 为 cdev
}
mutex_lock(domain->lock);
for (i = 0, p -= n /* 把p复位,指向要加入的首个probe */; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255]; // probes是元素为指针的数组,s指向数组元素
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s; // 加入链表
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
注册完cdev后,cdev加入cdev_map表待使用
open分析
- 根据pathname找到 inode
- 根据inode的i_flag知道是设备文件,并使用 inode->i_rdev找到驱动
- 将cdev->ops复制给 file->f_op,返回file
open后,file->f_op就关联了驱动代码
具体看如何通过 设备号找到 cdev 和 fops
int chrdev_open(struct inode * inode, struct file * filp)
{
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
p = inode->i_cdev; // 第一次open时,p == NULL
if (!p) { // 第一次open
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); // 根据设备号找 kobj
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj); // struct cdev 包含 kobj 属性,偏移以获得 cdev
spin_lock(&cdev_lock);
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
inode->i_cindex = idx;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
filp->f_op = fops_get(p->ops); // 根据cdev->ops 获得 fops 记录到 file
if (!filp->f_op) {
cdev_put(p);
return -ENXIO;
}
if (filp->f_op->open) {
lock_kernel();
ret = filp->f_op->open(inode,filp); // 回调 具体驱动的 open
unlock_kernel();
}
if (ret)
cdev_put(p);
return ret;
}
根据 设备号获得 kobj
struct kobject *kobj_lookup(struct kobj_map *domain, dev_t dev, int *index)
{
struct kobject *kobj;
struct probe *p;
unsigned long best = ~0UL;
retry:
mutex_lock(domain->lock);
for (p = domain->probes[MAJOR(dev) % 255] /* 以主设备号做哈希键 */; p; p = p->next) {
struct kobject *(*probe)(dev_t, int *, void *);
struct module *owner;
void *data;
if (p->dev > dev || p->dev + p->range - 1 < dev)
continue;
if (p->range - 1 >= best)
break;
if (!try_module_get(p->owner))
continue;
owner = p->owner;
data = p->data;
probe = p->get;
best = p->range - 1;
*index = dev - p->dev;
if (p->lock && p->lock(dev, data) < 0) {
module_put(owner);
continue;
}
mutex_unlock(domain->lock);
kobj = probe(dev, index, data);
/* Currently ->owner protects _only_ ->probe() itself. */
module_put(owner);
if (kobj)
return kobj; // return
goto retry;
}
mutex_unlock(domain->lock);
return NULL;
}
read分析
可见由于open后,file->f_op 为 cdev->fops 所以,基于文件的操作都通过file->f_op就能调用驱动。
另一种写法
#include <linux/module.h>
#include <linux/file.h>
#include <linux/rtc.h>
static ssize_t rtc_read (struct file *fp, char __user *buf, size_t sz, loff_t *pos)
{
printk("%s\n", __func__);
return 0;
}
static ssize_t rtc_write (struct file *fp, const char __user *buf, size_t sz, loff_t *pos)
{
printk("%s\n", __func__);
return 0;
}
static int rtc_open (struct inode *pinode, struct file *fp)
{
printk("%s\n", __func__);
return 0;
}
static int rtc_release (struct inode *pinode, struct file *fp)
{
printk("%s\n", __func__);
return 0;
}
static const struct file_operations rtc_fops = {
.owner = THIS_MODULE,
.read = rtc_read,
.write = rtc_write,
.open = rtc_open,
.release = rtc_release,
};
static dev_t dev;
static struct cdev *rtc_cdev;
static int __init rtc_init(void)
{
// 构建cdev
rtc_cdev = cdev_alloc();
cdev_init(rtc_cdev, &rtc_fops);
// 申请设备号
dev = MKDEV(222, 0);
register_chrdev_region(dev, 1, "rtc-demo");
// 将cdev加入kobj树
cdev_add(rtc_cdev, dev, 1);
return 0;
}
static void __exit rtc_exit(void)
{
// 将cdev从kobj树移除
cdev_del(rtc_cdev);
// 释放设备号
unregister_chrdev_region(dev, 1);
}
module_init(rtc_init);
module_exit(rtc_exit);
MODULE_LICENSE("GPL");
动态申请设备号
主设备号高12位,次设备号低20位
设备号的转换
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
// 申请设备号
#if 0
dev = MKDEV(222, 0);
register_chrdev_region(dev, 1, "rtc-demo");
#else
alloc_chrdev_region(&dev, 0, 1, "rtc-demo");
printk("major : %d, minor : %d\n", MAJOR(dev), MINOR(dev));
#endif
自动创建设备节点
由于动态申请设备号所以需要自动创建设备节点
udev/mdev : 一个用户空间程序,通过扫描sysfs获得内核编号,用于创建设备节点。此外该程序还依赖 tmpfs。
为了udev获得驱动的信息,驱动必须将信息导出到sysfs,方法是 class_create , device_create
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to device_create().
*
* Returns &struct class pointer on success, or ERR_PTR() on error.
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
/**
* device_create - creates a device and registers it with sysfs
* @class: pointer to the struct class that this device should be registered to
* @parent: pointer to the parent struct device of this new device, if any
* @devt: the dev_t for the char device to be added
* @drvdata: the data to be added to the device for callbacks
* @fmt: string for the device's name
*
* This function can be used by char device classes. A struct device
* will be created in sysfs, registered to the specified class.
*
* A "dev" file will be created, showing the dev_t for the device, if
* the dev_t is not 0,0.
* If a pointer to a parent struct device is passed in, the newly created
* struct device will be a child of that device in sysfs.
* The pointer to the struct device will be returned from the call.
* Any further sysfs files that might be required can be created using this
* pointer.
*
* Returns &struct device pointer on success, or ERR_PTR() on error.
*
* Note: the struct class passed to this function must have previously
* been created with a call to class_create().
*/
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_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, vargs);
va_end(vargs);
return dev;
}
static const struct file_operations rtc_fops = {
.owner = THIS_MODULE,
.read = rtc_read,
.write = rtc_write,
.open = rtc_open,
.release = rtc_release,
};
static dev_t dev;
static struct cdev *rtc_cdev;
static struct class *rtc_test_class;
static struct device *rtc_device;
static int __init rtc_init(void)
{
// 构建cdev
rtc_cdev = cdev_alloc();
cdev_init(rtc_cdev, &rtc_fops);
// 申请设备号
#if 0
dev = MKDEV(222, 0);
register_chrdev_region(dev, 1, "rtc-demo");
#else
alloc_chrdev_region(&dev, 0, 1, "rtc-demo");
printk("major : %d, minor : %d\n", MAJOR(dev), MINOR(dev));
#endif
// 将cdev加入kobj树
cdev_add(rtc_cdev, dev, 1);
// 在/sys/class下创建rtc-class类
rtc_test_class = class_create(THIS_MODULE, "rtc-class");
if (IS_ERR(rtc_test_class)) {
printk("class_create failed\n");
return -1;
}
// 在/sys/class/rtc-class下创建rtc-demo%d设备
rtc_device = device_create(rtc_test_class, NULL, dev, NULL, "rtc-demo%d", 0);
if (IS_ERR(rtc_device)) {
printk("device_create failed\n");
return -1;
}
return 0;
}
static void __exit rtc_exit(void)
{
// 将cdev从kobj树移除
cdev_del(rtc_cdev);
// 释放设备号
unregister_chrdev_region(dev, 1);
device_unregister(rtc_device);
class_destroy(rtc_test_class);
}
module_init(rtc_init);
module_exit(rtc_exit);
加载模块后,发现/sys/class已经创建了rtc-class类
yangxr@vexpress:/root # ls /sys/class/rtc-class/
rtc-demo0
yangxr@vexpress:/root # ls /sys/class/rtc-class/rtc-demo0/
dev power subsystem uevent
yangxr@vexpress:/root # cat /sys/class/rtc-class/rtc-demo0/dev
248:0
yangxr@vexpress:/root # cat /sys/class/rtc-class/rtc-demo0/uevent
MAJOR=248
MINOR=0
DEVNAME=rtc-demo0
运行mdev -s ,创建设备节点
yangxr@vexpress:/root # mdev -s
yangxr@vexpress:/root # ls /dev/rtc-demo0
/dev/rtc-demo0
填充fops
需要注意 用户空间和内核空间基于指针输入输出数据时,相关指向用户空间的指针要用 __user 描述。
填充read write
inline static unsigned long rtc_tm_to_time(struct rtc_time_s *ptm)
{
return ptm->hour * 3600 + ptm->min * 60 + ptm->sec;
}
static ssize_t rtc_write (struct file *fp, const char __user *buf, size_t sz, loff_t *pos)
{
struct rtc_time_s tm;
int len = sizeof(tm);
if ( !access_ok(buf, len))
return -1;
if (copy_from_user(&tm, buf, len) != 0) {
printk("rtc_write failed\n");
return -1;
}
regs->RTCLR = rtc_tm_to_time(&tm);
return len;
}
static void rtc_time_s_trans(unsigned long n, struct rtc_time_s *p)
{
p->hour = (n % 86400) / 3600;
p->min = (n % 3600) / 60;
p->sec = (n % 60);
}
static ssize_t rtc_read (struct file *fp, char __user *buf, size_t sz, loff_t *pos)
{
unsigned long cur_time = regs->RTCDR;
struct rtc_time_s tm;
int len = sizeof(tm);
rtc_time_s_trans(cur_time, &tm);
if (copy_to_user(buf, &tm, len) != 0) {
printk("rtc_read error\n");
return -1;
}
return len;
}
ioctl
struct file_operations {
...
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); // 需要兼容不同位宽时使用
};
ioctl 对应两种回调,常用 unlocked_ioctl
传参都为
long (*unlocked_ioctl) (struct file *fp, unsigned int cmd, unsigned long arg);
其中cmd使用ioctl的宏设置
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
比如定义设置RTC时间的命令,和读取RTC时间的命令
#define RTC_CMD_MAGIC 'R'
#define RTC_CMD_GET _IOR(RTC_CMD_MAGIC, 0, struct rtc_time_s *)
#define RTC_CMD_SET _IOW(RTC_CMD_MAGIC, 1, struct rtc_time_s *)
ioctl实现
static long rtc_ioctl (struct file *fp, unsigned int cmd, unsigned long arg)
{
struct rtc_time_s __user *buf = (struct rtc_time_s __user *)arg;
unsigned long cur_time;
struct rtc_time_s tm;
switch (cmd) {
case RTC_CMD_GET:
cur_time = regs->RTCDR;
rtc_time_s_trans(cur_time, &tm);
if (copy_to_user(buf, &tm, sizeof(tm)) != 0) {
return -1;
}
break;
case RTC_CMD_SET:
if (copy_from_user(&tm, buf, sizeof(tm)) != 0) {
return -1;
}
cur_time = rtc_tm_to_time(&tm);
regs->RTCLR = cur_time;
break;
default:
return -1;
}
return 0;
}
private_data
struct file {
...
/* needed for tty driver, and maybe others */
void *private_data;
};
fp->private_data可用于实现驱动会话。
如在 open 操作时分配会话上下文,挂在 private_data上,其他操作调用时 fp->private_data 可以获得当前用户的会话信息。
提高驱动的安全性
为了避免用户非法输入导致内核挂掉,需要先检查用户数据。
常见的检查方法:
检查ioctl命令
_IOC_TYPE(cmd) 判断命令type是否合法
_IOC_DIR(cmd) 判断命令是读还是写
检查用户内存地址是否合法
access_ok(addr, sz) 判断用户传递的内存是否合法
返回值:1 成功,0 失败
有些函数内部自带检测:copy_from_user, copy_to_user, get_user, put_user
分支预测优化:likely, unlikely
比如
if (copy_from_user(&tm, buf, len) != 0) { // 由于这里出错可能性低
printk("rtc_write failed\n"); // 所以这里不应该先缓存
return -1;
}
regs->RTCLR = rtc_tm_to_time(&tm); // 应该先缓存这里
使用 unlikely 告诉编译器为真的分支大概率不会执行,于是cache缓存时会跳过那部分指令
if (unlikely(copy_from_user(&tm, buf, len) != 0)) {
printk("rtc_write failed\n");
return -1;
}
regs->RTCLR = rtc_tm_to_time(&tm);
制作库
由于直接基于文件系统导出的驱动接口不能直观描述接口功能,所以还需要做一个库,库直接操作驱动接口,用户使用库实现应用。
如 rtc的库可以这样声明
#ifndef __RTCLIB_H
#define __RTCLIB_H
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct rtc_time {
unsigned int year;
unsigned int mon;
unsigned int day;
unsigned int hour;
unsigned int min;
unsigned int sec;
};
int rtc_open(const char *pathname);
int rtc_close(int fd);
int rtc_set_time(int fd, struct rtc_time *ptm);
int rtc_get_time(int fd, struct rtc_time *ptm);
#endif
示例
有三个led灯,创建4个设备 /dev/leds /dev/led1 /dev/led2 /dev/led3,提供开启和关闭操作
#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/module.h>
dev_t dev;
struct cdev *cdev;
struct class *led_class;
static void *gpio_va;
#define GPIO_OFT(x) ((x) - 0x56000000)
#define GPFCON (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
#define GPFDAT (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))
#define LED_NUM 3
#define DEVICE_NAME "led"
ssize_t led_write (struct file *fp, const char __user *user, size_t sz, loff_t *offset)
{
unsigned int minor, val;
/* 获得当前设备的此设备号 */
minor = MINOR(fp->f_path.dentry->d_inode->i_rdev);
if (copy_from_user(&val, user, sz) != 0) {
printk(DEVICE_NAME "Failed to copy_from_user\n");
return -1;
}
switch (minor) {
case 0: /* /dev/leds */
if (val == 0)
GPFDAT |= (1 << 4) | (1 << 5) | (1 << 6);
else
GPFDAT &= ~((1 << 4) | (1 << 5) | (1 << 6));
break;
case 1: /* /dev/led1 */
if (val == 0)
GPFDAT |= (1 << 4);
else
GPFDAT &= ~(1 << 4);
break;
case 2: /* /dev/led2 */
if (val == 0)
GPFDAT |= (1 << 5);
else
GPFDAT &= ~(1 << 5);
break;
case 3: /* /dev/led3 */
if (val == 0)
GPFDAT |= (1 << 6);
else
GPFDAT &= ~(1 << 6);
break;
}
return 0;
}
static int led_open (struct inode *inode, struct file *fp)
{
/* 将GPIO设置为输出模式 */
GPFCON &= ~((0x3 << 8) | (0x3 << 10) | (0x3 << 12));
GPFCON |= (1 << 8) | (1 << 10) | (1 << 12);
return 0;
}
static const struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
};
static int __init led_init(void)
{
unsigned int major, i;
/* 把寄存器映射从物理地址到虚拟地址 */
gpio_va = ioremap(0x56000000, 0x100000);
if (gpio_va == NULL)
return -EIO;
/* 动态申请主设备号, 次设备号从0开始,一个三个设备 */
if (alloc_chrdev_region(&dev, 0, LED_NUM + 1, "led") < 0) {
printk(DEVICE_NAME "Failed to alloc dev number\n");
return -1;
}
/* 构造cdev */
if ((cdev = cdev_alloc()) == NULL) {
printk(DEVICE_NAME "Failed to alloc cdev\n");
return -1;
}
cdev->owner = THIS_MODULE;
cdev->ops = &led_ops;
/* 将cdev加入cdev_map,这样才能通过设备号找到cdev */
if (cdev_add(cdev, dev, LED_NUM + 1) < 0) {
printk(DEVICE_NAME "Failed to add cdev to system\n");
return -1;
}
/* 创建class,方便mdev扫描/sys以添加设备节点 */
led_class = class_create(THIS_MODULE, "led");
if (led_class == NULL) {
printk(DEVICE_NAME "Failed to create class\n");
return -1;
}
/* 创建4个device,主设备号由系统分配,此设备号从0开始 */
major = MAJOR(dev);
device_create(led_class, NULL, MKDEV(major, 0), "leds");
for (i = 1; i < LED_NUM + 1; i++) {
if (device_create(led_class, NULL, MKDEV(major, i), "led%d", i) == NULL) {
printk(DEVICE_NAME "Failed to creat dev led%d", i);
return -1;
}
}
printk(DEVICE_NAME "init\n");
return 0;
}
static void __exit led_exit(void)
{
unsigned int major, i;
/* 释放4个device */
major = MAJOR(cdev->dev);
for (i = 0; i < LED_NUM + 1; i++) {
device_destroy(led_class, MKDEV(major, i));
}
/* 释放class */
class_destroy(led_class);
/* 将cdev从cdev_map中移除, 并释放cdev */
cdev_del(cdev);
/* 释放主设备号 */
unregister_chrdev_region(dev, 0);
/* 释放虚拟空间 */
iounmap(gpio_va);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2021-11-16 cii——异常与断言