Blog

查看物理内存分布情况

Content #

外设所需要的内存主要包括外设的工作内存、DMA 区域和用于 IO 映射的内存。在 Linux 系统上,我们可以使用以下命令查看物理内存分布情况:

$ cat /proc/iomem
00000000-00000fff : reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c8dff : Video ROM
000c9000-000c99ff : Adapter ROM
000f0000-000fffff : reserved
  000f0000-000fffff : System ROM
00100000-3f7fefff : System RAM
  01000000-0172ac34 : Kernel code
  0172ac35-01d1c9bf : Kernel data
  01e74000-01fdbfff : Kernel bss
3f7ff000-3f7fffff : reserved
3f800000-3fffffff : RAM buffer
40000000-47ffffff : System RAM
f0000000-fbffffff : PCI Bus 0000:00
  f0000000-f1ffffff : 0000:00:02.0
    f0000000-f015ffff : efifb
  f2000000-f2ffffff : 0000:00:03.0
    f2000000-f2ffffff : xen-platform-pci
  f3000000-f300ffff : 0000:00:02.0
  f3020000-f3020fff : 0000:00:02.0
  f3021000-f3021fff : 0000:00:04.0
    f3021000-f3021fff : ehci_hcd
fc000000-ffffffff : reserved
  fec00000-fec003ff : IOAPIC 0
  fee00000-fee00fff : Local APIC

你会发现,物理内存最重要的三个部分是:

...

GOT的内容是运行时计算出来的

Content #

在动态链接对应的共享库,在共享库的 data section 里面,保存了一张全局偏移表(GOT,Global Offset Table)。虽然共享库的代码部分的物理内存是共享的,但是数据部分是各个动态链接它的应用程序里面各加载一份的。所有需要引用当前共享库外部的地址的指令,都会查询 GOT,来找到当前运行程序的虚拟内存里的对应位置。而 GOT 表里的数据,则是在加载一个个共享库的时候写进去的。

不同的进程,调用同样的 lib.so,各自 GOT 里面指向最终加载的动态链接库里面的虚拟内存地址是不同的。

这样,虽然不同的程序调用的同样的动态库,各自的内存地址是独立的,调用的又都是同一个动态库,但是不需要去修改动态库里面的代码所使用的地址,而是各个程序各自维护好自己的 GOT,能够找到对应的动态库就好了。

GOT 表位于共享库自己的数据段里。GOT 表在内存里和对应的代码段位置之间的偏移量,始终是确定的。这样,共享库就是地址无关的代码,对应的各个程序只需要在物理内存里面加载同一份代码。而又要通过各个可执行程序在加载时,生成的各不相同的 GOT 表,来找到它需要调用到的外部变量和函数的地址。

这是一个典型的、不修改代码,而是通过修改“地址数据”来进行关联的办法。它有点像在 C 语言里面用函数指针来调用对应的函数,并不是通过预先已经确定好的函数名称来调用,而是利用当时它在内存里面的动态地址来调用。

Viewpoint #

From #

在init函数中实现“注册模式”

在init函数中实现“注册模式” #

为了让你更好地理解,首先我们来看一段使用 lib/pq 包访问 PostgreSQL 数据库的代码示例:

import (
    "database/sql"
    _ "github.com/lib/pq"
)
func main() {
    db, err := sql.Open("postgres",
                        "user=pqgotest dbname=pqgotest sslmode=verify-full")
    if err != nil {
        log.Fatal(err)
    }

    age := 21
    rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
    ...
}

其实,这是一段“神奇”的代码,你可以看到示例代码是以空导入的方式导入 lib/pq 包的,main 函数中没有使用 pq 包的任何变量、函数或方法,这样就实现了对 PostgreSQL 数据库的访问。而这一切的奥秘,全在 pq 包的 init 函数中:

func init() {
    sql.Register("postgres", &Driver{})
}

这个奥秘就在,我们其实是利用了用空导入的方式导入 lib/pq 包时产生的一个“副作用”,也就是 lib/pq 包作为 main 包的依赖包,它的 init 函数会在 pq 包初始化的时候得以执行。

从上面代码中,我们可以看到在 pq 包的 init 函数中,pq 包将自己实现的 sql 驱动注册到了 sql 包中。这样只要应用层代码在 Open 数据库的时候,传入驱动的名字(这里是“postgres”),那么通过 sql.Open 函数,返回的数据库实例句柄对数据库进行的操作,实际上调用的都是 pq 包中相应的驱动实现。

...

init函数实现对包级变量的复杂初始化

init函数实现对包级变量的复杂初始化 #

有些包级变量需要一个比较复杂的初始化过程,有些时候,使用它的类型零值(每个 Go 类型都具有一个零值定义)或通过简单初始化表达式不能满足业务逻辑要求,而 init 函数则非常适合完成此项工作,标准库 http 包中就有这样一个典型示例:

var (
    http2VerboseLogs    bool // 初始化时默认值为false
    http2logFrameWrites bool // 初始化时默认值为false
    http2logFrameReads  bool // 初始化时默认值为false
    http2inTests        bool // 初始化时默认值为false
)
func init() {
    e := os.Getenv("GODEBUG")
    if strings.Contains(e, "http2debug=1") {
        http2VerboseLogs = true // 在init中对http2VerboseLogs的值进行重置
    }
    if strings.Contains(e, "http2debug=2") {
        http2VerboseLogs = true // 在init中对http2VerboseLogs的值进行重置
        http2logFrameWrites = true // 在init中对http2logFrameWrites的值进行重置
        http2logFrameReads = true // 在init中对http2logFrameReads的值进行重置
    }
}

标准库 http 包定义了一系列布尔类型的特性开关变量,它们默认处于关闭状态(即值为 false),但我们可以通过 GODEBUG 环境变量的值,开启相关特性开关。

可是这样一来,简单地将这些变量初始化为类型零值,就不能满足要求了,所以 http 包在 init 函数中,就根据环境变量 GODEBUG 的值,对这些包级开关变量进行了复杂的初始化,从而保证了这些开关变量在 http 包完成初始化后,可以处于合理状态。

Viewpoint #

From #

init函数重置包级变量值

init函数重置包级变量值 #

init 函数就好比 Go 包真正投入使用之前唯一的“质检员”,负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查。在 Go 标准库中,我们能发现很多 init 函数被用于检查包级变量的初始状态的例子,标准库 flag 包对 init 函数的使用就是其中的一个,这里我们简单来分析一下。

flag 包定义了一个导出的包级变量 CommandLine,如果用户没有通过 flag.NewFlagSet 创建新的代表命令行标志集合的实例,那么 CommandLine 就会作为 flag 包各种导出函数背后,默认的代表命令行标志集合的实例。

而在 flag 包初始化的时候,由于 init 函数初始化次序在包级变量之后,因此包级变量 CommandLine 会在 init 函数之前被初始化了,你可以看一下下面的代码:

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
    f := &FlagSet{
        name:          name,
        errorHandling: errorHandling,
    }
    f.Usage = f.defaultUsage
    return f
}
func (f *FlagSet) defaultUsage() {
    if f.name == "" {
        fmt.Fprintf(f.Output(), "Usage:\n")
    } else {
        fmt.Fprintf(f.Output(), "Usage of %s:\n", f.name)
    }
    f.PrintDefaults()
}

我们可以看到,在通过 NewFlagSet 创建 CommandLine 变量绑定的 FlagSet 类型实例时,CommandLine 的 Usage 字段被赋值为 defaultUsage。

...

init函数的三个行为特征

init函数的三个行为特征 #

  1. 执行顺位排在包内其他语法元素的后面;
  2. 每个 init 函数在整个 Go 程序生命周期内仅会被执行一次;
  3. init 函数是顺序执行的,只有当一个 init 函数执行完毕后,才会去执行下一个 init 函数。

基于上面这些特征,init 函数十分适合做一些包级数据初始化工作以及包级数据初始状态的检查工作。

Viewpoint #

From #

包的初始化次序

包的初始化次序 #

我们就通过一张流程图,来了解学习下 Go 包的初始化次序:

只需要记住这三点就可以了:

  1. 依赖包按“深度优先”的次序进行初始化;
  2. 每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;
  3. 包内的多个 init 函数按出现次序进行自动调用。

Viewpoint #

From #

文章开头三步法

文章开头三步法?(《人人都用得上的写作课》) #

  1. 找到写作内容中最具神秘、冲突的部分,把它记录下来。有时候,它甚至可以直接作为开头。

  2. 把写作内容中最具神秘、冲突的部分进行拆分,拆分成人物、事件、地点、时间、数据等小元素,然后把其中的 1~2 个小元素放大。

比如南香红撰写的《木卡姆:人间非典型音乐》,就是一个很好的例子,它把地点进行了放大。

> 在中国的地图上,你的眼睛一直向西,向西,再向西。找到新疆,找到喀什,再向西南方向,你会看到崇山峻岭的喀拉昆仑山和昆仑山上,有一条细幼的河蜿蜒而下,这条河上标有一个陌生又好听的名字———叶尔羌河。河在塔克拉玛干沙漠的西南部,划了一个弧和塔里木河连接在了一起。

> 一大片一大片黄色的沙漠,一小块一小块人类可以居住的绿洲。沿着这条河滋养的绿洲生活的人,他们把自己称作刀郎人,他们把这条叫叶尔羌的河称作刀郎河,他们把自己唱的歌跳的舞叫做刀郎木卡姆。

  1. 为那些放大的特质建立画面。这一点非常重要,这时你可以把自己想象成一名导演,想想该如何为这个特质构建一个画面。

Viewpoint #

From #

非流动负债(Non-current Liabilities)

Content #

偿还期在一年或者超过一年的一个营业周期以上的债务。非流动负债的主要项目有:

  • 长期借款
  • 应付债券
  • 长期应付款

Intrinsic机制

JVM的Intrinsic 机制,或者叫作内建方法,指的是什么? #

就是针对特别重要的基础方法,JDK 团队直接提供定制的实现,利用汇编或者编译器的中间表达方式编写,然后 JVM 会直接在运行时进行替换。

Viewpoint #

From #