Linux驱动开发进阶(四)- 内存管理

文章目录

1、前言

  1. 学习参考书籍以及本文涉及的示例程序:李山文的《Linux驱动开发进阶》
  2. 本文属于个人学习后的总结,不太具备教学功能。

2、内存管理机制

对于没有MMU的计算机而言,有如下两种常见的方式:位图和链表。

2.1、基于位图的内存管理

即使用bitmap来管理一个内存池。核心思想就是用1bit表示1个内存块的状态,0表示空闲,1表示已占用。假设系统有16个内存块,用两个字节的bitmap来表示就是:0010 1100 0001 1111,即第3、5、6、10、12~16 块已占用,其余空闲。使用bitmap管理小内存块时,其bitmap占用率就会比较高,导致内存利用率变低。

bitmap自身占用计算:

2.2、基于链表的内存管理

使用链表节点来记录内存块信息,如起始地址、大小、使用状态。每分配一个内存块就会产生一个节点,相邻的空闲节点会被合并成一块空闲节点。基于链表的内存一样有缺陷,当频繁分配/释放不同的大小块时,导致空闲内存分散。

2.3、伙伴算法(buddy)

buddy算法可以有效解决上面两种方法的缺点,即不易产生外部碎片,分配和释放内存的速度快,但容易产生内部碎片。可自行查阅了解。

3、MMU

MMU(Memory Management Unit)即内存管理单元。核心功能就是负责虚拟地址和物理地址之间的转换。

首先我们得明确一个结论,无论在用户态使用malloc()还是内核态使用kmalloc(),所返回的地址都是虚拟的,我们是不可能直接通过物理地址操作物理内存的,必须要通过虚拟地址访问物理内存,也就是必须经过MMU。

在32位处理器架构中,每个应用程序(进程)可以访问的虚拟内存空间为 4GB(即 232232 字节),但这并不意味着物理内存真有4GB,而是通过虚拟内存技术实现的抽象。

MMU如何通过虚拟地址找到物理地址?回答是通过页表的方式。

3.1、一级页表

低12位为页内偏移,高20位为虚拟页号。(图片来自李山文的《Linux驱动开发进阶》)

3.1、二级页表

二级页表中,将VPN拆分成了PDE和PTE。(图片来自李山文的《Linux驱动开发进阶》)

PDE(Page Directory Entry):页目录项,用来定位页表(PTE数组)。

PTE(Page Table Entry):页表项,定位物理页帧。

只有一个页目录(Page Directory),一个页目录包含1024个PDE。

每个有效的PDE指向一个页表(Page Table),一个页表包含1024个PTE。

所以理论上有:1024个PDE * 1024个PTE = 1M个PTE。实际上系统会按需分配页表,未使用的虚拟地址区域不分配PTE。

4、内存布局

4.1、内存地址映射

下图是x86架构32位系统中物理地址与虚拟地址映射图。4G的用户空间被划分为1GB内核空间+3GB用户空间。(图片来自李山文的《Linux驱动开发进阶》)

我们先来理解一下这3GB的用户空间和1GB的内核空间到底是干嘛的。首先它们都是虚拟地址。

用户空间的3GB:用于存放应用程序的代码和数据,如代码段,数据段,堆,栈,内存映射(mmap)。

内核空间的1GB:内核代码和数据,设备寄存器(ioremap)和DMA缓冲区,物理内存的直接映射。

而对于1GB的内核空间来说,其被分为三种不同的区域,分别是ZONE_DMA(16MB)ZONE_NORMAL(880M)ZONE_HIGHMEM(128MB),其中ZONE_DMA又被称为ZONE_LOWMEM

我们重点来看看ZONE_HIGHMEM。该区域是一个特殊的内存管理区域,专门用于处理物理内存超过896MB的部分。它的存在是32位架构地址空间限制下的妥协方案,核心目的是让内核能够访问所有物理内存,尽管虚拟地址空间不足。

假设物理内存内有2GB,内核空间如何访问物理内存的2GB-896M=1152MB?答案是,将超出896M的内存划为ZONE_HIGHMEM,动态按需映射。

而64位系统取消了ZONE_HIGHMEM,原因是64位系统可表示的虚拟地址空间极大,其内核空间和用户空间都是128TB,内核可以直接线性映射所有物理内存。下图是64位系统虚拟内存映射:(图片来自李山文的《Linux驱动开发进阶》)

5、内存分配

在内核中,主要使用kmalloc和vmalloc分配内存。这两种内存分配方式所使用的内存分配器是不同的。kmalloc使用slab分配器,vmalloc使用buddy分配器(伙伴算法)。

kmalloc:

  • 分配物理地址连续的内存块(虚拟内存自然也是连续的)
  • 适用于小内存分配(几字节到几MB)

vmalloc:

  • 分配虚拟内存连续但物理内存不一定连续的内存块
  • 适用于大内存分配

下图展示了malloc、kmalloc、vmalloc分配内存函数的比较:(图片来自李山文的《Linux驱动开发进阶》)

5.1、页分配器

在 Linux 内核中,__alloc_pages 是最底层的页框分配函数,直接由 Buddy System(伙伴系统) 实现,用于分配连续的物理页框。它是内核内存分配的核心,kmallocvmalloc 等高层接口最终都会调用它。

函数原型:

c 复制代码
struct page *__alloc_pages(gfp_t gfp_mask, unsigned int order, 
                           struct zonelist *zonelist, nodemask_t *nodemask);

参数:

  • gfp_mask:分配标志(如 GFP_KERNELGFP_ATOMIC),控制分配行为(是否可睡眠、内存区域等)。
  • order:请求的页数(2^order 页),如 order=0 表示 1 页(4KB),order=3 表示 8 页(32KB)。
  • zonelist:允许分配的内存区域列表(如 ZONE_NORMALZONE_DMA)。
  • nodemask:NUMA 节点掩码(指定从哪个物理节点分配)。

返回值:成功时返回指向第一个 struct page 的指针,失败返回 NULL

5.2、slab分配器

页分配器每次分配一个页框,默认为4K。slab分配器原理是在页分配器的基础上再细分,将4K的页划分为4个1K的chunk内存块,然后再将1K大小的chunk内存再次划分为更小的slab内存块。

5.3、kmalloc和vmalloc

在 Linux 内核中,kmallocvmalloc 是两种不同的内存分配机制,它们的主要区别在于:

  1. 内存来源:kmalloc 使用直接映射区(线性映射区),而 vmalloc 使用动态映射区(非连续内存区)。
  2. 物理地址连续性:kmalloc 保证物理地址连续,vmalloc 不保证。
  3. 适用场景:kmalloc 适合小内存、高频分配,vmalloc 适合大内存、非频繁操作。
  4. 在x86架构上,kmalloc函数可以分配最大连续内存块大小默认为128KB(可以通过内核配置修改)。vmalloc默认的最大连续内存块大小是通常的体系结构的页大小,一般为4KB(可以通过内核配置修改)。

(下图来自李山文的《Linux驱动开发进阶》)

5.4、mmap机制

5.4.1、共享文件映射

略,自主查阅。

共享文件映射用的最多。主要目的是将文件映射到内存中,不适合用read/write操作,如控制lcd设备节点/dev/fb0。

5.4.2、共享匿名映射

略,自主查阅。

主要用于进程间共享内存。操作/dev/zero设备节点,并对其mmap。另一个线程也是如此。但是要注意临界资源的访问。

5.4.3、私有文件映射

略,自主查阅。

5.4.4、私有匿名映射

略,自主查阅。

5.4.5、内核中的mmap

下面示例程序来自李山文的《Linux驱动进阶开发》,展示了在内核中如何实现mmap驱动。

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>       
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>   
#include <linux/mm.h>
#include <linux/slab.h>

struct dummy_test {
    struct device *dev;
    void          *buffer_addr;
    dev_t         dummy_dev_num;
    struct cdev   dummy_cdev;
    struct class  *dummy_class;
    struct device *dummy_dev;
    //...
};

static void dev_test_release(struct device *dev)
{

}

//作者这里增加一个设备仅仅是为了方便管理结构体
static struct device dev_mmap_test = {
    .init_name  = "mmap_test",
    .release    = dev_test_release,
};

static int dummy_open(struct inode *inode, struct file *filep)
{
    filep->private_data = container_of(inode->i_cdev, struct dummy_test, dummy_cdev);
    return 0;
}

static int dummy_mmap(struct file *file, struct vm_area_struct *vma)
{
    struct dummy_test *demo = file->private_data;
	unsigned long start = vma->vm_start;
	unsigned long size = vma->vm_end - vma->vm_start;
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long phy;

	dev_dbg(demo->dev, "create mmap region\n");

    //合法性检查
	if ((size != PAGE_SIZE) || (offset & ~PAGE_MASK)) {
		dev_err(demo->dev, "invalid params for mmap region\n");
		return -EINVAL;
	}
	/* 获得物理地址 */
	phy = virt_to_phys(demo->buffer_addr);

	/* 是否使用 cache, buffer */
	vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

	/* 开始创建映射内存区域 */
	if (remap_pfn_range(vma, start, phy >> PAGE_SHIFT, size, vma->vm_page_prot)) {
		printk(KERN_ERR "mmap: remap_pfn_range failed\n");
		return -ENOBUFS;
	}

	return 0;
}

static ssize_t dummy_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    int ret;
    struct dummy_test *demo = file->private_data;
    ret = copy_to_user(buf, demo->buffer_addr, count&(PAGE_SIZE-1));
    if( ret != 0) {
    	return -EFAULT;
    }
    return count&(PAGE_SIZE-1);
}

static ssize_t dummy_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    int ret;
    struct dummy_test *demo = file->private_data;
    ret = copy_from_user(demo->buffer_addr, buf, count&(PAGE_SIZE-1));
    if( ret != 0) {
    	return -EFAULT;
    }
    return count&(PAGE_SIZE-1);
}

static int dummy_close(struct inode *inode, struct file *filep)
{
    filep->private_data = NULL;
    return 0;
}

static const struct file_operations dummy_fops = {
	.owner		= THIS_MODULE,
	.open		= dummy_open,
    .read       = dummy_read,
    .write      = dummy_write,
    .mmap       = dummy_mmap,
	.release	= dummy_close,
};

static ssize_t mmap_data_read(struct device *dev,
		struct device_attribute *attr, char *buf)
{
    struct dummy_test *demo = dev_get_drvdata(dev);
	return sprintf(buf, "%s", (char*)demo->buffer_addr);
}

static ssize_t mmap_data_write(struct device *dev,
		struct device_attribute *attr, const char *buf, size_t size)
{
    struct dummy_test *demo = dev_get_drvdata(dev);
	sprintf(demo->buffer_addr, "%s", buf);
	return size;
}
static DEVICE_ATTR(data, 0644, mmap_data_read, mmap_data_write);

static int __init dymmy_mmap_init(void)
{
    int ret;
    struct device *dev = &dev_mmap_test;
    struct dummy_test *demo = NULL;
    ret = device_register(&dev_mmap_test);
    if(ret)
    {
        printk(KERN_ERR "dev_mmap_test register error!\n");
        return ret;        
    }
    demo = kmalloc(sizeof(struct dummy_test), GFP_KERNEL);
    if (demo == NULL) {
        dev_err(dev, "alloc buffer failed!\n");
        return -1;
    }
    ret = alloc_chrdev_region(&demo->dummy_dev_num ,0, 1, "dummy");  //动态申请一个设备号
    if(ret !=0) {
        dev_err(dev, "alloc_chrdev_region failed!\n");
        return -1;
    }
    demo->dummy_cdev.owner = THIS_MODULE;
    cdev_init(&demo->dummy_cdev, &dummy_fops);
    cdev_add(&demo->dummy_cdev, demo->dummy_dev_num, 1);
    demo->dummy_class = class_create(THIS_MODULE, "dummy_class");
    if(demo->dummy_class == NULL) {
        dev_err(dev, "dummy_class failed!\n");
        return -1;
    }
    demo->dummy_dev = device_create(demo->dummy_class, NULL, demo->dummy_dev_num, NULL, "dummy");
    if(IS_ERR(demo->dummy_dev)) {
        dev_err(dev, "device_create failed!\n");
        return -1;
    }
    demo->buffer_addr = kmalloc(PAGE_SIZE, GFP_KERNEL);
    if(demo->buffer_addr == NULL) {
        dev_err(dev, "alloc file buffer failed!\n");
        return -1;
    }
    dev_set_drvdata(dev, demo);
    demo->dev = dev;
    device_create_file(dev, &dev_attr_data);
    return 0;
}

