Linux驱动开发基础(定时器、mmap)

所学来自百问网

目录

1.定时器

[1.1 定时器时间单位](#1.1 定时器时间单位)

[1.2 内核函数](#1.2 内核函数)

[1.3 定时器的应用举例](#1.3 定时器的应用举例)

2.mmap

[2.1 内存映射现象与数据结构](#2.1 内存映射现象与数据结构)

[2.2 ARM 架构内存映射简介](#2.2 ARM 架构内存映射简介)

[2.2.1 一级页表映射过程](#2.2.1 一级页表映射过程)

[2.2.2 二级页表映射过程](#2.2.2 二级页表映射过程)

[2.2.3 应用程序新建内存映射](#2.2.3 应用程序新建内存映射)

[2.2.3.1 mmap调用过程](#2.2.3.1 mmap调用过程)

[2.2.3.2 cache和buffer](#2.2.3.2 cache和buffer)

[2.2.4 驱动程序](#2.2.4 驱动程序)

[2.2.5 编程](#2.2.5 编程)

[2.2.5.1 APP编程](#2.2.5.1 APP编程)

[2.2.5.2 驱动编程](#2.2.5.2 驱动编程)


1.定时器

1.1 定时器时间单位

编译内核时,可以在内核源码根目录下用"ls -a"看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:

CONFIG_HZ=100  

这表示内核每秒中会发生 100 次系统滴答中断(tick),这是Linux系统的心跳。每发生一次tick中断,全局变量jiffies就会累加1。 CONFIG_HZ=100 表示每个滴答是10ms。

定时器的时间就是基于jiffies的,我们修改超时时间时,一般使用这2种方法:

在add_timer之前,直接修改:

timer.expires = jiffies + xxx;   // xxx 表示多少个滴答后超时,也就是xxx*10ms  
timer.expires = jiffies + 2*HZ;  // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2秒  

*在add_timer之后,使用mod_timer修改:

mod_timer(&timer, jiffies + xxx);   // xxx 表示多少个滴答后超时,也就是xxx*10ms  
mod_timer(&timer, jiffies + 2*HZ);  // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2秒

1.2 内核函数

对于4.x.x版本的Linux内核函数

  • **setup_timer(timer, fn, data):**设置定时器,主要是初始化timer_list结构体,设置其中的函数、参数。

  • **void add_timer(struct timer_list *timer):**向内核添加定时器。timer->expires表示超时时间,当超时时间到达,内核就会调用这个函数: timer->function(timer->data)。

  • **int mod_timer(struct timer_list *timer, unsigned long expires):**修改定时器的超时时间它等同于:del_timer(timer); timer->expires = expires; add_timer(timer);

  • **int del_timer(struct timer_list *timer):**删除定时器。

1.3 定时器的应用举例

按下或松开一个按键,它的GPIO电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。

如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。

处理方式

1.在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报

弊端:太耗时,违背"中断要尽快处理"的原则,你的系统会很卡

2.使用定时器

**核心在于:**在GPIO中断中并不立刻记录按键值,而是修改定时器超时时间,10ms 后再处理。

  • 如果10ms内又发生了GPIO中断,那就认为是抖动,这时再次修改超时时间为10ms。

  • 只有10ms 之内再无GPIO中断发生,那么定时器的函数才会被调用。 在定时器函数中记录按键值。

2.mmap

应用程序和驱动程序之间传递数据时,可以通过read、write函数进行。这涉及在用户态buffer和内核态buffer之间传数据,如下图所示:

应用程序不能直接读写驱动程序中的 buffer,需要在用户态 buffer 和内核态buffer之间进行一次数据拷贝。这种方式在数据量比较小时没什么问题; 但是数据量比较大时效率就太低了。比如更新LCD显示时,如果每次都让APP传递一帧数据给内核,假设 LCD 采用 1024*600* 32bpp 的格式,一帧数据就有 1024**600**32/8=2.3MB 左右,这无法忍受。

改进的方法就是让程序可以直接读写驱动程序中的 buffer,这可以通过 mmap 实现(memory map),把内核的buffer映射到用户态,让APP在用户态直接读写。

2.1 内存映射现象与数据结构

假设有这样的程序,名为test.c:

#include <stdio.h> 
#include <unistd.h> 
#include <stdlib.h> 
int a; 
int main(int argc, char **argv) 
{ 
    if (argc != 2) 
    { 
    printf("Usage: %s <number>\n", argv[0]); 
    return -1; 
    } 
    a = strtol(argv[1], NULL, 0); 
    printf("a's address = 0x%lx, a's value = %d\n", &a, a); 
    while (1) 
    { 
    sleep(10); 
    } 
    return 0; 
} 

在PC上如下编译(必须静态编译):

gcc  -o  test  test.c  -staitc 

分别执行test程序2次,最后执行ps,可以看到这2个程序同时存在,这2个程序里a变量的地址相同,但是值不同。如下图:

观察到这些现象:

  • 2个程序同时运行,它们的变量a的地址都是一样的:0x6bc3a0;

  • 2个程序同时运行,它们的变量a的值是不一样的,一个是12,另一个是123。

疑问来了:

  • 这2个程序同时在内存中运行,它们的值不一样,所以变量a的地址肯定不同;

  • 但是打印出来的变量a的地址却是一样的。

这里要引入虚拟地址的概念:CPU发出的地址是虚拟地址,它经过 MMU(Memory Manage Unit,内存管理单元)映射到物理地址上,对于不同进程 的同一个虚拟地址,MMU会把它们映射到不同的物理地址。如下图:

  • 当前运行的是app1时,MMU会把CPU发出的虚拟地址addr映射为物理地址 paddr1,用paddr1去访问内存。

  • 当前运行的是app2时,MMU会把CPU发出的虚拟地址addr映射为物理地址 paddr2,用paddr2去访问内存。

  • MMU负责把虚拟地址映射为物理地址,虚拟地址映射到哪个物理地址去?

可以执行ps命令查看进程ID,然后执行"cat /proc/325/maps"得到映射关系。

每一个APP在内核里都有一个tast_struct,这个结构体中保存有内存信息:mm_struct。而虚拟地址、物理地址的映射关系保存在页目录表中,如下图所示:

解析如下:

  • 每个APP在内核中都有一个task_struct结构体,它用来描述一个进程;

  • 每个APP都要占据内存,在task_struct中用mm_struct来管理进程占用的内存;

    • 内存有虚拟地址、物理地址,mm_struct中用mmap来描述虚拟地址, 用pgd来描述对应的物理地址。

    • **注意:**pgd,Page Global Directory,页目录。

  • 每个APP都有一系列的VMA:virtual memory

    • 比如APP含有代码段、数据段、BSS段、栈等等,还有共享库。这些单元会保存在内存里,它们的地址空间不同,权限不同(代码段是只读的可运行的、数据段可读可写),内核用一系列的vm_area_struct来描述它们。

    • vm_area_struct 中的 vm_start、vm_end是虚拟地址。

  • vm_area_struct 中虚拟地址如何映射到物理地址去?

    • 每一个APP的虚拟地址可能相同,物理地址不相同,这些对应关系保存在pgd中。

2.2 ARM 架构内存映射简介

ARM 架构支持一级页表映射,也就是说MMU根据CPU发来的虚拟地址可以找到第1个页表,从第1个页表里就可以知道这个虚拟地址对应的物理地址。一级页表里地址映射的最小单位是1M。

ARM 架构还支持二级页表映射,也就是说MMU根据CPU发来的虚拟地址先找到第1个页表,从第1个页表里就可以知道第2级页表在哪里;再取出第2级页表,从第2个页表里才能确定这个虚拟地址对应的物理地址。二级页表地址映射 的最小单位有4K、1K,Linux使用4K。

一级页表项里的内容,决定了它是指向一块物理内存,还是指问二级页表, 如下图:

2.2.1 一级页表映射过程

一线页表中每一个表项用来设置1M的空间,对于32位的系统,虚拟地址空间有4G,4G/1M=4096。所以一级页表要映射整个4G空间的话,需要4096个页表项。

第0个页表项用来表示虚拟地址第0个1M(虚拟地址为0~0xFFFFF)对应哪一块物理内存,并且有一些权限设置;

第1个页表项用来表示虚拟地址第1个1M(虚拟地址为 0x100000~ 0x1FFFFF)对应哪一块物理内存,并且有一些权限设置;

依次类推。

使用一级页表时,先在内存里设置好各个页表项,然后把页表基地址告诉MMU, 就可以启动MMU了。

以下图为例介绍地址映射过程:

a) CPU发出虚拟地址vaddr,假设为0x12345678

b) MMU根据vaddr[31:20]找到一级页表项:

◆ 虚拟地址0x12345678是虚拟地址空间里第0x123个1M,所以找到页表里第0x123项,根据此项内容知道它是一个段页表项。

段内偏移是0x45678。

c) 从这个表项里取出物理基地址:Section Base Address,假设是 0x81000000

d) 物理基地址加上段内偏移得到:0x81045678 所以CPU要访问虚拟地址0x12345678时,实际上访问的是0x81045678的物理地址。

2.2.2 二级页表映射过程

首先设置好一级页表、二级页表,并且把一级页表的首地址告诉MMU。

以下图为例介绍地址映射过程:

  • CPU发出虚拟地址vaddr,假设为0x12345678

  • MMU根据vaddr[31:20]找到一级页表项:

虚拟地址0x12345678 是虚拟地址空间里第0x123个1M,所以找到页表里第0x123项。根据此项内容知道它是一个二级页表项。

  • 从这个表项里取出地址,假设是address,这表示的是二级页表项的物理地址;

  • vaddr[19:12]表示的是二级页表项中的索引index即0x45,在二级页表项中找到第0x45项;

  • 二级页表项格式如下:

里面含有这4K或1K物理空间的基地址page base addr,假设是 0x81889000: 它跟vaddr[11:0]组合得到物理地址:0x81889000 + 0x678 = 0x81889678。 所以CPU要访问虚拟地址0x12345678时,实际上访问的是0x81889678的物理地址

2.2.3 应用程序新建内存映射

2.2.3.1 mmap调用过程

要给APP新开劈一块虚拟内存,并且让它指向某块内核buffer,具体操作:

1.得到一个vm_area_struct,它表示APP的一块虚拟内存空间;APP 调用 mmap 系统函数时,内核就帮我们构造了一个 vm_area_stuct 结构体。里面含有虚拟地址的地址范围、权限。

2.确定物理地址:你想映射某个内核buffer,你需要得到它的物理地址

3.给vm_area_struct和物理地址建立映射关系:内核提供有相关函数。

APP 里调用mmap时,导致的内核相关函数调用过程如下:

2.2.3.2 cache和buffer

使用mmap时,需要有cache、buffer的知识。下图是CPU 和内存之间的 关系,有cache、buffer(写缓冲器)。Cache是一块高速内存;写缓冲器相当于 一个FIFO,可以把多个写操作集合起来一次写入内存。

程序运行时有"局部性原理",这又分为时间局部性、空间局部性。

  • 时间局部性: 在某个时间点访问了存储器的特定位置,很可能在一小段时间里,会反复地访问这个位置。

  • 空间局部性: 访问了存储器的特定位置,很可能在不久的将来访问它附近的位置。

  • 而CPU的速度非常快,内存的速度相对来说很慢。CPU要读写比较慢的内存时,怎样可以加快速度?根据"局部性原理",可以引入cache。

1.读取内存addr处的数据时:

◼ 先看看cache中有没有addr的数据,如果有就直接从cache里返回数据:这被称为cache命中。

◼ 如果cache中没有addr 的数据,则从内存里把数据读入,注意:它不是仅仅读入一个数据,而是读入一行数据(cache line)。

◼ 而CPU很可能会再次用到这个addr的数据,或是会用到它附近的数据,这时就可以快速地从cache中获得数据。

2.写数据:

◼ CPU要写数据时,可以直接写内存,这很慢;也可以先把数据写入cache, 这很快。

◼ 但是cache中的数据终究是要写入内存的啊,这有2种写策略:

a) 写通(write through):

◆ 数据要同时写入cache和内存,所以cache和内存中的数据保持一致,但是它的效率很低。能改进吗?可以!使用"写缓冲器": cache 大哥,你把数据给我就可以了,我来慢慢写,保证帮你写完。

◆ 有些写缓冲器有"写合并"的功能,比如CPU执行了4条写指令: 写第0、1、2、3个字节,每次写1字节;写缓冲器会把这4个写操作合并成一个写操作:写word。对于内存来说,这没什么差别, 但是对于硬件寄存器,这就有可能导致问题。

◆ 所以对于寄存器操作,不会启动buffer功能;对于内存操作,比如LCD的显存,可以启用buffer功能。

b) 写回(write back):

◆ 新数据只是写入cache,不会立刻写入内存,cache和内存中的数据并不一致。

◆ 新数据写入cache 时,这一行cache 被标为"脏"(dirty);当 cache 不够用时,才需要把脏的数据写入内存。

使用写回功能,可以大幅提高效率。但是要注意cache和内存中的数据很可 能不一致。这在很多时间要小心处理:比如CPU产生了新数据,DMA把数据从内 存搬到网卡,这时候就要CPU执行命令先把新数据从cache刷到内存。反过来 也是一样的,DMA从网卡得过了新数据存在内存里,CPU读数据之前先把cache 中的数据丢弃。

是否使用 cache、是否使用 buffer,就有 4 种组合(Linux 内核文件 arch\arm\include\asm\pgtable-2level.h):

上面4种组合对应下表中的各项,一一对应(下表来自s3c2410芯片手册,高架构的cache、buffer更复杂,但是这些基础知识没变):

  • 第1种是不使用cache也不使用buffer,读写时都直达硬件,这适合寄存器的读写。

  • 第2种是不使用cache但是使用buffer,写数据时会用buffer进行优化,可能会有"写合并",这适合显存的操作。因为对显存很少有读 操作,基本都是写操作,而写操作即使被"合并"也没有关系。

  • 第3种是使用cache不使用buffer,就是"write through",适用于只读设备:在读数据时用cache加速,基本不需要写。

  • 第4种是既使用cache又使用buffer,适合一般的内存读写。

2.2.4 驱动程序

驱动程序要做的事情有3点:

1.确定物理地址

2.确定属性:是否使用cache、buffer

3.建立映射关系

参考Linux源文件,示例代码如下:

还有一个更简单的函数:

2.2.5 编程

在驱动程序中申请一个8K的buffer,让APP通过mmap能直接访问。

2.2.5.1 APP编程

open驱动、buf=mmap(......)映射内存,直接读写buf就可以了, 代码如下:

22      /* 1. 打开文件 */ 
23      fd = open("/dev/hello", O_RDWR); 
24      if (fd == -1) 
25      { 
26              printf("can not open file /dev/hello\n"); 
27              return -1; 
28      } 
29 
30      /* 2. mmap 
31       * MAP_SHARED  : 多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。 
32       *               就是说多个APP、驱动程序实际上访问的都是同一块内存 
33       * MAP_PRIVATE : 创建一个copy on write的私有映射。 
34       *               当APP对该内存进行修改时,其他程序是看不到这些修改的。 
35       *               就是当APP写内存时, 内核会先创建一个拷贝给这个APP, 
36       *               这个拷贝是这个APP私有的, 其他APP、驱动无法访问。 
37       */ 
38      buf =  mmap(NULL, 1024*8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
39      if (buf == MAP_FAILED) 
40      { 
41              printf("can not mmap file /dev/hello\n"); 
42              return -1; 
43      } 

最难理解的是mmap函数MAP_SHARED、MAP_PRIVATE参数。使用 MAP_PRIVATE映射时,在没有发生写操作时,APP、驱动访问的都是同一块内存; 当APP发起写操作时,就会触发"copy on write",即内核会先创建该内存块的拷贝,APP的写操作在这个新内存块上进行,这个新内存块是APP私有的,别的APP、驱动看不到。

仅用MAP_SHARED参数时,多个APP、驱动读、写时,操作的都是同一个内存块,"共享"。

MAP_PRIVATE映射是很有用的,Linux中多个APP都会使用同一个动态库,在没有写操作之前大家都使用内存中唯一一份代码。当APP1发起写操作时,内核会为它复制一份代码,再执行写操作,APP1就有了专享的、私有的动态库,在里面做的修改只会影响到APP1。其他程序仍然共享原先的、未修改的代码。

有了这些知识后,下面的代码就容易理解了,请看代码中的注释:

45      printf("mmap address = 0x%x\n", buf); 
46      printf("buf origin data = %s\n", buf); /* old */ 
47 
48      /* 3. write */ 
49      strcpy(buf, "new"); 
50 
51      /* 4. read & compare */ 
52      /* 对于MAP_SHARED映射:  str = "new" 
53       * 对于MAP_PRIVATE映射: str = "old" 
54       */ 
55      read(fd, str, 1024); 
56      if (strcmp(buf, str) == 0) 
57      { 
58              /* 对于MAP_SHARED映射,APP写的数据驱动可见 
59               * APP和驱动访问的是同一个内存块 
60               */ 
61              printf("compare ok!\n"); 
62      } 
63      else 
64      { 
65              /* 对于MAP_PRIVATE映射,APP写数据时, 是写入另一个内存块(是原内存块的"拷贝") 
66               */ 
67              printf("compare err!\n"); 
68              printf("str = %s!\n", str);  /* old */ 
69              printf("buf = %s!\n", buf);  /* new */ 
70      } 

执行测试程序后,查看到它的进程号PID,执行这样的命令查看这个程序的内存使用情况:

[root@100ask:~]# cat  /proc/PIC/maps
2.2.5.2 驱动编程

1.分配一块8K的内存

我们应该使用kmalloc或kzalloc,这样得到的内存物理地址是连续的, 在mmap时后APP才可以使用同一个基地址去访问这块内存。(如果物理地址不连续,就要执行多次mmap了)。

2.提供mmap函数

关键在于mmap函数,代码如下:

要注意的是,remap_pfn_range中,pfn的意思是"Page Frame Number"。 在Linux中,整个物理地址空间可以分为第0页、第1页、第2页,诸如此类, 这就是pfn。假设每页大小是4K,那么给定物理地址phy,它的pfn = phy / 4096 = phy >> 12。内核的page一般是4K,但是也可以配置内核修改page 的大小。所以为了通用,pfn = phy >> PAGE_SHIFT。

APP 调用mmap后,会导致驱动程序的mmap函数被调用,最终APP的虚拟地址和驱动程序中的物理地址就建立了映射关系。APP可以直接访问驱动程序的 buffer。

相关推荐
Komorebi.py11 分钟前
【Linux】-学习笔记05
linux·笔记·学习
Mr_Xuhhh17 分钟前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
梓仁沐白3 小时前
ubuntu+windows双系统切换后蓝牙设备无法连接
windows·ubuntu
内核程序员kevin3 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
网易独家音乐人Mike Zhou5 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
搬砖的小码农_Sky7 小时前
C语言:数组
c语言·数据结构
朝九晚五ฺ8 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream8 小时前
Linux的桌面
linux
xiaozhiwise8 小时前
Makefile 之 自动化变量
linux