• xen-timer


    目的

    主要了解一下arm timer spec 和xen项目中timer是怎么使用,如何实现的。同时也是对学习过程的一些记录。

    学习链接

    文章链接
    时间子系统http://www.wowotech.net/sort/timer_subsystem
    arm timer spechttps://developer.arm.com/documentation/102379/0101/The-processor-timers

    xen timer代码文件

    xen/arch/arm/time.c
    xen/arch/arm/vtimer.c
    xen/common/timer.c
    xen/include/xen/time.h
    xen/include/xen/timer.h
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Generic Timer

    硬件架构

    所有使用ARM处理器的系统中都会包含一个标准化的通用定时器(Generic Timer)框架。这个通用定时器提供了一个系统计数器(System Counter)和一组定时器(Timer),其结构如下图:

    在这里插入图片描述

    系统计数器(System Counter)是一个始终打开的设置,它提供了一个固定频率递增的系统计数值,系统计数值被广播到系统的中的所有核心,使核心对时间的流逝有一个共同的视角。系统计数器值的宽度在56到64位之间,从Armv8.6和Armv9.1-A开始,计数的频率固定在1GHz;在Armv8.6a之前,计数频率由系统设计自身实现选择,通常在1MHz到50MHz的范围内。

    每个核都有一组定时器(Timer)。这些定时器(Timer)是比较器,它们与系统计数器(System Counter)提供的广播系统计数进行比较。软件可以配置定时器(Timer),在将来的设定值生成中断或事件。软件还可以使用系统计数来添加时间戳,因为系统计数为所有核心提供了一个公共参考点。

    The processor timer

    每一个Arm核都配备一组专门为自己服务的定时器。定时器设定的计数值到了之后会通过私有的PPI中断(Private Peripheral Interrupt)向通用中断控制器发中断请求。按照不同的指令集扩展,每组都有最多7个定时器,但无论如何最基本的都会提供4个,它们分别是:

    Timer nameWhen is the timer presentProvide
    EL1 physical timerAlwaysyes
    EL1 virtual timerAlwaysyes
    Non-secure EL2 physical timerImplements EL2yes
    Non-secure virtual physical timerImplements ARMv8.1-VHEno
    EL3 physical timerImplements EL3yes
    Secure EL2 physical timerImplements ARMv8.4-SecEL2no
    Secure EL2 virtual timerImplements ARMv8.4-SecEL2no

    计数和频率

    CNTPCT_ELx

    对于系统计数器来说,可以通过读取控制寄存器CNTPCT_EL0来获得当前的系统计数值(System Counter的值,无论处于哪个异常级别),也就是通过以下汇编指令:

    MRS Xn, CNTPCT_EL0
    
    • 1

    这条指令是可以乱序执行的,使用的时候要适当保护。当读取计数器的顺序很重要时,可以使用ISB,如下面的代码所示:

    loop:  // Polling for some communication to indicate a requirement to read the timer
      LDR X1, [X2] #以X2寄存器的值作为内存地址,加载这个内存地址的值到X1寄存器中
      CBZ x1, loop #比较(Compare),如果结果为零(Zero)就跳转到loop
      ISB         // Without this, the CNTPCT could be read before the memory location in
                  // [X2] has had the value 0 written to it
      MRS X1, CNTPCT_EL0
      # 事实上,在xen的代码里,也很可以看到确实会用ISB来保持指令执行的前后次序
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    CNTFRQ_EL0

    CNTFRQ_EL0报告系统计数的频率。但是,这个寄存器不是由硬件填充的。寄存器在实现的最高异常级别上是可写的,在所有异常级别上是可读的。固件(通常在EL3上运行)将这个寄存器填充为早期系统初始化的一部分。更高级别的软件,如操作系统,可以使用寄存器来获取频率。

    Timer registers

    每个定时器有以下三种系统寄存器,如下:

    RegisterPurpose
    _CTL_EL每组定时器都还有一个控制寄存器(CTL),其只有最低三位有意义,其它位全是保留的。具体参考下表
    _CVAL_EL比较寄存器有64位,如果设置了之后,当系统计数器达到或超过了这个值之后(CVAL < System Counter),就会触发定时器中断。通过这种方式来实现第一类定时任务
    _TVAL_EL定时寄存器有32位,如果设置了之后,会将比较寄存器设置成当前系统计数器加上设置的定时寄存器的值(CVAL= System Counter + TVAL)。当系统计数器(System Counter)达到或超过了这个值后,就会触发定时中断。通过这种方式来实现第二种定时任务

    控制寄存器低三位表示:

    bitfuncpurpose
    0ENBALE是否打开定时器,使其工作
    1IMASK中断掩码,若设置成1,则即使定时器是工作的,仍然不会发生中断
    2ISTATUS定时器中断的状态。 该位是只读的,1表示产生了中断

    在寄存器名称中, 标识正在访问的定时器。 下表显示了可能的值:

    TimerRegister prefixEL
    EL1 physical timerCNTPEL0
    EL1 virtual timerCNTPEL0
    Non-secure EL2 physical timerCNTHPEL2
    Non-secure virtual physical timerCNTHPEL2
    EL3 physical timerCNTPSEL1
    Secure EL2 physical timerCNTHPSEL2
    Secure EL2 virtual timerCNTHVSEL2

    For example, CNTP_CVAL_EL0 is the Comparator register of the EL1 physical timer.

    访问timer

    对于某些定时器,可以配置哪些异常级别可以访问定时器

    1. EL1物理和虚拟定时器:对这些定时器的EL0访问由CNTKCTL_EL1控制。

    2. EL2物理和虚拟定时器:当HCR_EL2.{TGE,E2H}=={1,1}, EL0对这些定时器的访问由CNTKCTL_EL2控制。

    配置定时器

    有两种方法配置定时器,要么使用比较器(CVAL)寄存器,要么使用定时器(TVAL)寄存器。

    • 比较寄存器CVAL是一个64位寄存器。软件向这个寄存器写入一个值,当计数达到或超过该值时,计时器就会触发,如下所示:
    Timer Condition Met: CVAL <= System Count
    
    • 1
    • 定时寄存器有32位,如果设置了之后,会将比较寄存器设置成当前系统计数器加上设置的定时寄存器的值(CVAL= 系统计数器 + TVAL),后面的流程就跟方式1一致了:
    CVAL = TVAL + System Counter 
    Timer Condition Met: CVAL <= System Count
    
    • 1
    • 2

    记住,TVAL和CVAL是对同一个定时器进行编程的不同方法。它们不是两个不同的定时器。

    中断关联

    可以通过配置定时器来产生中断。来自核心定时器的中断只能被传递到该核心。这意味着一个核心的定时器不能用来生成针对另一个核心的中断。

    中断的生成是通过CTL寄存器的这些字段来控制的:

    • ENABLE 置1打开定时器,使其工作;
    • IMASK 中断屏蔽。0:启用中断生成 1:禁用中断生成
    • ISTATUE ENABLE==1 同时(Cval <= System Count)置1,报告定时器是否正在触发

    为了产生中断,软件必须将ENABLE设置为1,将IMASK设置为0。当定时器触发时CVAL <= System Count,中断信号被assert到中断控制器。

    每个定时器的中断号如下所示:

    TimerRegister prefixINITID
    EL1 physical timerCNTP30
    EL1 virtual timerCNTP27
    Non-secure EL2 physical timerCNTHP26
    Non-secure virtual physical timerCNTHP28
    EL3 physical timerCNTPS29
    Secure EL2 physical timerCNTHPS20
    Secure EL2 virtual timerCNTHVS19

    这些INTID在私有外围中断(PPI)范围内。这些INITD对于特定的核心是私有的。这意味着每个核心都将其EL1物理计时器视为INTID 30。

    定时器生成的中断触发类型是电平触发。这意味着,一旦达到定时器触发条件,定时器将继续发出中断信号,直到发生以下情况之一:

    • IMASK 置位1,屏蔽timer对应中断;
    • ENBALE 置位0,关闭该定时器;
    • TVAL or CVAL 更新,以致(CVAL <= System Count)不成立。

    在为定时器编写中断处理程序时,软件在GIC中取消中断之前清除中断是很重要的。否则GIC将重新发出相同的中断信号。

    timer虚拟化

    前面,我们介绍了处理器中不同的定时器。这些计时器可以分为两组:虚拟定时器和物理定时器。

    物理定时器(如EL3物理定时器、CNTPS)与系统计数器提供的计数值进行比较。这个值被称为物理计数,由CNTPCT_EL0提供。

    虚拟定时器(如EL1虚拟定时器、CNTV)与虚拟计数进行比较。虚拟计数的计算方法为:

    Virtual Count = Physical Count - <offset>
    
    • 1

    offset在寄存器CNTVOFF_EL2中指定,它只能在EL2或EL3处访问(后缀为EL2)。该配置如下图所示:

    在这里插入图片描述

    note:如果EL2没有实现,offset固定为0。这意味着虚拟计数和物理计数值总是相同的。

    虚拟计数允许管理程序向虚拟机(VM)显示虚拟时间。例如,hypervisor可以使用offset来隐藏虚拟机未被调度时的时间流逝。这意味着虚拟计数可以表示虚拟机经历的时间,而不是wall clock time。

    事件流

    Generic Timer 还可以用来生成事件流,作为等待事件机制的一部分。WFE指令将核心置于低功耗状态,由事件唤醒核心。

    有几种方法来生成一个事件,包括:

    • 在不同的核心上执行SEV(发送事件)指令;
    • 清除核心的全局独占监视器;
    • 从核心的通用定时器使用事件流。

    Generic Timer 可以配置为以定期间隔生成事件流。此配置的一个用途是生成超时。通常在等待资源可用时使用WFE,此时等待时间预计不会很长。来自计时器的事件流意味着核心处于低功耗状态的最大时间是有限的。

    事件流可以从物理计数CNTPCT_EL0或虚拟计数CNTVCT_EL0生成。

    • CNTKCTL_EL1 - 控制从CNTVCT_EL0生成事件流
    • CNTKCTL_EL2 - 控制从CNTPCT_EL0生成事件流

    对于每个寄存器,位域如下:

    • EVNTEN Enables or disables the generation of events
    • EVNTI Controls the rate of events
    • EVNTDIR Controls when the event is generated

    EVNTI:指定了0到15范围内的位位置。当所选位置的位发生变化时,将产生事件。例如,如果EVNTI设置为3,则当计数的[3]位发生变化时,将生成一个事件。

    EVNTDIR:控制所选位从1到0或从0到1转换时是否生成事件。

    总结

    该表总结了本节中讨论的不同计时器的信息。

    在这里插入图片描述

    对于这些计时器,虚拟偏移量(CNTVOFFSET_EL2)总是表现为0。因此,尽管这些计时器与虚拟计数值进行比较,但实际上它们使用的是物理计数器值。

    Subjects to re-direction when HCR_EL2.E2H==1

    数据结构

    问题:如何抽象一个定时器?

    回答:xen中用下面的数据结构来表示timer。

    /* xen/include/xen/timer.h */
    struct timer {
        s_time_t expires; // System time expiry value (nanoseconds since boot):系统时间到期值
    
        union {     // Position in active-timer data structure
            unsigned int heap_offset; // Timer-heap offset (TIMER_STATUS_in_heap)
            struct timer *list_next;  // Linked list (TIMER_STATUS_in_list)
            struct list_head inactive;// Linked list of inactive timers TIMER_STATUS_inactive
        };
    
        /* On expiry, '(*function)(data)' will be executed in softirq context. */
        void (*function)(void *); // timer处理函数(当cnt值达到expires时就会触发运行)
        void *data;               // timer处理函数的参数
    
        uint16_t cpu;   // 将在其上安装和执行此计时器的CPU
    
        uint8_t status; // timer 状态
        /* 包含如下几种状态:
         * TIMER_STATUS_invalid     : Should never see this
         * TIMER_STATUS_inactive    : Not in use; can be activated
         * TIMER_STATUS_killed      : Not in use; cannot be activated
         * TIMER_STATUS_in_heap     : In use; on timer heap
         * TIMER_STATUS_in_list     : In use; on overflow linked list
         */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    timer是管理一个定时器实例,xen中的timers就是管理一个cpu中的所有timer实例,两者在单词就差一个s,切记需要认真区分。

    /* xen/common/timer.c */
    struct timers {
        spinlock_t     lock;
        struct timer **heap;
        struct timer  *list;
        struct timer  *running;    /* cpu正在运行执行的timer */
        struct list_head inactive;
    } __cacheline_aligned;
    
    static DEFINE_PER_CPU(struct timers, timers); // 每个cpu都有一份
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    流程分析

    timer系统初始化

    --> start_xen
        -------- (1) 获取设备树的节点信息,如频率,各组定时器的中断号 --------
        --> preinit_xen_time // 获取timer节点,获取cpu_khz/boot_count
        	--> preinit_dt_xen_time
        		--> dt_device_noe timer = dt_find_matching_node // 获取设备树的timer节点
        		--> dt_device_set_used_by(timer, DOMID_XEN) // timer被xen占用
        		--> dt_property("clock_frequency", rate) // 若有 cpu_khz = rate/1000
        	--> cpu_khz=READ_SYSREG(CNTFRQ_EL0&CNTFRQ_MASK)/1000 //设备树没有设置运行频率,去读取CNTFRQ_EL0寄存器(在EL3阶段设置该寄存器)
        	--> platform_init_time //没有注册对应平台的init_time,空函数
        	--> boot_count = get_cycles // 系统启动时间 本质:READ_SYSREG64(CNTPCT_EL0)
        --> init_xen_time  // 获取timer中断号
        	--> init_dt_xen_time // 获取设备树的timer INIID,保存在timer_irq[]数组中
    
        -------- (2) 初始化timer寄存器,同时根据定时器中断号注册对应的服务函数 --------
        --> init_timer_interrupt // timer与gic的关联,注册对应的timer中断服务函数
        	--> WRITE_SYSREG64(0, CNTVOFF_EL2) // No VM-specific offset
        	--> WRITE_SYSREG(CNTHCTL_EL2_EL1PCTEN, CNTHCTL_EL2) //Kernel/user can access to physical counter
        	--> WRITE_SYSREG(0, CNTP_CTL_EL0) // Physical timer disabled
        	--> WRITE_SYSREG(0, CNTHP_CTL_EL2) // Hypervisor's timer disabled
        	--> request_irq(timer_irq[TIMER_HYP_PPI],timer_interrupt) // 注册HYP timer handler
        	--> request_irq(timer_irq[TIMER_VIRT_PPI], vtimer_interrupt)
        	--> request_irq(timer_irq[TIMER_PHYS_NONSECURE_PPI],timer_interrupt)
    
        -------- (3) 初始化软件定时器和调试软定时器接口 --------
        --> timer_init // timer定时的软中断注册
        	--> open_softirq(TIMER_SOFTIRQ,timer_softirq_action) // 注册timer软中断
        	--> cpu_callback(&cpu_nfb, CPU_UP_PREPARE, cpu) // Only initialise ts once
        		--> struct timers *ts = &per_cpu(timers, cpu)
                --> INIT_LIST_HEAD(&ts->inactive)
                --> spin_lock_init(&ts->lock)
                --> ts->heap = dummy_heap
        	--> register_cpu_notifier(&cpu_nfb) // 注册给非boot cpu初始化 struct timers
        	--> register_keyhandler('a', dump_timerq) // timer调试接口
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    初始化timer

    init_timer(struct timer *timer,       // 一个timer抽象出一个struct timer定时器实体
               void (*function)(void *), // 处理函数
               void *data,               // 处理函数的参数
               uint  cpu)                // 绑定到具体cpu上
    {
        timer->function = function;
        timer->data = data;
        timer->cpu = cpu;
        timer->status = TIMER_STATUS_inactive; // 初始化的timer的状态为inactive
        list_add(&timer->inactive, &per_cpu(timers, cpu).inactive); // 加到链表中维护
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    初始化后,只有expires的值以及status还没准备好,其他的资源都就绪了。

    设置timer

    void set_timer(struct timer *timer, s_time_t expires)
    {
        /* 1: 设置前需要判断timer是否在激活状态,如果是,需要更改如下内容:
         * (1)从对应的链表删除该timer,根据情况抛出cpu_raise_softirq(timer->cpu, TIMER_SOFTIRQ)
         * (2)timer->status = TIMER_STATUS_inactive
         * (3)添加到timers.inactive链表中
         */
        if ( active_timer(timer) ) // TIMER_STATUS_in_heap/in_list为活跃状态
            deactivate_timer(timer); 
        /* 2: 设置定时到期点 */
        timer->expires = expires;
    	/* 3: 激活timer,更改状态 */
        activate_timer(timer);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    激活timer

    static inline void activate_timer(struct timer *timer)
    {
        ASSERT(timer->status == TIMER_STATUS_inactive);
        timer->status = TIMER_STATUS_invalid;
        list_del(&timer->inactive); //从inactive链表中删除需要激活的timer
    
        if ( add_entry(timer) ) /* 把该timer添加到per cpu变量timers的heap以及list里面 */
            cpu_raise_softirq(timer->cpu, TIMER_SOFTIRQ); /* 唤醒timer软中断,处理timers->list */
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    停止timer

    void stop_timer(struct timer *timer)
    {
        if ( active_timer(timer) )
            deactivate_timer(timer);
    }
    
    /* 判断timer是否激活状态,激活状态机为 TIMER_STATUS_in_heap || TIMER_STATUS_in_list */
    bool active_timer(const struct timer *timer)
    {
        return timer_is_active(timer);
        /* timer->status >= TIMER_STATUS_in_heap */
    }
    
    void deactivate_timer(struct timer *timer)
    {
        if ( remove_entry(timer) ) // 之前状态为active的,需要从对应链表剔除timer
            cpu_raise_softirq(timer->cpu, TIMER_SOFTIRQ); // 同时抛出软中断,处理其他状态
    
        timer->status = TIMER_STATUS_inactive; // timer状态值更改为inactive
        list_add(&timer->inactive, &per_cpu(timers, timer->cpu).inactive); // 插入inactive链表
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    执行timer

    前面有提到,xen注册了3个timer中断,分别是物理timer、虚拟timer以及hyp timer(phys=30 virt=27 hyp=26)

    其中物理timer和hyp timer共用一个中断服务函数timer_interrupt,虚拟timer使用vtimer_interrupt中断服务函数。

    timer_interrupt

    static void timer_interrupt(int irq, void *dev_id, struct cpu_user_regs *regs)
        if ( irq == (timer_irq[TIMER_HYP_PPI]) && READ_SYSREG(CNTHP_CTL_EL2) & CNTx_CTL_PENDING ) /* 中断属于hyp并且检查产生了中断 */
        {
            perfc_incr(hyp_timer_irqs); /* hyp_timer_irqs用来统计产生了多少次hyp timer中断 */
            raise_softirq(TIMER_SOFTIRQ); /* 唤醒timer软中断来处理 */
            WRITE_SYSREG(0, CNTHP_CTL_EL2); /* disable EL2 timer interrupt */
        }
    	if ( irq == (timer_irq[TIMER_PHYS_NONSECURE_PPI]) && READ_SYSREG(CNTP_CTL_EL0) & CNTx_CTL_PENDING ) /* 中断属于phy */
        {
            perfc_incr(phys_timer_irqs); /* phys_timer_irqs用来统计产生了多少次phy timer中断 */
            raise_softirq(TIMER_SOFTIRQ); /* 唤醒timer软中断来处理 */
            WRITE_SYSREG(0, CNTP_CTL_EL0); /* disable EL1 timer interrupt */
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    两者都调用raise_softirq(TIMER_SOFTIRQ)来唤醒软中断来做进一步处理,该软中断对应的服务函数为:

    static void timer_softirq_action(void)
        /* Execute ready heap timers. */
        while ( (heap_metadata(heap)->size != 0) && ((t = heap[1])->expires < now) )
        {
            remove_from_heap(heap, t);
            execute_timer(ts, t);
        }
        /* Execute ready list timers. */
        while ( ((t = ts->list) != NULL) && (t->expires < now) )
        {
            ts->list = t->list_next;
            execute_timer(ts, t);
        }
    	......
        if ( !reprogram_timer(this_cpu(timer_deadline)) ) /* 该函数会重新enable EL2定时器中断 */
            raise_softirq(TIMER_SOFTIRQ);        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    static void execute_timer(struct timers *ts, struct timer *t)
        void (*fn)(void *) = t->function;
        void *data = t->data;
    
        t->status = TIMER_STATUS_inactive;
        list_add(&t->inactive, &ts->inactive); /* 处理完该timer后,重新插入inactive链表里面 */
    
        ts->running = t;
        spin_unlock_irq(&ts->lock);
        (*fn)(data); /* 运行init_timer设置的timer处理函数 */
        spin_lock_irq(&ts->lock);
        ts->running = NULL;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    vtimer_interrupt

    static void vtimer_interrupt(int irq, void *dev_id, struct cpu_user_regs *regs)
        if ( unlikely(is_idle_vcpu(current)) )
            return;
    
        perfc_incr(virt_timer_irqs);
    
        current->arch.virt_timer.ctl = READ_SYSREG(CNTV_CTL_EL0);
        WRITE_SYSREG(current->arch.virt_timer.ctl | CNTx_CTL_MASK, CNTV_CTL_EL0);
        vgic_inject_irq(current->domain, current, current->arch.virt_timer.irq, true);   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    疑问

    问题描述回答
    1CNTFRQ_EL0由硬件自动设置,用于报告系统计数的频率?FALSE. It is the responsibility of boot software in EL3 to populate the register with the correct value
    2在中断处理程序中,软件如何清除定时器中断?It can set IMASK (masking interrupts), it can clear ENABLE (disabling the timer) or update CVAL/TVAL
    3当使用通用定时器生成事件流时,如何控制事件的速率?EVNTI通过选择必须更改计数中的哪个位以生成事件来控制事件的速率。EVNTDIR控制触发事件的位的转换是0到1还是1到0
    4中断关联章节的中断号INITID和设备树timer节点写的中断号(10、11、13、14)有什么关系?定时器中断属于PPI中断类型,其范围是16~31,所以要加上偏移16,才是真正的物理INITID
    5struct timers 结构体里的heap成员是做什么用的?
    6什么时候使用virt timer,什么时候使用phy timer?
  • 相关阅读:
    TCP/IP 七层架构模型
    2023年【陕西省安全员C证】最新解析及陕西省安全员C证试题及解析
    java基于springboot的学院资产管理系统
    vue项目中使用高德地图
    WordPress多语言翻译插件小语种互译
    过午不食有依据吗
    浅谈 UUID 生成原理及优缺点
    计算机网络网络层数据链路层协议详解
    Mybatis小记
    Rust 笔记:Rust 语言中的字符串
  • 原文地址:https://blog.csdn.net/u012010054/article/details/132999118