Linux-Kernel字符设备驱动程序实验

字符设备: 是指只能一个字节一个字节进行读写操作的设备,不能随机读取设备中的某一数据、读取数据要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等。

一般每个字符设备或者块设备都会在 /dev 目录(可以是任意目录,这样是为了统一)下对应一个设备文件。linux 用户层程序通过设备文件来使用驱动程序操作字符设备或块设备。**linux 下一切都是文件。**

编写代码和 Makefile 文件

内核的源代码文件,

// filename: mydriver.c
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/irq.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#define BUFF_SIZE 1024
static unsigned int major;
static char buff[BUFF_SIZE];

int mydriver_open(struct inode *inode, struct file *file) {
printk("mydriver_open is called. \n");
return 0;
}

ssize_t mydriver_read(struct file *file, char __user *buf, size_t size,
loff_t *ppos) {
printk("mydriver_read is called. \n");
copy_to_user(buf, buff, size);
return size;
}

ssize_t mydriver_write(struct file *file, const char __user *buf, size_t size,
loff_t *ppos) {
printk("mydriver_write is called. \n");
copy_from_user(buff, buf, size);
return size;
}

// 当执行这些函数时,模块会调用的函数
static const struct file_operations mydriver_ops = {
.open = mydriver_open,
.read = mydriver_read,
.write = mydriver_write,
};

static int __init mydriver_init(void) {
printk("mydriver_init is called. \n");
// 注册设备,major 是操作系统给设备分配的一个设备号,第二个参数是设备名称
major = register_chrdev(0, "mydriver", &mydriver_ops);
printk("register_chrdev. major = %d\n", major);
return 0;
}

static void __exit mydriver_exit(void) {
printk("mydriver_exit is called. \n");
// 卸载设备,参数为主设备号和设备名
unregister_chrdev(major, "mydriver");
}

MODULE_LICENSE("GPL");
module_init(mydriver_init); // 指定模块的起始函数
module_exit(mydriver_exit); // 指定模块的退出函数

对应的 Makefile 文件,

ifneq ($(KERNELRELEASE),)
obj-m := mydriver.o # 目的文件
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build # 内核源码目录
PWD := $(shell pwd) # 当前工作目录
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules # 调用内核源码位置的 makefile 来编译当前程序
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean # 调用内核源码位置的 makefile 来清除编译残留
endif

加载字符设备驱动

  1. 执行 make 命令,我们会得到 mydriver.ko 驱动文件:

image-20221101233450347

  1. 使用 insmod 加载驱动模块,可以看到 /proc/devices 中已经有了我们的设备和设备号。

image-20221101233555420

  1. 我们也可以使用 dmesg 指令来查看驱动模块的日志信息:

image-20221101233757919

可以看到,这里已经执行了 init 函数和设备注册函数。

  1. /dev 目录下,还不存在这个设备的节点,需要我们手动创建。
sudo mknod -m 660 /dev/mydriver c 510 0 
# -m 660 设置文件权限为 660 c 表示字符设备,之后是主设备号和次设备号
  1. 查看到 /dev/mydriver 是否创建成功。

image-20221101234624555

测试驱动程序

到目前为止,我们的驱动程序已经完成了,接下来我们编写代码测试它。

  1. 测试代码,依次执行了 openwriteread 这三个函数(close 我太懒了)
// filename: test.c 
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
int ret;
char read_data[10];
char write_data[10] = "ceyewan";
int fd = open("/dev/mydriver", O_RDWR);
if (-1 != fd) {
ret = write(fd, write_data, 8);
printf("write ret = %d \n", ret);
ret = read(fd, read_data, 8);
printf("read ret = %d \n", ret);
printf("read : %s\n", read_data);
} else {
printf("open /dev/mydriver failed! \n");
}
return 0;
}
  1. 执行测试代码,注意,需要有 root 权限才能正确执行

image-20221101235144887

  1. 使用 dmesg 命令查看系统日志,可以看到执行了 mydriver_openmydriver_writemydriver_read 三个函数

image-20221101235328176

卸载驱动程序

sudo rmmod mydriver # 从 /proc/devices 中移除
sudo rm /dev/mydriver # 删除 /dev/mydriver

同样我们可以使用 dmesg 命令查看日志,可以看到 mydriver_exit 函数被执行。

image-20221101235710583

问题和思考

  1. 内核模块和用户程序有什么区别,需要注意哪些安全问题?

内核模块运行在内核空间,用户程序运行在用户空间。内核模块结束时要做一些清除性的工作,并且需要严格避免溢出和野指针的问题,把自己系统玩崩了概不负责。

  1. struct cdev 结构体和 struct file_operations 结构体中有哪些内容,有什么作用?

内核中每个字符设备都对应一个 struct cdev 结构的变量,我这里根本没用到。

struct file_operations 是驱动框架,在应用层的函数(eg: open)被调用时,驱动程序中对应的函数(eg: mydriver_open)就会被执行。

  1. 用于编译内核模块编译的 Makefile 文件中各个部分有什么作用?

看上面代码的注释吧。

  1. 什么是主设备号和次设备号,有什么作用?

主设备号用来表示一个特定的驱动程序,次设备号用来表示使用该驱动程序的各设备。具体到这里我们的主设备号为 510 是确定的,但是创建到 /dev 中可以有多个次设备号,多个设备。

  1. 字符设备驱动是如何访问用户空间的数据的?

使用内核提供的 copy_to_usercopy_from_user 这两个函数。

参考链接

  1. Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?
  2. 字符设备驱动实验