Linux驱动基础知识

Gryphon Lv3

Linux驱动基础知识

linux驱动是linux内核驱动的全称。对设备驱动最通俗的解释就是“驱使硬件设备行动”,有操作系统的存在则大大降低了应用软件与硬件平台的耦合度。

1.设备驱动分类

linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。在文件系统下,都有对应文件与键盘、鼠标、硬盘等实实在在硬件硬件设备关联,访问这些文件就可以访问实际硬件。

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

1.1 字符设备

字符设备指能够像字节流串行顺序依次进行访问的设备,对它的读写是以字节为单位。

字符设备的特点:

  • 一个字节一个字节读写的设备
  • 读取数据需要按照先后数据(顺序读取)
  • 每个字符设备在/dev目录下对应一个设备文件,linux用户程序通过设备文件(或称 设备节点)来使用驱动程序操作字符设备。
  • 常见的字符设备有鼠标、键盘、串口、SPI、I2C等

1.2 块设备

块设备是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。

块设备的特点:

  • 数据以固定长度进行传输,比如512K
  • 块设备能够随机访问,而字符设备则只能顺序访问。
  • 块设备包括硬盘、磁盘、U盘和SD卡等
  • 每个块设备在/dev目录下对应一个设备文件,linux用户程序可以通过设备文件(或称设备节点)来使用驱动程序操作块设备。
  • 块设备可以容纳文件系统,所以一般都通过文件系统来访问,而不是/dev设备节点。

1.3. 网络设备

网络设备驱动不同于字符设备和块设备,不在/dev下以文件节点代表,而是通过单独的网络接口来代表。

网络设备的特点:

  • 网络接口没有像字符设备和块设备一样的设备号和/dev设备节点,只有接口名,如eth0,eth1
  • 对网络设备的访问只能通过socket操作,而不是open、closc、read、write

1.4 对比

驱动类型 数据交换单位 访问模式 典型设备示例
字符设备驱动 字节(Byte) 顺序访问 串口、键盘、鼠标
块设备驱动 固定大小的数据块(如 512B/4KB) 随机访问 硬盘、SSD、U盘
网络设备驱动 数据包(Packet / Frame) 无固定顺序 网卡、Wi-Fi 模块

2.驱动的构建方式(Build Target Type)

  1. 内置(Built-in) —— 编译后直接链接进内核镜像(如 vmlinux / zImage / uImage);
  2. 模块(Module) —— 编译成 .ko 文件,可动态加载/卸载。关于这一部分的实现原理可用参考linux内核模块;

3.linux内核驱动的实现方式

Linux 内核驱动的实现方式可分为:

  1. 传统驱动(Legacy Driver / Board-file based Driver)
  2. 现代驱动(Device Tree based Driver / DT-enabled Driver)
特性 传统驱动(Legacy) 现代驱动(Device Tree based)
硬件描述方式 硬编码在驱动或 arch/ 板级文件中 通过设备树(.dts)描述硬件资源
资源获取方式 platform_get_resource(), 宏定义、全局变量 of_get_named_gpio(), of_property_read_u32()
设备注册 手动 platform_device_register() 内核自动从设备树生成 platform_device
可移植性 ❌ 差,换板子要改驱动或板文件 ✅ 好,只需改设备树,驱动通用
维护性 ❌ 差,驱动与硬件强耦合 ✅ 好,驱动与硬件解耦
内核主线支持 ⚠️ 逐步淘汰,仅老平台保留 ✅ 推荐,ARM/RISC-V 新平台必须使用
典型内核版本 Linux 2.6 ~ 3.10 Linux 3.7+(ARM强制),4.4+(主流全面支持)

4.一个简单的内核模块加载/卸载的演示

Linux内核模块的代码框架通常由下面几个部分组成:

  • 模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
  • 模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
  • 模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
  • 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
  • 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
  • 模块的其他相关信息: 可以声明模块作者等信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <linux/init.h>
#include <linux/module.h>

static int module_init_func(void)
{
printk(KERN_INFO "Init Hello World module...\n");
return 0;
}

static void module_exit_func(void)
{
printk(KERN_INFO "Hello World Exit...\n");
}

module_init(module_init_func); // 驱动入口
module_exit(module_exit_func); // 驱动出口

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Gryphon");
MODULE_DESCRIPTION("A simple hello world module");
MODULE_ALIAS("Hello world module");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 目标模块名称(生成 helloworld.ko)
obj-m += helloworld.o

# 内核源码目录(你提供的路径)
KDIR := /home/gryphon/SDK/linux

# 当前目录
PWD := $(shell pwd)

CROSS_COMPILE := /home/gryphon/SDK/tools/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
ARCH := arm
CC := $(CROSS_COMPILE)gcc


# 默认目标
default:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC=$(CC) modules

# 清理
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules clean

.PHONY: default clean

输出log示例:

1
2
3
4
5
6
7
8
9
10
11
# lsmod
Module Size Used by Tainted: G
# insmod helloworld.ko
[ 7377.171567] Init Hello World module...
#
# lsmod
Module Size Used by Tainted: G
helloworld 16384 0
# rmmod helloworld.ko
[ 7386.308034] Hello World Exit...
#

