Project RC

/**
 * Filename:    stdrc.cc
 * Description: 一只腊鸡的技术成长。
 * Author:      Richard Chien
 */

#include <stdrc.hh>

ARM GIC 虚拟化学习笔记

创建于
分类:Note
标签:ARMGIC虚拟化LinuxKVMOS

这是一篇学习过程中的笔记,因为时间原因不再组织成流畅的语言,而是直接分享了~

References

GICv2

Non-virtualization

  • 中断进入 distributor,然后分发到 CPU interface
  • 某个 CPU 触发中断后,读 GICC_IAR 拿到中断信息,处理完后写 GICC_EOIR 和 GICC_DIR(如果 GICC_CTLR.EOImodeNS 是 0,则 EOI 的同时也会 DI)
  • GICD、GICC 寄存器都是 MMIO 的,device tree 中会给出物理地址

Virtualization

  • HCR_EL2.IMO 设置为 1 后,所有 IRQ 都会 trap 到 HYP
  • HYP 判断该 IRQ 是否需要插入到 vCPU
  • 插入 vIRQ 之后,在切换到 VM 之前需要 EOI 物理 IRQ,即 priority drop,降低运行优先级,使之后 VM 运行时能够再次触发该中断
  • 回到 VM 后,GIC 在 EL1 触发 vIRQ,这时候 EOI 和 DI 会把 vIRQ 和物理 IRQ 都 deactivate,因此不需要再 trap 到 HYP,不过如果是 SGI 的话并不会 deactivate,需要 HYP 自己处理(通过 maintenance 中断?)

HYP interface (GICH)

  • GICH base 物理地址在 device tree 中给出
  • 控制寄存器:GICH_HCR、GICH_VMCR 等
  • List 寄存器:GICH_LRn
  • KVM 中,这些寄存器保存在 struct vgic_cpuvgic_v2 字段,struct vgic_cpu 本身放在 struct kvm_vcpu_arch,每个 vCPU 一份
  • vCPU switch 的时候,需要切换这些寄存器(KVM 在 vgic-v2-switch.S 中定义相关切换函数)
  • VM 无法访问 GICH 寄存器,因为根本没有映射

List register (LR)

