《Linux 内核设计与实现》12. 内存管理

news2024/11/14 20:40:17

文章目录

    • 获得页
      • 获得填充为 0 的页
      • 释放页
    • kmalloc()
      • gfp_mask 标志
      • kfree()
      • vmalloc()
    • slab 层
      • slab 层的设计
      • slab 分配器的接口
    • 在栈上的静态分配
      • 单页内核栈
    • 高端内存的映射
      • 永久映射
      • 临时映射
    • 每个 CPU 的分配
    • 新的每个 CPU 接口

struct page 结构表示系统中的物理页,位于:include/linux/mm_types.h

image-20230425204231276

  • flag:页的状态。
  • _count:页的引用次数。值为 -1 表示当前内核没有引用这一页,该页可分配。内核调用 page_count() 进行检查。
  • virtual:页的虚拟地址。

由于硬件存在缺陷而引起的内存寻址问题:

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

因为存在这些约束,因此 Linux 主要使用了 4 种分区:

  • ZONE_DMA —— 这个区包含的页能用来执行 DMA 操作。
  • ZONE_DMA32 —— 和 ZOME_DMA 类似,该区包含的页面可以用来执行 DMA 操作;而和 ZONE_DMA 不同之处在于,这些页面只能被 32 位设备访问。在某些体系结构中,该区比 ZONE_DMA 更大。
  • ZONE_NORMAL —— 这个区包含的都是正常能映射的页。
  • ZONE_HIGHEM —— 这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。

区的实际使用和分布都与体系结构有关。

image-20230425210206824

  • Linux 把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。
  • 注意,区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组。

获得页

以页为单位分配内存,分配 2 o r d e r 2^{order} 2order 个物理页,位于:include/linux/gfp.h

alloc_pages(gfp_t gfp_mask, unsigned int order) {
	return alloc_pages_current(gfp_mask, order);
}

将给定的页转为它的逻辑地址:

  • include/linux/mm.h
  • mm/highmem.c
/**
 * page_address - get the mapped virtual address of a page
 * @page: &struct page to get the virtual address of
 *
 * Returns the page's virtual address.
 */
void *page_address(struct page *page) {
	unsigned long flags;
	void *ret;
	struct page_address_slot *pas;

	if (!PageHighMem(page))
		return lowmem_page_address(page);

	pas = page_slot(page);
	ret = NULL;
	spin_lock_irqsave(&pas->lock, flags);
	if (!list_empty(&pas->lh)) {
		struct page_address_map *pam;

		list_for_each_entry(pam, &pas->lh, list) {
			if (pam->page == page) {
				ret = pam->virtual;
				goto done;
			}
		}
	}
done:
	spin_unlock_irqrestore(&pas->lock, flags);
	return ret;
}

上面函数返回一个指针,指向给定物理页当前所在的逻辑地址。若用不到 struct page,可以用:

路径:mm/page_alloc.c

/*
 * Common helper functions.
 */
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	/*
	 * __get_free_pages() returns a 32-bit address, which cannot represent
	 * a highmem page
	 */
	VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

	page = alloc_pages(gfp_mask, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}
EXPORT_SYMBOL(__get_free_pages);

获得填充为 0 的页

带有 zeroed 的函数,会将分配好的页都填充成 0。用户空间的页在返回之前,必须将分配的页填充为 0。

image-20230425212743938

释放页

void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {
		if (order == 0)
			free_hot_cold_page(page, 0);
		else
			__free_pages_ok(page, order);
	}
}

EXPORT_SYMBOL(__free_pages);

void free_pages(unsigned long addr, unsigned int order)
{
	if (addr != 0) {
		VM_BUG_ON(!virt_addr_valid((void *)addr));
		__free_pages(virt_to_page((void *)addr), order);
	}
}

EXPORT_SYMBOL(free_pages);

记住,分配完内存后,必须检测是否分配成功,否则会出现大问题。

kmalloc()

以字节为单位申请一块连续的内核空间。

位于:

  • include/linux/slab.h

  • include/linux/slab_def.h

static __always_inline void *kmalloc(size_t size, gfp_t flags) { ... }

gfp_mask 标志

行为修饰符:

image-20230426011009074

多个行为修饰符可以使用或运算符连接,如:__GFP_WAIT | __GPT_IO | __GFP_FS

区修饰符:

image-20230426011044230

指定从哪个区分配资源,若未指定任何标志,默认从 ZONE_DMA 或 ZONE_NORMAL 分配,优先从 ZONE_NORMAL 分配。

不能给 _get_free_pages() 或 kalloc() 指定 ZONE_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是 page 结构,这两个函数分配的内存当前有可能还没有映射到内核的虚拟地址空间,因此,也可能根本就没有逻辑地址。

类型标志:

image-20230426011156349

通常,要么用 GFP_KERNEL,要么是 GFP_ATOMIC。

image-20230426012014793

kfree()

kmalloc() 所对应的内存释放函数 kfree()。

void kfree(const void *);

kfree() 只能释放由 kmalloc() 分配出来的内存资源。若想要释放的内存不是由 kmalloc() 申请的,或想要释放的内存早被释放了,再调用 kfree() 的话,就会导致很严重的后果。

和用户空间一样,申请和释放函数都是一对的。

vmalloc()

kmalloc() 确保页在物理地址上是连续的(虚拟地址自然也是连续的)。

vmalloc() 只确保页在虚拟地址是连续的。

硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚拟地址连续上的块。应用程序所使用的内存块可以使用只有虚拟地址连续的内存块。

通过 vmalloc() 获得的页必须一个一个地进行映射(因为物理地址可以不连续),这就导致比直接内存映射大得多的 TLB 抖动。

对应释放:vfree()。

路径:

  • include\linux\vmalloc.h
  • mm\vmalloc.c

slab 层

空闲链表:便于数据的频繁分配和回收。当需要内存块时,就从空闲链表中取出一个,不用时就放回去,而不是释放它。

缺点:在内核中,空闲链表无法全局控制。当可用内存变得紧缺时,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放出一些内存。

slab 层,即 slab 分配器来nibu缺陷。

slab 分配器在如下基本原则中寻求一种平衡:

image-20230426193635889

slab 层的设计

一个高速缓存由多个 slab 组成,而一个 slab 由多个同类型对象。

slab 由一个或多个物理上连续的页组成。

slab 的三种状态:

  • 满:没有空闲对象可分配。
  • 部分满:有部分空闲对象可分配。
  • 空:没有一个被分配的对象。

image-20230426194456357

高速缓存的结构:

// include/linux/slab_def.h
struct kmem_cache {
    // ...
    struct kmem_list3 *nodelists[MAX_NUMNODES];
    // ...
}
// mm/slab.c
/*
 * The slab lists for all objects.
 */
struct kmem_list3 {
	struct list_head slabs_partial;	/* partial list first, better asm code */
	struct list_head slabs_full;
	struct list_head slabs_free;
    // ...
}

slab 结构:

// mm/slab.c
/*
 * struct slab
 *
 * Manages the objs in a slab. Placed either at the beginning of mem allocated
 * for a slab, or allocated from an general cache.
 * Slabs are chained into three list: fully used, partial, fully free slabs.
 */
struct slab {
	struct list_head list;   // 三种状态的链表
	unsigned long colouroff; // slab 着色的偏移量
	void *s_mem;		     // 在 slab 中的第一个对象
	unsigned int inuse;	     // slab 中已分配的对象数
	kmem_bufctl_t free;      // 第一个空闲对象
	unsigned short nodeid;
};

slab 描述符要么在 slab 之外另行分配,要么就放在 slab 自身开始的地方。

slab 分配器可以创建新的 slab,这通过 __get_free_pages() 低级内核页分配器进行。

// mm/slab.c
/*
 * Interface to system's page allocator. No need to hold the cache-lock.
 *
 * If we requested dmaable memory, we will get it. Even if we
 * did not request dmaable memory, we might get it, but that
 * would be relatively rare and ignorable.
 */
static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
{
	struct page *page;
	int nr_pages;
	int i;

#ifndef CONFIG_MMU
	/*
	 * Nommu uses slab's for process anonymous memory allocations, and thus
	 * requires __GFP_COMP to properly refcount higher order allocations
	 */
	flags |= __GFP_COMP;
#endif

	flags |= cachep->gfpflags;
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		flags |= __GFP_RECLAIMABLE;

	page = alloc_pages_exact_node(nodeid, flags | __GFP_NOTRACK, cachep->gfporder);
	if (!page)
		return NULL;

	nr_pages = (1 << cachep->gfporder);
	if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
		add_zone_page_state(page_zone(page),
			NR_SLAB_RECLAIMABLE, nr_pages);
	else
		add_zone_page_state(page_zone(page),
			NR_SLAB_UNRECLAIMABLE, nr_pages);
	for (i = 0; i < nr_pages; i++)
		__SetPageSlab(page + i);

	if (kmemcheck_enabled && !(cachep->flags & SLAB_NOTRACK)) {
		kmemcheck_alloc_shadow(page, cachep->gfporder, flags, nodeid);

		if (cachep->ctor)
			kmemcheck_mark_uninitialized_pages(page, nr_pages);
		else
			kmemcheck_mark_unallocated_pages(page, nr_pages);
	}

	return page_address(page);
}

slab 层的管理就是在每个高速缓存的基础上,通过提供给整个内核一个简单的接口来完成的。通过接口就可以创建和撤销新的高速缓存,并在高速缓存内分配和释放对象。高速缓存及其内 slab 的复杂管理完全通过 slab 层的内部机制来处理。当你创建了一个高速缓存后,slab 层所起的作用就像一个专门的分配器,可以为具体的对象类型进行分配。

slab 分配器的接口

连续上了一个下午的讲座,头疼,不记了,艹!

在栈上的静态分配

单页内核栈

进程设置为单页内核栈的好处:

  • 可以让每个进程减少内存消耗。
  • 随着时间的推移,避免因为物理内存碎片化而造成给新进程分配两个未分配、连续的页变得困难。

中断栈:

  • 中断栈为每个进程提供一个用于中断处理程序的栈。
  • 这样中断处理程序就不用再和被中断的进程共享同一个内核栈了,它们可以使用自己的栈了。

高端内存的映射

在高端内存中的页不能永久映射到内核地址空间上。因此通过 alloc_pages() 以 __GFP_HIGHMEM 标志获得的页不可能有逻辑地址。

x86 的高端内存中的页被映射到 3GB~4GB。

永久映射

// linux/high.mem.h
// 将 page 映射到内核地址空间
void *kmap(struct page *page)
// 对应的解除映射
void kunmap(struct page *page)
  • 高端或低端内存都可以使用。
  • 若 page 对应低端那一页,返回该页虚拟地址。若是高端,则建立一个永久映射,返回物理地址。
  • 该函数可休眠,因此只能用在进程上下文中。

临时映射

void *kmap_atomic(struct page *page, enum km_type type)
void *kunmap_atomic(struct page *kvaddr, enum km_type type)
  • 该函数不可休眠。
  • type 描述了临时映射的目的。

每个 CPU 的分配

  • 支持 SMP 的现代操作系统使用每个 CPU 上的数据,对于给定的处理器其数据是唯一的。
  • 一般来说,每个 CPU 的数据存放在一个数组中。数组中的每一项对应着系统上的一个处理器。

声明数据:

unsigned long my_percpu[NR_CPUS];

按照如下方式访问:

int cpu;
cpu = get_cpu(); // 获得当前处理器,并禁止内核抢占
my_percpu[cpu]++; // ...
printk("my_percpu on cpu=%d is %lu\n", cpu, my_percpu[cpu]);
put_cpu(); // 激活内核抢占

因为所操作的数据对当前处理器来说是唯一的。没有其它处理器可以访问到该数据,不存在并发访问的问题,因此当前处理器可以在不用锁的情况下安全访问它。

内核抢占引起的问题:

  • 如果你的代码被其他处理器抢占被重新调度,那么这时 CPU 变量就会无效,因为它指向的是错误的处理器(通常,代码获得当前处理器后是不可休眠的)。
  • 如果另一个任务抢占了你的代码,那么有可能在同一个处理器上发生并发访问 my_percpu 的情况,显然这属于一个竞争条件。

使用下面的方法来保护数据安全,就不需要自己手动禁止内核抢占:

  • get_cpu():获得当前处理器号。该函数会禁止内核抢占。
  • put_cpu():重新激活当前处理器号。

新的每个 CPU 接口

略…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/505582.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

区间预测 | MATLAB实现QRCNN卷积神经网络分位数回归时间序列区间预测

区间预测 | MATLAB实现QRCNN卷积神经网络分位数回归时间序列区间预测 目录 区间预测 | MATLAB实现QRCNN卷积神经网络分位数回归时间序列区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 区间预测 | MATLAB实现QRCNN卷积神经网络分位数回归时间序列区间预测…

超级详细的 FinalShell 安装 及使用教程

一、引言 FinalShell 是一款免费的国产的集 SSH 工具、服务器管理、远程桌面加速的良心软件&#xff0c;同时支持 Windows,macOS,Linux&#xff0c;它不单单是一个 SSH 工具&#xff0c;完整的说法应该叫一体化的的服务器&#xff0c;网络管理软件&#xff0c;在很大程度上可以…

新唐NUC980使用记录(5.10.y内核):在用户应用中使用GPIO

文章目录 目的使用参考与演示使用参考存在的问题问题定位修改设备树使用测试 总结设备树文件内容 目的 GPIO是最基础的外设&#xff0c;使用频率也非常高&#xff0c;这篇文章将简单体验在NUC980 Liunx用户应用中使用GPIO功能。 这篇文章中内容均在下面的开发板上进行测试&am…

【java EE】Redis基础

Redis基础 业务中会遇到的问题&#xff1a; 数据量巨大数据模式的不确定性数据的频繁读数据的频繁更改大量数据的统计分析 集中数据库的特点 Redis简介&#xff1a; Redis&#xff08;Remote Dictionary Server&#xff09;是一个使用ANSI C语言编写的开源数据库高性能的 …

解密Web自动化测试:你需要了解的四大难点

B站首推&#xff01;2023最详细自动化测试合集&#xff0c;小白皆可掌握&#xff0c;让测试变得简单、快捷、可靠https://www.bilibili.com/video/BV1ua4y1V7Db 目录 前言 问题1&#xff1a;测试稳定性 问题2&#xff1a;测试可维护性 问题3&#xff1a;测试性能 问题4&am…

阿里熔断限流框架Sentinel实现流程和动态规则数据源

文章目录 1.简单介绍2.使用示例3.主要实现原理和组成部分4.动态规则数据源 本篇文章主要介绍熔断限流框架Sentinel的使用示例、组成原理和动态规则数据源的实现原理。 1.简单介绍 阿里的熔断限流框架Sentinel基于滑动时间窗口实现熔断限流管控的&#xff0c;支持多样的管控场景…

PHP 基础入门

目录 1、标记 2、注释 3、输出语句 4、关键字 5、常量的定义与使用 6、预定义常量 7、变量的赋值&#xff08;传参赋值与引用赋值&#xff09; 8、可变变量 9、双引号和单引号的区别 10、heredoc结构和nowdoc结构 11、其他符号 1、标记 <?php 和 ?> 是PHP标…

【MySQL】EXPLAIN 语句 各字段 详解

EXPLAIN 语句 概貌 在连接查询的执行计划中&#xff1a; 每个表都会对应一条记录&#xff0c;这些记录的 id 列的值是相同的&#xff1b; 在包含子查询的执行计划中 &#xff1a;每个 select关键字都会对应一个唯一的 id 值。 驱动表&#xff1a;出现在前面的表&#xff1b; …

Apache ECharts 一个基于 JavaScript 的开源可视化图表库

一&#xff1a; ECharts 特性 ECharts&#xff0c;一个使用 JavaScript 实现的开源可视化库&#xff0c;可以流畅的运行在 PC 和移动设备上&#xff0c;兼容当前绝大部分浏览器&#xff08;IE9/10/11&#xff0c;Chrome&#xff0c;Firefox&#xff0c;Safari等&#xff09;&a…

谷歌正在向所有账户推出密码终止技术

谷歌宣布让其个人帐户持有人使用称为“密码”的密码替代登录的一项重大努力。 该功能面向公司的数十亿帐户推出&#xff0c;用户将能够主动寻找并启用它。谷歌表示&#xff0c;它计划在未来几个月推广密码&#xff0c;并开始推动账户持有人将他们传统的用户名和密码登录转换为…

vscode 远程开发:免密登入设置

文章目录 1. vscode 安装2. vscode 插件安装&#xff08;1&#xff09; 中文界面设置&#xff08;2&#xff09; ssh远程插件安装 3. 免密登入 1. vscode 安装 vscode 官网下载地址&#xff1a;https://code.visualstudio.com/ 安装很简单&#xff1a; 可以默认方式&#xff0…

新建一台VMware虚拟机

文章目录 前言一、问题二、步骤1.确认已安装VMware Workstation&#xff0c;已下载Windows 10 光盘镜像2.新建虚拟机pc13.自定义虚拟机pc1的硬件 总结 前言 新建一台VMware虚拟机。 一、问题 本例要求在VMware Workstation软件中创建一台新虚拟机&#xff0c;相关说明如下。 …

Go语言字符串基础

目录 字符串基础 合并和分割字符串 分割 合并 判断是否包含 strings.Contains() 查找子串出现的位置 strings.Index() strings.LastIndex() 字符串基础 1.字符串是由一串Unicode字符组成的序列&#xff0c;每个Unicode字符都占用一个或多个字节的存储空间。 2.字符串…

【腾讯云 Finops Crane 集训营】老板喜欢降本增效?学会 Crane,让腾讯每月省千万的奇迹在你手中上演

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…

SpringBoot 简单多模块构建

前言 SpringBoot系列到现在虽然代码不多&#xff0c;但是感觉结构很乱&#xff0c;随着项目的复杂性提高&#xff0c;代码会越来越臃肿&#xff0c;耦合性高。 所以SpringBoot多模块很有必要&#xff0c;简单来说就是由以前按包分模块变为jar包分模块。在多模块jar模式下可以将…

【勝讯云 Finops Crane 集训营】之集群优化实战

重要通知 由腾讯云联合 CSDN 推出的“腾讯云 Finops Crane 开发者集训营”活动&#xff0c;主要面向广大开发者&#xff0c;旨在通过线上直播、组织动手实验、有奖征文&#xff0c;开源项目贡献者招募这一系列技术实践活动中既能通过活动对 Finops Crane 开源项目有一个深入的]…

mathtype不激活能用吗 mathtype产品密钥如何取得

在文档中输入数学式子时一般会用到mathtype&#xff0c;虽然mathtype为广大用户提供了一定期限的试用期&#xff0c;但试用期后如果没有成为正式用户&#xff0c;那么部分功能可能就用不了了。有些小伙伴可能会对mathtype不激活能用吗&#xff0c;mathtype产品密钥如何取得这两…

PostGIS五分钟入门【空间数据库】

在本文中&#xff0c;我们将介绍 PostGIS 的一些基础知识及其功能&#xff0c;以及一些可用于简化解决方案或提高性能的提示和技巧。 推荐&#xff1a;用 NSDT设计器 快速搭建可编程3D场景。 简而言之 - PostGIS 是一个 Postgres 扩展&#xff0c;增加了对存储和操作空间数据类…

PyQt5桌面应用开发(11):摸鱼也要讲基本法之桌面精灵

本文目录 PyQt5桌面应用系列鼠标不要钱&#xff0c;手腕还不要钱吗&#xff1f;PyQt5源程序python文件资源定义界面定义文件 技术要素资源文件StyleSheetsQMainWindow设置窗体几何 结论 PyQt5桌面应用系列 PyQt5桌面应用开发&#xff08;1&#xff09;&#xff1a;需求分析 PyQ…

orbslam3 编译时 Thirdparty sophus 库多种错误 redefinition, not declared in this scope

问题 在装了 ROS 的机器人系统里编译 orbslam3 时, 发现 Thirdparty sophus 库密集报错, 导致 orbslam3 无法完成编译 排查 同样的代码在装了 ROS 的笔记本 ubuntu18.04 系统里可以成功通过编译, 但是在装了同版本 ROS 的机器人 ubuntu18.04 系统里无法编译 Sophus 库本身…