5.内核模块传参与符号共享

内核模块作为一个可拓展的动态模块,为Linux内核提供了灵活性,但是有时我们需要根据不同的应用场景给内核传递不同的参数,例如在程序中开启调试模式、设置详细输出模式以及制定与具体模块相关的选项,都可以通过参数的形式来改变模块的行为。

Linux内核提供一个宏来实现模块的参数传递。

module_param函数 (内核源码/include/linux/moduleparam.h) #define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #define module_param_array(name, type, nump, perm) \ module_param_array_named(name, name, type, nump, perm) 内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。 其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是charp。 实现模块的参数传递
/include/linux/export.h #define EXPORT_SYMBOL(sym) __EXPORT_SYMBOL(sym, “”) //EXPORT_SYMBOL宏用于向内核导出符号,这样的话,其他模块也可以使用我们导出的符号了。

5.1 测试用例:

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
#include <linux/init.h>
#include <linux/module.h>

static int itype = 0;
module_param(itype, int, 0);
EXPORT_SYMBOL(itype);


static bool btype = 0;
module_param(btype, bool, 0644);

static char ctype = 0;
module_param(ctype, byte, 0);

static char *stype = 0;
module_param(stype, charp, 0644);

static int __init param_init(void)
{
printk(KERN_ALERT "param init!\n");
printk(KERN_ALERT "itype=%d\n", itype);
printk(KERN_ALERT "btype=%d\n", btype);
printk(KERN_ALERT "ctype=%d\n", ctype);
printk(KERN_ALERT "stype=%s\n", stype);

return 0;
}

static void param_exit(void)
{
printk(KERN_ALERT "param exit!\n");
printk(KERN_ALERT "itype=%d\n", itype);
printk(KERN_ALERT "btype=%d\n", btype);
printk(KERN_ALERT "ctype=%d\n", ctype);
printk(KERN_ALERT "stype=%s\n", stype);
}

int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);

int my_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(my_sub);
module_init(param_init); // 驱动入口
module_exit(param_exit); // 驱动出口

image.png

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
// calculation.h
#ifndef __CALCULATION_H__
#define __CALCULATION_H__

extern int itype;

int my_add(int a, int b);
int my_sub(int a, int b);

#endif

// calculation.c
#include <linux/init.h>
#include <linux/module.h>
#include "calculation.h"
static int __init calculation_init(void)
{
printk(KERN_ALERT "calculation init!\n");
printk(KERN_ALERT "itype+1 = %d, itype-1 = %d\n", my_add(itype, 1), my_sub(itype, 1));
return 0;
}
static void calculation_exit(void)
{
printk(KERN_ALERT "calculation init!\n");
printk(KERN_ALERT "itype+1 = %d, itype-1 = %d\n", my_add(itype, 1), my_sub(itype, 1));
}
module_init(calculation_init);
module_exit(calculation_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Gryphon");
MODULE_DESCRIPTION("A simple hello world module with calculation functions");
MODULE_ALIAS("Hello world module");

// Makefile
# 目标模块名称(生成 moduleparam_test.ko)
obj-m += moduleparam_test.o

# 内核源码目录(你提供的路径)
KDIR := /home/gryphon/SDK/linux

# 当前目录
PWD := $(shell pwd)

CROSS_COMPILE := /home/gryphon/SDK/tools/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
ARCH := arm
CC := $(CROSS_COMPILE)gcc

# 默认目标
default:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC=$(CC) modules

# 清理
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean

.PHONY: default clean

测试log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# insmod moduleparam_test.ko itype=123 btype=1 ctype=200 stype=abc
[ 144.225190] moduleparam_test: module license 'unspecified' taints kernel.
[ 144.232153] Disabling lock debugging due to kernel taint
[ 144.238806] param init!
[ 144.241271] itype=123
[ 144.243539] btype=1
[ 144.245634] ctype=200
[ 144.248043] stype=abc
# insmod calculation.ko
[ 147.224498] calculation init!
[ 147.227575] itype+1 = 124, itype-1 = 122
#
# rmmod calculation.ko
[ 150.404026] calculation init!
[ 150.407110] itype+1 = 124, itype-1 = 122
# rmmod moduleparam_test.ko
[ 154.941217] param exit!
[ 154.943692] itype=123
[ 154.946010] btype=1
[ 154.948234] ctype=200
[ 154.950513] stype=abc
#

image.png

参考链接

2. Linux驱动简述 — [野火]嵌入式Linux驱动开发实战指南——基于LubanCat-全志系列板卡 文档

驱动入口点——platform_device解析 - 知乎

  • Title: Linux驱动基础知识
  • Author: Gryphon
  • Created at : 2025-10-13 08:46:43
  • Updated at : 2025-10-13 08:46:43
  • Link: https://phoenixs.gitlab.io/2025/10/13/youdaonote/linux驱动/000.linux驱动简介/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments