三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【Linux 驱动小白教程】四、模块符号导出 EXPORT_SYMBOL + 模块传参完整实操

【Linux 驱动小白教程】四、模块符号导出 EXPORT_SYMBOL + 模块传参完整实操

接上篇:内核 Kconfig & Makefile 配置编译全流程
前面我们学会了驱动模块编译、Kconfig 配置。本篇讲解两个内核模块高频知识点:模块符号导出 EXPORT_SYMBOL模块命令行传参。实际项目多模块联合开发、调试参数不用重新编译驱动全靠这两套 API,面试经常考。

一、模块符号导出 EXPORT_SYMBOL

1. 什么是符号导出

内核每个.ko模块有自己的函数、全局变量。
默认情况下:A 模块的函数,B 模块无法直接调用
EXPORT_SYMBOL()作用:把本模块的函数 / 全局变量符号导出到内核符号表,其他内核模块就可以调用这个函数。

区分两个宏

  1. EXPORT_SYMBOL(sym)
    导出符号,GPL 与非 GPL 模块都可以使用该符号

  2. EXPORT_SYMBOL_GPL(sym)
    只允许声明MODULE_LICENSE("GPL")的模块使用;非 GPL 模块调用会内核告警。项目开发优先使用 EXPORT_SYMBOL_GPL

示例代码 drv_a.c(提供函数,导出符号)

#include<linux/module.h>#include<linux/init.h>// 待导出函数intdrv_add(inta,intb){returna+b;}// 导出符号,其他ko可以调用drv_addEXPORT_SYMBOL_GPL(drv_add);staticint__initdrv_a_init(void){printk("drv_a 模块加载成功\n");return0;}staticvoid__exitdrv_a_exit(void){printk("drv_a 模块卸载成功\n");}module_init(drv_a_init);module_exit(drv_a_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("ZKX");MODULE_DESCRIPTION("导出符号模块A");

drv_b.c(外部模块调用 drv_a 导出的函数)

#include<linux/module.h>#include<linux/init.h>// 声明外部符号,告诉编译器这个函数在别的模块externintdrv_add(inta,intb);staticint__initdrv_b_init(void){intret=drv_add(10,20);printk("drv_b调用drv_add结果:%d\n",ret);return0;}staticvoid__exitdrv_b_exit(void){printk("drv_b 模块卸载成功\n");}module_init(drv_b_init);module_exit(drv_b_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("ZKX");MODULE_DESCRIPTION("使用外部符号模块B");

⚠️实操顺序非常重要

  1. 编译两个 ko:drv_a.kodrv_b.ko

  2. 先加载提供符号的模块 drv_a.ko

sudoinsmod drv_a.ko# 再加载使用符号的模块sudoinsmod drv_b.ko
  1. 查看打印dmesg

  2. 卸载顺序反过来:先 rmmod drv_b,再 rmmod drv_a

踩坑:先加载 drv_b 会报错:Unknown symbol drv_add。符号还没导出,就调用。

符号存放:内核符号表,/proc/kallsyms,可以 grep 查看导出符号

cat/proc/kallsyms|grepdrv_add

多模块编译注意:编译时需要Module.symvers,drv_a 编译生成的 Module.symvers 拷贝到 drv_b 编译目录,否则编译阶段报未定义警告。


二、模块传参:module_param 系列 API

应用场景:加载模块insmod/modprobe的时候,命令行直接给驱动传入参数,不需要重新编译驱动,调试非常方便。

头文件必须引入:

#include<linux/moduleparam.h>

四个核心 API

  1. module_param(name, type, perm)普通单个参数

  2. module_param_array(name, type, nump, perm)数组参数

  3. module_param_string(name, string, len, perm)字符串参数

  4. MODULE_PARM_DESC(_parm, desc)参数说明,modinfo 可以看到

参数说明
  • name:变量名,驱动内部定义的全局变量

  • type:支持类型:byte, short, ushort, int, uint, long, ulong, bool, charp

    • charp:字符指针,字符串
  • perm:sysfs 文件权限/sys/module/xxx/parameters/

    • 0:不在 sysfs 创建文件,只能命令行传参

    • 0644:rw‑r‑‑r‑‑ 用户可读,root 可写;常用 0644

