Linux字符设备驱动程序
前言
在Linux中,设备一般被分成三种类型:字符设备、块设备和网络设备。这里我们主要关注字符设备,这也是Linux系统中最常见的设备类型,在Linux的ls命令的文件属性中用c表示这个文件是一个字符设备节点,常见的字符设备有LED、鼠标、键盘、串口和显示器等。下面我们通过从零开始编写一个简单的交互型字符设备驱动程序为例,即可以接收用户程序发送的内容并且输出该内容的内核模块,逐步掌握字符设备驱动程序的写法。由于设备驱动程序是内核态的进程,所以并不能直接和用户空间进行交互,用户空间要和驱动程序交互需要通过设备节点或者其他虚拟文件系统进行交互。下面我们除了需要编写一个字符设备驱动程序之外还需要编写一个测试用的用户程序。
环境
操作系统:Debian12
内核版本:Linux 6.1.0
编译器版本:gcc 12.2.0
数据结构
在字符设备驱动程序中,我们主要围绕struct file_operations
这个数据结构来进行开发,这个数据结构在源码文件include/linux/fs.h
中,下面是这个数据结果的部分代码:
struct file_operations {
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
...
}
完整的struct file_operations
结构体还有很多内容,这里我们暂时不关注,我们这里只关注owner、open、release、read、write
这几个成员就好。
函数
这里我们关注一下这些函数:
register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
:位于include/linux/fs.h
中,用于向内核注册字符设备驱动程序。unregister_chrdev(unsigned int major, const char *name)
:位于include/linux/fs.h
中,用于从内核注销字符设备驱动程序。copy_to_user(void __user *to, const void *from, unsigned long n)
:位于include/linux/uaccess.h
中,用于将数据从内核空间复制到用户空间。copy_from_user(void *to, const void __user *from, unsigned long n)
:位于include/linux/uaccess.h
中,用于将数据从用户空间复制到内核空间。
代码
下面我们先写一个设备驱动的框架:
/*
* mycdev.c
*/
#include <linux/init.h>
#include <linux/module.h>
static int __init mycdev_init(void)
{
return 0;
}
static void __exit mycdev_exit(void)
{}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
这是Makefile
的内容:
obj-m += mycdev.o
PWD = $(shell pwd)
# 这里写上自己具体的内核源码路径
KDIR = /lib/modules/$(shell uname -r)/build
build:
$(MAKE) -C $(KDIR) M=$(PWD) modules
之后make
一下没有问题我们就继续下面的内容。
我们接着在代码中添加如下代码:
/*
* mycdev.c
*/
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
int major = 240;
ssize_t
myread(struct file *filp, char __user *userbuff, size_t len, loff_t *lpos)
{
return 0;
}
ssize_t
mywrite(struct file *filp, const char __user *userbuff, size_t len, loff_t *lops)
{
return 0;
}
int
myopen(struct inode *inod, struct file *filp)
{
return 0;
}
int
myrelease(struct inode *inod, struct file *filp)
{
return 0;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.open = myopen,
.release = myrelease,
.read = myread,
.write = mywrite
};
static int __init mycdev_init(void)
{
register_chrdev(major, "mycdev", &fops);
return 0;
}
static void __exit mycdev_exit(void)
{
unregister_chrdev(major, "mycdev");
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");
以上就是一个最简单的字符设备驱动程序框架,这里我们指定主设备号为240,每一个设备驱动程序都有一个主设备号用于匹配驱动程序,接下来我们编写具体的open、release、read、write
的功能。
- open():open函数传入
struct inode
和struct file
两个类型的参数,并且成功返回0。 - release():release函数传入
struct inode
和struct file
两个类型的参数,并且成功返回0。 - read():当用户程序对设备节点调用read函数就会触发驱动的read函数,传入的参数有
struct file
和用户空间内存指针和空间长度还有偏移量。 - write():类似read函数,参数也相同,当用户程序调用write函数之后会调用此函数。
由于我们只需要实现用户程序和内核模块的信息交换的功能,我们只需要完善read和write就好了,为myread和mywrite添加以下代码:
#include <linux/string.h>
ssize_t
myread(struct file *filp, char __user *userbuff, size_t len, loff_t *lpos)
{
int ret;
char *buff = "Hello kernel!";
ret = copy_to_user(userbuff, buff, strlen(buff) + 1);
return ret;
}
ssize_t
mywrite(struct file *filp, const char __user *userbuff, size_t len, loff_t *lops)
{
int ret;
char buff[100];
ret = copy_from_user(buff, userbuff, len);
return ret;
}
我们在/dev
目录下使用mknod生成一个对应的节点:
$ sudo mknod /dev/mycdevnode c 240 0
之后我们编写测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd;
char *str = "Hello application!";
fd = open("/dev/mycdevnode", O_RDWR);
read(fd, buff, 20);
printf("%s\n", buff);
write(fd, str, strlen(str) + 1);
close(fd);
return 0;
}
编译运行(注意需要root权限)这个程序就可以看到有Hello kernel!的输出了,这就是简单的字符设备驱动程序和用户程序交互了。
总结
这样一个字符设备驱动程序框架非常简单,这里还有很多没实现的,比如ioctl、mmap等等函数,具体的实现还需要我们自己后期学习。后面学习的很多驱动程序都是以字符设备驱动程序为基础,所以熟练字符设备驱动程序框架是十分重要的,这里很多细节没写出来,比如如何动态创建设备节点?动态生成设备号等等问题,大家可以自己去找找资料学习学习,谢谢。