提示:本文为系统的本地化文档,想要按照本文档完成系统的移植,需要你提前准备好Android的原生SDK。
前言
提示:感谢北京迅为电子
感谢【北京迅为电子】,本文是参考北京迅为电子的相关视频总结而成。
提示:以下是本篇文章正文内容
第一部分 驱动基础
一、驱动概述
驱动的分类
种类 | 描述 |
---|---|
字符设备 | 必须以串行顺序依次进行访问的设备,如鼠标 |
块设备 | 按照任意顺序进行访问,如硬盘 |
网络设备 | 面向数据包的接收和发送 |
Linux官网
https://www.kernel.org/
给内核开发者或者半导体厂商使用。
Linux源码目录
目录 | 内容 |
---|---|
arch | 存放不同平台体系相关代码 |
block | 存放块设备相关代码 |
crypto | 存放加密、压缩、CRC 校验等算法相关代码 |
Documentation | 存放相关说明文档,很多实用文档,包括驱动编写等 |
drivers | 存放 Linux 内核设备驱动程序源码。该目录包含众多驱动,目录按照设备类别进行分类,如 char、block 、input、i2c、spi、pci、usb 等。 |
firmware | 存放处理器相关的一些特殊固件 |
fs | 存放虚拟文件系统代码 |
include | 存放内核所需、与平台无关的头文件 |
init | Linux 系统启动初始化相关的代码 |
ipc | 存放进程间通信代码 |
kernel | Linux 内核的核心代码,包含了进程调度子系统,以及和进程调度相关的模块。 |
lib | 库文件代码, 实现需要在内核中使用的库函数,例如 CRC、FIFO、list、MD5等。 |
mm | 实现存放内存管理代码 |
net | 存放网络相关代码 |
samples | 存放提供的一些内核编程范例 |
scripts | 存放一些脚本文件 |
security | 存放系统安全性相关代码 |
sound | 存放声音、声卡相关驱动 |
tools | 一些常用工具,如性能剖析、自测试等 |
usr | 用于生成 initramfs 的代码。 |
virt | 提供虚拟机技术(KVM 等)的支持 |
二、驱动框架
驱动六大要素
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void) //驱动入口函数
{
printk(KERN_EMERG "helloworld_init\r\n");// 注意:内核打印用 printk 而不是 printf
return 0;
}
static void __exit helloworld_exit(void) //驱动出口函数
{
printk(KERN_EMERG "helloworld_exit\r\n");
}
//模块加载函数
//当使用加载驱动模块时,内核会执行模块加载函数,完成模块加载函数中的初始化工作。
module_init(helloworld_init); //注册入口函数
//模块卸载函数
//当卸载某模块时,内核会执行模块卸载函数,完成模块卸载函数中的退出工作。
module_exit(helloworld_exit); //注册出口函数
//模块许可证声明
//许可证声明描述了内核模块的许可权限,如果不声明模块许可,模块在加载的时候,会收到“内核被污染(kernel tainted)”的警告。可接受的内核模块声明许可包括“GPL”“GPL v2”
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
//模块作者信息等说明(可选择)
MODULE_AUTHOR("auther"); //作者信息(可选)
//模块参数(可选择)
//模块参数是模块被加载的时候可以传递给它的值。
//模块导出符号(可选择)
//内核模块可以导出的符号,如果导出,其他模块可以使用本模块中的变量或函数。
驱动内无法使用标准C库,所以使用printk接口
三、编译驱动模块
编译驱动程序的两种方法:
- 模块驱动
- 内核驱动(模块驱动后缀是.ko)
实际操作:
在驱动源码同级目录下创建一个文件夹,创建两个文件:
kernel/test/Makefile
export ARCH=arm64
export CROSS_COMPILE=/work/topeet/rk3568/rk_android11.0_sdk/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
obj-m += hello_world.o #helloworld.c 对应 .o 文件的名称。名称要保持一致。
PWD ?= $(shell pwd)
KDIR :=/work/topeet/rk3568/rk_android11.0_sdk/kernel #内核源码所在虚拟机 ubuntu 的实际路径
all:
echo $(KDIR)
echo $(PWD)
make -C $(KDIR) M=$(PWD) modules #make 操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean 操作
编译后的输出文件:
├── hello_world.c
├── hello_world.ko ★
├── hello_world.mod.c
├── hello_world.mod.o
├── hello_world.o
├── Makefile
├── modules.order
└── Module.symvers
驱动加载命令说明
命令 | 用法 | 说明 |
---|---|---|
insmod | insmod hello_world.ko | 载入linux内核模块,如果依赖模块没有加载会提示 |
modprobe | modprobe hello_world.ko | 加载Linux内核模块,同时这个模块依赖的模块也同时被加载 |
rmmod | rmmod hello_world.ko | 移除已经载入Linux的内核模块 |
lsmod | lsmod | 列出已经载入Linux的内核模块,也可以用cat /proc/modules查看 |
modinfo | modinfo hello_world.ko | 查看内核模块信息 |
编码端执行:
adb root;adb remount;adb push *.ko /sdcard/work/
串口端执行:
cd /sdcard/work/;su root
四、编译内核驱动
1.前置准备
使用 menuconfig 图形化配置界面之前需要先在Ubuntu 中安装 ncurses 库
sudo apt-get install build-essential
sudo apt-get install libncurses5-dev
各种图形化配置命令
make config (基于文本的最为传统的配置界面,不推荐使用)
make menuconfig (基于文本菜单的配置界面)
make xconfig (要求 QT 被安装)
make gconfig (要求 GTK+ 被安装)
打开menuconfig命令
export ARCH=arm64;make rockchip_linux_defconfig;make menuconfig
make ARCH=arm64 savedefconfig;cp .config arch/arm64/configs/itop_rockchip_defconfig
与make menuconfig有关的文件
名称 | 描述 |
---|---|
Kconfig | 饭店的菜单 |
.config | 使用饭店的菜单点完的菜品 |
Makefile | 菜的做法 |
2.Kconfig语法
kernel/Kconfig
# mainmenu 顾名思义就是主菜单,也就是我们输入完“make menuconfig”以后默认打开的界面
# mainmenu 用来设置主菜单的标题
mainmenu "Linux/$(ARCH) $(KERNELVERSION) Kernel Configuration"
# comment 语句出现在界面的第一行,用于定义一些提示信息。
comment "Compiler: $(CC_VERSION_TEXT)"
# source 用于读取另一个 Kconfig 文件
# 比如“source "init/Kconfig"”就是读取 init 目录下的Kconfig 文件。
source "scripts/Kconfig.include"
source "init/Kconfig"
source "kernel/Kconfig.freezer"
source "fs/Kconfig.binfmt"
source "mm/Kconfig"
source "net/Kconfig"
source "drivers/Kconfig"
source "fs/Kconfig"
source "security/Kconfig"
source "crypto/Kconfig"
source "lib/Kconfig"
source "lib/Kconfig.debug"
kernel/init/Kconfig
...
# menu/endmenu 条目用于生成菜单
menu "General setup"
config BROKEN
bool
...
config COMPILE_TEST
bool "Compile also drivers which will not load" # 决定选项 类型 提示
defaulty n # 默认状态
# 指定依赖关系,
# 当依赖的选项被选中时,当前的配置选项的信息才会在菜单中显示出来,才能操作该选项的内容
depends on HAS_IOMEM
# 帮助关系
# 在图形化界面按下 h 按键,弹出来的就是 help 的内容。
help
Some drivers can be compiled on a different platform than they are
intended to be run on. Despite they cannot be loaded there (or even
...
# choice 条目将多个类似的配置选项组合到一起,供用户选择,
# 用户选择是从“choice”开始,从“endchoice”结束,
# “choice”和“endchoice”之间有很多的 config 条目,这些 config 条目是提供用户选择的,
choice
prompt "Kernel compression mode"
default KERNEL_GZIP
depends on HAVE_KERNEL_GZIP || HAVE_KERNEL_BZIP2 || HAVE_KERNEL_LZMA || HAVE_KERNEL_XZ
|| HAVE_KERNEL_LZO || HAVE_KERNEL_LZ4 || HAVE_KERNEL_UNCOMPRESSED
help
The linux kernel is a kind of self-extracting executable...
If in doubt, select 'gzip'
config KERNEL_GZIP
bool "Gzip"
depends on HAVE_KERNEL_GZIP
help
The old and tried gzip compression. It provides a good balance
between compression ratio and decompression speed.
config KERNEL_BZIP2
bool "Bzip2"
depends on HAVE_KERNEL_BZIP2
help
Its compression ratio and speed is intermediate...
config KERNEL_LZMA
bool "LZMA"
depends on HAVE_KERNEL_LZMA
help
This compression algorithm's ratio is best...
config KERNEL_XZ
bool "XZ"
depends on HAVE_KERNEL_XZ
help
XZ uses the LZMA2 algorithm and instruction set specific..
config KERNEL_LZO
bool "LZO"
depends on HAVE_KERNEL_LZO
help
Its compression ratio is the poorest among the choices...
config KERNEL_LZ4
bool "LZ4"
depends on HAVE_KERNEL_LZ4
help
LZ4 is an LZ77-type compressor with a fixed, byte-oriented encoding...
config KERNEL_UNCOMPRESSED
bool "None"
depends on HAVE_KERNEL_UNCOMPRESSED
help
Produce uncompressed kernel image...
endchoice
...
endmenu # General setup
# select 关键字用来表示反向依赖关系,当指定当前选项被选中时,此时 select 后面的选项也会被自动选中。
关键字 | 描述 |
---|---|
bool | y/n |
tristate | y/n/m |
string | 字符串 |
hex | 16进制 |
int | 10进制 |
config配置文件位于kernel/arch/arm64/configs/
.config配置文件位于kernel/.config
defconfig 配置文件
自定义菜单
3.代码编辑
kernel/drivers/char新建三个文件
mkdir -p kernel/drivers/char/hello
touch kernel/drivers/char/hello/hello.c kernel/drivers/char/hello/Kconfig kernel/drivers/char/hello/Makefile
修改涉及文件
drivers/char/
├── hello
│ ├── hello.c(add)
│ ├── Kconfig(add)
│ └── Makefile(add)
├── Kconfig(modify)
└── Makefile(modify)
code drivers/char/hello/hello.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void)
{
printk(KERN_EMERG "helloworld_init\r\n");
return 0;
}
static void __exit helloworld_exit(void)
{
printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
code drivers/char/hello/Kconfig
config HELLO_WORLD
tristate "hello world"
default y
help
hello hello
code drivers/char/hello/Makefile
obj-$(CONFIG_HELLO)+=helloworld.o
code drivers/char/Kconfig
menu "Character devices"
source "drivers/tty/Kconfig"
(+)source "drivers/char/hello/Kconfig"
code drivers/char/Makefile
(+)obj-$(CONFIG_HELLO_WORLD) += hello/
obj-y += mem.o random.o
4.配置编译
make ARCH=arm64 itop_rockchip_defconfig;make ARCH=arm64 menuconfig
make ARCH=arm64 savedefconfig;cp .config arch/arm64/configs/itop_rockchip_defconfig
# 文档对比
meld arch/arm64/configs/rockchip_defconfig arch/arm64/configs/itop_rockchip_defconfig
五、驱动模块传参
1.参数接口
kernel/include/linux/moduleparam.h
- name:模块参数的名称
- type: 模块参数的数据类型
- perm: 模块参数的访问权限
#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)
- name:外部传入的参数名,即加载模块时的传入值
- string:内部的变量名,即程序内定义的参数名
- len:以 string 命名的 buffer 大小(可以小于 buffer 的大小,但是没有意义)
- perm:模块参数的访问权限
#define module_param_string(name, string, len, perm) \
static const struct kparam_string __param_string_##name \
= { len, string }; \
__module_param_call(MODULE_PARAM_PREFIX, name, \
¶m_ops_string, \
.str = &__param_string_##name, perm, -1, 0);\
__MODULE_PARM_TYPE(name, "string")
#define MODULE_PARM_DESC(_parm, desc) \
__MODULE_INFO(parm, _parm, #_parm ":" desc)
类型说明
- bool : 布尔型
- inbool : 布尔反值
- charp: 字符指针(相当于 char *,不超过 1024 字节的字符串)
- short: 短整型
- ushort : 无符号短整型
- int : 整型
- uint : 无符号整型
- long : 长整型
- ulong: 无符号长整型。
权限说明
kernel/include/linux/stat.h
#define S_IRWXUGO (S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
#define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO (S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO (S_IXUSR|S_IXGRP|S_IXOTH)
kernel/include/uapi/linux/stat.h
#define S_IRWXU 00700 /*文件所有者可读写执行*/
#define S_IRUSR 00400 /*文件所有者可读*/
#define S_IWUSR 00200 /*文件所有者可写*/
#define S_IXUSR 00100 /*文件所有者可执行*/
#define S_IRWXG 00070 /*与文件所有者同组的用户可读写执行*/
#define S_IRGRP 00040 /*与文件所有者同组的用户可读*/
#define S_IWGRP 00020 /*与文件所有者同组的用户可写*/
#define S_IXGRP 00010 /*与文件所有者同组的用户可执行*/
#define S_IRWXO 00007 /*与文件所有者不同组的用户可读写执行*/
#define S_IROTH 00004 /*与文件所有者不同组的用户可读*/
#define S_IWOTH 00002 /*与文件所有者不同组的用户可写*/
#define S_IXOTH 00001 /*与文件所有者不同组的用户可执行*/
2.代码编辑
vscode添加头文件路径
.vscode/c_cpp_properties.json
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/zuozhongkai/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/include",
"/home/zuozhongkai/linux/atk-mp1/linux/my_linux/linux-5.4.31/include",
"/home/zuozhongkai/linux/atk-mp1/linux/my_linux/linux-5.4.31/arch/arm/include/generated"
(+)"/work/topeet/rk3568/rk_android11.0_sdk/kernel/include"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu11",
"cppStandard": "gnu++14",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
04_Linux_Drivers/02_parameter/parameter.c
#include <linux/init.h>
#include <linux/module.h>
static int number;//定义int类型变量number
static char *name;//定义char类型变量name
static int para[8];//定义int类型的数组
static char str1[10];//定义char类型字符串str1
static int n_para;//定义int类型的用来记录module_param_array函数传递数组元素个数的变量n_para
module_param(number, int, S_IRUGO);//传递int类型的参数number,S_IRUGO表示权限为可读
MODULE_PARM_DESC(number,"e.g number=1");
module_param(name, charp, S_IRUGO);//传递char类型变量name
MODULE_PARM_DESC(name,"e.g name=c");
module_param_array(para , int , &n_para , S_IRUGO);//传递int类型的数组变量para
MODULE_PARM_DESC(para,"e.g para=1,2,3");
module_param_string(str, str1 ,sizeof(str1), S_IRUGO);//传递字符串类型的变量str1
MODULE_PARM_DESC(str,"e.g name=abdc");
static int __init parameter_init(void)//驱动入口函数
{
static int i;
printk(KERN_EMERG "%d\n",number);
printk(KERN_EMERG "%s\n",name);
for(i = 0; i < n_para; i++)
{
printk(KERN_EMERG "para[%d] : %d \n", i, para[i]);
}
printk(KERN_EMERG "%s\n",str1);
return 0;
}
static void __exit parameter_exit(void)//驱动出口函数
{
printk(KERN_EMERG "parameter_exit\n");
}
module_init(parameter_init);//注册入口函数
module_exit(parameter_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet"); //作者信息
3.执行结果
insmod parameter.ko number=1 name=ss para=1,2,3 str=hellohaha
[ 649.799545] 1
[ 649.799614] ss
[ 649.799619] para[0] : 1
[ 649.799624] para[1] : 2
[ 649.799629] para[2] : 3 console:/storage/emulated/0/Download #
[ 649.799634] hellohaha
六、驱动符号导出
1.调用接口
EXPORT_SYMBOL(num);
导出去的符号可以被其他模块使用,使用前声明一下即可。
2.代码编辑
接下来要在hello.c中调用mathmodule.c
04_Linux_Drivers/03_mathmodule/mathmodule.c
#include <linux/init.h>
#include <linux/module.h>
int num = 10;//★★★定义参数num
EXPORT_SYMBOL(num);//★★★导出参数num
int add(int a, int b)//★★★定义数学函数add(),用来实现加法
{
return a + b;
}
EXPORT_SYMBOL(add);//★★★导出数学函数add()
static int __init math_init(void)//驱动入口函数
{
printk("math_moudle init\n");
return 0;
}
static void __exit math_exit(void)//驱动出口函数
{
printk("math_module exit\n");
}
module_init(math_init);//注册入口函数
module_exit(math_exit);//注册出口函数
MODULE_LICENSE("GPL");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息
04_Linux_Drivers/03_mathmodule/hello.c
#include <linux/init.h>
#include <linux/module.h>
extern int num;//★★★导入int类型变量num
extern int add(int a, int b);//★★★导入函数add
static int __init hello_init(void)//驱动入口函数
{
static int sum;
printk("num = %d\n", num);//打印num值
sum = add(3, 4);//使用add函数进行3+4的运算
printk("sum = %d\n", sum);//打印add函数的运算值
return 0;
}
static void __exit hello_exit(void)//驱动出口函数
{
printk("Goodbye hello module\n");
}
module_init(hello_init);//注册入口函数
module_exit(hello_exit);//注册出口函数
MODULE_LICENSE("GPL");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息
3.执行结果
console:/storage/emulated/0/Download # insmod mathmodule.ko
[ 1561.278299] math_moudle init
console:/storage/emulated/0/Download # insmod hello.ko
[ 1578.781695] num = 10
[ 1578.781759] sum = 7
第二部分 驱动原理
一、内核驱动加载
04_Linux_Drivers/03_mathmodule/hello.c
module_init(hello_init);//注册入口函数
module_exit(hello_exit);//注册出口函数
kernel/include/linux/module.h
#ifndef MODULE
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
#else /* MODULE */
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define core_initcall_sync(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define postcore_initcall_sync(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define subsys_initcall_sync(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define fs_initcall_sync(fn) module_init(fn)
#define rootfs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define device_initcall_sync(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define late_initcall_sync(fn) module_init(fn)
#define console_initcall(fn) module_init(fn)
#define security_initcall(fn) module_init(fn)
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
MODULE的决定值:
kernel/Makefile
KBUILD_AFLAGS_KERNEL :=
KBUILD_CFLAGS_KERNEL := # ★编译进内核
KBUILD_AFLAGS_MODULE := -DMODULE
KBUILD_CFLAGS_MODULE := -DMODULE # ★编译成模块
kernel/include/linux/init.h
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn; # ★
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id) # ★
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6) # ★
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
#define __initcall(fn) device_initcall(fn) # ★
调用关系图
最终为一个函数指针:
typedef int (*initcall_t)(void);
所以,当使用 module_init(helloworld)宏定义模块的入口函数后,会创建一个 __initcall_hello_world6 函数指针变量,并将其初始化为 hello_world 函数,这个__initcall_hello_world6 函数指针变量的目的是将模块的入口函数放置在内核的初始化调用链中,以便在系统引导期间自动执行。
kernel/include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
__initcall##level##s_start = .; \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
__initcall_end = .;
INIT_CALLS_LEVEL(level) 宏用于定义特定优先级(level)的初始化调用函数的布局。它会
创建以下两个符号:
__initcall[level]_start:表示该优先级初始化调用函数段的起始位置。
__initcall[level]s_start:表示该优先级初始化调用函数段的起始位置(用于静态初始化)。
接着,INIT_CALLS 宏用于定义整个初始化调用函数的布局。它按照一定的顺序将不同优先
级的初始化调用函数放置在链接器脚本的相应位置。具体的步骤如下:
1.定义 __initcall_start 符号,表示初始化调用函数段的起始位置。
2.使用 KEEP 命令保留所有.initcallearly.init 段中的内容。这个段包含了一些早期的初始化调用函数,它们会在其他优先级之前被调用。
3.依次调用 INIT_CALLS_LEVEL 宏,传入不同的优先级参数,将相应优先级的初始化调用函数放置在链接器脚本中的正确位置。
4.定义 __initcall_end 符号,表示初始化调用函数段的结束位置。链接器在链接过程中会根据这些符号的位置信息,将初始化调用函数按照优先级顺序放置在对应的段中。这样,当系统启动时,初始化调用函数将按照定义的顺序被调用,实现系统的
初始化和功能注册。
展开之后的 INIT_CALLS 宏内容如下所示:
__initcall_start = .;
KEEP(*(.initcallearly.init))
__initcall0_start = .;
KEEP(*(.initcall0.init))
__initcall0s_start = .;
KEEP(*(.initcall0s.init))
.....................
__initcall7_start = .;
KEEP(*(.initcall7.init))
__initcall7s_start = .;
KEEP(*(.initcall7s.init))
__initcall_end = .;
__initcall0_start 等以_start 结尾的相关变量记录了.initcall0.init 等段的首地址,这些变量在init/main.c 中通过 extern 关键字进行引用,并将这些首地址放置在数组 initcall_levels 中,具体内容如下所示:
kernel/init/main.c
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
"pure",
"core",
"postcore",
"arch",
"subsys",
"fs",
"device",
"late",
};
initcall加载过程
kernel/init/main.c
static void __init do_initcall_level(int level)
{
initcall_entry_t *fn;
strcpy(initcall_command_line, saved_command_line);
parse_args(initcall_level_names[level],
initcall_command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, &repair_env_string);
trace_initcall_level(initcall_level_names[level]);
#ifdef CONFIG_INITCALL_ASYNC
if (initcall_nr_workers)
if (do_initcall_level_threaded(level) == 0)
return;
#endif
// ★★★
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
#ifdef CONFIG_INITCALL_ASYNC
initcall_init_workers();
#endif
// ★★★
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
#ifdef CONFIG_INITCALL_ASYNC
initcall_free_works();
#endif
}
static void __init do_basic_setup(void)
{
cpuset_init_smp();
shmem_init();
driver_init();
init_irq_proc();
do_ctors();
usermodehelper_enable();
do_initcalls(); //★★★
}
static noinline void __init kernel_init_freeable(void)
{
...
do_basic_setup();
...
}
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
...
}
static noinline void __ref rest_init(void)
{
...
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
...
}
asmlinkage __visible void __init start_kernel(void)
{
...
/* Do the rest non-__init'ed, we're now alive */
rest_init(); //★★★
prevent_tail_call_optimization();
}
加载流程图
二、内核驱动优先级
驱动A 优先加载
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void) //驱动入口函数
{
printk(KERN_EMERG "This is helloworld A\r\n");//注意:内核打印用 printk 而不是 printf
return 0;
}
static void __exit helloworld_exit(void) //驱动出口函数
{
printk(KERN_EMERG "helloworld_exit\r\n");
}
arch_initcall(helloworld_init); //★★★★注册入口函数
module_exit(helloworld_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_AUTHOR("topeet"); //作者信息
驱动B 正常加载
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void) //驱动入口函数
{
printk(KERN_EMERG "This is helloworld B\r\n");//注意:内核打印用 printk 而不是 printf
return 0;
}
static void __exit helloworld_exit(void) //驱动出口函数
{
printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init); //注册入口函数
module_exit(helloworld_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_AUTHOR("topeet"); //作者信息
三、驱动使用Makefile宏
kernel/include/linux/module.h
#ifndef MODULE
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
#else /* MODULE */
..........
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
..........
#endif
MODULE的决定值:
kernel/Makefile
KBUILD_AFLAGS_KERNEL :=
KBUILD_CFLAGS_KERNEL := # ★编译进内核
KBUILD_AFLAGS_MODULE := -DMODULE
KBUILD_CFLAGS_MODULE := -DMODULE # ★编译成模块
在 Makefile 中 KBUILD_CFLAGS_MODULE 和 EXTRA_CFLAGS 都是用于指定编译内核模块时的
编译选项的变量,下面是两个变量的详细介绍。
( 1 ) KBUILD_CFLAGS_MODULE :
KBUILD_CFLAGS_MODULE 是一个 makefile 变量,用于指定编译内核模块时的编译选项。在makefile 中,可以使用 KBUILD_CFLAGS_MODULE 变量来添加特定于模块的编译选项。通常,通过在 makefile 中使用 KBUILD_CFLAGS_MODULE 变量,可以将特定于模块的编译选项添加到模块的编译命令中。例如,可以使用 KBUILD_CFLAGS_MODULE 添加宏定义、优化选项、警告选项等。
在给 KBUILD_CFLAGS_MODULE 赋值时,可以使用+=操作符来追加编译选项,以确保不覆盖已有的编译选项。
例如,KBUILD_CFLAGS_MODULE += -DDEBUG 表示将-DDEBUG 编译选项添加到模块的编译命令中,定义了一个名为 DEBUG 的宏。
( 2 ) EXTRA_CFLAGS :
EXTRA_CFLAGS 也是一个 makefile 变量,用于指定额外的编译选项。与 BUILD_CFLAGS_MO
DULE 类似,可以使用 EXTRA_CFLAGS 变量来添加编译选项。
同样地,在给 EXTRA_CFLAGS 赋值时,也可以使用+=操作符来追加编译选项,以确保不覆
盖已有的编译选项。
例如,EXTRA_CFLAGS += -DDEBUG 表示将-DDEBUG 编译选项添加到全局的编译命令中,定
义了一个名为 DEBUG 的宏。
注:两个变量的最终效果是相同的,在目前的内核中主要使用 KBUILD_CFLAGS_MODULE
变量的方式,EXTRA_CFLAGS 已经弃之不用了,但仍旧支持这种方法。
04_Linux_Drivers/50_define/define.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void) //驱动入口函数
{
#ifndef DEBUG
printk("helloworld a!\n");
#else
printk("helloworld b!\n");
#endif
return 0;
}
static void __exit helloworld_exit(void) //驱动出口函数
{
printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init); //注册入口函数
module_exit(helloworld_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_AUTHOR("topeet"); //作者信息
04_Linux_Drivers/50_define/Makefile
# ★★,EXTRA_CFLAGS += 将-DDEBUG 编译选项添加到全局的编译命令中,定义了一个名为 DEBUG 的宏。
# KBUILD_CFLAGS_MODULE += -DDEBUG
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=/work/topeet/rk3568/rk_android11.0_sdk/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-#交叉编译器前缀
obj-m += define.o #helloworld.c对应.o文件的名称。名称要保持一致。
KDIR :=/work/topeet/rk3568/rk_android11.0_sdk/kernel #内核源码所在虚拟机ubuntu的实际路径
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
四、驱动模块的加载(insmod)
运行ko文件用到insmod命令
busybox-1.34.1/modutils/insmod.c
int insmod_main(int argc UNUSED_PARAM, char **argv)
{
char *filename;
int rc;
IF_FEATURE_2_4_MODULES(
getopt32(argv, INSMOD_OPTS INSMOD_ARGS);
argv += optind - 1;
);
filename = *++argv;
if (!filename)
bb_show_usage();
//****
rc = bb_init_module(filename, parse_cmdline_module_options(argv, 0));
if (rc)
bb_error_msg("can't insert '%s': %s", filename, moderror(rc));
return rc;
}
busybox-1.34.1/modutils/modutils.c
//★★系统调用
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
#if defined(__NR_finit_module)
//★★系统调用
# define finit_module(fd, uargs, flags) syscall(__NR_finit_module, fd, uargs, flags)
#endif
#define delete_module(mod, flags) syscall(__NR_delete_module, mod, flags)
int FAST_FUNC bb_init_module(const char *filename, const char *options)
{
size_t image_size;
char *image;
int rc;
bool mmaped;
if (!options)
options = "";
//TODO: audit bb_init_module_24 to match error code convention
#if ENABLE_FEATURE_2_4_MODULES
if (get_linux_version_code() < KERNEL_VERSION(2,6,0))
return bb_init_module_24(filename, options);
#endif
# ifdef __NR_finit_module
{
//★★第一种方法(有限):通过句柄fd
int fd = open(filename, O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
rc = finit_module(fd, options, 0) != 0;//★★系统调用
close(fd);
if (rc == 0)
return rc;
}
}
# endif
//★★第二种方法:获取ko文件的地址,并且加载到内存
image_size = INT_MAX - 4095;
mmaped = 0;
image = try_to_mmap_module(filename, &image_size);//映射内存
if (image) {
mmaped = 1;
} else {
errno = ENOMEM;
image = xmalloc_open_zipped_read_close(filename, &image_size);
if (!image)
return -errno;
}
errno = 0;
init_module(image, image_size, options);//★★系统调用
rc = errno;
if (mmaped)
munmap(image, image_size);
else
free(image);
return rc;
}
加载模块代码编辑(句柄方式)
04_Linux_Drivers/51_module/02_module/helloworld.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init helloworld_init(void) //驱动入口函数
{
dump_stack();
return 0;
}
static void __exit helloworld_exit(void) //驱动出口函数
{
printk(KERN_EMERG "helloworld_exit\r\n");
}
module_init(helloworld_init); //注册入口函数
module_exit(helloworld_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意 GPL 开源协议
MODULE_AUTHOR("auther"); //作者信息
执行结果
console:/storage/emulated/0/Download # insmod helloworld.ko
[12358.531219] type=1400 audit(1699191169.584:108): avc: denied { module_load } for comm="insmod" path="/storage/emulated/0/Dconsole:/storage/emulated/0/Downlooad # wnload/helloworld.ko" dev="fuse" ino=3469 scontext=u:r:su:s0 tcontext=u:object_r:fuse:s0 tclass=system permissive=1
[12358.532939] CPU: 2 PID: 1986 Comm: insmod Tainted: G O 4.19.193 #13
[12358.532979] Hardware name: Rockchip RK3568 EVB1 DDR4 V10 Board (DT)
[12358.532992] Call trace:
[12358.533027] dump_backtrace+0x0/0x158
[12358.533040] show_stack+0x14/0x1c
[12358.533056] dump_stack+0xb8/0xf0
[12358.533071] helloworld_init+0xc/0x1000 [helloworld]
[12358.533085] do_one_initcall+0x110/0x26c
[12358.533101] do_init_module+0x5c/0x1f8
[12358.533111] load_module+0x2dcc/0x358c
[12358.533122] __arm64_sys_finit_module+0xb0/0xe0
[12358.533135] el0_svc_common+0x98/0x160
[12358.533147] el0_svc_handler+0x5c/0x64
[12358.533156] el0_svc+0x8/0xc
测试完成记得rmmod,不然无法进行下一步
04_Linux_Drivers/51_module/03_app/app.c
#include <stdio.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
#define finit_module(fd, uargs, flags) syscall(__NR_finit_module, fd, uargs, flags)
int main(int argc, char **argv)
{
int fd; // 文件描述符
int ret; // 返回值
// 打开文件,以只读方式打开并设置O_CLOEXEC标志
fd = open(argv[1], O_RDONLY | O_CLOEXEC);
if (fd < 0) { // 打开文件失败
printf("open error\n");
return -1;
}
// 调用finit_module系统调用加载模块
ret = finit_module(fd, "", 0);
return ret; // 返回加载结果
}
加载模块代码编辑(内存映射方式)
04_Linux_Drivers/52_module_02/02_app/myinsmod.c
#include <stdio.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
int main(int argc, char **argv)
{
int fd; // 文件描述符
int ret; // 返回值
struct stat statbuf; // 存储文件信息的结构体
size_t image_size; // 文件大小
char *image; // 文件内容缓冲区
fd = open(argv[1], O_RDONLY | __O_CLOEXEC); // 以只读方式打开文件
if (fd < 0)
{
printf("open error \n"); // 打开文件失败
return -1;
}
fstat(fd, &statbuf); // 获取文件信息
image_size = statbuf.st_size; // 获取文件大小
image = malloc(image_size); // 为文件内容分配内存空间
read(fd, image, image_size); // 读取文件内容到缓冲区
ret = init_module(image, image_size, ""); // 调用系统调用初始化内核模块
if (ret < 0)
{
printf("error \n"); // 内核模块初始化失败
}
else
{
printf("ok \n"); // 内核模块初始化成功
}
free(image); // 释放内存空间
return ret; // 返回结果
}
五、驱动模块的加载(系统调用)
上层执行
//★★内存映射方式insmod
init_module(image, image_size, "");
//★★fd句柄方式insmod
finit_module(fd, "", 0);
init_module原型
#include "modutils.h"
#include <sys/syscall.h>
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
#if defined(__NR_finit_module)
# define finit_module(fd, uargs, flags) syscall(__NR_finit_module, fd, uargs, flags)
#endif
#define delete_module(mod, flags) syscall(__NR_delete_module, mod, flags)
-- module_image:指向内核模块的内存映像的指针。
-- len:内核模块的大小(以字节为单位)。
-- param_values:用于传递内核模块参数的字符串。
返回值:成功加载并初始化内核模块时,返回 0。加载和初始化内核模块失败时,返回负数值,表示
错误代码。syscall 函数是一个系统调用的包装函数,用于在 C/C++程序中调用系统调用。
每个系统有一个唯一的系统调用号来标识对应的函数。它的原型如下:
long syscall(long number, ...);
参数说明:
-- number:系统调用的编号。不同的系统调用有不同的编号,可以在系统调用的文档或头文件中找
到相应的编号。
-- ...:可变参数,用于传递系统调用的参数。具体的参数个数和类型取决于不同的系统调用。
返回值:系统调用执行成功时,返回系统调用的结果或返回值。系统调用执行失败时,返回负数值,
表示错误代码。错误代码的具体含义可以在系统调用的文档或头文件中找到相应的定义。
系统调用号与内核函数绑定
kernel/include/uapi/asm-generic/unistd.h
/* kernel/module.c */
#define __NR_init_module 105
__SYSCALL(__NR_init_module, sys_init_module)
#define __NR_delete_module 106
__SYSCALL(__NR_delete_module, sys_delete_module)
kernel/kernel/module.c
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
kernel/include/linux/syscalls.h
#ifndef SYSCALL_DEFINE0
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void); \
ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \
asmlinkage long sys_##sname(void)
#endif /* SYSCALL_DEFINE0 */
// 1~6个函数参数
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE_MAXARGS 6
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
__diag_push(); \
__diag_ignore(GCC, 8, "-Wattribute-alias", \
"Type aliasing is used to sanitize syscall arguments");\
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(__se_sys##name)))); \
ALLOW_ERROR_INJECTION(sys##name, ERRNO); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
__diag_pop(); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */
向系统中添加一个系统调用
- 在内核源码中添加自己的服务,需要编译进内核
可以放到kernel的任意目录下
新增
kernel/drivers/char/helloworld/helloworld.c
#include <linux/kernel.h>
#include <linux/syscalls.h>
SYSCALL_DEFINE0(helloworld) {
printk("This is helloworld syscall\n");
return 0;
}
- 添加系统调用号
在kernel/include/uapi/asm-generic/unistd.h修改
(+)#define __NR_helloworld 435
(+)__SYSCALL(__NR_helloworld , sys_helloworld)
(-)#define __NR_syscalls 435
(+)#define __NR_syscalls 436
- 编译并烧写内核到开发板
./build.sh kernel
- 应用程序
04_Linux_Drivers/53_syscall/syscall.c
#include <stdio.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#define __NR_helloworld 435
int main(int argc,char **argv){
syscall(__NR_helloworld);
return 0;
}
- 应用程序
..../aarch64-linux-gnu-gcc syscall.c -o syscall
内核运行 ko 流程分析
当我们使用系统调用时,内核会执行 SYSCALL_DEFINE3 宏定义的函数,如
SYSCALL_DEFINE3(finit_module,int, fd, const char __user *, uargs, int, flags)
或
SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long, len,const char __user *,uargs)。这些函数最终都会调用 load_module 函数,
而在 load_module函数中,会通过do_one_initcall(mod->init)来执行驱动程序的入口函数。
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
struct module *mod;
long err = 0;
char *after_dashes;
err = elf_header_check(info);
if (err)
goto free_copy;
err = setup_load_info(info, flags);
if (err)
goto free_copy;
if (blacklisted(info->name)) {
err = -EPERM;
goto free_copy;
}
err = module_sig_check(info, flags);
if (err)
goto free_copy;
err = rewrite_section_headers(info, flags);
if (err)
goto free_copy;
/* Check module struct version now, before we try to use module. */
if (!check_modstruct_version(info, info->mod)) {
err = -ENOEXEC;
goto free_copy;
}
/* Figure out module layout, and allocate all the memory. */
mod = layout_and_allocate(info, flags);
if (IS_ERR(mod)) {
err = PTR_ERR(mod);
goto free_copy;
}
audit_log_kern_module(mod->name);
/* Reserve our place in the list. */
err = add_unformed_module(mod);
if (err)
goto free_module;
#ifdef CONFIG_MODULE_SIG
mod->sig_ok = info->sig_ok;
if (!mod->sig_ok) {
pr_notice_once("%s: module verification failed: signature "
"and/or required key missing - tainting "
"kernel\n", mod->name);
add_taint_module(mod, TAINT_UNSIGNED_MODULE, LOCKDEP_STILL_OK);
}
#endif
/* To avoid stressing percpu allocator, do this once we're unique. */
err = percpu_modalloc(mod, info);
if (err)
goto unlink_mod;
/* Now module is in final location, initialize linked lists, etc. */
err = module_unload_init(mod);
if (err)
goto unlink_mod;
init_param_lock(mod);
/* Now we've got everything in the final locations, we can
* find optional sections. */
err = find_module_sections(mod, info);
if (err)
goto free_unload;
err = check_module_license_and_versions(mod);
if (err)
goto free_unload;
/* Set up MODINFO_ATTR fields */
setup_modinfo(mod, info);
/* Fix up syms, so that st_value is a pointer to location. */
err = simplify_symbols(mod, info);
if (err < 0)
goto free_modinfo;
err = apply_relocations(mod, info);
if (err < 0)
goto free_modinfo;
err = post_relocation(mod, info);
if (err < 0)
goto free_modinfo;
flush_module_icache(mod);
/* Now copy in args */
mod->args = strndup_user(uargs, ~0UL >> 1);
if (IS_ERR(mod->args)) {
err = PTR_ERR(mod->args);
goto free_arch_cleanup;
}
dynamic_debug_setup(mod, info->debug, info->num_debug);
/* Ftrace init must be called in the MODULE_STATE_UNFORMED state */
ftrace_module_init(mod);
/* Finally it's fully formed, ready to start executing. */
err = complete_formation(mod, info);
if (err)
goto ddebug_cleanup;
err = prepare_coming_module(mod);
if (err)
goto bug_cleanup;
/* Module is ready to execute: parsing args may do that. */
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
-32768, 32767, mod,
unknown_module_param_cb);
if (IS_ERR(after_dashes)) {
err = PTR_ERR(after_dashes);
goto coming_cleanup;
} else if (after_dashes) {
pr_warn("%s: parameters '%s' after `--' ignored\n",
mod->name, after_dashes);
}
/* Link in to sysfs. */
err = mod_sysfs_setup(mod, info, mod->kp, mod->num_kp);
if (err < 0)
goto coming_cleanup;
if (is_livepatch_module(mod)) {
err = copy_module_elf(mod, info);
if (err < 0)
goto sysfs_cleanup;
}
/* Get rid of temporary copy. */
free_copy(info);
/* Done! */
trace_module_load(mod);
return do_init_module(mod);
sysfs_cleanup:
mod_sysfs_teardown(mod);
coming_cleanup:
mod->state = MODULE_STATE_GOING;
destroy_params(mod->kp, mod->num_kp);
blocking_notifier_call_chain(&module_notify_list,
MODULE_STATE_GOING, mod);
klp_module_going(mod);
bug_cleanup:
mod->state = MODULE_STATE_GOING;
/* module_bug_cleanup needs module_mutex protection */
mutex_lock(&module_mutex);
module_bug_cleanup(mod);
mutex_unlock(&module_mutex);
/* we can't deallocate the module until we clear memory protection */
module_disable_ro(mod);
module_disable_nx(mod);
ddebug_cleanup:
ftrace_release_mod(mod);
dynamic_debug_remove(mod, info->debug);
synchronize_sched();
kfree(mod->args);
free_arch_cleanup:
module_arch_cleanup(mod);
free_modinfo:
free_modinfo(mod);
free_unload:
module_unload_free(mod);
unlink_mod:
mutex_lock(&module_mutex);
/* Unlink carefully: kallsyms could be walking list. */
list_del_rcu(&mod->list);
mod_tree_remove(mod);
wake_up_all(&module_wq);
/* Wait for RCU-sched synchronizing before releasing mod->list. */
synchronize_sched();
mutex_unlock(&module_mutex);
free_module:
/* Free lock-classes; relies on the preceding sync_rcu() */
lockdep_free_key_range(mod->core_layout.base, mod->core_layout.size);
module_deallocate(mod, info);
free_copy:
free_copy(info);
return err;
}
...
...
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
do_one_initcall 函数的作用是执行一个初始化调用函数,并检查执行过程中是否存在抢占
不平衡或中断被禁用的情况。如果存在错误信息,会打印警告信息。最后,返回初始化调用函
数的返回值。这个函数通常在内核初始化过程中使用,用于执行各个模块的初始化函数。
kernel/include/linux/module.h
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
上述代码是用于定义模块的初始化函数和清理函数的宏定义。通过使用 module_init 宏和
module_exit宏,开发人员可以指定模块的入口函数和清理函数。这些宏定义了一些内联函数
和与之关联的特殊命名的函数,以及一些属性和别名,用于在编译时进行检查并将用户定义的
函数与特定的模块函数关联起来。
在上图的代码中,
int init_module(void) __copy(initfn) attribute((alias(#initfn)));:定义一个名为 init_module 的函数,用于作为模块的入口函数。
__copy(initfn)表示复制用户定义的初始化函数,attribute((alias(#initfn)))表示将 init_module 函数与用户定义的初始化函数 initfn 关联起来。
将 init_module 作为函数 initfn 的别名。init_module 是驱动加载函数的统一别名,当我们编译 ko 文件的时候,会生成一个.mod.c 的文件。
总结一下,要加载内核模块,通常需要使用 insmod 命令或类似工具触发系统调用,然后 在内核中执行相应的系统调用函数(如sys_finit_module 或 sys_init_module)来加载模块。在 加载过程中,会调用 load_module函数,进而执行模块的初始化函数。这些过程构成了加载内 核模块的流程。
第三部分 驱动框架
一、字符设备
1.代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#define GPIO_DR 0xFDD60000
struct device_test{
dev_t dev_num; //设备号
int major ; //主设备号
int minor ; //次设备号
struct cdev cdev_test; // cdev
struct class *class; //类
struct device *device; //设备
char kbuf[32];
unsigned int *vir_gpio_dr;
};
struct device_test dev1;
static int cdev_test_open(struct inode *inode, struct file *file)
{
file->private_data=container_of(inode->i_cdev, struct device_test, cdev_test);
printk("This is cdev_test_open\r\n");
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
if (copy_from_user(test_dev->kbuf, buf, size) != 0) {
printk("copy_from_user error\r\n");
return -1;
}
if (test_dev->kbuf[0] == 1) {
*(test_dev->vir_gpio_dr) = 0x8000c040;
printk("test_dev->kbuf [0] is %d\n",test_dev->kbuf[0]);
} else if (test_dev->kbuf[0]==0){
*(test_dev->vir_gpio_dr) = 0x80004040;
printk("test_dev->kbuf [0] is %d\n",test_dev->kbuf[0]);
}
return 0;
}
/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) {
printk("copy_to_user error\r\n");
return -1;
}
printk("This is cdev_test_read\r\n");
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release\r\n");
return 0;
}
/*设备操作函数*/
struct file_operations cdev_test_fops = {
.owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = cdev_test_open, //将open字段指向chrdev_open(...)函数
.read = cdev_test_read, //将open字段指向chrdev_read(...)函数
.write = cdev_test_write, //将open字段指向chrdev_write(...)函数
.release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};
static int __init chr_fops_init(void) //驱动入口函数
{
int ret;
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name");
if (ret < 0) {
goto err_chrdev;
}
printk("alloc_chrdev_region is ok\n");
dev1.major = MAJOR(dev1.dev_num); //获取主设备号
dev1.minor = MINOR(dev1.dev_num); //获取次设备号
printk("major is %d \r\n", dev1.major); //打印主设备号
printk("minor is %d \r\n", dev1.minor); //打印次设备号
dev1.cdev_test.owner = THIS_MODULE;
cdev_init(&dev1.cdev_test, &cdev_test_fops);
ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
if(ret<0) {
goto err_chr_add;
}
dev1. class = class_create(THIS_MODULE, "test");
if(IS_ERR(dev1.class)) {
ret=PTR_ERR(dev1.class);
goto err_class_create;
}
dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
if(IS_ERR(dev1.device)) {
ret=PTR_ERR(dev1.device);
goto err_device_create;
}
dev1.vir_gpio_dr=ioremap(GPIO_DR,4);
if(IS_ERR(dev1.vir_gpio_dr)) {
ret=PTR_ERR(dev1.vir_gpio_dr);
goto err_ioremap;
}
return 0;
err_ioremap:
iounmap(dev1.vir_gpio_dr);
err_device_create:
class_destroy(dev1.class);
err_class_create:
cdev_del(&dev1.cdev_test);
err_chr_add:
unregister_chrdev_region(dev1.dev_num, 1);
err_chrdev:
return ret;
}
static void __exit chr_fops_exit(void) //驱动出口函数
{
/*注销字符设备*/
unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
cdev_del(&dev1.cdev_test); //删除cdev
device_destroy(dev1.class, dev1.dev_num); //删除设备
class_destroy(dev1.class); //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
2.驱动入口私有数据
struct device_test
{
dev_t dev_num; //设备号
int major; //主设备号
int minor; //次设备号
struct cdev cdev_test; // cdev
struct class *class; //类
struct device *device; //设备
char kbuf[32];
};
// 分配设备号
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name");
dev1.major = MAJOR(dev1.dev_num); //获取主设备号
dev1.minor = MINOR(dev1.dev_num); //获取次设备号
printk("major is %d \r\n", dev1.major); //打印主设备号
printk("minor is %d \r\n", dev1.minor); //打印次设备号
// 初始化新增设备
dev1.cdev_test.owner = THIS_MODULE;
cdev_init(&dev1.cdev_test, &cdev_test_fops);
ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
// 新增设备节点
dev1. class = class_create(THIS_MODULE, "test");
dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
3.container_of设置私有数据
file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);
函数原型:
container_of(ptr,type,member)
函数作用:
通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址。
参数含义:
ptr 是结构体变量中某个成员的地址。
type 是结构体的类型
member 是该结构体变量的具体名字
4.file->private_data获取私有数据
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev = (struct device_test *)file->private_data;
...
if (copy_from_user(test_dev->kbuf, buf, size) != 0) {
printk("copy_from_user error\r\n");
return -1;
}
...
return 0;
}
5.copy_from_user内核用户空间数据交换
/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev = (struct device_test *)file->private_data;
if (test_dev->minor == 0) {
//如果次设备号是0,则为dev1
if (copy_from_user(test_dev->kbuf, buf, size) != 0) {
printk("copy_from_user error\r\n");
return -1;
}
printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
} else if (test_dev->minor == 1) {
//如果次设备号是1,则为dev2
if (copy_from_user(test_dev->kbuf, buf, size) != 0) {
printk("copy_from_user error\r\n");
return -1;
}
printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);
}
return 0;
}
/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
//获取私有数据
struct device_test *test_dev = (struct device_test *)file->private_data;
if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) {
printk("copy_to_user error\r\n");
return -1;
}
printk("This is cdev_test_read\r\n");
return 0;
}
6.寄存器相关
//映射
dev1.vir_gpio_dr=ioremap(GPIO_DR,4);
//解除映射
iounmap(dev1.vir_gpio_dr);
//点亮
*(test_dev->vir_gpio_dr) = 0x8000c040;
//灭灯
*(test_dev->vir_gpio_dr) = 0x80004040;
7.错误处理
static int __init chr_fops_init(void) //驱动入口函数
{
ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
if(ret<0) {
goto err_chr_add;
}
...
dev1. class = class_create(THIS_MODULE, "test");
if(IS_ERR(dev1.class)) {
ret=PTR_ERR(dev1.class);
goto err_class_create;
}
return 0;
```
err_class_create:
cdev_del(&dev1.cdev_test); //删除 cdev
err_chr_add:
unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
...
}
dev1. class = class_create(THIS_MODULE, "test");
返回一个指针
- 合法
- 不合法
- NULL
检查错误
IS_ERR(dev1.class)检查是否在0xfffffffffffff000~0xffffffffffffffff以内
PTR_ERR(dev1.class);对应错误码错误码:include/uapi/asm-generic/errno-base.h
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
...
8.APP操作
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int fd;
char buf[32] = {0};
fd = open("/dev/test", O_RDWR); //打开led驱动
if (fd < 0) {
perror("open error \n");
return fd;
}
// atoi()将字符串转为整型,这里将第一个参数转化为整型后,存放在 buf[0]中
buf[0] =atoi(argv[1]);
//向/dev/test文件写入数据
write(fd,buf,sizeof(buf));
//关闭文件
close(fd);
return 0;
}
二、杂项设备
1.简要说明
- 是字符设备的一种
- 主设备号固定为10
- 会自己创建设备节点
struct miscdevice {
int minor; /* 子设备号 需要用户填写*/
const char *name; /* 设备名 需要用户填写*/
const struct file_operations *fops; /* 设备操作集 需要用户填写*/
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
int misc_register(struct miscdevice *misc)
int misc_deregister(struct miscdevice *misc)
#include <linux/miscdevice.h>
2.动态申请次设备号
struct miscdevice misc_dev = { //杂项设备结构体
.minor = MISC_DYNAMIC_MINOR, //动态申请的次设备号
.name = "misc_test", //杂项设备名字是 hello_misc
.fops = &misc_fops, //文件操作集
};
第四部分 高级字符设备
一、定时器
#include <linux/timer.h>
extern u64 __cacheline_aligned_in_smp jiffies_64;
extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies;
static void function_test(struct timer_list *t)
DEFINE_TIMER(timer_test,function_test);
static void function_test(struct timer_list *t)
{
printk("this is function test \n");
mod_timer(&timer_test,jiffies_64 + msecs_to_jiffies(5000));
}
static int __init timer_mod_init(void) //驱动入口函数
{
timer_test.expires = jiffies_64 + msecs_to_jiffies(5000);
add_timer(&timer_test);
return 0;
}
static void __exit timer_mod_exit(void)
{
del_timer(&timer_test);
printk("module exit \n");
}
struct timer_list {
struct hlist_node entry;
unsigned long expires;/* 定时器超时时间,单位是节拍数 */
void (*function)(struct timer_list *);/* 定时处理函数 */
u32 flags;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
};
设置节拍数
-> Kernel Features
-> Timer frequency (<choice> [=y])
函数 | 作用 |
---|---|
void add_timer(struct timer_list *timer) | 向Linux内核注册定时器,使用add_timer函数向内核注册定时器以后,定时器就会开始运行 |
int del_timer(struct timer_list *timer) | 删除一个定时器 |
int mod_timer(struct timer_list *timer, unsigned long expires) | 修改定时值,如果定时器还没有激活的话,mod_timer函数会激活定时器 |
函数 | 作用 |
---|---|
int jiffies_to_msecs(const unsigned long j) | 将 jiffies 类型的参数 j 转换为对应的毫秒 |
int jiffies_to_usecs(const unsigned long j) | 将 jiffies 类型的参数 j 转换为对应的微秒 |
u64 jiffies_to_nsecs(const unsigned long j) | 将 jiffies 类型的参数 j 转换为对应的纳秒 |
long msecs_to_jiffies(const unsigned int m) | 将毫秒转换为 jiffies 类型 |
long usecs_to_jiffies(const unsigned int u) | 将微秒转换为 jiffies 类型 |
unsigned long nsecs_to_jiffies(u64 n) | 将纳秒转换为 jiffies 类型 |
二、秒字符设备
读出来的数据随每秒增加1
//module.ko
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/atomic.h>
struct device_test{
dev_t dev_num; //设备号
int major ; //主设备号
int minor ; //次设备号
struct cdev cdev_test; // cdev
struct class *class; //类
struct device *device; //设备
int sec; //秒
};
static void function_test(struct timer_list *t);
DEFINE_TIMER(timer_test,function_test);
static void function_test(struct timer_list *t)
{
atomic64_inc(&v);
dev1.sec = atomic_read(&v);
//printk("the sec is %d\n",dev1.sec);
mod_timer(&timer_test,jiffies_64 + msecs_to_jiffies(1000));
}
static int cdev_test_open(struct inode *inode, struct file *file)
{
file->private_data=&dev1;
add_timer(&timer_test);
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
if(copy_to_user(buf,&dev1.sec,sizeof(dev1.sec))){
printk("copy_to_user error \n");
return -1;
}
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
del_timer(&timer_test);//删除一个定时器
return 0;
}