三种常见的 linux 设备的驱动介绍及框架

  • 按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。而Linux三大驱动就是指对这些设备的驱动,即字符设备、块设备驱动和网络设备驱动。

1. 字符设备 (Character Devices)

  • 字符设备是一种按字节流(character stream)进行访问的设备,不可寻址,没有缓冲。你请求 5 个字节,它就给你 5 个字节(如果设备里有的话)。它不支持随机访问,数据只能顺序读写。
  • 原理

    • 核心: file_operations 结构体。里面定义了当用户空间程序对设备文件调用 open(), read(), write(), ioctl() 等系统调用时,内核应该执行的对应驱动函数。
    • VFS (虚拟文件系统):open("/dev/mydevice", ...) 时,VFS 会根据路径找到对应的 inode(索引节点),inode 中包含了设备号(主设备号和次设备号)。
    • 驱动注册: 驱动在加载时,会通过 register_chrdev()alloc_chrdev_region() + cdev_add() 来告诉内核能处理主设备号为 X 的设备,操作函数菜单是file_operations 结构体。
    • 连接: VFS 通过主设备号找到驱动和 file_operations,然后调用实现的 my_open(), my_read() 等函数,从而将用户空间的操作连接到了驱动代码上。
  • 典型例子:

    • 串口 (/dev/ttyS*)、控制台 (/dev/console)、鼠标 (/dev/input/mouse0)、键盘 (/dev/input/event*)
    • I2C/SPI 设备: 虽然挂在特定总线上,但最终给用户提供的接口往往是字符设备,如一个 I2C 接口的温湿度传感器,可能会表现为 /dev/i2c-1 或通过 sysfs 访问。
    • 裸设备驱动: 各种自定义的、简单的控制类设备
  • 代码示例


2. 块设备 (Block Devices)

  • 块设备是按“块”(Block)为单位进行数据访问的设备,块是固定大小的(如 512 字节、4KB)。与字符设备最大的不同是: 支持随机寻址它可以随机访问(直接读写第 N 个块),并且 有内核I/O缓冲区
  • 原理

    • 核心: block_device_operations 结构体和 请求队列 (Request Queue)
    • I/O 调度器: 当用户程序请求读写数据时,请求不会立即发送给硬件。而是被分解成一个个对“块”的操作请求(struct request),放入一个请求队列中。内核的 I/O 调度器 会对队列里的请求进行合并、排序,以提高磁盘寻道效率(比如把对相邻块的请求放在一起处理)。
    • 缓冲/缓存 (Buffer Cache): 内核会把频繁访问的块设备数据缓存在内存中(Page Cache/Buffer Cache)。当用户请求读取数据时,如果缓存里有,就直接从内存返回,速度极快,根本不需要访问物理设备。写操作也可能先写入缓存,稍后再“刷”到磁盘上。
    • 驱动的角色: 块设备驱动的主要工作不是直接处理 read/write,而是从请求队列中取出已经由 I/O 调度器优化好的 request,然后根据 request 里的信息(起始块号、块数量、方向),操作硬件来完成真正的数据传输。
    • 使用: 通常不直接用 read/write/dev/sda 这样的裸设备进行操作(虽然也可以)。但更常见的用法是:在块设备上创建文件系统(mkfs.ext4 /dev/sda1),然后 mount 到一个目录上。之后,用户和程序就通过文件系统来访问,享受到了文件系统和块设备层共同带来的高效和便利。
  • 典型例子: 硬盘 (HDD/SSD)(/dev/sda)、U盘 (/dev/sdb)、SD卡 (/dev/mmcblk0)、RAM disk(内存模拟的块设备)、Flash 存储 (通过 MTD): NAND/NOR Flash 在 MTD 层之上也可以表现为块设备。

  • 代码示例


