SpringBoot 问题汇总

简介

最近上手项目, 开始接触了 SpringBoot, 相比较 SpringMVC 而言, 省去了繁琐的配置, 秉承默认大于配置的原则, 使用起来更加简单方便。但越是
黑盒(简单), 使用起来越不安, 这里将学习过程中遇到的问题记录下来, 慢慢地打开这个黑盒

  • 初始化工程问题

    1. 使用 Maven 初始化, 有两种方式, 以此解决 Maven 不支持多 parent 的问题

      • 继承默认的 parent

        1
        2
        3
        4
        5
        6
        <!-- Inherit defaults from Spring Boot -->
        <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.7.RELEASE</version>
        </parent>
    - 将 SpringBoot 的依赖添加到 dependencyManagement 中,并且设置  scope=import 

        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencyManagement>
<dependencies>
<!-- Override Spring Data release train provided by Spring Boot -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-releasetrain</artifactId>
<version>Fowler-SR2</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- Spring Boot basic dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.7.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. Maven用户可以继承 spring-boot-starter-parent 项目来获取合适的默认设置。该parent项目提供以下特性
1
2
3
4
5
6
7
8
- 默认编译级别为Java 1.6
- 源码编码为 UTF-8
- 一个 dependency management 节点,允许你省略常见依赖的 <version> 标签,继承自 spring-boot-dependencies POM。
- 恰到好处的资源过滤
- 恰到好处的插件配置(exec插件,surefire,Git commit ID,shade)
- 恰到好处的对 application.properties 和 application.yml 进行筛选,
- 包括特定 profile(profile-specific)的文件,比如 applicationfoo.
- properties 和 application-foo.yml
3. Spring Boot Starters 列表 官方:https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-starter 中文:http://blog.csdn.net/chszs/article/details/50610474 4. 配置工程自检功能:`spring-boot-starter-actuator`
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置文件中增加:management.security.enabled=false 访问以下网站:
1
2
3
4
5
6
http://localhost:8080/beans
http://localhost:8080/env
http://localhost:8080/health
http://localhost:8080/metrics
http://localhost:8080/trace
http://localhost:8080/mappings
  • 默认 的奥秘,来源于:spring-boot-autoconfigure 的 JAR 文件

    1. @SpringBootApplication 注解等价于以默认属性使用 @Configuration , @EnableAutoConfiguration 和 @ComponentScan,即开启组件扫描和自动配置

    2. application.properties(yml) 默认加载路径:当前目录下 /config 子目录,当前目录,classpath 下的 /config,classpath 根路径。如果不喜欢将 application.properties 作为配置文件名,你可以通过指定 spring.config.name 环境属性来切换其他的名称,也可以使用spring.config.location 环境属性引用一个明确的路径(目录位置或文件路径列表以逗号分割)

    3. Spring 4.0 的条件化配置是自动配置的基础,当满足一定条件时才会执行某段程序,比如在 classpath 中发现了 JdbcTemplate 时才自动注入,这使得众多 autoconfig 选项得以简化

    4. 配置的加载顺序

      Spring Boot设计了一个非常特别的 PropertySource 顺序,以允许对属性值进行合理的覆盖,属性会以如下的顺序进行设值:

      1. home目录下的devtools全局设置属性( ~/.spring-bootdevtools.properties ,如果devtools激活)

      2. 测试用例上的@TestPropertySource注解

      3. 测试用例上的@SpringBootTest#properties注解

      4. 命令行参数

      5. 来自 SPRING_APPLICATION_JSON 的属性(环境变量或系统属性中内嵌的内联JSON)

      6. ServletConfig 初始化参数

      7. ServletContext 初始化参数

      8. 来自于 java:comp/env 的JNDI属性

      9. Java系统属性(System.getProperties())

      10. 操作系统环境变量

      11. RandomValuePropertySource,只包含 random.* 中的属性

      12. 没有打进jar包的Profile-specific应用属性( application-{profile}.properties 和YAML变量)

      13. 打进jar包中的Profile-specific应用属性( application-{profile}.properties 和YAML变量)

      14. 没有打进jar包的应用配置( application.properties 和YAML变量)

      15. 打进jar包中的应用配置( application.properties 和YAML变量)

      16. @Configuration 类上的 @PropertySource 注解

      17. 默认属性(使用 SpringApplication.setDefaultProperties 指定)

  • 开发调试问题

    1. 使用 spring-boot-devtools 来实现热部署(TODO)

    2. Maven 构建插件的主要功能是把项目打包成一个可执行的超级 JAR(uber-JAR),包括把应用程序的所有依赖打入 JAR 文件内,并为 JAR 添加一个描述文件,其中的内容能够通过 java -jar 来运行应用程序

  • Spring 启动问题

    精辟:读取配置说明(xml,java配置,Groovy配置,其他类型配置),再应用程序上下文里初始化 Bean,将 Bean 注入依赖他们的其他 Bean 中,Spring 帮你通过 组件扫描,自动织入和生命切面等额外辅助功能,帮你简单的做了初始化的事情

  • 导入的 starter 是如何 在 application.properties 中给出配置提示的

    starter 的包 /META-INF/spring.factories 中会有 org.springframework.boot.autoconfigure.EnableAutoConfiguration 自动配置的实现类,打开这个类可以看到实现了 ImportBeanDefinitionRegistrar 接口的 registerBeanDefinitions 方法,如此一来,配置文件中就会给出提示啦

  • Spring Cloud 大家族

