linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化

计算机科学家David Wheeler有一句名言:计算机科学中的任何问题都可以通过增加一个中间层来解决。这句话简洁而深刻地说明了虚拟化的思想存在于计算机科学中的各个领域。QEMU就是这种思想的一个具体实现。

系统环境

我们用neofetch看一下系统环境信息:

neofetch && uname -a|lolcat

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第1张图片

下载QEMU以及runtime工具

sudo apt-get install qemu libncurses5-dev gcc-arm-linux-gnueabi build-essential

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第2张图片

安装busybox

wget https://busybox.net/downloads/busybox-1.24.2.tar.bz2

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第3张图片

编译静态busybox库:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabi-
make menuconfig

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第4张图片

编译

make install

编译完成之后,在busybox目录下得到安装好的文件系统目录_install

下载内核,如果觉得下载过慢,可以使用axel多线程下载

axel -a -n 8 https://www.kernel.org/pub/linux/kernel/v4.x/linux-4.0.tar.gz

创建文件系统:

cp -fr ../../busybox-1.24.2/_install .
cd _install/
mkdir etc/ dev/ mnt/ -p etc/init.d

进入linux-4.0/_install/etc/init.d,创建rcS文件,并写入

mkdir -p /proc                                                                                                                                                                                          
mkdir -p /tmp
mkdir -p /sys
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

脚本的大概逻辑是,在创建了系统目录后,调用mount -a扫描fstab文件中的挂载项,对文件系统进行逐一挂载。之后,执行 chmod a+x rcS修改为可执行

 进入linux-4.0/_install/etc,创建fstab文件,并写入

proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
devtmpfs /dev devtmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0

注意这里的devtmpfs文件系统挂载,如果不添加这一行:

devtmpfs /dev devtmpfs defaults 0 0

系统运行也没有问题,只是用系统根目录一样的文件系统作为/dev的文件系统类型。

但是当使用devtmpfs挂载后,rsS脚本中的/bin/mount -a会触发扫描fstab文件, 系统就会用devtmpfs类型挂载新的/dev.此时看到的就是/dev/目录以devtmpfs挂载的了:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第5张图片

进入linux-4.0/_install/etc,创建inittab文件,并写入

::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r

使用上面的配置,默认启动QEMU后会显示提示登陆信息,需要回车后才能看到控制台输入命令。有的时候QEMU启动后按回车无响应,原因未知,所以最好直接登陆,PASS掉回车这一步,方法是将第三行的askfirst改为respawn.

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第6张图片

下一步进入linux-4.0/_install/dev,在root权限下创建如下节点

$ sudo mknod console c 5 1
$ sudo mknod null c 1 3

设置编译环境:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabi-
make vexpress_defconfig
make menuconfig

配置initarmfs

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第7张图片

将配置好的根文件系统设置进来:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第8张图片

设置地址空间的3G/1G划分

Kernel Features-> Memory split (3G/1G user/kernel split):

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第9张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第10张图片

开始编译kernel

make bzImage -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
make dtbs

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第11张图片

期间遇见缺少inlude/linux/compiler-gcc7.h时,可以将compiler-gcc4.h重名为compiler-gcc7.h即可,但是对于 linux-4.15版的kernel,却不需要这一步,直接编译即可运行。所以这个测试还是最好用linux-4.15.tar.gz这一版本。

之后执行 make dtbs创建device tree blob.

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第12张图片

万事具备,只欠运行了

运行QEMU模拟4核Cortex-A9

qemu-system-arm -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic

退出qemu,直接 ctrl+a X即可。

SMP模式四核验证:

VEXPRESS 平台DTB描述文件描述了4个核的虚拟平台,但是上面的命令默认只用了1个CPU

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第13张图片

如果需要将四个核全部用起来,需要加入-smp cpus=#NUM参数,完整的命令如下图所示:

qemu-system-arm -M vexpress-a9 -smp cpus=4 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographi

启动后,处理器信息如下: 

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第14张图片

如果启动qemu指定的核数大于DTB中描述的核心数,则启动qemu时会失败

使用linux-4.15.18搭建环境重新测试,内核配置方法相同,发现linux-4.15.18仍然可以和当前的busybox匹配兼容使用,并且没有了上面的编译错误,那就用linux-4.15.18了。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第15张图片

