Linux 中 initcall 机制详解

源码基于:Linux 5.4

 

 

0. 前言

Linux 对驱动程序提供静态编译进内核和动态加载两种方式,当采用静态方式时,开发者如果想要在系统中启动这个驱动通常调用类似 xxx_init() 接口。

最直观的做法:开发者试图添加一个驱动初始化程序时,在内核启动 init 程序的某个地方直接添加调用自己驱动程序的 xxx_init() 接口函数,在内核启动时就自然会启动这个驱动程序,类似:

void kernel_init()
{a_init();b_init();...m_init();
}

但是,这种做法在小系统中或许可以,对于 linux 庞大的系统来说,驱动很多,不可能每添加一个驱动就会改动一下 kernel_init() 代码,这将会是一场灾难。

Linux 内核提供了解决方案:

  • 在编译的时候,通过使用告知编译器连接,自定义一个专门用来存放这些初始化函数的地址段,将对应的函数入口统一放在一起;
  • 驱动程序中调用linux 内核提供的专门的 xxx_init() 接口,由编译器来收集这些入口函数,集中存放在一个地方;
  • 内核启动时,统一扫描这段的开始地址,按照顺序执行被添加的驱动初始化程序;
  • init 初始化代码,基本上只会执行一次,因此在这类 xxx_init() 代码所在的特殊段在初始化 完成之后会被内存管理器回收,同时节省了这部分的内存;

1. initcall 源码

上文提到过 Linux 对驱动程序提供静态编译进内核和动态加载两种方式,Linux 的 initcall 机制也是根据静态编译和动态加载的两种方式选择不同的编译、运行流程。

include/linux/init.h#ifndef MODULE... //静态加载#else... //动态加载#endif

MODULE 是在编译的时候,通过编译器参数来传入。例如,在编译 ko 时会使用如下两个编译选项,如果是链接到内核,则不会使用:

//MakefileKBUILD_AFLAGS_MODULE  := -DMODULE
KBUILD_CFLAGS_MODULE  := -DMODULE

通过 MODULE 的配置,选择静态编译还是动态加载。

本文将分开单独剖析这两种情况下的 initcall 机制。

2. 静态编译

2.1 initcall 接口 

include/linux/init.h/** Early initcalls run before initializing SMP.** Only for built-in code, not modules.*/
#define early_initcall(fn)		__define_initcall(fn, early)/** A "pure" initcall has no dependencies on anything else, and purely* initializes variables that couldn't be statically initialized.** This only exists for built-in code, not for modules.* Keep main.c:initcall_level_names[] in sync.*/
#define pure_initcall(fn)		__define_initcall(fn, 0)#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)#define __initcall(fn) device_initcall(fn)#define __exitcall(fn)						\static exitcall_t __exitcall_##fn __exit_call = fn#define console_initcall(fn)	___define_initcall(fn, con, .con_initcall)

对于静态编译 initcall 接口如上,其中 pure_initcall() 只能在静态编译中存在。

当然,对于静态编译的驱动也可以调佣 module_init() 接口:

include/linux/module.h#define module_init(x)	__initcall(x);#define module_exit(x)	__exitcall(x);

此时的 module_init() 就是 device_initcall()。

2.2 initcall 级别

2.3 __define_initcall()

include/linux/init.h#ifdef CONFIG_LTO_CLANG/** With LTO, the compiler doesn't necessarily obey link order for* initcalls, and the initcall variable needs to be globally unique* to avoid naming collisions.  In order to preserve the correct* order, we add each variable into its own section and generate a* linker script (in scripts/link-vmlinux.sh) to ensure the order* remains correct.  We also add a __COUNTER__ prefix to the name,* so we can retain the order of initcalls within each compilation* unit, and __LINE__ to make the names more unique.*/#define ___lto_initcall(c, l, fn, id, __sec) \static initcall_t __initcall_##c##_##l##_##fn##id __used \__attribute__((__section__( #__sec \__stringify(.init..##c##_##l##_##fn)))) = fn;#define __lto_initcall(c, l, fn, id, __sec) \___lto_initcall(c, l, fn, id, __sec)#define ___define_initcall(fn, id, __sec) \__lto_initcall(__COUNTER__, __LINE__, fn, id, __sec)
#else#define ___define_initcall(fn, id, __sec) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(#__sec ".init"))) = fn;
#endif
#endif#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

下文会继续细化分析,这里提前提示:

