Linux驱动开发笔记

环境搭建

Linux驱动开发依赖驱动运行内核的版本和对应的编译工具。内核版本查看使用命令uname -r

对于嵌入式来说,一般都是源码编译,编译驱动时需要指定目标系统的内核源码。此外,需要设置交叉编译工具变量CROSS_COMPILE为指定的工具。

对于通用内核来说,可以使用包管理工具直接下载内核,例如在Ubuntu系统中使用命令sudo apt-get install -y linux-headers-$(uname -r)。默认内核安装路径在/lib/modules/$(uname -r)。编译驱动时,需要将内核路径指定到/lib/modules/$(uname -r)/build。对应的头文件在路径/usr/src/linux-headers-$(uname -r)/include中。

对于wsl来说,与通用内核类似,只是内核需要单独从微软wsl2内核仓库下载。然后手动进行编译安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装编译wsl特供内核依赖
sudo apt-get install -y libelf-dev build-essential pkg-config bison build-essential flex libssl-dev libelf-dev bc dwarves

# 解压进入到目录
tar -xvf WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1.tar.gz
cd WSL2-Linux-Kernel-linux-msft-wsl-5.15.90.1
cp Microsoft/config-wsl .config

# 编译安装
sudo make scripts
sudo make modules -j$(nproc)
sudo make modules_install

cd ..

驱动简介

Linux驱动可以使用多种方式加载到系统中。

基本框架

hello.c文件的内容如下

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <linux/module.h> // 包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> // 包含模块编程相关的宏定义,如:MODULE_LICENSE

/*
init初始化函数在模块被插入进内核时调用,主要作用为驱动功能做好预备工作被称为模块的入口函数

__init的作用 :
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init hello_init(void)
{
// 具体初始化逻辑
printk("hello module init.\n");
return 0;
}

/*
module_init 宏
1. 用法:module_init(模块入口函数名)
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(hello_init);

/*
exit退出函数在模块从内核中被移除时调用,主要作用做些init函数的反操作被称为模块的出口函数

__exit的作用:
1. 一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .exit.text区段
3. 在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit hello_exit(void)
{
// 具体反初始化逻辑
printk("hello module exit.\n");
}

/*
module_exit宏
1. 用法:module_exit(模块出口函数名)
2. 动态加载的模块在卸载时,对应函数被调用
3. 静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5. 对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(hello_exit);

/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2" "GPL and additional rights" "Dual BSD/GPL" "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用
其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:
myhello:module license 'unspecified' taints kernel
Disabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");

Makefile文件的内容如下

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
ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /root/ldd4/linux-4.14.334
# OBJECTDIR ?= /root/ldd4/objects/vexpress-v2p-ca9
ROOTFS ?= /root/ldd4/rootfs-arm32
CROSS_COMPILE ?= arm-linux-gnueabi-
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) CROSS_COMPILE=$(CROSS_COMPILE) O=$(OBJECTDIR) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
rm -rf *.o *.ko .*.cmd *.mod* modules.order Module.symvers .tmp_versions

else
obj-m += hello.o

endif

驱动编译成功后,使用附录中的常用命令进行测试。

字符设备驱动

Linux字符设备会涉及到关键数据结构cdevfile_operations结构体的操作方法。

cdev定义在文件include/linux/cdev.h中,主要描述设备驱动基本信息。

file_operations定义在文件include/linux/fs.h中,主要描述设备驱动提供的基本接口函数,比如open、read、write、llseek、unlocked_ioctl等基本操作函数。

关键函数

初始化基本流程为:注册设备号->申请驱动内存->初始化与新增字符设备描述符

注销基本流程为:删除字符设备描述符->释放驱动内存->删除设备号

设备号

1
2
3
4
5
6
7
typedef u32 dev_t;
// 生成设备号
#define MKDEV(ma,minor) (((ma) << 20) | (mi))
// 获取主设备号
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
// 获取次设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

dev_t为设备编号类型,为32位无符号整数。

  • ma为主设备号major
  • mi为次设备号minor

在创建设备节点时,通过设备号来绑定驱动模块。

1
2
3
4
5
6
// 静态注册设备号
int register_chrdev_region(dev_t dev, unsigned count, const char *name);
// 动态申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned minor, unsigned count, const char *name);
// 注销设备号
void unregister_chrdev_region(dev_t dev, unsigned count);
  • dev为设备编号,动态申请时通过指针传值方式返回
  • minor为需要分配的起始次设备号
  • count为需要分配的设备数量,主设备号一样,次设备号依次累加
  • name为设备驱动名称,可以在/proc/devices中查看

驱动内存

值得注意的是,设备驱动运行在内核空间,因此内存需要使用内核内存管理函数。

1
2
3
4
// 申请内存并置零。
void *kzalloc(size_t size, gfp_t flags);
// 内存释放
void kfree(const void *p)
  • size申请内存大小
  • flags内存标志位,这里使用GFP_KERNEL
  • p内存指针指向待释放的内存

字符设备

1
2
3
4
5
6
// 初始化字符设备描述符
void cdev_init(struct cdev *cdev, const struct file_operations *ops);
// 新增字符设备描述符
int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);
// 删除字符设备描述符
void cdev_del(struct cdev *cdev);
  • cdev字符设备描述符
  • ops文件操作描述符
  • dev设备编号

其他宏

名称作用必选
module_init导出设备驱动的初始化函数
module_exit导出设备驱动的退出函数
module_param导出设备驱动参数
MODULE_LICENSE声明许可信息
MODULE_AUTHOR声明作者信息
MODULE_DESCRIPTION声明描述信息

简单的模拟缓存设备

支持最多创建10个设备节点,每个节点可以读取或写入数据。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

#define GLOBALMEM_SIZE 0x1000
#define GLOBALMEM_MAJOR 230
#define GLOBALMEM_MINOR 0
#define DEVICE_NUM 10

// 为避免多个设备命令污染,Linux推荐使用_IO _IOR _IOW _IOWR来定义ioctl的命令
// 已经定义的设备类型可以见内核文档Documentation/ioctl/ioctl-number.txt
// 内核预定义的控制命令不会被设备驱动处理,这些定义在include/uapi/asm-generic/ioctls.h
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)

static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);

typedef struct globalmem_dev
{
// 设备号
dev_t id;
// 字符设备
struct cdev cd;
// 模拟的设备内存
unsigned char mem[GLOBALMEM_SIZE];
} globalmem_dev_t;

globalmem_dev_t *globalmem_devp;

// 设备驱动的打开函数
static int globalmem_open(struct inode *inode, struct file *filp)
{
globalmem_dev_t *dev = container_of(inode->i_cdev, globalmem_dev_t, cd);
filp->private_data = dev;
return 0;
}

// 设备驱动的释放函数
static int globalmem_release(struct inode *, struct file *)
{
return 0;
}

// 设备驱动的I/O控制函数
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
globalmem_dev_t *dev = filp->private_data;

switch (cmd)
{
case MEM_CLEAR:
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
break;

default:
return -EINVAL;
}

return 0;
}

// 设备驱动的读操作。*ppos是要读的位置相对于内存开头的偏移,如果大于或等于GLOBALMEM_SIZE,则会返回0(EOF)
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
globalmem_dev_t *dev = filp->private_data;

if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;

if (copy_to_user(buf, dev->mem + p, count))
{
ret = -EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}

return ret;
}
// 驱动设备的写操作
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
globalmem_dev_t *dev = filp->private_data;

if (p > GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;

if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}

return ret;
}

// 设备驱动的定位操作。
static loff_t globalmem_llseek_impl(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
loff_t f_pos = 0;

switch (orig)
{
case 1: /* 从内存当前位置开始seek */
f_pos = filp->f_pos;
case 0: /* 从内存开头位置seek */
if ((f_pos + offset) < 0)
{
ret = -EINVAL;
break;
}
if ((f_pos + offset) > GLOBALMEM_SIZE)
{
ret = -EINVAL;
break;
}
filp->f_pos = f_pos;
ret = filp->f_pos;
break;

default:
ret = -EINVAL;
break;
}

return ret;
}

// 设备驱动的文件操作结构体
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek_impl,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};

// cdev的初始化和添加
static void globalmem_setup_cdev(globalmem_dev_t *dev)
{
int err;
cdev_init(&dev->cd, &globalmem_fops);
dev->cd.owner = THIS_MODULE;
err = cdev_add(&dev->cd, dev->id, 1);
if (err)
printk(KERN_NOTICE "Error %d adding globalmem", err);
}

