Structure of Linux Kernel Device Driver
ref. https://www.youtube.com/watch?v=XoYkHUnmpQo&list=LL&index=1&t=272s
Device Drivers
Def.: 设备驱动(Device Drivers),实际上就是硬件设备对应的抽象,用户能够通过这样的一个抽象与对应的硬件进行交互
设备驱动与固件的区别:设备驱动是运行在主机内核上(偶尔也有运行在用户空间)的,让操作系统知道如何与对应的硬件设备进行通信
而固件则是运行在设备硬件上的,只有具备一定智能程度的设备才会拥有固件(Only Devices with some level of intelligence)。
操作系统的职责之一就是提供一个基础框架用于编写和运行设备驱动。
在Unix based的操作系统上,最常见的抽象形式就是文件,也就是说用户通常是通过某个文件来与硬件设备进行交互,如下图所示:
\(\mathcal{Intuition}\): The development of hardware driver composed by two parts:
- talk to the driver: 用户在内核中注册驱动程序,生成对应的设备文件,然后通过处理文件的方式与驱动交互
- talk to the hardware: 由驱动完成对硬件设备的控制
内核会提供一些API,用于将设备硬件导出为用户空间的文件,这些文件通常位于文件系统中的/dev, /sys等目录下。
这些目录下的文件被称为设备节点或者设备文件,设备文件拥有以下基础属性:
- 类型:block or char,对于char类型的设备文件,用户通过字节流与之通信;而对于block类型的设备文件,用户通过以块为单位的字节与之通信。
- 主号(设备号):用于标识设备的类型,比如sata设备和音频设备拥有不同的设备号
- 次号:用于区分同类型的不同设备
主号-次号对是一个设备在系统中的标识符,因此需要保证每一个设备的主号-次号都是唯一的。
上图中,/dev/ttyS0的设备类型是char类型,主次设备号为4/64;/dev/ram0的设备类型是block类型,主次设备号为1/0。
Talk to a char driver
对于Char类型的设备文件,用户可以通过处理文件的方式与运行在内核系统中的驱动进行交互:
用于读写文件的系统调用,比如open(), read(), write(), close(), lseek(), mmap() etc. 将会被操作系统重定向(redirected)硬件设备对应的驱动程序。设备驱动程序则是一个内核组件(通常是一个模块, module)用于与硬件设备交互。
从设备驱动的角度,用户只需要编写回调函数然后在内核中注册该回调函数,那么用户就能够与设备驱动通信。而设备驱动运行在内核,因此也能和硬件设备通信。
开发设备驱动的三个基本步骤:
- 为设备驱动分配设备号(主设备号/次设备号),这个步骤通过register_chrdev_region()或者alloc_chrdev_region()函数完成
- 实现文件处理操作的回调函数,e.g.,open(), read(), write(), ioctl()
- 在内核中注册该驱动,这个步骤通过cdev_init()以及cdev_add()函数完成
开发设备驱动的步骤可以大致理解为:creating a link from the driver to the device node that the user can see.
在内核中,使用结构体struct cdev
来代表一个char设备,这个结构体主要用于在系统中注册一个驱动,其初始化过程主要涉及两个成员:
typedef unsigned int cdev_t /* An integer type used for device IDs */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
[...]
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
[...]
int (*open) (struct inode *, struct file *);
[...]
}
从上面的代码中可以看到,open(), read(), write()函数与常见的系统调用不同,其参数中不包含文件路径或者文件描述符,而是将另外两个结构体作为参数:struct file
& struct inode
,这两个结构体都是用于代表一个文件,不过是角度不同,inode
结构体是从文件系统的角度表示一个文件,其属性包括大小、权限已经唯一的标识符;file
结构体则是从用户的角度表示一个文件,其属性包括inode、文件名、文件打开属性(e.g. O_RDONLY etc.)以及文件内部偏移。
这两个结构体的区别:
To understand the differences between inode and file, we will use an analogy from object-oriented programming: if we consider a class inode, then the files are objects, that is, instances of the inode class. Inode represents the static image of the file (the inode has no state), while the file represents the dynamic image of the file (the file has state).
https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html
file
结构体的主要成员:
- f_mode:用于标识读(FMODE_READ)或者写(FMODE_WRITE)
- f_flags:用于标识文件打开的属性(O_RDONLY, O_NONBLOCK, etc.)
- f_op:用于标识与文件相关联的操作(pointer to the
file_operations
structure) - private_data:可以用于存储由设备特定数据的指针,这个指针将会被初始化值是程序员分配的内存位置
- f_pos:文件内的偏移量
inode
结构体的主要成员为i_cdev指针,该指针用于指向char设备对应的结构体
Implementation of file operations
在设备驱动开发的过程中,推荐使用一个结构体用于保存和跟踪设备相关的信息以及模块所使用到的信息。对于char设备,这样的结构体中需要包含指向该设备的结构体,如下:
#include <linux/fs.h>
#include <linux/cdev.h>
struct my_device_data {
struct cdev cdev;
/* my data starts here */
//...
};
static int my_open(struct inode *inode, struct file *file)
{
struct my_device_data *my_data;
my_data = container_of(inode->i_cdev, struct my_device_data, cdev);
file->private_data = my_data;
//...
}
static int my_read(struct file *file, char __user *user_buffer, size_t size, loff_t *offset)
{
struct my_device_data *my_data;
my_data = (struct my_device_data *) file->private_data;
//...
}
可以使用inode
结构的i_cdev字段(使用container_of宏)找到指向cdev成员的指针。在file
结构的 private_data 字段中,可以在打开后存储信息,然后使用read(), write(), release()等函数访问这些数据。
在回调函数中,需要读取和写入数据到用户地址空间,这些通常使用下面的这些宏\函数来实现:
#include <asm/uaccess.h>
/* return 0 in case of success and another value in case of error */
put_user(type val, type *address);
get_user(type val, type *address);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
Registration and unregistration of char devices
驱动设备的注册与注销过程都和设备的主次设备号相关,可以使用MKDEV
宏静态生成一个dev_t,用于保存设备的唯一标识符。不过推荐使用alloc_chrdev_region()
函数来动态分配主次设备号。随后,使用函数register_chrdev_region()
或者unregister_chrdev_region()
来注册或者注销驱动,这些函数原型如下:
dev_t MKDEV(unsigned int maj, unsigned int min);
/*
arguments:
- dev: output parameter for first assigned number
- baseminor: first of the requested range of minor numbers
- count: the number of minor numbers required
- name: the name of the associated device or driver
Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t * dev, unsigned baseminor, unsigned count, const char * name);
int register_chrdev_region(dev_t first, unsigned int count, char *name);
void unregister_chrdev_region(dev_t first, unsigned int count);
下面这段代码用于注册"my_minor_count"个设备驱动,其标识符以"my_major:my_first_minor"开始连续递增。
#include <linux/fs.h>
...
err = register_chrdev_region(MKDEV(my_major, my_first_minor), my_minor_count,
"my_device_driver");
if (err != 0) {
/* report error */
return err;
}
...
在实现了文件操作并分配了驱动标识符后,将进行驱动初始化(cdev_init()
)并且通知内核添加该驱动(cdev_add()
),下面这段代码用于添加"MY_MAX_MINORS"个设备驱动:
#include <linux/fs.h>
#include <linux/cdev.h>
#define MY_MAJOR 42
#define MY_MAX_MINORS 5
struct my_device_data {
struct cdev cdev;
/* my data starts here */
//...
};
struct my_device_data devs[MY_MAX_MINORS];
const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
.unlocked_ioctl = my_ioctl
};
int init_module(void)
{
int i, err;
err = register_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS,
"my_device_driver");
if (err != 0) {
/* report error */
return err;
}
for(i = 0; i < MY_MAX_MINORS; i++) {
/* initialize devs[i] fields */
cdev_init(&devs[i].cdev, &my_fops);
cdev_add(&devs[i].cdev, MKDEV(MY_MAJOR, i), 1);
}
return 0;
}
下面这段代码用于删除并注销驱动:
void cleanup_module(void)
{
int i;
for(i = 0; i < MY_MAX_MINORS; i++) {
/* release devs[i] fields */
cdev_del(&devs[i].cdev);
}
unregister_chrdev_region(MKDEV(MY_MAJOR, 0), MY_MAX_MINORS);
}
在file_operations
结构体的初始化中,使用结构体的成员名称进行初始化,这样的初始化操作是在C99中定义的,而未初始化的成员将会保持默认值,比如上面这段代码中my_fops.mmap
将会是一个空指针。
在设备终端中加载对应的驱动:modeprobe [driver_name]
,内核将会执行上面的代码,完成驱动的初始化并加载该驱动
然后打印已添加的驱动:cat /proc/devices
,找到对应驱动的主次设备号
创建用户空间可以访问的设备文件:mknod /dev/[file_name] [c/b] [major num] [minor num]
随后,用户对设备文件的读写操作都会调用设备驱动对应的回调函数。
标签:Kernel,struct,data,Driver,Part,cdev,file,my,设备 From: https://www.cnblogs.com/TheFutureIsNow/p/18303258