内核调度抢占模式——voluntary和full对比

一、背景

在之前的内核调度子系统专栏里,我们已经把调度有关的如CFS调度/RT调度,调度时间片,调度时延,cfs唤醒抢占特性,这些基本概念和细节都讲了一遍。其实这些细节更多的是帮助我们理解调度系统是如何运作的,调度系统里的大部分参数其实我们都是不会去调整,或者说不敢去做大的调整的(除了cfs唤醒抢占特性从我个人角度在一些pub/sub模型比较多的系统上,我是觉得应该是调整关闭它的)。

这篇博客我们讲到的这个内核调度抢占模式,是我们可以去配置去做选择的,而且也是一个常见的选择配置。

我们在第二章里先讲一下桌面版linux默认的配置CONFIG_PREEMPT_VOLUNTARY,以及和CONFIG_PREEMPT的区别。然后我们在第三章里做一些实验来印证他们的差别。

在讲voluntary模式和full模式的区别之前,有必要提一下preempt_rt模式,这个模式是在打上rt-linux补丁以后默认选用的模式,这个模式要比full模式还要更加的重视实时性,如果要简单来概括一下为什么打上rt-linux补丁以后系统会更加实时,是因为rt-linux补丁让spinlock不再禁用抢占,另外,把中断都默认线程化,还默认开启了防优先级反转的rt-mutex开关,主要是这三个改动,让系统变得更加实时。详细细节会在后面的实时补丁相关的博文里展开。

二、普通linux内核默认的voluntary模式,和full模式的区别

在之前的 CFS及RT调度整体介绍_cfs rt 调度-CSDN博客 博客里已经讲到了时钟中断进来以后进行时间片检查判断等一系列逻辑,来决定是否需要进行上下文切换。

这里说的上下文切换,也就是抢占,即preempt。voluntary和full是普通内核抢占的两种主要的模式。

2.1 preempt模式(voluntary/full)的选择影响的只是内核态执行期间的行为

普通linux内核(非rt-linux)默认选用的是CONFIG_PREEMPT_VOLUNTARY的抢占模式:

CONFIG_PREEMPT_VOLUNTARY表示的是在一些自愿的可以允许抢占的点,可以被其他进程抢占。这里需要强调是,这些抢占选项都是在讨论内核态期间的抢占事情,用户态的代码在tick时钟中断进来以后,判断要进行抢占的话都是会立马进行抢占的,与这里的抢占选项是voluntary还是full无关。

2.2 关于禁用抢占

说到抢占,就得提到禁用抢占,一旦某个cpu禁用抢占,那么这个cpu上执行的线程就会继续执行,知道禁用抢占的行为被取消,cpu上才能发生线程切换。所以,一旦是禁用抢占状态,无论是哪种抢占模式,这个cpu都无法进行调度

禁用抢占是一个只会在内核态发生的事情,用户态执行期间,是不会出现“禁用抢占”状态的

为什么用户态代码执行期间会不会禁用了抢占,这个得从源头上来阐述,之所以要禁用抢占是用于在内核态代码执行关键逻辑期间不期望被打断执行,以免发生逻辑上因为嵌套和负载均衡迁移所导致的cpu变化等情况才禁用的抢占。

所以有两个基础的约定:

1)抢占的禁用得是非常短暂的,保护的是一个“原子的操作”;

2)禁用抢占所需要保护的行为是一个系统底层逻辑行为,与系统底层的数据结构相关,自然也得是在内核态执行,一旦退出了内核态回到用户态,也就脱离了这些底层的逻辑,也就自然不需要去禁用抢占了;

抢占的禁用是一个计数的累加,所有的禁抢占的计数在内核态回到用户态时都一定会清完成0。

2.2.1 哪些情况会出现禁用抢占

最最常见的就是spin_lock,另外,还有使用percpu变量时,tracepoint触发的逻辑里,线程销毁前的最后资源释放时等等。

2.3 voluntary模式在内核态期间只允许might_sleep的位置才可以发生线程切换

内核里除了might_sleep调用的位置,还有cond_resched调用的位置也可以发生线程切换,当然还有一些其他的类似的函数。

要注意,might_sleep这些调度点不能在禁用抢占期间调用。这也很好理解,我关了抢占,别人跑不了,我自己释放了给谁跑呢。