3. 网络设备 (Network Devices)

  • 网络设备是用于收发数据包(Packet)的设备。它和其他两类设备有本质区别,它不对应 /dev 目录下的文件节点。而是通过单独的网络接口来代表。
  • 原理

    • 核心: net_device_ops 结构体和 sk_buff (Socket Buffer)
      • 接口而非文件: 网络设备在内核中被抽象成一个接口(Interface),如 eth0, wlan0。用户空间程序通过 Socket APIsocket(), bind(), sendto(), recvfrom())等内核协议栈来与内核的 TCP/IP 协议栈交互,而不是操作设备文件。
      • 数据流:
        • 发送: 用户数据通过 Socket API 进入内核协议栈,被层层打包(加上 TCP/UDP 头、IP 头等),最终形成一个 sk_buff 结构体。这个 sk_buff 被交给网络设备驱动。驱动的 ndo_start_xmit 函数(定义在 net_device_ops 中)负责将 sk_buff 里的数据包通过物理网卡发送出去。
        • 接收: 网卡收到一个数据包,产生硬件中断。驱动的中断处理程序把数据从硬件接收到内存,封装成一个新的 sk_buff,然后把它交给内核网络协议栈。协议栈逐层解包,最后通过 Socket 将数据送达正确的应用程序。
      • 驱动的角色: 网络设备驱动是硬件和内核协议栈之间的“搬运工”,主要负责:初始化网卡、启动/停止数据收发、在 sk_buff 和硬件之间传递数据包。
  • 典型例子: 有线网卡 (eth0, enp3s0)、无线网卡 (wlan0)虚拟网络接口、 CAN总线设备、 USB网络适配器。

  • 代码示例


对比

特性 字符设备 (Char) 块设备 (Block) 网络设备 (Net) 平台驱动 (Platform)
数据单位 字节流 (Stream) 数据块 (Block) 数据包 (Packet) 不直接处理数据流
访问方式 顺序访问 随机访问 Socket API N/A
I/O 缓冲 无 (或很简单) 有内核缓冲/缓存和I/O调度 有 Socket 缓冲 N/A
用户接口 /dev 文件节点 /dev 文件节点, 文件系统 Socket 接口, ifconfig 通常是为其他驱动提供服务
核心结构体 file_operations block_device_operations net_device_ops platform_driver
核心机制 VFS 文件操作映射 请求队列和I/O调度 协议栈和sk_buff 设备与驱动的分离、匹配、探测
主要用途 简单、串行 I/O 设备 存储设备 网络通信 SoC 内部集成外设的管理框架

字符设备代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h> // 包含 file_operations 结构体
#include <linux/cdev.h> // 包含 cdev 结构体和相关函数
#include <linux/device.h> // 包含 class_create 和 device_create
#include <linux/uaccess.h> // 包含 copy_to_user 和 copy_from_user
#include <linux/slab.h> // 包含 kmalloc 和 kfree

#define DEVICE_NAME "mymem_char"
#define CLASS_NAME "mymem_class"
#define MAX_BUFFER_SIZE 1024

// --- 驱动核心数据结构 ---
static int major_number; // 主设备号
static char *kernel_buffer; // 内核数据缓冲区
static struct class* my_class = NULL; // 设备类
static struct cdev my_cdev; // 字符设备结构

// --- file_operations 函数实现 ---

// open 函数:当设备文件被打开时调用
static int my_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "MyCharDev: Device opened.\n");
// 通常可以在这里为每个打开实例分配私有数据
// file->private_data = ...
return 0;
}

// release 函数:当设备文件被关闭时调用
static int my_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "MyCharDev: Device closed.\n");
// 清理 open 时分配的私有数据
// kfree(file->private_data);
return 0;
}

// read 函数:从设备读取数据
static ssize_t my_read(struct file *filp, char __user *user_buf, size_t len, loff_t *offset)
{
int bytes_to_read;

// 检查读取长度是否有效
if (*offset >= MAX_BUFFER_SIZE)
return 0; // End of file
if (*offset + len > MAX_BUFFER_SIZE)
len = MAX_BUFFER_SIZE - *offset;

bytes_to_read = len;

// 使用 copy_to_user 将内核数据拷贝到用户空间
if (copy_to_user(user_buf, kernel_buffer + *offset, bytes_to_read) != 0) {
printk(KERN_ERR "MyCharDev: Failed to copy data to user.\n");
return -EFAULT;
}

*offset += bytes_to_read; // 更新文件偏移
printk(KERN_INFO "MyCharDev: Read %d bytes.\n", bytes_to_read);
return bytes_to_read;
}