// 设备驱动的初始化函数
static int __init globalmem_init(void)
{
int ret;
int i;

// 设备号的申请
dev_t id = MKDEV(globalmem_major, GLOBALMEM_MINOR);
if (globalmem_major)
// 1. 静态申请设备号
ret = register_chrdev_region(id, DEVICE_NUM, "globalmemND");
else
{
// 2. 动态申请设备号
ret = alloc_chrdev_region(&id, 0, DEVICE_NUM, "globalmemND");
globalmem_major = MAJOR(id);
}
if (ret < 0)
return ret;

// 从内核中申请一份globalmem_dev的内存并清零
globalmem_devp = kzalloc(sizeof(globalmem_dev_t) * DEVICE_NUM, GFP_KERNEL);
if (!globalmem_devp)
{
ret = -ENOMEM;
goto fail_malloc;
}

for (i = 0; i < DEVICE_NUM; ++i)
{
(globalmem_devp + i)->id = MKDEV(globalmem_major, i);
globalmem_setup_cdev(globalmem_devp + i);
}
return 0;

fail_malloc:
unregister_chrdev_region(id, DEVICE_NUM);
return ret;
}
// 导出设备驱动的初始化函数
module_init(globalmem_init);

// 设备驱动的退出函数
static void __exit globalmem_exit(void)
{
int i;
for (i = 0; i < DEVICE_NUM; ++i)
cdev_del(&(globalmem_devp + i)->cd);
kfree(globalmem_devp);
unregister_chrdev_region(globalmem_devp->id, DEVICE_NUM);
}
// 导出设备驱动的退出函数
module_exit(globalmem_exit);

// 作者版权声明
MODULE_AUTHOR("johnny <johnny@gmail.com>");
MODULE_LICENSE("GPL v2");

阻塞和非阻塞

异步通知和异步I/O

异步通知

异步通知使用Linux信号机制。

设备驱动中使用异步通知,主要用到一个数据结构和两个函数。

1
2
3
4
5
6
7
// 异步通知数据结构
struct fasync_struct;
// 处理标志变更
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
// 释放信号函数
void kill_fasync(struct fasync_struct **fa, int sig, int band);

异步I/O

Linux内核AIO。

AIO无法解决系统调用问题,已经被摒弃,使用io_uring替代AIO机制。一文图解原理|Linux I/O 神器之 io_uring

中断与时钟

Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)。

ARM Linux默认情况下,中断都是在CPU0上产生的,需要通过接口irq_set_affinity把中断irq设定到CPU i上去。

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。

中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。

关键函数

顶半部

1
2
3
4
5
6
7
8
9
10
11
12
typedef irqreturn_t (*irq_handler_t)(int, void *);
typedef int irqreturn_t;
// 申请irq
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
// 申请irq。区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出错处理和remove()接口里再显式的释放。
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id);
// 释放irq
void free_irq(unsigned int irq,void *dev_id);
// 屏蔽使能中断源
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);

中断共享需要在申请时,增加IRQF_SHARED标志。

底半部

Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。

内核定时器

定时器

timer_list

工作队列

内核延时

短延时

1
2
3
4
5
6
7
8
9
10
// 忙等待
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
// 睡眠
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
// 睡着延时
schedule_timeout

内存与I/O访问

x86处理器中存在I/O空间的概念,而大多数嵌入式微处理器中并不提供I/O空间。

Linux内存管理

在Linux系统中,进程的4GB内存空间被分为两个部分——用户空间与内核空间。用户空间的地址一般分布为03GB(即PAGE_OFFSET,在0x86中它等于0xC0000000),而34GB为内核空间。用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只能通过系统调用等方式才可以访问到内核空间。

内核地址空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域。

对于x86系统而言,一般情况下,物理内存映射区最大长度为896MB。当系统物理内存大于896MB时,超过物理内存映射区的那部分内存称为高端内存。

内核空间最顶部FIXADDR_TOP~4GB的区域作为保留区。

紧接着最顶端的保留区以下的一段区域为专用页面映射区(FIXADDR_START~FIXADDR_TOP)。

virt_to_phys()和phys_to_virt()方法仅适用于DMA和常规区域,高端内存的虚拟地址与物理地址之间不存在如此简单的换算关系。

内存存取

用户空间动态申请

1
2
malloc
free

内核空间动态申请

1
2
3
4
5
6
7
8
9
// 依赖底层__get_free_pages()来实现,分配标志的前缀GFP正好是这个底层函数的缩写。
// 最常用的分配标志是GFP_KERNEL,其含义是在内核空间的进程中申请内存。
// 使用GFP_KERNEL标志申请内存时,若暂时不能满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用GFP_KERNE申请内存。
// 使用GFP_ATOMIC标志申请内存时,若不存在空闲页,则不等待,直接返回。
void *kmalloc(size_t size, int flags);
kfree
// 一般只为存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存
void *vmalloc(unsigned long size);
void vfree(void * addr);

slab缓存

