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 源码下载

我们可以在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)头文件(必须有)
驱动需要包含内核相关头文件。必须包含<linux/module.h><linux/init.h>

(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 <linux/module.h>
    #include <linux/init.h>
    
    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 <linux/module.h>
#include <linux/init.h>

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 <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>

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 <linux/module.h>
#include <linux/init.h>

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 <linux/module.h>
#include <linux/init.h>

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
  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux驱动是用于与硬件设备进行交互的软件模块。它允许操作系统与硬件进行通信,控制设备的功能和行为。以下是一些Linux驱动基础知识: 1. 设备驱动程序:设备驱动程序是一个软件模块,用于控制特定的硬件设备,例如网卡、声卡或显示器。驱动程序通过与硬件设备进行交互,向操作系统提供访问该设备的接口。 2. 内核空间和用户空间:Linux驱动程序可以运行在内核空间或用户空间。内核空间是操作系统的核心部分,具有直接访问硬件的能力。用户空间是应用程序运行的环境,无法直接访问硬件。大多数驱动程序在内核空间运行。 3. 内核模块:Linux驱动程序通常以内核模块的形式存在。内核模块可以在运行时加载和卸载,而无需重新启动操作系统。内核模块通常是编译成单独的文件(.ko文件),并通过insmod或modprobe命令加载到内核中。 4. 设备节点:在Linux系统中,每个设备都有一个对应的设备节点。设备节点是一个特殊文件,用于与设备进行通信。设备节点可以通过设备驱动程序创建,并通过文件系统进行访问。 5. 设备树:设备树是一种描述硬件设备及其连接关系的数据结构。它在Linux驱动开发中起到重要的作用,用于在系统启动时自动加载适当的驱动程序。 这些是Linux驱动程序的基础知识,希望能对你有所帮助。如果你有更具体的问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值