一个简单的字符设备驱动

拿朋友的作业,简单写个小 demo。

需求

Write a device driver for a character device which implements a simple way of message passing. The kernel maintains a list of messages. To limit memory usage, we impose a limit of 4KB = 4*1024 bytes for each message and also impose a limit of the total number of messages stored in the kernel, which is 1000.

Your device driver should perform the following operations:

  • When the module is loaded, the device is created. An empty list of messages is created as well.
  • Removing the module deallocates all messages, removes the list of messages and removes the device.
  • Reading from the device returns one message, and removes this message from the kernel list. If the list of messages is empty, the reader returns -EAGAIN.
  • Writing to the device stores the message in kernel space and adds it to the list if the message is below the maximum size, and the limit of the number of all messages stored in the kernel wouldn't be surpassed with this message. If the message is too big, -EINVAL is returned, and if the limit of the number of all messages was surpassed, -EBUSY is returned.
  • The kernel module which implements this driver must be called charDeviceDriver.ko.

You need to ensure that your code deals with multiple attempts at reading and writing at the same time. Moreover, your code should handle several read and write attempts concurrently. Your critical sections should be as short as possible. The reader should obtain the messages in a FIFO (first in first out) manner.

简单来说,就是要求写一个字符设备的驱动程序,实现 FIFO 模式的消息读写,且支持并发操作。

思路

概念上没啥复杂的,用一个单向链表就能实现,主要是写的时候有不少细节需要注意,不要漏加锁/放锁,不要忘记释放内存,尤其是在异常情况下。

编写设备驱动基本上就是实现以下几个函数:

  • init_module:加载驱动时调用,对应 insmod 命令,通过 register_chrdev() 分配主设备号,同时在这里初始化消息链表
  • cleanup_module:卸载驱动时调用,对应 rmmod 命令,在这里要释放资源并 unregister_chrdev()
  • device_open:打开设备文件时调用,增加该模块的引用计数
  • device_release:关闭设备文件时调用,减少该模块的引用计数
  • device_read:读取设备文件时调用,从链表头弹出一条消息
  • device_write:写入设备文件时调用,向链表尾插入一条消息

因为涉及到一些原来不知道的知识点,踩了一些坑,主要是:

  • 内核直接访问用户内存会 crash 导致系统重启,需要先通过 copy_to_user()copy_from_user()get_user()put_user() 这些函数拷贝到内核空间中才能使用
  • 内核中申请内存要用 kmalloc(),对应的释放函数是 kfree()

实现

这里贴一下之前提到的几个函数的实现,完整代码及测试用例见 Github 仓库

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
/*
* This function is called when the module is loaded
*/
int init_module(void)
{
Major = register_chrdev(0, DEVICE_NAME, &fops);

if (Major < 0) {
printk(KERN_ALERT "Registering char device failed with %d\n", Major);
return Major;
}

printk(KERN_INFO "I was assigned major number %d. To talk to\n", Major);
printk(KERN_INFO "the driver, create a dev file with\n");
printk(KERN_INFO "'mknod /dev/%s c %d 0'.\n", DEVICE_NAME, Major);
printk(KERN_INFO "Try various minor numbers. Try to cat and echo to\n");
printk(KERN_INFO "the device file.\n");
printk(KERN_INFO "Remove the device file and module when done.\n");

list_init();

return SUCCESS;
}

/*
* This function is called when the module is unloaded
*/
void cleanup_module(void)
{
list_destroy();
/* Unregister the device */
unregister_chrdev(Major, DEVICE_NAME);
}

/*
* Methods
*/

/*
* Called when a process tries to open the device file, like
* "cat /dev/mycharfile"
*/
static int device_open(struct inode *inode, struct file *file)
{

mutex_lock (&devLock);
if (Device_Open) {
mutex_unlock (&devLock);
return -EBUSY;
}
Device_Open++;
mutex_unlock (&devLock);
// sprintf(msg, "I already told you %d times Hello world!\n", counter++);
try_module_get(THIS_MODULE);

return SUCCESS;
}

/* Called when a process closes the device file. */
static int device_release(struct inode *inode, struct file *file)
{
mutex_lock (&devLock);
Device_Open--; /* We're now ready for our next caller */
mutex_unlock (&devLock);
/*
* Decrement the usage count, or else once you opened the file, you'll
* never get get rid of the module.
*/
module_put(THIS_MODULE);

return 0;
}

/*
* Called when a process, which already opened the dev file, attempts to
* read from it.
*/
static ssize_t device_read(struct file *filp, /* see include/linux/fs.h */
char *buffer, /* buffer to fill with data */
size_t length, /* length of the buffer */
loff_t * offset)
{
/* result of function calls */
int result;

Node* p = list_pop_front();
if (p == NULL)
return -EAGAIN;

/*
* Actually put the data into the buffer
*/
if (strlen(p->buf) + 1 < length)
length = strlen(p->buf) + 1;
result = copy_to_user(buffer, p->buf, length);
kfree(p);

if (result > 0)
return -EFAULT; /* copy failed */
/*
* Most read functions return the number of bytes put into the buffer
*/
return length;
}

/* Called when a process writes to dev file: echo "hi" > /dev/hello */
static ssize_t
device_write(struct file *filp, const char *buff, size_t len, loff_t * off)
{
int ret;
Node* p;

if (len > MAX_MSG_SIZE) {
return -EINVAL;
}

mutex_lock (&devLock);
if (list_len >= MAX_MSG_NUM) {
mutex_unlock (&devLock);
return -EBUSY;
}
mutex_unlock (&devLock);

p = make_node(buff, len);
if (p == NULL)
return -EFAULT;

ret = list_push_back(p);
if (ret != SUCCESS) {
kfree(p);
return ret;
}

return len;
}