ctrl+x A推出QEMU或者在另一个终端中输入killall qemu-system-arm退出

调试

用如下命令安装ARM GDB 

sudo apt install gcc-arm-none-eabi

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第16张图片

安装完之后发现还是没有arm gdb命令,问杜娘才知道UBUNTU18.04对ARM GDB的支持比较特殊,至于怎么特殊是个long story,感兴趣的可以去查,这里不想在GDB安装上浪费时间,直接用MELIS 的裸机GDB尝试一下,感觉裸机的GDB应该也是可以的,都支持标准的GDB调试协议嘛。

又不得不祭出melis:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第17张图片

 之后输入调试命令

qemu-system-arm -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -S -s

根据命令选项,第一个-S表示CPU 停止住,第二个-s表示在1234端口等待GDB连接调试。

 编译代DEBUG INFO的内核,默认已经打开了DEBUG INFO选项。 

新开一个调试

$/home/caozilong/Workspace/arm-tool/gcc-arm-melis-eabi-9-2020-q2-update/bin/arm-melis-eabi-gdb -tui vmlinux
$target remote localhost:1234
$b start_kernel
或者:
$/home/caozilong/Workspace/arm-tool/gcc-arm-melis-eabi-9-2020-q2-update/bin/arm-melis-eabi-gdb -tui vmlinux -ex "target remote localhost:1234"

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第18张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第19张图片

melis工具链果然也是可以的,之后,就可以愉快的展开调试了。

melis工具链是baremetal的,如果担心有问题,可以去ARM官网工具链主页去下载linux host工具链,官网链接为:

Arm GNU Toolchain Downloads – Arm Developer

优化GDB的调试环境

内核DEBUG模式编译:

Linux内核默认使用-O2编译,社区声名不支持O0和O1编译模式,不过经过测试,O0确实不支持,不过O1可以正常编译并且执行,所以我们将其构建改为O0模式: 

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第20张图片

GDB调试环境虽然已经建立,但是还不太友好,表现在每次都要重复输入连接命令,可以擦考下面这篇博客对GDB环境进行优化。

GDB -x选项以及命令脚本的编写_papaofdoudou的博客-CSDN博客_gdb命令脚本

针对qemu的调试环境,我们可以创建.gdbinit文件并收入如下命令

arm-melis-eabi-gdb -x .gdbinit 命令进行触发。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第21张图片

使用命令 CTRL X + A进行TUI窗口的切换。

磁盘挂载

为了方便HOST机和QEMU机之间的文件传输,可以使用QEMU提供的 -sd选项将HOST机上的一个文件系统镜像文件映射为QEMU机上的文件系统的方式来实现。

首先生成文件系统镜像文件:

dd if=/dev/zero of=disk.img bs=1024 count=65536

并将其格式化为EXT4

mkfs.ext4 ./disk.img 

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第22张图片

挂载为本地回环:

mkdir tempfs
sudo mount -o loop ./disk.img ./tempfs/
sudo cp ./a.out tempfs/
sudo umount ./tempfs

添加helloworld程序,并且为静态链接。

 启动,在前面QEMU启动命令的基础上,添加-sd disk.img 选项。

qemu-system-arm -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic -sd disk.img

成功启动后,我们可以看到了/dev/mmcblk0设备,它就是disk.img对应的设备。

输入挂载命令

mount -t ext4 /dev/mmcblk0 /mnt

出错,提示开启CONFIG_LBDAF

开启CONFIG_LBDAF,重新配置内核:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第23张图片

之后,就可以正常的启动测试了,可以看到helloworld正确执行。

device-tree反编译

dtc -I dtb -O dts -o zilong.dts vexpress-v2p-ca9.dtb

设置开机登陆

增加时间戳打印

qemu-system-arm -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8 printk.time=1" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第24张图片

打印INIT CALL 信息:

qemu-system-arm -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8 printk.time=1 initcall_debug" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第25张图片

将启动LOG重定向到文件

qemu-system-arm -M vexpress-a9 -smp cpus=4 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic -serial file:/tmp/qemu-output.log

生成BOOTCHART

