linux驱动开发关于内核模块、设备模型、内存管理、模块参数介绍

内核功能

  • ① 多任务管理
  • ② 内存管理
  • ③ 文件管理
  • ④ 网络协议栈
  • ⑤ 设备管理---设备驱动

----------------------------------------------我是分割线------------------------------------------------------------

  • 一、内核开发特点

  • ① 内核代码的运行时机

  • 启动目标硬件
  • 应用程序调用系统调用
  • 硬件中断调用中断处理
  • Linux内核和驱动间的接口(kABI)不稳定
  • 内核升级后,接口可能会发生变化(以往驱动指针访问的结构体可能会存在异常)
  • 将驱动写入内核更加稳定
  • 二进制驱动需要匹配内核版本号才可运行,尽量选择longterm版本
  • Andord用的是LTS内核
  • ② 与应用层代码的区别

  • 内核的大部分代码是被动执行的
  • 32位的内核代码能够访问3G-4G的虚拟地址空间。64的操作系统则可以访问内核代码虚拟地址为4G-8G。
  • 与C库的代码不一样,无法使用C标准库进行系统调用。内核将C库的代码重新再写了一遍,比如printf ->printk(但不完全一样)
  • 内核代码中尽量不要使用浮点数,因为它会访问所有内容,影响代码的性能和移植行。
  • ③ 与裸机代码的区别

  • 内核代码使用虚拟地址,裸机代码使用物理地址
  • 操作设备时需要按照内核的框架编写代码
  • ④ 内核编译流程

  • 导出环境变量 、选择内核架构、添加交叉编辑工具链、导入默认配置、导入当前芯片的内核架构默认配置、修改默认配置
  • 二、内核模块特点

  • 定义:将内核一部分代码编译到单独的目标文件,需要调用的时候再加载。(uname -r获取内核模块的信息)【使用内核模块的话,很多设备厂商可以独立去发布一些驱动代码】
  • 文件目录分级:net下是文件协议栈;driver下是驱动信息。
  • 大部分设备驱动可以被编译成内核模块
  • 可以减少内存的占用,不用的时候不加载,需要时候再移入内存。同样也减少了内核的启动时间。
  • 进行驱动升级和修复时,方便操作。同时重新上架的时候可减少内核的重启次数。
  • 如何编译内核模块

  • ① 内部模块(内核源码自带的模块)

  • 先make menuconfig 编辑时将需要的内容配置为M, *是与内核一起编译
  • make modules -j2(用两个处理器进行编译内部模块)
  • ② 外部模块(第三方厂家开发的内核模块)

先建立一个目录,在内部创建一个类似Makefile的文件(Kbuild)。

在Kbuild文件内需要输入:obj-m  =  test.o(自己创建的.c文件的二进制)。
解释:最终编译的模块源文件来自于哪一个.c文件。

make -C  内核路径  M=模块路径 modules  【make -C /home/linux/linux M=`pwd` modules】
解释:利用内核makefile编译外部模块
建立多个:obj-m = hello.o (模块)  修改Kbuild文件,模块名不能和C文件名重复,增加模块名-objs 规则。
hello-objs = test.o func.o (依赖)
  • 如何使用内核模块

  • ① 在.c文件里敲内核模块功能,将其用内核进行编译
  • ② 安装内核模块至根文件系统
  • ③ 去目标端进行寻找内核模块
  • ④ 查看已加载的内核模块(若代码编译为模块,需要提供入口函数)

 

 设备驱动与模块参数

  • ① 模块加载的时候可以传递参数,参数类型只能是以下几种

* Standard types are:
* byte, short, ushort, int, uint, long, ulong
* charp: a character pointer 一个字符型的指针
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
  • ② 模块代码中使用module_param宏获取参数。

  • 可以使用MODULE_PARM_DESE宏来设置模块参数的帮助信息。使用前可以用modinfo命令查看模块支持的参数。
  • 可以通过sysfs虚拟文件系统访问模块参数(放在/sys/module/模块名/parameters目录中),可以通过cat或echo读取和修改模块参数。

内核模块可以调用EXPROT_SYMBOL宏导出的函数,若调用未导出的函数,加载模块时会报错。

  • 设备驱动

  • 字符设备:按字节进行访问的设备(大部分不支持随机访问 如SPI)
  • 块设备:按块进行访问的设备(可随机访问,使用文件接口)
  • 网络设备:没有设备文件(利用socket接口进行访问)
  • 设备号:
  • 主设备号(高12位的设备号major dev): 区分设备类型 从设备号(低20位的):区分同类中不同的设备
  •  一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