完全使用页为单元申请和释放内存容易导致浪费(如果要申请少量字节,也需要用1页);另一方面,在操作系统的运作过程中,经常会涉及大量对象的重复生成、使用和释放内存问题。如果我们能够用合适的方法使得对象在前后两次被使用时分配在同一块内存或同一类内存空间且保留了基本的数据结构,就可以大大提高效率。slab算法就是针对上述特点设计的。

1
2
3
4
5
6
7
// 创建slab缓存
struct kmem_cache *kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags,void (*ctor)(void*, struct kmem_cache *, unsigned long),void (*dtor)(void*, struct kmem_cache *, unsigned long));
// 分配和释放slab缓存
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
// 回收slab缓存
int kmem_cache_destroy(struct kmem_cache *cachep);

内存池

内存池技术也是一种非常经典的用于分配大量小对象的后备缓存技术。

1
2
3
4
5
6
7
8
9
// 创建内存池
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,mempool_free_t *free_fn, void *pool_data);
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data); // 标准对象分配的函数指针
typedef void (mempool_free_t)(void *element, void *pool_data); // 标准对象回收的函数指针
// 内存池中分配和回收对象
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
// 回收内存池
void mempool_destroy(mempool_t *pool);

I/O端口和I/O内存

当位于I/O空间时,通常被称为I/O端口;当位于内存空间时,对应的内存空间被称为I/O内存。

I/O端口访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 读写字节端口(8位宽)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
// 读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
// 读写长字端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
// 读写一串字节
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
// 读写一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
// 读写一串长字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);

I/O内存访问

在内核中访问I/O内存(通常是芯片内部的各个I2C、SPI、USB等控制器的寄存器或者外部内存总线上的设备)之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址上。

1
2
3
4
5
6
// 返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围,这个虚拟地址位于vmalloc映射区域。
// 通过devm_ioremap进行的映射通常不需要在驱动退出和出错处理的时候进行iounmap
void *ioremap(unsigned long offset, unsigned long size);
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,unsigned long size);
// 释放ioremap映射的地址
void iounmap(void * addr);

是Linux内核推荐用一组标准的API来完成设备内存映射的虚拟地址的读写。

没有_relaxed后缀的版本与有_relaxed后缀的版本的区别是前者包含一个内存屏障。

以下分别是读写8bit、16bit、32bit的寄存器的版本。

1
2
3
4
5
6
7
8
// 读寄存器
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16__v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
// 写寄存器
#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })

申请释放I/O端口和I/O内存

Linux内核提供了一组函数以申请和释放I/O端口,表明该驱动要访问这片区域。

1
2
3
4
5
// 申请I/O端口
// 变体devm_request_region
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
// 归还I/O端口
void release_region(unsigned long start, unsigned long n);

Linux内核也提供了一组函数以申请和释放I/O内存的范围。此处的“申请”表明该驱动要访问这片区域,它不会做任何内存映射的动作,更多的是类似于“reservation”的概念。

1
2
3
4
5
// 申请I/O内存
// 变体devm_request_mem_region
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
// 归还I/O内存
void release_mem_region(unsigned long start, unsigned long len);

I/O端口和I/O内存访问流程

I/O端口访问的一种途径是直接使用I/O端口操作函数:在设备打开或驱动模块被加载时申请I/O端口区域,之后使用inb()、outb()等进行端口访问,最后,在设备关闭或驱动被卸载时释放I/O端口范围。

I/O内存的访问步骤,首先是调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间虚拟地址,之后就可以通过Linux设备访问编程接口访问这些设备的寄存器了。访问完成后,应对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的I/O内存资源。

设备地址映射到用户空间

一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap()函数,这个函数可使得用户空间能直接访问设备的物理地址。

mmap()必须以PAGE_SIZE为单位进行映射。

1
2
// 驱动中mmap原型
int(*mmap)(struct file *, struct vm_area_struct*);

vm_operations_struct结构体的实体会在file_operations的mmap()成员函数里被赋值给相应的vma->vm_ops。一般open()函数也通常在mmap()里调用,close()函数会在用户调用munmap()的时候被调用到。

1
2
// 创建页表项
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot);

I/O内存被映射时需要是nocache的,这时候,我们应该对vma->vm_page_prot设置nocache标志之后再映射。

当访问的页不在内存里,即发生缺页异常时,fault()会被内核自动调用,而fault()的具体行为可以自定义。

I/O内存静态映射

在将Linux移植到目标电路板的过程中,有得会建立外设I/O内存物理地址到虚拟地址的静态映射,这个映射通过在与电路板对应的map_desc结构体数组中添加新的成员来完成。

