第三十二章 linux-模块的加载过程二

news2024/11/16 5:31:33

第三十二章 linux-模块的加载过程二


文章目录

  • 第三十二章 linux-模块的加载过程二
  • HDR视图的第二次改写
  • 模块导出的符号


HDR视图的第二次改写

在这次改写中,HDR视图中绝大多数的section会被搬移到新的内存空间中,之后会根据这些section新的内存地址再次改写图1·2中的HDR视图,使其中section header table中各entry的sh_addr指向新的也是最终的内存地址。
load_module->layout_and_allocate->layout_sections

static void layout_sections(struct module *mod, struct load_info *info)
{
	static unsigned long const masks[][2] = {
		/* NOTE: all executable code must be the first section
		 * in this array; otherwise modify the text_size
		 * finder in the two loops below */
		{ SHF_EXECINSTR | SHF_ALLOC, ARCH_SHF_SMALL },
		{ SHF_ALLOC, SHF_WRITE | ARCH_SHF_SMALL },
		{ SHF_WRITE | SHF_ALLOC, ARCH_SHF_SMALL },
		{ ARCH_SHF_SMALL | SHF_ALLOC, 0 }
	};//标记了SHF_ALLOC的section定义了四种类型:
	//code、only-data、read-write data和small data。
	unsigned int m, i;
	
	//遍历Section header table

	for (i = 0; i < info->hdr->e_shnum; i++)
		info->sechdrs[i].sh_entsize = ~0UL;

	pr_debug("Core section allocation order:\n");
	for (m = 0; m < ARRAY_SIZE(masks); ++m) {
		for (i = 0; i < info->hdr->e_shnum; ++i) {
			Elf_Shdr *s = &info->sechdrs[i];
			const char *sname = info->secstrings + s->sh_name;

			if ((s->sh_flags & masks[m][0]) != masks[m][0]
			    || (s->sh_flags & masks[m][1])
			    || s->sh_entsize != ~0UL
			    || strstarts(sname, ".init"))
				continue;
			s->sh_entsize = get_offset(mod, &mod->core_size, s, i);
			pr_debug("\t%s\n", sname);
		}
		switch (m) {
		case 0: /* executable */
			mod->core_size = debug_align(mod->core_size);
			mod->core_text_size = mod->core_size;
			break;
		case 1: /* RO: text and ro-data */
			mod->core_size = debug_align(mod->core_size);
			mod->core_ro_size = mod->core_size;
			break;
		case 3: /* whole core */
			mod->core_size = debug_align(mod->core_size);
			break;
		}
	}

	pr_debug("Init section allocation order:\n");
	for (m = 0; m < ARRAY_SIZE(masks); ++m) {
		for (i = 0; i < info->hdr->e_shnum; ++i) {
			Elf_Shdr *s = &info->sechdrs[i];
			const char *sname = info->secstrings + s->sh_name;

			if ((s->sh_flags & masks[m][0]) != masks[m][0]
			    || (s->sh_flags & masks[m][1])
			    || s->sh_entsize != ~0UL
			    || !strstarts(sname, ".init"))
				continue;
			s->sh_entsize = (get_offset(mod, &mod->init_size, s, i)
					 | INIT_OFFSET_MASK);
			pr_debug("\t%s\n", sname);
		}
		switch (m) {
		case 0: /* executable */
			mod->init_size = debug_align(mod->init_size);
			mod->init_text_size = mod->init_size;
			break;
		case 1: /* RO: text and ro-data */
			mod->init_size = debug_align(mod->init_size);
			mod->init_ro_size = mod->init_size;
			break;
		case 3: /* whole init */
			mod->init_size = debug_align(mod->init_size);
			break;
		}
	}
}

在为那些需要移动的section分配新的内存空间地址之前,内核需要决定出HDR视图中哪些section需要移动,如果移动的话要移动到什么位置。内核代码中layout_sections函数用来做这件事,在layout_sections函数中,内核会遍历HDR视图中的每一个section,对每一个标记有SHF_ALLOC的section,将其划分到两大类section当中:CORE和INIT。为了完成这种分类,layout_sections函数首先为标记了SHF_ALLOC的section定义了四种类型:code、only-data、read-write data和small-datao任何一个标记了SHF_ALLOC的section必定属于这四类中的一类。之后,对应每一个分类,函数都会遍历Section header table中的所有项,将section name不是以".init"开始的section划归为CORE section,并且修改HDR视图中Section header table中对应entry的sh_entsize,用以记录当前section在COREsection中的偏移量。

mod->core_size = debug_align(mod->core_size);

同时用struct module结构中的成员变量core size记录下到当前正在操作的section为止CORE section的空间大小。

mod->core_size = debug_align(mod->core_size);

对于CORE section中的code section,内核用struct module结构中的core text size来记录。

mod->core_text_size = mod->core_size;

对于INIT section的分类,和CORE section的划分基本一样,不同的地方在于属于INIT section的section,其name必须以与nit"开始,内核用struct module结构中的成员变量init size来记录当前INITsection空间的大小。

mod->init_size = debug_align(mod->init_size);

对于INIT section中的code section,内核用struct module结构中的init text size来记录。在对section进行搬移之前,接下来会有个对符号表的处理,内核代码中通过调用layout_symtab函数来完成。Linux的内核源码中根据是否启用了内核配置选项CONFIG_KALLSYMS给出了layout_symtab函数的两种不同的定义。

在启用了CONFIG_KALLSYMS选项的L皿源码树基础上编译内核模块,会导致内核模块也会保留模块中的所有符号,这些符号都放在ELF符号表section中。由于在内核模块的ELF文件中,符号表所在的section没有SHFALLOC标志,所以上面提到的layout_secttons函数不会把符号表section划到CORE section或者是INITsecuon中,这也是为什么要通过另外一个函数layout_symtab来把符号表搬移到CORE section内存区中的原因。

在对内核模块ELF文件中的section进行了CORE和INIT的划分之后,内核调用vmalloc相关的函数为CORE section和INITsection分配对应的内存空间,基地址分别记录在mod->module_core和mod->module_init中,然后把对应的section数据搬移到其在CORE section和INITsection内存空间的最终位置上。显然,在把各section搬移到其新的内有地址之后,内核需要改写HDR视图中的Section header table中对应entry的shaddr,以使其指向新的地址。注意,由于ci时“gnu.linkoncethismodule”section是一个带有SHF_ALLOC标志的可写数据section,也会被搬移到CORE section内存空间中,所以必须更新mod变量使之指向新的内存地址。

load_module->layout_and_allocate->move_module

static int move_module(struct module *mod, struct load_info *info)
{
	int i;
	void *ptr;

	/* Do the allocs. */
	ptr = module_alloc_update_bounds(mod->core_size);
	/*
	 * The pointer to this block is stored in the module structure
	 * which is inside the block. Just mark it as not being a
	 * leak.
	 */
	kmemleak_not_leak(ptr);
	if (!ptr)
		return -ENOMEM;

	memset(ptr, 0, mod->core_size);
	mod->module_core = ptr;

	if (mod->init_size) {
		ptr = module_alloc_update_bounds(mod->init_size);
		/*
		 * The pointer to this block is stored in the module structure
		 * which is inside the block. This block doesn't need to be
		 * scanned as it contains data and code that will be freed
		 * after the module is initialized.
		 */
		kmemleak_ignore(ptr);
		if (!ptr) {
			module_memfree(mod->module_core);
			return -ENOMEM;
		}
		memset(ptr, 0, mod->init_size);
		mod->module_init = ptr;
	} else
		mod->module_init = NULL;

	/* Transfer each section which specifies SHF_ALLOC */
	pr_debug("final section addresses:\n");
	for (i = 0; i < info->hdr->e_shnum; i++) {
		void *dest;
		Elf_Shdr *shdr = &info->sechdrs[i];

		if (!(shdr->sh_flags & SHF_ALLOC))
			continue;

		if (shdr->sh_entsize & INIT_OFFSET_MASK)
			dest = mod->module_init
				+ (shdr->sh_entsize & ~INIT_OFFSET_MASK);
		else
			dest = mod->module_core + shdr->sh_entsize;

		if (shdr->sh_type != SHT_NOBITS)
			memcpy(dest, (void *)shdr->sh_addr, shdr->sh_size);
		/* Update sh_addr to point to copy in image. */
		shdr->sh_addr = (unsigned long)dest;
		pr_debug("\t0x%lx %s\n",
			 (long)shdr->sh_addr, info->secstrings + shdr->sh_name);
	}

	return 0;
}

这里之所以要对HDR视图中的某些section做这样的搬移,是因为在模块加载过程结束时,系统会释放掉IIDR视图所在的内存区域,不仅如此,在模块初始化工作完成后,INITsection所在的内存区域也会被释放掉。由此可见,当一个模块被成功加载进系统,初始化工作完成之后,最终留下的仅仅是CORE section中的内容,因此CORE section中的数据应是模块在系统中整个存活期会使用到的数据。

如此处理之后,我们在图1·2的基础上得到了图1·4:

在这里插入图片描述

模块导出的符号

如果一个内核模块向外界导出了自己的符号,那么将由模块的编译工具链负责生成这些导出符号section,而且这些section都带有SHFALLOC标志,所以在模块加载过程中会被搬移到CORE section区域中。如果模块没有向外界导出任何符号,那么在模块的ELF文件中,将不会产生这些section。

显然,内核需要对模块导出的符号进行管理,以便在处理其他模块中那些“未解决的引用”符号时能够找到这些符号。内核对模块导出的符号的管理使用到了structmodule结构中如下的成员变量:

struct module {

	。。。。
#ifdef CONFIG_UNUSED_SYMBOLS
	/* unused exported symbols. */
	const struct kernel_symbol *unused_syms;
	const unsigned long *unused_crcs;
	unsigned int num_unused_syms;

	/* GPL-only, unused exported symbols. */
	unsigned int num_unused_gpl_syms;
	const struct kernel_symbol *unused_gpl_syms;
	const unsigned long *unused_gpl_crcs;
#endif

#ifdef CONFIG_MODULE_SIG
	/* Signature was verified. */
	bool sig_ok;
#endif

	/* symbols that will be GPL-only in the near future. */
	const struct kernel_symbol *gpl_future_syms;
	const unsigned long *gpl_future_crcs;
	unsigned int num_gpl_future_syms;
	。。。。
}

在把HDR视图中的section搬移到最终的CORE section和INIT section之后,内核通过对HDR视图中Section header table的查找,获得“__ksymtab",“__ksymtab_gpl”和”__ksymtab_gpl_future”section在CORE section中的地址,将其记录在mod->syms、mod->gpl_syms和mod->gpl_future_syms中。
在这里插入图片描述

  • find_symbol

在模块加载过程中,find_symbol是个非常重要的函数,顾名思义,它用来查找一个符号。该函数的原型如下:

const struct kernel_symbol *find_symbol(const char *name,
					struct module **owner,
					const unsigned long **crc,
					bool gplok,
					bool warn)
{
	struct find_symbol_arg fsa;

	fsa.name = name;
	fsa.gplok = gplok;
	fsa.warn = warn;

	if (each_symbol_section(find_symbol_in_section, &fsa)) {
		if (owner)
			*owner = fsa.owner;
		if (crc)
			*crc = fsa.crc;
		return fsa.sym;
	}

	pr_debug("Failed to find symbol %s\n", name);
	return NULL;
}

在深入到这个函数内部之前,有必要先介绍几个数据结构,这几个数据结构将在find_symbol函数中用到。

//对应要查找的每一个符号表section,换句话说,对要查找的每个符号表section,内核代码都要为之产生一个struct symsearch类型的实例。
struct symsearch {
	const struct kernel_symbol *start, *stop;
	const unsigned long *crcs;
	enum {
		NOT_GPL_ONLY,
		GPL_ONLY,
		WILL_BE_GPL_ONLY,
	} licence;
	bool unused;
};

struct symsearch用来对应要查找的每一个符号表section,换句话说,对要查找的每个符号表section,内核代码都要为之产生一个struct symsearch类型的实例。结构体中的成员变量和stop分别指向对应section的开始和结束地址,bool型的unused成员用来表示内核是否配置了CONFIG_UNUSED_SYMBOLS选项,不过这个选项是“非主流”的,长远看这个选项最终会消失,因此本书只在这里提一下,在后续的章节中将忽略所有该选项被启用时才起作用的代码·另一个比较重要的成员是型的licence,GPL_ONLY表示符号只提供给满足GPL协议的模块使用,NOT_GPL_ONLY表示不一定要只给满足GPL协议的模块使用,WILL_BE_GPLONLY表示将来只提供给满足GPL协议的模块使用。再提醒一下·NOT_GPL_ONLY符号由EXPORT_SYMBOL负责导出,GPLONLY符号由EXPORT_SYMBOL_GPL负责导出,WILL_BE_GPL_ONLY符号由EXPORT_SYMBOL_GPL_FUTURE负责导出。

//查找符号的标识参数
struct find_symbol_arg {
	/* Input */
	const char *name;
	bool gplok;
	bool warn;

	/* Output */
	struct module *owner;
	const unsigned long *crc;
	const struct kernel_symbol *sym;
};

find_symbol_arg用做查找符号的标识参数,可以看到其大部分数据成员与find_symbol函数原型中的参数完全一致,其中的kernel_symbol是一个用以表示内核符号构成的数据结构,在前面的“EXPORT_SYMBOL的内核实现”一节中介绍过。

find_symbol函数首先构造被查找模块的标识参数№,然后通过each-symbol来查找符号。each_symbol是用来进行符号查找的主要函数,为节约篇幅起见,这里不再摘录其源代码,而是直接讲述其主要功能框架。

总体上,each_symbol函数可以分成两个部分:第一部分是在内核导出的符号表中查找对应的符号,如果找到,就通过返回该符号的信息,否则,再进行第二部分的查找:第2部分是在系统中己加载的模块(系统中所有己成功加载的模块都以链表的形式保存在一个全局变量modules中)的导出符号表中查找对应的符号,如果找到就通过返回该符号的信息,否则函数返回false。图1·6展示了find-symbol在查找一个符号时的搜索路径:

在这里插入图片描述
第一部分在对内核符号表进行查找时,首先构造一个struct symsearch类型的数组arr。

bool each_symbol_section(bool (*fn)(const struct symsearch *arr,
				    struct module *owner,
				    void *data),
			 void *data)
{
	struct module *mod;
	static const struct symsearch arr[] = {
		{ __start___ksymtab, __stop___ksymtab, __start___kcrctab,
		  NOT_GPL_ONLY, false },
		{ __start___ksymtab_gpl, __stop___ksymtab_gpl,
		  __start___kcrctab_gpl,
		  GPL_ONLY, false },
		{ __start___ksymtab_gpl_future, __stop___ksymtab_gpl_future,
		  __start___kcrctab_gpl_future,
		  WILL_BE_GPL_ONLY, false },
#ifdef CONFIG_UNUSED_SYMBOLS
		{ __start___ksymtab_unused, __stop___ksymtab_unused,
		  __start___kcrctab_unused,
		  NOT_GPL_ONLY, true },
		{ __start___ksymtab_unused_gpl, __stop___ksymtab_unused_gpl,
		  __start___kcrctab_unused_gpl,
		  GPL_ONLY, true },
#endif
	};

	if (each_symbol_in_section(arr, ARRAY_SIZE(arr), NULL, fn, data))
		return true;

	list_for_each_entry_rcu(mod, &modules, list) {
		struct symsearch arr[] = {//构造一个struct symsearch类型的数组arr
			{ mod->syms, mod->syms + mod->num_syms, mod->crcs,
			  NOT_GPL_ONLY, false },
			{ mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
			  mod->gpl_crcs,
			  GPL_ONLY, false },
			{ mod->gpl_future_syms,
			  mod->gpl_future_syms + mod->num_gpl_future_syms,
			  mod->gpl_future_crcs,
			  WILL_BE_GPL_ONLY, false },
#ifdef CONFIG_UNUSED_SYMBOLS
			{ mod->unused_syms,
			  mod->unused_syms + mod->num_unused_syms,
			  mod->unused_crcs,
			  NOT_GPL_ONLY, true },
			{ mod->unused_gpl_syms,
			  mod->unused_gpl_syms + mod->num_unused_gpl_syms,
			  mod->unused_gpl_crcs,
			  GPL_ONLY, true },
#endif
		};

		if (mod->state == MODULE_STATE_UNFORMED)
			continue;

		if (each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data))
			return true;
	}
	return false;
}

注意这里的__start__ksymtab、__start__kcrctab和_stop__ksymtab等变量己经在前面的“EXPORT_SYMBOL的内核实现”一节中交代过,它们在内核的链接脚本中定义,由链接器负责产生,由内核源码负责声明,现在到了使用它们的时候了。

接下来函数通过调用each_symbol_in_section查询内核的导出符号表,
each_symbol_in_section的核心代码如下(经过适当改写):

each_symbol_section->each_symbol_in_section

static bool each_symbol_in_section(const struct symsearch *arr,
				   unsigned int arrsize,
				   struct module *owner,
				   bool (*fn)(const struct symsearch *syms,
					      struct module *owner,
					      void *data),
				   void *data)
{
	unsigned int j;

	for (j = 0; j < arrsize; j++) {
		if (fn(&arr[j], owner, data))
			return true;
	}

	return false;
}

为了在内核的导出符号表中查找某一指定的符号名,each_symbol_insection函数使用了两层循环:外层j引导的for循环用来遍历符号可能所在的内核导出符号表中的各section:内层i引导的for循环用来遍历外层j引导for循环所指定的section中的每个struct kernel_symbol类型的元素。对于每个kernel_symbol,都会调用find_symbol_in_setion函数。

为了清楚地理解内核加载模块时如何处理“未解决的引用”符号,有必要仔细分析一下find_symbol_in_section函数的主要功能。因为对Linux下的设备驱动程序员而言,几乎每天都在和这个功能打交道,清楚地理解其内核机制,将来一旦在加载模块时出现相关问题,也可以将其快速定位并最终解决。另外,对于带“GPL”后缀的符号名,在写驱动程序的内核模块时常常会遇到,然而其背后到底蕴涵着怎样的设计理念呢?通过分析find_symbol_in_section函数,就可以得到所需的答案:

static bool find_symbol_in_section(const struct symsearch *syms,
				   struct module *owner,
				   void *data)
{
	struct find_symbol_arg *fsa = data;
	struct kernel_symbol *sym;

	sym = bsearch(fsa->name, syms->start, syms->stop - syms->start,
			sizeof(struct kernel_symbol), cmp_name);

	if (sym != NULL && check_symbol(syms, owner, sym - syms->start, data))
		return true;

	return false;
}

函数首先用strcmp函数来比较kernel_symbol结构体中的name与№中的name(正在查找的符号名,即要加载的内核模块中出现的“未解决的引用”的符号)是否匹配,如果不匹配,那么函数直接返回false。

fsa->gplok和fsa->warn的设定最早是在find_symbol函数中,是通过后者的函数参数传入的。fsa->wam主要用来控制警告信息的输出。fsa->gplok用来表示当前的模块是不是满足GPL协议(GPL module或non-GPL module),fsa->gplok=true表明这是个GPL module,否则就是non-GPL modulee内核判断一个模块是否GPL兼容,要使用到本章后面的“模块的信息”部分中的内容。

对于一个non-GPLmoduie而言,它不能使用内核导出的属于GPL_ONLY的那些符号,所以即使要查找的符号匹配上一个属于GPL_ONLY的符号,也不能认为查找成功。但是如果要查找的符号匹配上一个属于WILL_BE_GPL_ONLY的符号,因为这个导出的符号“将要成为GPL_ONLY”,所以即使现在还不是GPL_ONLY,查找姑且算是成功的,不过即便如此,内核对模块将来对该符号的成功使用没有保障,所以应该给出一个警告信息。对于一个GPL module而言,一切好说,可以使用内核导出的所有符号。

函数如果成功查找到符号,利用传进来的d指针将符号相关信息传给上层调用的函数。

至此,find_symbol的第一部分,即在内核导出的符号表中查找指定的符号己经结束。如果指定的符号没有出现在内核导出的符号表中,那么将进入find_symbol函数的第二部分。

下面开始介绍find_symbol的第二部分,在系统己经加载的模块导出的符号表中查找符号。内核为达成此目的,需要在加载一个内核模块时完成下面两件事。

第一,模块成功加载进系统之后,需要将表示该模块的struct module类型变量mod加入到modules中,后者是一个全局的链表变量,用来记录系统中所有己加载的模块。

list_for_each_entry_rcu(mod, &modules, list) ->list_entry_rcu

第二,模块导出的符号信息记录在mod的相关成员变量中,这个过程的详细描述参见本章前面的“模块导出的符号”部分。

each_symbol用来在系统所有己加载的模块导出的符号中查找某一指定符号,其核心代码片段如下:

list_for_each_entry_rcu(mod, &modules, list) {
		struct symsearch arr[] = {//构造一个struct symsearch类型的数组arr
			{ mod->syms, mod->syms + mod->num_syms, mod->crcs,
			  NOT_GPL_ONLY, false },
			{ mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
			  mod->gpl_crcs,
			  GPL_ONLY, false },
			{ mod->gpl_future_syms,
			  mod->gpl_future_syms + mod->num_gpl_future_syms,
			  mod->gpl_future_crcs,
			  WILL_BE_GPL_ONLY, false },
#ifdef CONFIG_UNUSED_SYMBOLS
			{ mod->unused_syms,
			  mod->unused_syms + mod->num_unused_syms,
			  mod->unused_crcs,
			  NOT_GPL_ONLY, true },
			{ mod->unused_gpl_syms,
			  mod->unused_gpl_syms + mod->num_unused_gpl_syms,
			  mod->unused_gpl_crcs,
			  GPL_ONLY, true },
#endif
		};

		if (mod->state == MODULE_STATE_UNFORMED)
			continue;

		if (each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data))
			return true;

相对于find_symbol的第一部分〈在内核导出的符号表中查找某一符号),第二部分唯一的区别在于构造的arr数组。函数在全局链表modules中遍历所有己加载的内核模块,对其中的每一模块都构造一个新的arr数组,然后在其中查找特定的符号。

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

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

相关文章

[附源码]计算机毕业设计Python“小世界”私人空间(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等…

知到/智慧树——程序设计基础(C语言)进阶篇

目录 第一章测试 第二章测试 第三章测试 第四章测试 第五章测试 第一章测试 第1部分总题数: 10 1 【单选题】 (10分) 在C语言中&#xff0c;将属于不同类型的数据作为一个整体来处理时&#xff0c;常用&#xff08; &#xff09;。 A. 简单变量 B. 数组类型数据 C. 结…

论文投稿指南——中文核心期刊推荐(力学)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

10.union all、N天连续登录

有日志如下&#xff0c;请写出代码求得所有用户和活跃用户的总数及平均年龄。&#xff08;活跃用户指连续两天都有访问记录的用户&#xff09; 数据准备 最后需完成的结果表 步骤1&#xff0c;所有用户的总数及平均年龄 (1). 将数据去重 with t1 as (select distinctuser_i…

如何使用交换机、路由器及防火墙进行组网以及他们之间的功能和区别

如何使用交换机、路由器及防火墙进行组网以及他们之间的功能和区别。 几乎大部分网络都有交换机、路由器和防火墙这三种基本设备,因此这三种设备对于网络而言非常重要,很多人对这三种设备的使用容易弄混。 一般网络部署: 或者抽象为这种部署模式: 几乎每个网络都有交换…

别再写jsp了,Thymeleaf它不香吗?

啥是 Thymeleaf在学 Thymeleaf 之前我们先看一下使用 jsp 开发遇到的主要问题&#xff1a;jsp 的痛点1.页面包含大量 java 代码&#xff0c;代码太混乱<% page contentType"text/html;charsetUTF-8" language"java" %> <html> <head> &l…

webpack实现自动代码编译

前置 使用webpack构建开发的代码&#xff0c;为了运行需要有两个操作&#xff1a; 操作一&#xff1a;npm run build&#xff0c;编译相关的代码。操作二&#xff1a;通过live server或者直接通过浏览器&#xff0c;打开index.html代码&#xff0c;查看效果。为了完成自动编译&…

《图解TCP/IP》阅读笔记(第五章 5.7、5.8)—— IP隧道与其他IP相关技术

5.7 IP隧道 IP隧道技术顾名思义&#xff0c;是用于在两片网络区域间直接建立通信的通路&#xff0c;而绕过此间的其他网络的一种技术&#xff0c;如下图所示&#xff1a; 网络A与网络B使用IPv6技术&#xff0c;使用IP隧道技术&#xff0c;便可以绕过网络C。 那么其工作原理是…

机器学习 波士顿房价预测 Boston Housing

目录 一&#xff1a;前言 二&#xff1a;模型预测(KNN算法) 三&#xff1a;回归模型预测比对 一&#xff1a;前言 波士顿房价是机器学习中很常用的一个解决回归问题的数据集 数据统计于1978年&#xff0c;包括506个房价样本&#xff0c;每个样本包括波士顿不同郊区房屋的13种…

SQL - MySQL回表

一、回表概念&#xff1b;现象 回表&#xff0c;顾名思义就是回到表中&#xff0c;也就是先通过普通索引&#xff08;我们自己建的索引不管是单列索引还是联合索引&#xff0c;都称为普通索引&#xff09;扫描出数据所在的行&#xff0c;再通过行主键ID 取出索引中未包含的数据…

[附源码]计算机毕业设计Python创新创业管理系统(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

Trie(Trie字符串统计)【实质就是二维数组 表示 链表】【二维数组的第一行就是 头结点】

欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09; 文章字体风格&#xff1a; 红色文字表示&#xff1a;重难点✔ 蓝色文字表示&#xff1a…

探索MapReduce

文章目录一&#xff0c;案例分析&#xff08;一&#xff09;TopN分析法介绍&#xff08;二&#xff09;案例需求二&#xff0c;案例实施&#xff08;一&#xff09;准备数据文件&#xff08;1&#xff09;启动hadoop服务&#xff08;2&#xff09;在虚拟机上创建文本文件&#…

ArcGIS中的OBJECTID、FID 和 OID 的区别!不要傻傻分不清

喜欢就关注我们吧 时常有很多我朋友分不清OBJECTID、FID 和 OID有什么区别&#xff0c;不懂得怎么应用和管理&#xff0c;今天我们来说个明白。 ArcGIS Desktop 产品要求独立表和属性表均具有 ObjectID字段&#xff0c;该字段包含唯一的长整型用于标识每个记录。 此 ID 由 Esri…

第二证券|元宇宙发展规划出炉,3只元宇宙概念股估值创年内新低

本月以来&#xff0c;北上资金加仓23只元国际概念股。 12月15日晚间&#xff0c;浙江省发改委等5部门联合印发《浙江省元国际工业开展举动计划&#xff08;2023—2025年&#xff09;》&#xff08;以下简称《计划》&#xff09;。《计划》中说到&#xff0c;到2025年&#xff0…

查找外文文献必备网站

查找下载外文文献不仅先要找到文献数据库资源&#xff0c;并且还需要有数据库的文献下载权限&#xff0c;如果你没有文献数据库使用权限就用下面这个方法。 文献党下载器&#xff08;wxdown.org&#xff09;有资源有权限&#xff0c;覆盖全科。下面举几个例子&#xff0c;用文…

脑电图分类任务中深度神经网络模型的数据增强:综述2021

Data Augmentation for Deep Neural Networks Model in EEG Classification Task: A Review 脑电图&#xff08;EEG&#xff09;的分类是测量神经活动节奏振荡的关键方法&#xff0c;是脑机接口系统&#xff08;BCI&#xff09;的核心技术之一。然而&#xff0c;从非线性和非平…

RNA-seq 详细教程:似然比检验(13)

学习内容 应用似然比检验 (LRT) 进行假设检验将 LRT 生成的结果与使用 Wald 检验获得的结果进行比较从 LRT 显著基因列表中识别共享表达谱似然比检验 在评估超过两个水平的表达变化时&#xff0c;DESeq2 还提供似然比检验作为替代方法。被确定为重要的基因是那些在不同因子水平…

Vue基础6

Vue基础6收集表单数据过滤器局部过滤器全局过滤器内置指令v-textv-html使用安全性cookie工作原理v-html不安全案例v-cloak指令v-once指令v-pre指令自定义指令函数式对象式自定义命名组合式全局自定义指令自定义指令总结&#xff1a;收集表单数据 <!DOCTYPE html> <ht…

VMware、linux虚拟机设置网络实现虚拟机与主机网络互通

需要配置三个方面才可以&#xff0c;即&#xff1a;WMware网络配置、windows主机网络配置、虚拟机网络配置。 首先&#xff0c;确认VMware虚拟机设置中配置的网络连接方式为&#xff1a;NAT模式 可以在VMware上方的菜单中找到&#xff1a;虚拟机---->设置&#xff0c;然后…