获取主设备号与从设备号的宏如下:

  • 设备文件,设备文件本身不包含任何数据。索引节点(i-node)才是区分文件之间(通过设备号来找)的标识。文件名不是。
  • s =serial 串口 sg0=sda b开头的块设备 p是有名管道 c是字符设备
  • 设备文件本身不包含任何数据
  • 创建设备文件命令 mknod+文件名+类型(c\b\p)+主设备号(%255)+从设备号
  • 设备驱动流程如下:

 

  • 字符设备的注册流程

  • 申请设备号 采用函数如下
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数自左到右为: 【1、操作系统分配的主设备号(全局变量)】【2、申请的主设备号数量】【3、主设备号对应的驱动或从设备名称(显示在/proc/devices文件中)】【成功返回0,失败返回错误码】
  • 创建cdev(字符设备管理)结构体对象
struct cdev {
      struct kobject kobj;                  //内嵌的内核对象.
      struct module *owner;                 //该字符设备所在的内核模块的对象指针.
      const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
         struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
      dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
      unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};
  • 该cdev中结构体链表构成如下
struct list_head  {
struct list_head* next;  指头的
struct list_head* prev;  ☞尾巴的
};
初始化该链表函数:void INIT_LIST_HEAD(struct list_head *list);
头插该链表函数:void list_add(struct list_head *new, struct list_head *head); 
尾插该链表函数:void list_add_tail(struct list_head *new, struct list_head *head);
删除该链表函数:void list_del(struct list_head *entry); 
遍历该链表:list_for_each_entry(pos, head, member) {
...} pos:当前节点指针    head:链表头   member:链表节点在结构体的名称   
通过结构体成员的地址计算当前结构体所在的外部结构体首地址:
#define container_of(ptr, type, member) ({ const struct list_head *__mptr = (ptr); 
(type *)( (char *)__mptr - offsetof(type,member) );}) ptr:链表当前节点地址  type:链表节点所在结构体地址  member:链表节点成员名称
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
  • 初始化cdev对象,采用函数如下
void cdev_init(struct cdev *cdev, const struct file_operations *fops) 
参数自左到右为:【1、cdev对象的指针】【2、保存cdev对象指定的系统函数结构体的指针】
  • 将cdev对象放入字符设备的哈希表中(cdev_map)
int cdev_add(struct cdev *p, dev_t dev, unsigned count) 
参数自左到右为:【1、cdev对象的指针】【2、当前设备号】【3、当前设备(驱动)数量】

整体流程:

 ----------------------------------------------我是分割线---------------------------------------------------------

  • 字符设备文件读取内容(Read)

ssize_t test_read(struct file * filp, char __user * buf, size_t count, loff_t * pos)
buf:看一看用户需要读取的目标设备文件是什么,然后写入buf,发送给用户
count:一次读取多少内容给用户
pos:来自于vfs层级的偏移指针,不受此函数程序的影响。可用来增加实际读取的字节数
返回值:文件结束返回0



unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
等同于memcpy,但会用来检查用户空间是否有效(将内核空间处的内容拷贝至用户空间)
to:用户空间的地址 
from:内核的空间地址
n:复制的字节数
返回值:未赋值的字节数

int test_release(struct inode * A, struct file *B); 
A:索引节点   B:已打开的文件
进程退出时,操作系统会自动关闭已打开的文件。
  • 字符设备文件写入内容(write)

ssize_t test_write(struct file * filp, const char __user * buf, size_t count,
loff_t * pos)
参数从左到右分别为:【1、打开的文件】【2、写入数据的地址(__user宏来检查指针是否在用户空间
的0-3G)因为调用的是write,所以内核处write用get_user(内核变量,用户空间指针)来接收))】【3、本次需要写入的字节数】
【4、从个位置哪里开始写】


unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
等同于memcpy,从内存拷贝。不限数据类型,但不一样的是这个函数会检查用户空间内存的有效性(将用户空间处的内容拷贝至内核空间)
参数从左到右为【1、拷贝至哪个对象的地址】【2、拷贝来源】【3、复制的字节数】【返回未拷贝的字节数】