注意:perm 不能写 0777,内核不允许。

  • nump:module_param_array 专用,输出实际传入数组元素个数,传 NULL 不接收。

  • len:module_param_string,字符串缓冲区最大长度。

完整可运行示例代码 param_drv.c

#include<linux/module.h>#include<linux/init.h>#include<linux/moduleparam.h>// 1.普通单个参数staticintdev_id=1;module_param(dev_id,int,0644);MODULE_PARM_DESC(dev_id,"设备ID,int类型,默认1");staticbool enable_log=false;module_param(enable_log,bool,0644);MODULE_PARM_DESC(enable_log,"是否开启日志打印,bool");// 2.数组参数staticintbuf[8];staticintbuf_cnt;module_param_array(buf,int,&buf_cnt,0644);MODULE_PARM_DESC(buf,"int数组,最多8个元素");//3.字符串参数staticchardev_name[32];module_param_string(dev_name,dev_name,sizeof(dev_name),0644);MODULE_PARM_DESC(dev_name,"设备名字符串");staticint__initparam_drv_init(void){inti;printk("====模块传参打印====\n");printk("dev_id = %d\n",dev_id);printk("enable_log = %d\n",enable_log);printk("dev_name = %s\n",dev_name);printk("数组实际传入元素个数:%d\n",buf_cnt);for(i=0;i<buf_cnt;i++){printk("buf[%d] = %d\n",i,buf[i]);}return0;}staticvoid__exitparam_drv_exit(void){printk("param_drv 模块卸载\n");}module_init(param_drv_init);module_exit(param_drv_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("ZKX");MODULE_DESCRIPTION("模块传参demo");

实操命令

编译生成param_drv.ko

1. insmod 传递参数
# 传参语法 insmod xxx.ko 变量名=值 数组=1,2,3,4sudoinsmod param_drv.kodev_id=100enable_log=1dev_name="mydev"buf=10,20,30

查看打印

dmesg
2. modprobe 传参

modprobe 传参不加 ko 后缀

sudomodprobe param_drvdev_id=200dev_name="testmod"buf=1,3,5
3. modinfo 查看参数描述(MODULE_PARM_DESC 生效)
modinfo param_drv.ko

可以看到 parm 一栏,每个参数的说明文字。

4.sysfs 下查看参数(perm=0644 才生成)
ls/sys/module/param_drv/parameters/cat/sys/module/param_drv/parameters/dev_id

卸载模块

sudormmod param_drv

关键注意点

  1. 参数必须定义为全局静态变量 static,局部栈变量不能用于 module_param。

  2. module_param_string:拷贝字符串到缓冲区;charp类型 module_param 只是指针,指向用户传入字符串,不要修改。

  3. 数组module_param_array超出数组定义长度的元素会被丢弃。

  4. perm=0:sysfs 不生成参数文件,只能加载时赋值,运行时不能改;0644 运行时 root 可以修改 sysfs 文件动态改参数。

  5. MODULE_PARM_DESC 仅仅是注释信息,给 modinfo 读取,不影响代码逻辑。

本篇知识点总结

模块符号导出

作用
EXPORT_SYMBOL(sym)导出符号,任意模块都可以调用
EXPORT_SYMBOL_GPL(sym)仅 GPL 协议模块可调用,项目推荐

实操要点:先加载导出符号的模块,再加载使用符号的模块;卸载顺序反过来;查看符号cat /proc/kallsyms

模块传参

API功能
#include <linux/moduleparam.h>模块传参头文件,必须包含
module_param(name,type,perm)单个变量传参
module_param_array(name,type,nump,perm)数组传参,nump 返回实际元素数量
module_param_string(name,string,len,perm)字符串拷贝到缓冲区
MODULE_PARM_DESC(parm,desc)参数描述,modinfo 查看

传参类型:int、bool、charp(字符串指针);perm 常用0644或者0;insmod/modprobe 加载时变量=值传参。

以上为作者知识总结,教学不易,点赞收藏达标持续更新!

← 返回列表