RISC-V Linux 并发修改和执行指令 (CMODX)¶
CMODX 是一种编程技术,程序执行由程序本身修改的指令。在 RISC-V 硬件上,指令存储和指令缓存 (icache) 不保证同步。因此,程序必须使用非特权 fence.i 指令强制执行自己的同步。
但是,默认的 Linux ABI 禁止在用户空间应用程序中使用 fence.i。在任何时候,调度器都可能将任务迁移到新的 hart 上。如果在用户空间使用 fence.i 同步 icache 和指令存储后发生迁移,则新 hart 上的 icache 将不再干净。这是因为 fence.i 的行为仅影响调用它的 hart。因此,任务迁移到的 hart 可能没有同步的指令存储和 icache。
有两种方法可以解决这个问题:使用 riscv_flush_icache() 系统调用,或使用 PR_RISCV_SET_ICACHE_FLUSH_CTX
prctl() 并在用户空间中发出 fence.i。系统调用执行一次性 icache 刷新操作。prctl 会更改 Linux ABI 以允许用户空间发出 icache 刷新操作。
顺便提一下,内核中有时可以触发“延迟” icache 刷新。在编写本文时,这种情况仅在 riscv_flush_icache() 系统调用和内核使用 copy_to_user_page() 时发生。这些延迟刷新仅在 hart 使用的内存映射更改时才会发生。如果 prctl() 上下文导致 icache 刷新,则此延迟的 icache 刷新将被跳过,因为它是冗余的。因此,在 prctl() 上下文中使用 riscv_flush_icache() 系统调用时,不会有额外的刷新。
prctl() 接口¶
调用 prctl() 并将 PR_RISCV_SET_ICACHE_FLUSH_CTX
作为第一个参数。其余参数将委托给下面详述的 riscv_set_icache_flush_ctx 函数。
-
int riscv_set_icache_flush_ctx(unsigned long ctx, unsigned long scope)¶
启用/禁用用户空间中的 icache 刷新指令。
参数
unsigned long ctx
设置用户空间中允许/禁止的 icache 刷新指令的类型。下面描述了支持的值。
unsigned long scope
设置允许发出 icache 刷新指令的范围。下面描述了支持的值。
描述
ctx 支持的值
PR_RISCV_CTX_SW_FENCEI_ON
:允许在用户空间中使用 fence.i。PR_RISCV_CTX_SW_FENCEI_OFF
:禁止在用户空间中使用 fence.i。当scope == PR_RISCV_SCOPE_PER_PROCESS
时,进程中的所有线程都将受到影响。因此,必须小心;仅当您可以保证进程中没有线程会从此刻开始发出 fence.i 时才使用此标志。
scope 支持的值
PR_RISCV_SCOPE_PER_PROCESS
:确保此进程中任何线程的 icache在迁移时与指令存储保持一致。
PR_RISCV_SCOPE_PER_THREAD
:确保当前线程的 icache在迁移时与指令存储保持一致。
当 scope == PR_RISCV_SCOPE_PER_PROCESS
时,进程中的所有线程都允许发出 icache 刷新指令。每当进程中的任何线程被迁移时,都会保证相应的 hart 的 icache 与指令存储保持一致。这不会强制执行迁移之外的任何保证。如果一个线程修改了另一个线程可能尝试执行的指令,则另一个线程在尝试执行可能被修改的指令之前,仍必须发出 icache 刷新指令。这必须由用户空间程序执行。
在每个线程的上下文中(例如 scope == PR_RISCV_SCOPE_PER_THREAD
),仅允许调用此函数的线程发出 icache 刷新指令。当线程迁移时,会保证相应的 hart 的 icache 与指令存储保持一致。
在没有配置 SMP 的内核上,此函数是空操作,因为不会发生跨 hart 的迁移。
用法示例
以下文件旨在相互编译和链接。modify_instruction() 函数将 add with 0 替换为 add with one,导致 get_value() 中的指令序列从返回零变为返回一。
cmodx.c
#include <stdio.h>
#include <sys/prctl.h>
extern int get_value();
extern void modify_instruction();
int main()
{
int value = get_value();
printf("Value before cmodx: %d\n", value);
// Call prctl before first fence.i is called inside modify_instruction
prctl(PR_RISCV_SET_ICACHE_FLUSH_CTX, PR_RISCV_CTX_SW_FENCEI_ON, PR_RISCV_SCOPE_PER_PROCESS);
modify_instruction();
// Call prctl after final fence.i is called in process
prctl(PR_RISCV_SET_ICACHE_FLUSH_CTX, PR_RISCV_CTX_SW_FENCEI_OFF, PR_RISCV_SCOPE_PER_PROCESS);
value = get_value();
printf("Value after cmodx: %d\n", value);
return 0;
}
cmodx.S
.option norvc
.text
.global modify_instruction
modify_instruction:
lw a0, new_insn
lui a5,%hi(old_insn)
sw a0,%lo(old_insn)(a5)
fence.i
ret
.section modifiable, "awx"
.global get_value
get_value:
li a0, 0
old_insn:
addi a0, a0, 0
ret
.data
new_insn:
addi a0, a0, 1