static void __exit dymmy_mmap_exit(void)
{
    struct dummy_test *demo = dev_get_drvdata(&dev_mmap_test);
    cdev_del(&demo->dummy_cdev);
    unregister_chrdev_region(demo->dummy_dev_num, 1);
    device_destroy(demo->dummy_class, demo->dummy_dev_num);
    class_destroy(demo->dummy_class);
    kfree(demo);
    device_unregister(&dev_mmap_test);
}

module_init(dymmy_mmap_init);
module_exit(dymmy_mmap_exit);

MODULE_LICENSE("GPL");      
MODULE_AUTHOR("[email protected]");   
MODULE_VERSION("0.1");          
MODULE_DESCRIPTION("mmap test"); 

重点的东西有两个,都集中在dummy_mmap()函数中,一个是形参struct vm_area_struct结构体,用来描述用户进程虚拟内存区域的结构体。一个是remap_pfn_range()函数,该函数将物理地址映射到用户虚拟地址空间:

c 复制代码
static int dummy_mmap(struct file *file, struct vm_area_struct *vma)
{
    ...

	/* 开始创建映射内存区域 */
	if (remap_pfn_range(vma, start, phy >> PAGE_SHIFT, size, vma->vm_page_prot)) {
		printk(KERN_ERR "mmap: remap_pfn_range failed\n");
		return -ENOBUFS;
	}

	return 0;
}

下面是对应的应用程序:

c 复制代码
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int fd;
    char *map;
    // 打开文件
    fd = open(argv[1], O_RDWR);
    if (fd == -1) {
        perror("open failed\n");
        return 1;
    }
    // 创建内存映射, 注意:这里映射一个内核PAGE内存,即4096字节
    map = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) {
        perror("mmap failed\n");
        return 1;
    }
    // 对映射的文件进行操作
    printf("data: %s\n", map);
    // 修改映射的文件内容
    map[0] = 'H';
    map[1] = 'e';
    map[2] = 'l';
    map[3] = 'l';
    map[4] = 'o';
    // 解除内存映射
    if (munmap(map, 4096) == -1) {
        perror("munmap failed\n");
        return 1;
    }
    // 关闭文件
    if (close(fd) == -1) {
        perror("close failed\n");
        return 1;
    }
    return 0;
}

加载ko文件,首先使用属性文件进行读写测试:

使用应用程序对设备节点进行读写测试:

6、DMA

  1. 申请dma通道
c 复制代码
/**
 * dma_request_chan - try to allocate an exclusive slave channel
 * @dev:	pointer to client device structure
 * @name:	slave channel name
 *
 * Returns pointer to appropriate DMA channel on success or an error pointer.
 */
struct dma_chan *dma_request_chan(struct device *dev, const char *name)
  1. 申请用于DMA传输的内存。在上面我们了解过内核空间中有一块ZONE_DMA区域,我们需要使用专门的函数来申请DMA内存。

申请内存一致性DMA映射:

c 复制代码
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
		dma_addr_t *dma_handle, gfp_t gfp)
{

	return dma_alloc_attrs(dev, size, dma_handle, gfp,
			(gfp & __GFP_NOWARN) ? DMA_ATTR_NO_WARN : 0);
}

申请流式DMA映射:

c 复制代码
dma_addr_t dma_map_single(struct device *dev, void *ptr,
                         size_t size, enum dma_data_direction dir)

申请分散聚合DMA映射:

c 复制代码
// struct scatterlist 用来描述内核中的分散聚合表
dma_addr_t dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,
                         enum dma_data_direction dir)
  1. 配置DMA
c 复制代码
static inline int dmaengine_slave_config(struct dma_chan *chan,
					  struct dma_slave_config *config)
{
	if (chan->device->device_config)
		return chan->device->device_config(chan, config);

	return -ENOSYS;
}
  1. 获取DMA描述符

获取流式DMA映射传输描述符:

c 复制代码
static inline struct dma_async_tx_descriptor *dmaengine_prep_slave_single(
	struct dma_chan *chan, dma_addr_t buf, size_t len,
	enum dma_transfer_direction dir, unsigned long flags)
{
	struct scatterlist sg;
	sg_init_table(&sg, 1);
	sg_dma_address(&sg) = buf;
	sg_dma_len(&sg) = len;

	if (!chan || !chan->device || !chan->device->device_prep_slave_sg)
		return NULL;

	return chan->device->device_prep_slave_sg(chan, &sg, 1,
						  dir, flags, NULL);
}

获取分散聚合DMA映射传输描述符:

c 复制代码
static inline struct dma_async_tx_descriptor *dmaengine_prep_slave_sg(
	struct dma_chan *chan, struct scatterlist *sgl,	unsigned int sg_len,
	enum dma_transfer_direction dir, unsigned long flags)
{
	if (!chan || !chan->device || !chan->device->device_prep_slave_sg)
		return NULL;

	return chan->device->device_prep_slave_sg(chan, sgl, sg_len,
						  dir, flags, NULL);
}

获取内存一致性DMA映射传输描述符:

c 复制代码
static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic(
		struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,
		size_t period_len, enum dma_transfer_direction dir,
		unsigned long flags)
{
	if (!chan || !chan->device || !chan->device->device_prep_dma_cyclic)
		return NULL;

	return chan->device->device_prep_dma_cyclic(chan, buf_addr, buf_len,
						period_len, dir, flags);
}
  1. 提交DMA描述符
c 复制代码
static inline dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc)
{
	return desc->tx_submit(desc);
}
  1. 开启数据传输
c 复制代码
/**
 * dma_async_issue_pending - flush pending transactions to HW
 * @chan: target DMA channel
 *
 * This allows drivers to push copies to HW in batches,
 * reducing MMIO writes where possible.
 */
static inline void dma_async_issue_pending(struct dma_chan *chan)
{
	chan->device->device_issue_pending(chan);
}

6.1、示例

书籍的DMA示例程序可以参考李山文的《Linux驱动进阶开发》

后续我会基于这篇《在ARM Linux应用层下使用SPI驱动WS2812》文章,实现在Linux应用层下使用SPI + DMA的方式驱动WS2812。

相关推荐
musk121225 分钟前
vim 中快速删除引号中的字符串
linux·编辑器·vim
让子弹飞021 小时前
15.1linux设备树下的platform驱动编写(知识)_csdn
linux·ubuntu·platform·stm32mp157·驱动的分离和分层
心灵宝贝1 小时前
openssl-1.0.1e.tar.gz编译安装步骤
linux·运维·服务器
菜鸟xy..3 小时前
麒麟系统桌面版本v10安装教程
linux·运维·服务器·虚拟机·安装教程·麒麟
什么半岛铁盒3 小时前
存储基石:深度解读Linux磁盘管理机制与文件系统实战
linux·运维·服务器
w23617346014 小时前
Linux常用基础命令应用
linux·服务器·php
White の algo4 小时前
【Linux系统】linux下的软件管理
linux·运维·服务器
松树戈4 小时前
Ubuntu挂载HDD迁移存储PostgreSQL数据
linux·ubuntu·postgresql
矛取矛求4 小时前
Linux 系统安装与优化全攻略:打造高效开发环境
linux·运维·服务器