void *ioremap(phys_addr_t offset, unsigned long size) 
帮助内核访问物理地址处的外设寄存器函数,需要先将物理地址映射为虚拟地址。再通过虚拟地址修改寄存器
offset:物理地址    size:映射的内存大小   返回值:虚拟地址


int test_release(struct inode * A, struct file *B); 
A:索引节点   B:已打开的文件
进程退出时,操作系统会自动关闭已打开的文件。
  • 用户空间调用open函数打开某个设备文件后,在进程中会为设备文件分配一个未使用过的文件描述符,并且生成一个空白struct file结构体,然后从文件系统中查找到文件对应的inode(自己在文件系统中创建的设备节点)然后把该inode的i_fop赋值给进程中的struct file的f_op,也就是说此时进程已经找到了设备节点了,但是还没有调用我们自己写的驱动,即真正的file_operation接口。接下来为了调用真正的file_operation接口,我们从内核哈希表cdev_map中,根据设备号查找自己注册的sturct cdev(字符设备),获取cdev中的file_operation接口,把cdev中的file_operation接口赋值给struct file的f_op,此时进程已经可以最终调用自己实现的file_operation接口中的open函数,至此进程获得了该设备文件的自己实现的读写等操作。

---------------------------------------------------------我是分割线--------------------------------------------------------

伪字符设备

  • 定义:没有实际物理硬件的设备
  • 常见的伪字符设备: 1、/dev/null:空设备,丢弃写入的任何设备。2、/dev/zero:读出的数据全为0。3、/dev/random:读出的数据为随机值。

内存管理

  • 在ARM阶段开发时,操作地址为物理地址。在应用层开发时地址为虚拟地址。(映射关系)
MMU(内存管理单元):负责翻译CPU过来的信号,翻译成物理地址。根据字典(页表)进行翻译。
页表保存了虚拟地址与物理地址的关系,进程的页表在内存中
工作流程:MMU查看虚拟地址是否在自己缓冲区(TLB)->若在直接使用,不存在去读取页表(内存中读取)->进行翻译
物理内存实际上是在使用时才给分配的。

  • 进程创建的内核会为每个进程分配一个大页表。当进程分配内存时,内核会修改进程页表加入新的页表项。
  • 进程切换时会切换大页表,因为随着进程的切换,虚拟地址到物理地址的映射已经改变。
  • 分配小于一个页面(4k)的内存时,采用kmalloc函数
void *kmalloc(size_t size, gfp_t flags);
void kfree(void *p);
size:申请内存的大小 flags:标志(GFP_KERNEL:内核代码使用)   
返回值:分配的内存地址
其分配物理地址和虚拟地址全部连续,一般用于分配小内存

void *vmalloc(unsigned long size);
void vfree(const void *addr);
vmalloc一般用于较大内存的分配,比如内核模块的加载,给其分配一个物理内存。
限制大小[128M]   分配的虚拟地址连续,物理不一定连续,所以要给其虚拟地址转化一个页表。故其分配内存效率低于kamlloc


unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
gfp_mask:标志
order:申请页面个数的对数 1 = 2^0,2 =2^1 (0-11)
返回值:分配页面的首地址(虚拟地址)    
其分配的物理地址和虚拟地址全部连续,一般分配一个页面(4kb)的大小

----------------------------------------------------------我是分割线-------------------------------------------------

设备模型(平台总线驱动)

关于总线使用此类结构体定义
static struct bus_type vbus = {
.name = "vbus",     //重构此结构体名字为vbus
.match = vbus_match, //总线进行搭建驱动和设备的结构体函数
};

关于设备使用此类结构体定义    
static struct device vdev={
  .init_name = "vdev", 重构此结构体名字为vdev
  .bus = &vbus, 外部引入总线的结构体,以便使用match找寻驱动
  .release = vdev_release,  连接成功后释放此结构体的内容  
};