__define_initcall() 其实就是定义了一个 static initcall_t 的函数指针

include/linux/init.htypedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);

2.3.1 __used

include/linux/compiler_attributes.h#define __used                          __attribute__((__used__))

这是一种 attribute 修饰属性的一种,意思是告诉编译器:这个静态符号在编译的时候,即使没有使用也要保留,不能优化掉。

详细可以查看《__attribute__机制详解》一文。

2.3.1 __attribute__ ((__section__(...)))

__attribute__ 是 GNU C 的一大特色,可以用来修饰对象、函数、结构体类型等等。

这里用来修改 section,意思是将作用的函数放入指定的 section name 对应的段中。

详细可以查看《__attribute__机制详解》一文。

 

2.3.2 __stringify()

include/linux/stringify.h#define __stringify_1(x...)	#x
#define __stringify(x...)	__stringify_1(x)

将 __stringify() 中内容字符串化。

2.4 举例理解initcall接口

上面initcall 接口最终有各种宏转换,可能看着还是一头雾水。本小节用实例来剖析这个接口。

假如,我们在驱动使用如下接口:

module_init(hello_init);

那么,在编译的时候编译器会通过 initcall 接口产生:

static initcall_t __initcall_1_23_hello_init6 __attribute__(__used) \__attribute__((__section__(".initcall6.init..1_23_hello_init"))) = hello_init;

2.5 linux 编译后的initcall 函数

通过 arch64-linux-gnu-nm 或者 aarch64-linux-gnu/bin/nm 命令:

aarch64-linux-gnu/bin/nm -n vmlinux | grep -E -C 2 '_initcall.*(_start|_end)$'

会显示编译之后的 initcall 函数:

System.map:282341:ffffffc012032ee0 D __initcall_start
System.map-282342-ffffffc012032ee0 D __setup_end
System.map-282343-ffffffc012032ee8 d __initcall_224_66_trace_init_flags_sys_exitearly
--
System.map-282363-ffffffc012032f88 d __initcall_131_37_dummy_timer_registerearly
System.map-282364-ffffffc012032f90 d __initcall_312_768_initialize_ptr_randomearly
System.map:282365:ffffffc012032f98 D __initcall0_start
System.map-282366-ffffffc012032f98 d __initcall_241_771_bpf_jit_charge_init0
System.map-282367-ffffffc012032fa0 d __initcall_141_53_init_mmap_min_addr0
System.map-282368-ffffffc012032fa8 d __initcall_209_6528_pci_realloc_setup_params0
System.map-282369-ffffffc012032fb0 d __initcall_339_1143_net_ns_init0
System.map:282370:ffffffc012032fb8 D __initcall1_start
System.map-282371-ffffffc012032fb8 d __initcall_160_1437_fpsimd_init1
System.map-282372-ffffffc012032fc0 d __initcall_181_669_tagged_addr_init1
--
System.map-282427-ffffffc012033178 d __initcall_347_1788_init_default_flow_dissectors1
System.map-282428-ffffffc012033180 d __initcall_360_2821_netlink_proto_init1
System.map:282429:ffffffc012033188 D __initcall2_start
...

__initcall 后面跟 __COUNTER__ 和 __LINE__,接着加上初始化函数 fun,最后是 initcall 的级别。

当然通过命令 readelf 或者 objdump (objdump -h vmlinux.o)都能看到字段:

Sections:
Idx Name          Size      VMA               LMA               File off  Algn0 .initcall0.init 00000020  0000000000000000  0000000000000000  00000040  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA1 .initcall1.init 000001d0  0000000000000000  0000000000000000  00000060  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA2 .initcall2.init 00000138  0000000000000000  0000000000000000  00000230  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA3 .initcall2s.init 00000008  0000000000000000  0000000000000000  00000368  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA4 .initcall3.init 000000b0  0000000000000000  0000000000000000  00000370  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA5 .initcall3s.init 00000008  0000000000000000  0000000000000000  00000420  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA6 .initcall4.init 000004f0  0000000000000000  0000000000000000  00000428  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA7 .initcall4s.init 00000008  0000000000000000  0000000000000000  00000918  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA8 .initcall5.init 00000168  0000000000000000  0000000000000000  00000920  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA9 .initcall5s.init 00000008  0000000000000000  0000000000000000  00000a88  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA10 .initcall6.init 00001140  0000000000000000  0000000000000000  00000a90  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA11 .initcall7.init 00000140  0000000000000000  0000000000000000  00001bd0  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA12 .initcall7s.init 00000028  0000000000000000  0000000000000000  00001d10  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA13 .con_initcall.init 00000008  0000000000000000  0000000000000000  00001d38  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA14 .initcallearly.init 000000b8  0000000000000000  0000000000000000  00001d40  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA15 .initcallrootfs.init 00000008  0000000000000000  0000000000000000  00001df8  2**3CONTENTS, ALLOC, LOAD, RELOC, DATA

 

2.6 initcall 的函数如何被调用

init/main.cstart_kernel()---->arch_call_rest_init()---->rest_init()---->kernel_init()---->kernel_init_freeable()---->do_basic_setup()---->do_initcalls()
init/main.cstatic initcall_entry_t *initcall_levels[] __initdata = {__initcall0_start,__initcall1_start,__initcall2_start,__initcall3_start,__initcall4_start,__initcall5_start,__initcall6_start,__initcall7_start,__initcall_end,
};static void __init do_initcalls(void)
{int level;for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)do_initcall_level(level);
}

for 循环是一个指针数组,该数组会被存放在 __initdata 段。

另外,这个指针数组的类型为 initcall_entry_t,其实就是在上文第 2.3 节提到的 initcall_t 函数指针类型。

继续来看下这个指针数组中的元素:__initcall0_start ~ __initcall_end,而这些元素的值在本 c 文件中已经声明:

init/main.cextern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];

不难看出,initcall_levels 中存放的是这些函数指针数组的首地址。

那么这些实际的指针数组是在哪里呢?从上文initcall 函数,都会被定义成static initcall_t 类型,并且保存在 .initcall##level##.init 段中,那么 initcall_levels 与其是怎么关联的呢?

答案在 vmlinux.lds.h 中。

 

2.6.1 vmlinux.lds.h

include/asm-generic/vmlinux.lds.h#define INIT_CALLS_LEVEL(level)						\__initcall##level##_start = .;				\KEEP(*(.initcall##level##.init))			\KEEP(*(.initcall##level##s.init))			\#define INIT_CALLS							\__initcall_start = .;					\KEEP(*(.initcallearly.init))				\INIT_CALLS_LEVEL(0)					\INIT_CALLS_LEVEL(1)					\INIT_CALLS_LEVEL(2)					\INIT_CALLS_LEVEL(3)					\INIT_CALLS_LEVEL(4)					\INIT_CALLS_LEVEL(5)					\INIT_CALLS_LEVEL(rootfs)				\INIT_CALLS_LEVEL(6)					\INIT_CALLS_LEVEL(7)					\__initcall_end = .;

在这里首先定义了__initcall_start,将其关联到".initcallearly.init"段。

然后对每个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开之后的结果是定义 __initcall##level##_start,并将__initcall##level##_start关联到".initcall##level##.init"段和".initcall##level##s.init"段。

        __initcall_start = .;           \*(.initcallearly.init)          \__initcall0_start = .;          \*(.initcall0.init)              \*(.initcall0s.init)             \// 省略1、2、3、4、5__initcallrootfs_start = .;     \*(.initcallrootfs.init)         \*(.initcallrootfss.init)            \__initcall6_start = .;          \*(.initcall6.init)              \*(.initcall6s.init)             \__initcall7_start = .;          \*(.initcall7.init)              \*(.initcall7s.init)             \__initcall_end = .;

上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序。

.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。

2.6.2 do_initcall_level()

init/main.cstatic void __init do_initcall_level(int level)
{initcall_entry_t *fn;strcpy(initcall_command_line, saved_command_line);parse_args(initcall_level_names[level],initcall_command_line, __start___param,__stop___param - __start___param,level, level,NULL, &repair_env_string);trace_initcall_level(initcall_level_names[level]);for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)do_one_initcall(initcall_from_entry(fn));
}

do_one_initcall() 的参数就是获取到函数的函数指针。

init/main.cint __init_or_module do_one_initcall(initcall_t fn)
{...ret = fn();...return ret;
}

 

3. 动态加载

当模块以 ko 的形式存在,并被加载重定位到内核,其作用域和静态连接的代码是完全等价的。这种运行方式的优点:

  • 可根据系统需要运行动态加载模块,以扩充内核功能,不需要时可以将其卸载,以释放内存空间;
  • 当需要修改内核功能时,只需要编译模块,而不必重新编译整个内核;

