Linux 驱动之内核相关基础知识学习
创始人
2025-06-01 03:58:02
0

知识图

Linux内核模块一.初识Linux设备驱动什么是Linux驱动并认识Linux源码二.编写第一个驱动helloworl最简单的Linux驱动结构三.如何编写驱动程序1.将驱动编译成内核模块1.Makefile文件的编写2.模块加载命令2.将驱动编译进内核1.menuconfig图像化配置界面2.Kconfig语法四.驱动模块传参1.传递基本类型参数2.传递数组类型参数3.传递字符串类型参数五.内核模块符号表1.模块间的相互依赖,A模块调用B模块中的函数

文章目录

    • 知识图
    • 认识Linux 设备驱动
      • 驱动的作用
      • 无操作系统时的驱动(裸机)
      • 有操作系统时的驱动(Linux系统)
      • 驱动的分类
      • Linux 源码下载
      • Linux 源码目录结构
    • 编写第一个驱动
      • 最简单的Linux驱动结构
    • 如何编译驱动程序
      • Linux内核模块的编译
        • 什么是Linux内核模块
        • 例子
        • Linux 内核模块命令
      • 将驱动编译在Linux内核里面
        • make menuconfig图形化配置界面
        • 与make menuconfig有关的文件
          • Makefile,config,Kconfig关系
          • Kconfig文件
          • config文件和.config文件
        • Kconfig语法
          • mainmenu
          • 配置选项
          • 依赖关系
          • 可选择项
          • 注释
          • source
    • 驱动模块传参
      • 驱动传参的意义
        • 驱动可以传递的参数类型
        • 如何给驱动传递参数
        • 参数的读写权限
        • 例子
    • 内核符号表
      • 符号表
      • 内核符号表导出
      • 例子

认识Linux 设备驱动

驱动的作用

驱动的作用
从字面上解释,驱动就是“让硬件动起来",所以驱动是直接和硬件打交道的,是底层硬件和上层软件的桥梁。

无操作系统时的驱动(裸机)

有的时候并不一定需要操作系统,比如用单片机进行简单的通断控制,从编程角度来说,直接控制寄存器就可以了,也就是直接和硬件打交道。

有操作系统时的驱动(Linux系统)

有了操作系统以后,编写驱动就变的比较复杂,要基于Linux的各种驱动框架进行编程。但是当驱动都按照系统给出的框架进行编程以后,就可以提供一个统一的接口给应用程序调用。

应用操作系统驱动硬件

驱动的分类

Linux将驱动分为三类。

字符设备:字符设备指那些必须以串行顺序依次进行访问的设备,如鼠标。

网络设备:块设备可以按照任意顺序进行访问,如硬盘。

块设备:网络设备是面向数据包的接收和发送。

Linux 源码下载

我们可以在https://www.kernel.org/下载到最新的Linux内核源码。历史版本可以在 https://www.kernel.org/pub/下载历史版本,一般我们使用的半导体产商提供的源码:NPX(恩智浦),MTK(联发科),ALLWINNER(全智),RK,TI(德州仪器)等等。

编译介绍

Linux 源码目录结构

Linux内核源码包含多级目录,形成一个巨大的树状结构,进入源码所在的目录,就是Linux源码的顶层目录。例举linux-5.15

目录说明
arch架构相关目录,里面存放了许多CPU的架构,如arm,X86,MIPs等。
block存放块设备相关代码,在Linux 中用block表示块设备。比如硬盘,SD卡,都是块设备
certs存储了认证 和签名 相关代码
crypto存放加密算法目录
Documentation存放官方Linux内核文档
drivers驱动目录,里面存放了Linux系统支持的硬件设备驱动源码
firmware存放固件目录
fs存放支持的文件系统的代码目录,比如fat,ext2,ext3 等。
include存放公共的头文件目录
init存放Linux内核启动初始化的代码
ipc存放进程间通信代码
kernel存放内核本身的代码文件夹
lib存放库函数的文件夹
mm存放内存管理的目录,mm就是memory management的缩写
net存放网络相关代码,比如TCP/IP协议栈
samples内核实列代码
scripts存放脚本的文件夹
security存放安全相关代码
sound存放音频相关代码
tools存放Linux用到的工具文件夹
usr和Linux内核的启动有关代码
virt内核虚拟机相关代码

编写第一个驱动

最简单的Linux驱动结构

一个最简单的Linux驱动主要由以下几个部分组成:
(1)头文件(必须有)
驱动需要包含内核相关头文件。必须包含