在前面的基础上,获取启动LOG打印,之后执行如下命令

perl bootgraph.pl dmesg.txt > boot.svg

生成BOOTCHART

使用bootgraph.py

Linux 自带的bootgraph.pl不够友好,GITHUB上另外有一个类似的更好用的工具,bootgraph.py,可以得到更加丰富的信息:

GitHub - arnoldlu/pm-graph: The Suspend/Resume project provides a tool for system developers to visualize the activity between suspend and resume, allowing them to identify inefficiencies and bottlenecks.

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第26张图片

处理命令为:

sudo python ~/../caozilong/Workspace/bootchart/pm-graph-master/bootgraph.py -dmesg dmesg.txt -addlogs

命令执行后,得到如下两个输出文件:

用浏览器打开HTML文件:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第27张图片

dump page table

多种架构下,包括ARM,X86等等支持在debugfs中dump pagetable.在ARM中是打开CONFIG_ARM_PTDUMP选项。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第28张图片

打开后,debugfs 中将会增加一个/sys/kernel/debug/kernel_page_tables节点,查看其内容:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第29张图片

for x86, you must enable CONFIG_X86_PTDUMP and CONFIG_X86_PTDUMP_CORE to get the page table info.

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第30张图片

内存分配分析:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第31张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第32张图片

FIXED MAP:

kmap/kmap_atomic从FIXMAP区域分配虚拟地址,地址区间在[0xffc80000UL,0xfff00000UL]之间:

实现文件在linux-x.x.x/arch/arm/mm/highmem.c中:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第33张图片

PKMAP

pkmap is abbrevation of persistent kernel mapping. vmalloc信息可以通过/proc/vmallocinfo节点查看,根据此节点,可以找到系统中每个调用vmalloc分配内存的地方,分配大小以及分配区域。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第34张图片

页号(PFN)分配

添加页号分配打印,重点关注PFN的起始号和结束号,以及最大的page分配数max_mapnr

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第35张图片

分析证明,页号的分配是以全局物理地址空间为基准的,并非从0开始。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第36张图片

以上内存分布可以从devicetree文件中获得:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第37张图片

kernel bringup阶段,通过解析devicetree,调用memblock_add将此块内存加入到系统管理。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第38张图片

根据PFN  VALID宏定义可以看出一些端倪。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第39张图片

mem_map allocation

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第40张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第41张图片

高端内存[0x90000000,0xA0000000]被memoryblock_remove掉,用作高端区域的动态映射或者给其它IP或者协处理器使用。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第42张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第43张图片

对于没有定义ARCH_PFN_OFFSET的架构,其PFN从0开始,比如X86在开启CONFIG_SPARSEMEM_VMEMMAP=y的情况下:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第44张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第45张图片

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第46张图片

 PFN从0开始很重要,因为这样BUDDY系统的查找BUDDY算法就不需要考虑内存基础OFFSET了。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第47张图片

CONFIG_FLAGMEM属于比较老旧配置,只有X32上才支持,X64上无法验证。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第48张图片

ARM内存初始化

adjust_lowmem_bounds 被调用两次,中间调用arm_memblock_init对保留内存进行设置,之后再次执行。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第49张图片

struct page结构体初始化过程

每个NUMA node节点维护一个struct page* 数组,在kernel bringup阶段分配,调用链条为:

start_kernel->setup_arch->paging_init->bootmem_init->zone_sizes_init->free_area_init_node->alloc_node_mem_map->...

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第50张图片

分配调用栈:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第51张图片

struct page大小为32个字节,QEMU中虽然DTS指定了1G内存,但是高端的256M内存被memblock_remove掉,实际上只映射了0x60000000-0x90000000共768M的内存,一共有4K页面0x30000个,所以占用struct page对象数组大小为0x30000*32 = 0x600000=6M

上图中分配的mem_map地址为0x8f9fb000,所以struct page范围为[0x8f9fb000,0x8fffb000],这和reserved节点的信息是吻合的。

另外至于为何分配的数组从768M末尾开始,原因有两个,第一个是调用的函数__memblock_find_range_top_down已经说的很清楚,这是从上到下的一次分配。另外,从0x8fffb000开始的0x5000已经名花有主了,在进入alloc_node_mem_map时刻,gdb抓取reserved成员数据,打印的reserved信息如下,明显看出,这部分已经被分配出去了:

是谁分配的呢?分配这20K的调用栈如下图所示: 

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第52张图片

PS:使用qemu仿真调试仿真的一个好处是可以不断复现同一种现场,由于没有异步事件干扰,每次每个模块分配的地址不会发生变化,这和在实际的板子上运行结果是不同的。

从bootmem allocator到page buddy system的转变

当bringup阶段的内存管理器完成它的任务后,将会被buddy system取代,在切换点,memblock.reserved中的存储将会以page的形式转换为reserved page继续由系统持有,注意下图,其中refcount为1,表明reserved的page由系统保留使用,引用计数为1,代表系统。

释放堆栈如下:start_kernel->mm_init->mem_init->free_all_bootmem->free_low_memory_core_early->reserve_bootmem_region->SetPageReserved.

根据调试信息来看,memblock.reserved成员对应的页和函数设置reserved标志的页恰好match.说明此时进行的是将memblock reserved内存设置为保留页。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第53张图片

SetPageReserved定义在linux-4.15/include/linux/page-flags.h中,注意,根据flags的定义和gdb调试输出的flags数值,2048为0x800,代表page结构体flags成员的bit11 被置位,恰好是PG_reserved定义的类型。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第54张图片

上面关于_refcount的描述可能存在错误,经过测试,此时所有page的_refcount均为1:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第55张图片

原因可能是还没有归还buddy系统,全部被系统引用,所以引用计数为1.而buddy启用后,归坏buddy的page引用计数为0,看下图,gdb观察到的被释放的page flag为0,引用计数为0,再看mem_map[0]flag和引用计数也都为0,表明早已经释放,但是mem_map[4],前面我们调试知道,这个页面是memblock.reserved记录的保留页面,它的状态仍然是2048(PG_reserved),引用计数为1,系统保留使用。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第56张图片

设置观察点后,发现设置refcount的地方在如下的调用堆栈所示:

空闲page的引用计数

空闲page的应用计数为0,我们可以在关键的释放page到buddy系统函数中(为何关键?因为它是唯一一个调用了buddy核心算法__find_buddy_pfn的page释放路径函数)添加对_refcount的判断逻辑:linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第57张图片

测试发现,并没有打印此处添加的LOG,证明被释放页的_refcount为0.

如何统计被BUDDY系统管理的内存数量

zong->managed_pages记录了BUDDY系统管理的页面数量

低端内存映射

低端内存映射的核心函数是map_lowmem,调用路径为:

start_kernel->setup_arch->paging_init->map_lowmem,通过create_mapping做映射。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第58张图片

映射逻辑如下图所示:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第59张图片

注意map_lowmem函数中的 memblock_is_nomap判断逻辑,如果是nomap类型的block,则不进行映射,这里涉及到保留内存:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第60张图片

系统零页

在匿名页面的缺页异常处理中,内核使用了系统零页,因为对于malloc的页面来讲,分配的仅仅是虚拟内存,如果用户直接去读,返回的是全0的数据,因此LINUX内核不必为这种情况单独分配物理内存。而使用系统零页去映射,属性为只读。当程序需要写入这个页面时就会触发一个缺页异常,进行写时拷贝。而如果用户直接去写的化,就不会经过系统零页的映射阶段,直接进行写时拷贝。

ARM的系统零页分配在paging_init函数中,记录在empty_zero_page变量。

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第61张图片

分配使用early_alloc从memblock分配:

linux4.15 arm qemu @ubuntu18.04环境搭建与bootgraph启动优化_第62张图片

总结

本环境对内核和BUSYBOX的版本要求并不严格,只要是同一个时期的内核和Busybox,都不会有太大问题,比如下面用的busybox-1.35.0.tar.bz2搭配linux-5.15.90.tar.xz也是可以的。

参考资料

关于系统保留内存,请参考:

Linux&Tina&Melis内存布局分析以及linux reserved memory机制_papaofdoudou的博客-CSDN博客


结束

你可能感兴趣的:(内存管理,虚拟化,QEMU,ARM,Linux)