- 字符驱动
- 注册字符设备
- 分配设备编号dev_t
- 分配注册cdev
- 实现简单设备操作函数
- 创建设备
- 扩展设备操作函数read and write
- 编写测试程序读写创建的设备
- 注册字符设备
- 问题及思考
- linux内核模块和普通用户程序的区别
- Makefile各个部分的作用
- char_drive源码
字符驱动
注册字符设备
分配设备编号dev_t
在linux中,每一个设备都有一个对应的主设备号和次设备号,linux在内核中使用dev_t持有设备编号,传统上dev_t为32位,12位为主设备号,20位为次设备号,主编号用来标识设备使用的驱动,也可以说是设备类型,次编号用来标识具体是那个设备,使用动态分配函数alloc_chrdev_region可以让内核自动为我们分配一个主设备号,同时在设备停止使用后,应当释放这些设备编号,释放设备编号的工作应该在卸载模块时完成,释放设备编号可以使用unregister_chrdev_region函数,分配和释放的部分如下:
// 分配设备主次编号
dev_t dev;
int error = alloc_chrdev_region(&dev, 0, 2, "cdrive");
if (error < 0) {
printk("allocate device number fail");
} else {
dev_number = dev;
}
// 取回分配编号
unregister_chrdev_region(dev_number, 2);
分配注册cdev
内核在内部使用struct cdev 结构来代表字符设备. 在内核调用设备操作前, 必须分配并注册一个或几个这些结构. 为此, 代码应当包含 <linux/cdev.h>,其中定义了这个结构和与之相关的一些函数,为了在运行时获得一个独立的cdev结构,我们可以使用cdev_alloc函数来获取一个cdev结构,并设置该结构对应的设备文件的文件操作函数,这些设备操作函数我们在程序开始就给予了声明。同时应将cdev的owner字段设置为THIS_MODULE,此时我们要对该结构进行初始化并通过cdev_add告知内核有关的信息.在设备使用结束时,应当删除该结构,该部分的代码如下:
// 注册并分配cdev设备
my_cdev = cdev_alloc();
my_cdev->ops = &cdev_ops;
my_cdev->owner = THIS_MODULE;
cdev_init(my_cdev, &cdev_ops);
cdev_add(my_cdev, dev, 1);
// 删除注册的设备
cdev_del(my_cdev);
实现简单设备操作函数
在完成一系列分配及初始化工作后,对设备文件对应的文件操作进行实现,这里仅让设备在被打开,关闭,读入,写出时都打印一条内核提示信息。并将文件操作的结构的open,close,read,write函数指针成员设置为我们定义的函数。这里可以使用C99语法。之所以要设定这些函数,是因为内核通过VFS与设备文件进行交互时会使用这些驱动程序设定的I/O函数,如果file_operations结构中对应的函数指针未被初始化,则会被默认设定为NULL,这样做是因为对某一特定类型的设备并非需要支持全部的操作。对于file_operations,我们之前已经在内核关键数据结构的实验中讨论过,这里只再提一下它的owner字段,它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE。该部分代码如下:
static int char_open(struct inode *, struct file *);
static int char_release(struct inode *, struct file *);
static ssize_t char_read(struct file *, char *, size_t, loff_t *);
static ssize_t char_write(struct file *, const char *, size_t, loff_t *);
// 初始化file_operations结构
struct file_operations cdev_ops = {.open = char_open,
.release = char_release,
.read = char_read,
.write = char_write,
.owner = THIS_MODULE};
创建设备
完成上述预备工作后,还未拥有一个真实存在的设备文件,需要创建一个设备,按理来说如果cdev是表示一个字符设备的结构的话,已经使用cdev_add向内核添加了有关该结构的信息,此时应该已经可以使用这个设备了,在dev目录下理应有我们注册的设备名,但实际上并非如此。其原因在于,内核并不使用cdev作为一个设备,而是将其作为一个设备接口,使用这个接口我们可以派生出具体的设备,这里需要深究cdev_add到底做了什么,实际上,注册一个cdev到内核只是将它放到cdev_map中,内核中真正用来管理设备的是kobject结构,该结构包含了大量设备必须的信息,kobject结构对应的是真正的设备,而cdev_map可以简单理解为完成从cdev到kobject的映射,因此如果我们想真正使用一个设备,还需要创建设备,可以使用mknod用对应设备号创建一个设备,为了方便,这里直接在模块中用device_create创建对应的设备,创建及销毁设备的部分如下:
// 创建设备文件,并注册到sysfs中
my_device = class_create(THIS_MODULE, "cdrive");
device_create(my_device, NULL, dev, NULL, "my-cdevice");
// 回收设备
device_destroy(my_device, dev_number);
class_unregister(my_device);
class_destroy(my_device);
至此设备已经可以访问了,make生成模块,并insmod插入,可以在/dev目录下看到我们的设备
用cat命令访问设备并用dmesg查看设备是否正常响应
可见,设备如预期正常工作
扩展设备操作函数read and write
到这一步,需要对设备操作函数read,write进行扩展,使其能够接收用户给它的输入并且用户可以从该设备获取数据。这时,回顾一下file_operations中read和write的原型。
注意到此处参数中包含字符串__user,这种注解是一种文档形式,它告诉我们,这个指针是一个不能被直接解引用的用户空间地址. 对于正常的编译, __user没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用.既然是用户空间的指针,那么他就不能被内核直接解引用,理由有下
- 用户空间指针当运行于内核模式可能根本是无效的. 可能没有那个地址的映射, 或者它可能指向一些其他的随机数据.
- 用户空间内存是分页的, 在做系统调用时这个内存可能没有在 RAM 中. 试图直接引用用户空间内存可能产生一个页面错误, 这是内核代码不允许做的事情. 结果可能是一个“oops”, 导致进行系统调用的进程死亡.
- 指针由一个用户程序提供, 它可能是错误的或者恶意的. 如果你的驱动盲目地解引用一个用户提供的指针,它提供了一个打开的门路使用户空间程序存取或覆盖系统任何地方的内存.
既然如此,就不能直接使用用户空间的指针,这时还需要能够从用户空间获取信息以完成工作,为安全起见必须使用内核提供的函数来完成这一任务,其中两个常用的读写函数原型如下:
使用这两个函数修改设备文件操作的read和wirte函数
static ssize_t char_read(struct file *file, char *str, size_t size,
loff_t *offset) {
char *data = "I haven't recieve any data!\n";
size_t datalen = strlen(data);
char *data2 = "I have user data!\n";
size_t datalen2 = strlen(data2);
if (!read_num) {
if (size > datalen) {
size = datalen;
}
if (_copy_to_user(str, data, size)) {
return -EFAULT;
}
} else {
if (size > datalen2) {
size = datalen2;
}
if (_copy_to_user(str, data2, size)) {
return -EFAULT;
}
}
printk("device is being read!");
return size;
}
static ssize_t char_write(struct file *file, const char *str, size_t size,
loff_t *offset) {
size_t maxdata = 20, copied;
char buf[maxdata];
if (size < maxdata) {
maxdata = size;
}
copied = _copy_from_user(buf, str, maxdata);
if (copied == 0) {
printk("get %zd bytes from user\n", maxdata);
read_num = 1;
} else {
printk("can't copy %zd bytes from user\n", copied);
}
buf[maxdata] = '\0';
printk("Data from user :%s\n", buf);
return size;
}
编写测试程序读写创建的设备
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFFER_LENGTH 256 ///< The buffer length (crude but fine)
static char receive[BUFFER_LENGTH]; ///< The receive buffer from the LKM
int main() {
int ret, fd;
char stringToSend[BUFFER_LENGTH];
printf("Starting device test code example...\n");
fd =
open("/dev/my-cdevice", O_RDWR); // Open the device with read/write access
if (fd < 0) {
perror("Failed to open the device...");
return errno;
}
printf("Type in a short string to send to the kernel module:\n");
scanf("%[^\n]%*c", stringToSend); // Read in a string (with spaces)
printf("Writing message to the device [%s].\n", stringToSend);
ret = write(fd, stringToSend,
strlen(stringToSend)); // Send the string to the LKM
if (ret < 0) {
perror("Failed to write the message to the device.");
return errno;
}
printf("Press ENTER to read back from the device...\n");
getchar();
printf("Reading from the device...\n");
ret = read(fd, receive, BUFFER_LENGTH); // Read the response from the LKM
if (ret < 0) {
perror("Failed to read the message from the device.");
return errno;
}
printf("The received message is: [%s]\n", receive);
printf("End of the program\n");
return 0;
}
测试结果:
问题及思考
linux内核模块和普通用户程序的区别
linux内核模块和普通用户程序有许多不同,比如最直观的内核模块的入口是init_module,而用户程序的入口一般为main,内核中不能使用C标准库。从系统的角度来说,内核模块工作在内核模式,而用户程序工作在用户模式,即内核在ring0,用户程序在ring3。因为内核模块具有很高的特权级,因此不能直接访问用户空间的数据,以防止恶意用户程序对系统造成损害。用户想与内核交互必须通过系统调用函数来完成,这些系统调用函数是由操作系统定义的,通过特殊的处理方式保证了一般情况下的安全性。
Makefile各个部分的作用
首先设定一个变量为MODULE_NAME,其值为我编写的模块的名字,随后obj-m表示把文件MODULE_NAME.o作为模块进行编译,不会直接编译到内核,但是会生成一个独立的ko文件,all指令下命令意味着先进入到本主机系统下build的文件夹运行make命令,然后返回当前文件夹生成一个模块,clean则是对生成的文件进行清理。 Makefile
##
# drive
#
# @file
# @version 0.1
MODULE_NAME :=char_drive
obj-m :=$(MODULE_NAME).o
all:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
$(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
# end
char_drive源码
#include <asm-generic/errno-base.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <stddef.h>
MODULE_LICENSE("GPL");
static dev_t dev_number;
static struct cdev *my_cdev;
static struct class *my_device;
static int read_num = 0;
static int char_open(struct inode *, struct file *);
static int char_release(struct inode *, struct file *);
static ssize_t char_read(struct file *, char *, size_t, loff_t *);
static ssize_t char_write(struct file *, const char *, size_t, loff_t *);
// 初始化file_operations结构
struct file_operations cdev_ops = {.open = char_open,
.release = char_release,
.read = char_read,
.write = char_write,
.owner = THIS_MODULE};
static int module_init_function(void) {
// 分配设备主次编号
dev_t dev;
int error = alloc_chrdev_region(&dev, 0, 2, "cdrive");
if (error < 0) {
printk("allocate device number fail");
} else {
dev_number = dev;
}
// 注册并分配cdev设备
my_cdev = cdev_alloc();
my_cdev->ops = &cdev_ops;
my_cdev->owner = THIS_MODULE;
cdev_init(my_cdev, &cdev_ops);
cdev_add(my_cdev, dev, 1);
// 创建设备文件,并注册到sysfs中
my_device = class_create(THIS_MODULE, "cdrive");
device_create(my_device, NULL, dev, NULL, "my-cdevice");
return 0;
}
static void module_exit_function(void) {
// 回收设备
device_destroy(my_device, dev_number);
class_unregister(my_device);
class_destroy(my_device);
// 删除注册的设备
cdev_del(my_cdev);
// 取回分配编号
unregister_chrdev_region(dev_number, 2);
}
//实现设备读写等操作
static int char_open(struct inode *inode, struct file *file) {
printk("device is open!");
return 0;
}
static int char_release(struct inode *inode, struct file *file) {
printk("divice is closed!");
return 0;
}
static ssize_t char_read(struct file *file, char *str, size_t size,
loff_t *offset) {
char *data = "I haven't recieve any data!\n";
size_t datalen = strlen(data);
char *data2 = "I have user data!\n";
size_t datalen2 = strlen(data2);
if (!read_num) {
if (size > datalen) {
size = datalen;
}
if (_copy_to_user(str, data, size)) {
return -EFAULT;
}
} else {
if (size > datalen2) {
size = datalen2;
}
if (_copy_to_user(str, data2, size)) {
return -EFAULT;
}
}
printk("device is being read!");
return size;
}
static ssize_t char_write(struct file *file, const char *str, size_t size,
loff_t *offset) {
size_t maxdata = 20, copied;
char buf[maxdata];
if (size < maxdata) {
maxdata = size;
}
copied = _copy_from_user(buf, str, maxdata);
if (copied == 0) {
printk("get %zd bytes from user\n", maxdata);
read_num = 1;
} else {
printk("can't copy %zd bytes from user\n", copied);
}
buf[maxdata] = '\0';
printk("Data from user :%s\n", buf);
return size;
}
module_init(module_init_function);
module_exit(module_exit_function);