1、Spring Cloud Config 配置中心,利用git集中管理程序的配置。

2、Spring Cloud Netflix 集成众多Netflix的开源软件

3、Spring Cloud Bus 消息总线,利用分布式消息将服务和服务实例连接在一起,用于在一个集群中传播状态的变化

4、Spring Cloud for Cloud Foundry 利用Pivotal Cloudfoundry集成你的应用程序

5、Spring Cloud Cloud Foundry Service Broker 为建立管理云托管服务的服务代理提供了一个起点。

6、Spring Cloud Cluster 基于Zookeeper, Redis, Hazelcast, Consul实现的领导选举和平民状态模式的抽象和实现。

7、Spring Cloud Consul 基于Hashicorp Consul实现的服务发现和配置管理。

8、Spring Cloud Security 在Zuul代理中为OAuth2 rest客户端和认证头转发提供负载均衡

9、Spring Cloud Sleuth SpringCloud应用的分布式追踪系统,和Zipkin,HTrace,ELK兼容。

10、Spring Cloud Data Flow 一个云本地程序和操作模型,组成数据微服务在一个结构化的平台上。

11、Spring Cloud Stream 基于Redis,Rabbit,Kafka实现的消息微服务,简单声明模型用以在Spring Cloud应用中收发消息。

12、Spring Cloud Stream App Starters 基于Spring Boot为外部系统提供spring的集成

13、Spring Cloud Task 短生命周期的微服务,为SpringBooot应用简单声明添加功能和非功能特性。

14、Spring Cloud Task App Starters

15、Spring Cloud Zookeeper 服务发现和配置管理基于Apache Zookeeper。

16、Spring Cloud for Amazon Web Services 快速和亚马逊网络服务集成。

17、Spring Cloud Connectors 便于PaaS应用在各种平台上连接到后端像数据库和消息经纪服务。

18、Spring Cloud Starters (项目已经终止并且在Angel.SR2后的版本和其他项目合并)

19、Spring Cloud CLI 插件用Groovy快速的创建Spring Cloud组件应用。

如需转载,请注明出处

NodeJS - 理解 内存控制

简介

V8 具有内存限制,所有的 JavaScript 对象都是通过堆来进行分配的,可以通过 process.memoryUsage() 来查看内存信息,其中 heapTotal 和 heapUsed 分别代表了已申请到的对内存和当前使用的量。如果当前代码已申请的堆空闲内存不够分配新的对象,将就申请堆内存,知道堆的大小超过 V8 的内存限制为止

为何采取 V8 内存限制:一次小的垃圾回收需要 50ms 以上,一次非增量式的垃圾回收甚至需要 1s 以上,这是垃圾回收引起的 JS 线程暂停执行的时间,应用的性能和响应时间都会大幅度下降

V8 的垃圾回收机制

采用 node –trace-gc xxx 命令行参数来查看内存垃圾回收日志,使用 –prof 可以得到 V8 执行时的性能分析数据,配合 tick-processor 工具查看统计信息

垃圾回收算法
  • 基于分代式垃圾回收(解决不同的对象生命周期不同)

    --max-old-space-size 指定老生代内存空间的最大值 + --max-new-space-size 指定新生代内存空间的最大值 = V8 堆的整体大小

  • 新生代的回收算法:Scavenge(base on Cheney)

    核心思想:空间一分为二,从 From 空间 复制 到 To 空间,缺点是只能使用堆内存的一般,好处是以空间换取时间(块),适合新生代频繁的内存回收

    对象晋升两个条件:是否经历过 Scavenge 回收,To空间的内存占用是否超过了比例(25%)

  • 老生代的回收算法:Mark-Sweep & Mark-Compact

    核心思想:标记 - 清楚 - 整理内存碎片

  • 回收策略:延迟清理(lazy sweeping)和 增量式整理(incremental compaction)

    为了降低全堆垃圾回收带来的线程停顿问题,采用增量标记来减少由于垃圾回收造成的停顿时间

高效使用内存

在正常的 JavaScript 执行中,无法立即回收的内存有必报和全局变量引用这两种情况,由于 V8 的内存限制,要十分小心此类变量是否无限制的增加,因为他会导致老生代的对象增多

内存指标

  • 使用 process.memoryUsage() 查看 Node 进程的内存占用情况
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
// outofmemory.js

function showMem(){
var mem = process.memoryUsage();
var format = function(bytes){
return (bytes / 1024 / 1024).toFixed(2) + 'MB';
}
console.log(`Process: heapTotal ${format(mem.heapTotal)} heapUsed ${format(mem.heapUsed)} rss ${mem.rss}`);
}

var useMem = function(){
var size = 20 * 1024 * 1024;
var arr = new Array(size);
for(var i = 0; i < size; i++){
arr[i] = 0;
}
return arr;
}

var total = [];
for(var j = 0; j < 15; j++){
showMem();
total.push(useMem());
}
showMem();
  • 使用 os.totalmem()os.freemem() 查看 操作系统 的内存情况

  • 堆外内存:那些不是通过 V8 分配的内存 - Buffer 对象

如需转载,请注明出处

NodeJS - 理解 Buffer

简介

Buffer 是一个像 Array 的对象,主要用于操作字节,数组元素为 16 进制的两位数,即 0 到 255的数值,如果超出或者不足,小数等情况,会使用叠加,递减,省略小数部分的措施保证元素数值合法性,此外,采用 JavaScript 和 C++ 相结合的模式,将性能部分用 C++ 来实现,非性能相关的部分用 JavaScript 来实现

内存分配机制

Buffer 对象的内存分配不是在 V8 的堆内存中,而是在 Node 的 C++ 层面实现内存的申请,采用 slab 动态内存管理机制(先申请,后分配),以 4KB 作为界限来区分 Buffer 是大对象还是小对象,针对大对象,每次都 alloc 一个足够长的 SlowBuffer 对象(C++层)作为 slab 单元,针对小对象,则共用一个 slab,当不足以分配时(可分配小于 4KB),才会 alloc 一个新的 slab 内存进行再分配。需要注意的是当且仅当一个 slab 上面的所有小对象在作用域释放并都可以回收时,slab 的 8KB 才会被回收,此处会存在由于编码不当导致的内存泄漏,浪费问题

在 Buffer 中创建一个数组,需要注意以下规则:

  • Buffer 是内存拷贝,而不是内存共享
  • Buffer 占用内存被解释为一个数组,而不是字节数组。比如,new Uint32Array(new Buffer([1,2,3,4])) 创建了 4 个 Uint32Array,它的成员为 [1,2,3,4],而不是 [0x1020304] 或 [0x4030201]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 4KB 作为界限的由来:createPool 判断条件
Buffer.poolSize = 8 * 1024;
function allocate(size) {
if (size <= 0) {
return new FastBuffer();
}
if (size < (Buffer.poolSize >>> 1)) {
if (size > (poolSize - poolOffset))
createPool();
var b = new FastBuffer(allocPool, poolOffset, size);
poolOffset += size;
alignPool();
return b;
} else {
return createUnsafeBuffer(size);
}
}

四种 内存分配的 API

  • Buffer.from

  • Buffer.alloc

  • Buffer.allocUnSafe

  • Buffer.allocUnSafeSlow

支持与字符串相互转换

目前支持的字符串编码类型有:ASCII,UTF-8,UTF-16LE/UCS-2,Base64,Binary,Hex,可以用 isEncoding() 来判断是否支持编码

对于不支持的编码类型,可以通过 iconviconv-lite 两个模块来支持更多编码类型转换

转换成 Buffer:new Buffer(str,[encoding]) 和 buf.write(string,[offset],[length],[encoding])

转换成 String:buf.toString([encoding],[start],[end])

正确的拼接方式

用一个数组来存储接收到的所有 Buffer 片段并记录下所有片段的总长度,然后调用 Buffer.concat() 方法生成一个合并的 Buffer 对象

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
35
36
Buffer.concat = function concat(list, length) {
var i;
if (!Array.isArray(list))
throw kConcatErr;

if (list.length === 0)
return new FastBuffer();

if (length === undefined) {
length = 0;
for (i = 0; i < list.length; i++)
length += list[i].length;
} else {
length = length >>> 0;
}

var buffer = Buffer.allocUnsafe(length);
var pos = 0;
for (i = 0; i < list.length; i++) {
var buf = list[i];
if (!isUint8Array(buf))
throw kConcatErr;
_copy(buf, buffer, pos);
pos += buf.length;
}

// Note: `length` is always equal to `buffer.length` at this point
if (pos < length) {
// Zero-fill the remaining bytes if the specified `length` was more than
// the actual total length, i.e. if we have some remaining allocated bytes
// there were not initialized.
buffer.fill(0, pos, length);
}

return buffer;
};

Buffer 与性能

  • 网络传输用 Buffer 比直接传字符串要快很多

    通过预先转换静态内容为 Buffer 对象,可以有效地减少 CPU 的重复使用,节省服务器资源。在构建 Web 应用中,可以选择将页面中的动态内容和静态内容分离,静态内容部分可以通过预先转换为 Buffer 的方式,是性能得到提升。由于文件自身是二进制数据,所以在不需要改变内容的场景下,尽量只读取 Buffer,然后直接传输,不做额外的转换,避免损耗

  • 文件读取速度与 highWaterMark 有关

    这个值代表了每次读取的长度,对 Buffer 内存的分配和使用有一定影响,设置过小,可能导致系统调用的次数过多,在读取一个相同的大文件时,该值越大,读取的速度越快

如需转载,请注明出处

读书笔记-程序员的自我修养(十一)