1
2
3
4
5
6
7
8
struct map_desc
{
unsigned long virtual; /* 虚拟地址 */
unsigned long pfn; /* __phys_to_pfn(phy_addr) */
unsigned long length; /* 大小 */
unsigned int type; /* 类型 */
};
// 然后通过函数iotable_init(struct map_desc&, size_t)建立映射。

驱动工程师可以对非常规内存区域的I/O内存(外设控制器寄存器、MCU内部集成的外设控制器寄存器等)依照电路板的资源使用情况添加到map_desc数组中,但是目前该方法已经不值得推荐。

DMA

DMA是一种无须CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。

DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后处理。

DMA与Cache一致性

如果DMA的目的地址与Cache所缓存的内存地址访问有重叠,经过DMA操作,与Cache缓存对应的内存中的数据已经被修改,而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据。这样就会发生Cache与内存之间数据“不一致性”的错误。

Cache的不一致性问题并不是只发生在DMA的情况下,实际上,它还存在于Cache使能和关闭的时刻。

Linux下的DMA编程

申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得的内存位于DMA区域中,并具备DMA能力。

在内核中定义了__get_free_pages()针对DMA的“快捷方式”__get_dma_pages(),它在申请标志中添加了GFP_DMA。

如果不想使用log2size(即order)为参数申请DMA内存,则可以使用另一个函数dma_mem_alloc()。

内核提供了如下函数以进行简单的虚拟地址/总线地址转换

1
2
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

在使用IOMMU或反弹缓冲区的情况下,上述函数一般不会正常工作。而且,这两个函数并不建议使用。

设备并不一定能在所有的内存地址上执行DMA操作,在这种情况下应该通过下列函数执行DMA地址掩码

1
int dma_set_mask(struct device *dev, u64 mask);

内核中提供了如下函数以分配一个DMA一致性的内存区域

1
2
3
4
5
6
7
8
9
10
11
12
13
// 申请Cache一致的DMA缓冲区
void * dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle,gfp_t gfp);
// 释放Cache一致的DMA缓冲区
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr,dma_addr_t handle);

// 申请写合并的DMA缓冲区
void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t*handle, gfp_t gfp);
#define dma_free_writecombine(dev,size,cpu_addr,handle) \
dma_free_coherent(dev,size,cpu_addr,handle)

// PCI设备申请DMA缓冲区
void * pci_alloc_consistent(struct pci_dev *pdev, size_t size, dma_addr_t *dma_addrp);
void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr,dma_addr_t dma_addr);

缓冲区来自内核的较上层(如网卡驱动中的网络报文、块设备驱动中要写入设备的数据等),上层很可能用普通的kmalloc()、__get_free_pages()等方法申请,这时候就要使用流式DMA映射。

对于单个已经分配的缓冲区而言,使用dma_map_single()可实现流式DMA映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DMA映射。第4个参数为DMA的方向,可能的值包括DMA_TO_DEVICE、DMA_FROM_DEVICE、DMA_BIDIRECTIONAL和DMA_NONE
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size,enum dma_data_direction direction);
// DMA反映射
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size,enum dma_data_direction direction);

// 获得DMA缓冲区的拥有权
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);
// 将其所有权返还给设备
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);

// 映射SG
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,enum dma_data_direction direction);
// 去除映射SG
void dma_unmap_sg(struct device *dev, struct scatterlist *list,int nents, enum dma_data_direction direction);
// 返回scatterlist对应的缓冲区的总线地址和缓冲区的长度
dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);

// 获得DMA缓冲区的拥有权
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);
// 将其所有权返还给设备
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg,int nents, enum dma_data_direction direction);

Linux内核目前推荐使用dmaengine的驱动架构来编写DMA控制器的驱动,同时外设的驱动使用标准的dmaengine API进行DMA的准备、发起和完成时的回调工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 申请DMA通道
struct dma_chan *dma_request_slave_channel(struct device *dev, const char *name);
struct dma_chan *__dma_request_channel(const dma_cap_mask_t *mask,dma_filter_fn fn, void *fn_param);
// 释放DMA通道
void dma_release_channel(struct dma_chan *chan);

// DMA完成回调函数原型:void (dma_fini_callback)(void*)

// 申请DMA描述符,然后填充callback和callback_param参数
dmaengine_prep_slave_single
// 把描述符插入队列
dmaengine_submit
// 发起DMA动作
dma_async_issue_pending

Linux设备驱动的软件架构思想

让驱动以某种标准方法拿到这些平台信息呢Linux总线、设备和驱动模型实际上可以做到这一点,驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,而驱动则以标准途径拿到板级信息。