vCPU interface (GICV, GICC in VM's view)

  • GICV 也是物理 GIC 上存在的,base 物理地址同样在 device tree 中给出
  • KVM 在系统全局的一个结构体(struct vgic_params vgic_v2_params)保存了这个物理地址
  • 创建 VM 时 HYP 把一个特定的 GPA(KVM 中通过 ioctl 设置该地址)映射到 GICV base 物理地址,然后把这个 GPA 作为 GICC base 在 device tree 中传给 VM
  • VM 以为自己在访问 GICC,实际上它在访问 GICV
  • 目前理解这些 GICV 寄存器在 vCPU switch 的时候是不需要保存的(KVM 里没有保存 GICV 相关的代码),因为它其实在硬件里访问的是 GICH 配置的那些寄存器,比如 LR

Virtual distributor (GICD in VM's view)

  • 实际是内核里的一个结构体(struct vgic_dist
  • 在 device tree 中给 VM 一个 GICD base,但实际上没有映射
  • VM 访问 GICD 时,trap & emulate,直接返回或设置 struct vgic_dist 里的字段(在 vgic-v2-emul.c 文件中)
  • 每个 VM 一个,而不是每个 vCPU 一个,所以 struct vgic_dist 放在 struct kvm_arch

VM's view

  • 从 device tree 获得 GICD、GICC base 物理地址(实际是 HYP 伪造的地址)
  • 配置 GICD 寄存器(实际上 trap 到 HYP,模拟地读写了内核某 struct 里的数据)
  • 执行直到发生中断(中断先到 HYP,HYP 在 LR 中配置了一个物理 IRQ 到 vIRQ 的映射,并且设置为 pending,回到 VM 之后 GIC 在 VM 的 EL1 触发中断)
  • 读 GICC_IAR(经过 stage 2 页表翻译,实际上读了 GICV_IAR,GIC 根据 LR 返回 vIRQ 的信息,vIRQ 状态从 pending 转为 active)
  • 写 GICC_EOIR、GICC_DIR(经过 stage 2 页表翻译,实际上写了 GICV_EOIR、GICV_DIR,GIC EOI 并 deactivate 对应的 vIRQ,并 deactivate vIRQ 对应的物理 IRQ)

GICv3

新特性:

  • CPU interface(GICC、GICH、GICV)通过 system register 访问(ICC_*_ELnICH_*_EL2ICV_*_ELn,ICC 和 ICV 在指令中的编码相同,硬件根据当前 EL 和 HCR_EL2 来路由),不再用 MMIO
  • 使用 affinity routing,支持最多 2^32 个 CPU 核心
  • 引入 redistributor,每个 CPU 一个,和各 CPU interface 连接,使 PPI 不再需要进入 distributor
  • 引入一种新的中断类型 LPI 和一个新的组件 ITS(还没太看懂是干啥用的)

Non-virtualization

Virtualization

ARM GIC 虚拟化学习笔记

理解 std::declval

创建于
分类:Dev
标签:C++类型模板标准库

这是一篇攒了很久的文章……相关参考链接一直放在收藏夹,今天终于决定写一下……

使用场景

在写 C++ 的时候,往往需要使用 decltype 获得一个类的成员函数的返回类型,像下面这样:

struct A {
    int foo();
};

int main() {
    decltype(A().foo()) foo = 1; // OK
}

由于 decltype 是不会真正执行它括号里的表达式的,所以 A 类的默认构造函数不会被执行,A() 这个临时对象不会被创建。

但有时候,一个类可能没有默认构造函数,这时就无法使用上面的方法,例如:

struct A {
    A() = delete;
    int foo();
};

int main() {
    decltype(A().foo()) foo = 1; // 无法通过编译
}

于是 std::declval 就派上了用场:

#include <utility>

struct A {
    A() = delete;
    int foo();
};

int main() {
    decltype(std::declval<A>().foo()) foo = 1; // OK
}

原理

于是自然想看它是如何实现的,通过阅读 cppreference 和搜索,发现它其实就只是一个模板函数的声明:

template<class T>
typename std::add_rvalue_reference<T>::type declval() noexcept;

因为前面说的 decltype 不会真正执行括号里的表达式,所以 std::declval 函数实际上不会执行,而只是用来推导类型,于是这个函数不需要定义,只需要声明。

为什么返回引用?

接着我有了一个疑惑,为什么 std::declval 要返回 std::add_rvalue_reference<T>::type 而不是直接返回 T 呢?在上面的例子中,如果将 declval 实现成下面这样也是能够工作的:

template<class T>
T declval() noexcept; // 某些场景下可以工作,但其实是有问题的

然后去搜了下,Stack Overflow 上有人跟我有同样的疑惑,看了答案恍然大悟,像 int[10] 这种数组类型是不能直接按值返回的,直接宣判了返回 T 方案的死刑,在更复杂的情况下只会更糟糕,因此还是需要返回一个引用。

为什么使用 std::add_rvalue_reference 而不是 std::add_lvalue_reference

网上还有人问了为什么要用 std::add_rvalue_reference 而不是 std::add_lvalue_reference。这个问题是比较显然的,添加右值引用可以进行引用折叠,最终 TT &&T &&T & 还是 T &,不会改变类型的性质,但如果添加左值引用,T 就会变 T &,性质直接变了,比如声明为 int foo() & 的成员函数,本来不能访问现在可以访问,显然是错误的。

参考资料

理解 std::declval

编译一个 AArch64 平台的最小 Linux 内核

创建于
分类:Dev
标签:LinuxOS内核BusyBoxQEMUARMAArch64

总结一下最近折腾的事情,方便以后查阅。

所有内容都假设已经安装了必须的构建工具链,如果没有装,可以在报错的时候再根据提示安装。

编译 BusyBox

需要先编译一个 BusyBox 作准备,之后作为 rootfs 加载。

这里 下载适当版本的 BusyBox 源码并解压,然后运行:

cd busybox-1.32.0
mkdir build

make O=build ARCH=arm64 defconfig
make O=build ARCH=arm64 menuconfig

这会首先生成默认配置,然后开启一个配置菜单。在「Settings」里面修改下面几项配置:

[*] Don't use /usr
[*] Build static binary (no shared libs)
(aarch64-linux-gnu-) Cross compiler prefix

然后保存并退出。运行:

make O=build # -j8
make O=build install
cd build/_install

这会使用刚刚保存的配置进行编译,然后安装到 build/_install 目录,此时该目录如下:

$ tree -L 1 .
.
├── bin
├── linuxrc -> bin/busybox
└── sbin

2 directories, 1 file

接着创建一些空目录:

mkdir -pv {etc,proc,sys,usr/{bin,sbin}}

然后创建一个 init 文件,内容如下:

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

exec /bin/sh

修改 init 文件为可执行:

chmod +x init

此时当前目录(build/_install)内容如下:

$ tree -L 1 .
.
├── bin
├── etc
├── init
├── linuxrc -> bin/busybox
├── proc
├── sbin
├── sys
└── usr

6 directories, 2 files

把这些目录和文件打包:

find . -print0 | cpio --null -ov --format=newc | gzip > ../initramfs.cpio.gz

生成的 gzip 压缩后的 cpio 映像放在了 build/initramfs.cpio.gz,此时 BusyBox ramdisk 就做好了,保存备用。

编译最小配置的 Linux 内核

这里 下载适当版本的内核源码并解压,然后运行:

cd linux-5.8.8
mkdir build

make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- allnoconfig
make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

这会首先初始化一个最小的配置(allnoconfig),然后打开配置菜单。在配置菜单中做以下修改:

-> General setup
[*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

-> General setup
  -> Configure standard kernel features
[*] Enable support for printk

-> Executable file formats / Emulations
[*] Kernel support for ELF binaries
[*] Kernel support for scripts starting with #!

-> Device Drivers
  -> Generic Driver Options
[*] Maintain a devtmpfs filesystem to mount at /dev
[*]   Automount devtmpfs at /dev, after the kernel mounted the rootfs

-> Device Drivers
  -> Character devices
[*] Enable TTY

-> Device Drivers
  -> Character devices
    -> Serial drivers
[*] ARM AMBA PL010 serial port support
[*]   Support for console on AMBA serial port
[*] ARM AMBA PL011 serial port support
[*]   Support for console on AMBA serial port

-> File systems
  -> Pseudo filesystems
[*] /proc file system support
[*] sysfs file system support

完成后保存并退出,再运行:

make O=build ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- # -j8

即可编译 Linux 内核,编译出来的两个东西比较有用,一个是 build/vmlinux,另一个是 build/arch/arm64/boot/Image,前者是 ELF 格式的内核,可以用来在 GDB 中加载调试信息,后者是可启动的内核映像文件。

编译 qemu-system-aarch64

这一步是可选的,直接使用包管理器安装 QEMU 也可以。

这里 下载适当版本的 QEMU 源码并解压,然后运行:

cd qemu-5.0.0

mkdir build
cd build

../configure --target-list=aarch64-softmmu
make # -j8

即可编译 AArch64 目标架构的 QEMU。

启动 Linux

为了清晰起见,回到上面三个源码目录的外层,即当前目录中内容如下:

$ tree -L 1 .
.
├── busybox-1.32.0
├── linux-5.8.8
└── qemu-5.0.0

3 directories, 0 files

然后使用 QEMU 启动刚刚编译的 Linux:

./qemu-5.0.0/build/aarch64-softmmu/qemu-system-aarch64 \
    -machine virt -cpu cortex-a53 -smp 1 -m 2G \
    -kernel ./linux-5.8.8/build/arch/arm64/boot/Image \
    -append "console=ttyAMA0" \
    -initrd ./busybox-1.32.0/build/initramfs.cpio.gz \
    -nographic

这里使用了 QEMU 的 virt 平台。

参考资料

编译一个 AArch64 平台的最小 Linux 内核

在 main 函数运行之前修改全局变量

创建于
分类:Dev
标签:C++奇技淫巧

动机

写 C++ 时往往有这样一种需求,即在 main 函数运行之前就对全局变量进行修改,比如向全局的容器对象中填充内容。考虑下面这个需求:作为一个第三方库,我希望向用户提供一种事件机制,在发生某些事件时,触发用户编写的某些代码,但我不希望用户自己编写 main 函数并在里面设置事件回调,而是提供一个更强制的接口。在这个需求下,设计接口如下:

// 用户代码 app.cpp

#include <somelib.hpp>

ON_EVENT(handler1) {
    // 事件处理程序 1
}

ON_EVENT(handler2) {
    // 事件处理程序 2
}

此时,用户的两个处理程序,就需要在静态区初始化时被注册到我所提供的第三方库的某个全局容器对象中。

有问题的实现

最开始通过下面的代码来实现上述机制:

// somelib.hpp

extern std::vector<std::function<void()>> event_callbacks;

#define ON_EVENT_A(Name)                                \
    static void __handler_##Name();                     \
    static bool __dummy_val_##Name = [] {               \
        event_callbacks.emplace_back(__handler_##Name); \
        return true;                                    \
    }();                                                \
    static void __handler_##Name()
// somelib.cpp

std::vector<std::function<void()>> event_callbacks;

这种实现乍看起来没有什么问题,并且在很多情况下真的可以正常工作。然而,这种表面上的正常是建立在 app.cpp 中的全局变量 __dummy_val_##Name 迟于 somelib.cpp 中的 event_callbacks 初始化的情况下的,也就是说,一旦 __dummy_val_##Nameevent_callbacks 之前初始化,那么调用 event_callbacks.emplace_back 的那个 lambda 表达式将会访问一个无意义的 event_callbacks

那么这种错误情况到底有没有可能出现呢,花费好几个小时调试程序的事实已经告诉我有可能。后来查阅资料发现,在同一个翻译单元,全局变量的初始化顺序是按定义顺序来的,但在不同的翻译单元之间,初始化顺序是未定义的。上面的实现中,两个变量正是处于不同的翻译单元。

正确的实现

同一篇资料里也提出了解决这个问题的方法,那就是利用函数内静态变量在第一次使用时初始化的特性。只需要一点点修改即可得到正确的实现:

// somelib.hpp

inline auto &event_callbacks() {
    static std::vector<std::function<void()>> _event_callbacks;
    return _event_callbacks;
}

#define ON_EVENT_A(Name)                                  \
    static void __handler_##Name();                       \
    static bool __dummy_val_##Name = [] {                 \
        event_callbacks().emplace_back(__handler_##Name); \
        return true;                                      \
    }();                                                  \
    static void __handler_##Name()

同时不再需要 somelib.cpp 了。

这里 event_callbacks() 函数调用保证了当它返回引用时,_event_callbacks 静态变量必然已经被初始化,因此 emplace_back 必然有效。

为什么可以是 inline

这是另一个话题了。上面的 event_callbacks 被标记为 inline 函数,这意味着它将在可能的情况下被原地展开。那么这会不会导致不同的用户 cpp 文件中拿到的 _event_callbacks 引用不一样呢?答案是不会,因为 inline 有一个特性就是,编译器会自动将多次导入的 inline 函数合并为一个,因此其中的静态变量也只有一个。

参考资料

在 main 函数运行之前修改全局变量

关于 Multimethods

创建于
分类:Dev
标签:编程语言多态多分派MultimethodsC++

最近学到了一个编程语言中叫作 multimethods 的概念,在这里做个笔记。

为什么

要解释这个概念,首先提出两个程序设计中会遇到的问题。

1. Expression Problem

案例

考虑 C++ 中这样一种情况:

struct Event {
    virtual ~Event() {}
};

struct MessageEvent : Event {
    virtual ~MessageEvent() {}

    std::string message;
    int user_id;
};

struct NoticeEvent : Event {
    virtual ~NoticeEvent() {}

    std::string notice;
};

如果此时需要给这些 Event 类添加一个序列化对象为 JSON 的成员函数,从面向对象的角度自然会想到给 Event 加一个虚函数,然后在子类中实现:

struct Event {
    virtual ~Event() {}
    virtual std::string to_json() const = 0;
};

struct MessageEvent : Event {
    virtual ~MessageEvent() {}

    std::string message;
    int user_id;

    std::string to_json() const override {
        return "json for message event";
    }
};

这种方法的问题在于,这些 Event 类很可能是第三方库中的,而它们对应的实现文件可能已经编译成了链接库,要修改类定义是很困难的。

要在不改变类定义的情况下给类添加新功能,这就是 expression problem

横切关注点(Cross-cutting Concern)

Expression problem 是横切关注点问题的一部分,以上面为例,to_json 是一种非常普遍的操作,不仅是 Event 类,其它各种类都可能需要此操作。类似的还有 to_stringtime_it 等,它们都是一种与很多模块都相关的横切面。

2. 多分派(Multiple Dispatch)

在 OOP 中,最常用的操作是通过形如 obj.operate() 的方式调用对象的方法(C++ 中的成员函数)。C++ 会隐式地给 operate 函数增加一个 this 参数,指示调用的方法所属的对象。这种操作使用一个参数来确定要调用的函数,因此称为单分派(single dispatch)

如果要使用两个或多个参数来确定要调用的函数,也就是多分派,该怎么办呢?一种方案是函数重载(override),例如:

struct Shape {
    virtual ~Shape() {}
    // ...
};

struct Circle : Shape {
    virtual ~Circle() {}
    // ...
};

struct Rect : Shape {
    virtual ~Rect() {}
    // ...
};

bool intersect(const Circle &a, const Circle &b);
bool intersect(const Rect &a, const Rect &b);
bool intersect(const Circle &a, const Rect &b);

但问题是,重载函数的选择是在编译期进行的,但实际编程中,常常会把各类型的对象都通过 Shape & 来引用,此时必须在运行期动态确定要调用的函数,函数重载就不可行了。

Open Multimethods

Open multimethods 可同时解决上面的两个问题,这里之所以是 open,是因为 method(方法)通常指类内定义的函数,这里 open 意思是方法定义在类外。

YOMM2 库提供的矩阵类为例可以比较容易理解 open multimethods 的思路:

// 不可修改的库代码部分:

// 矩阵基类
struct Matrix {
    virtual ~Matrix() {}
    // ...
};

// 稠密矩阵
struct DenseMatrix : Matrix { /* ... */ };
// 对角矩阵
struct DiagonalMatrix : Matrix { /* ... */ };

// 可修改的应用程序代码部分:

#include <yorel/yomm2/cute.hpp>

using yorel::yomm2::virtual_;

register_class(Matrix);
register_class(DenseMatrix, Matrix);
register_class(DiagonalMatrix, Matrix);

declare_method(string, to_json, (virtual_<const Matrix &>));

define_method(string, to_json, (const DenseMatrix &m)) {
    return "json for dense matrix...";
}

define_method(string, to_json, (const DiagonalMatrix &m)) {
    return "json for diagonal matrix...";
}

int main() {
    yorel::yomm2::update_methods();

    shared_ptr<const Matrix> a = make_shared<DenseMatrix>();
    shared_ptr<const Matrix> b = make_shared<DiagonalMatrix>();

    cout << to_json(*a) << "\n"; // json for dense matrix
    cout << to_json(*b) << "\n"; // json for diagonal matrix

    return 0;
}

可以看到,open multimethods 的定义和函数重载相似,但 ab 都是 Matrix 的指针,当调用 to_json 时,程序能够正确地在运行期根据 ab 的实际类型来选择函数。同时,不需要将 to_json 写到 Matrix 类的内部,减少了侵入和重新编译的成本。

除此之外,YOMM2 还用一个矩阵乘法的例子演示了多分派的实现:

declare_method(
    shared_ptr<const Matrix>,
    times,
    (virtual_<shared_ptr<const Matrix>>, virtual_<shared_ptr<const Matrix>>));

// 任意 Matrix * Matrix -> DenseMatrix
define_method(
    shared_ptr<const Matrix>,
    times,
    (shared_ptr<const Matrix> a, shared_ptr<const Matrix> b)) {
    return make_shared<DenseMatrix>();
}

// DiagonalMatrix * DiagonalMatrix -> DiagonalMatrix
define_method(
    shared_ptr<const Matrix>,
    times,
    (shared_ptr<const DiagonalMatrix> a, shared_ptr<const DiagonalMatrix> b)) {
    return make_shared<DiagonalMatrix>();
}

程序将在运行时通过 abtypeid 来选择最 specific 的函数来调用。

参考资料

关于 Multimethods