// write 函数:向设备写入数据
static ssize_t my_write(struct file *filp, const char __user *user_buf, size_t len, loff_t *offset)
{
int bytes_to_write;

// 检查写入位置是否有效
if (*offset >= MAX_BUFFER_SIZE) {
printk(KERN_WARNING "MyCharDev: No space left on device.\n");
return -ENOSPC; // No space left on device
}
if (*offset + len > MAX_BUFFER_SIZE)
len = MAX_BUFFER_SIZE - *offset;

bytes_to_write = len;

// 使用 copy_from_user 将用户数据拷贝到内核空间
if (copy_from_user(kernel_buffer + *offset, user_buf, bytes_to_write) != 0) {
printk(KERN_ERR "MyCharDev: Failed to copy data from user.\n");
return -EFAULT;
}

*offset += bytes_to_write; // 更新文件偏移
printk(KERN_INFO "MyCharDev: Wrote %d bytes.\n", bytes_to_write);
return bytes_to_write;
}

// --- file_operations 结构体定义 ---
// 将实现的函数与标准文件操作关联起来
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};

// --- 模块初始化函数 ---
static int __init memchar_init(void)
{
dev_t dev_num;

// 1. 分配内核缓冲区
kernel_buffer = kmalloc(MAX_BUFFER_SIZE, GFP_KERNEL);
if (!kernel_buffer) {
printk(KERN_ERR "MyCharDev: Failed to allocate kernel buffer.\n");
return -ENOMEM;
}

// 2. 动态分配主设备号
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_ERR "MyCharDev: Failed to allocate major number.\n");
kfree(kernel_buffer);
return -1;
}
major_number = MAJOR(dev_num);
printk(KERN_INFO "MyCharDev: Major number allocated: %d\n", major_number);

// 3. 初始化 cdev 结构体,并与 file_operations 关联
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;

// 4. 将 cdev 添加到内核
if (cdev_add(&my_cdev, dev_num, 1) < 0) {
printk(KERN_ERR "MyCharDev: Failed to add cdev to the kernel.\n");
unregister_chrdev_region(dev_num, 1);
kfree(kernel_buffer);
return -1;
}

// 5. 创建设备类
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
printk(KERN_ERR "MyCharDev: Failed to create device class.\n");
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
kfree(kernel_buffer);
return PTR_ERR(my_class);
}

// 6. 创建设备文件 (/dev/mymem_char)
if (device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME) == NULL) {
printk(KERN_ERR "MyCharDev: Failed to create device file.\n");
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
kfree(kernel_buffer);
return -1;
}

printk(KERN_INFO "MyCharDev: Driver loaded successfully.\n");
return 0;
}

// --- 模块卸载函数 ---
static void __exit memchar_exit(void)
{
dev_t dev_num = MKDEV(major_number, 0);

// 逆序清理资源
device_destroy(my_class, dev_num); // 销毁设备文件
class_destroy(my_class); // 销毁设备类
cdev_del(&my_cdev); // 从内核移除 cdev
unregister_chrdev_region(dev_num, 1); // 释放设备号
kfree(kernel_buffer); // 释放内核缓冲区

printk(KERN_INFO "MyCharDev: Driver unloaded.\n");
}

module_init(memchar_init);
module_exit(memchar_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver for memory simulation.");

块设备代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/genhd.h> // 包含 gendisk
#include <linux/fs.h> // 包含 block_device_operations
#include <linux/blkdev.h> // 包含请求队列相关函数
#include <linux/vmalloc.h> // 使用 vmalloc 分配大块内存

#define DEVICE_NAME "myram_block"
#define SECTOR_SIZE 512
#define DEVICE_SECTORS 20480 // 10MB (20480 * 512 bytes)

// --- 驱动核心数据结构 ---
static int major_number; // 主设备号
static u8 *device_data; // 模拟磁盘的内存区域
static struct gendisk *my_disk; // gendisk 结构,代表一个独立的磁盘
static struct request_queue *my_queue; // 请求队列
static spinlock_t lock; // 用于保护请求队列的自旋锁

// --- 请求处理函数 ---
// 这是块设备驱动的核心,处理来自I/O调度器的请求
static void my_request_fn(struct request_queue *q)
{
struct request *req;

// 循环处理队列中的所有请求
while ((req = blk_fetch_request(q)) != NULL) {
// 检查请求是否合法(这里简化处理,只检查读写请求)
if (req == NULL || (rq_data_dir(req) != READ && rq_data_dir(req) != WRITE)) {
printk(KERN_NOTICE "MyRamBlock: Skipping non-RW request\n");
__blk_end_request_all(req, -EIO);
continue;
}

// 计算物理地址和大小
// blk_rq_pos(req) 返回起始扇区号
// blk_rq_cur_bytes(req) 返回请求的总字节数
unsigned long offset = blk_rq_pos(req) * SECTOR_SIZE;
unsigned long num_bytes = blk_rq_cur_bytes(req);

// 模拟数据传输
if (rq_data_dir(req) == WRITE) {
// bio_for_each_segment 遍历请求中的所有段 (segment)
// 将请求缓冲区中的数据拷贝到我们的模拟磁盘内存
memcpy(device_data + offset, bio_data(req->bio), num_bytes);
} else {
// 将模拟磁盘内存中的数据拷贝到请求缓冲区
memcpy(bio_data(req->bio), device_data + offset, num_bytes);
}

// 标记请求完成
__blk_end_request_all(req, 0); // 0 表示成功
}
}

// --- block_device_operations ---
// 对于简单的驱动,这个结构体可以为空
static struct block_device_operations my_bops = {
.owner = THIS_MODULE,
};

// --- 模块初始化函数 ---
static int __init ramblock_init(void)
{
// 1. 分配模拟磁盘的内存
device_data = vmalloc(DEVICE_SECTORS * SECTOR_SIZE);
if (!device_data) {
return -ENOMEM;
}

// 2. 注册块设备,获取主设备号
major_number = register_blkdev(0, DEVICE_NAME);
if (major_number < 0) {
vfree(device_data);
return major_number;
}

// 3. 初始化自旋锁和请求队列
spin_lock_init(&lock);
my_queue = blk_init_queue(my_request_fn, &lock);
if (!my_queue) {
unregister_blkdev(major_number, DEVICE_NAME);
vfree(device_data);
return -ENOMEM;
}

// 4. 分配和初始化 gendisk 结构
my_disk = alloc_disk(1); // 1个次设备 (分区)
if (!my_disk) {
blk_cleanup_queue(my_queue);
unregister_blkdev(major_number, DEVICE_NAME);
vfree(device_data);
return -ENOMEM;
}

// 5. 填充 gendisk 信息
my_disk->major = major_number;
my_disk->first_minor = 0;
my_disk->fops = &my_bops;
my_disk->queue = my_queue;
snprintf(my_disk->disk_name, 32, DEVICE_NAME);
set_capacity(my_disk, DEVICE_SECTORS); // 设置磁盘容量(以扇区为单位)

// 6. 将 gendisk 添加到系统,使其可见
add_disk(my_disk);

printk(KERN_INFO "MyRamBlock: Driver loaded. Major: %d\n", major_number);
return 0;
}

// --- 模块卸载函数 ---
static void __exit ramblock_exit(void)
{
del_gendisk(my_disk); // 从系统移除 gendisk
put_disk(my_disk); // 释放 gendisk 引用
blk_cleanup_queue(my_queue); // 清理请求队列
unregister_blkdev(major_number, DEVICE_NAME); // 注销块设备
vfree(device_data); // 释放内存

printk(KERN_INFO "MyRamBlock: Driver unloaded.\n");
}

module_init(ramblock_init);
module_exit(ramblock_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple RAM-based block device driver.");

网络设备代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netdevice.h> // 包含 net_device 和相关函数
#include <linux/etherdevice.h> // 包含 alloc_etherdev

#define DEVICE_NAME "mynet"

// --- 驱动核心数据结构 ---
// 我们将自定义的统计信息和设备指针放在一个结构体中
struct mynet_priv {
struct net_device_stats stats;
struct net_device *dev;
};

static struct net_device *my_net_dev;

// --- net_device_ops 函数实现 ---

// open 函数:当接口被 "ifconfig up" 启动时调用
static int mynet_open(struct net_device *dev)
{
// 启动传输队列
netif_start_queue(dev);
printk(KERN_INFO "%s: Device opened.\n", dev->name);
return 0;
}

// stop 函数:当接口被 "ifconfig down" 关闭时调用
static int mynet_stop(struct net_device *dev)
{
// 停止传输队列
netif_stop_queue(dev);
printk(KERN_INFO "%s: Device stopped.\n", dev->name);
return 0;
}

// 发包函数:这是网络驱动的核心,负责发送数据包
static netdev_tx_t mynet_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct mynet_priv *priv = netdev_priv(dev);

printk(KERN_INFO "%s: Transmitting packet (len: %u)\n", dev->name, skb->len);

// 更新统计信息
priv->stats.tx_packets++;
priv->stats.tx_bytes += skb->len;

// --- 模拟环回 ---
// 正常驱动会在这里把 skb 的数据通过 DMA 发送到硬件
// 我们直接将 skb 重新送回收包路径
skb->protocol = eth_type_trans(skb, dev); // 设置协议类型
skb->dev = dev;
netif_rx(skb); // 将 skb 传递给内核协议栈的接收部分

// 告诉内核数据包已发送,可以释放 skb
// dev_kfree_skb(skb); // 真实驱动中发送完会释放 skb
// 但因为我们环回了,协议栈会负责释放它

return NETDEV_TX_OK; // 返回 OK 表示发送成功
}

// 获取统计信息函数
static struct net_device_stats *mynet_get_stats(struct net_device *dev)
{
struct mynet_priv *priv = netdev_priv(dev);
return &priv->stats;
}

// --- net_device_ops 结构体定义 ---
static const struct net_device_ops mynet_ops = {
.ndo_open = mynet_open,
.ndo_stop = mynet_stop,
.ndo_start_xmit = mynet_start_xmit,
.ndo_get_stats = mynet_get_stats,
};

// --- setup 函数,用于初始化设备 ---
void mynet_setup(struct net_device *dev)
{
// 设置为以太网设备
ether_setup(dev);

// 关联我们的操作函数
dev->netdev_ops = &mynet_ops;

// 分配一个随机的 MAC 地址
eth_hw_addr_random(dev);

// 其他设备特性标志
dev->flags |= IFF_NOARP;
}

// --- 模块初始化函数 ---
static int __init netloop_init(void)
{
struct mynet_priv *priv;

// 1. 分配 net_device 结构体,并为私有数据分配空间
my_net_dev = alloc_netdev(sizeof(struct mynet_priv), DEVICE_NAME, NET_NAME_UNKNOWN, mynet_setup);
if (!my_net_dev) {
return -ENOMEM;
}

// 获取私有数据指针
priv = netdev_priv(my_net_dev);
priv->dev = my_net_dev;

// 2. 注册网络设备到内核
if (register_netdev(my_net_dev)) {
printk(KERN_ERR "Failed to register net device\n");
free_netdev(my_net_dev);
return -1;
}

printk(KERN_INFO "%s: Driver loaded.\n", my_net_dev->name);
return 0;
}

// --- 模块卸载函数 ---
static void __exit netloop_exit(void)
{
printk(KERN_INFO "%s: Unloading driver.\n", my_net_dev->name);
unregister_netdev(my_net_dev); // 从内核注销
free_netdev(my_net_dev); // 释放 net_device
}

module_init(netloop_init);
module_exit(netloop_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple loopback network device driver.");

三种常见的 linux 设备的驱动介绍及框架

https://goko-son626.github.io/post/the-Three-Basic-Linux-Driver-Models.html

作者

GoKo Mell

发布于

2024-03-05

更新于

2025-09-11

许可协议

评论

:D 一言句子获取中...