编辑
2025-03-30
工作知识
0

系统调用eventfd介绍

eventfd是一个利用匿名文件设计的系统调用,用作高效的进程间通信,本文介绍一下eventfd的内核实现和用户测试。方便后续编程时可以考虑使用eventfd

内核实现

eventfd是通过syscall实现,如下

SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags) { return do_eventfd(count, flags); } SYSCALL_DEFINE1(eventfd, unsigned int, count) { return do_eventfd(count, 0); }

其实现如下:

static int do_eventfd(unsigned int count, int flags) { struct eventfd_ctx *ctx; struct file *file; int fd; /* Check the EFD_* constants for consistency. */ BUILD_BUG_ON(EFD_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON(EFD_NONBLOCK != O_NONBLOCK); if (flags & ~EFD_FLAGS_SET) return -EINVAL; ctx = kmalloc(sizeof(*ctx), GFP_KERNEL); if (!ctx) return -ENOMEM; kref_init(&ctx->kref); init_waitqueue_head(&ctx->wqh); ctx->count = count; ctx->flags = flags; ctx->id = ida_simple_get(&eventfd_ida, 0, 0, GFP_KERNEL); flags &= EFD_SHARED_FCNTL_FLAGS; flags |= O_RDWR; fd = get_unused_fd_flags(flags); if (fd < 0) goto err; file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx, flags); if (IS_ERR(file)) { put_unused_fd(fd); fd = PTR_ERR(file); goto err; } file->f_mode |= FMODE_NOWAIT; fd_install(fd, file); return fd; err: eventfd_free_ctx(ctx); return fd; }

do_eventfd比较重要的点在于anon_inode_getfile,这里通过匿名页来设置此系统调用。

再重要的就是eventfd_ctx结构,如下:

struct eventfd_ctx { struct kref kref; wait_queue_head_t wqh; /* * Every time that a write(2) is performed on an eventfd, the * value of the __u64 being written is added to "count" and a * wakeup is performed on "wqh". A read(2) will return the "count" * value to userspace, and will reset "count" to zero. The kernel * side eventfd_signal() also, adds to the "count" counter and * issue a wakeup. */ __u64 count; unsigned int flags; int id; };

这里看到了我们read和write作用的是count值,所以这个fd只能通过count来传递信息。而read/write是通过标准的fops实现,如下

static const struct file_operations eventfd_fops = { #ifdef CONFIG_PROC_FS .show_fdinfo = eventfd_show_fdinfo, #endif .release = eventfd_release, .poll = eventfd_poll, .read_iter = eventfd_read, .write = eventfd_write, .llseek = noop_llseek, };

read操作的核心实现是eventfd_ctx_do_read,这里如果是flag设置了semaphore则只会减1,否则可以直接是count值,如下:

void eventfd_ctx_do_read(struct eventfd_ctx *ctx, __u64 *cnt) { lockdep_assert_held(&ctx->wqh.lock); *cnt = ((ctx->flags & EFD_SEMAPHORE) && ctx->count) ? 1 : ctx->count; ctx->count -= *cnt; }

write操作直接在eventfd_write中,每次write会自增,也就是

ctx->count += ucnt

poll操作根据poll_wait来等待,READ_ONCE来保证count的读取只有一次,根据注释我们可以知道,wqh锁和qwh锁不会竞争问题,也就是安全的。

* poll write * ----------------- ------------ * lock ctx->wqh.lock (in poll_wait) * count = ctx->count * __add_wait_queue * unlock ctx->wqh.lock * lock ctx->qwh.lock * ctx->count += n * if (waitqueue_active) * wake_up_locked_poll * unlock ctx->qwh.lock * eventfd_poll returns 0

其代码实现如下

static unsigned int eventfd_poll(struct file *file, poll_table *wait) { struct eventfd_ctx *ctx = file->private_data; unsigned int events = 0; u64 count; poll_wait(file, &ctx->wqh, wait); count = READ_ONCE(ctx->count); if (count > 0) events |= POLLIN; if (count == ULLONG_MAX) events |= POLLERR; if (ULLONG_MAX - 1 > count) events |= POLLOUT; return events; }

而对于内核空间对eventfd的调用,可以通过eventfd_signal函数,其实现是eventfd_signal_mask,这里同样是自加如下:

__u64 eventfd_signal_mask(struct eventfd_ctx *ctx, __u64 n, unsigned mask) { unsigned long flags; /* * Deadlock or stack overflow issues can happen if we recurse here * through waitqueue wakeup handlers. If the caller users potentially * nested waitqueues with custom wakeup handlers, then it should * check eventfd_signal_count() before calling this function. If * it returns true, the eventfd_signal() call should be deferred to a * safe context. */ if (WARN_ON_ONCE(this_cpu_read(eventfd_wake_count))) return 0; spin_lock_irqsave(&ctx->wqh.lock, flags); this_cpu_inc(eventfd_wake_count); if (ULLONG_MAX - ctx->count < n) n = ULLONG_MAX - ctx->count; ctx->count += n; if (waitqueue_active(&ctx->wqh)) wake_up_locked_poll(&ctx->wqh, EPOLLIN | mask); this_cpu_dec(eventfd_wake_count); spin_unlock_irqrestore(&ctx->wqh.lock, flags); return n; }

应用测试

为了使用eventfd,我们可以直接使用c库封装的eventfd函数,示例如下:

#include <sys/eventfd.h> #include <unistd.h> #include <stdint.h> #include <stdio.h> int main() { int efd; uint64_t value; efd = eventfd(0, 0); if (efd == -1) { perror("eventfd"); return 1; } value = 1; if (write(efd, &value, sizeof(value)) == -1) { perror("write"); return 1; } if (write(efd, &value, sizeof(value)) == -1) { perror("write"); return 1; } if (read(efd, &value, sizeof(value)) == -1) { perror("read"); return 1; } printf("[kylin]: read value: %lu\n", value); close(efd); return 0; }

运行后结果如下:

[kylin]: read value: 2

至此eventfd介绍完成,详细大家在使用高性能的进程通信的时候,可以适当考虑eventfd

编辑
2025-03-29
工作知识
0

GICv3中断简介

GICv3中断是GICv2的改进,本文主要基于GICv2,简单讨论GICv3的改进内容

中断类型

我们知道GICv2默认支持如下中断类型:

  • SPI(Software Generated Interrupt) 软件生成中断,其目的是对多核之间发送的软件中断信号
  • PPI(Private Peripheral Interrupt) 私有外设中断,此中断是某个CPU核心独有的中断,如定时器
  • SPI(Shared Peripheral Interrupt) 共享外设中断,所有外设中断,可以被所有CPU响应

在GICv3中,新增了一个中断类型

  • LPI(Locality-specific Peripheral Interrupt)本地特殊外设中断,Message-Based的中断类型

中断号分配

GICv2的中断类型,如下

中断类型中断号
SGI0-15
PPI16-31
SPI32-1019
Reserved1020-1023

而GICv3的中断号相比于GICv2增加了LPI和一些扩展,如下

中断类型中断号
SGI0-15
PPI16-31
SPI32-1019
Reserved1020-8191
LPI8192-Implementation defined

ITS

为了支持Message-Based的中断LPI,GICv3的中断控制器引入了ITS组件,ITS的作用是将Device_id的Event_ID转换成INTID(LPI硬件中断号),然后通过查表转换成对应的Redistributor,最后转发给CPU上。

MSI

PCIe总线协议支持消息中断MSI,这个实现是通过GICv3的ITS实现的

编辑
2025-03-23
工作知识
0

异常

aarch64架构有多个异常方式,例如:中断,软件异常,同步异常等等

这些异常在代码中需要对应一个内存地址用作跳转,在aarch64中,每个异常都可以设置入口函数,从而实现异常的处理。与armv7不同的是,v8上默认向量表的空间更大了,支持0x80大小

linux的定义

在linux中,异常向量表通过如下定义

SYM_CODE_START(vectors) kernel_ventry 1, t, 64, sync // Synchronous EL1t kernel_ventry 1, t, 64, irq // IRQ EL1t kernel_ventry 1, t, 64, fiq // FIQ EL1t kernel_ventry 1, t, 64, error // Error EL1t kernel_ventry 1, h, 64, sync // Synchronous EL1h kernel_ventry 1, h, 64, irq // IRQ EL1h kernel_ventry 1, h, 64, fiq // FIQ EL1h kernel_ventry 1, h, 64, error // Error EL1h kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0 kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0 kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0 kernel_ventry 0, t, 64, error // Error 64-bit EL0 kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0 kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0 kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0 kernel_ventry 0, t, 32, error // Error 32-bit EL0 SYM_CODE_END(vectors)

通过上面可以看到,linux定义了这些异常的实现,右边有注释,就不解释了。

kernel_ventry的实现

代码如下:

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req .align 7 .Lventry_start\@: .if \el == 0 /* * This must be the first instruction of the EL0 vector entries. It is * skipped by the trampoline vectors, to trigger the cleanup. */ b .Lskip_tramp_vectors_cleanup\@ .if \regsize == 64 mrs x30, tpidrro_el0 msr tpidrro_el0, xzr .else mov x30, xzr .endif .Lskip_tramp_vectors_cleanup\@: .endif sub sp, sp, #PT_REGS_SIZE #ifdef CONFIG_VMAP_STACK /* * Test whether the SP has overflowed, without corrupting a GPR. * Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT) * should always be zero. */ add sp, sp, x0 // sp' = sp + x0 sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp tbnz x0, #THREAD_SHIFT, 0f sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0 sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp b el\el\ht\()_\regsize\()_\label 0: /* * Either we've just detected an overflow, or we've taken an exception * while on the overflow stack. Either way, we won't return to * userspace, and can clobber EL0 registers to free up GPRs. */ /* Stash the original SP (minus PT_REGS_SIZE) in tpidr_el0. */ msr tpidr_el0, x0 /* Recover the original x0 value and stash it in tpidrro_el0 */ sub x0, sp, x0 msr tpidrro_el0, x0 /* Switch to the overflow stack */ adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0 /* * Check whether we were already on the overflow stack. This may happen * after panic() re-enables interrupts. */ mrs x0, tpidr_el0 // sp of interrupted context sub x0, sp, x0 // delta with top of overflow stack tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range? b.ne __bad_stack // no? -> bad stack pointer /* We were already on the overflow stack. Restore sp/x0 and carry on. */ sub sp, sp, x0 mrs x0, tpidrro_el0 #endif b el\el\ht\()_\regsize\()_\label .org .Lventry_start\@ + 128 // Did we overflow the ventry slot? .endm .macro tramp_alias, dst, sym, tmp mov_q \dst, TRAMP_VALIAS adr_l \tmp, \sym add \dst, \dst, \tmp adr_l \tmp, .entry.tramp.text sub \dst, \dst, \tmp .endm

这段汇编有点长,我跳过一下清理和栈必要操作,下面开始解析关键部分。

宏定义

首先查看宏定义如下

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req

这里宏有四个参数,其中req指的是required,四个参数含义如下:

  • el等级
  • 是否hypervisor
  • 寄存器大小(32/64)
  • 异常类型

跳转异常

b el\el\ht\()_\regsize\()_\label

这里对应c是如下

el##${el}${ht}_${regsize}_${label}

以fiq举例如下:

kernel_ventry 1, t, 64, fi

则跳转函数el1t_64_fiq

向量入口大小

.org .Lventry_start\@ + 128

这里确保每个异常向量入口占用恰好 128 字节的空间。

确认符号

为了确定汇编宏扩展是否正确,可以通过nm查看,如下

# nm entry.o | awk '{print $3}' | grep "^el" el0t_32_error el0t_32_fiq el0t_32_irq el0t_32_sync el0t_64_error el0t_64_fiq el0t_64_irq el0t_64_sync el1h_64_error el1h_64_fiq el1h_64_irq el1h_64_sync el1t_64_error el1t_64_fiq el1t_64_irq el1t_64_sync
编辑
2025-03-21
工作知识
0

浅谈程序跑飞

我们原来最早调试51的时候,碰到程序跑飞了就不管了,那个时候认为程序跑飞是概率事件,所以没有考虑其跑飞的真正原因。最近发现aarch64架构的bl指令,非常容易导致程序跑飞。本文复现这种跑飞的情况,用来说明一个非常常见的跑飞现象,并且给出解决思路。 同样,思考以前调试51的时候,跑飞很可能是芯片的一些特性导致的问题。只怪当时知识尚浅,无法理解。

跑飞的代码

本文以rtems上的汇编为例,当然可以在linux中复现,本文就不在linux重复实验了,问题一样会出现,如下是代码diff

diff --git a/bsps/aarch64/shared/start/start.S b/bsps/aarch64/shared/start/start.S index 0a89d85035..04e2b1a8fa 100644 --- a/bsps/aarch64/shared/start/start.S +++ b/bsps/aarch64/shared/start/start.S @@ -43,6 +43,18 @@ .globl _start .section ".bsp_start_text", "ax" +kylin_call: + mov x0, xzr + ret + +kylin_out_of_control: + mov x0, xzr + bl kylin_call + mov x0, xzr + ret + /* Start entry */ _start: @@ -341,6 +353,8 @@ _start: /* Branch to start hook 1 */ bl bsp_start_hook_1 + bl kylin_out_of_control + /* Branch to boot card */ mov x0, #0 bl boot_card

上面的代码我定义了一个kylin_out_of_contrl标签,通过bl来跳转此标签,然后在kylin_out_of_control中,添加了一个没有意义的指令来代替其他指令,没有意义的指令是将x0清空。然后调用kylin_call标签,在kylin_call中继续将x0清空后返回到kylin_out_of_control,然后kylin_out_of_control内继续调用x0清空来代替其他指令,最后返回。

思考

根据我们对汇编的理解,上面的代码完全正常,bl是跳转指令,ret返回指令,mov移动指令。每个指令简单易懂,这些代码为什么组合会跑飞呢。可以思考一下

关于x30寄存器

为了理解这个问题,我们需要了解x30寄存器。在aapcs中我们可以知道x30作为lr寄存器,这个寄存器保存的是上一个函数调用pc值的+4,也就是 last_pc + 4 的值。这里出现跑飞的原因是x30寄存器不正常了。

那么我们可以思考,为什么x30会不正常?

关于ret指令

我们知道ret指令是返回指令,那么有一个问题,ret指令它会操作系统寄存器吗(x0-x30寄存器)?
aarch64的定义上,ret没有提到会操作寄存器,我们看看ret的说明如下:

image.png

根据上面的信息,我们知道ret默认返回x30寄存器。完全没问题啊,不会操作任何寄存器

那么问题就出来了,ret不会操作寄存器,但是我们bl进入的时候,会根据aapce记录x30寄存器为原pc+4。也就是会改写x30寄存器。

发现没有,x30寄存器会被aapcs改写,但是不会在ret还原。所以真相是:

如果bl出现嵌套,那么两次ret时,最外层的函数的x30和内层函数的x30相等,那么程序就一直在这里打转。于是出现了跑飞现象

演示

上面文字介绍不是很直观,下面开始演示

qemu运行rtems

如下

qemu-system-aarch64 -no-reboot -nographic -serial mon:stdio -machine xlnx-zcu102 -m 4096 -smp 4 -kernel build/aarch64/zynqmp_qemu/testsuites/libtests/kylinos.exe -s -S

挂gdb

gdb开始监测

aarch64-rtems6-gdb build/aarch64/zynqmp_qemu/testsuites/libtests/malloctest.exe

挂上断点

(gdb) b kylin_out_of_control Breakpoint 1 at 0x18008: file ../../../bsps/aarch64/shared/start/start.S, line 51. (gdb) c Continuing. Thread 1 hit Breakpoint 1, kylin_out_of_control () at ../../../bsps/aarch64/shared/start/start.S:51 51 mov x0, xzr (gdb) l 46 kylin_call: 47 mov x0, xzr 48 ret 49 50 kylin_out_of_control: 51 mov x0, xzr 52 //mov x19, x30 53 bl kylin_call 54 //mov x30, x19 55 mov x0, xzr

盯住x30寄存器

此时x30寄存器为

(gdb) x/x $x30 0x180e4 <_start+204>

这里还是一切正常,x30正好是_start+200 + 4

此时进入kylin_call,如下

(gdb) x/x $x30 0x18010 <kylin_out_of_control+8>:

然后我们第一个ret返回,如下

(gdb) s kylin_out_of_control () at ../../../bsps/aarch64/shared/start/start.S:55 55 mov x0, xzr (gdb) x/x $x30 0x18010 <kylin_out_of_control+8>: 0xaa1f03e0 (gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> => 0x0000000000018010 <+8>: mov x0, xzr 0x0000000000018014 <+12>: ret End of assembler dump.

可以看到在kylin_out_of_control中了,但是异常出现了,kylin_out_of_control函数中的x30寄存器并不是我们认为的_start+204,而是kylin_out_of_control+8

按照代码逻辑,即将在0x0000000000018014处调用ret,而这个ret默认找的x30是kylin_out_of_control+8。程序会死循环了。也就是我们说的跑飞了。

确认一下x0-x30常规寄存器

为了说明这个问题,我们需要看看两次bl和ret是否是bl修改了x30,而ret不会修改任何寄存器。信息如下

第一个bl,进入kylin_out_of_control时,寄存器如下

x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x180e4 98532

然后再一个bl进入kylin_call函数,我们查看x0-x30如下

x0 0x0 0 x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x18010 98320

发现没有,bl寄存器会修改x30寄存器。

然后我们看第一个ret后的寄存器,此时应该在函数kylin_call的ret后,也就是kylin_out_of_control中

x0 0x0 0 x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x18010 98320

可以发现,此时寄存器和bl后的寄存器完全一致,x30没有发生改变

然后查看第二次ret的寄存器信息

x0 0x0 0 x1 0x0 0 x2 0xffffffffffffffe8 -24 x3 0x1030c0 1061056 x4 0x103128 1061160 x5 0x4 4 x6 0x4 4 x7 0xf 15 x8 0x1c 28 x9 0x4 4 x10 0x3c0 960 x11 0x3fffffff 1073741823 x12 0x0 0 x13 0x8000000000 549755813888 x14 0x40000000 1073741824 x15 0x200000 2097152 x16 0x0 0 x17 0x0 0 x18 0x0 0 x19 0x0 0 x20 0x0 0 x21 0x0 0 x22 0x0 0 x23 0x0 0 x24 0x0 0 x25 0x0 0 x26 0x0 0 x27 0x0 0 x28 0x0 0 x29 0x0 0 x30 0x18010 98320

完全没有变化,而且代码回到了bl后的地址上。详细如下

(gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> => 0x0000000000018010 <+8>: mov x0, xzr 0x0000000000018014 <+12>: ret End of assembler dump. (gdb) s 56 ret (gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> 0x0000000000018010 <+8>: mov x0, xzr => 0x0000000000018014 <+12>: ret End of assembler dump. (gdb) s 55 mov x0, xzr (gdb) disassemble Dump of assembler code for function kylin_out_of_control: 0x0000000000018008 <+0>: mov x0, xzr 0x000000000001800c <+4>: bl 0x18000 <bsp_vector_table_begin> => 0x0000000000018010 <+8>: mov x0, xzr 0x0000000000018014 <+12>: ret End of assembler dump.

修复

修复这个问题很简单,我们可以利用x19这种aapcs约定临时存储寄存器来保存x30,如下

diff --git a/bsps/aarch64/shared/start/start.S b/bsps/aarch64/shared/start/start.S index 0a89d85035..f284cf2baa 100644 --- a/bsps/aarch64/shared/start/start.S +++ b/bsps/aarch64/shared/start/start.S @@ -43,6 +43,17 @@ .globl _start .section ".bsp_start_text", "ax" +kylin_call: + mov x0, xzr + ret + +kylin_out_of_control: + mov x19, x30 + bl kylin_call + mov x30, x19 + mov x0, xzr + ret + /* Start entry */ _start: @@ -341,6 +352,8 @@ _start: /* Branch to start hook 1 */ bl bsp_start_hook_1 + bl kylin_out_of_control + /* Branch to boot card */ mov x0, #0 bl boot_card

此时运行代码,系统不再跑飞了。

编辑
2025-03-11
工作知识
0

vanblog的云部署步骤

安装包

apt install docker docker.io docker-compose

设置docker仓库地址

/etc/docker/daemon.json

{ "dns": ["8.8.8.8", "8.8.4.4"], "registry-mirrors": [ "https://docker.m.daocloud.io/", "https://huecker.io/", "https://dockerhub.timeweb.cloud", "https://noohub.ru/", "https://dockerproxy.com", "https://docker.mirrors.ustc.edu.cn", "https://docker.nju.edu.cn", "https://xx4bwyg2.mirror.aliyuncs.com", "http://f1361db2.m.daocloud.io", "https://registry.docker-cn.com", "http://hub-mirror.c.163.com" ] }

运行配置文件

curl -L https://vanblog.mereith.com/vanblog.sh -o vanblog.sh && chmod +x vanblog.sh && ./vanblog.sh

备份

按下10备份即可

10. 备份 VanBlog

此时获得文件 vanblog-backup-20250311153742.tar.gz

恢复

按下11 恢复即可

11. 恢复 VanBlog

至此,全部完成