话说字符设备驱动

最近在学习字符设备驱动,涉及到很多框架层面的东西,这里就来记录一下。

概述

  • 开始,就先把框架图放出来。

  • 在用户空间通过insmod调用module_init模块加载函数激活相应的设备驱动初始化函数

  • 接着就是添加字符设备驱动
    • 在字符设备驱动初始化前,先要分配主次设备号
    • 在对应目录中创建相应的类文件和设备文件
    • 并填充file_operations结构体
  • 字符设备驱动注册到内核后就可以使用之前应用层的readwriteioctl等函数
    • 应用层所使用的对文件进行读写操作的函数都绑定了file_operations中的方法

应用层调用接口

常用方法

  1. open打开设备文件
  2. read读取设备文件内容
  3. write写入设备文件内容
  4. ioctl进行IO操作
  5. close关闭设备文件

设备类

  • struct kobject数据结构在sysfs中代表一个目录
  • struct driverstruct devicestruct class均由kobject派生
  • struct driver_attributestruct device_attributestruct class_attribute代表普通文件
  • struct ksetstruct kobject的容器,可以成为同一类struct kobject的父亲,而其自身也有struct kobject成员,因此其又可以和其他struct kobject成为上一级struct kset的子成员

数据结构

cdev

  • 这个是存放字符设备驱动的相关数据的结构体
1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj;
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵该字符设备文件的方法
struct list_head list; // 对应字符设备文件的inode->i_devices的链表头
dev_t dev; // 设备号
unsigned int count; // 次设备个数
};

file_operation

  • 定义字符设备驱动提供给VFS的接口函数集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *, int datasync);
int (*aio_fsync) (struct kiocb *, 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 **);
};

创建过程

  • 由于年代的变迁,字符设备号的分配接口有新的和旧的,不过它们的底层还是调用了相同的函数

1. 分配字符设备号(旧接口)

① 分配

1
2
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
  • 传入参数分别为主设备号、设备名称、文件操作集

② 释放

1
static inline void unregister_chrdev(unsigned int major, const char *name)
  • 传入参数分别为主设备号、设备名称

2. 分配字符设备号(新接口)

① 静态分配

1
int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • 传入参数分别为设备号、设备数量、设备名称

② 动态分配

1
2
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
  • 传入参数分别为设备号、次设备号基址、设备数量、设备名称

③ 释放

1
void unregister_chrdev_region(dev_t from, unsigned count)
  • 传入参数分别为设备号、设备个数

3. 创建设备文件

  • 可以在应用层使用命令行cat /proc/devices查看所有设备的设备名、以及主次设备号

① 手工创建

  • mknod filename type major minor

② 自动创建

class_create

  • 首先创建一个设备类
1
2
3
4
5
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

device_create

  • 接着创建一个设备
1
2
3
4
5
6
7
8
9
10
11
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_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
  • 在驱动代码中调用class_create()为设备创建一个设备类,再为每个设备调用device_create()创建对应的设备,udev(mdev)会自动创建一个设备文件

原理:利用udev(mdev)来实现设备文件的自动创建,由busybox配置。在加载模块的时候,用户空间的mdev会自动去/sysfs下相应的目录寻找对应的类从而创建设备结点

1
2
3
4
5
6
7
8
// /etc/init.d/rcS
#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
···中间省略···
echo /sbin/mdev > /proc/sys/kernel/hotplug // 与这个运行脚本有关
mdev -s
···中间省略···

class_destroy

  • 删除设备类
1
2
3
4
5
6
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}

device_destroy

  • 删除设备
1
2
3
4
5
6
7
8
9
void device_destroy(struct class *class, dev_t devt)
{
struct device *dev;
dev = class_find_device(class, NULL, &devt, __match_devt);
if (dev) {
put_device(dev);
device_unregister(dev);
}
}

4. 注册/注销字符设备驱动

① cdev_alloc

  • 获取一个字符设备结构体
1
2
static struct cdev *pcdev = NULL;
pcdev = cdev_alloc();

② cdev_init

  • 字符设备驱动初始化
  • 绑定字符设备结构体(cdev)与文件操作集(fops)
  • void cdev_init(struct cdev *cdev, const struct file_operations *fops)

③ cdev_add

  • 添加字符设备驱动
  • 绑定字符设备驱动(cdev)与设备号(dev)
  • int cdev_add(struct cdev *p, dev_t dev, unsigned count)

④ cdev_del

  • 注销字符设备驱动
  • void cdev_del(struct cdev *p)

5. 内核的虚拟地址映射

若需要用到gpio等资源就需要虚拟地址映射

① request_mem_region

  • 向内核请求需要映射的内存资源
  • request_mem_region(start,n,name)

② ioremap

  • 映射传入的物理地址返回一个虚拟地址
  • ioremap(cookie,size)

③ iounmap

  • 传入虚拟地址,取消地址映射
  • iounmap(cookie)

④ release_mem_region

  • 释放内核请求需要映射的内存资源
  • release_mem_region(start,n)

6. Others

① printk

  • 内核信息打印函数
1
2
3
4
5
6
7
8
9
10
11
12
13
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */
#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */
// 未指定日志级别的printk的默认级别为DEFAULT_MESSAGE_LOGLEVEL
printk(KERN_INFO "Hello, world!/n");
  • 使用命令行cat /proc/sys/kernel/printk
    显示: 4 4 1 7
  • 分别表示
    • 当前控制台日志级别
    • 未明确指定日志级别的默认信息日志级别
    • 最高允许设置的控制台日志级别
    • 引导时默认的日志级别
  • dmesg可查看printk打印的信息

② copy_from_user

  • 使用file_operations中的方法集 write函数将数据从用户空间复制到内核空间
  • `static inline unsigned long __must_check copy_from_user(void *to,
    const void __user *from, unsigned long n)`
    

③ copy_to_user

  • 使用file_operations中的方法集 read函数将数据从内核空间复制到用户空间
  • `static inline unsigned long must_check copy_to_user(void user *to,
    const void *from, unsigned long n)`
    

参考文章
Linux字符设备驱动剖析
深入理解Linux字符设备驱动

:本文内容部分来自互联网整理,部分来自个人经验总结;本文将持续收集更新,欢迎留言补充!

要是觉得不错,就鼓励一下吧!