字符设备: 是指只能一个字节一个字节进行读写操作的设备,不能随机读取设备中的某一数据、读取数据要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等。
一般每个字符设备或者块设备都会在 /dev
目录(可以是任意目录,这样是为了统一)下对应一个设备文件。linux
用户层程序通过设备文件来使用驱动程序操作字符设备或块设备。**linux
下一切都是文件。**
编写代码和 Makefile 文件
内核的源代码文件,
#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 = 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 clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean endif
|
加载字符设备驱动
- 执行
make
命令,我们会得到 mydriver.ko
驱动文件:
- 使用
insmod
加载驱动模块,可以看到 /proc/devices
中已经有了我们的设备和设备号。
- 我们也可以使用
dmesg
指令来查看驱动模块的日志信息:
可以看到,这里已经执行了 init
函数和设备注册函数。
- 在
/dev
目录下,还不存在这个设备的节点,需要我们手动创建。
sudo mknod -m 660 /dev/mydriver c 510 0
|
- 查看到
/dev/mydriver
是否创建成功。
测试驱动程序
到目前为止,我们的驱动程序已经完成了,接下来我们编写代码测试它。
- 测试代码,依次执行了
open
、write
、read
这三个函数(close
我太懒了)
#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; }
|
- 执行测试代码,注意,需要有
root
权限才能正确执行
- 使用
dmesg
命令查看系统日志,可以看到执行了 mydriver_open
、mydriver_write
、mydriver_read
三个函数
卸载驱动程序
sudo rmmod mydriver sudo rm /dev/mydriver
|
同样我们可以使用 dmesg
命令查看日志,可以看到 mydriver_exit
函数被执行。
问题和思考
- 内核模块和用户程序有什么区别,需要注意哪些安全问题?
内核模块运行在内核空间,用户程序运行在用户空间。内核模块结束时要做一些清除性的工作,并且需要严格避免溢出和野指针的问题,把自己系统玩崩了概不负责。
struct cdev
结构体和 struct file_operations
结构体中有哪些内容,有什么作用?
内核中每个字符设备都对应一个 struct cdev
结构的变量,我这里根本没用到。
struct file_operations
是驱动框架,在应用层的函数(eg: open
)被调用时,驱动程序中对应的函数(eg: mydriver_open
)就会被执行。
- 用于编译内核模块编译的
Makefile
文件中各个部分有什么作用?
看上面代码的注释吧。
- 什么是主设备号和次设备号,有什么作用?
主设备号用来表示一个特定的驱动程序,次设备号用来表示使用该驱动程序的各设备。具体到这里我们的主设备号为 510 是确定的,但是创建到 /dev
中可以有多个次设备号,多个设备。
- 字符设备驱动是如何访问用户空间的数据的?
使用内核提供的 copy_to_user
和 copy_from_user
这两个函数。
参考链接
- Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?
- 字符设备驱动实验