当然,有些模块是必须要编译到内核,随内核一起运行,从不卸载,例如 vfs、platform_bus 等。

 

3.1 initcall 接口

当动态加载时,会在Makefile中添加上 MODULE 的定义:

//MakefileKBUILD_AFLAGS_MODULE  := -DMODULE
KBUILD_CFLAGS_MODULE  := -DMODULE

而initcall 代码也将从 init.h 转换到 module.h:

include/module.h#ifndef MODULE...#else#define early_initcall(fn)		    module_init(fn)
#define core_initcall(fn)		    module_init(fn)
#define core_initcall_sync(fn)		module_init(fn)
#define postcore_initcall(fn)		module_init(fn)
#define postcore_initcall_sync(fn)	module_init(fn)
#define arch_initcall(fn)		    module_init(fn)
#define subsys_initcall(fn)		    module_init(fn)
#define subsys_initcall_sync(fn)	module_init(fn)
#define fs_initcall(fn)			    module_init(fn)
#define fs_initcall_sync(fn)		module_init(fn)
#define rootfs_initcall(fn)		    module_init(fn)
#define device_initcall(fn)		    module_init(fn)
#define device_initcall_sync(fn)	module_init(fn)
#define late_initcall(fn)		    module_init(fn)
#define late_initcall_sync(fn)		module_init(fn)#define console_initcall(fn)		module_init(fn)/* Each module must use one module_init(). */
#define module_init(initfn)					\static inline initcall_t __maybe_unused __inittest(void)		\{ return initfn; }					\int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));/* This is only required if you want to be unloadable. */
#define module_exit(exitfn)					\static inline exitcall_t __maybe_unused __exittest(void)		\{ return exitfn; }					\void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));#endif

moudle_init() 共做了两件事情:

  • 定义 static initcall_t __inittest() { ... }
  • 声明 int init_module();

__inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型,不过不是 __inittest 类型在编译的时候会报错。所以真正使用的是 init_module() 函数的声明。

注意:

  • __copy(initfn):从 initfn 赋值函数属性,从 gcc-9 开始支持;
  • __attribute__((alias(#initfn))):为 init_module 创建别名,指向原来的 initfn;

这里alias 是 gcc 的特有属性,将定义 init_module 为函数initfn 的别名。即对于module_init() 作用就是定义一个变量名 init_module,其地址与 initfn 是一样的。

 

3.2 insmod

编译后的模块 xxx.ko 需要通过 insmod 或 modprobe 将其加载到内核,由于 insmod 是bubybox 提供的用户层命令,所以需要阅读 busybox 源码:

modutils/insmod.cint insmod_main(int argc UNUSED_PARAM, char **argv)
{char *filename;int rc;/* Compat note:* 2.6 style insmod has no options and required filename* (not module name - .ko can't be omitted).* 2.4 style insmod can take module name without .o* and performs module search in default directories* or in $MODPATH.*/IF_FEATURE_2_4_MODULES(getopt32(argv, INSMOD_OPTS INSMOD_ARGS);argv += optind - 1;);filename = *++argv;if (!filename)bb_show_usage();rc = bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));if (rc)bb_error_msg("can't insert '%s': %s", filename, moderror(rc));return rc;
}

bb_init_module():

modutils/modutils.cint FAST_FUNC bb_init_module(const char *filename, const char *options)
{size_t image_size;char *image;int rc;bool mmaped;if (!options)options = "";//TODO: audit bb_init_module_24 to match error code convention
#if ENABLE_FEATURE_2_4_MODULESif (get_linux_version_code() < KERNEL_VERSION(2,6,0))return bb_init_module_24(filename, options);
#endif/** First we try finit_module if available.  Some kernels are configured* to only allow loading of modules off of secure storage (like a read-* only rootfs) which needs the finit_module call.  If it fails, we fall* back to normal module loading to support compressed modules.*/
# ifdef __NR_finit_module{int fd = open(filename, O_RDONLY | O_CLOEXEC);if (fd >= 0) {rc = finit_module(fd, options, 0) != 0;close(fd);if (rc == 0)return rc;}}
# endifimage_size = INT_MAX - 4095;mmaped = 0;image = try_to_mmap_module(filename, &image_size);if (image) {mmaped = 1;} else {errno = ENOMEM; /* may be changed by e.g. open errors below */image = xmalloc_open_zipped_read_close(filename, &image_size);if (!image)return -errno;}errno = 0;init_module(image, image_size, options);rc = errno;if (mmaped)munmap(image, image_size);elsefree(image);return rc;
}

init_module() 定义如下:

modutils/modutils.c
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)

