Kernel Exploring
  • 前言
  • 支持
  • 老司机带你探索内核编译系统
    • 编译出你的第一个内核
    • 内核编译中的小目标
    • 可能是kbuild中最直接的小目标 – help
    • 使用了一个kbuild函数的目标 – cscope
    • 内核中单个.o文件的编译过程
    • 根目录vmlinux的编译过程
    • 启动镜像bzImage的前世今生
    • setup.bin的诞生记
    • 真假vmlinux–由vmlinux.bin揭开的秘密
    • bzImage的全貌
    • kbuild系统浅析
  • 启动时的小秘密
    • INIT_CALLS的秘密
    • 内核参数
  • 内核加载全流程
    • bootloader如何加载bzImage
    • 内核压缩与解压
    • 内核加载的几个阶段
    • 保护模式内核代码赏析
  • 内存管理
    • 内核页表成长记
      • 未解压时的内核页表
      • 内核早期的页表
      • cleanup_highmap之后的页表
      • 映射完整物理地址
      • 启用init_level4_pgt
    • 自底而上话内存
      • e820从硬件获取内存分布
      • 原始内存分配器--memblock
      • 页分配器
        • 寻找页结构体的位置
        • 眼花的页结构体
        • Node-Zone-Page
        • 传说的伙伴系统
        • Compound Page
        • GFP的功效
        • 页分配器的用户们
      • slub分配器
        • slub的理念
        • 图解slub
      • 内存管理的不同粒度
      • 挑战和进化
        • 扩展性的设计和实现
        • 减少竞争 per_cpu_pageset
        • 海量内存
        • 延迟初始化
        • 内存热插拔
        • 连续内存分配器
    • 虚拟内存空间
      • 页表和缺页中断
      • 虚拟地址空间的管家--vma
      • 匿名反向映射的前世今生
      • 图解匿名反向映射
      • THP和mapcount之间的恩恩怨怨
      • 透明大页的玄机
      • NUMA策略
      • numa balance
      • 老版vma
    • 内存的回收再利用
      • 水线
      • Big Picture
      • 手动触发回收
      • Page Fram Reclaim Algorithm
      • swapfile原理使用和演进
    • 内存隔离
      • memcg初始化
      • 限制memcg大小
      • 对memcg记账
    • 通用
      • 常用全局变量
      • 常用转换
    • 测试
      • 功能测试
      • 性能测试
  • 中断和异常
    • 从IDT开始
    • 中断?异常?有什么区别
    • 系统调用的实现
    • 异常向量表的设置
    • 中断向量和中断函数
    • APIC
    • 时钟中断
    • 软中断
    • 中断、软中断、抢占和多处理器
  • 设备模型
    • 总线
    • 驱动
    • 设备
    • 绑定
  • nvdimm初探
    • 使用手册
    • 上帝视角
    • nvdimm_bus
    • nvdimm
    • nd_region
    • nd_namespace_X
    • nd_dax
      • dev_dax
  • KVM
    • 内存虚拟化
      • Qemu内存模型
      • KVM内存管理
  • cgroup
    • 使用cgroup控制进程cpu和内存
    • cgroup文件系统
    • cgroup层次结构
    • cgroup和进程的关联
    • cgroup数据统计
  • 同步机制
    • 内存屏障
    • RCU
  • Trace/Profie/Debug
    • ftrace的使用
    • 探秘ftrace
    • 内核热补丁的黑科技
    • eBPF初探
    • TraceEvent
    • Drgn
  • 内核中的数据结构
    • 双链表
    • 优先级队列
    • 哈希表
    • xarray
    • B树
    • Maple Tree
    • Interval Tree
  • Tools
  • Good To Read
    • 内核自带文档
    • 内存相关
    • 下载社区邮件
Powered by GitBook
On this page
  • 虚拟地址和物理地址
  • __pa_symbol()
  • __pa()
  • __va() / phys_to_virt()
  • pfn和struct page
  • __pfn_to_page(pfn)
  • __page_to_pfn(pg)
  • 虚拟地址和struct page
  • virt_to_page()
  • page_to_virt()

Was this helpful?

  1. 内存管理
  2. 通用

常用转换

Previous常用全局变量Next测试

Last updated 1 year ago

Was this helpful?

虚拟地址和物理地址

__pa_symbol()

获取内核程序中符号的物理地址,也就是nm vmlinux命令能显示出的内容。

比如 nm vmlinux | grep -w _text,其结果是“ffffffff81000000 T _text”。

#define __START_KERNEL_map	_AC(0xffffffff80000000, UL)


#define __pa_symbol(x) \
	__phys_addr_symbol(__phys_reloc_hide((unsigned long)(x)))


#define __phys_addr_symbol(x) \
	((unsigned long)(x) - __START_KERNEL_map + phys_base)

这里我们只看x86_64的定义。从定义中我们可以看到,符号的物理地址是虚拟地址 减去 __START_KERNEL_map 再加上 phys_base。

当配置了kaslr时,内核加载的物理地址和编译时的加载地址会有个偏移,这个偏移记录在phys_base。具体解释可见

正因为phys_base的偏移是基于__START_KERNEL_map计算出来的,所以如此计算后就得到了内核代码中符号的物理地址。

__pa()

计算出虚拟地址对应的物理地址。

static __always_inline unsigned long __phys_addr_nodebug(unsigned long x)
{
	unsigned long y = x - __START_KERNEL_map;

	/* use the carry flag to determine if x was < __START_KERNEL_map */
	x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));

	return x;
}

#define __phys_addr(x)		__phys_addr_nodebug(x)

#define __pa(x)		__phys_addr((unsigned long)(x))

其实这里计算的时候分了两种情况。

  • 虚拟地址 > __START_KERNEL_map

  • 其他虚拟地址

第一种情况,说明虚拟地址在内核代码空间,所以实际上就退化成和__pa_symbol()一样。 第二种情况,转换过程和__va相反。这说明传入的虚拟地址需要是在内核页表上映射的空间内。

__va() / phys_to_virt()

只能对有内核映射的地址调用该函数,来获得对应地址的虚拟地址。

static inline void *phys_to_virt(phys_addr_t address)
{
	return __va(address);
}

#define __PAGE_OFFSET_BASE_L4	_AC(0xffff888000000000, UL)
unsigned long page_offset_base __ro_after_init = __PAGE_OFFSET_BASE_L4;

#define __PAGE_OFFSET           page_offset_base
#define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)

#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))

再x86_64架构下,大概率是这么个定义。

也就是一个物理地址(非内核代码范围内的)的虚拟地址 = 虚拟地址 + 0xffff888000000000。

pfn和struct page

这里先列出只有sparsemem的情况

__pfn_to_page(pfn)

static inline struct page *__section_mem_map_addr(struct mem_section *section)
{
	unsigned long map = section->section_mem_map;
	map &= SECTION_MAP_MASK;
	return (struct page *)map;
}

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})

先根据pfn找到对应的section,然后对section->section_mem_map做一个偏移运算得到struct page的地址。

__page_to_pfn(pg)

static inline unsigned long page_to_section(const struct page *page)
{
	return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

先根据page找到对应的section,注意这里找是因为把sectino number写在了page->flags里。 然后再对section->section_mem_map做一个偏移得到pfn。

虚拟地址和struct page

有了上面两个转换,自然可以推导出这个转换。也就是形成了地址和struct page之间的关系。

virt_to_page()

#define virt_to_page(kaddr)	pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)

这个转换没有什么太多可以解释的,不过这里有另一个检查的函数值得一看。

bool __virt_addr_valid(unsigned long x)
{
	unsigned long y = x - __START_KERNEL_map;

	/* use the carry flag to determine if x was < __START_KERNEL_map */
	if (unlikely(x > y)) {
		x = y + phys_base;

		if (y >= KERNEL_IMAGE_SIZE)
			return false;
	} else {
		x = y + (__START_KERNEL_map - PAGE_OFFSET);

		/* carry flag will be set if starting x was >= PAGE_OFFSET */
		if ((x > y) || !phys_addr_valid(x))
			return false;
	}

	return pfn_valid(x >> PAGE_SHIFT);
}

就是这个用来判断虚拟地址是否有效的函数。可以看出在内核中认为有效的虚拟地址空间有两个:

  • __START_KERNEL_map以上的内核代码空间

  • PAGE_OFFSET以上的direct mapping空间

其余部分都是无效空间。

page_to_virt()

#define page_to_virt(x)	__va(PFN_PHYS(page_to_pfn(x)))

这是因为在映射整个内存空间时,是这么操作的。具体参见

常用全局变量
映射完整物理地址