一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题。在SoC系统中集成的独立外设控制器、挂接在SoC内存空间的外设等却不依附于此类总线。Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动成为platform_driver。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct platform_device
{
const char *name;
int id;
bool id_auto;
struct devicedev;
u32 num_resources;
struct resource *resource;

const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */

/* MFD cell pointer */
struct mfd_cell *mfd_cell;

/* arch specific additions */
struct pdev_archdata archdata;
};

struct platform_driver
{
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device * pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};

struct device_driver
{
const char *name;
struct bus_type *bus;

struct module *owner;
const char *mod_name; /* used for built-in modules */

bool suppress_bind_attrs; /* disables bind/unbind via sysfs */

const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;

int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct attribute_group **groups;

const struct dev_pm_ops *pm;

struct driver_private *p;
};

与platform_driver地位对等的i2c_driver、spi_driver、usb_driver、pci_driver中都包含了device_driver结构体实例成员。它其实描述了各种xxx_driver(xxx是总线名)在驱动意义上的一些共性。

资源本身由resource结构体描述

1
2
3
4
5
6
7
8
struct resource
{
resource__size_t start;
resource_size_t end;
const char *name;
unsigned long flags; // 值可以为IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORE-SOURCE_DMA等
struct resource *parent, *sibling, *child;
};

对resource的定义也通常在BSP的板文件中进行,而在具体的设备驱动中通过platform_get_resource()这样的API来获取

1
2
3
4
// 获取资源通用接口
struct resource *platform_get_resource(struct platform_device *, unsigned int,unsigned int);
// 获取IRQ资源封装接口,相当于platform_get_resource(dev, IORESOURCE_IRQ, num);
int platform_get_irq(struct platform_device *dev, unsigned int num);

platform也提供了platform_data的支持,platform_data的形式是由每个驱动自定义的

将globalfifo作为platform设备

globalfifo驱动挂接到platform总线上,这要完成两个工作:

  • 将globalfifo移植为platform驱动
  • 在板文件中添加globalfifo这个platform设备

移植时需要屏蔽module_init和module_exit宏定义的入口。

为了完成在板文件中添加globalfifo这个platform设备的工作,需要在板文件arch/arm/mach-<soc名>/mach-<板名>.c中添加相应的代码

1
2
3
4
static struct platform_device globalfifo_device = {
.name = "globalfifo",
.id = -1,
};

设备驱动分层思想

非常推荐使用misc类型设备驱动框架,编写字符类设备。

可以额外编写驱动触发设备驱动xxx_probe函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int __init globalfifodev_init(void)
{
int ret;

globalfifo_pdev = platform_device_alloc("globalfifo", -1);
if (!globalfifo_pdev)
return -ENOMEM;

ret = platform_device_add(globalfifo_pdev);
if (ret)
{
platform_device_put(globalfifo_pdev);
return ret;
}
return ret;
}
module_init(globalfifodev_init);

Linux块设备驱动

块设备是与字符设备并列的概念,这两类设备在Linux中的驱动结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多。缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。

Linux块设备驱动结构

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
29
30
// 块设备操作描述符
struct block_device_operations
{
// 打开和释放
int (*open)(struct block_device *, fmode_t);
void (*release)(struct gendisk *, fmode_t);

// I/O控制
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);

// 介质改变,以后会被check_events取代
int (*media_changed)(struct gendisk *);

// 使介质有效
int (*revalidate_disk)(struct gendisk *);

// 获取驱动器信息
int (*getgeo)(struct block_device *, struct hd_geometry *);

// 模块指针,通常指向THIS_MODULE
struct module *owner;

// 其他
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*direct_access)(struct block_device *, sector_t, void **, unsigned long *);
unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);
void (*unlock_native_capacity)(struct gendisk *);
void (*swap_slot_free_notify)(struct block_device *, unsigned long);
};

gendisk

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
29
30
31
32
33
34
35
36
37
// 磁盘描述符
struct gendisk
{
// major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同
int major;
int first_minor;
int minors;

// 描述的块设备操作集合
const struct block_device_operations *fops;
// 管理这个设备的I/O请求队列的指针
struct request_queue *queue;
// 指向磁盘的任何私有数据,与字符设备驱动的private_data类似
void *private_data;
// 表示一个分区
struct hd_struct part0;
// 容纳分区表。与part0的关系:disk->part_tbl->part[0] = &disk->part0;
struct disk_part_tbl __rcu *part_tbl;

char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);

unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */

int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;

struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};

操作函数