最终进行syscall 的系统调用:

kernel/module.cSYSCALL_DEFINE3(init_module, void __user *, umod,unsigned long, len, const char __user *, uargs)
{int err;struct load_info info = { };err = may_init_module();if (err)return err;pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",umod, len, uargs);err = copy_module_from_user(umod, len, &info);if (err)return err;return load_module(&info, uargs, 0);
}

其实无论是 insmod 或者是 modprobe,最终都是调用到内核的 load_module()。

下面的流程为:

kernel/module.cload_module()---->do_init_module()---->do_one_initcall()

最终 do_one_initcall() 同静态编译:

init/main.cint __init_or_module do_one_initcall(initcall_t fn)
{...ret = fn();...return ret;
}

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

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

相关文章

【Python入门二】安装第三方库(包)

安装第三方库/包 1 使用pip安装2 使用PyCharm软件安装3 离线安装&#xff0c;使用whl文件安装参考 在Python中&#xff0c;有多种安装第三方库的方法&#xff0c;下面是一些常用的方法&#xff1a; 1 使用pip安装 pip是Python中最常用的包管理工具&#xff0c;也是最常用的在线…

电商中的百万量级

背景 我们一般在设计app架构或者访问量评估时&#xff0c;特别是在小公司时&#xff0c;老板总是希望我们能支持百万级的访问量&#xff0c;但是实际我们真的需要这么多吗&#xff0c;你知道百万访问的概念是多大吗 支持百万级 一般来说&#xff0c;百万级的页面访问也就意味…

代码随想录 Day35 动态规划04 01背包问题和完全背包问题 LeetCode T416 分割等和子集

背包问题 说到背包问题大家都会想到使用动规的方式来求解,那么为什么用动规呢,dp数组代表什么呢?初始化是什么,遍历方式又是什么,这篇文章笔者将详细讲解背包问题的经典例题0-1背包问题和完全背包问题的解题方式,希望能帮助到大家 1.暴力方式 有人一提到背包问题就只会使用动态…

OpenGL ES入门教程(一)编写第一个OpenGL程序

OpenGL ES入门教程&#xff08;一&#xff09;编写第一个OpenGL程序 前言 从本文开始我将参考学习OpenGL ES应用开发实践指南 Android卷 [&#xff08;美&#xff09;KevinBrothaler著]&#xff08;提取码: 394m&#xff09;&#xff0c;并基于自己的理解以更加通俗易懂的方式…

近独立粒子的最概然分布

近独立粒子&#xff1a;粒子之间相互作用微弱基本粒子中&#xff0c;自旋量子数为半整数的有 电子 、 质子 、中子、中微子自旋量子数为整数的有 光子、pi介子 经典力学描述系统的微观运动状态 经典力学中&#xff0c;全同粒子可以分辨量子力学&#xff0c;全同粒子不可以分辨微…

2023-11-02 LeetCode每日一题(环和杆)

2023-11-02每日一题 一、题目编号 2103. 环和杆二、题目链接 点击跳转到题目位置 三、题目描述 总计有 n 个环&#xff0c;环的颜色可以是红、绿、蓝中的一种。这些环分别穿在 10 根编号为 0 到 9 的杆上。 给你一个长度为 2n 的字符串 rings &#xff0c;表示这 n 个环在…

BetterDisplay Pro v1.4.15(显示器管理管理软件)

BetterDisplay Pro是一款屏幕显示优化工具&#xff0c;可用于Windows和Mac操作系统。它可以帮助用户调整屏幕的亮度、对比度、色彩等参数&#xff0c;以获得更好的视觉体验。此外&#xff0c;BetterDisplay Pro还提供了一些额外的功能&#xff0c;如屏幕分割、窗口管理、快捷键…

Django3框架-(3)-[使用websocket]:使用channels实现websocket功能;简化的配置和实际使用方式

概述&#xff1a; 对于Django使用channels实现websocket的功能&#xff0c;之前就写了几篇博文了。随着在项目的使用和实际维护来说&#xff0c;重新设置了相关处理方法。 一般来说&#xff0c;前后端都只维护一个全局的连接&#xff0c;通过携带数据来判断具体的操作&#x…

