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

相关内容

热门资讯

原创 快... 最近,网络上又流传出关于德云社的假消息。这次的焦点是所谓的德云社头牌徒弟,有消息称这位徒弟的合同即将...
每周股票复盘:大连热电(600... 截至2025年12月26日收盘,大连热电(600719)报收于6.22元,较上周的5.78元上涨7....
2026春夏十大流行色 “岁序更替,华章日新。” 当时间翻开崭新的篇章,色彩亦成为我们拥抱未来的无声语言。 今天让我们在...
胖东来集团公布销量数据,202... 2026年第一天,胖东来集团公布的销量数据显示, 截至2025年12月31日,2025年集团合计销售...
卖爆了!短短半月,货架已空 2025年以来,以黄金为代表的贵金属受到资本热捧。在美联储降息、央行购金热潮、ETF持仓增加以及避险...
每周股票复盘:苏农银行(603... 截至2025年12月26日收盘,苏农银行(603323)报收于5.1元,较上周的5.18元下跌1.5...
芜湖这16家门店,供应平价菜! 为保障2026年元旦、春节期间市场供应稳定,促进节日消费,芜湖市发改委发布通知,于元旦和春节期间统一...
声纹分析AI脑语引擎专业品牌探... 《中国老龄事业发展统计公报(2025)》显示,我国60岁及以上人口已突破2.64亿,占总人口18.7...
精准捕捉市场空白 于双碳赛道书... 编者按:当下,创业的热潮席卷三湘大地,“创业”无疑成为今年湖南备受瞩目的热词。为此,湖南红网新媒体集...
全球超大城市排名:东京滑落为全... 近日,共同社援引联合国经济和社会事务部2025年11月发布的报告,抛出了一组颠覆认知的全球城市人口数...
闪迪:股价年涨559%,26财... 【人工智能热潮带动闪迪股价飙升,营收增长显著】1月1日消息,人工智能热潮为存储芯片带来强劲需求。美国...
新年第一天,金饰克价还在跌 专家呼吁消费者理性看待短期金价波动和金饰营销话术。 1月1日,元旦假期首日,各大黄金珠宝品牌的克重金...
2025年最后一天,美联储“创... 美联储常备回购便利在2025年最后一个交易日的使用规模创下历史新高。纽约联储的常备回购便利(SRF)...
凯文教育等教育股:12 月 3... 【12月31日A股教育股走强,受教育部政策消息刺激】12月31日,A股市场教育股表现强劲。凯文教育涨...
海口机场2025年货邮吞吐量创... 中新网海口1月1日电(黄裕光 蔡莲珠)海口美兰国际机场(下称美兰机场)1日介绍,美兰机场2025年旅...
医疗AI时代来临,外科医生会被... 温馨提示:本文仅用于提供科普和专业信息,不能替代专业医生的诊断与治疗。建议患者根据自身情况咨询专业医...
又一家国产GPU企业,IPO辅... 每经编辑|张锦河 1月1日,证监会官网IPO辅导公示系统显示,上海燧原科技股份有限公司(简称“燧原...
崔传刚:科技为国而商,为需而兴 来源:经济学家圈 科技为国而商,为需而兴——2025年中国商业科技的发展与2026年展望 崔传刚 ...
原创 美... 想想看,2025年这个年头,美国人居然在自家论坛上纠结这么个问题,总觉得有点讽刺。明明全球经济风起云...
2025投行排行:中信卫冕,国... 文丨惠凯 编辑丨承承 2025年A股IPO市场,头部券商主导市场,半导体企业IPO成为投行业绩关键。...