反过来说,如果voluntary模式下,在执行内核代码期间,如果tick进来以后判断出需要进行线程调度,也就是reschedule的标志位置上了,在时钟中断处理完退出时,这时候当前这个线程的内核态代码会继续执行,直到这个线程的内核代码执行完返回用户态,或者主动调用might_sleep/cond_resched,系统才会把它切换出去,换其他饥饿的线程来执行。

2.4 full模式在检查到需要进行调度后会里面进行线程切换,除非禁用了抢占

full模式的情况,在tick中断进来以后,检查到需要进行调度后,在该tick硬中断处理完成以后,如果这时候没有禁用抢占,则会立马进行线程切换,这一点会在 3.2 有例子验证。

对于有禁用抢占的情况,遇到tick中断进来,检查到需要进行调度后,会在抢占不再被禁用的那一刻进行线程切换,这一点会在 3.4 有例子验证。

三、voluntary模式和full模式的实验

这一章里会把在第二章里讲到的一些细节做一些实验验证。

通过抓perf record -g -C <cpu>来抓切换时刻的堆栈,

或通过cat /proc/<pid>/schedstat看线程的调度时延(关于如何通过系统方法或者一些定制的方法看调度时延可以参考之前的博客 调度时延的观测_csdn 调度时延的观测 杰克崔-CSDN博客)

这两个方式可以帮我们观测调度的情况,并了解内核里进行调度的时机。

如果是重编内核的方式,来使用voluntary模式还是full模式,是通过配置config里的下面两个互斥的config来配置

CONFIG_PREEMPT_VOLUNTARY=y

表示启用voluntary模式

CONFIG_PREEMPT=y

表示启用full模式

这里要注意,是CONFIG_PREEMPT而不是CONFIG_PREEMPTION,关于CONFIG_PREEMPTION在下面的例子里讲相关原理时会介绍

3.1 voluntary模式和full模式更便捷的切换方法——CONFIG_PREEMPT_DYNAMIC选项

如果打开了CONFIG_PREEMPT_DYNAMIC选项,voluntary模式和full模式可以不用编译内核进行切换,直接设置cmdline即可。

如下图,设置的是full模式:

关于preempt的dynamic的选项进行的动态切换逻辑,是在kernel/sched/core.c里的__setup("preempt=", setup_preempt_mode);根据传入的参数,在调用setup_preempt_mode时调用sched_dynamic_update进行模式选择的相关函数的准备。

如下图:

关于上图中的部分,有几个点需要介绍:

1)preempt_dynamic_enable和preempt_dynamic_disable两个宏是借助了内核里的static call机制,在下面 3.1.1 详细介绍,简单来说就是静态或者动态的修改函数指针实现高效的函数重新指向

2)cond_resched和might_resched仅仅在voluntary模式下才需要使用。为什么?因为preempt_none内核态完全不抢占当然不需要检查抢占,preempt_full则在需要抢占时立马执行了抢占,也不需要在cond_resched和might_resched里才去做抢占,这里面有禁用抢占场景的例子,见 3.4 一节

3)preempt_schedule仅仅在full模式下才需要使用。那么问题来了,preempt_voluntary怎么做的线程切换呢,preempt_voluntary模式线程切换时在直接调用的schedule函数,分为exit_to_user_mode_prepare(3.2 一节里有例子),或是might_sleep时调用的__cond_resched再到__schedule进行的线程切换(3.5 一节里有例子),或是下图中的schedule_idle再到__schedule进行的快进入idle时前的检查有任务要跑的线程切换,关于schedule_idle以及其他的与负载均衡有关的逻辑关注后面的博文。

关于按照编译选项进行preempt模式的初始化的逻辑:

默认初始化为undefined

这样在preempt_dynamic_init里就按照编译选项开关选择用那个mode调用sched_dynamic_update

sched_dynamic_update函数在上面已经有介绍

3.1.1 内核static call机制

单独用一节来讲这个机制,除了做介绍以外,更重要的是让大家知道内核里有这么一个机制,可以实现函数的重新指向,其目的是为了更高的性能:节省一个jmp命令,减少指令缓存的压力。

下面的截图是一个编译器的一个函数定义的选择,在内核里是能编过的,但是很明显,下面这些都是在编译器时确定的,而不能动态更新

如果要动态更新,得使用函数指针,并在函数更新时重新赋值函数指针,在真正做函数调用时,是通过一个跳板函数,再去跳真正要执行的函数,而static call这套机制就省去了这个jmp,而是直接跳到这个真正要执行的函数。更细的细节,这里并不展开。

说一下使用,在include/linux/static_call.h里:

在3.4.3.1 里有介绍preempt_schedule函数,里面有个__preempt_schedule宏如下定义:

这就是在调用该机制的函数时的一个例子

3.2 无论是voluntary模式还是full模式,若检查到需要进行调度,用户态代码都是会立马进行线程切换

针对这个case,我们基于voluntary模式,来进行测试:

下图的操作,先执行若干个deadloop程序,绑核到20上执行,再执行一个实时线程(下面有展示watch_rt.sh的脚本,脚本里面是chrt -r过这个启动的deadloop)并用watch去观测它的调度实验,下图为了展示更加方便,就直接用cat,隔10秒再cat来展示,调度时延没有明显增长。

关于watch_rt.sh的脚本如下:

#!/bin/bash# 检查传入的参数
if [ $# -eq 0 ]; thenecho "Usage: $0 <command> [args...]"exit 1
fi# 获取去掉 ./ 的命令名
command_name="${1#.*/}"  # 去掉 ./ 或者任何前缀# 使用 pkill 杀死相应的进程
pkill "$command_name"# 执行程序并获取 PID
taskset -c 20 chrt -r 80 "$@" &  # 以后台进程运行
pid=$!  # 获取最后一个后台进程的 PID# 输出 PID
echo "Started process with PID: $pid"# 使用 watch 命令监控 /proc/<pid>/schedstat
watch -n 0.1 "cat /proc/$pid/schedstat"

可以看到运行了很久,这个rt线程的调度延迟也是比较小的,而且不会随着时间明显增长

这种用户态时接收到tick硬中断进来,并进行调度时的堆栈情况,用perf sched record -g -C <cpu>可以抓到,如下图:

上图中可以看到,这种用户态代码运行期间进行线程切换时的堆栈,是在硬件tick中断进来处理完逻辑以后,exit_to_user_mode_prepare里进行schedule的。关于硬件tick中断进来以后具体做了哪些事,参考之前的 CFS及RT调度整体介绍_cfs rt 调度-CSDN博客 博客。

3.3 full模式下,内核代码执行期间,如果检查到需要进行调度,若没有禁用抢占,会立马进行线程切换

注意,这一节的标题里,有内核代码执行期间这个条件,对于用户态代码,在 3.2 一节中已经举例了。

这一节的操作省去了 3.2 一节里已经介绍的观测调度时延的操作步骤,这里只写抓出线程切换堆栈的步骤,来证明是否在内核态执行期间,检查到需要进行调度,会立马进行线程切换。

实验的步骤是先绑核运行几个死循环程序在核20上:

taskset -c 20 ./deadloop &

然后写一个insmod的ko,内核执行的代码如下:

下面就是执行一段持续10秒的死循环,循环里啥也不做

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/spinlock.h>
#include <linux/delay.h>
#include <linux/jiffies.h>MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("A simple Linux kernel module using spinlock running 10 second.");
MODULE_VERSION("1.0");static DEFINE_SPINLOCK(my_spinlock);static int __init testspinlock_init(void) {unsigned long flags;unsigned long start_time, end_time, start1, end1;int index = 0;printk(KERN_INFO "testspinlock: Module loaded.\n");printk(KERN_INFO "testspinlock: Spinlock enter/quit running for 10 second...\n");// 获取当前时间start_time = jiffies;// 死循环,直到时间超过 10 秒while (time_before(jiffies, start_time + HZ * 10)) {// 空循环等待//spin_lock(&my_spinlock);start1 = jiffies;//printk("run index[%d]\n", index);index++;//while (time_before(jiffies, start1 + HZ * 5)) {//}}printk(KERN_INFO "testspinlock: Spinlock released.\n");return 0;
}static void __exit testspinlock_exit(void) {printk(KERN_INFO "testspinlock: Module unloaded.\n");
}module_init(testspinlock_init);
module_exit(testspinlock_exit);

make出testspinlock_times.ko

然后,先执行下面的命令抓取20核上的调度行为及其调度行为时的堆栈

perf sched record -g -C 20

另起一个窗口,通过绑核insmod来执行上面这段循环10秒的内核代码

taskset -c 20 insmod testspinlock_times.ko

这个ko里的这个持续10秒的逻辑就会和该20核上的另外几个刚起的deadloop进程抢20核上的cpu资源去执行。

几秒以后,ctrl+C停止perf sched record -g -C 20的命令执行,输入下面的命令解析出调度行为相关的堆栈

perf script > script.txt

查看script.txt文件,就可以看到下面的截图

解释一下上图里的内容,可以看到insmod的这个20核上的进程,在full模式下,发现需要进行调度后,是立马进行了线程切换,可以看到线程切换时insmod线程的prev_state是R+的,并没有主动释放cpu时就被切换走了,切换的时刻是在x86上的timer的tick中断进来的

asm_sysvec_apic_timer_interrupt中断处理函数里到硬中断处理完时,检查打断前是用户态还是内核态,如果是内核态,就会执行到下面的irqentry_exit_cond_resched逻辑:

CONFIG_PREEMPTION一般都是打开的,而irqentry_exit_cond_resched在 3.1 一节中有过介绍,在CONFIG_PREEMPT_VOLUNTARY时是会被定义成空函数,而在CONFIG_PREEMPT_FULL时才会被定义成,如下图:

raw_irqentry_exit_cond_resched的实现如下:

最后调用到了上面perf sched record -g抓到的这个preempt_sched_irq函数里

3.4 full模式下,spinlock一旦unlock时就会检查reschedule标志并进行按需调度

上面已经证明了在内核态执行期间,检查到需要调度,会在tick中断处理完以后进行线程切换。

但是,如果这时候在内核态代码里使用了spinlock且还是在spinlock禁用抢占保护期间内呢,full模式是在后面什么时候进行的调度呢?——答案是在spin_unlock时也就是禁用抢占的count清零时,就会进行调度

内核代码如下,也增加了might_sleep,我们预期在full模式下,这个might_sleep没有作用:

直接上抓到的堆栈:

可以从perf script导出的文件里去搜__cond_resched是搜不到,关于__cond_resched在3.5一节中有例子介绍,搜不到__cond_resched,和预期的一致,就是full模式并不需要这个__cond_resched的逻辑,见 3.1 一节

为了再次确保,把might_sleep在spin_lock执行前也放一个:

内核代码:

依然搜不到__cond_resched

我们再研究一下这个spin_unlock到preempt_schedule的调用栈,理一下代码逻辑,再贴一下刚贴过的这个调用栈:

看到是_raw_spin_unlock->preempt_schedule_thunk->preempt_schedule

3.4.1 关于spin_lock和spin_unlock

先看一下spin_lock加锁的时候

spin_lock的代码在普通内核里直接定义成了

而raw_spin_lock定义成了_raw_spin_lock

而_raw_spin_lock调用到了__raw_spin_lock

__raw_spin_lock的实现:

再回到正题,看解锁的时候:

spin_unlock调用了raw_spin_unlock再定义成了_raw_spin_unlock再调用了__raw_spin_unlock

这么跟代码有时候会比较难受,因为会存在多个定义,另外,很多时候因为有inline函数以及编译器的优化,导致代码一级级很难轻松找到,所以我们顺便讲一下通过objdump -S解析出带源文件和汇编对应关系的文件,来看就会好定位很多

3.4.2 通过objdump -S导出到文件,再找调用链

objdump -S vmlinx > vmlinux.txt

注意这个vmlinx不是vmlinz,不能压缩,要用编出来的原始的大的文件来objdump

我们看vmlinux.txt里,搜索函数的方式:

/_raw_spin_unlock>:

注意要加上>:这两个符号,可以准确定位到函数的定义的位置。

怎么来看这个文件呢?

通过通过下图中的0x2d

找到_raw_spin_unlock的起始的0xffffffff81f05780的位置加上2d也就是0xffffffff81f057ad

0xffffffff81f057ad的上一条也就是下图红色框

3.4.3 通过正向的vmlinux解析出来的文件和反向的堆栈看到的preempt_schedule_thunk往前推,找到了最终的_raw_spin_unlock到preempt_schedule_thunk的调用链

spin_unlock

    ->raw_spin_unlock

    即_raw_spin_unlock

        ->__raw_spin_unlock

            ->preempt_enable

                ->preempt_count_dec_and_test

                ->__preempt_schedule

include/linux/preempt.h下有

preempt_enable的定义:

但是从__preempt_schedule到preempt_schedule_thunk还有一些细节

3.4.3.1 关于preempt_schedule函数

我们在代码里可以搜到preempt_schedule的export_symbol如下:

从__preempt_schedule到preempt_schedule_thunk还有一些细节,

__preempt_schedule是在x86里被定义成了下面

关于 STATIC_CALL_TRAMP_STR 就是内核static call机制,见 3.1.1

所以__preempt_schedule宏是去找preempt_schedule实际对应的函数,如下:

而preempt_schedule_dynamic_enabled在preempt.h里定义如下:

而preempt_schedule在full模式下会被定义成preempt_schedule_thunk

关于preempt_schedule_thunk是preempt_schedule的封装,相关的注释如下:

代码上,其实就是做一些额外的工作后再调用preempt_schedule

3.5 voluntary模式下,未禁用抢占时,might_sleep抢占点可以进行线程切换

和之前的几节的例子差不多,先通过./watch_rt ./deadloop来看调度时延是否和理论的一致

下图中的操作是先执行中间的perf sched record -g -C 20的操作抓20核上调度相关事件,再绑核20进行insmod执行加了might_sleep逻辑的内核代码(这个内核代码持续10秒),然后马上执行左边的./watch_rt.sh ./deadloop_50(deadloop_50是一半时间死循环,避免rt的95%时间的超限导致的调度时延增加),可以看到最后insmod退出后,看这个rt的deadloop_50的调度时延也并没有明显变大。

上面执行的内核代码在每个循环内部的spin_lock和spin_unlock之后增加might_sleep逻辑:

这期间抓的perf下图中可以看到在insmod时调用的might_sleep最后调用的是__cond_resched再调用__schedule进行的调度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/466494.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【网络原理】关于HTTP状态码以及请求的构造的哪些事

前言 &#x1f31f;&#x1f31f;本期讲解关于HTTP协议的重要的机制~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不…

Day13杨辉三角

给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 class Solution {public List<List<Integer>> generate(int numRows) {List<List<Integer>> res new Arra…

Docker基本概念汇总(更全面了解Docker)

Docker是一种开源的平台&#xff0c;用于开发、部署和运行应用程序。它通过“容器”技术实现了轻量级虚拟化&#xff0c;使应用程序和其依赖项能够一起打包、部署并运行。以下是Docker基本概念的详细解释。 图片来源网络 1. Docker 容器&#xff08;Container&#xff09; 容…

OpenCV视觉分析之目标跟踪(8)目标跟踪函数CamShift()使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 找到物体的中心、大小和方向。 CamShift&#xff08;Continuously Adaptive Mean Shift&#xff09;是 OpenCV 中的一种目标跟踪算法&#xff0…

【每日刷题】Day151

【每日刷题】Day151 &#x1f955;个人主页&#xff1a; 开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 【模板】01背包_牛客题霸_牛客网 【模板】01背包_牛客题霸_牛客网 //思路&#xff1a;动态规划 #incl…

学习Vue之商城案例(代码+详解)

目前&#xff0c;我们学习Vue的一些基础的知识&#xff0c;那么就让我们做一个像下图这样简单的商城案例吧。 目录 通过脚手架创建项目 安装axios和bootstrap组件 安装axios和bootstrap 在保存的时候不进行格式化校验 初步定义App.vue文件 初步渲染组件页面 根据接口渲染…

【测试】【Debug】vscode中同一个测试用例出现重复

这种是正常的情况 当下面又出现一个 类似python_test->文件夹名->test_good ->test_pad 同一个测试用例出现两次&#xff0c;名称都相同&#xff0c;显然是重复了。那么如何解决&#xff1f; 这种情况是因为在终端利用“pip install pytest”安装 之后&#xff0c;又…

基于C++的决策树C4.5机器学习算法(不调包)

目前玩机器学习的小伙伴&#xff0c;上来就是使用现有的sklearn机器学习包&#xff0c;写两行代码&#xff0c;调调参数就能跑起来&#xff0c;看似方便&#xff0c;实则有时不利于个人能力发展&#xff0c;要知道现在公司需要的算法工程师&#xff0c;不仅仅只是会调参&#x…

Mac解决 zsh: command not found: ll

Mac解决 zsh: command not found: ll 文章目录 Mac解决 zsh: command not found: ll解决方法 解决方法 1.打开bash_profile 配置文件vim ~/.bash_profile2.在文件中添加配置&#xff1a;alias llls -alF键盘按下 I 键进入编辑模式3. alias llls -alF添加完配置后&#xff0c;按…

VBA10-处理Excel的动态数据区域

end获取数据边界 1、基本语法 1-1、示例&#xff1a; 2、配合row和column使用 2-1、示例1 2-2、示例2 此时&#xff0c;不管这个有数值的区域&#xff0c;怎么增加边界&#xff0c;对应的统计数据也会跟着变的&#xff01;

无人车之路径规划篇

无人车的路径规划是指在一定的环境模型基础上&#xff0c;给定无人车起始点和目标点后&#xff0c;按照性能指标规划出一条无碰撞、能安全到达目标点的有效路径。 一、路径规划的重要性 路径规划对于无人车的安全、高效运行至关重要。它不仅能够提高交通效率&#xff0c;减少交…

【前端基础】CSS基础

目标&#xff1a;掌握 CSS 属性基本写法&#xff0c;能够使用文字相关属性美化文章页。 01-CSS初体验 层叠样式表 (Cascading Style Sheets&#xff0c;缩写为 CSS&#xff09;&#xff0c;是一种 样式表 语言&#xff0c;用来描述 HTML 文档的呈现&#xff08;美化内容&#…

一种高度集成的数字化管理平台:城市管理综合执法系统(源码)

什么是城市管理综合执法系统&#xff1f; 城市管理综合执法系统是一种高度集成的数字化管理平台&#xff0c;它旨在通过整合信息技术资源&#xff0c;实现对城市环境、秩序、设施等多方面的综合管理和高效执法。 城市管理综合执法系统通常包含以下几个核心要素和功能&#xff…

【Python】强大的正则表达式工具:re模块详解与应用

强大的正则表达式工具&#xff1a;re模块详解与应用 在编程和数据处理中&#xff0c;字符串的处理是不可避免的一项任务。无论是从文本中提取信息、验证数据格式&#xff0c;还是进行复杂的替换操作&#xff0c;正则表达式&#xff08;Regular Expression&#xff0c;简称Rege…

计算机毕业设计Python+图神经网络手机推荐系统 手机价格预测 手机可视化 手机数据分析 手机爬虫 Django Flask Spark 知识图谱

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

03.DDD六边形架构

学习视频来源&#xff1a;DDD独家秘籍视频合集 https://space.bilibili.com/24690212/channel/collectiondetail?sid1940048&ctype0 文章目录 什么是依赖DDD四层架构六边形架构代码实现 想要详细了解六边形架构&#xff0c;可以看我之前的一篇文章。是对六边形架构原文的翻…

前端开发实现自定义勾选/自定义样式,可复选,可取消勾选

基于后端返回数组实现多选、复选 以下代码基于vue2&#xff0c;如果有需要React/Vue3或者其他框架代码的&#xff0c;可以通过国内直连GPT4o进行代码转换&#xff0c;转换正确率99% 前端代码如下(直接拷贝到你的vue代码即可)&#xff1a; <!-- CustomCheckboxList.vue --&g…

新型智慧城市顶层设计方案(118页word)

文档介绍&#xff1a; 新型智慧城市顶层设计方案是一种全局性、前瞻性的规划&#xff0c;旨在通过整合城市各类资源&#xff0c;运用新一代信息技术&#xff0c;推动城市治理、民生服务、产业发展等领域的全面升级&#xff0c;以实现城市的可持续发展和居民生活质量的提升。该…

nginx-proxy-manager实现反向代理+自动化证书(实战)

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 cnginx-proxy-manager实现反向代理自动化证书 nginx-proxy-manager是什么搭建nginx-proxy-manage…

定时器入门:Air780E定时器基础与进阶

今天我们学习的是Air780E定时器基础与进阶&#xff0c;让大家更深入的了解定时器。 一、定时器(timer)的概述 在Air780E模组搭载的LuatOS系统中&#xff0c;定时器&#xff08;timer&#xff09;是一项基础且关键的服务。它允许开发者在特定的时间点或周期性地执行代码段&…