Flink1.18新特性生产环境应用的重点解读!

大家好&#xff0c;我是你们的群主王知无呀。 Flink 1.18已经于近期发布了。在这个新版本中新增了很多新的功能和特性。在这些特性中&#xff0c;有一些是生产环境非常重要的能力&#xff0c;大家在使用过程中可以重点参考和了解其中的原理。 算子级别状态保留时间TTL设置 首先…

GitHub经常打不开或者访问解决办法

访问慢或无法访问的原因&#xff1a;DNS解析是最为基础的一个环节。由于Github的服务器在全球各地&#xff0c;域名解析所需的时间也会不同&#xff0c;这就导致了在特定地区可能会出现Github无法正常访问的情况。 解决&#xff1a;查询到github对应的IP&#xff0c;然后在host…

精准测试:提高软件质量和用户满意度的利器

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

Oracle数据库中的table@xyz是什么意思?

是DBlink访问外部表的语法。xyz是其他Oracle数据库在你所登录的用户下建立的Dblink名。通过这种方式访问其他数据库中的表。 在Oracle数据库中&#xff0c;表名后跟着符号和一个连接字符串&#xff08;xyz&#xff09;是一种用法&#xff0c;它用于指定要访问的远程数据库。 …

腾讯云域名备案后,如何解析到华为云服务器Linux宝塔面板

一、购买域名并且进行备案和解析&#xff0c;正常情况下&#xff0c;购买完域名&#xff0c;如果找不到去哪备案&#xff0c;可以在腾讯云上搜索“备案”关键词就会出现了&#xff0c;所以这里不做详细介绍&#xff0c;直接进行步骤提示&#xff1a; 二、申请ssl证书&#xff0…

uniapp使用抖音微信自定义组件

tt.vue中使用video-player组件 用到的目录如下&#xff1a; pages.json {"path": "pages/Tabbar/tt/tt","style": {"navigationBarTitleText": "","enablePullDownRefresh": false,// 使用自定义组件"using…

ST7789LCD调试笔记

1.采用4线模式SPI GND&#xff1a;接地引脚VCC&#xff1a;接电源引脚&#xff0c;接3.3V直流电源即可SCL&#xff1a;SPI的时钟线SCLKSDA&#xff1a;SPI的数据输入SDO: SPI的数据输出RES&#xff1a;复位接口&#xff0c;低电平初始化&#xff08;复位&#xff09;&#xff…

Nginx服务器安装证书并启用SSL(acme.sh)

前提 您已购置vps服务器&#xff0c;例如阿里云全球站ecs、AWS EC2、Azure VM、GCP Compute等安全组已开启80、443端口&#xff0c;且访问源设置为0.0.0.0/0域名已设置A记录指向当前操作服务器&#xff0c;若您使用aws ec2&#xff0c;有公有 IPv4 DNS&#xff0c;可供使用 安…

03.从简单的sql开始

从简单的sql开始 一、sql语句的种类二、oracle的工作原理三、oracle数据库常见基础命令 一、sql语句的种类 下面是SQL语句的分类、常用语句、使用方法&#xff1a; 分类语句使用方法解释数据查询SELECTSELECT column1, column2, … FROM table_name WHERE condition;用于从表…

2023年最新版潮乎盲盒源码含搭建教程

后台开发语言&#xff1a;后端 Laravel 框架开发 前端开发框架&#xff1a;uniappvue 环境配置: php7.4 mysql5.6 nginx1.22 redis&#xff08;建议宝塔面板或 lnmp&#xff09; 源码获取请自行百度&#xff1a;一生相随博客 一生相随博客致力于分享全网优质资源&#x…

Python条件判断的运用

问题 在生活中&#xff0c;我们可以通过判断条件是否成立&#xff0c;来决定执行哪个分支。选择语句有多种形式&#xff1a;if语句&#xff0c;if-else语句&#xff0c;if-elif-else语句等。 Python使用if条件判断语句来实现条件判断时&#xff0c;可以在多个循环中实现对问题的…

JAVA亡了?那么多岗位去哪了?

1.java现在有多卷&#xff1f; 虽然近年来出现了许多其他编程语言和技术。但JAVA依旧是热度最高的。它仍然被广泛用于大型企业应用、后端开发、Android应用开发以及嵌入式系统等领域。此外&#xff0c;Java在大数据、云计算和物联网等新兴领域也有着重要的地位。 因此&#x…