0%

Linux-Lab3-Character device drivers

Character device drivers

实验室目标:

  • 了解字符设备驱动程序背后的概念
  • 了解可以在字符设备上执行的各种操作
  • 使用等待队列

设备驱动程序是与硬件设备交互的内核组件(通常是模块)

在UNIX中有两类设备文件:

  • 第一类设备,字符设备:(例如:键盘、鼠标、串行端口、声卡、操纵杆)
    • 慢速设备
    • 管理少量数据
    • 访问数据不需要频繁的查找查询
    • 通常,这些设备的 Read/Write 是按字节顺序执行
  • 第二类设备,块设备:(例如:硬盘驱动器、光盘、RAM 磁盘、磁带驱动器)
    • 数据量大
    • 数据按块组织
    • 搜索频繁
    • 对于这些设备,Read/Write 是在数据块级别完成的

因此,UNIX 提供了两种设备驱动程序:

  • 字符驱动 - character driven
  • 块驱动 - block driven
  • PS:Linux中还有一种网络驱动 - plot driven(不是本篇文章的重点)

对于这两种类型的设备驱动程序,Linux 内核提供了不同的 API,其中大多数参数都有直接含义:

  • fileinode:标识设备类型文件
  • size:要读取或写入的字节数
  • offset:要读取或写入的位移(将相应更新)
  • user_buffer:从中 Read/Write 的用户缓冲区
  • whence:搜索方式(搜索操作开始的位置)
  • cmdarg:用户发送到 ioctl 调用的参数(IO控制)

Majors and Minors

Linux 中,设备具有与之关联的唯一固定标识符,由两部分组成:major and minor

  • major:标识设备的类型(IDE 磁盘、SCSI 磁盘、串行端口等)
  • minor:标识具体的设备(第一个磁盘、第二个串行端口等)

PS:因为物理设备已经被驱动抽象为“在 Linux 上运行的软件”,所以 Linux 可以通过这种方式定位具体的物理设备

Inode and File

从文件系统的角度来看,inode 表示文件:

  • inode 的属性是与文件关联的大小,权限,时间
  • inode 唯一标识文件系统中的文件

从用户的角度来看,file 表示文件:

  • file 的属性是 inode,文件名,文件打开属性,文件位置
  • 所有打开的文件都有与之关联的 file 结构体

回到设备驱动程序,有两个实体几乎总是具有标准的使用方式:

  • inode:更用于确定执行操作的设备的 major and minor
  • file:用于确定打开文件的标志,还用于保存和访问(以后)私有数据

Registration and unregistration of character devices

设备的注册/注销是通过指定 major and minor 设备来实现的

  • 类型 dev_t 用于保留设备的标识符(major and minor),并且可以使用 MKDEV 宏获取

对于设备标识符的静态分配和静态注销:

1
2
3
4
#include <linux/fs.h>

int register_chrdev_region(dev_t first, unsigned int count, char *name); /* 创建一个字符设备区 */
void unregister_chrdev_region(dev_t first, unsigned int count); /* 删除一个字符设备区 */

分配标识符后,必须初始化字符设备并且必须通知内核,然后才能注册/删除字符设备:

1
2
3
4
5
#include <linux/cdev.h>

void cdev_init(struct cdev *cdev, struct file_operations *fops); /* 初始化字符设备,并通知内核 */
int cdev_add(struct cdev *dev, dev_t num, unsigned int count); /* 注册字符设备 */
void cdev_del(struct cdev *dev); /* 删除字符设备 */

使用案例如下:

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
#include <linux/fs.h>
#include <linux/cdev.h>

#define MY_MAJOR 42
#define MY_MAX_MINORS 5

struct my_device_data {
struct cdev cdev;
/* my data starts here */
//...
};

struct my_device_data devs[MY_MAX_MINORS];

const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
.unlocked_ioctl = my_ioctl
};

int init_module(void)
{
int i, err;
err = register_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS,
"my_device_driver"); /* MKDEV获取设备标识符 */
if (err != 0) {
return err;
}

for(i = 0; i < MY_MAX_MINORS; i++) {
cdev_init(&devs[i].cdev, &my_fops); /* 绑定自己设置的file_operations */
cdev_add(&devs[i].cdev, MKDEV(MY_MAJOR, i), 1);
}

return 0;
}

void cleanup_module(void)
{
int i;
for(i = 0; i < MY_MAX_MINORS; i++) {
/* release devs[i] fields */
cdev_del(&devs[i].cdev);
}
unregister_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS);
}

同一个 dev_t 可以注册多个字符设备,每次 open(DEVICE_PATH, O_RDONLY) 时,本质上是和一个具体的字符设备进行交互(使用 struct cdev 父类结构体中的数据)

因此,如果两个进程访问同一个字符设备,就很可能在临界区引发安全问题,所以我们要在字符设备的 open 上加锁,禁止其被二次打开

为了程序的并发性,通常我们需要为同一个 dev_t 注册多个字符设备,在进程 open 提供不同的字符设备供其使用

Access to the address space of the process

设备的驱动程序是应用程序和硬件之间的接口,因此,我们经常必须访问用户空间数据(但不能以取消引用用户空间指针的方式,来直接访问用户空间)

直接访问用户空间指针可能会导致:

  • 不正确的行为(根据体系结构的不同,用户空间指针可能无效或映射到内核空间)
  • 内核 oops(用户模式指针可以引用非驻留内存区域)
  • 安全问题

因此通过调用下面的宏函数来正确访问用户空间数据:

1
2
3
4
5
6
#include <asm/uaccess.h>

put_user(type val, type *address);
get_user(type val, type *address);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

下图说明了 Read 操作以及如何在用户空间和驱动程序之间传输数据:

  • 当驱动 driver 有足够多的可用数据时,它将准确地将所需 size 的数据传输给用户
  • 当驱动 driver 没有足够多的可用数据时,它将把所有的可用数据传输给用户

Read 操作的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int my_read(struct file *file, char __user *user_buffer,
size_t size, loff_t *offset)
{
struct my_device_data *my_data = (struct my_device_data *) file->private_data;
ssize_t len = min(my_data->size - *offset, size);

if (len <= 0)
return 0;

/* read data from my_data->buffer to user buffer */
if (copy_to_user(user_buffer, my_data->buffer + *offset, len))
return -EFAULT;

*offset += len;
return len;
}

下图说明了 Write 操作以及如何在用户空间和驱动程序之间传输数据:

  • 写入操作将响应来自用户空间的写入请求,其范围不会大于最大驱动程序容量 MAXSIZ

Write 操作的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int my_write(struct file *file, const char __user *user_buffer,
size_t size, loff_t * offset)
{
struct my_device_data *my_data = (struct my_device_data *) file->private_data;
ssize_t len = min(my_data->size - *offset, size);

if (len <= 0)
return 0;

/* read data from user buffer to my_data->buffer */
if (copy_from_user(my_data->buffer + *offset, user_buffer, len))
return -EFAULT;

*offset += len;
return len;
}

Ioctl

除了 Read 和 Write 操作之外,驱动程序还需要能够执行某些物理设备控制任务(这些操作是通过实现函数来完成的)

可用通过如下函数完成此操作:

1
static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
  • file:打开的设备文件描述符
  • cmd:从用户空间发送的命令
  • arg:指向用户空间的指针,使用 copy_from_user 来安全地获取其值

在实现该功能之前,必须选择与命令对应的数字(建议使用宏定义 _IOC(dir, type, nr, size) 来完成此操作),然后在一个 Switch-Case 中完成各个命令

使用案例如下:(在用户空间调用 ioctl,对应到内核就是 my_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <asm/ioctl.h>

#define MY_IOCTL_IN _IOC(_IOC_WRITE, 'k', 1, sizeof(my_ioctl_data))

static long my_ioctl (struct file *file, unsigned int cmd, unsigned long arg)
{
struct my_device_data *my_data =
(struct my_device_data*) file->private_data;
my_ioctl_data mid;

switch(cmd) {
case MY_IOCTL_IN:
if( copy_from_user(&mid, (my_ioctl_data *) arg,
sizeof(my_ioctl_data)) )
return -EFAULT;

/* process data and execute command */
break;
default:
return -ENOTTY;
}

return 0;
}

Waiting queues

等待队列是正在等待特定事件的进程的列表,使用 wait_queue_head_t 类型定义,可由函数/宏使用:

1
2
3
4
5
6
7
8
9
10
#include <linux/wait.h>

DECLARE_WAIT_QUEUE_HEAD(wq_name); /* 在编译时初始化队列 */
void init_waitqueue_head(wait_queue_head_t *q); /* 初始化队列 */
int wait_event(wait_queue_head_t q, int condition); /* 在条件为false时将当前线程添加到队列中,将其设置为TASK_UNINTERRUPTIBLE,并调度新线程 */
int wait_event_interruptible(wait_queue_head_t q, int condition); /* 在条件为false时将当前线程添加到队列中,将其设置为TASK_INTERRUPTIBLE,并调度新线程 */
int wait_event_timeout(wait_queue_head_t q, int condition, int timeout); /* 和wait_event一样,只是timeout耗尽时同样会退出 */
int wait_event_interruptible_timeout(wait_queue_head_t q, int condition, int timeout); /* 和wait_event_interruptible一样,只是timeout耗尽时同样会退出 */
void wake_up(wait_queue_head_t *q); /* 从目标等待队列中唤醒一个进程 */
void wake_up_interruptible(wait_queue_head_t *q); /* 仅唤醒状态为TASK_INTERRUPTIBLE的线程 */

Exercises

要解决练习,您需要执行以下步骤:

  • 从模板准备 skeletons
  • 构建模块
  • 将模块复制到虚拟机
  • 启动 VM 并在 VM 中测试模块
1
2
3
make clean
LABS=device_drivers make skels
make build

直接看最终代码:

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
/*
* Character device drivers lab
*
* All tasks
*/

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/sched.h>
#include <linux/wait.h>

#include "../include/so2_cdev.h"

MODULE_DESCRIPTION("SO2 character device");
MODULE_AUTHOR("SO2");
MODULE_LICENSE("GPL");

#define LOG_LEVEL KERN_INFO

#define MY_MAJOR 42
#define MY_MINOR 0
#define NUM_MINORS 1
#define MODULE_NAME "so2_cdev"
#define MESSAGE "hello\n"
#define IOCTL_MESSAGE "Hello ioctl"

#ifndef BUFSIZ
#define BUFSIZ 4096
#endif

struct so2_device_data {
/* TODO 2: add cdev member */
struct cdev cdev;
/* TODO 4: add buffer with BUFSIZ elements */
char buffer[BUFSIZ];
/* TODO 7: extra members for home */
wait_queue_head_t queue;
/* TODO 3: add atomic_t access variable to keep track if file is opened */
atomic_t access;
};

struct so2_device_data devs[NUM_MINORS];

static int so2_cdev_open(struct inode *inode, struct file *file)
{
struct so2_device_data *data;

/* TODO 2: print message when the device file is open. */
printk("message:%s",MESSAGE);
/* TODO 3: inode->i_cdev contains our cdev struct, use container_of to obtain a pointer to so2_device_data */
data = container_of(inode->i_cdev, struct so2_device_data, cdev);
file->private_data = data;
/* TODO 3: return immediately if access is = 0, use atomic_cmpxchg */
/* 这里我的想法和原实验不一样:
设置已经open的字符设备的"data->access"为'1',
设置没有open的字符设备的"data->access"为'0',
初始化时,所有的"data->access"都为'0',表明所有字符设备都没有open过
如果一个进程尝试open一个"data->access"为'1'的'/dev/so2_cdev'字符设备,就不会立刻返回,而是执行后面的语句并睡眠
(so2_device_data也属于临界区,如果两个进程open同一个字符设备就可能有安全问题)
*/
if(!atomic_cmpxchg(&data->access,0,1)){
return 0;
}

set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(10 * HZ);
return 0;
}

static int
so2_cdev_release(struct inode *inode, struct file *file)
{
/* TODO 2: print message when the device file is closed. */
printk("message:%s",MESSAGE);
#ifndef EXTRA
struct so2_device_data *data =
(struct so2_device_data *) file->private_data;
/* TODO 3: reset access variable to 0, use atomic_set */
atomic_set(&data->access,0);
#endif
return 0;
}

static ssize_t
so2_cdev_read(struct file *file,
char __user *user_buffer,
size_t size, loff_t *offset)
{
struct so2_device_data *data =
(struct so2_device_data *) file->private_data;
size_t to_read;

#ifdef EXTRA
/* TODO 7: extra tasks for home */
#endif

/* TODO 4: Copy data->buffer to user_buffer, use copy_to_user */
to_read = min(BUFSIZ-*offset,size);
if(copy_to_user(user_buffer,data->buffer+*offset,to_read))
return -EFAULT;
*offset += to_read;
return to_read;
}

static ssize_t
so2_cdev_write(struct file *file,
const char __user *user_buffer,
size_t size, loff_t *offset)
{
struct so2_device_data *data =
(struct so2_device_data *) file->private_data;


/* TODO 5: copy user_buffer to data->buffer, use copy_from_user */
size_t to_write = min(BUFSIZ-*offset,size);
if(copy_from_user(data->buffer+*offset,user_buffer,to_write))
return -EFAULT;
*offset += to_write;
/* TODO 7: extra tasks for home */

return size;
}

static long
so2_cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct so2_device_data *data =
(struct so2_device_data *) file->private_data;
int ret = 0;
int remains;
printk("ioctl start");
switch (cmd) {
/* TODO 6: if cmd = MY_IOCTL_PRINT, display IOCTL_MESSAGE */
case MY_IOCTL_PRINT:
printk("%s",IOCTL_MESSAGE);
break;
/* TODO 7: extra tasks, for home */
case MY_IOCTL_SET_BUFFER:
if(copy_from_user(data->buffer,(char *)arg,strlen(arg)))
return -EFAULT;
printk("buffer from usr:%s",data->buffer);
break;
case MY_IOCTL_GET_BUFFER:
if(copy_to_user((char *)arg,data->buffer,strlen(data->buffer)))
return -EFAULT;
break;
case MY_IOCTL_DOWN:
wait_event(data->queue,0); /* 这里有小迷,当前进程直接进入等待队列,程序卡死 */
break;
case MY_IOCTL_UP:
wake_up(&data->queue); /* 这里就更迷了,根本不能确定唤醒了那个进程 */
break;
default:
ret = -EINVAL;
}

return ret;
}

static const struct file_operations so2_fops = {
.owner = THIS_MODULE,
/* TODO 2: add open and release functions */
.open = so2_cdev_open,
.release = so2_cdev_release,
/* TODO 4: add read function */
.read = so2_cdev_read,
/* TODO 5: add write function */
.write = so2_cdev_write,
/* TODO 6: add ioctl function */
.unlocked_ioctl = so2_cdev_ioctl,
};

static int so2_cdev_init(void)
{
int err;
int i;

/* TODO 1: register char device region for MY_MAJOR and NUM_MINORS starting at MY_MINOR */
err = register_chrdev_region(MKDEV(MY_MAJOR, 0),NUM_MINORS,MODULE_NAME);


for (i = 0; i < NUM_MINORS; i++) {
#ifdef EXTRA
/* TODO 7: extra tasks, for home */
#else
/* TODO 4: initialize buffer with MESSAGE string */
strcpy(devs[i].buffer,MESSAGE);
#endif
/* TODO 7: extra tasks for home */
init_waitqueue_head(&devs[i].queue);
/* TODO 3: set access variable to 0, use atomic_set */
atomic_set(&devs[i].access,0);
/* TODO 2: init and add cdev to kernel core */
cdev_init(&devs[i].cdev,&so2_fops);
cdev_add(&devs[i].cdev,MKDEV(MY_MAJOR,0),NUM_MINORS);
}

return 0;
}

static void so2_cdev_exit(void)
{
int i;

for (i = 0; i < NUM_MINORS; i++) {
/* TODO 2: delete cdev from kernel core */
cdev_del(&devs[i].cdev);
}

/* TODO 1: unregister char device region, for MY_MAJOR and NUM_MINORS starting at MY_MINOR */
unregister_chrdev_region(MKDEV(MY_MAJOR,0),NUM_MINORS);
}

module_init(so2_cdev_init);
module_exit(so2_cdev_exit);
  • 在用户态执行的测试代码:
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
/*
* SO2 Lab - Linux device drivers (#4)
* User-space test file
*/

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include "../include/so2_cdev.h"

#define DEVICE_PATH "/dev/so2_cdev"

/*
* prints error message and exits
*/

static void error(const char *message)
{
perror(message);
exit(EXIT_FAILURE);
}

/*
* print use case
*/

static void usage(const char *argv0)
{
printf("Usage: %s <options>\n options:\n"
"\tp - print\n"
"\ts string - set buffer\n"
"\tg - get buffer\n"
"\td - down\n"
"\tu - up\n"
"\tn - open with O_NONBLOCK and read data\n", argv0);
exit(EXIT_FAILURE);
}

/*
* Sample run:
* ./so2_cdev_test p ; print ioctl message
* ./so2_cdev_test d ; wait on wait_queue
* ./so2_cdev_test u ; wait on wait_queue
*/

int main(int argc, char **argv)
{
int fd;
char buffer[BUFFER_SIZE];

if (argc < 2)
usage(argv[0]);

if (strlen(argv[1]) != 1)
usage(argv[0]);

fd = open(DEVICE_PATH, O_RDONLY);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}

switch (argv[1][0]) {
case 'p': /* print */
if (ioctl(fd, MY_IOCTL_PRINT, 0) < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}

break;
case 's': /* set buffer */
if (argc < 3)
usage(argv[0]);
memset(buffer, 0, BUFFER_SIZE);
strncpy(buffer, argv[2], BUFFER_SIZE);
if (ioctl(fd, MY_IOCTL_SET_BUFFER, buffer) < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
break;
case 'g': /* get buffer */
if (ioctl(fd, MY_IOCTL_GET_BUFFER, buffer) < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
buffer[BUFFER_SIZE-1] = 0;
printf("IOCTL buffer contains %s\n", buffer);
break;
case 'd': /* down */
if (ioctl(fd, MY_IOCTL_DOWN, 0) < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
break;
case 'u': /* up */
if (ioctl(fd, MY_IOCTL_UP, 0) < 0) {
perror("ioctl");
exit(EXIT_FAILURE);
}
break;
case 'n':
if (fcntl(fd, F_SETFL, O_RDONLY | O_NONBLOCK) < 0) {
perror("fcntl");
exit(EXIT_FAILURE);
}

if (read(fd, buffer, BUFFER_SIZE) < 0) {
perror("read");
exit(EXIT_FAILURE);
}
buffer[BUFFER_SIZE-1] = 0;
printf("Device buffer contains %s\n", buffer);
break;
default:
error("Wrong parameter");
}

close(fd);

return 0;
}
  • 结果:
1
2
root@qemux86:~/skels/device_drivers/kernel# insmod so2_cdev.ko
root@qemux86:~/skels/device_drivers# mknod /dev/so2_cdev c 42 0
1
2
3
4
5
root@qemux86:~/skels/device_drivers# ./user/so2_cdev_test p                     
message:hello
ioctl start
Hello ioctl
message:hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@qemux86:~/skels/device_drivers# ./user/so2_cdev_test g                     
message:hello
IOCTL buffer contains hello

ioctl start
message:hello
root@qemux86:~/skels/device_drivers# ./user/so2_cdev_test s yhellow
message:hello
ioctl start
buffer from usr:yhellow
message:hello
root@qemux86:~/skels/device_drivers# ./user/so2_cdev_test g
message:hello
IOCTL buffer contains yhellow
ioctl start
message:hello
1
2
3
4
root@qemux86:~/skels/device_drivers# cat /dev/so2_cdev                          
message:hello
hello
message:hello

感觉在锁和等待队列这一块还不是很熟悉,还需要多写代码