(2)驱动加载函数。(必须有)
当加载驱动的时候,驱动加载函数会自动被内核执行。

(3)驱动卸载函数(必须有)
当卸载驱动的时候,驱动卸载函数会自动被内核执行。

(4)许可证声明(必须有)
Linux内核是开源的,遵守GPL协议,驱动在加载的时候也要遵守相关的协议,可以接收的License有"GPL"、“GPL v2”、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MIT/GPL"、"Dual MPL/GPL"。内核驱动中最常见的是GPL v2

(5)模块参数(可选)
模块参数是模块被加载的时候传递给内核模块的值。
可以声明驱动的作者信息和代码的版本信息。

如何编译驱动程序

第一种编译方法:将驱动放在Linux内核里面,然后编译Linux内核。将驱动编译到Linux内核里面。
第二种编译方法:将驱动编译成内核模块,独立于Linux内核以外。

编写驱动程序编译进Linux内核编译成Linux内核模块

Linux内核模块的编译

什么是Linux内核模块

内核模块是Linux系统中一个特殊的机制,可以将一些使用频率很少或者暂时不用的功能编译成内核模块,在需要的时候在动态加载到内核里面。
使用内核模块可以减小内核的体积,加快启动速度。并且可以在系统运行的时候插入或者卸载驱动,无需重启系统。内核模块的后缀是.ko

Makefile解析:

# 表示把目标文件helloworld.o作为模块进行编译。obj就是object的缩写,-m表示编译成模块。
obj-m += helloworld.o# 使用绝对路径的方式指定内核源码的路径。
# KDIR:=/lib/modules/$(shell uname -r)/build 在ubuntu 编译,
# 在某版本的linux内核源码编译
KDIR:=/home/fengzc/study/linux-5.15/# 获取Makefile文件所在的路径
PWD?=$(shell pwd)# 进到KDIR目录,使用PWD路径下源码和Makefile文件编译驱动模块
make -C $(KDIR) M=$(PWD) modules# 清除编译文件
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order

例子

  1. 驱动源码helloworld.c

    #include 
    #include static int helloworld_init(void){printk("helloworld init\n");return 0;
    }static void helloworld_exit(void){printk("helloworld exit");
    }module_init(helloworld_init);
    module_exit(helloworld_exit);MODULE_LICENSE("GPL");
    MODULE_AUTHOR("FZC");
    MODULE_VERSION("V1.0");
    
  2. 在驱动源码目录下创建Makefile文件

    obj-m += helloworld.o
    KDIR:=/home/fengzc/study/linux-5.15/
    PWD?=$(shell pwd)
    all:make -C $(KDIR) M=$(PWD) modules
    clean:rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
    
  3. [Linux 源码下载](Linux 源码下载)的内核源码编译通过,内核不编译通过,无法编译内核模块

  4. 设置环境变量(看设置)

    #设置架构,x86,arm64等
    export ARCH=arm64
    export CROSS_COMPILE=xxxxxx
    
  5. 输入make编译,编译成功后驱动源码目录下,生成helloworld.ko

Linux 内核模块命令

模块加载命令

  • insmod命令

    功能:载入Linux内核模块

    语法:

    insmod 模块名

    举例:

    insmod helloworld.ko

  • modprobe命令
    功能:加载Linux内核模块,同时这个模块所依赖的模块也同时被加载语法

    语法:

    modprobe 模块名

    举例:

    modprobe helloworld.ko

模块卸载命令

  • rmmode 命令

    功能:移除已经载入Linux的内核模块

    语法:rmmod 模块名
    rmmod helloworld.ko

查看模块信息命令

  • lsmod命令

    功能:列出已经载入Linux的内核模块
    也可以使用命令cat /proc/modules来查看模块是否加载成功。

  • modinfo命令

    功能:查看内核模块信息

    语法:modinfo 模块名

    举例:
    modinfo helloworld.ko

将驱动编译在Linux内核里面

make menuconfig图形化配置界面

  1. 打开图形化配置界面
    使用命令export ARCH=arm设置平台架构,平台架构是armarm64,还是mips要根据实际开发板架构选择。然后在内核源码的顶层目录下,输入命令make menuconfig

  2. 常见错误

    • 如果提示错误"'make menuconfig' requires the ncurses libraries",请使用命令sudo apt-get install libncurses5-dev安装ncurses
    • 提示"Your display is too small to run Menuconfig!"这个错误是因为控制终端的窗口太小,放大窗口或者全屏操作即可。
    • 提示"make:*** No rule to make target 'menuconig'. Stop."这个错误是没有在内核源码的顶层目录输入make menuconfig
  3. 图形化配置界面操作

    • 移动

      使用键盘的上,下,左,右按键可以移动光标。

    • 搜索功能

      输入“/”即可弹出搜索界面,然后输入我们要搜索的内容即可

    • 配置驱动选项状态操作

      • 把驱动编译成模块,用M来表示
      • 驱动编译到内核里面,用*
      • 不编译

      使用“空格”按键来配置这三种不同的状态。
      选项的状态有[],<>,()三种表示状态,其中

      []表示有两种状态,只能设置成选中或者不选中,

      <>M表示有三种状态,可以设置成选中,不选中,和编译成模块。

      ()表示用来存放字符串或者16进制数。

与make menuconfig有关的文件

Makefile,config,Kconfig关系

用做菜类比他们之间的关系:

Makefile文件相当于菜的做法。

Kconfig文件相当于饭店的菜单

.config文件相当于我们使用饭店的菜单点完的菜品

Kconfig文件

Kconfig文件是图形化配置界面的的源文件,图形化配置界面中的选项由Kconfig文件决定。当我们执行命令make menuconfig命令的时候,内核的配置工具会读取内核源码目录下的arch/xxx/Kconfigxxx是命令export ARCH=arm中的ARCH的值。然后生成对应的配置界面供开发者使用。

config文件和.config文件

config文件和.config文件都是Linux内核的配置文件,config文件位于Linux内核源码的arch/$(ARCH)/configs目录下,是Linux系统默认的配置文件。.config文件位于Linux内核源码的顶层目录下,编译linux内核时会使用.config文件里面的配置来编译内核镜像。
.config存在,make menuconfig界面的默认配置即当前.config文件的配置,若修改了图形化配置界面的设置并保存,则.config文件会被更新
.config文件不存在,make menuconfig界面的默认配置则为Kconfig文件中的默认配置。使用命令make xxx_defconfig命令会根据arch/$(ARCH)/configs目录下默认文件生成.config文件。

.configmake menuconfigmake 编译内核

Kconfig语法

mainmenu

可以用menu,endmenu来生成菜单,menu是菜单开始的标志,endmenu是菜单结束的标志。这俩个是成对出现的。

menuconfig HELLOWORLD
endmenu

配置选项

使用关键字config来定义一个新的选项。每个选项都必须指定类型,类型包括bool,tristate,string,hex,int。最常见的是bool,tristate,string这三个。其中:

bool类型有俩种值:y和n

tristat有三种值:y、m和n

string:为字符串类型

help表示帮助信息,当我们在图形化界面按下h按键,弹出来的就是help的内容。

举例:

config HELLOWORLDbool "hello world support"default yhelphello world
依赖关系

Kconfig中的依赖关系可以用depends onselect

直接举例说明:
depends on表示直接依赖关系:

config Adepends on B
#表示选项A依赖选项B,只有当B选项被选中时,A选项才可以被选中。	

select表示反向依赖关系:

config Aselect B
#在A选项被选中的情况下,B选项自动被选中。
可选择项

使用choiceendchoice定义可选择项。

直接举例说明:

choicebool "a“
config bboot b1
config cboot c1
...
endchoice
注释

Kconfig中使用comment用于注释,不过此注释非彼注释,这个注释是在图形化界面中显示一行注释。
举例:

config TEST_CONFIGbool "test"default yhelpjust test
comment "just test"
source

source用于读取另一个Kconfig文件,如source "init/Kconfig"就是读取init目录下的Kconfig文件。

例子

1、/home/fengzc/study/linux-5.15/drivers/char创建helloworld文件夹

2、/home/fengzc/study/linux-5.15/drivers/char/helloworld创建Kconfig

config HELLOWORLDbool "hello world support"default yhelphello world

3、/home/fengzc/study/linux-5.15/drivers/char/Kconfig里增加

source "drivers/char/helloworld/Kconfig"

4、通过make menuconfig命令,查看Device Drivers ---> Character devices ---> [*] hello world support ,然后save保存退出

5、编写/home/fengzc/study/linux-5.15/drivers/char/helloworld驱动

helloworld.c

#include 
#include static int helloworld_init(void){printk("helloworld init\n");return 0;
}static void helloworld_exit(void){printk("helloworld exit");
}module_init(helloworld_init);
module_exit(helloworld_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");

6、编译写Makefile

obj-$(CONFIG_HELLOWORLD) += helloworld.o
KDIR:=/home/fengzc/study/linux-5.15/
PWD?=$(shell pwd)
all:make -C $(KDIR) M=$(PWD) modules
clean:rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order

7、/home/fengzc/study/linux-5.15/drivers/char/Makefile修改添加

obj-y                           += helloworld/

8、查看有没有编译Linux内核里(烧录镜像有没有helloworld日志打印)

驱动模块传参

驱动传参的意义

优势:

  1. 通过驱动传参,可以让驱动程序更加灵活。兼容性更强。
  2. 可以通过驱动传参,设置安全校验,防止驱动被盗用。

不足:

  1. 使驱动代码变得复杂化
  2. 增加了驱动的资源占用。

驱动可以传递的参数类型

c语言中常用的数据类型内核大部分都支持驱动传参。这里将内核支持的驱动传递参数的类型分成三类:

基本类型:char,bool,int,long,short,byte,ushort,uint。

数组:array

家符串:string

如何给驱动传递参数

驱动支持的参数类型有基本类型,数组,字符串。这三个类型分别对应函数:

module_param:传递基本类型函数

函数功能:传递基本类型参数给驱动

函数原型:module_param(name,type,perm)

函数参数:

name:要传递给驱动代码中的变量的名字。

type:参数类型。

perm:参数的读写权限。

module_param_array:传递数组类型函数

函数功能:传递数组类型参数给驱动

函数原型: module_param_array(name,type,nump,perm)

函数参数:

name:要传递给驱动代码中的变量的名字。

type:参数类型。

nump:数组的长度。

perm:参数的读写权限。

module_param_string:传递字符串类型函数

函数功能:传递字符串类型参数给驱动

函数原型:module_param(name,string,len,perm)

函数参数:

name:要传递给驱动代码中的变量的名字。

string:驱动程序中变量的名字,要和参数name的名字保持一致。

len:字符串的大小。

perm:参数的读写权限。

MODULE_PARM_DESC函数

函数功能:描述模块参数的信息。

include/linux/moduleparam.h

定义函数原型: MODULE_PARM_DESC(_parm, desc)

函数参数:

_parm:要描述的参数的参数名称。

desc:描述信息。

这三个函数在Linux内核源码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)#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")

参数的读写权限

读写权限在include/linux/stat.hinclude/uapi/linux/stat.h下有定义,一般使用S_IRUGO,也可以使用数字表示,如444表示S_IRUGO
include/uapi/linux/stat.h

# S_I不管,R:可读,W:可写,X:可执行,U、USR:用户所有者user,G、GRP:用户组,O,TH:其他人
#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

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)

例子

#include 
#include 
#include static int a = 0;
static int array[5] = {0};
static int array_size;
static char str1[10] = {}module_param(a, int ,S_IRUGO)
MODULE_PARAM_DESC(a, "e.g. a = 1");module_param_array(array, int, &array_size, S_IRUGO);
MODULE_PARAM_DESC(array, "e.g. array=1, 2, ");module_param_string(str, str1, sizeof(str1), S_IRUGO);
MODULE_PARAM_DESC(str, "e.g. str = hellow")static int helloworld_init(void){int i = 0;printk("a is %d\n, a");for(i = 0;i< array_size;i ++){printk("array[%d] is %d\n", i, }printk("str1 is %s\n", str1);printk("helloworld init\n");return 0;
}static void helloworld_exit(void){printk("helloworld exit");
}module_init(helloworld_init);
module_exit(helloworld_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");

编译成内核模块 ,通过命令insmod helloworld.ko a=1 array=1,2,3 str=hello

内核符号表

驱动程序可以编译成内核模块,也就是KO文件。每个KO文件是相互独立的,也就是说模块之间无法互相访问。但是在某些使用场景下要互相访问,如B模块要用A模块中的函数。此时符号表的概念就引入了

符号表

所谓“符号"就是内核中的函数名,全局变量名等。符号表就是用来记录这些“符号”的文件。

内核符号表导出

模块可以使用一下宏EXPORT_SYMBOLEXPORT_SYMBOL_GPL导出符号到内核符号表中。
例:

EXPORT_SYMBOL(符号名);

EXPORT_SYMBOL_GPL(符号名);//只适用于包含GPL许可的模块。

导出去的符号可以被其他模块使用。使用前只需要声明一下即可。

例子

a.c

#include 
#include extern int add(int a, int b);int add(int a, int b){return a + b;
}EXPORT_SYMBOL(add);static int helloA_init(void){printk("helloA init\n");return 0;
}static void helloA_exit(void){printk("helloA exit");
}module_init(helloA_init);
module_exit(helloA_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");

b.c

#include 
#include extern int add(int a, int b);static int helloB_init(void){printk("helloB init\n");int a = 0;a = add(1,2);printk("a value = %d", a);return 0;
}static void helloB_exit(void){printk("helloB exit");
}module_init(helloB_init);
module_exit(helloB_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");
  1. 编译a.c,得到a.koModule.symvers,然后将它复制b.c的同级目录
  2. 编译b.c得到b.ko
  3. 先加载insmod a.ko,然后insmod b.ko,这样b模块就可以使用a模块的函数了
  4. 卸载先rmmod b.ko,然后rmmod a.ko

相关内容

热门资讯

“强实名”仍一票难求?遏制技术... 暑期来临,演唱会、音乐节、话剧等演出活动热度飙升。无论手速多快,总是一票难求,让众多消费者叫苦不迭。...
上证红利回报指数上涨0.83%... 金融界7月21日消息,上证指数高开高走,上证红利回报指数 (上红回报,H50019)上涨0.83%,...
为啥股票与基金的走势相反? 虚位以待! 平姐姐摄于毛里求斯网红酒店 昨天的文章,标题就很明确,那就是《准备出击》,在半年报不少上...
美加密货币相关法案落地引发三连... 当地时间7月18日,美国总统特朗普在白宫正式签署《指导与建立美国稳定币国家创新法案》(简称《天才法案...
股市必读:湖南黄金(00215... 截至2025年7月21日收盘,湖南黄金(002155)报收于18.33元,上涨2.57%,换手率3....
四川发布六大红色旅游新线路 四川发布六大红色旅游新线路 “锦绣天府·安逸四川”之红色旅游央地媒体联动采访启动 “锦绣天府·安...
北交所上市公司中航泰达大宗交易... 每经讯,2025年7月21日,北交所上市公司中航泰达(836263,收盘价:16.11元)发生一笔大...
金价突然猛拉,重回3400美元... 记者丨叶麦穗 编辑丨曾芳 金珊 7月21日晚,现货黄金突然猛拉大涨,截至22:40,涨超1.5%,站...
嘉实港股互联网产业核心资产混合... AI基金嘉实港股互联网产业核心资产混合A(011924)披露2025年二季报,第二季度基金利润532...
中信证券:特朗普“唱白脸”+贝... 来源:市场资讯 中信证券研究 文|李翀 崔嵘 韦昕澄 贾天楚 当地时间7月14日,美国总统特朗普表示...
机器人ETF易方达(15953... 截至收盘,国证机器人产业指数上涨2.1%,中证装备产业指数上涨1.9%,中证军工指数上涨0.9%,中...
北京工商大学教授吕来明:整治“... 今年以来,治理“内卷式”竞争引发高度关注。从水泥、光伏、汽车到电商,多个领域吹响“反内卷”的号角。 ...
民航局:加快新兴市场布局,提升... 7月21日,在国新办举行的“高质量完成‘十四五’规划”系列主题新闻发布会上,中国民航局局长宋志勇介绍...
二季度券商北交所、新三板业务执... 头部券商优势地位保持稳固,多家中小券商排名大幅跃升 本报记者 于宏 7月18日晚间,北交所、全国股转...
股票行情快报:美新科技(301... 证券之星消息,截至2025年7月21日收盘,美新科技(301588)报收于19.0元,上涨1.39%...
破解中小企业融资难!产业数字金... “中国经济的核心在于产业经济,产业经济离不开金融,产业经济、产业金融都离不开数字技术的赋能,中小企业...
京东美团“暗战”具身智能,战火... 在科技赛道的激烈角逐中,具身智能正成为巨头们争夺的新焦点。王兴之后,刘强东也在具身智能领域强势出击。...
“未来已来”指数涨跌不一,关注... 截至收盘,国证机器人产业指数上涨2.1%,中证新能源指数上涨1.5%,中证人工智能主题指数下跌0.0...
“未来能源”指数上涨,关注新能... 截至收盘,中证上海环交所碳中和指数上涨1.9%,中证光伏产业指数上涨1.6%,中证新能源指数上涨1....