要干点什么

编写一个简单的字符设备驱动程序,该程序并不驱动特定的硬件, 而是用内核空间模拟字符设备,要求该字符设备包括以下几个基本操作,打开、读、写和释放,并编写测试程序用于测试所编写的字符设备驱动程序。在此基础上,编写程序实现对该字符设备的同步操作。

相关数据结构与函数

设备驱动程序结构

字符设备的结构描述如下:

1
2
3
4
5
6
7
8
struct Scull_Dev{
struct cdev devm; //字符设备
struct semaphore sem; //信号量,实现读写时的 PV操作
wait_queue_head_t outq;//等待队列,实现阻塞操作
int flag; //阻塞唤醒条件
char buffer[MAXNUM+1]; //字符缓冲区
char *rd,*wr,*end; //读,写,尾指针
};

字符设备的数据接口

字符设备的数据接口将文件的读、写、打开、释放等操作映射为相应的函数。

1
2
3
4
5
6
7
struct file_operations globalvar_fops =
{
.read = globalvar_read,
.write = globalvar_write,
.open = globalvar_open,
.release = globalvar_release,
};

字符设备的注册与注销

字符设备的注册采用静态申请和动态分配相结合的方式,使用register_chrdev_region函数和alloc_chrdev_region函数来完成。字符设备的注销采用unregister_chrdev_region函数来完成。具体实现见下文代码。

字符设备的打开与释放

打开设备是通过调用file_operations结构中的函数open()来完成的。设备的打开提供给驱动程序初始化的能力,从而为以后的操作准备,此外还会递增设备的使用记数,防止在文件被关闭前被卸载出内核。释放设备是通过调用file_operations结构中的函数release()来完成的。设备的释放作用刚好与打开相反,但基本的释放操作只包括设备的使用计数递减。

字符设备的读写操作

字符设备的读写操作相对比较简单,直接使用函数read( )和write( )就可以了。文件读操作的原型如下:Ssize_t device_read(struct file filp,char __user buff,size_t le n,loff_t* offset);其中,filp 是文件对象指针;buff 是用户态缓冲区,用来接受读到的数据;len 是希望读取的数据量;offset 是用户访问文件的当前偏移。文件写操作的原型和读操作没有区别,只是操作方向改变而已。由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到用户空间的复制。

实验步骤

