Linux内核设备驱动之内存管理笔记整理

/**********************
 * linux的内存管理
 **********************/

到目前为止,内存管理是unix内核中最复杂的活动。我们简单介绍一下内存管理,并通过实例说明如何在内核态获得内存。

(1)各种地址

对于x86处理器,需要区分以下三种地址:

*逻辑地址(logical address)

只有x86支持。每个逻辑地址都由一个段(segment)和一个偏移量(offset)组成,偏移量指明了从段的开始到实际地址之间的距离。

逻辑地址共48位,段选择符16位,偏移量32位。linux对逻辑地址的支持很有限

*线性地址(linear address)

也称为虚拟地址(virtual address)。

32位无符号整数,从0x0000,0000到0xffff,ffff,共4GB的地址范围。无论是应用程序还是驱动程序,我们在程序中使用的地址都是虚拟地址。

*物理地址(physical address)

32位无符号整数,与从CPU的地址引脚发送到存储器总线上的电信号相对应。用于存储器寻址。

找一个程序,如scanf.c,运行两个,然后执行下面指令观察:

$>pmap $(pid)
$>cat /proc/$(pid)/maps

(2)物理内存和虚拟内存

a.物理内存

就是系统中实际存在的RAM,比如我们常说的一条256兆RAM。x86处理器和物理内存之间是通过实际的物理线路连接的。

另外,x86处理器还通过主板连接了很多的外设,这些外设也通过实际的物理线路和处理器相连。

对于处理器来说,多数的外设和RAM的访问方式是一致的,都是由程序发出物理地址访问实际的物理器件。

外设和RAM共享一个4G大小的物理内存空间。

b.虚拟内存

是在物理内存之上为每个进程构架的一种逻辑内存,处于应用程序的内存请求与硬件内存管理单元(Memory Management Unit, MMU) 之间.MMU将应用程序使用的虚拟内存根据预先定义好的页表转化为物理地址,然后通过物理地址对实际的外设或RAM进行访问。

虚拟内存有很多用途和优点:

  • *若干个进程可以并发地执行
  • *应用程序所需内存大于物理内存时也可以运行
  • *程序只有部分代码装入内存时进程可以执行它
  • *允许每个进程访问可用物理内存的一个子集
  • *进程可以共享库函数或程序的一个单独内存映像
  • *程序是可重定位的,也就是说,可以把程序放在物理内存的任何地方
  • *编程者可以编写与机器无关的代码,不必关心物理内存的组织结构

(3)RAM的使用

linux将实际的物理RAM划分为两部分使用,其中若干兆字节专门用于存放内核映像(也就是内核代码和内核静态数据结构),RAM的其余部分通常由虚拟内存系统来处理,并用在以下3种可能的方面:

  • *满足内核对缓存,描述符和其他动态内核数据结构的请求
  • *满足进程对一般内存区的请求及对文件内存映射的请求
  • *借助于高速缓存从磁盘及其他缓冲设备获得较好的性能

虚拟内存必须解决的一个主要问题是内存碎片,因为通常内核使用连续的物理内存,所以碎片过多可能导致请求失败。

/**********************
 * 在内核中获取内存
 **********************/

和在用户空间中一样,在内核中也可以动态分配和释放内存,但受到的限制要比用户空间多一些。

(1)内核中的内存管理

内核把物理页作为内存管理的基本单位。这主要是因为内存管理单元(MMU)是以页为单位进行虚拟地址和物理地址转换的,从虚拟内存的角度来看,页就是最小单位。大多数32位体系结构支持4KB的页。

a.页

内核用struct page表示系统中的每个物理页。

包括<linux/mm.h>就可以使用page,其实际定义在<linux/mm_types.h>

struct page{
 page_flags_t flags;
 atomic_t _count;
 atomic_t _mapcount;
 unsigned long private;
 struct address_space *mapping;
 pgoff_t index;
 struct list_head lru;
 void *virtual;
};

flags用于存放页的状态,定义在<linux/page-flags.h>,状态包括页是不是脏的,是不是被锁定在内存中等等。_count存放页的引用计数。

page结构与物理页相关,并非与虚拟页相关。结构的目的再于描述物理内存本身,而不是其中的数据。

内核根据page结构来管理系统中所有的页,内核通过page可以知道一个页是否空闲(也就是页有没有被分配)。

如果页已经被分配,内核还需要知道谁拥有这个页。

拥有者可能是用户空间进程,动态分配的内核数据,静态内核代码,或页高速缓存等。

系统中的每个物理页都要分配这样一个结构。如果结构体40字节大小,则128MB物理内存(4K的页)需要分配1MB多用于page结构。

b.区

由于硬件的限制,内核不能对所有的页一视同仁。内核使用区(zone)对具有相似特性的页进行分组。这些特性包括:

  • *一些硬件只能用某些特定的内存地址来执行DMA
  • *一些体系结构其内存的物理寻址范围远大于虚拟寻址范围,这样,就有一些内存不能永久地映射到内核空间

针对这些限制,linux采用了三种区(<linux/mmzone.h>):

  • ZONE_DMA:这个区包含的页能执行DMA操作
  • ZONE_NORMAL:这个区包含的都是能正常映射的页
  • ZONE_HIGHMEM:这个区包含高端内存(大于896M),其中的页不能永久地映射到内核的地址空间

对于x86,这3个区对于的物理内存分别是:

  • ZONE_DMA: <16MB
  • ZONE_NORMAL: 16~896MB
  • ZONE_HIGHMEM: >896MB

见<linux/mmzone.h>中的struct zone。

系统中只有3个这样的区结构。

(2)页分配

内核是使用页进行内存管理的,因此,我们在内核中也可以要求系统以页为单位给我们分配内存。当然,以页为单位分配可能造成内存浪费,因此,只有在我们确定需要整页内存时才调用他们。

a.分配

#include <linux/gfp.h>
1. struct page * alloc_pages(
    unsigned int gfp_mask,
    unsigned int order);
//分配2的order次方个连续的物理页。
2. void *page_address(
    struct page *page);
//返回一个指针,指向给定物理页当前的虚拟地址
3. unsigned long __get_free_pages(
    unsigned int gfp_mask,
    unsigned int order);
//相当于上两个函数结合
4. struct page * alloc_page(
    unsigned int gfp_mask);
5. unsigned long __get_free_page(
    unsigned int gfp_mask);
6. unsigned long get_zeroed_page(
    unsigned int gfp_mask);
//只分配一页

b.gfp_mask标志

这个标志决定了内核在分配内存时的行为,以及从哪里分配内存。

#include <linux/gfp.h>
#define GFP_ATOMIC
//原子分配,不会休眠,可用于中断处理。
#define GFP_KERNEL
//首选,内核可能会睡眠,用在进程上下文中

c.释放页

void __free_pages(struct page *page,
    unsigned int order);
void free_pages(unsigned long addr,
    unsigned int order);
void free_page(unsigned long addr);

注意!只能释放属于你的页。错误的参数可能导致内核崩溃。

(3)通过kmalloc获取内存

kmalloc和malloc很象,是内核中最常用的内存分配函数。

kmalloc不会对分配的内存区域清0,分配的区域在物理内存中是连续的。

a.分配

#include <linux/slab.h>
void *kmalloc(size_t size, int flags)

size是要求分配的内存的大小

kmalloc的参数flags可以控制kmalloc分配时的行为。和alloc_page时使用的标志是一致的。注意,kmalloc不能分配高端内存

b.释放

#include <linux/slab.h>
 void kfree(const void *ptr);

如果要释放的内存已经被释放了,或者释放属于内核其他部分的内存,则会产生严重的后果。调用kfree(NULL)是安全的。

要注意!内核只能分配一些预定义的,固定大小的字节数组。kmalloc能处理的最小内存块是32或64。由于kmalloc分配的内存在物理上连续,所以有分配上限,通常不要超过128KB。

(4)通过vmalloc获得内存

vmalloc()分配的内存虚拟地址是连续的,但物理地址不需要连续。这也是malloc()的分配方式。vmalloc分配非连续的内存块,再修改页表,把内存映射到逻辑空间连续的区域内。

大多数情况下,只有硬件设备需要得到物理地址连续的内存,内核可以使用通过vmalloc获得的内存。但内核中多采用kmalloc,这主要是考虑性能,因为vmalloc会引起较大的TLB抖动,除非映射大块内存时采用vmalloc。例如模块动态加载时,就是加载到通过vmalloc分配的内存。

vmalloc在<linux/vmalloc.h>声明,在<mm/vmalloc.c>定义,用法和malloc()相同。

 void* vmalloc(unsigned long size);
 void vfree(void *addr);

vmalloc会引起睡眠

(5)通过slab机制获得内存

分配和释放数据结构是内核最普遍的操作之一。

一种常用的方法是构建一个空闲链表,其中包含有可供使用的,已经分配好的数据结构块。

每次要分配数据结构就不用再申请内存,而是直接从这个空闲链表中分配数据块,释放结构时将内存还回这个链表。

这实际上是一种对象高速缓存(缓存对象).

linux针对这种要求提供了一个slab分配器来完成这一工作。

slab分配器要在几个基本原则之间寻求平衡:

  • *频繁使用的数据结构会频繁分配和释放,需要缓存
  • *频繁分配和回收必然导致内存碎片,为避免这一现象,空闲链表中的缓存会连续存放,从而避免碎片
  • *分配器可以根据对象大小,页大小和总的高速缓存大小来进行优化

kmalloc就建立在slab之上。

a.创建一个新的高速缓存

#include <linux/slab.h>
struct kmem_cache *kmem_cache_create(
   const char *name,
   size_t size,
   size_t align,
   unsigned long flags,
   void(*ctor)(...));

name: 高速缓存的名字。出现在/proc/slabinfo
size: 缓存中每个元素的大小
align: 缓存中第一个对象的偏移,常用0
flags:分配标志。常用SLAB_HWCACHE_ALIGH,表明按cache行对齐,见slab.h

b.销毁高速缓存

#include <linux/slab.h>
void kmem_cache_destroy(struct kmem_cache *cachep);

必须在缓存中的所有对象都被释放后才能调用。

c.从高速缓存中获得对象

void *kmem_cache_alloc(
   struct kmem_cache *cachep, int flags);
flags:
   GFP_KERNEL

d.将对象释放回高速缓存

void kmem_cache_free(
   struct kmem_cache *cachep, void *objp);

可参见kernel/fork.c

(6)高端内存的映射

在高端内存中的页不能永久地映射到内核地址空间,因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有虚拟地址。需要通过函数为其动态分配。

a.映射

要映射一个给定的page结构到内核地址空间,可以使用:

void *kmap(struct page *page);

函数可以睡眠

b.解除映射

void kunmap(struct page* page);

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • 移植新内核到Linux系统上的操作步骤

    1.在ubuntu官网下载ubuntu16.04的镜像和对应ubuntu16.04的内核版本源代码,或者在镜像源上找 2.安装ubuntu16.04到PC主机上 接下来执行以下: 编译新的Linux内核给X86内核使用出现以下错误: scripts/sign-file.c:25:30: fatal error: openssl/opensslv.h: No such file or directory 解决方法: (1)下载openssl-1.0.1d.tar.gz tar xzf openss

  • Linux内核设备驱动之系统调用笔记整理

    /**************************** * 系统调用 ****************************/ (1)什么是系统调用 系统调用是内核和应用程序间的接口,应用程序要访问硬件设备和其他操作系统资源,必须通过系统调用来完成. 在linux中,系统调用是用户空间访问内核的唯一手段,除异常和中断外,他们是内核唯一的合法入口.系统调用的数量很少,在i386上只有大概300个左右. (2)c库和系统调用的关系 应用程序员通过C库中的应用程序接口(API)而不是直接通过系统

  • Linux内核设备驱动之内核中链表的使用笔记整理

    /******************** * 内核中链表的应用 ********************/ (1)介绍 在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织.这些链表大多采用在include/linux/list.h实现的一个相当精彩的链表数据结构. 链表数据结构的定义很简单: struct list_head { struct list_head *next, *prev; }; list_head结构包含两个指向list_head结构的

  • Linux内核设备驱动之内核的调试技术笔记整理

    /****************** * 内核的调试技术 ******************/ (1)内核源代码中的一些与调试相关的配置选项 内核的配置选项中包含了一些与内核调试相关的选项,都集中在"kernel hacking"菜单中.包括: CONFIG_DEBUG_KERNEL 使其他的调试选项可用,应该选中,其本身不会打开所有的调试功能. 具体的调试选项说明可参见驱动一书,或通过menuconfig的help说明查看. (2)如何通过宏对printk调试语句进行全局控制 通

  • Linux内核设备驱动之内核的时间管理笔记整理

    /****************** * linux内核的时间管理 ******************/ (1)内核中的时间概念 时间管理在linux内核中占有非常重要的作用. 相对于事件驱动而言,内核中有大量函数是基于时间驱动的. 有些函数是周期执行的,比如每10毫秒刷新一次屏幕: 有些函数是推后一定时间执行的,比如内核在500毫秒后执行某项任务. 要区分: *绝对时间和相对时间 *周期性产生的事件和推迟执行的事件 周期性事件是由系统系统定时器驱动的 (2)HZ值 内核必须在硬件定时器的帮

  • Linux内核设备驱动之proc文件系统笔记整理

    /***************** * proc文件系统 *****************/ (1)/proc文件系统的特点和/proc文件的说明 /proc文件系统是一种特殊的.由软件创建的文件系统,内核使用它向外界导出信息,/proc系统只存在内存当中,而不占用外存空间. /proc下面的每个文件都绑定于一个内核函数,用户读取文件时,该函数动态地生成文件的内容.也可以通过写/proc文件修改内核参数 /proc目录下的文件分析  /proc/$pid 关于进程$pid的信息目录.每个进程

  • Linux内核设备驱动之虚拟文件系统笔记整理

    /******************** * 虚拟文件系统VFS ********************/ (1)VFS介绍 虚拟文件系统VFS作为内核的子系统,为用户空间程序提供了文件系统的相关接口. VFS使得用户可以直接使用open()等系统调用而无需考虑具体文件系统和实际物理介质. VFS提供了一个通用的文件系统模型,该模型囊括了我们所能想到的文件系统的常用功能和行为.通过这个抽象层,就可以实现利用通用接口对所有类新的文件系统进行操作. a.调用模型 write(): 用户空间 --

  • Linux内核设备驱动之Linux内核基础笔记整理

    1. Linux内核驱动模块机制 静态加载, 把驱动模块编进内核, 在内核启动时加载 动态加载, 把驱动模块编为ko, 在内核启动后,需要用时加载 2. 编写内核驱动 #include <linux/module.h> #include <linux/init.h> static int __init test_init(void) { return 0; //返回0表示成功, 返加负数退出加载模块 } //__init 当内核把驱动初始化完后, 释放此函数的代码指令空间 stat

  • Linux内核设备驱动之字符设备驱动笔记整理

    /******************** * 字符设备驱动 ********************/ (1)字符设备驱动介绍 字符设备是指那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动. 此类驱动适合于大多数简单的硬件设备.比如并口打印机,我们通过在/dev下建立一个设备文件(如/dev/printer)来访问它. 用户应用程序用标准的open函数打开dev/printer,然后用write向文件中写入数据,用read从里面读数据. 调用流程: write(): 用户空间 -->

  • Linux内核设备驱动之Linux内核模块加载机制笔记整理

    #include <linux/moduleparam.h> 1. 模块参数 在驱动定义变量 static int num = 0; //当加载模块不指定num的值时则为0 module_param(变量名, 类型, 权限);类型: byte, int, uint, short, ushort, long, ulong, bool, charp,权限不能有写的权限 传参数: insmod test.ko 变量名1=值1  变量名2=值2 module_param的调用关系如下: #define

  • 将Linux代码移植到Windows的简单方法

    一.前言 Linux拥有丰富各种源代码资源,但是大部分代码在Windows平台情况是无法正常编译的.Windows平台根本无法直接利用这些源代码资源.如果想要使用完整的代码,就要做移植工作.因为C/C++ Library的不同和其他的一些原因,移植C/C++代码是一项困难的工作.本文将以一个实际的例子(Tar)来说明如何把Linux代码移植到Windows平台上.移植过程将尽量少修改代码,以便代码的运行逻辑不会发生任何变动.保留绝大部分软件主要功能. 二.准备工作 Tar是Linux平台下面一个

  • Linux内核设备驱动地址映射笔记整理

    #include <asm/io.h> #define ioremap(cookie,size) __arm_ioremap(cookie, size, MT_DEVICE) //cookie表示物理地址, size表示映射大小. ioremap把指定的物理地址映射到空闲的虚拟地址 void __iomem * __arm_ioremap(unsigned long phys_addr, size_t size, unsigned int mtype) { return __arm_iorem

  • Linux内核设备驱动之高级字符设备驱动笔记整理

    /****************** * 高级字符设备驱动 ******************/ (1)ioctl 除了读取和写入设备外,大部分驱动程序还需要另外一种能力,即通过设备驱动程序执行各种类型的硬件控制.比如弹出介质,改变波特率等等.这些操作通过ioctl方法支持,该方法实现了同名的系统调用. 在用户空间,ioctl系统调用的原型是: int ioctl(int fd, unsigned long cmd, ...); fd: 打开的设备文件描述符 cmd: 命令 第三个参数:根据

随机推荐