关于驱动使用此类结构体定义
static struct device_driver vdrv ={
    .name = "vder" ,
    .bus = &vbus,
};
  • 平台总线:虚拟的总线,不属于其他总线的设备,可以挂载到平台总线下。
  • 平台设备:CPU可以控制的,属于平台总线的设备。
  • struct resource(存放内容的)
  • PC机的bios界面有一个自检表(ACPI),会收集外设信息。服务器平台使用uefi加载garbal来收集硬件信息。嵌入式产品一般使用设备树。
  • 平台总线进行操作匹配平台设备、平台驱动时有四个方法。
  • Intel的设备寄存器和内存的访问方法是不一样的,ARM中访问寄存器和内存是一样的。
  • 设备和驱动匹配的种类

  • 1.使用设备树节点的compatible属性进行匹配。 2.根据BIOS中的ACPI表信息进行匹配驱动 3.根据设备的ID进行匹配。4.根据设备的名称匹配驱动。
  • 设备与匹配驱动的流程

  • 定义平台设备的结构体变量(struct device filsd),将其添加至平台总线(struct bus_type vbus)。
  • 定义平台驱动的结构体变量(struct device_driver vdrc),将其添加至平台总线(struct bus_type vbus)。
  • 总线遍历所有设备,调用match函数绑定设备与驱动。
  • 调用驱动的probe函数对设备进行初始化

  • probe函数的处理:1,注册设备号(dev_t dev)。2,分配字符设备管理(cdev)对象并注册至哈希表。3,从设备结构体(struct device filsd)中获取寄存器的地址。4,利用iomap将操作寄存器地址映射到虚拟地址作为基准。
  • 具体probe初始化设备函数源码如下:
static int fsled_probe(struct platform_device *pdev)
{
int ret; 判断设备号是否注册至系统的变量
dev_t dev;  获取的设备号 
struct fsled_dev *fsled; 此结构体中的con成员用于定义硬件寄存器的虚拟地址
struct resource *res;  用于保存硬件寄存器的物理地址
//从设备信息中获取引脚编号
unsigned int pin = *(unsigned int*)pdev->dev.platform_data;
//使用LED的ID作为次设备号,生成设备号
dev = MKDEV(FSLED_MAJOR, pdev->id);
//注册设备号
ret = register_chrdev_region(dev, 1, FSLED_DEV_NAME);
if (ret)
goto reg_err;
//分配cdev对象
fsled = kzalloc(sizeof(struct fsled_dev), GFP_KERNEL);
if (!fsled) {
ret = -ENOMEM;
goto mem_err;
}
cdev_init(&fsled->cdev, &fsled_ops);
fsled->cdev.owner = THIS_MODULE;
//将cdev对象添加到字符设备哈希表中
ret = cdev_add(&fsled->cdev, dev, 1);
if (ret)
goto add_err;
//从设备信息中获取寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);  
if (!res) {
ret = -ENOENT;
goto res_err;
}
fsled->con = ioremap(res->start, resource_size(res));  将寄存器的物理地址映射为虚拟地址
if (!fsled->con) {
ret = -EBUSY;
goto map_err;
}
//计算数据寄存器地址
fsled->dat = fsled->con + 1;
fsled->pin = pin;
//只允许一个进程打开设备文件
atomic_set(&fsled->available, 1);
//配置GPIO工作模式为输出
writel((readl(fsled->con) & ~(0xF << 4 * fsled->pin)) | (0x1 << 4 *
fsled->pin), fsled->con);
//设置GPIO引脚输出低电平
writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
//将cdev对象的指针保存到设备信息中,在remove函数中释放内存
platform_set_drvdata(pdev, fsled);
return 0;
map_err:
res_err:
cdev_del(&fsled->cdev);
add_err:
kfree(fsled);
mem_err:
unregister_chrdev_region(dev, 1);
reg_err:
return ret;
}

 子系统(bus总线)的作用

  • 设备文件的创建规则可能是依赖于设备属性,所以在创建设备文件时需要特别创建目录,然后在目录下生成设备文件。
  • 设备进入操作系统----->总线识别并添加设备信息---->总线利用match函数加载驱动信息绑定该设备(此时已调用class_create建立设备目录在sysfs下)------>初始化该设备 (内核发出创建设备文件信号给上层子系统)------>在子系统生成的目录下,mdev用device-creat函数进行创建设备文件。
  • 除读写之外的其他操作可以使用ioctl系统调用实现。
  • udevd守护进程(嵌入式中使用mdev)创建设备文件,扫描规定系统目录,如果发生修改,就修改设备存放目录下的设备文件。
  • 在驱动代码中,调用device_create函数在规定系统目录中生成对应的目录,再由udevd在设备存放目录下生成设备文件。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值