编写模块程序

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
//globalvar.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#define MAXNUM 100
#define MAJOR_NUM 456 //主设备号 ,没有被使用
struct Scull_Dev
{
struct cdev devm; //字符设备
struct semaphore sem; //信号量,实现读写时的 PV 操作
wait_queue_head_t outq;
//等待队列,实现阻塞操作
int flag; //阻塞唤醒标志
char buffer[MAXNUM + 1]; //字符缓冲区
char *rd, *wr, *end; //读,写,尾指针
};
struct Scull_Dev globalvar;
//static struct class *my_class;
int major = MAJOR_NUM;
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t *);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t *);
static int globalvar_open(struct inode *inode, struct file *filp);
static int globalvar_release(struct inode *inode, struct file *filp);
struct file_operations globalvar_fops =
{
.read = globalvar_read,
.write = globalvar_write,
.open = globalvar_open,
.release = globalvar_release,
};
static int globalvar_init(void)
{
int result = 0;
int err = 0;
dev_t dev = MKDEV(major, 0);
if (major)
{
//静态申请设备编号
result = register_chrdev_region(dev, 1, "charmem");
}
else
{
//动态分配设备号
result = alloc_chrdev_region(&dev, 0, 1, "charmem");
major = MAJOR(dev);
}
if (result < 0)
return result;
cdev_init(&globalvar.devm, &globalvar_fops);
globalvar.devm.owner = THIS_MODULE;
err = cdev_add(&globalvar.devm, dev, 1);
if (err)
printk(KERN_INFO "Error %d adding char_mem device", err);
else
{
printk("globalvar register success\n");
sema_init(&globalvar.sem, 1); //初始化信号量
init_waitqueue_head(&globalvar.outq); //初始化等待队列
globalvar.rd = globalvar.buffer; //读指针
globalvar.wr = globalvar.buffer; //写指针
globalvar.end = globalvar.buffer + MAXNUM; //缓冲区尾指针
globalvar.flag = 0; // 阻塞唤醒标志置 0
}
//my_class = class_create(THIS_MODULE, "chardev0");
//device_create(my_class, NULL, dev, NULL, "chardev0");
return 0;
}
static int globalvar_open(struct inode *inode, struct file *filp)
{
try_module_get(THIS_MODULE); //模块计数加一
printk("This chrdev is in open\n");
return (0);
}
static int globalvar_release(struct inode *inode, struct file *filp)
{
module_put(THIS_MODULE); //模块计数减一
printk("This chrdev is in release\n");
return (0);
}
static void
globalvar_exit(void)
{
//device_destroy(my_class, MKDEV(major, 0));
//class_destroy(my_class);
cdev_del(&globalvar.devm);
unregister_chrdev_region(MKDEV(major, 0), 1);
//注销设备
}
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
if (wait_event_interruptible(globalvar.outq, globalvar.flag != 0)) //不可读时 阻塞读进程
{
return -ERESTARTSYS;
}
if (down_interruptible(&globalvar.sem)) //P 操作
{
return -ERESTARTSYS;
}
globalvar.flag = 0;
printk("into the read function\n");
printk("the rd is %c\n", *globalvar.rd); //读指针
if (globalvar.rd < globalvar.wr)
len = min(len, (size_t)(globalvar.wr - globalvar.rd)); //更新读写长度
else
len = min(len, (size_t)(globalvar.end - globalvar.rd));
printk("the len is %d\n", len);
if (raw_copy_to_user(buf, globalvar.rd, len))
{
printk(KERN_ALERT "copy failed\n");
up(&globalvar.sem);
return -EFAULT;
}
printk("the read buffer is %s\n", globalvar.buffer);
globalvar.rd = globalvar.rd + len;
if (globalvar.rd == globalvar.end)
globalvar.rd = globalvar.buffer;
//字符缓冲区循环
up(&globalvar.sem); //V 操作
return len;
}
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
if (down_interruptible(&globalvar.sem)) //P 操作
{
return -ERESTARTSYS;
}
if (globalvar.rd <= globalvar.wr)
len = min(len, (size_t)(globalvar.end - globalvar.wr));
else
len = min(len, (size_t)(globalvar.rd - globalvar.wr - 1));
printk("the write len is %d\n", len);
if (raw_copy_from_user(globalvar.wr, buf, len))
{
up(&globalvar.sem); //V 操作
return -EFAULT;
}
printk("the write buffer is %s\n", globalvar.buffer);
printk("the len of buffer is %d\n", strlen(globalvar.buffer));
globalvar.wr = globalvar.wr + len;
if (globalvar.wr == globalvar.end)
globalvar.wr = globalvar.buffer; //循环
up(&globalvar.sem);
//V 操作
globalvar.flag = 1; //条件成立,可以唤醒读进程
wake_up_interruptible(&globalvar.outq); //唤醒读进程
return len;
}
module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL");

同目录下建立Makefile文件

为了便于对其上述设备驱动程序globalvar.c(模块源程序)进行编译,建立 Makefile文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
CONFIG_MODULE_SIG=n
ifneq ($(KERNELRELEASE),)
obj-m := globalvar.o
else
KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

编译模块程序

利用Makefile对字符设备驱动程序globalvar.c进行编译, 编译后在当前目录生成globalvar.ko

1
make

加载模块

1
sudo insmod globalvar.ko

创建设备节点

1
sudo mknod /dev/chardev0 c 456 0

编写读写测试程序read.c和write.c

为了验证字符设备驱动程序globalvar.c的可用性,编写用户程序read.c和write.c,以测试对字符设备的读与写。其读程序read.c和写程序write.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
//read.c
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
main(void)
{
int fd,i;
char num[101];
fd= open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
while(1)
{
for(i=0;i<101;i++)
num[i]='\0';
read(fd,num,100);
printf("%s\n",num);
if(strcmp(num,"quit")==0)
{
close(fd);
break;
}
}
}
else
{
printf("device open failure,%d\n",fd);
}
}
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
//write.c
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
void main()
{
int fd;
char num[100];
fd= open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);
if(fd!=-1)
{
while(1)
{
printf("Please input the globar:\n");
scanf("%s",num);
write(fd,num,strlen(num));
if(strcmp(num,"quit")==0)
{
close(fd);
break;
}
}
}
else
{
printf("device open failure\n");
}
}

编译测试程序

1
2
gcc read.c -o read
gcc write.c -o write

打开两个终端同时运行读写测试程序

在左边终端write并回车后,右侧终端即可读取,实现了同步,最后输入quit并回车退出,效果如图:

result

删除设备节点

1
sudo rm /dev/chardev0

卸载模块

1
sudo rmmod globalvar

OVER!!!