1
2
3
4
5
6
7
8
9
// 分配gendisk
struct gendisk *alloc_disk(int minors);
// 增加gendisk
void add_disk(struct gendisk *disk);
// 释放gendisk
void del_gendisk(struct gendisk *gp);
// gendisk引用计数
struct kobject *get_disk(struct gendisk *disk);
void put_disk(struct gendisk *disk);

bio、request和request_queue

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
29
30
31
32
struct bvec_iter
{
sector_t bi_sector; /* device address in 512byte sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};

struct bio
{
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
...

struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};

与bio对应的数据每次存放的内存不一定是连续的,因此需要一个向量。向量中的每个元素实际是一个[page,offset,len],我们一般也称它为一个片段。

1
2
3
4
5
6
struct bio_vec
{
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};

I/O调度算法可将连续的bio合并成一个请求。请求是bio经由I/O调度进行调整后的结果,这是请求和bio的区别。

每个块设备或者块设备的分区都对应有自身的request_queue,从I/O调度器合并和排序出来的请求会被分发(Dispatch)到设备级的request_queue。

主要API

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
29
30
31
32
33
34
35
36
37
// 初始化请求队列
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
// 清除请求队列
void blk_cleanup_queue(request_queue_t * q);
// 分配请求队列
request_queue_t *blk_alloc_queue(int gfp_mask);
// 对于RAMDISK这种完全随机访问的非机械设备,并不需要进行复杂的I/O调度。
// 使用如下函数来绑定请求队列和“制造请求”函数
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);
// 提取请求
struct request * blk_peek_request(struct request_queue *q);
// 启动请求
void blk_start_request(struct request *req);
// 报告完成
void __blk_end_request_all(struct request *rq, int error);
void blk_end_request_all(struct request *rq, int error);
// 用blk_queue_make_request()绕开I/O调度,但是在bio处理完成后应该使用bio_endio
void bio_endio(struct bio *bio, int error);

// 如果是I/O操作故障,可以调用快捷函数bio_io_error()
#define bio_io_error(bio) bio_endio((bio), -EIO)
// 遍历一个请求的所有bio
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
// 遍历一个bio的所有bio_vec
#define __bio_for_each_segment(bvl, bio, iter, start) \
for (iter = (start); \
(iter).bi_size && \
((bvl = bio_iter_iovec((bio), (iter))), 1); \
bio_advance_iter((bio), &(iter), (bvl).bv_len))
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
// 迭代遍历一个请求所有bio中的所有segment
#define rq_for_each_segment(bvl, _rq, _iter) \
__rq_for_each_bio(_iter.bio, _rq) \
bio_for_each_segment(bvl, _iter.bio, _iter.iter)

I/O调度器

Linux 2.6以后的内核包含4个I/O调度器,它们分别是Noop I/O调度器(适合Flash)、Anticipatory I/O调度器、Deadline I/O调度器(适合读取多的场景,数据库)与CFQ I/O调度器(适合多媒体应用)。其中,Anticipatory I/O调度器算法已经在2010年从内核中去掉了。

可以通过给内核添加启动参数,选择所使用的I/O调度算法

1
kernel elevator=deadline

通过类似如下的命令,改变一个设备的调度器

1
$ echo SCHEDULER > /sys/block/DEVICE/queue/scheduler

Linux块设备驱动初始化

1
2
3
4
5
6
// 注册设备
// major参数是块设备要使用的主设备号,name为设备名,它会显示在/proc/devices中。
// 如果major为0,内核会自动分配一个新的主设备号,register_blkdev()函数的返回值就是这个主设备号。
int register_blkdev(unsigned int major, const char *name);
// 注销设备
int unregister_blkdev(unsigned int major, const char *name);

块设备的打开释放

块设备驱动的open()函数和其字符设备驱动的对等体不太相似,前者不以相关的inode和file结构体指针作为参数。

1
2
int (*open)(struct block_device *bdev, fmode_t mode);
void (*release)(struct gendisk *disk, fmode_t mode);

块设备ioctl函数

与字符设备驱动一样,块设备可以包含一个ioctl()函数以提供对设备的I/O控制能力。高层的块设备层代码处理了绝大多数I/O控制。例如,drivers/block/floppy.c实现了与软驱相关的命令,drivers/mmc/card/block.c实现了MMC子系统的命令处理。

块设备驱动的I/O请求处理

使用请求队列的源码见drivers/memstick/core/ms_block.c

使用请求队列对于一个机械磁盘设备而言的确有助于提高系统的性能,但是对于RAMDISK、ZRAM(Compressed RAM Block Device)等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。源码见drivers/block/zram/zram_drv.c

实例:vmem_disk驱动

vmem_disk硬件原理

vmem_disk是一种模拟磁盘,其数据实际上存储在RAM中。它使用通过vmalloc()分配出来的内存空间来模拟出一个磁盘,以块设备的方式来访问这片内存。该驱动是对字符设备驱动章节中globalmem驱动的块方式改造。

加载vmem_disk.ko后,在使用默认模块参数的情况下,系统会增加4个块设备节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ls -l /dev/vmem_disk*
brw-rw---- 1 root disk 252, 0 2月 25 14:00 /dev/vmem_diska
brw-rw---- 1 root disk 252, 16 2月 25 14:00 /dev/vmem_diskb
brw-rw---- 1 root disk 252, 32 2月 25 14:00 /dev/vmem_diskc
brw-rw---- 1 root disk 252, 48 2月 25 14:00 /dev/vmem_diskd

$ sudo mkfs.ext2 /dev/vmem_diska
mke2fs 1.42.9 (4-Feb-2014)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
Stride=0 blocks, Stripe width=0blocks
64 inodes, 512 blocks
25 blocks (4.88%) reserved for the super user
First data block=1
Maximum filesystem blocks=524288
1 block group
8192 blocks per group, 8192fragments per group
64 inodes per group
Allocating group tables: done
Writing inode tables: done
Writing superblocks and filesystem accounting information: done

驱动开发常用项

驱动属性项

linux/device.h

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
// 设备属性处理函数
static ssize_t xxx1_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t n)
{
int input;
if (kstrtoint(buf, 10, &input))
return -EINVAL;
...
schedule_work(&work);
return n;
}
// 声明设备属性结构体dev_attr_xxx1,并赋值xxx1_store为对应的处理函数
static DEVICE_ATTR_WO(xxx1);
static struct attribute *xxx_attrs[] = {
&dev_attr_xxx1.attr,
&dev_attr_xxx2.attr,
NULL,
};
static const struct attribute_group xxx_group = {
.attrs = xxx_attrs,
};

// 在进行初始化时进行,例如probe中
// 注册设备属性
sysfs_create_group(&dev->kobj, &xxx_group);
// 移除设备属性
sysfs_remove_group(&dev->kobj, &xxx_group);

工作队列

内核驱动处理时,遇到比较费时类型的任务时,可以将任务放到工作队列,稍后在合适的时候进行处理。

1
2
3
4
5
6
7
8
// 工作队列结构体
struct work_struct{}
// 工作队列处理函数
void xxx_handle(struct work_struct *work);
// 初始化工作队列
INIT_WORK(&xxx_work, xxx_handle);
// 调度工作队列
schedule_work(&work);

附录

常用命令

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
29
30
31
32
33
34
35
36
37
38
39
40
41
# 加载驱动
$ sudo insmod globalmem.ko
# 卸载驱动
$ sudo rmmod globalmem

# 只查看最新的内核打印消息
$ dmesg -W

# 查看驱动主设备号
$ cat /proc/devices | grep globalmem
230 globalmem
# 查看驱动设备节点信息
$ ls -l /dev/globalmem
crwxrwxrwx 1 root root 230, 0 Dec 27 17:40 /dev/globalmem
# 查看platform驱动
ls /sys/devices/platform/globalfifo -l
total 0
lrwxrwxrwx 1 root root 0 1月 2 17:00 driver -> ../../../bus/platform/drivers/globalfifo
-rw-r--r-- 1 root root 4096 1月 2 17:03 driver_override
-r--r--r-- 1 root root 4096 1月 2 17:03 modalias
drwxr-xr-x 2 root root 0 1月 2 17:03 power
lrwxrwxrwx 1 root root 0 1月 2 17:03 subsystem -> ../../../bus/platform
-rw-r--r-- 1 root root 4096 1月 2 17:00 uevent

# 创建设备节点
$ sudo mknod /dev/globalmem c 230 0
# 删除设备节点
$ sudo unlink /dev/globalmem
# 创建支持多设备的节点
$ sudo mknod /dev/globalmem0 c 230 0
$ sudo mknod /dev/globalmem1 c 230 1

# 创建的设备普通用户没有写入权限,需要增加写入权限
# 或者给予全权限
$ sudo chmod 777 /dev/globalmem*

# 向设备写入数据
$ echo "hello world 0" >> /dev/globalmem0
# 读取设备中的数据
$ cat /dev/globalmem0
hello world 0

参考

Linux设备驱动开发详解:基于最新的Linux4.0内核.pdf

嵌入式王道长-Linux内核驱动开发