kzalloc
函数名 | kzalloc |
---|---|
功能 | 申请空间并清零空间 |
正确返回值 | 空间地址 |
错误返回值 | NULL |
err.h 建议错误返回值 | -ENOMEM |
注销函数 | kfree |
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/slab.h>
#include <linux/err.h>
gpio_dev_t *gpio_dev = NULL;
//申请空间
gpio_dev = kzalloc(sizeof(gpio_dev_t), GFP_KERNEL);
if(gpio_dev == NULL)
return -ENOMEM;
//销毁空间(申请空间成功,创建设备类失败执行)
create_class_fail:
kfree(gpio_dev);
gpio_dev = NULL;
class_create
class 是设备的更高层视图,抽象出了底层的实现细节。驱动程序会区分 SCSI 硬盘和 ATA 硬盘,但在 class 层他们都是硬盘;
class_create 会动态创建设备的逻辑类,将其添加到内核中,成功创建后会在
/sys/class/
下生成如gpio_class
的目录;
函数名 | class_create |
---|---|
功能 | 创建设备的逻辑类(自动申请空间) |
正确返回值 | struct class * |
错误返回值 | NULL |
err.h 建议错误返回值 | \ |
注销函数 | class_destroy |
示例:
1
2
3
4
5
6
7
8
9
10
11
#include <linux/device.h>
#include <linux/err.h>
//创建设备类
gpio_dev->class = class_create(THIS_MODULE, "gpio_class");
if (IS_ERR(gpio_dev->class))
goto create_class_fail;
//销毁设备类(创建类成功,申请设备号失败执行)
region_chrdev_fail:
class_destroy(gpio_dev->class);
alloc_chrdev_region
alloc_chrdev_region 会自动分配一段设备号,分配成功后会给 *dev 传参赋值第一个设备号,也会在
/proc/devices
文件中显示主设备号和设备名;
函数名 | alloc_chrdev_region |
---|---|
功能 | 自动分配字符类设备号(主设备号分配未使用的) |
传参 | - dev_t *dev: 传入指针,返回第一个设备号 - unsigned baseminor: 次设备号起始递增 - unsigned count: 次设备号个数 - const char *name: 执行 cat /proc/devices 显示的名称 |
正确返回值 | 0 |
错误返回值 | <0 |
err.h 建议错误返回值 | \ |
注销函数 | unregister_chrdev_region |
示例:
1
2
3
4
5
6
7
8
9
10
#include <linux/fs.h>
//申请设备号
rev = alloc_chrdev_region(&gpio_dev->devt, 100/*次设备号起始*/, 8/*次设备号个数*/, "gpio_dev");
if (rev < 0)
goto region_chrdev_fail;
//销毁设备号(申请设备号成功,添加 cdev 失败执行)
add_cdev_fail:
unregister_chrdev_region(gpio_dev->devt/*第一个设备号*/, 8/*次设备号个数*/);
cdev 的使用:cdev_init
执行 cdev_init 函数,将 cdev 和 file_operations 关联起来,实际没有加入到内核;
函数名 | cdev_init |
---|---|
功能 | 初始化字符设备,关联文件操作函数 |
传参 | @ struct cdev *cdev: 将要初始化的 cdev @ const struct file_operations *fops: 绑定的文件操作函数 |
返回值 | void |
注销函数 | 未加入到内核,无需注销 |
示例:
1
2
3
4
5
#include <linux/cdev.h>
//初始化 cdev
cdev_init(&gpio_dev->cdev, &gpio_fops);
gpio_dev->cdev.owner = THIS_MODULE;
cdev 的使用:cdev_add
使用 cdev_add 函数,将 cdev 和设备号关联起来,即设备号和 fops 文件操作关联了起来,此时内核中就有 cdev 信息了;
接着可用 mknod 根据设备号手动创建设备节点,建立内核和用户系统的联系。
函数名 | cdev_add |
---|---|
功能 | 关联 cdev 和设备号 |
传参 | @ struct cdev *p: 传入 cdev @ dev_t dev: 第一个设备号 @ unsigned count: 次设备号个数 |
正确返回值 | 0 |
错误返回值 | 非 0 值 |
err.h 建议错误返回值 | \ |
注销函数 | cdev_del |
示例:
1
2
3
4
5
6
7
8
9
10
#include <linux/cdev.h>
//添加设备到 cdev 并加入内核,绑定了 devt 和 fops
rev = cdev_add(&gpio_dev->cdev, gpio_dev->devt, 8/*次设备号个数*/);
if(rev)
goto add_cdev_fail;
//销毁内核中 cdev 的设备(添加到 cdev 成功,创建设备失败执行)
create_dev_fail:
cdev_del(&gpio_dev->cdev);
device_create
device_create 函数会在
/dev
目录下创建设备节点,建立内核和用户系统的联系。
函数名 | device_create |
---|---|
功能 | 在 /dev 目录下创建设备节点 |
传参 | @ class: 设备类 @ parent: 一般为 NULL @ devt: 目标设备号 @ void *drvdata: 一般为 NULL @ fmt: 可格式化的设备名 |
正确返回值 | struct device * |
错误返回值 | NULL |
err.h 建议错误返回值 | \ |
注销函数 | device_destroy |
示例:
1
2
3
4
5
6
7
8
9
10
11
#include <linux/device.h>
#include <linux/kdev_t.h>
//创建设备节点(for 循环可创建多个)
gpio_dev->dev = device_create(gpio_dev->class, NULL, MKDEV(my_major, 0), NULL, "my_gpio" "%d", 0);
if (IS_ERR(gpio_dev->dev))
goto create_dev_fail;
//销毁设备节点(创建设备节点成功,后续失败执行)
next_fail:
device_destroy(&gpio_dev->class, MKDEV(my_major, 0));
用户空间和内核空间互访
用户空间传给内核的地址是虚拟地址,如果该地址未分配对应物理地址,就会触发内核缺页异常。copy_from_user 和 copy_to_user 会检查地址以及拷贝数据(类似 memcpy),当发生缺页后内核可分配物理地址并修改映射表解决该异常。
为什么要拷贝?
答:直接传入虚拟地址使用的话,调度一段实际后,页表都变了,你这个 buf 地址还能找着人?
为什么不直接用 memcpy?
答:如果进程之前使用过这个虚拟地址,那么该地址会分配对应的物理地址,短暂使用 memcpy 不会出现错误,调度一段时间或者该地址未分配物理地址就会出现内核缺页异常。
copy_from_user
1
long copy_from_user(void *to, const void __user * from, unsigned long n);
示例:
1
2
3
4
5
6
7
8
9
10
#include <asm/uaccess.h>
ssize_t mywrite(struct file *filp, const char __user *user_buf, size_t size, loff_t *offset)
{
int ret = 0;
struct test *pt = filp->private_data;
ret = copy_from_user(pt->kbuf, user_buf, size);
return ret;
}
copy_to_user
1
long copy_to_user(void __user *to, const void *from, unsigned long n);
示例:
1
2
3
4
5
6
7
8
9
10
#include <asm/uaccess.h>
ssize_t myread(struct file *filp, char __user *user_buf, size_t size, loff_t *offset)
{
int ret = 0;
struct test *pt = filp->private_data;
ret = copy_to_user(pt->kbuf, user_buf, size);
return ret;
}
简单驱动示例
驱动层
demo.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/ioctl.h>
#define KARG_SIZE 36
struct karg{
int kval;
char kbuf[KARG_SIZE];
};
#define CMDT 'A'
#define CMD_OFF _IO(CMDT, 0) //命令 0 无方向 无参数
#define CMD_ON _IO(CMDT, 1) //命令 1 无方向 无参数
#define CMD_R _IOR(CMDT, 2, struct karg) //命令 2 方向读取 参数长度 sizeof(struct karg)
#define CMD_W _IOW(CMDT, 3, struct karg) //命令 3 方向写入 参数长度 sizeof(struct karg)
demo.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
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
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/err.h>
#include <asm/uaccess.h>
#include "demo.h"
/* 内核对象结构体 */
struct demo_obj{
struct class *class; //设备类
dev_t devt; //第一个设备号
unsigned major; //主设备号
unsigned base_minor; //第一个次设备号
unsigned count; //次设备号个数
struct cdev cdev; //cdev
};
/* 内核对象 */
struct demo_obj s_demo_obj = {
.base_minor = 0, //次设备号起始
.count = 10, //次设备号个数
};
/* 内核数据 */
static struct karg s_karg = {
.kval = 0,
.kbuf = {0},
};
static int dev_demo_open(struct inode *inode, struct file *file)
{
return nonseekable_open(inode, file); //通知内核不支持 llseek
}
static int dev_demo_close(struct inode *inode, struct file *file)
{
return 0;
}
static long dev_demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
/* cmd 为命令代码,arg 为命令参数或者数据的地址 */
printk("ioctl: cmd=%ld arg=%ld\n", cmd, arg);
switch(cmd){
case CMD_ON:
s_karg.kval = 1;
break;
case CMD_OFF:
s_karg.kval = 0;
break;
case CMD_R:
if(_IOC_SIZE(cmd) != sizeof(struct karg))
return -EINVAL;
if(copy_to_user((struct karg *)arg, &s_karg, sizeof(s_karg)))
return -EAGAIN;
break;
case CMD_W:
if(_IOC_SIZE(cmd) != sizeof(struct karg))
return -EINVAL;
if(copy_from_user(&s_karg, (struct karg *)arg, sizeof(s_karg)))
return -EAGAIN;
break;
default: break;
};
return 0;
}
static const struct file_operations dev_demo_fops = {
.owner = THIS_MODULE,
.open = dev_demo_open, //一定要有函数(函数里直接 return 也行),否则无法 open 设备节点
.unlocked_ioctl = dev_demo_ioctl,
.release = dev_demo_close, //同理,函数里直接 return 就行
};
static int __init demo_init(void)
{
int i;
printk("---demo_init\n");
//创建类 同时创建了 /sys/class/demo_class/ 目录
s_demo_obj.class = class_create(THIS_MODULE, "demo_class");
//申请设备号 会加入到 /proc/devices 文件
alloc_chrdev_region(&s_demo_obj.devt, s_demo_obj.base_minor/*次设备号起始*/, s_demo_obj.count/*次设备号个数*/, "demo_dev");
s_demo_obj.major = MAJOR(s_demo_obj.devt);
//初始化 cdev,绑定 fops
cdev_init(&s_demo_obj.cdev, &dev_demo_fops);
s_demo_obj.cdev.owner = THIS_MODULE;
//设备号的设备加入到 cdev,即和 fops 绑定
cdev_add(&s_demo_obj.cdev, s_demo_obj.devt/*设备号起始*/, s_demo_obj.count/*次设备号个数*/);
//打印一下设备号 比如 major=251 base_minor=0 count=10 可手动 mknod /dev/test/demo1 c 251 0 创建设备节点
printk("major=%d base_minor=%d count=%d\n", s_demo_obj.major, s_demo_obj.base_minor, s_demo_obj.count);
//以下通过 device_create 在 /dev/ 目录下创建设备节点,注意设备节点的权限
for(i = 0; i < s_demo_obj.count; i++){
device_create(s_demo_obj.class, NULL, MKDEV(s_demo_obj.major, s_demo_obj.base_minor +i), NULL, "demo" "%d", i);
}
return 0;
}
static void __exit demo_exit(void)
{
int i;
for(i = 0; i < s_demo_obj.count; i++){
device_destroy(s_demo_obj.class, MKDEV(s_demo_obj.major, s_demo_obj.base_minor +i));
}
cdev_del(&s_demo_obj.cdev);
unregister_chrdev_region(s_demo_obj.devt/*第一个设备号*/, s_demo_obj.count/*次设备号个数*/);
class_destroy(s_demo_obj.class);
printk("---demo_exit\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_DESCRIPTION("Demo Driver");
MODULE_LICENSE("GPL");
Makefile:
1
2
3
4
5
6
7
8
obj-m := demo.o
KDIR := /lib/modules/`uname -r`/build
all:
make -C $(KDIR) M=$(PWD) modules
.PHONY : clean
clean :
-rm -rf *.o *.ko .tmp_versions *.mod.c *.order *.symvers .demo*
应用层
test.h:(内容和 demo.h 除包含文件外相同)
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/ioctl.h>
#define KARG_SIZE 36
struct karg{
int kval;
char kbuf[KARG_SIZE];
};
#define CMDT 'A'
#define CMD_OFF _IO(CMDT, 0) //命令 0 无方向 无参数
#define CMD_ON _IO(CMDT, 1) //命令 1 无方向 无参数
#define CMD_R _IOR(CMDT, 2, struct karg) //命令 2 方向读取 参数长度 sizeof(struct karg)
#define CMD_W _IOW(CMDT, 3, struct karg) //命令 3 方向写入 参数长度 sizeof(struct karg)
test.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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include "test.h"
static struct karg s_uarg = {
.kval = 0,
.kbuf = "test",
};
static struct karg s_uarg_r;
int main(int argc, char *argv[]){
int fd;
fd = open(argv[1], O_RDWR);
if(fd < 0){
printf("Cannot open %s\n", argv[1]);
exit(1);
}
ioctl(fd, CMD_W, &s_uarg); //写入参数:&s_uarg
ioctl(fd, CMD_ON); //无参数
ioctl(fd, CMD_R, &s_uarg_r); //读取返回到参数:&s_uarg_r
printf("kval is %d\n", s_uarg_r.kval);
printf("kbuf is %s\n", s_uarg_r.kbuf);
close(fd);
return 0;
}
结果展示
加载驱动后:
测试 /dev/demo0 节点,其他节点绑定的 fops 相同: