Linux内核brk系统调用深度解析:堆内存管理的设计与实现

前言

在Linux操作系统的内存管理体系中,堆空间的动态管理是支撑应用程序灵活内存需求的核心机制。brk系统调用作为这一机制的基石,通过精细的地址空间操作和资源管理,为进程提供了高效、安全的内存扩展与收缩能力。本文将以Linux 2.6.10内核为例,深入剖析brk系统调用的完整实现链路,从用户态接口到内核态执行的每一个关键环节。

文章将系统性地解析三个核心函数组成的调用栈:sys_brk ------作为系统调用的入口点,负责参数验证和基本逻辑控制;find_vma_intersection ------承担地址空间冲突检测的关键职责,确保内存映射的完整性;do_brk------执行实际的堆空间扩展操作,涉及虚拟内存区域的创建、合并和资源记账。通过对这一完整调用路径的逐层分析,我们将揭示Linux内核如何在保证系统稳定性的前提下,实现堆内存的高效动态管理,以及如何通过精细的锁机制、资源限制检查和性能优化策略,在功能性与安全性之间达到精妙平衡。

堆空间的动态扩展和收缩sys_brk

c 复制代码
asmlinkage unsigned long sys_brk(unsigned long brk)
{
	unsigned long rlim, retval;
	unsigned long newbrk, oldbrk;
	struct mm_struct *mm = current->mm;

	down_write(&mm->mmap_sem);

	if (brk < mm->end_code)
		goto out;
	newbrk = PAGE_ALIGN(brk);
	oldbrk = PAGE_ALIGN(mm->brk);
	if (oldbrk == newbrk)
		goto set_brk;

	/* Always allow shrinking brk. */
	if (brk <= mm->brk) {
		if (!do_munmap(mm, newbrk, oldbrk-newbrk))
			goto set_brk;
		goto out;
	}

	/* Check against rlimit.. */
	rlim = current->signal->rlim[RLIMIT_DATA].rlim_cur;
	if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
		goto out;

	/* Check against existing mmap mappings. */
	if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
		goto out;

	/* Ok, looks good - let it rip. */
	if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
		goto out;
set_brk:
	mm->brk = brk;
out:
	retval = mm->brk;
	up_write(&mm->mmap_sem);
	return retval;
}

函数功能分析

sys_brk函数用于调整进程的数据段(heap)大小,是内存管理中的核心系统调用,负责堆空间的动态扩展和收缩。

内存描述符获取与锁保护

c 复制代码
struct mm_struct *mm = current->mm;
down_write(&mm->mmap_sem);
  • 当前进程内存结构:获取当前进程的内存管理描述符
  • 写信号量保护 :使用mmap_sem写锁保护内存结构,防止并发修改

地址有效性验证

c 复制代码
if (brk < mm->end_code)
	goto out;
  • 代码段保护 :确保新的brk值不低于代码段结束地址
  • 内存布局完整性:维护进程地址空间的基本布局规范

地址对齐处理

c 复制代码
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
	goto set_brk;
  • 页面边界对齐 :将brk地址按页面大小对齐,确保内存映射的规范性
  • 无变化优化 :如果新旧brk值相同,直接跳转到设置阶段,避免不必要的操作

堆空间收缩处理

c 复制代码
if (brk <= mm->brk) {
	if (!do_munmap(mm, newbrk, oldbrk-newbrk))
		goto set_brk;
	goto out;
}
  • 收缩优先检查:首先处理堆空间收缩的情况
  • 内存取消映射 :调用do_munmap释放不再需要的堆内存页面
  • 操作结果验证 :如果取消映射成功,继续设置新的brk

资源限制检查

c 复制代码
rlim = current->signal->rlim[RLIMIT_DATA].rlim_cur;
if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim)
	goto out;
  • RLIMIT_DATA获取:从进程信号结构中获取数据段资源限制
  • 限制有效性检查:确保资源限制不是无限值
  • 大小超限验证:检查新的堆大小是否超过资源限制

地址空间冲突检测

c 复制代码
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
	goto out;
  • VMA重叠检查 :查找是否在旧brk到新brk扩展区域存在已有的VMA映射
  • 地址空间冲突预防:防止堆扩展覆盖现有的内存映射区域

堆空间扩展执行

c 复制代码
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
	goto out;
  • 实际扩展操作 :调用do_brk函数执行堆空间的实际扩展
  • 返回值验证:检查扩展操作是否成功,失败则跳转到退出路径

brk值更新与清理

c 复制代码
set_brk:
	mm->brk = brk;
out:
	retval = mm->brk;
	up_write(&mm->mmap_sem);
	return retval;
  • brk值设置 :成功更新进程的brk指针
  • 返回值准备 :返回设置后的brk
  • 锁释放:释放内存管理信号量,允许其他内存操作

函数功能总结

sys_brk系统调用是Linux进程堆内存管理的核心接口,通过精细的边界检查、资源限制验证和地址空间冲突检测,安全地实现堆空间的动态调整。整个处理流程涵盖了从参数验证、权限检查到实际内存操作的全过程,确保了进程堆空间扩展和收缩的可靠性与安全性。

检查在给定的地址范围内是否已经存在内存映射find_vma_intersection

c 复制代码
/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
   NULL if none.  Assume start_addr < end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
	struct vm_area_struct * vma = find_vma(mm,start_addr);

	if (vma && end_addr <= vma->vm_start)
		vma = NULL;
	return vma;
}

该函数用于查找第一个与指定地址区间[start_addr, end_addr-1]相交的VMA(虚拟内存区域),如果没有找到则返回NULL。前提条件是start_addr < end_addr,即起始地址必须小于结束地址。这个函数主要用于检查在给定的地址范围内是否已经存在内存映射。

函数功能分析

find_vma_intersection函数用于在进程的地址空间中查找与指定区间存在重叠的第一个虚拟内存区域,是内存映射冲突检测的核心工具。

初始VMA查找

c 复制代码
struct vm_area_struct * vma = find_vma(mm,start_addr);
  • 基础查找 :调用find_vma函数查找包含或紧接start_addr之后的第一个VMA
  • 查找算法:利用红黑树优化查找性能,时间复杂度O(log n)
  • 返回结果 :返回的VMA可能是:
    • 完全包含start_addr的VMA
    • 第一个起始地址大于start_addr的VMA
    • NULL(如果没有任何VMA在start_addr之后)

区间重叠验证

c 复制代码
if (vma && end_addr <= vma->vm_start)
	vma = NULL;
  • 重叠条件检查:验证找到的VMA是否确实与目标区间重叠
  • 无重叠情况:如果目标区间的结束地址小于等于VMA的起始地址,说明没有重叠
  • 重叠判断逻辑
    • 有重叠:vma->vm_start < end_addrstart_addr < vma->vm_end
    • 无重叠:end_addr <= vma->vm_start(目标区间完全在VMA之前)

结果返回

c 复制代码
return vma;
  • 有效VMA:如果找到重叠的VMA,返回该VMA指针
  • NULL返回:如果没有重叠的VMA,返回NULL
  • 调用方处理:调用者根据返回值判断地址区间是否可用

函数功能总结

find_vma_intersection函数是Linux内核内存管理中的关键基础设施,通过高效的区间重叠检测机制,确保了新内存映射操作不会破坏现有的地址空间布局。该函数结合了红黑树查找的高效性和简洁的重叠判断逻辑,为brk、mmap等系统调用提供了可靠的冲突检测能力,是维护进程地址空间完整性和安全性的重要保障。其设计体现了内核在性能与正确性之间的精细平衡。

扩展进程的堆空间do_brk

c 复制代码
/*
 *  this is really a simplified "do_mmap".  it only handles
 *  anonymous maps.  eventually we may be able to do some
 *  brk-specific accounting here.
 */
unsigned long do_brk(unsigned long addr, unsigned long len)
{
	struct mm_struct * mm = current->mm;
	struct vm_area_struct * vma, * prev;
	unsigned long flags;
	struct rb_node ** rb_link, * rb_parent;
	pgoff_t pgoff = addr >> PAGE_SHIFT;

	len = PAGE_ALIGN(len);
	if (!len)
		return addr;

	if ((addr + len) > TASK_SIZE || (addr + len) < addr)
		return -EINVAL;

	/*
	 * mlock MCL_FUTURE?
	 */
	if (mm->def_flags & VM_LOCKED) {
		unsigned long locked, lock_limit;
		locked = mm->locked_vm << PAGE_SHIFT;
		lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
		locked += len;
		if (locked > lock_limit && !capable(CAP_IPC_LOCK))
			return -EAGAIN;
	}

	/*
	 * Clear old maps.  this also does some error checking for us
	 */
 munmap_back:
	vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
	if (vma && vma->vm_start < addr + len) {
		if (do_munmap(mm, addr, len))
			return -ENOMEM;
		goto munmap_back;
	}

	/* Check against address space limits *after* clearing old maps... */
	if ((mm->total_vm << PAGE_SHIFT) + len
	    > current->signal->rlim[RLIMIT_AS].rlim_cur)
		return -ENOMEM;

	if (mm->map_count > sysctl_max_map_count)
		return -ENOMEM;

	if (security_vm_enough_memory(len >> PAGE_SHIFT))
		return -ENOMEM;

	flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;

	/* Can we just expand an old private anonymous mapping? */
	if (vma_merge(mm, prev, addr, addr + len, flags,
					NULL, NULL, pgoff, NULL))
		goto out;

	/*
	 * create a vma struct for an anonymous mapping
	 */
	vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
	if (!vma) {
		vm_unacct_memory(len >> PAGE_SHIFT);
		return -ENOMEM;
	}
	memset(vma, 0, sizeof(*vma));

	vma->vm_mm = mm;
	vma->vm_start = addr;
	vma->vm_end = addr + len;
	vma->vm_pgoff = pgoff;
	vma->vm_flags = flags;
	vma->vm_page_prot = protection_map[flags & 0x0f];
	vma_link(mm, vma, prev, rb_link, rb_parent);
out:
	mm->total_vm += len >> PAGE_SHIFT;
	if (flags & VM_LOCKED) {
		mm->locked_vm += len >> PAGE_SHIFT;
		make_pages_present(addr, addr + len);
	}
	return addr;
}

函数功能分析

do_brk函数是brk系统调用的核心实现,专门负责扩展进程的堆空间,通过创建或扩展匿名内存映射来动态调整堆大小。

参数初始化与基本验证

c 复制代码
struct mm_struct * mm = current->mm;
struct vm_area_struct * vma, * prev;
unsigned long flags;
struct rb_node ** rb_link, * rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;

len = PAGE_ALIGN(len);
if (!len)
	return addr;

if ((addr + len) > TASK_SIZE || (addr + len) < addr)
	return -EINVAL;
  • 内存对齐:将长度按页面大小对齐,确保内存映射边界正确
  • 零长度检查:如果长度为0,直接返回原地址(空操作)
  • 地址空间验证:检查扩展后的地址是否超出进程地址空间限制或发生整数溢出

内存锁定检查

c 复制代码
if (mm->def_flags & VM_LOCKED) {
	unsigned long locked, lock_limit;
	locked = mm->locked_vm << PAGE_SHIFT;
	lock_limit = current->signal->rlim[RLIMIT_MEMLOCK].rlim_cur;
	locked += len;
	if (locked > lock_limit && !capable(CAP_IPC_LOCK))
		return -EAGAIN;
}
  • 锁定内存检查:如果进程默认锁定内存,检查资源限制
  • 权限验证:计算新的锁定内存总量,如果超过RLIMIT_MEMLOCK限制且无CAP_IPC_LOCK权限,拒绝操作
  • 安全机制:防止非特权进程锁定过多内存

现有映射清理

c 复制代码
munmap_back:
vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);
if (vma && vma->vm_start < addr + len) {
	if (do_munmap(mm, addr, len))
		return -ENOMEM;
	goto munmap_back;
}
  • 重叠VMA查找 :使用find_vma_prepare查找与目标区间重叠的VMA,同时获取红黑树操作所需参数
  • 冲突解决 :如果存在重叠的VMA,调用do_munmap将其解除映射
  • 重试机制 :使用goto标签确保彻底清理所有重叠映射

系统资源限制检查

c 复制代码
if ((mm->total_vm << PAGE_SHIFT) + len > current->signal->rlim[RLIMIT_AS].rlim_cur)
	return -ENOMEM;

if (mm->map_count > sysctl_max_map_count)
	return -ENOMEM;

if (security_vm_enough_memory(len >> PAGE_SHIFT))
	return -ENOMEM;
  • 地址空间限制:检查虚拟内存总量是否超过RLIMIT_AS限制
  • 映射数量限制:检查VMA数量是否超过系统最大映射数限制
  • 安全模块检查:调用安全子系统检查是否有足够内存
  • 全面验证:在分配前进行完整的资源可用性检查

VMA合并优化

c 复制代码
flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;

if (vma_merge(mm, prev, addr, addr + len, flags, NULL, NULL, pgoff, NULL))
	goto out;
  • 标志位设置:组合默认数据段标志、记账标志和进程默认标志
  • 合并尝试:尝试与相邻的现有VMA合并,避免创建不必要的碎片
  • 性能优化:如果合并成功,直接跳转到统计更新,减少内存分配开销

新VMA创建

c 复制代码
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
if (!vma) {
	vm_unacct_memory(len >> PAGE_SHIFT);
	return -ENOMEM;
}
memset(vma, 0, sizeof(*vma));

vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = protection_map[flags & 0x0f];
vma_link(mm, vma, prev, rb_link, rb_parent);
  • VMA分配 :从专用slab缓存分配vm_area_struct结构
  • 内存记账回滚:如果分配失败,撤销之前的内存记账
  • 结构初始化:清零并设置VMA的基本属性
  • 地址范围设置:设置起始地址、结束地址和页面偏移
  • 权限映射:根据标志位计算页面保护权限
  • 红黑树插入:将新VMA插入到进程的红黑树中维护

统计更新与内存提交

c 复制代码
out:
mm->total_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED) {
	mm->locked_vm += len >> PAGE_SHIFT;
	make_pages_present(addr, addr + len);
}
return addr;
  • 虚拟内存统计:更新进程的虚拟内存总量统计
  • 锁定内存处理:如果要求锁定内存,更新锁定内存计数并立即提交物理页面
  • 成功返回:返回扩展后的堆起始地址

函数功能总结

do_brk函数是Linux内核堆内存管理的核心组件,通过精细的资源检查、智能的VMA合并优化和安全的内存分配机制,实现了高效的堆空间扩展功能。该函数体现了内核在内存管理方面的多个重要设计原则:资源限制的严格执行、性能优化的积极尝试、错误处理的全面覆盖以及安全机制的深度集成。作为sys_brk系统调用的实际执行者,do_brk确保了进程堆空间能够安全、高效地动态调整,为应用程序的内存分配需求提供了可靠的基础设施支持。

相关推荐
网络坤子-蔡先生2 小时前
openEuler 22.03 ARM64 KVM虚拟化安装
linux·开源·负载均衡
偶像你挑的噻2 小时前
2-Linux驱动开发-内核;内核模块;设备树;设备树插件
linux·运维·驱动开发
张暮笛2 小时前
Linux内核LED驱动开发:实现可控制闪烁与常亮的GPIO驱动
linux·驱动开发
CheungChunChiu2 小时前
[特殊字符] 嵌入式音频接口全景图解:I2S、TDM、PDM、SPDIF、AC’97 与 PCM 的关系
linux·audio·pulseaudio
Nimsolax3 小时前
Linux网络数据链路层
linux·网络
小武~3 小时前
嵌入式网络编程实战:从Socket基础到高并发优化
linux·网络
大聪明-PLUS3 小时前
Rsync:管理员详细指南 第2部分
linux·嵌入式·arm·smarc
chenzhou__3 小时前
LinuxC语言文件i/o笔记(第十七天)
linux·c语言·笔记·学习
chenzhou__3 小时前
LinuxC语言文件i/o笔记(第十八天)
linux·c语言·笔记·学习