系统调用与API

  • 普及:现代操作系统中程序本身是 没有权利访问过多系统资源 的,为了防止程序访问冲突,操作系统将可能产生冲突的系统资源给 保护起来

  • 普及:每个操作系统都会提供 一套接口,以供应用程序使用,这些接口往往通过 中断 来实现,比如 Linux 使用 0x80 号中断作为系统调用的入口,Windows 采用 0x2E 号中断作为系统调用入口

  • 系统调用的弊端

    • 使用不便,操作系统提供的系统调用接口往往过于原始,程序员需要了解很多与操作系统相关的细节

    • 各个操作系统之间系统调用不兼容,定义和实现都不大一样

    • 为此:运行时库将不同的操作系统的系统调用包装为统一固定的接口,使得童颜的代码,在不同的操作系统下都可以直接编译,并产生一致的效果,这就是源码级上的可移植性

  • 系统调用的原理

    • 用户态内核态:现代操作系统的CPU在不同特权级别下执行不同的指令,称之为 用户模式(User Mode)内核模式(Kernel Mode)

    • 系统调用 是运行在内核态的,而 应用程序 基本都是运行在用户态的

    • 操作系统一般通过 中断 来从用户态 切换 到内核态,中断是一个硬件或软件发出的 请求,要求 CPU 暂停当前的工作 转手去处理 更加重要的事情

      • 中断的两个属性:中断号(Interrupt Number)(中断类型)中断处理程序(Interrupt Service Routine)(既然中断了就要告诉CPU去干什么)

      • 中断向量表(Interrupt Vector Table):是个数组,第 n 项包含了指向第 n 号中断的中断处理程序的 指针

      • 中断的流程:当中断到来时,CPU 会暂停当前执行的代码,根据中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU 会继续执行之前的代码

      • 硬件中断软件中断:硬件中断通常来源于硬件的异常或其他事情的发生,软件中断通常是一条 指令(i386 下是 int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断并执行其终端处理程序,比如 int 0x80 会调用第 0x80 号中断的处理程序

  • Windows API

    • Windows API 是以 DLL 导出函数的形式暴露给应用开发者的,规模上非常庞大,其中一个头文件 “Windows.h” 包含了核心部分

    • 基本服务:kernel32.dll、图形设备接口:gdi32.dll、用户接口:user32.dll、高级服务:advapi32.dll、通用对话框:comdlg32.dll、通用控件:comctl32.dll、Shell:shell32.dll、网络服务:ws2_32.dll

如需转载,请注明出处

读书笔记-程序员的自我修养(十)

运行库

  • 入口函数和程序初始化

    • 普及:操作系统装载程序之后,首先运行的代码并不是 main 的第一行,而是 某些别的代码,这写代码负责准备好 main 函数执行所需要的环境,并且负责调用 main 函数,我们称这写代码为 入口函数入口点(Entry Point)

    • 程序初始化:Main函数之前有个入口函数

    1. 操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数

    2. 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等

    3. 入口函数在完成初始化之后,调用 main 函数,正式开始执行程序主体部分

    4. main 函数执行完毕以后,返回到入口函数,入口函数进行清理工作,然后进行系统调用结束进程

    • 入口函数示例

      • GLIBC(Linux):__start -> __libc_start_main -> exit -> _exit,即在 __libc_start_main 执行之前,做了参数压入栈,寄存器初始化,全局变量赋值等操作

      • MSVC CRT(Windows):初始化和 OS 版本有关的全局变量,初始化堆初始化 I/O,获取命令行参数和环境变量,初始化C库的一些数据,调用 main 并记录返回值,检查错误并将 main 的返回值返回

        • 初始化:mainCRTStartup –> heap_init() –> HeapCreate 方法

        • I/O 初始化:建立 打开文件表,如果能够继承自父进程,那么从父进程 获取 继承的句柄,初始化 标准输入输出

  • 运行库与 I/O

    • 对于程序来说,I/O 涵盖的范围还要广一些,一个程序的 I/O 指代了程序与外界的交互,包括文件、管道、网络、命令行、信号等

    • I/O 初始化函数需要在用户空间中建立 stdin、stdout、stderr 及其 对应的 FILE 结构,是的程序进入 main 之后可以直接使用 printf,scanf 等函数

  • C++运行库

    • 普及:任何一个 C 程序,它的背后都有一套庞大的代码来进行支撑,以使得该程序能够正常运行。这套代码至少包括入口函数,及其所以来的函数所构成的函数集合,当然,他还理应包括各种标砖函数的实现,我们称之为 运行时库(Runtime Library),亦或 C运行库(CRT)

    • CRT大致包含了如下功能

      • 启动与退出:包括入口函数及入口函数所依赖的其他函数等

      • 标准函数:由C语言标准规定的 C语言标准库 所拥有的函数实现

      • I/O:I/O功能的封装和实现

      • :堆的封装和实现

      • 语言实现:语言中一些特殊功能的实现

      • 调试:实现调试功能的代码

    • C语言标准库由 24 个 C头文件 组成:标准输入输出(stdio.h),文件操作(stdio.h),字符操作(ctype.h),字符串操作(string.h),数学函数(math.h),资源管理(stdlib.h),格式转换(stdlib.h),时间/日期(time.h),断言(assert.h),各种类型上的常熟(limits.h & float.h),变长参数(stdarg.h),非局部跳转(setjump.h

    • 运行库是和平台(操作系统)强相关 的,提供了不同操作系统平台的底层抽象,Linux 和 Window 平台下的两个主要 C 语言运行库分别为: glibc (GNC C Library)MSVCRT(Microsoft Visual C Run-time)

  • 运行库与多线程

    • Window API :CreateThread() 和 ExitThread()

    • MSVCRT:_beginThread() / _beginthreadex 和 _endthread()

    • Glibc:pthread_create() 和 pthread_exit()

如需转载,请注明出处

读书笔记-程序员的自我修养(九)

内存:一个承载程序运行的介质,也是程序进行各种运算和表达的场所

  • 程序的内存(进程的地址空间)布局
  1. :用于维护函数调用的上下文,执行 函数调用 的功能

  2. :用于程序 动态分配 的内存区域,也是 malloc 或 new 分配的内存区域

  3. 可执行文件映像:存储着可执行文件在内存里的映像,装载器

  4. 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,比如有些地址不允许访问等等

  • 栈与调用惯例

    • 作用:保存了一个函数调用所需要的 维护信息(函数返回地址和参数,临时变量,保存的上下文),常称为 堆栈帧(Stack Frame)

    • 函数的调用方和被调用方对于函数如何调用,遵循 惯用惯例,包括:函数参数的传递顺序和方式,栈的维护方式,名字修饰策略。常用的管理模式有:cdecl(函数调用方),stdcall(函数本身),fastcall(函数本身),pascal(函数本身)

  • 堆与内存管理

    • 普及:全局变量没有办法动态地产生,只能在编译的时候定义

    • 形象解释:运行库相当于是向操作系统 “批发” 了一块较大的对控件,然后 “零售” 给程序用。当全部 “售完” 或程序有大量的内存需求时,再根据实际需求向操作系统 “进货”

    • Linux 提供两种系统调用:brk 和 mmap,Window 提供四种系统调用:HeapCreate,HeapAlloc,HeapFree,HeapDestroy,对向上封装的函数就是著名的 malloc

    • 要点

      • 堆里的同一片内存不能重复释放两次

      • malloc 申请的内存,进程结束后,所有资源都会回收,因此不存在了

      • malloc 申请的内存,逻辑上是连贯的,物理上不一定

    • 堆分配算法:如何管理一大块连续的内存空间,能够按照需求分配、释放其中的空间

      • 空闲链表

        核心原理:在堆里的每一个空闲空间的开头(或结尾)有一个头(header),头结构里记录了上一个(prev)和下一个(next)空间块的地址,这样所有空闲块形成了一个链表,申请的时候,查找符合大小的空闲块,然后将这块空闲空间从链表中”删除”,供使用,回收的时候,需要知道头指针和空间大小,因此在申请的时候,往往申请 K 空间,分配 K + 4 的空间,多余的 4 个记录大小,但这样一旦堆操作越界,破坏了这 4 个里面的数据,整个对就无法正常工作了

      • 位图

        将整个堆划分为大量的 块(Block),每个块大小相同,申请的时候,总是分配整数个块的空间,且称已经分配的第一块为 头(head),其余的称为 主体(Body),优点:速度快,整个堆的空闲信息存储在一个数组中,因此访问该数组时cache容易命中,稳定性好:为了避免用户越界读写破坏,只需要简单的备份下位图即可,缺点:分配容易产生碎片(平均划分),位图很大,cache命中率降低

      • 对象池

        以申请空间的大小作为分水岭,综合应用上述两种算法

如需转载,请注明出处

读书笔记-程序员的自我修养(八)

Windows 下的动态链接

  • 普及:Windows 下的 DLL 文件和 EXE 文件实际上是一个概念,都是 PE 格式 的二进制文件,扩展名并不唯一:.dll.ocx 或是 .cpl 都是 dll 文件,软件更新包(Service Packs)机制就是通过升级 DLL 的形式进行自我完善

  • 普及:当一个 dll 被装载到内存中之后,通常有两个地址概念分别是:基地址(Base Address)相对地址(RVA, Relative Virtual Address),基地址就是被装载的起始地址,RVA地址就是 地址 + 基地址

  • 普及:dll 实现进程共享数据段,不推荐,存在一定的安全风险,应该尽量 避免 这种 dll 共享数据段来实现进程间通信

  • 普及:声明 dll 中的某个函数为 导出函数 的办法有两种:一种是使用 __declspec(dllexport) 扩展,另一种是采用模块定义(.def)文件声明

  • 普及:dll 支持显示运行时链接,提供了 3 个 API 分别是:LoadLibrary,GerProcAddress,FreeLibrary

  • 符号导入导出表

    • 导出表,EXP文件,导出重定向,导入表,导入函数调用,这些都是 dll 中存在的结构和方式,均与符号和函数相关
  • DLL优化

    • 应用程序在加载 dll 导致启动速度变慢,这里主要有两个原因:1. dll 的代码段和数据段本身并不是地址无关的,当被装载的目标地址被占用时,整个 dll 便会引起 rebase,频繁的 rebase 情况更加糟糕; 2. 导入函数的符号在运行时需要被 逐个查找解析,这里用的是二分查找,整个过程随着 dll 数量的增加,也是非常耗时的

    • 重定基地址(Rebasing):只需要重新定位基地址即可,其余的 RVA 只是的偏移量,不受影响,当且仅当 dll 被装载时,基地址被占用的情况下,才会发生 重定位,所以 windows 系统为本身自带的许多库单独划分了一段空间区域(0x70000000 ~ 0x80000000),用于映射这些系统常用的 dll

    • dll 绑定:把导出函数的地址保存到模块的导入表中,省去了每次启动时符号解析的过程,根本优化在于:当程序每次运行时,所有被依赖的 dll 都会装载,并且一些列的导入导出符号依赖关系都会 被重新解析,大多数情况下,这些 dll 都会 以同样的顺序 被装载到 同样的内存地址,所以他们的到处符号的地址都是不变的

  • 在使用 C++ 时需要注意的动态链接事项

    1. 所有接口函数都应该是抽象的,所有的方法都应该是纯虚的

    2. 所有的全局函数都应该是用 extern “C” 来防止名字修饰的不兼容,并且导出函数都是应该是 __stdcall 调用规范的

    3. 不要使用 C++ 标准的 STL

    4. 不要使用异常

    5. 不要使用虚析构函数

    6. 不要在 dll 里面申请内存,而且在 dll 外释放(或者相反)

    7. 不要再接口中使用重载方法

  • DLL HELL:主要是更新过程中的兼容性问题,解决方法如下:

    • 静态链接

    • 防止 dll 覆盖(DLL Stomping)

    • 避免 dll 冲突(Conflicting DLLs)

    • 应用程序使用 Manifest 文件来打包,控制自身依赖的 dll

如需转载,请注明出处

读书笔记-程序员的自我修养(七)

动态链接(Dynamic Linking):一个单一个可执行文件模块被拆分成若干模块,在程序运行时进行链接的一种方式

  • 欲扬先抑:静态链接饱受 内存磁盘空间浪费(同一个目标文件存在多份供使用),更新发布需要重新链接 等问题,而动态链接解决了上面的问题,提供了 可扩展性(动态选择加载),但饱受 兼容性 (如不兼容,程序将崩溃无法运行),性能损耗(装载时重新链接)困扰

  • 基本思想:把程序按照模块 拆分 成各个相对 独立 部分,在程序 运行时 才将它们链接在一起形成一个 完整 的程序,而不是像静态链接一样把所有的程序模块都链接成一个单独的可执行文件

  • 普及: Linux 下,ELF 动态链接文件叫 动态共享对象(DSO,Dynamic Shared Objects),以 .so 结尾;Windows 下,动态链接文件叫 动态链接库(Dynamical Linking Library),以 .dll 结尾

  • 普及动态链接器 才完成以上操作,策略:延迟绑定(Lazy Binding)

  • 普及PIC(Position-independent Code)地址无关代码 技术,GOT(Global Offset Table)全局偏移量表

  • 动态链接的地址空间分配 奥秘

    • 共享对象的 最终装载地址 在编译时是 不确定 的,而是在 装载 时,装载器根据当前地址空间的空闲情况,动态分配一块 足够大小 的虚拟地址空间给相应的共享对象

    • 普及:静态链接时提到的重定位叫做 链接时重定位(Link Time Relocation),而动态链接提到的重定位叫做 装载时重定位(Load Time Relocation),也成 基址重置(Rebasing)

    • 共享对象的虚拟地址空间分配技术离不开:地址无关代码(PIC机制)共享模块的全局变量(线程私有存储 Thread Local Storage)数据段地址无关性,也正是这些无关性技术,才保证了共享对象被多个程序引用时,能够发挥各自的作用,确保程序不出现崩溃

  • 延迟绑定(Lazy Binding)的 艺术

    • 性能的事实:动态链接是以 牺牲性能 为代价的,具体表现在:一方面 对于全局和静态数据的访问、模块间的调用都要进行复杂的 GOT定位,然后 间接寻址另一方面动态链接的工作是在 运行时 完成的,不是事先链接好的

    • 优化的手段:延迟绑定,理解为当函数第一次被用到时才进行绑定(符号查找、重定位等),使用 PLT (Procedure Linkage Table) 来实现,通常以 .plt 作为段名,保存在 ELF 文件中

    • 实现原理通过 PLT 中的待跳转指令,将各个函数的待跳转偏移量链接到地址栏,等运行时使用的时候,直接读取偏移量进行跳转,这里不是直接读取GOT链接到真正的目标函数地址,而是读取偏移量,进行jump,再到目标函数

    • 原话:而是将上面代码中第二条指令"push n"的地址填入到 bar@GOT 中,这个步骤不需要查找任何符号

  • 动态链接 相关结构

    • .interp 段:保存可执行文件需要的 动态链接器 的路径,内容就是个字符串

    • .dymanic 段:保存了动态链接器所需要的 基本信息:依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等,位于经典的头文件 elf.h (Elf32_Dyn)

    • 动态符号表(Dynamic Symbol Table)[.dynsym 段]:只保留动态链接这些模块之间的符号导入导出关系

    • 动态链接重定位表.rel.dyn 是对数据引用的重定位修正,它所修正的位置位于 .got 及 数据段.rel.plt 是对函数引用的修正,它所修正的位置位于 .rel.plt

  • 动态链接时进程 堆栈初始化信息

    • 堆栈里保存了关于进程 执行环境:程序执行入口命令行参数 等信息,还保存了动态链接器所需要的一些 辅助信息数组(Auxiliary Vector)

    • int main(int argc, char argv[]) ,argc 表示参数的个数,*argv数组 是参数

  • 动态链接器的 步骤实现

    • 步骤:

      • 启动动态链接器本身

        动态链接器本身也是一个共享对象,因此它的编写除了不能依赖于其他任何共享对象,而且本身所需要的全局和静态变量的重定位工作也由它本身来完成,因此有一段精致的启动代码称之为 自举(Bootstrap)

      • 装载所有需要的共享对象

        动态链接器会根据 .dynamic 段中所依赖的共享对象,使用 广度优先的图遍历 算法,来按顺序状态共享对象;全局符号表(Global Symbol Table) 的介入,为了解决相同符号名的链接冲突,如已存在,则后加入的符号被忽略

      • 重定位和初始化

        动态链接器根据进程的全局符号表,对需要重定位的位置进行修正,如果某个共享对象有 .init 段,那么动态链接器会执行,实现共享对象 特有的 初始化过程(常见的,C++全局/静态对象的构造就在此初始化)

    • 实现:

      • 普及内核在装载完 ELF 可执行文件以后,就返回到用户空间,将控制权交给程序的入口。对于静态链接,入口地址是 e_entry 制定的地址,对于动态链接,入口地址是将动态链接器映射至进程地址空间,然后把控制权交给动态链接器

      • 普及:Linux 动态链接器:/lib/ld-linux.so.2 -> /lib/ld-x.y.z.so

      • 动态链接器在实现中的 几个问题

        1. 动态链接器本身是动态链接还是静态链接? 答:静态链接,不依赖于任何共享对象

        2. 动态链接器本身必须是 PIC 吗?答:不一定,如果是 PIC 会简单些

        3. 动态链接器可以被当作可执行文件运行,装载地址是多少?答:0x00000000 无效的,内核 会装载它时 分配 一个 有效

      • 灵活技术:显示运行时链接(Explicit Run-time Linking)

        • 顾名思义:让程序自己在运行时控制加载制定的模块,并且可以在不需要该模块时将其卸载,操作对象是 DLL

        • 动态链接库提供了 4 个 API 来实现,它们分别是:dlopen(),dlsym(),dlerror(),dlclose()

  • 总结一波

    • 动态链接可以更加有效地利用内存和磁盘资源,可以更加方便的维护升级程序,可以让程序的重用变得更加有效和可行

    • 装载时重定位和地址无关代码是解决绝对地址引用的两个方法,装载时重定位的缺点是无法共享代码段,但是它的运行速度较快;而地址无关代码的缺点是运行速度较慢,但它可以实现代码段在各个进程之间的共享,还介绍了 ELF 的延迟绑定 PLT 技术

    • .interp、.dynamic、动态符号表、重定位表等接口,它们是实现 ELF 动态链接的关键结构。动态链接器实现自举,装载共享对象,实现重定位和初始化的过程,实现动态链接,最后关键技术:显示运行时链接

如需转载,请注明出处

读书笔记-程序员的自我修养(六)

可执行文件的装载与进程

前言:源文件经过编译,链接过程,生成了目标可执行文件,可执行文件只有装载到内存以后才能被CPU执行

  • 程序进程:前者理解为一些预先编译好的指令和数据集合的一个 文件,是一个 静态 的概念,后者理解为程序运行时的一个 过程,是一个 动态 的概念

  • 每个进程都被操作系统分配以 虚拟的地址空间(Virtual Address Space) 来供运行时分配使用,如果被操作系统捕捉到进程非法访问了额外控件,将当做 非法操作强制结束进程,常见的 Windows:”进程因非法操作需要关闭” 和 Linux:Segmentation fault

  • 限制: 对于32位系统而言,允许分配给进程最大为:4GB的地址空间使用,而64位系统,则17179869184GB,即使 4GB,操作系统也会分配占用之后,最终留给进程最大3GB的地址空间使用,再通俗一点:整个进程在执行的时候,所有代码、数据包括通过 C 语言 malloc() 等方法申请的虚拟空间之和不得超过 3GB

  • PAE(Physical Address Extension)和 AWE(Address Windowing Extension):Intel 扩展了地址线,由 对应的32根(32位)扩展到了 36根 地址线,为此 多出来了256MB的物理空间,使用 窗口映射 技术,变向的增大了内存空间,进而也打破了 3GB 的限制,作为一种补救地址空间不够大的非常规手段

  • 装载的方式

    • 原理:程序执行时所需要的指令和数据必须 在内存中 才能够正常运行

    • 装载的方法:都是利用了 程序局部性 的原理,即某一时刻程序只用到了局部的一些执行和数据,并非全局

      • 以时间换取空间(淘汰):覆盖装入(Overlay)

      • 页映射(Paging),通常32位的Intel都是用4096字节的页作为页长,假设内存为16K,那么会被分为4个页,假设程序需要32K的内存,即需要8个页的空间,那么如何在4个页中不影响程序执行的情况下,自如装载8个页呢? 内存页的分配算法

        • 先进先出算法(FIFO

        • 最少使用算法(LUR

  • 操作系统角度 看可执行文件的装载,干了三件事

    • 创建 一个独立的虚拟地址空间(这也是进程最关键的特征,实质上是创建映射函数所需要的相应的数据结构)

    • 读取 可执行文件头,并建立虚拟空间与可执行文件的映射关系

    • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行

    • 操作系统通过不断的 页错误 来不停地将程序通过虚拟地址管理器装载到物理内存,进程才得以正常运行

  • 进程虚拟空间 分布

    • 区分概念 的关系:ELF段在映射时长度应该都是系统页长度的 整数倍,如果不是,那么多余部分也会占用一个页,进而造成 浪费,实际上,操作系统装载可执行文件时,只会根据段的 权限 来区分,对于相同权限的段,把它们合并到一起当做一个段来映射

    • 普及概念 Linux 中将进程虚拟空间中的一个段叫做 虚拟内存区域(VMA,Virtual Memory Area),Windows 叫做 虚拟段(Virtual Section)

    • 两次合并第一次是链接器把各个目标文件中的相同段(Section,链接视图)合并,统一存放于可执行文件中,第二次是操作系统按照权限把可执行文件中的各个段(Segement,执行视图)合并,映射到虚拟内存区域中

    • 核心操作系统 通过给进程空间划分出一个个 VMA管理 进程的虚拟空间,基本原则是将相同 权限 属性的、有相同 映像文件 的映射成一个VMA,一个进程基本上可以分为如下几 VMA区域:

      • 代码VMA,权限只读、可执行;有映像文件

      • 数据VMA,权限可读写,可执行;有映像文件

      • 堆VMA,权限可读写、可执行;无映像文件,匿名,可向上扩展

      • 栈VMA,权限可读写、不可执行;无映像文件,匿名,可向下扩展

    • 一些限制

      • 堆的 最大 申请数量:受操作系统版本、程序本身大小、用到的动态/共享库数量、大小、程序栈数量、大小等因素影响(随机地址空间分布技术

      • 段地址 对齐:受页和段的长度 整数倍映射 问题,Unix采取了 段合并 映射的方法:各个段接壤部分共享一个物理页面

      • 进程栈 初始化:进程初始化启动之前,一些系统环境变量和进程的运行 参数 会提前保存到 Stack VMA 中,程序的库部分会把堆栈里面的初始化信息中的参数信息传递给 main()argcargv 两个参数

  • 大致了解:Linux 内核装载 ELF 过程简介

    1. bash 进程会调用 fork() 系统调用创建一个新的进程,然后新的进程调用 execve() 系统调用加载执行指定的 ELF 文件
    2. 检查 ELF 可执行文件格式的有效性,比如魔数、程序头表中段的数量
    3. 寻找动态链接的 .interp 段,设置动态链接器路径
    4. 根据 ELF 可执行文件的程序头表的描述,对 ELF 文件进行映射,比如代码、数据、只读数据
    5. 初始化 ELF 进程环境,动态链接准备
    6. 将系统调用的返回地址修改成 ELF 可执行文件的入口点
  • 大致了解:Windows PE 的装载 过程简介

    1. 先读取文件的第一个页,包含了 DOS 头、PE文件头 和 段表
    2. 检查进程地址空间中,目标地址是否可用
    3. 使用段表中提供的信息,将 PE 文件中所有的段一一映射到地址空间中相应的位置
    4. 如果装载地址不是目标地址,则进行 Rebasing
    5. 装载所有 PE 文件所需要的 DLL 文件
    6. 对 PE 文件中的所有导入符号进行解析
    7. 根据 PE 头中制定的参数,建立初始化栈和堆
    8. 建立主线程并启动进程
如需转载,请注明出处

读书笔记-程序员的自我修养(五)

静态链接 到底做了什么?

  • 模块间符号的引用,其实就是链接器的主要职责,即把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接,通俗一点来讲:两个目标文件如何链接成一个可执行文件实质 上是将各个 目标文件 中的 地址,结合其他目标文件,包括了 运行时库(Runtime Library),确定其正确的地址,以下:

  • 任务一: 空间与地址分配(Address and Storage):合并-重组各个目标文件的段,以完成空间和地址的分配

    • 共识:可执行文件中的代码段和数据段都是由输入的目标文件中合并而来的

    • 【摒弃】 方案一:按序叠加:直接将各个目标文件中的相应段依次叠加到一起,缺点就是:内存空间大量碎片化。

    • 方案二:相似段合并:1. 空间与地址分配:先扫描所有输入的目标文件,获取各个段的长度,属性,位置,将输入目标文件中的符号表中所有的符号定义和引用统一放到一个 全局符号表 中。2. 符号解析与重定位:根据第一部收集的所有信息,进行符号解析重定位调整代码中的地址(增加偏移量)

  • 任务二: 符号解析与重定位(Resolution and Relocation)

    • 重定位 :输入目标文件中,凡是引用了 外部符号,全用 0x0000000 这 4 个字节来代替,凡是引用了 外部函数,全用 0xFFFFFFFC 这四个字节来代替。链接器负责把目标文件被外部引用的函数符号的 偏移量,来 替换 这些初始代替量,链接写入输出 可执行文件

    • 重定位表链接器用于识别哪些引用的符号或者函数需要重定位,使用 objdump -r a.o 来查看

    • 符号解析 :之所以链接是因为目标文件中用到的符号被定义在其他目标文件中,使用 objdump -s a.o 来查看符号表,其中 UND 的符号就是未定义类型,就是 待链接对象,即链接的过程经历了符号解析

    • 指令修正方式:由于不同处理器的指令千差万别,寻址的方式也不尽相同,因此在链接过程中替换目标文件地址,需要经历指令修正来确保地址有效性,其中 绝对寻址修正 后的地址为该符号的实际地址,相对寻址修成 后的地址为符号距离被修正位置的地址差

  • 番外介绍:特殊模块:COMMON块

    • 作用:用于链接时存放待链接的 多个命名相同的弱符号,作为储备池,因为链接器无法区分符号类型,故无法判断是否一致,因此链接器链接的选择原则是: 取占用空间最大的那个弱符号为准
  • 任务三: 静态库链接(.a/.lib)

    • 静态库可以简单的看成 一组目标文件的集合,用 ar lib 等工具可以看静态库的内容

    • 静态库里面的一个目标文件只包含一个函数,避免链接函数过多导致程序臃肿,空间浪费

    • 使用 gcc -static --verbose -fno-builtin hello.c 查看详细的链接过程,涉及到的目标文件非常多,过程也相当复杂

  • 综上:链接的 过程控制

    • 链接过程要确定的内容有:使用哪些目标文件?使用哪些库文件?是否在最终可执行文件中保留调试信息?输出文件格式是什么?是否导出符号表等

    • 链接器支持 使用命令行参数控制将链接指令存放在目标文件里使用链接控制脚本 来完成以上内容

    • ld 链接器的链接脚本语法继承于 AT&T 链接器命令语言的语法,风格有点像 C 语言,分为 命令语句赋值语句

    • BFD库 的出现,旨在解决不同软硬件平台上,通过一种 统一 的接口来处理不同的目标文件格式

如需转载,请注明出处