Linux 驱动之内核相关基础知识学习

知识图

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文件。

.config
make menuconfig
make 编译内核

Kconfig语法

mainmenu

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

menu
	config HELLOWORLD
endmenu

配置选项

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

bool类型有俩种值:y和n

tristat有三种值:y、m和n

string:为字符串类型

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

举例:

config HELLOWORLD
	bool "hello world support"
	default y
	help
		hello world
依赖关系

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

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

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

select表示反向依赖关系:

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

使用choiceendchoice定义可选择项。

直接举例说明:

choice
	bool "a“
config b
	boot b1
config c
	boot c1
...
endchoice
注释

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

config TEST_CONFIG
	bool "test"
	default y
	help
		just 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 HELLOWORLD
	bool "hello world support"
	default y
	help
		hello 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,                  \
                            &param_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

你可能感兴趣的:(linux,linux,学习,驱动开发)