嵌入式BootLoader技术内幕(二)

三、Boot Loader 的主要任务与典型结构框架

在继续本节的讨论之前,首先我们做一个假定,那就是:假定内核映像与根文件系统映像
都被加载到 RAM 中运行。之所以提出这样一个假设前提是因为,在嵌入式系统中内核映像
与根文件系统映像也可以直接在 ROM 或 Flash 这样的固态存储设备中直接运行。但这种
做法无疑是以运行速度的牺牲为代价的。从操作系统的角度看,Boot Loader 的总目标就
是正确地调用内核来执行。

另外,由于 Boot Loader 的实现依赖于 CPU 的体系结构,因此大多数 Boot Loader 都分
为 stage1 和 stage2 两大部分。依赖于 CPU 体系结构的代码,比如设备初始化代码等,
通常都放在 stage1 中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而 sta
ge2 则通常用C语言来实现,这样可以实现给复杂的功能,而且代码会具有更好的可读性和
可移植性。

Boot Loader 的 stage1 通常包括以下步骤(以执行的先后顺序):
·硬件设备初始化。
·为加载 Boot Loader 的 stage2 准备 RAM 空间。
·拷贝 Boot Loader 的 stage2 到 RAM 空间中。
·设置好堆栈。
·跳转到 stage2 的 C 入口点。
Boot Loader 的 stage2 通常包括以下步骤(以执行的先后顺序):
·初始化本阶段要使用到的硬件设备。
·检测系统内存映射(memory map)。
·将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中。
·为内核设置启动参数。
·调用内核。

3.1 Boot Loader 的 stage1

3.1.1 基本的硬件初始化

这是 Boot Loader 一开始就执行的操作,其目的是为 stage2 的执行以及随后的 kernel
的执行准备好一些基本的硬件环境。它通常包括以下步骤(以执行的先后顺序):

1.屏蔽所有的中断。为中断提供服务通常是 OS 设备驱动程序的责任,因此在 Boot Loa
der 的执行全过程中可以不必响应任何中断。中断屏蔽可以通过写 CPU 的中断屏蔽寄存器
或状态寄存器(比如 ARM 的 CPSR 寄存器)来完成。

2.设置 CPU 的速度和时钟频率。

3.RAM 初始化。包括正确地设置系统的内存控制器的功能寄存器以及各内存库控制寄存器
等。

4.初始化 LED。典型地,通过 GPIO 来驱动 LED,其目的是表明系统的状态是 OK 还是
Error。如果板子上没有 LED,那么也可以通过初始化 UART 向串口打印 Boot Loader 的
Logo 字符信息来完成这一点。

5. 关闭 CPU 内部指令/数据 cache。

3.1.2 为加载 stage2 准备 RAM 空间

为了获得更快的执行速度,通常把 stage2 加载到 RAM 空间中来执行,因此必须为加载
Boot Loader 的 stage2 准备好一段可用的 RAM 空间范围。

由于 stage2 通常是 C 语言执行代码,因此在考虑空间大小时,除了 stage2 可执行映象
的大小外,还必须把堆栈空间也考虑进来。此外,空间大小最好是 memory page 大小(通
常是 4KB)的倍数。一般而言,1M 的 RAM 空间已经足够了。具体的地址范围可以任意安排
,比如 blob 就将它的 stage2 可执行映像安排到从系统 RAM 起始地址 0xc0200000 开始
的 1M 空间内执行。但是,将 stage2 安排到整个 RAM 空间的最顶 1MB(也即(RamEnd-1M
B) - RamEnd)是一种值得推荐的方法。

为了后面的叙述方便,这里把所安排的 RAM 空间范围的大小记为:stage2_size(字节),
把起始地址和终止地址分别记为:stage2_start 和 stage2_end(这两个地址均以 4 字节
边界对齐)。因此:

stage2_end=stage2_start+stage2_size



另外,还必须确保所安排的地址范围的的确确是可读写的 RAM 空间,因此,必须对你所安
排的地址范围进行测试。具体的测试方法可以采用类似于 blob 的方法,也即:以 memor
y page 为被测试单位,测试每个 memory page 开始的两个字是否是可读写的。为了后面
叙述的方便,我们记这个检测算法为:test_mempage,其具体步骤如下:

1.先保存 memory page 一开始两个字的内容。

2.向这两个字中写入任意的数字。比如:向第一个字写入 0x55,第 2 个字写入 0xaa。


3.然后,立即将这两个字的内容读回。显然,我们读到的内容应该分别是 0x55 和 0xaa
。如果不是,则说明这个 memory page 所占据的地址范围不是一段有效的 RAM 空间。


4.再向这两个字中写入任意的数字。比如:向第一个字写入 0xaa,第 2 个字中写入 0x
55。

5.然后,立即将这两个字的内容立即读回。显然,我们读到的内容应该分别是 0xaa 和
0x55。如果不是,则说明这个 memory page 所占据的地址范围不是一段有效的 RAM 空间


6.恢复这两个字的原始内容。测试完毕。

为了得到一段干净的 RAM 空间范围,我们也可以将所安排的 RAM 空间范围进行清零操作


3.1.3 拷贝 stage2 到 RAM 中

拷贝时要确定两点:(1) stage2 的可执行映象在固态存储设备的存放起始地址和终止地址
;(2) RAM 空间的起始地址。

3.1.4 设置堆栈指针 sp

堆栈指针的设置是为了执行 C 语言代码作好准备。通常我们可以把 sp 的值设置为(stag
e2_end-4),也即在 3.1.2 节所安排的那个 1MB 的 RAM 空间的最顶端(堆栈向下生长)。
此外,在设置堆栈指针 sp 之前,也可以关闭 led 灯,以提示用户我们准备跳转到 stag
e2。经过上述这些执行步骤后,系统的物理内存布局应该如下图2所示。

3.1.5 跳转到 stage2 的 C 入口点

在上述一切都就绪后,就可以跳转到 Boot Loader 的 stage2 去执行了。比如,在 ARM
系统中,这可以通过修改 PC 寄存器为合适的地址来实现。


http://tech.ccidnet.com/pub/attachme.../12/268047.gif

图2 bootloader 的 stage2 可执行映象刚被拷贝到 RAM 空间时的系统内存布局


3.2 Boot Loader 的 stage2

正如前面所说,stage2 的代码通常用 C 语言来实现,以便于实现更复杂的功能和取得更
好的代码可读性和可移植性。但是与普通 C 语言应用程序不同的是,在编译和链接 boot
loader 这样的程序时,我们不能使用 glibc 库中的任何支持函数。其原因是显而易见的
。这就给我们带来一个问题,那就是从那里跳转进 main() 函数呢?直接把 main() 函数
的起始地址作为整个 stage2 执行映像的入口点或许是最直接的想法。但是这样做有两个
缺点:1)无法通过main() 函数传递函数参数;2)无法处理 main() 函数返回的情况。一种
更为巧妙的方法是利用 trampoline(*簧床)的概念。也即,用汇编语言写一段trampolin
e 小程序,并将这段 trampoline 小程序来作为 stage2 可执行映象的执行入口点。然后
我们可以在 trampoline 汇编小程序中用 CPU 跳转指令跳入 main() 函数中去执行;而当
main() 函数返回时,CPU 执行路径显然再次回到我们的 trampoline 程序。简而言之,
这种方法的思想就是:用这段 trampoline 小程序来作为 main() 函数的外部包裹(exter
nal wrapper)。

下面给出一个简单的 trampoline 程序示例(来自blob):

.text

.globl _trampoline
_trampoline:
bl main
/* if main ever returns we just call it again */
b _trampoline



可以看出,当 main() 函数返回后,我们又用一条跳转指令重新执行 trampoline 程序―
―当然也就重新执行 main() 函数,这也就是 trampoline(*簧床)一词的意思所在。


3.2.1初始化本阶段要使用到的硬件设备

这通常包括:(1)初始化至少一个串口,以便和终端用户进行 I/O 输出信息;(2)初始
化计时器等。在初始化这些设备之前,也可以重新把 LED 灯点亮,以表明我们已经进入
main() 函数执行。

设备初始化完成后,可以输出一些打印信息,程序名字字符串、版本号等。

3.2.2 检测系统的内存映射(memory map)

所谓内存映射就是指在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的
RAM 单元。比如,在 SA-1100 CPU 中,从 0xC000,0000 开始的 512M 地址空间被用作系
统的 RAM 地址空间,而在 Samsung S3C44B0X CPU 中,从 0x0c00,0000 到 0x1000,0000
之间的 64M 地址空间被用作系统的 RAM 地址空间。虽然 CPU 通常预留出一大段足够的
地址空间给系统 RAM,但是在搭建具体的嵌入式系统时却不一定会实现 CPU 预留的全部
RAM 地址空间。也就是说,具体的嵌入式系统往往只把 CPU 预留的全部 RAM 地址空间中
的一部分映射到 RAM 单元上,而让剩下的那部分预留 RAM 地址空间处于未使用状态。由
于上述这个事实,因此 Boot Loader 的 stage2 必须在它想干点什么 (比如,将存储在
flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况,也即它必须知
道 CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元,哪些是处于 "u
nused" 状态的。

(1) 内存映射的描述

可以用如下数据结构来描述 RAM 地址空间中的一段连续(continuous)的地址范围:

typedef struct memory_area_struct {
u32 start; /* the base address of the memory region */
u32 size; /* the byte number of the memory region */
int used;
} memory_area_t;



这段 RAM 地址空间中的连续地址范围可以处于两种状态之一:(1)used=1,则说明这段连
续的地址范围已被实现,也即真正地被映射到 RAM 单元上。(2)used=0,则说明这段连续
的地址范围并未被系统所实现,而是处于未使用状态。

基于上述 memory_area_t 数据结构,整个 CPU 预留的 RAM 地址空间可以用一个 memory
_area_t 类型的数组来表示,如下所示:

memory_area_t memory_map[NUM_MEM_AREAS] = {
[0 ... (NUM_MEM_AREAS - 1)] = {
.start = 0,
.size = 0,
.used = 0
},
};



(2) 内存映射的检测

下面我们给出一个可用来检测整个 RAM 地址空间内存映射情况的简单而有效的算法:


/* 数组初始化 */
for(i = 0; i < NUM_MEM_AREAS; i++)
memory_map[i].used = 0;

/* first write a 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
* (u32 *)addr = 0;

for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
/*
* 检测从基地址 MEM_START+i*PAGE_SIZE 开始,大小为
* PAGE_SIZE 的地址空间是否是有效的RAM地址空间。
*/
调用3.1.2节中的算法test_mempage();
if ( current memory page isnot a valid ram page) {
/* no RAM here */
if(memory_map[i].used )
i++;
continue;
}

/*
* 当前页已经是一个被映射到 RAM 的有效地址范围
* 但是还要看看当前页是否只是 4GB 地址空间中某个地址页的别名?
*/
if(* (u32 *)addr != 0) { /* alias? */
/* 这个内存页是 4GB 地址空间中某个地址页的别名 */
if ( memory_map[i].used )
i++;
continue;
}

/*
* 当前页已经是一个被映射到 RAM 的有效地址范围
* 而且它也不是 4GB 地址空间中某个地址页的别名。
*/
if (memory_map[i].used == 0) {
memory_map[i].start = addr;
memory_map[i].size = PAGE_SIZE;
memory_map[i].used = 1;
} else {
memory_map[i].size += PAGE_SIZE;
}
} /* end of for (…) */



在用上述算法检测完系统的内存映射情况后,Boot Loader 也可以将内存映射的详细信息
打印到串口。

3.2.3 加载内核映像和根文件系统映像

(1) 规划内存占用的布局

这里包括两个方面:(1)内核映像所占用的内存范围;(2)根文件系统所占用的内存范围
。在规划内存占用的布局时,主要考虑基地址和映像的大小两个方面。

对于内核映像,一般将其拷贝到从(MEM_START+0x8000) 这个基地址开始的大约1MB大小的
内存范围内(嵌入式 Linux 的内核一般都不操过 1MB)。为什么要把从 MEM_START 到 MEM
_START+0x8000 这段 32KB 大小的内存空出来呢?这是因为 Linux 内核要在这段内存中
放置一些全局数据结构,如:启动参数和内核页表等信息。

而对于根文件系统映像,则一般将其拷贝到 MEM_START+0x0010,0000 开始的地方。如果用
Ramdisk 作为根文件系统映像,则其解压后的大小一般是1MB。

(2)从 Flash 上拷贝

由于像 ARM 这样的嵌入式 CPU 通常都是在统一的内存地址空间中寻址 Flash 等固态存储
设备的,因此从 Flash 上读取数据与从 RAM 单元中读取数据并没有什么不同。用一个简
单的循环就可以完成从 Flash 设备上拷贝映像的工作:

while(count) {
*dest++ = *src++; /* they are all aligned with word boundary */
count -= 4; /* byte number */
};



3.2.4 设置内核的启动参数

应该说,在将内核映像和根文件系统映像拷贝到 RAM 空间中后,就可以准备启动 Linux
内核了。但是在调用内核之前,应该作一步准备工作,即:设置 Linux 内核的启动参数。


Linux 2.4.x 以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。启动参
数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参
数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header
定义在 Linux 内核源码的include/asm/setup.h 头文件中:

/* The list ends with an ATAG_NONE node. */
#define ATAG_NONE 0x00000000

struct tag_header {
u32 size; /* 注意,这里size是字数为单位的 */
u32 tag;
};
……
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;

/*
* Acorn specific
*/
struct tag_acorn acorn;

/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};



在嵌入式 Linux 系统中,通常需要由 Boot Loader 设置的常见启动参数有:ATAG_CORE、
ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。比如,设置 ATAG_CORE 的代
码如下:

params = (struct tag *)BOOT_PARAMS;

params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);

params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;

params = tag_next(params);



其中,BOOT_PARAMS 表示内核启动参数在内存中的起始基地址,指针 params 是一个 str
uct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标
记的下一个标记的起始地址。注意,内核的根文件系统所在的设备ID就是在这里设置的。


下面是设置内存映射情况的示例代码:

for(i = 0; i < NUM_MEM_AREAS; i++) {
if(memory_map[i].used) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size(tag_mem32);

params->u.mem.start = memory_map[i].start;
params->u.mem.size = memory_map[i].size;

params = tag_next(params);
}
}



可以看出,在 memory_map[]数组中,每一个有效的内存段都对应一个 ATAG_MEM 参数标
记。

Linux 内核在启动时可以以命令行参数的形式来接收信息,利用这一点我们可以向内核提
供那些内核不能自己检测的硬件参数信息,或者重载(override)内核自己检测到的信息。
比如,我们用这样一个命令行参数字符串"console=ttyS0,115200n8"来通知内核以 ttyS0
作为控制台,且串口采用 "115200bps、无奇偶校验、8位数据位"这样的设置。下面是一
段设置调用内核命令行参数字符串的示例代码:

char *p;

/* eat leading white space */
for(p = commandline; *p == ' '; p++)
;

/* skip non-existent command lines so the kernel will still
* use its default command line.
*/
if(*p == '\0')
return;

params->hdr.tag = ATAG_CMDLINE;
params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;

strcpy(params->u.cmdline.cmdline, p);

params = tag_next(params);



请注意在上述代码中,设置 tag_header 的大小时,必须包括字符串的终止符'\0',此外
还要将字节数向上圆整4个字节,因为 tag_header 结构中的size 成员表示的是字数。


下面是设置 ATAG_INITRD 的示例代码,它告诉内核在 RAM 中的什么地方可以找到 initr
d 映象(压缩格式)以及它的大小:

params->hdr.tag = ATAG_INITRD2;
params->hdr.size = tag_size(tag_initrd);

params->u.initrd.start = RAMDISK_RAM_BASE;
params->u.initrd.size = INITRD_LEN;

params = tag_next(params);



下面是设置 ATAG_RAMDISK 的示例代码,它告诉内核解压后的 Ramdisk 有多大(单位是K
B):

params->hdr.tag = ATAG_RAMDISK;
params->hdr.size = tag_size(tag_ramdisk);

params->u.ramdisk.start = 0;
params->u.ramdisk.size = RAMDISK_SIZE; /* 请注意,单位是KB */
params->u.ramdisk.flags = 1; /* automatically load ramdisk */

params = tag_next(params);



最后,设置 ATAG_NONE 标记,结束整个启动参数列表:

static void setup_end_tag(void)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}



3.2.5 调用内核

Boot Loader 调用 Linux 内核的方法是直接跳转到内核的第一条指令处,也即直接跳转到
MEM_START+0x8000 地址处。在跳转时,下列条件要满足:

1. CPU 寄存器的设置:
·R0=0;
@R1=机器类型 ID;关于 Machine Type Number,可以参见 linux/arch/arm/tools/mach
-types。
@R2=启动参数标记列表在 RAM 中起始基地址;

2. CPU 模式:
·必须禁止中断(IRQs和FIQs);
·CPU 必须 SVC 模式;

3. Cache 和 MMU 的设置:
·MMU 必须关闭;
·指令 Cache 可以打开也可以关闭;
·数据 Cache 必须关闭;

如果用 C 语言,可以像下列示例代码这样来调用内核:

void (*theKernel)(int zero, int arch, u32 params_addr)
= (void (*)(int, int, u32))KERNEL_RAM_BASE;
……
theKernel(0, ARCH_NUMBER, (u32) kernel_params_start);



注意,theKernel()函数调用应该永远不返回的。如果这个调用返回,则说明出错。

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

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

相关文章

MongoDB数据库的迁移

最近公司开始要换服务器啦&#xff0c;MongoDB上面的数据又得迁移&#xff0c;还是记录一下比较好。 1&#xff09;、将MongoDB的压缩包解压至相对应的路径(压缩文件在本地服务器的地址192.168.0.22的/opt/zip文件下面) 2&#xff09;、配置好mongodb.conf文件&#xff0c;配…

excel vba 如何将日期周几转换成文字_这5个超实用的Excel技巧,让你的办公效率更高...

导读&#xff1a;对于办公职员来说&#xff0c;Excel是几乎每天都会接触的办公软件。在Excel中&#xff0c;有非常多的小技巧&#xff0c;学习这些小技巧需要不断的积累和应用&#xff0c;今天指北针就来给大家分享5个超实用的Excel技巧&#xff0c;让办公变得更加有效率。文/芒…

VMware创建Linux及局域网内独立访问IP和访问外网IP的配置

好早之前有一篇是配置远程连接Linux和部署Tomcat的文章&#xff0c;但是并没有讲解如何配置IP的相关知识。最近公司在搞集群配置&#xff0c;我就先拿电脑上的VMware上的Linux做个测试&#xff0c;分享和总结一下经验吧&#xff0c;也算是为了补齐之前的那个空白&#xff01; …

每位设计师都应该拥有的50个CSS代码片段

每位设计师都应该拥有的50个CSS代码片段

C#浅拷贝与深拷贝区别

也许会有人这样解释C# 中浅拷贝与深拷贝区别&#xff1a; 浅拷贝是对引用类型拷贝地址&#xff0c;对值类型直接进行拷贝。 不能说它完全错误&#xff0c;但至少还不够严谨。比如&#xff1a;string 类型咋说&#xff1f; 其实&#xff0c;我们可以通过实践来寻找答案。 首先&a…

内网安装nginx+keepalived环境配置及简单使用

分享一下这次艰难的配置过程&#xff0c;衔接上一篇的配置内网独立IP虚拟机。 先吐槽一波&#xff0c;由于公司网络属于内网&#xff0c;与外网互不相通&#xff0c;所以在安装nginx的时候可能会去外网找相对应rpm文件&#xff0c;而且也有许多的版本不兼容问题&#xff0c;好…

cad连续标注数字123怎么弄_实例讲解CAD模型与布局中的各种比例

好课推荐&#xff1a;零基础CAD&#xff1a;点我CAD室内&#xff1a;点我 周站长CAD&#xff1a;点我CAD机械&#xff1a;点我 Bim教程&#xff1a;点我CAD建筑&#xff1a;点我CAD三维&#xff1a;点我全屋定制&#xff1a;点我 ps教程&#xff1a;点我苹果版CAD:点我 3dmax教…

SpringMvc异步请求的使用及部分原理

最近隔壁项目组的项目又出问题了&#xff0c;一直被用户投诉太卡了&#xff0c;页面白屏的那种&#xff0c;打开源代码一看&#xff0c;全是非异步请求&#xff0c;类似于以下写法&#xff1a; ResponseBodyRequestMapping(value "/getTest")public String getTest(…

Microsoft BizTalk ESB Toolkit 2.0

[>>> 更多<BizTalk开发系列>文章 ] 微软于6月8号发布了BizTalk Server 2009企业集成平台的最后一个功能组件:ESB Toolkit 2.0 (原名:ESB Guidance 2.0)&#xff0c;ESB ToolKit 2.0一个是工具和代码集扩展了BizTalk Server 2009对于松耦合和动态消息架构的支持…

python解释器环境中用于表示上一次运算结果的特殊变量_判断正误 PUSH CL_学小易找答案...

【单选题】将数学关系式2 【填空题】请用4位十六进制写出每条指令结束后AX的值。 MOV AX, 0 DEC AX ADD AX, 7FFFH ADC AX, 1 NEG AX OR AX, 3FDFH AND AX, 0EBEDH XCHG AH, AL SAL AX, 1 RCL AX, 1 【判断题】判断正误 MOV DX, 09H 【判断题】判断正误 MOV [1200H], [SI] 【单…

Java线程的使用及共享协作

创建线程的三种方式 1、继承Thread&#xff1b; static class MyThread extends Thread{Overridepublic void run() {//do something...} } public static void main(String[] args) throws InterruptedException {MyThread thread new MyThread ();thread.start(); } 2、实…

WCF学习笔记(三):开启net.tcp端口

正在做一个使用tcp协议的WCF示例&#xff0c;遇到很多问题。首当其冲的问题就是——如何为WCF打开tcp端口。。。 具体步骤如下&#xff1a; 1、在IIS中为WCF安装支持TCP协议的组件&#xff1a; 2、在防火墙的入栈规则中开启808端口&#xff1b; 3、在servies.msc中打开两个服务…

孪生神经网络_轩辕实验室:数字孪生:基于机器学习的汽车数字孪生模型

本文来源&#xff1a;A. Rassolkin, T. Vaimann, A. Kallaste, and V. Kuts, “Digital twin for propulsion drive of autonomous electric vehicle,” in 2019 IEEE 60th International Scientific Conference on Power and Electrical Engineering of Riga Technical Univer…

Java线程Fork/Join思想及实现

最近在看线程这一块的东西&#xff0c;所以之前的那篇文章就是用来记录的&#xff0c;但看起来好简单的样子&#xff0c;哈哈哈&#xff01; 这两天看的是Fork/Join 分而治之的思想&#xff0c;Doug Lea大师的JUC还是挺强的&#xff0c;学并发编程应该没有人不知道这个大佬吧&…

Sgen.exe: Speed up XmlSerializer's Startup Performance [.NET 2.0, XML Serialization]

Sgen.exe: Speed up XmlSerializers Startup Performance [.NET 2.0, XML Serialization] Written by Allen Lee 1. Why Sgen.exe? 在《Serialize Your Deck with Positron [XML Serialization, XSD, C#]》一文中&#xff0c;我们领略到 XML Serialization 是如何简化我们的 X…

Java线程并发常用工具类使用

这次整理了一些比较常用的线程工具类啦。 CountDownLatch&#xff1a;在一组线程执行完后&#xff0c;才能开始执行调用等待的线程。上片文章提到过junit的测试尽量不要测试线程&#xff0c;如果硬是要可以使用CountDownLatch进行测试 CyclicBarrier&#xff1a;在一组线程中…

三维图形几何变换算法实验_计算机视觉方向简介 | 深度学习视觉三维重建

点击上方“计算机视觉life”&#xff0c;选择“星标”快速获得最新干货作者&#xff1a; Moonsmilehttps://zhuanlan.zhihu.com/p/79628068本文已由作者授权&#xff0c;未经允许&#xff0c;不得二次转载三维重建意义三维重建作为环境感知的关键技术之一&#xff0c;可用于自动…

读《高效程序员的45个习惯——敏捷开发修炼之道》

本书主要用平易的语言讲述了45个有助于提高程序员自身敏捷的习惯&#xff0c;个人感觉这种老外写的书翻译成中文就少了很多意思。 主要的45个习惯是&#xff1a; 做事欲速则不达对事不对人排除万难跟踪变化对团队投资懂得丢弃打破沙锅问到底把握开发节奏让客户做决定让设计指导…

Java线程CAS原子操作

这次分享一些关于原子操作(CAS)的东西. 定义 CAS(Compare And Swap)是CPU的一个指令级别的操作&#xff0c;叫原子操作&#xff0c;原子操作是不可分割的&#xff0c;跟事务差不多&#xff0c;要么全部执行完成&#xff0c;要么不执行&#xff1b; 像这种操作有点类似阻塞锁…

python 导航栏_解析导航栏的url--selnium,beautifulsoup实战

前段时间做ui自动化测试的时候&#xff0c;导航栏菜单始终有点问题&#xff0c;最后只好直接获取到url&#xff0c;然后直接使用driver.get(url)进入页面&#xff1b;包括做压测的时候&#xff0c;比如我要找出所有报表菜单的url&#xff0c;这样不可能手动去一个一个找出来&am…