Writings

A place to place some writings

这篇文章主要记录了一些我在学习 Go 语言实现原理过程中的笔记。

本人使用的 go version go1.22.4 darwin/amd64

Go interface

interface 是 Go 语言中很有意思的一个特性,它能让你像纯动态语言一样使用duck typing,而且编译器还能像静态语言一样做类型检查。

如果一个东西叫起来像鸭子,走路也像鸭子,那么它就是鸭子。

也就是说,interface 允许你定义一组方法的集合,任何实现了这组方法的类型都可以被看作是这个 interface 的实现。

有了 interface,可以做很多有意思的事情。

1、可以通过 interface 来抽象,然后再通过组合与继承,让程序足够有表达力。

2、如果不用 interface,一个函数就只能接受具体的类型作为参数,而且也只能返回具体的类型作为返回值,这个函数的适用范围就比较局限。有了 interface,就能让这个函数接受和返回更抽象的类型的参数(某个 interface),相当于让这个函数的适用范围更广了。

3、interface 让程序可以很方便地实现各种编程模式,提升程序的抽象性和解耦度,让程序更容易维护和扩展。


下面我们主要关注 interface 的底层实现原理,也就是要理解这段代码的原理(直接用 AI 生成几段代码给我们):

类型断言

1
2
3
var i interface{} = 42
n, ok := i.(int) // n == 42, ok == true
s, ok := i.(string) // s == "", ok == false

类型转化:把一种类型转化为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// 定义一个接口
type Speaker interface {
Speak() string
}

// 定义一个实现了该接口的具体类型
type Dog struct{}

var d Dog

// 具体类型 转 空接口
var a interface{} = d

// 空接口 转 特定接口
var s Speaker
s = a.(Speaker) // 使用类型断言

类型 switch:尝试把某个变量转化为其他类型,是类型断言和类型转化的组合。

1
2
3
4
5
6
7
8
switch v := i.(type) {
case T1:
// v 是 T1 类型
case T2:
// v 是 T2 类型
default:
// v 是其他类型
}

动态派发:在运行的时候确定应该调用哪个函数。

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
type Speaker interface {
Speak() string
}

type Dog struct{}

func (d Dog) Speak() string {
return "Woof!"
}

type Cat struct{}

func (c Cat) Speak() string {
return "Meow!"
}

func makeSound(s Speaker) {
fmt.Println(s.Speak())
}

func main() {
d := Dog{}
c := Cat{}
makeSound(d) // 输出 "Woof!"
makeSound(c) // 输出 "Meow!"
}

Go interface 的实现原理

interface 在 Go 语言的实现其实就靠一个叫做 iface 的数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

type iface struct {
tab *itab
data unsafe.Pointer // 数据指针
}

type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

把所有子数据结构也展开,就是如下:

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
37
38
39
40
41
type iface struct { // `iface`
tab *struct { // `itab`
inter *struct { // `interfacetype`
typ struct { // `_type`
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
pkgpath name
mhdr []struct { // `imethod`
name nameOff
ityp typeOff
}
}
_type *struct { // `_type`
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
hash uint32
_ [4]byte
fun [1]uintptr
}
data unsafe.Pointer
}

用图来表示就是这样:

理解 iface 的运作原理的最好方式就是直接查看其汇编代码。

写一段简单的代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

type Speaker interface {
Speak() string
}

type Dog struct {
}

//go:noinline
func (*Dog) Speak() string {
return "Dog"
}

type Cat struct {
}

//go:noinline
func (*Cat) Speak() string {
return "Cat"
}

//go:noinline
func JustSpeak(s Speaker) string {
return s.Speak()
}

//go:noinline
func IsSpeaker(s interface{}) bool {
v, ok := s.(Speaker)
if ok {
v.Speak()
}
return ok
}

//go:noinline
func Which(s interface{}) string {
switch s.(type) {
case Cat:
return "Cat"
case Dog:
return "Dog"
case Speaker:
return "Speaker"
default:
return "interface{}"
}
}

//go:noinline
func DogSpeak() {
var s Speaker = &Dog{}
JustSpeak(s)
}

func main() {
}

然后直接一个命令把代码编译为 Plan 9 汇编代码:

GOOS=linux GOARCH=amd64 go build -gcflags="-S" ./main.go 2> ./main.S

先来看 JustSpeak 函数的汇编代码:

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
37
38
39
40
41
42
main.JustSpeak STEXT size=67 args=0x10 locals=0x10 funcid=0x0 align=0x0
0x0000 00000 (/main.go:24) TEXT main.JustSpeak(SB), ABIInternal, $16-16
0x0000 00000 (/main.go:24) CMPQ SP, 16(R14)
0x0004 00004 (/main.go:24) PCDATA $0, $-2
0x0004 00004 (/main.go:24) JLS 40
0x0006 00006 (/main.go:24) PCDATA $0, $-1
0x0006 00006 (/main.go:24) PUSHQ BP
0x0007 00007 (/main.go:24) MOVQ SP, BP
0x000a 00010 (/main.go:24) SUBQ $8, SP
0x000e 00014 (/main.go:24) MOVQ AX, main.s+24(FP)
0x0013 00019 (/main.go:24) MOVQ BX, main.s+32(FP)
0x0018 00024 (/main.go:24) FUNCDATA $0, gclocals·IuErl7MOXaHVn7EZYWzfFA==(SB)
0x0018 00024 (/main.go:24) FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)
0x0018 00024 (/main.go:24) FUNCDATA $5, main.JustSpeak.arginfo1(SB)
0x0018 00024 (/main.go:24) FUNCDATA $6, main.JustSpeak.argliveinfo(SB)
0x0018 00024 (/main.go:24) PCDATA $3, $1
0x0018 00024 (/main.go:25) MOVQ 24(AX), CX
0x001c 00028 (/main.go:25) MOVQ BX, AX
0x001f 00031 (/main.go:25) PCDATA $1, $1
0x001f 00031 (/main.go:25) NOP
0x0020 00032 (/main.go:25) CALL CX
0x0022 00034 (/main.go:25) ADDQ $8, SP
0x0026 00038 (/main.go:25) POPQ BP
0x0027 00039 (/main.go:25) RET
0x0028 00040 (/main.go:25) NOP
0x0028 00040 (/main.go:24) PCDATA $1, $-1
0x0028 00040 (/main.go:24) PCDATA $0, $-2
0x0028 00040 (/main.go:24) MOVQ AX, 8(SP)
0x002d 00045 (/main.go:24) MOVQ BX, 16(SP)
0x0032 00050 (/main.go:24) CALL runtime.morestack_noctxt(SB)
0x0037 00055 (/main.go:24) PCDATA $0, $-1
0x0037 00055 (/main.go:24) MOVQ 8(SP), AX
0x003c 00060 (/main.go:24) MOVQ 16(SP), BX
0x0041 00065 (/main.go:24) JMP 0
0x0000 49 3b 66 10 76 22 55 48 89 e5 48 83 ec 08 48 89 I;f.v"UH..H...H.
0x0010 44 24 18 48 89 5c 24 20 48 8b 48 18 48 89 d8 90 D$.H.\$ H.H.H...
0x0020 ff d1 48 83 c4 08 5d c3 48 89 44 24 08 48 89 5c ..H...].H.D$.H.\
0x0030 24 10 e8 00 00 00 00 48 8b 44 24 08 48 8b 5c 24 $......H.D$.H.\$
0x0040 10 eb bd ...
rel 2+0 t=R_USEIFACEMETHOD type:main.Speaker+96
rel 32+0 t=R_CALLIND +0
rel 51+4 t=R_CALL runtime.morestack_noctxt+0

别被这一大段汇编代码吓到了,其实很好读懂。

最关键的是这一行:

1
0x0020 00032 (/main.go:25)	CALL	CX

这就是动态调用的实现。

在调用这个函数之前,它会先构造一个 iface 来表示传入的 Speaker 类型的变量,然后它会把传入这个的参数的偏移 24 字节作为函数进行调用,实际上就是调用的 Speak() 函数。

那么 iface 是如何被构造出来的?继续看这段代码。

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
37
38
39
40
go:itab.*main.Dog,main.Speaker SRODATA dupok size=32
0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x0010 29 fe 4c 82 00 00 00 00 00 00 00 00 00 00 00 00 ).L.............
rel 0+8 t=R_ADDR type:main.Speaker+0
rel 8+8 t=R_ADDR type:*main.Dog+0
rel 24+8 t=RelocType(-32767) main.(*Dog).Speak+0
main.DogSpeak STEXT size=50 args=0x0 locals=0x18 funcid=0x0 align=0x0
0x0000 00000 (/main.go:56) TEXT main.DogSpeak(SB), ABIInternal, $24-0
0x0000 00000 (/main.go:56) CMPQ SP, 16(R14)
0x0004 00004 (/main.go:56) PCDATA $0, $-2
0x0004 00004 (/main.go:56) JLS 43
0x0006 00006 (/main.go:56) PCDATA $0, $-1
0x0006 00006 (/main.go:56) PUSHQ BP
0x0007 00007 (/main.go:56) MOVQ SP, BP
0x000a 00010 (/main.go:56) SUBQ $16, SP
0x000e 00014 (/main.go:56) FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x000e 00014 (/main.go:56) FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x000e 00014 (/main.go:58) LEAQ go:itab.*main.Dog,main.Speaker(SB), AX
0x0015 00021 (/main.go:58) LEAQ runtime.zerobase(SB), BX
0x001c 00028 (/main.go:58) PCDATA $1, $0
0x001c 00028 (/main.go:58) NOP
0x0020 00032 (/main.go:58) CALL main.JustSpeak(SB)
0x0025 00037 (/main.go:59) ADDQ $16, SP
0x0029 00041 (/main.go:59) POPQ BP
0x002a 00042 (/main.go:59) RET
0x002b 00043 (/main.go:59) NOP
0x002b 00043 (/main.go:56) PCDATA $1, $-1
0x002b 00043 (/main.go:56) PCDATA $0, $-2
0x002b 00043 (/main.go:56) CALL runtime.morestack_noctxt(SB)
0x0030 00048 (/main.go:56) PCDATA $0, $-1
0x0030 00048 (/main.go:56) JMP 0
0x0000 49 3b 66 10 76 25 55 48 89 e5 48 83 ec 10 48 8d I;f.v%UH..H...H.
0x0010 05 00 00 00 00 48 8d 1d 00 00 00 00 0f 1f 40 00 .....H........@.
0x0020 e8 00 00 00 00 48 83 c4 10 5d c3 e8 00 00 00 00 .....H...]......
0x0030 eb ce ..
rel 2+0 t=R_USEIFACE type:*main.Dog+0
rel 17+4 t=R_PCREL go:itab.*main.Dog,main.Speaker+0
rel 24+4 t=R_PCREL runtime.zerobase+0
rel 33+4 t=R_CALL main.JustSpeak+0
rel 44+4 t=R_CALL runtime.morestack_noctxt+0

还记得上面的 iface 数据结构吗?

1
2
3
4
type iface struct {
tab *itab
data unsafe.Pointer // 数据指针
}

你不用很懂 Plan 9 Assembly 也可以看出来,这段代码的核心就是在把 iface 给构造出来并通过指针传递给下一个函数,而构造 iface 的核心步骤之一就是把 go:itab.*main.Dog,main.Speaker 复制到了 AX 上,而这是一个 rodata 只读数据,我也把它列出来了。

go:itab.*main.Dog,main.Speaker 看名字我们也知道,实际上就是 Dog 这个 struct 实现的 Speaker 的函数表(当然 itab 里也包含了变量类型和接口类型的相关信息)。

因此,我们可以大胆地做两个推测:

  • 所有的 interface 实现都有一个对应的 itab,如果 3 个 struct 分别实现了 5 个 interface,那么就会有 15 个这样的 itab rodata。
  • 每个变量在底层都有一个对应的 iface,iface 包含了该变量所需的类型信息和函数表信息,底层是通过把变量组装为 iface,然后以此作为“介质”进行更高级的类型推断或类型转化的实现的。类型转换无非就是从这个样的 iface 转换为另外一个 iface 而已,类型推断无非就是对 itab 做一些相应的判断。

runtime 代码分析

在那个汇编文件里,我们会看到很多 runtime 函数,这些 runtime 函数在类型推断和转化的过程中扮演了关键角色。

  • runtime.typeAssert
  • runtime.interfaceSwitch

我们直接来看函数的实现:

当进行 type assert 时,会先把把 abi.TypeAssert 和 _type 这俩给构造出来,然后把它们的指针放到寄存器里,然后再调用 runtime.typeAssert 函数。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240

type TypeAssert struct {
Cache *TypeAssertCache
Inter *InterfaceType
CanFail bool
}
type TypeAssertCache struct {
Mask uintptr
Entries [1]TypeAssertCacheEntry
}
type TypeAssertCacheEntry struct {
// type of source value (a *runtime._type)
Typ uintptr
// itab to use for result (a *runtime.itab)
// nil if CanFail is set and conversion would fail.
Itab uintptr
}

type InterfaceType struct {
Type
PkgPath Name // import path
Methods []Imethod // sorted by hash
}

type _type = abi.Type // 就是上面的 Type 类型

// typeAssert builds an itab for the concrete type t and the
// interface type s.Inter. If the conversion is not possible it
// panics if s.CanFail is false and returns nil if s.CanFail is true.
func typeAssert(s *abi.TypeAssert, t *_type) *itab {
var tab *itab
if t == nil {
if !s.CanFail {
panic(&TypeAssertionError{nil, nil, &s.Inter.Type, ""})
}
} else {
tab = getitab(s.Inter, t, s.CanFail)
}

if !abi.UseInterfaceSwitchCache(GOARCH) {
return tab
}

// Maybe update the cache, so the next time the generated code
// doesn't need to call into the runtime.
if cheaprand()&1023 != 0 {
// Only bother updating the cache ~1 in 1000 times.
return tab
}
// Load the current cache.
oldC := (*abi.TypeAssertCache)(atomic.Loadp(unsafe.Pointer(&s.Cache)))

if cheaprand()&uint32(oldC.Mask) != 0 {
// As cache gets larger, choose to update it less often
// so we can amortize the cost of building a new cache.
return tab
}

// Make a new cache.
newC := buildTypeAssertCache(oldC, t, tab)

// Update cache. Use compare-and-swap so if multiple threads
// are fighting to update the cache, at least one of their
// updates will stick.
atomic_casPointer((*unsafe.Pointer)(unsafe.Pointer(&s.Cache)), unsafe.Pointer(oldC), unsafe.Pointer(newC))

return tab
}

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.Methods) == 0 {
throw("internal error - misuse of itab")
}

// easy case
if typ.TFlag&abi.TFlagUncommon == 0 {
if canfail {
return nil
}
name := toRType(&inter.Type).nameOff(inter.Methods[0].Name)
panic(&TypeAssertionError{nil, typ, &inter.Type, name.Name()})
}

var m *itab

// First, look in the existing table to see if we can find the itab we need.
// This is by far the most common case, so do it without locks.
// Use atomic to ensure we see any previous writes done by the thread
// that updates the itabTable field (with atomic.Storep in itabAdd).
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
}

// Not found. Grab the lock and try again.
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}

// Entry doesn't exist yet. Make a new entry & add it.
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.Methods)-1)*goarch.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
// The hash is used in type switches. However, compiler statically generates itab's
// for all interface/type pairs used in switches (which are added to itabTable
// in itabsinit). The dynamically-generated itab's never participate in type switches,
// and thus the hash is irrelevant.
// Note: m.hash is _not_ the hash used for the runtime itabTable hash table.
m.hash = 0
m.init()
itabAdd(m)
unlock(&itabLock)
finish:
if m.fun[0] != 0 {
return m
}
if canfail {
return nil
}
// this can only happen if the conversion
// was already done once using the , ok form
// and we have a cached negative result.
// The cached result doesn't record which
// interface function was missing, so initialize
// the itab again to get the missing function name.
panic(&TypeAssertionError{concrete: typ, asserted: &inter.Type, missingMethod: m.init()})
}

// init fills in the m.fun array with all the code pointers for
// the m.inter/m._type pair. If the type does not implement the interface,
// it sets m.fun[0] to 0 and returns the name of an interface function that is missing.
// It is ok to call this multiple times on the same m, even concurrently.
func (m *itab) init() string {
inter := m.inter
typ := m._type
x := typ.Uncommon()

// both inter and typ have method sorted by name,
// and interface names are unique,
// so can iterate over both in lock step;
// the loop is O(ni+nt) not O(ni*nt).
ni := len(inter.Methods)
nt := int(x.Mcount)
xmhdr := (*[1 << 16]abi.Method)(add(unsafe.Pointer(x), uintptr(x.Moff)))[:nt:nt]
j := 0
methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
var fun0 unsafe.Pointer
imethods:
for k := 0; k < ni; k++ {
i := &inter.Methods[k]
itype := toRType(&inter.Type).typeOff(i.Typ)
name := toRType(&inter.Type).nameOff(i.Name)
iname := name.Name()
ipkg := pkgPath(name)
if ipkg == "" {
ipkg = inter.PkgPath.Name()
}
for ; j < nt; j++ {
t := &xmhdr[j]
rtyp := toRType(typ)
tname := rtyp.nameOff(t.Name)
if rtyp.typeOff(t.Mtyp) == itype && tname.Name() == iname {
pkgPath := pkgPath(tname)
if pkgPath == "" {
pkgPath = rtyp.nameOff(x.PkgPath).Name()
}
if tname.IsExported() || pkgPath == ipkg {
ifn := rtyp.textOff(t.Ifn)
if k == 0 {
fun0 = ifn // we'll set m.fun[0] at the end
} else {
methods[k] = ifn
}
continue imethods
}
}
}
// didn't find method
m.fun[0] = 0
return iname
}
m.fun[0] = uintptr(fun0)
return ""
}

// interfaceSwitch compares t against the list of cases in s.
// If t matches case i, interfaceSwitch returns the case index i and
// an itab for the pair <t, s.Cases[i]>.
// If there is no match, return N,nil, where N is the number
// of cases.
func interfaceSwitch(s *abi.InterfaceSwitch, t *_type) (int, *itab) {
cases := unsafe.Slice(&s.Cases[0], s.NCases)

// Results if we don't find a match.
case_ := len(cases)
var tab *itab

// Look through each case in order.
for i, c := range cases {
tab = getitab(c, t, true)
if tab != nil {
case_ = i
break
}
}

if !abi.UseInterfaceSwitchCache(GOARCH) {
return case_, tab
}

// Maybe update the cache, so the next time the generated code
// doesn't need to call into the runtime.
if cheaprand()&1023 != 0 {
// Only bother updating the cache ~1 in 1000 times.
// This ensures we don't waste memory on switches, or
// switch arguments, that only happen a few times.
return case_, tab
}
// Load the current cache.
oldC := (*abi.InterfaceSwitchCache)(atomic.Loadp(unsafe.Pointer(&s.Cache)))

if cheaprand()&uint32(oldC.Mask) != 0 {
// As cache gets larger, choose to update it less often
// so we can amortize the cost of building a new cache
// (that cost is linear in oldc.Mask).
return case_, tab
}

// Make a new cache.
newC := buildInterfaceSwitchCache(oldC, t, case_, tab)

// Update cache. Use compare-and-swap so if multiple threads
// are fighting to update the cache, at least one of their
// updates will stick.
atomic_casPointer((*unsafe.Pointer)(unsafe.Pointer(&s.Cache)), unsafe.Pointer(oldC), unsafe.Pointer(newC))

return case_, tab
}

源代码虽然有点长,但是稍微耐心点是很容看懂的。

最关键的是要理解 func getitab(inter *interfacetype, typ *_type, canfail bool) *itab 这个函数在做什么。

它尝试从 interface type 和 struct type 里构造出一个 itab,如果能构造成功,那么就意味着这个 struct 实现了该 interface。

而且你可以看到,它还用了缓存机制,第一次构造成功之后就会缓存起来,后续再进行这样的构造时就直接从缓存里拿了,空间换时间,提高性能。

最终核心的函数是func (m *itab) init() string。它会尝试从可执行文件里寻找所有 interface/struct pair(主要是 rodata 和 text 段的数据),也就是看这个 struct 是否都实现了 interface 所规定的所有函数。

type switch 呢?一样的,最终都会落到 func getitab(inter *interfacetype, typ *_type, canfail bool) *itab 这个函数上。

自此,我们基本上理解了 interface 在 Go 中是如何实现的了,当然其中还有很多细节,但是对于我们理解整体的实现原理并无影响。

总结一下

  • 查看汇编代码可以很直观地理解 go 语言运行的过程。
  • 程序的底层是以 iface/eface/interfacetype/_type 等各种 abi 数据结构为“介质”来进行交互的。如果说调用一个函数的参数是某种 interface 类型,那么从汇编程序中就可以看出,程序现在堆栈上构造出 iface/eface,然后再调用函数,之后就可以实现 interface 的各种功能了。
  • go 会为每一种 type x interface 记录一条 itab,这个非常重要。比如说有三种 interface,而且分别有三个具体类型都实现这三个接口,那么汇编代码里就有 9 个 itab 的 rodata。有了这些,就可以实现 interface 那些神奇的功能了。
  • 很多 interface 最终都是调用了 go 写的函数而不是汇编级代码,比如说类型 switch 就对应了 runtime.interfaceSwitch,类型推断对应了 runtime.typeAssert,等等。
  • 编译器和 runtime 之间需要有一个规约(abi),才能相互无缝地相互配合,让程序运行起来。
  • 真正理解了 itab iface eface 等数据结构,那么 interface 的各种特性的实现则是不言自明的。

反射的实现

聊完 interface 的实现后再来聊 reflection 就顺理成章了。

具体实现是怎么样的呢?

interface 实现源码中的  eface 和  iface  会和反射实现源码中的 emptyInterface 和 nonEmptyInterface 是一样的数据结构,它们保持同步。

反射中提供了两个核心的数据结构,Type 和 Value,在此基础上实现了各种方法,这两个都叫做反射对象。

Type 和 Value 提供了非常多的方法:例如获取对象的属性列表、获取和修改某个属性的值、对象所属结构体的名字、对象的底层类型(underlying type)等等。

Go 中的反射,在使用中最核心的就两个函数:

  • reflect.TypeOf(x)
  • reflect.ValueOf(x)

这两个函数可以分别将给定的数据对象转化为以上的 Type 和 Value,这两个都叫做反射对象。

这两个函数的实现源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}

// toType converts from a *rtype to a Type that can be returned
// to the client of package reflect. In gc, the only concern is that
// a nil *rtype must be replaced by a nil Type, but in gccgo this
// function takes care of ensuring that multiple *rtype for the same
// type are coalesced into a single Type.
func toType(t *abi.Type) Type {
if t == nil {
return nil
}
return toRType(t)
}

func toRType(t *abi.Type) *rtype {
return (*rtype)(unsafe.Pointer(t))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i any) Value {
if i == nil {
return Value{}
}
return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i any) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// NOTE: don't read e.word until we know whether it is really a pointer or not.
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}

实际上也是一个 pack 和 unpack 出 emptyInterface/nonEmptyInterface 的过程。


在 Go 中,反射是在类型系统的基础上包装了一套更高级的类型系统的用法。上面说那些类型推断、类型转换只不过是这套类型系统的一个应用而已,而且这个应用直接集成到了代码语法上。

反射无非就是把 itab 这样的静态数据的构造从编译阶段放到了运行阶段。或者从另外一个角度来说,就是把静态类型检查从编译阶段放到了运行阶段。

Go 中反射机制的本质是,Go 会把函数和类型的元数据(尤其是 itab,比如:go.itab.*“”.MyStruct,””.MyInterface SRODATA dupok size=48)存储在 rodata 里,在运行时,通过读取这些元数据,来动态构造出 iface,然后在此基础上进行一些数据修改或函数的调用。

反射三定律

根据 Go 官方关于反射的博客,反射有三大定律:

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

关于第三条,记住一句话:如果想要操作原变量,反射变量 Value 必须要 hold 住原变量的地址才行。

反射的优劣

使用反射的优势:

  • 程序抽象性提升
  • 程序表达力提升

适用反射的坏处:

  • 程序维护性降低
  • 程序性能降低
  • 程序安全性降低

参考资料

最近的一次技术分享

最近我搞明白了一个困扰我们很久的内存回收的问题,欣喜之际,我打算在组内分享一下我对这个问题的最新理解,并借此机会分享一下内核内存管理的基本知识。

于是我花了半天时间做了个非常粗略的 PPT,然后在组内做了这次分享,但分享的效果可以说是很差。

下来后我反思了一下,原因大概有这么几点:

一、企图在短时间内分享很多内容。

这次分享,我准备了物理内存管理、Cgroup v1 的原理、内存统计、内存回收等内容。要在短短的一个小时里把这些东西都讲清楚基本上是不太可能的。另外,内容越多,涉及到的细节也就越多,分享也就越容易失控。

二、没有考虑到所讲内容和听众的接受程度。

组内的同学对内核内存管理方面基本上没有什么了解,大部分人甚至都没有听说过“slab”。在这个前提下,如果还要强行分享内核内存管理的相关概念以及甚至是一些实现细节,那肯定如同听天书。

果然,分享结束后,组里的同学打趣道:“刚开始尝试理解你说的东西,但是很快就跟不上了,就放弃挣扎了。”

在不同场合、不同受众,分享的内容应该是不一样的,这样的内容放到某个内核爱好者会议上去讲,那我觉得应该就不错。

三、自己准备得不够充分。

理查德·费曼说过:“if you can’t explain a theory in a simple way understandable to kids, then you didn’t understand it well.”

也就是说,如果你不能把一个理论讲清楚,那么就说明你还不够理解。

一方面,我自己对我要分享的内容并不够充分的理解,另外一方面,演讲的内容也准备地不够充分,PPT 也都是当天制作的,也没有打好腹稿。

准备不够充分,也就导致分享非常不顺畅,频繁出现卡壳。

改进措施

以后再做类似的技术分享,应该按照如下的方式来做准备:

一、如果是内容很多的分享,尽量写下来,通过文档的方式来分享,而不是通过用嘴巴说的方式来分享。道理很简单,一个复杂的问题是很难通过嘴巴来描述清楚的。

二、每次做技术分享,应该只分享一个相对较小的主题。可以是最近遇到的一个问题、可以是某个工具的使用、可以是最近看到的……如果能把这个相对较小主题给讲清楚,那这次技术分享就算成功了。不求大而全,求精而细。

三、内容应该契合听众的接受度。

四、可以围绕要分享的主题准备一些故事或者段子,在讲的时候穿插着抛出这些故事和段子,让枯燥的技术分享多一点趣味。这是我从一个前辈那里学到的,有奇效。

最后,当然是多锻炼演讲的能力。我们工作性质的原因,大部分时间都是一个人对着电脑,和他人交流也都是在线聊天或者写文档,很少有面向多人做严肃演讲的机会,所以这方面的能力很自然就是欠缺的。没有办法,只能多创造演讲机会,多锻炼。

最近在研究 Linux 内核是如何被构建出来的。

什么是 kbuild system?

总所周知,Linux 内核可以说是世界最成功的操作系统了,它已经运行在全世界几乎每一个角落。

Linux 内核在很早之前就有上千万行代码,而且贡献者也达到了数千人,如何让这样体量的项目可以顺利地维护下去,是一项不小的挑战。为了解决这个挑战,Linux 内核的开发者们专门开发了一套构建系统,也就他们称之为的 Linux kernel build system,简称 kbuild。这个 kbuild 其实就是各种各样的 makefile,在这些 makefile 的共同工作下完成内核的各种构建工作。

这个话题我们再说远一点。

对于一个大型操作系统项目来说,好的构建系统应该是怎么样的?

  • 它应该允许用户很灵活地配置哪些代码被编译进内核里;
  • 它应该允许内核开发者很方便地加入代码而不需要大幅度地调整构建系统;
  • 它应该能够最大程度地判断一个构建 target 是否应该被重新构建;

据我的一番探索,我发现 kbuild 其实就实现了上述所有这些点。

Linux kernel 正是有了 kbuild 这样强大的构建系统,它才能很灵活地调整哪些代码应该被编译进内核中,才能编译出适合各种硬件类型的内核,才能让 Linux 占领各种各样的硬件市场。

而且由于 Linux 内核的各个模块之间分割得比较好,所以各种开发者也可以方便地进行开发,这也是 Linux 内核如此成功的原因之一。


编译内核是一件非常简单的事情,如果你还没有试过编译运行 Linux Kernel,那么请参考我的另外一篇文章。

编译 Linux 内核一般要经过两个步骤:

  • 第一步就是生成构建配置:make *config
  • 第二步就是构建内核:make

我们就从这两个步骤中剖析一下 kbuild 是如何工作的。

(以下代码都来来自于 linux-4.14.336)

执行 make menuconfig 会发生什么?

执行这个命令之后,它会在命令行中出现一个配置界面,通过这个配置界面,用户可以勾选各种配置参数,然后点击保存,它就会生成.config这个文件,这个文件就是编译整个内核的配置文件。

执行make menuconfig 会触发根目录下的Makefile里的%config 这个 target:

1
2
%config: scripts_basic outputmakefile FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@

它依赖于 scripts_basic 和 outputmakefile 和 FORCE 这三个依赖。

scripts_basic

1
2
3
4
5
# Basic helpers built in scripts/basic/
PHONY += scripts_basic
scripts_basic:
$(Q)$(MAKE) $(build)=scripts/basic
$(Q)rm -f .tmp_quiet_recordmcount

先说结论,scripts_basic 这个 target 其实就是编译 scripts/basic/ 这个目录下的代码,编译出叫做 fixdepbin2c的可执行文件,这两个小程序在后面的构建工作中会被用到。

outputmakefile

1
2
3
4
5
6
7
8
9
10
PHONY += outputmakefile
# outputmakefile generates a Makefile in the output directory, if using a
# separate output directory. This allows convenient use of make in the
# output directory.
outputmakefile:
ifneq ($(KBUILD_SRC),)
$(Q)ln -fsn $(srctree) source
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile \
$(srctree) $(objtree) $(VERSION) $(PATCHLEVEL)
endif

通过注释我们可以大致了解这个 target 是做什么的,它其实就是在编译产物的目录里写入 makefile,允许用户在编译产物的目录里可以很方便地使用 make。

这个不是我们分析的关键,我们可以先不用关注。

构建并运行 mconf

我们重点来分析下面这关键的一行做了什么:

1
$(Q)$(MAKE) $(build)=scripts/kconfig $@

$(build) 这个变量是定义在另外一个叫做 scripts/Kbuild.include 的 makefile 里的:

1
2
3
4
5
###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

所以上述那关键的一行展开就是:

1
make -f ./script/Makefile.build obj=script/kconfig menuconfig

这一行命令就相当于在 makefile 里面再次调用 make,相当于执行 ./script/Makefile.build,传入的参数是 obj=script/kconfig,触发的 target 为 menuconfig

组件式构建,称为递归 make,是 GNU make  管理大型项目的常用方法。

kbuild 是递归 make 的一个很好的例子。通过将源文件划分为不同的模块/组件,每个组件都由其自己的 makefile 管理。当你开始构建时,顶级 makefile 以正确的顺序调用每个组件的 makefile、构建组件,并将它们收集到最终的执行程序中。

接下来,我们就到 ./script/Makefile.build 中一探究竟。

我们发现,这里面并没有一个叫做 menuconfig 的 target 啊?为什么它还可以执行下去?

其实它使用了 include 指令来导入 script/kconfig/ 目录下的 makefile,这个里面就包含了 menuconfig 这个 target。

这就是为什么它要传入一个 obj 参数的原因,这个 obj 就是告诉 ./script/Makefile.build 应该 include 哪个 makefile。

具体的 include 的代码在这里:

1
2
3
4
# The filename Kbuild has precedence over Makefile
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)

于是它就会触发script/kconfig/Makefile里的 menuconfig 的 target:

1
2
menuconfig: $(obj)/mconf
$< $(silent) $(Kconfig)

这个 target 只做了两件事情:第一,构建出 $(obj)/mconf 这个二进制文件;第二, $< $(silent) $(Kconfig),执行 mconf 这个二进制文件,参数为 $(Kconfig)

这里的 mconf 二进制文件就是打开配置界面的那个程序,就是文章上面第一个图。

它主要负责收集配置,然后提供一个配置界面,接收用户的交互,最终产生 .config文件。

我们继续说说这个 mconf 的工作细节,这有助于我们理解 kbuild 的工作方式。开始之前,我们有几个疑惑:

  • mconf 里的这些配置项是哪里来的?
  • 当我保存之后,生成的 .config 文件里有什么?
  • .config 文件会如何被 kbuild 使用?

第一个问题,这些配置项是定义在各处的 Kconfig 文件里的,用的是一种 kbuild 专属的配置语言。

mconf 的任务就是读取根目录的 Kconfig 文件,而这个 Kconfig 文件自己也会把各处的 kconfig 文件给 include 进来。

然后 mconf 还会读取各处已经存在的配置信息,比如说之前执行过配置产生的 .config 文件,等等。

然后就是渲染出图形界面,接受用户的交互,最后保存,产生一个新的 .config 文件。

mconf 的工作细节

所有相关的代码都在 scripts/kconfig/ 路径下。

执行 make 会发生什么?

上面我们说了,构建内核的最后一步就是直接运行 make,它会构建出我们想要的内核。

我们知道,当我们运行 make 的时候,它会默认触发第一个出现的 target 或者名字叫做 all 的 target,如下:

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
# That's our default target when none is given on the command line
PHONY := _all
_all:

# ===========================================================================
# Build targets only - this includes vmlinux, arch specific targets, clean
# targets and others. In general all targets except *config targets.

# If building an external module we do not care about the all: rule
# but instead _all depend on modules
PHONY += all
ifeq ($(KBUILD_EXTMOD),)
_all: all
else
_all: modules
endif

# The all: target is the default when no target is given on the
# command line.
# This allow a user to issue only 'make' to build a kernel including modules
# Defaults to vmlinux, but the arch makefile usually adds further targets
all: vmlinux

# Build modules
#
# A module can be listed more than once in obj-m resulting in
# duplicate lines in modules.order files. Those are removed
# using awk while concatenating to the final file.

PHONY += modules
modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin
$(Q)$(AWK) '!x[$$0]++' $(vmlinux-dirs:%=$(objtree)/%/modules.order) > $(objtree)/modules.order
@$(kecho) ' Building modules, stage 2.';
$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost

所以,它会触发一个叫_all 的 target,它会根据参数 $(KBUILD_EXTMOD) 分为两种情况,一种是 all,一种是 modules,我们现在先把注意力放到 all: vmlinux 上,因为这里的 vmlinux 就是我们编译内核的 target。

接下来,我把 vmlinux 这个 target 相关的内容都摘录过来,我们一起看看都包含了哪些内容:

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
vmlinux: scripts/link-vmlinux.sh vmlinux_prereq $(vmlinux-deps) FORCE
+$(call if_changed,link-vmlinux)

PHONY += vmlinux_prereq
vmlinux_prereq: $(vmlinux-deps) FORCE
ifdef CONFIG_HEADERS_CHECK
$(Q)$(MAKE) -f $(srctree)/Makefile headers_check
endif

vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) $(KBUILD_VMLINUX_LIBS)

# The actual objects are generated when descending,
# make sure no implicit rule kicks in
$(sort $(vmlinux-deps)): $(vmlinux-dirs) ;

PHONY += $(vmlinux-dirs)
$(vmlinux-dirs): prepare scripts
$(Q)$(MAKE) $(build)=$@

vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
$(core-y) $(core-m) $(drivers-y) $(drivers-m) \
$(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y)))

export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y)
export KBUILD_VMLINUX_LIBS := $(libs-y1)
export KBUILD_LDS := arch/$(SRCARCH)/kernel/vmlinux.lds

vmlinux依赖于scripts/link-vmlinux.shvmlinux_prereq$(vmlinux-deps),我们重点来关注 $(vmlinux-deps) 的构建。

我们可以看到,vmlinux-deps 实际上有四大依赖,分别是:

  • $(KBUILD_LDS)
  • $(KBUILD_VMLINUX_INIT)
  • $(KBUILD_VMLINUX_MAIN)
  • $(KBUILD_VMLINUX_LIBS)

这里先说结论,假设我们的内核的目标是运行在 x86 架构上,那么经过一系列的变量赋值和函数操作后:

$(KBUILD_LDS) 的值为 arch/$(SRCARCH)/kernel/vmlinux.lds,这里的$(SRCARCH) 就是我们在构建配置里指定的 CPU 架构,这里就是 x86

$(KBUILD_VMLINUX_INIT) 的值展开为:

1
2
arch/x86/kernel/head_64.o arch/x86/kernel/head64.o arch/x86/kernel/ebda.o arch/x86/kernel/platform-quirks.o
init/built-in.o

$(KBUILD_VMLINUX_MAIN) 的值展开为:

1
2
3
usr/built-in.o arch/x86/built-in.o kernel/built-in.o certs/built-in.o mm/built-in.o
fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o lib/built-in.o arch/x86/lib/built-in.o drivers/built-in.o sound/built-in.o
firmware/built-in.o arch/x86/pci/built-in.o arch/x86/power/built-in.o arch/x86/video/built-in.o net/built-in.o virt/built-in.o

$(KBUILD_VMLINUX_LIBS) 的值为:

1
lib/lib.a arch/x86/lib/lib.a

vmlinux-dirs的值为:

1
init usr arch/x86 kernel certs mm fs ipc security crypto block drivers sound firmware arch/x86/pci arch/x86/power arch/x86/video net lib arch/x86/lib virt

vmlinux-dirs实际上就是要参与构建内核的代码的目录。

vmlinux-deps 的值为:

1
2
3
4
arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_64.o arch/x86/kernel/head64.o arch/x86/kernel/ebda.o arch/x86/kernel/platform-quirks.o
init/built-in.o usr/built-in.o arch/x86/built-in.o kernel/built-in.o certs/built-in.o mm/built-in.o
fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.olib/built-in.o arch/x86/lib/built-in.o drivers/built-in.o sound/built-in.o
firmware/built-in.o arch/x86/pci/built-in.o arch/x86/power/built-in.o arch/x86/video/built-in.o net/built-in.o virt/built-in.o lib/lib.a arch/x86/lib/lib.a

实际上,每一个参与构建的目录下面都会最终产生一个叫做 built-in.o 的产物,kbuild 会通过链接脚本 arch/x86/kernel/vmlinux.lds 把这些 .olib.a 链接在一起,最终产生 vmlinux

对于上述内容,我画了一个图,来帮助读者更好地理解:

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
37
38
39
40
41
                        +-----------------------+
+--------------| $(KBUILD_LD) |<-------------------- arch/$(SRCARCH)/kernel/vmlinux.lds ---------
| +-----------------------+ \
| \
| \
| arch/x86/kernel/head_64.o |
| +-----------------------+<----- $(head-y)<---- arch/x86/kernel/head64.o |
| | | arch/x86/kernel/ebda.o |
| +--------+$(KBUILD_VMLINUX_INIT) | arch/x86/kernel/platform-quirks.o |
| | | | |
v v +-----------------------+<----- $(init-y)<---- init/built-in.o |
+------------------+ |
| | |
| .--. | |
| |o_o | | usr/built-in.o |
| |:_/ | | arch/x86/built-in.o |
| // \ \ | kernel/built-in.o |
| (| | ) | certs/built-in.o |
| /'\_ _/`\ | mm/built-in.o
| \___)=(___/ | +------------------------+<----- $(core-y)<---- fs/built-in.o $(vmlinux-deps)
| | | | ipc/built-in.o
| vmlinux | | | security/built-in.o |
| | | | crypto/built-in.o |
| 4.14.336 | | | block/built-in.o |
+--------+---------+ | | |
| ^ | | arch/x86/lib/built-in.o |
| | | |<--- $(libs-y2) <---- lib/built-in.o |
| +-------+ $(KBUILD_VMLINUX_MAIN) | |
| | | drivers/built-in.o arch/x86/pci/built-in.o |
| | |<- $(drivers-y) <---- sound/built-in.o arch/x86/power/built-in.o |
| | | firmware/built-in.o arch/x86/video/built-in.o |
| | | |
| | |<----- $(net-y) <---- net/built-in.o |
| | | |
| +------------------------+<---- $(virt-y) <---- virt/built-in.o |
| |
| /
| /
| +------------------------+ lib/lib.a /
+-------------+ $(KBUILD_VMLINUX_LIBS) |<--- $(libs-y1) <---- arch/x86/lib/ib.a --------------------------
+------------------------+

接下来,我们只要知道这些 build-in.o 是怎么被构建出来的,那么我们就知道内核是如何被构建出来的了。

构建 built-in.o

下面我们来看看这些 built-in.o 是如何被构建出来的,主要看这部分的代码:

1
2
3
PHONY += $(vmlinux-dirs)
$(vmlinux-dirs): prepare scripts
$(Q)$(MAKE) $(build)=$@

这里就要谈到 make 的一个特性了,像 $(vmlinux-dirs) 这样的变量其实是多个目录用空格连起来的字符串,那么 make 在执行这个目标的时候,它会循环把每一个目录给取出来,然后执行这个目标。

说白了,就是每个参与构建的目录都会触发这个 target 每一次都会执行命令 $(Q)$(MAKE) $(build)=$@

init 为例,那么这个命令展开就是:

1
make -f ./script/Makefile.build obj=init

这个形式的命令我们很熟悉了,就是去执行 ./script/Makefile.build,参数为 obj=init

我们来看看这个 ./script/Makefile.build,因为我们的命令没有指定任何 target,所以就触发默认的 target,也就是这个 __build

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Init all relevant variables used in kbuild files so
# 1) they have correct type
# 2) they do not inherit any value from the environment
obj-y :=
obj-m :=
lib-y :=
lib-m :=
always :=
targets :=
subdir-y :=
subdir-m :=
EXTRA_AFLAGS :=
EXTRA_CFLAGS :=
EXTRA_CPPFLAGS :=
EXTRA_LDFLAGS :=
asflags-y :=
ccflags-y :=
cppflags-y :=
ldflags-y :=

__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
$(subdir-ym) $(always)
@:

当然,在这之前,已经把init/Makefile给 include 进来了,这个init/Makefile 里会给 obj-yobj-mlib-ylib-malwaysextra-y 这些变量赋予具体的值。

obj-y 的值都是 xxx.o yyy.o zzz.o 这样的形式,于是乎,这个__buildtarget 就会执行这些依赖:

1
2
3
4
5
6
7
$(builtin-target)
$(lib-target)
$(extra-y)
$(obj-m)
$(modorder-target)
$(subdir-ym)
$(always)

我们只讨论一个$(builtin-target),其他的都差不多,读者可以自行推导。

$(builtin-target) 可以展开为:

1
2
3
4
5
6
ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
builtin-target := $(obj)/built-in.o
endif

$(builtin-target): $(obj-y) FORCE
$(call if_changed,link_o_target)

在这里它就等于 init/built-in.o,要生成它,则需要依赖于 $(obj-y),而我们上面说了,$(obj-y) 实际上就是 init 目录下的 Makefile 里赋值好的:

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
37
38
39
40
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the linux kernel.
#

ccflags-y := -fno-function-sections -fno-data-sections

obj-y := main.o version.o mounts.o
ifneq ($(CONFIG_BLK_DEV_INITRD),y)
obj-y += noinitramfs.o
else
obj-$(CONFIG_BLK_DEV_INITRD) += initramfs.o
endif
obj-$(CONFIG_GENERIC_CALIBRATE_DELAY) += calibrate.o

ifneq ($(CONFIG_ARCH_INIT_TASK),y)
obj-y += init_task.o
endif

mounts-y := do_mounts.o
mounts-$(CONFIG_BLK_DEV_RAM) += do_mounts_rd.o
mounts-$(CONFIG_BLK_DEV_INITRD) += do_mounts_initrd.o
mounts-$(CONFIG_BLK_DEV_MD) += do_mounts_md.o

# dependencies on generated files need to be listed explicitly
$(obj)/version.o: include/generated/compile.h

# compile.h changes depending on hostname, generation number, etc,
# so we regenerate it always.
# mkcompile_h will make sure to only update the
# actual file if its content has changed.

chk_compile.h = :
quiet_chk_compile.h = echo ' CHK $@'
silent_chk_compile.h = :
include/generated/compile.h: FORCE
@$($(quiet)chk_compile.h)
$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkcompile_h $@ \
"$(UTS_MACHINE)" "$(CONFIG_SMP)" "$(CONFIG_PREEMPT)" "$(CC) $(KBUILD_CFLAGS)"

如果你在 .config 里配置好了 CONFIG_ARCH_INIT_TASK=y,那么在这里的 obj-y 变量里就包含 init_task.o 这个构建目标,而且这个目标是要被构建进内核的。

这样,你就知道我们在第一个步骤里创建的 .config 是如何影响到这些代码是否被编译进内核的。

接下来就是看 obj-y 这一堆的 .o 文件是如何被构建出来的了。

当需要构建任何一个 .o 文件的时候,make 发现找不到目标,那么 make 就会去通配符匹配,通过下面的代码,可以看到它匹配到了 $(obj)/%.o 这个 target,这个就是构建出这些 .o 文件的地方。

它所做的事情非常简单,就是去寻找名字一样的 .c.S 文件作为源文件,然后构建出 .o文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Usage: $(call if_changed_rule,foo)
# Will check if $(cmd_foo) or any of the prerequisites changed,
# and if so will execute $(rule_foo).
if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ), \
@set -e; \
$(rule_$(1)), @:)

define rule_cc_o_c
$(call echo-cmd,checksrc) $(cmd_checksrc) \
$(call cmd_and_fixdep,cc_o_c) \
$(call echo-cmd,objtool) $(cmd_objtool) \
$(cmd_modversions_c) \
$(call echo-cmd,record_mcount) $(cmd_record_mcount)
endef

# Built-in and composite module parts
$(obj)/%.o: $(src)/%.c $(recordmcount_source) $(objtool_dep) FORCE
$(call cmd,force_checksrc)
$(call if_changed_rule,cc_o_c)

$(obj)/%.o: $(src)/%.S $(objtool_dep) FORCE
$(call if_changed_rule,as_o_S)

最终,kbuild 会把同一个目录下产生的一堆 .o 文件合并为一个 built-in.o 文件:

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
#
# Rule to compile a set of .o files into one .o file
#
ifdef builtin-target

ifdef CONFIG_THIN_ARCHIVES
cmd_make_builtin = rm -f $@; $(AR) rcSTP$(KBUILD_ARFLAGS)
cmd_make_empty_builtin = rm -f $@; $(AR) rcSTP$(KBUILD_ARFLAGS)
quiet_cmd_link_o_target = AR $@
else
cmd_make_builtin = $(LD) $(ld_flags) -r -o
cmd_make_empty_builtin = rm -f $@; $(AR) rcs$(KBUILD_ARFLAGS)
quiet_cmd_link_o_target = LD $@
endif

# If the list of objects to link is empty, just create an empty built-in.o
cmd_link_o_target = $(if $(strip $(obj-y)),\
$(cmd_make_builtin) $@ $(filter $(obj-y), $^) \
$(cmd_secanalysis),\
$(cmd_make_empty_builtin) $@)

$(builtin-target): $(obj-y) FORCE
$(call if_changed,link_o_target)

targets += $(builtin-target)
endif # builtin-target

链接内核

当我们走到这一步时,我们已经有了一大堆的 built-in.olib.a 文件,接下来就是把这些文件链接到一起,生成最终的产物 vmlinux

这一行就是在链接内核:+$(call if_changed,link-vmlinux)

从下面这些代码可以看出,它最终会调用函数cmd_link-vmlinux,它会做两件事情:

  • 链接内核
  • 如果ARCH_POSTLINK存在,那么再走 arch pass

cmd_link-vmlinux 函数中的 $< 实际上就是 scripts/link-vmlinux.sh$@ 实际上就是 vmlinux

所以第一行展开就是执行这个脚本,参数是后面三个,就是 /bin/bash scripts/link-vmlinux.sh ld$(LDFLAGS)$(LDFLAGS_vmlinux) 都为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
vmlinux: scripts/link-vmlinux.sh vmlinux_prereq $(vmlinux-deps) FORCE
+$(call if_changed,link-vmlinux)

# Final link of vmlinux with optional arch pass after final link
cmd_link-vmlinux = \
$(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux) ; \
$(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

# Execute command if command has changed or prerequisite(s) are updated.
if_changed = $(if $(strip $(any-prereq) $(arg-check)), \
@set -e; \
$(echo-cmd) $(cmd_$(1)); \
printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)

接下来就是看看 scripts/link-vmlinux.sh 这个文件里是啥逻辑,因为这个 shell 脚本是由 make 所启动的,所以在这个 shell 脚本里是可以读取 makefile 里定义的各种变量的,比如说这几个:$(KBUILD_LDS)$(KBUILD_VMLINUX_INIT)$(KBUILD_VMLINUX_MAIN)$(KBUILD_VMLINUX_LIBS)

我尝试摘录一些重要的代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
#
# link vmlinux
#
# vmlinux is linked from the objects selected by $(KBUILD_VMLINUX_INIT) and
# $(KBUILD_VMLINUX_MAIN) and $(KBUILD_VMLINUX_LIBS). Most are built-in.o files
# from top-level directories in the kernel tree, others are specified in
# arch/$(ARCH)/Makefile. Ordering when linking is important, and
# $(KBUILD_VMLINUX_INIT) must be first. $(KBUILD_VMLINUX_LIBS) are archives
# which are linked conditionally (not within --whole-archive), and do not
# require symbol indexes added.
#
# vmlinux
# ^
# |
# +-< $(KBUILD_VMLINUX_INIT)
# | +--< init/version.o + more
# |
# +--< $(KBUILD_VMLINUX_MAIN)
# | +--< drivers/built-in.o mm/built-in.o + more
# |
# +--< $(KBUILD_VMLINUX_LIBS)
# | +--< lib/lib.a + more
# |
# +-< ${kallsymso} (see description in KALLSYMS section)
#
# vmlinux version (uname -v) cannot be updated during normal
# descending-into-subdirs phase since we do not yet know if we need to
# update vmlinux.
# Therefore this step is delayed until just before final link of vmlinux.
#
# System.map is generated to document addresses of all kernel symbols

# Link of vmlinux
# ${1} - optional extra .o files
# ${2} - output file
vmlinux_link()
{
local lds="${objtree}/${KBUILD_LDS}"
local objects

if [ "${SRCARCH}" != "um" ]; then
if [ -n "${CONFIG_THIN_ARCHIVES}" ]; then
objects="--whole-archive \
built-in.o \
--no-whole-archive \
--start-group \
${KBUILD_VMLINUX_LIBS} \
--end-group \
${1}"
else
objects="${KBUILD_VMLINUX_INIT} \
--start-group \
${KBUILD_VMLINUX_MAIN} \
${KBUILD_VMLINUX_LIBS} \
--end-group \
${1}"
fi

${LD} ${LDFLAGS} ${LDFLAGS_vmlinux} -o ${2} \
-T ${lds} ${objects}
else
if [ -n "${CONFIG_THIN_ARCHIVES}" ]; then
objects="-Wl,--whole-archive \
built-in.o \
-Wl,--no-whole-archive \
-Wl,--start-group \
${KBUILD_VMLINUX_LIBS} \
-Wl,--end-group \
${1}"
else
objects="${KBUILD_VMLINUX_INIT} \
-Wl,--start-group \
${KBUILD_VMLINUX_MAIN} \
${KBUILD_VMLINUX_LIBS} \
-Wl,--end-group \
${1}"
fi

${CC} ${CFLAGS_vmlinux} -o ${2} \
-Wl,-T,${lds} \
${objects} \
-lutil -lrt -lpthread
rm -f linux
fi
}

其实从这脚本最上面的注释我们就可以很清楚地知道,$(KBUILD_VMLINUX_INIT)$(KBUILD_VMLINUX_MAIN)$(KBUILD_VMLINUX_LIBS)${kallsymso}这几个会按照顺序被链接进 vmlinux 中。

于是我们就获得了一个 vmlinux,但是注意,这个还不是最终我们可以运行的迹象,因为我们还没有把 bootloader 加进去,而且也没有进行压缩操作,镜像的体积非常大。

依赖追踪

文章最开头说了,一个好的构建系统必定是要高效的,一定是可以最大程度避免重复编译的。

先思考一个问题,什么时候某个 .o 文件需要被重新编译。

  1. 构建这个 .o 的代码文件 .c.h 文件发生变动时;
  2. .config 配置文件发生变动时,也就是说 CONFIG_ 变量发生变化时;
  3. 构建命令发生变动时;

如何理解这三点?

第一点非常好理解,这种情况下肯定要重新编译,而且 make 会自动查看依赖文件的修改时间来判断是否需要重新编译;

第二点也很好理解,如果我突然不想把某个驱动链接进内核中,那么我会修改 .config 文件,那么相关的构建和链接都要重新执行;

第三点,每一次的构建都可能会附带一些参数,比如说 gcc 的编译参数,如果这些参数发生变化了,那么这条构建命令肯定也要重新执行。

后面两点则是由 kbuild 通过一定的机制来实现的。

  • fixdep:The secret behind this is that fixdep will parse the depfile (.d file), then parse all the dependency files inside, search the text for all the CONFIG_ strings, convert them to the corresponding empty header file, and add them to the target’s prerequisites. Every time the configuration changes, the corresponding empty header file will be updated, too, so kbuild can detect that change and rebuild the target that depends on it. Because the command line is also recorded, it is easy to compare the last and current compiling parameters.
  • .cmd 文件和 if_changed 函数:当调用 if_changed 函数的时候,它会判断该命令的参数是否发生变化,如果发生了变化,那么就执行该命令,并且把命令记录在 .cmd 文件中,当下一次再调用这个函数的时候,它就会先读取该文件来判断命令是否发生改变。

具体的工作原理等我过几天再详细聊聊。

从 kbuild 中,我们可以学习到什么?

把整个 kbuild 研究下来之后,我们会学到几点:

  • 用原生的 make 也可以管理现代大型项目,不一定非要用 cmake 这类工具;
  • 递归 make 非常适合用来管理大型项目,每个目录下的 makefile 只需要管该目录下的构建逻辑,通过这种方式可以把构建系统的逻辑分离地很好,开发者也就可以很方便地贡献代码了;
  • 好的构建系统对于一个软件项目的成功来说是必不可少的。

其实 kbuild 也算是一个构建框架,它是可以很方便地移植到其他项目中的,比如说 busybox,它的构建系统基本上就是 kbuild 这一套。

参考文献

观点集

我会持续不断地放一些我本人的观点在这里。

  • 尽量把能自动化的事情都自动化,虽然前期要做一些投入,但是可以源源不断产生收益。

  • 反推法是特别适于做计划。先想象一个未来预期的样子,再反过来推理达到那个目标需要什么,那么你就知道当前应该做什么了。

  • 不断地反问是追寻本质的一个方式,如果有些事情想不明白,那就不断反问。

  • 写作是促进思考的绝佳方式,请保持写作的习惯,最好能享受写作,热爱写作。

  • 人的能力由工具赋予,请永远使用最棒的工具,哪怕要付出一些成本。

  • 快乐首先来自于生理,其次才是来自于心理,先保证生理的情况下再做好心理建设。

  • 低调比炫耀更值得追求,我们要永远克服人天生的虚荣感。这方面可以经常去看看罗翔和芒格。

  • 做内容,拼质量而不是拼数量,两个月出一份好内容比每天都出差内容要好很多,如果想做视频或者写博客,这个逻辑要清楚。

  • 不要一直埋头苦干,而是冷静下来思考,好好想想自己的优势,好好想想自己应该以何种方式实现自己的目标,而绝对不是埋头苦干。

  • 不要看一个人怎么说的,而是应该看一个人怎么做的,因为说很容易,做却很难。

  • 想买一个东西的时候,可以去二手网站看看别人为什么要卖出,有效防止冲动消费。

  • 塞翁失马是个非常有启发性的故事,它告诉我们不要去给一件事情下定论,因为我们很难知道这件事情对我们未来到底会产生什么影响。但行好事,莫问前程。

  • 提升认知对个人的发展很有帮助,提升认知最好的办法就是去和那些高认知的人交朋友,同时自己也要多学习新知识。

  • 不要让工作成为你生活的全部,工作只是达到目的的一个手段,你应该掌握工作而不是工作掌握你。

  • 不要拿自己和别人比较,不要因此而产生优越感和自卑感。

  • 创造力就是把脑海中不同的事物联系起来。——《贪婪的多巴胺》

  • 成功的定义是多元的,健康、快乐、友谊、家庭幸福、追寻智慧……都可以算作成功的,把事业作为成功的唯一标准是偏激的。

  • 得 meme 者得天下。(别人说的)

  • 如果你想做技术创新,深入了解底层系统(如果是软件开发,那就是操作系统、计算机网络、数据库系统、浏览器这些)是至关重要的。

  • 很多所谓的方法论对解决问题毫无帮助,它只是事成之后的一种总结。

  • 学习需要的不是一时的冲劲而是持之以恒。日拱一卒,功不唐捐。所以,学习一定要戒骄戒躁,把心给沉到河底。

  • 如果你开始相信物质(房子、车子)能让你变得更开心的话,你就已经开始被异化了。

  • 得之我幸,失之我命。以六分搏十分,搏到了就是赚到,搏不到那也应该的。这是人生的大智慧。

  • 低端的编程工作一定会被取代的,这是技术发展的必然趋势,或早或晚。

  • 不要沉浸在忙碌的快感中,应该经常停下来看一看想一想。

  • Learn from the best. 永远向那些最优秀的人学习!

  • 学校、公司、职称虽然可能代表了一种荣誉,但是不要用这些东西来标榜自己,而是要用作品、贡献或者做过的事情来标榜自己。这也是识人的基本道理。

  • 很多“方法论”只是事成者事后的总结,以便作为其往后的谈资。这些“方法论”往往看起来有条有理,总能让读者感觉“学到了”、“被点悟到了”,但对解决实际的问题没有丝毫帮助。

  • 成长

    • 阅读经典书籍是自我提升最有效的方法。阅读这些书就是在和大师对话,平时多么难得见到的大师,统统都摆在你面前,把毕生所学传授给你。
    • 只要你不使用国内搜索引擎,你就超越国内大多数人了,同时你习惯于用英语读写,你就超越国内绝大部分人了。
    • 想尽一切办法学好英语,因为学英语这是一个收益比巨高的事情。
    • 做长期主义者,但也要适当地投机,这两者不矛盾。
    • 在我的认知里,有两件珍贵的东西要好好保护,它们稍纵即逝,一个是你的业余时间,另外一个是你对某件事情的热情。
    • 学习一个复杂的东西最需要的不是智力而是耐心。
  • 哲学

    • 智慧是我们一生都应该去追求的东西。
    • 记住每个人终有一天都会死掉,在这个基础上再想想自己的活着的方式。
    • 不要给一个活人下定论,我们每个人都是复杂的,这不是一两句话可以说清楚的。
  • 创业

    • 创业如果连自己要做什么都不清楚,那就不应该创业。
    • 管理公司的第一原则是以人为本,而不是以业务为本,更不是以钱为本。
    • 先想办法让员工快乐,在此基础上才能让客户快乐。做一家传递快乐的企业比做一家赚钱的企业更有追求意义。
    • 公司的氛围非常重要,最好能找一个“首席氛围官”。
    • 开放包容的氛围的前提是平等的对话,不是表面的平等,而是发自内心的平等。
    • 想尽一切办法向最优秀的创业者学习,最好是和他们成为朋友。
    • 在没有做出好产品之前,不要去搞增长,因为根增长不了。
    • 初创公司不需要增长团队,整个团队就是增长团队。
    • 关注和学习知识产权法,避免法律风险。(或者找到懂的人。)
    • 给公司或者产品取一个好名字非常非常非常重要!
    • 做好架构,让各个组织可以自然生长。(前期可以不用考虑那么细。)
    • 把最多的精力放到产品和服务上,这是一个公司的根本。
    • 有一些事情哪怕是大公司也需要很长的时间去做才能做到(那些砸人砸钱短时间做到的事情),比如说培养用户群体,创业公司如果在这样事情上领先同行半年一年的话,那么是非常有竞争优势的。
    • 应该先想能给用户什么样的产品,再反过来思考使用什么技术,而不是先用技术把自己给圈住。
    • 加入一个高增长的创业公司很可能比自己创业好,最后的收获可能还更多,并且冒的风险可能没有那么高。
    • 公司里每个人的坦诚很重要,只有让大家说出了真心话,才能找到真真要解决的问题。
    • 一个优秀的管理者需要有长远的眼光、长远的心态。
  • 编程

    • 操作系统就是一个中断驱动的死循环。(这是别人的观点,我觉得非常精炼。)

如何度过经济危机?

如何度过经济危机?我也不知道。只能说永远不要放弃信念,耐心潜伏,耐心沉淀,等待下一次机会。

辩证的眼光去看待,经济危机不一定全是坏事。正所谓“当潮水退去的时候,就知道谁在裸泳”,经济危机就是一把筛子,会把那些表现平庸只靠泡沫撑着的选手给清退出局,这对于新选手来说并不是什么坏事,因为在下一轮经济周期来临时,竞争局面会友好一些。

不管大环境如何的好与坏,我们都要有一种信念,那就是坚持做正确的且有价值的事情,结果如何不可知,静待花开即可。“尽人事,听天命”,说的就是这个道理。

我对简洁的小理解

说说我对简洁的理解:真正的简洁并不是简单地忽略复杂性,而是在深刻理解每一个部位的作用之后,丢掉那些没有必要的东西,在保证功能完整的前提下,达到最简单的程度。

或许,我们的生活也可以很简洁,只需要丢掉没必要的东西(既指实物也指虚物)就行了。🤔

经营一个 Twitter 要多少人?

经营一个推特需要 7500 名员工吗?我不知道。但是有几个类似的例子可以感受一下:

2012 年,当 Instagram 被 Facebook 收购时有 3 千万用户和 13 名员工。

2014 年,当 WhatsApp 被 Facebook 收购时有 5 亿的月活用户和 50 名员工。

2022 年,Telegram 有 7 亿的月活用户和 500-1000 名员工。

……这样的例子还有很多。

不得不说,至少在软件领域,小团队也可以有非常强悍的战斗力。

如何初步判断一个程序员的水平?

我自己总结的,初步判断一个程序员水平的方法,看这三点:英语水平怎么样?平时看什么网站?技术书籍读得怎么样?

我说说自己的理解:

第一点英语水平。整个计算机技术圈里最优质的东西都是被英语垄断的,英语不行的话那怎么可能把技术玩得好?同样,如果英语好,那很容易就能从中受益。

第二点平时看的网站(技术方面的)。平时看什么网站决定了接收什么样的信息以及沉浸在什么样的圈子里。严重地说甚至影响品味。

第三点技术书籍。系统性学习的必经之路就是阅读技术书籍,尤其是那些传颂的经典。不但要看读了哪些技术书籍,还要看读得有多深入,理解了多少内容。

同理,如果要提高自己作为程序员的水平,从这三方面入手,立竿见影。

Learn from the best

我一直秉持的一个理念:无论在哪个行业都要向行业里最厉害的那批人学习,用英语表达就是 Learn from the best。找到行业里最厉害的人,去看他们的文章和书,去听他们的演讲,去观察他们的日常活动,去模仿他们做事的方式,甚至给他们发邮件,想办法和他们沟通,大胆向他们请教。这样做,可以少走无数本没必要走的弯路。

不要记笔记

以前上学的时候从来没有考虑过“听课效率”这件事,老师们倡导的就是上课要好好记笔记,把老师的板书一字不差地给抄下来。现在回头想想其实不对,边听边写这样的操作会不停打断我们注意力,严重降低了“听课效率”。上课那几十分钟里,最重要的是全神贯注地去听讲,尽力理解老师的讲课思路。毕竟笔记随时都可以补充,但是从头到尾给你讲解一个东西的机会稍纵即逝。

这也可以解释为什么在学生时代,有一些同学不怎么记笔记,但是学得很好,另外一些同学写得一手漂亮的笔记,但是学得一般般。我挺希望当时有人可以给我解释这个逻辑。

这样的逻辑推而广之,也适用于开会、阅读的时候。重要的不是记笔记、标注,而是全神贯注地理解讲者或笔者所言之物。

BTW,很多国外大学在这方面就做得不错,他们会制作好课程笔记直接发给学生,同时建议学生上课不要记笔记。

计算机科学三大件

计算机网络、操作系统和数据库,是我认为的,在个人学习方面,投资收益率最高的三类技术。它们有几个特点,第一,这些技术是计算机(互联网)世界的水电煤,它们被应用在几乎每一个角落,很多上层应用实际上只是这几类技术的组合;第二,这些技术的保质期非常长,能让你一次学习终身受益,几乎没有知识失效的风险。

根据我自己的亲身体验,学习这些技术最好的方式是跟着顶尖大学的公开课来学,操作系统课程我推荐 MIT 的,计算机网络课程我推荐斯坦福的,数据库课程我推荐 CMU 的。不妨找来看看。

活到老学到老

人衰老的一大特征就是不再学习新事物,对任何新事物都失去了兴趣,认定自己已经“学不动了”,认定自己已经“学够了”;衰老的另一大特征是固守己见,爱用一堆陈旧的理论去构筑思想的城墙,却以为自己已经掌握了真理。这两大特征的本质是一样的——自我封闭。

抵抗衰老的方法也很简单:保持开放的心态,学习新东西,接收新观点。

前几天,看到有朋友在说学习新语言可以抵抗衰老,我好好想了想,还真有道理。学习一门新语言的过程,本身就是学习新事物以及开放心态去了解另一种文化的过程。正好符合上面的那个方法。

一个对抗衰老的现实榜样就是巴菲特和芒格。看过他们的书你就知道这两人有多爱学习新知识了。他们目前九十多岁,依旧可以生龙活虎地开股东大会。我相信这种老而不衰的现象和他们保持学习的习惯很有关系。

总之,活到老学到老,永远求知未满。

一些好建议

好好睡觉、好好吃饭:身体是一切的基础,我认为,好好睡觉和好好吃饭是保持个人状态最佳的方式。困了就赶快去睡觉,不要撑着,顺从自然本能。

时不时去散散步爬爬山:大脑和身体都需要放空,当身心都处于最放松的状态时才能有高创造力。尊重自然规律。

想尽一切办法学好英语:这个不用多说了,懂的都能懂,学英语真是一本万利的事情。

接受不完美的人事物:人生在世受自己能掌控的事情少之又少,学会接纳人生中的不如意。尽人事,听天命。

先设计好再动手写代码:思考清楚再动手,事半功倍。还没想清楚就动手写代码是新手爱犯的错误。

花费时间做计划是值得的:这一条的意思是说要想清楚为什么要做一件事情,值不值得做,如何做?

关注事物的底层与顶层:探究事物的本质才能知其所以然并融会贯通,探究逻辑链的最顶端,我们才知道如何运用本质。

不管做什么都多一些耐心:饭要一口一口吃,路要一步一步走,尊重客观规律,切不可急于求成。学习一个复杂的东西需要的是耐心而不是智力,很少有碰到需要高智力的时候。

华罗庚读书法

最近看到大家对于读书的评论,让我想起了华罗庚所推崇的读书法,这是他在自己的一篇文章中提到的。

他认为,要读好一本书,大致需要经历两个过程,第一个过程叫作“把书读厚”,第二个过程叫作“把书读薄”。

“把书读厚”在于读书的时候对每个章节旁征博引添加引用,对每一页做注释写笔记,这样一来,书自然而然就变厚了。

“把书读薄”在于逐渐掌握了书的实质内容,对书里的知识有了更透彻的理解后,自然也就感觉到书变薄了。

我认为读书不必贪多,重要的是充分吸收书里的精华。因为读书不同于吃饭喝水,不是过目就等于收获。如果只是一味堆积读书的量而不思考咀嚼消化,那便是走马观花、浅尝辄止,进而收效甚微。所以,对于那些谈论自己一年看了多少数量的书的行为,我总会下意识地抱以怀疑。我是认为,一年到头来,若能细细品味上三四本好书,那这一年就值得,不必烦恼于未完成那长长的书单。

最后我还是想感慨,有些人(当然这里是指华罗庚了)之所以能成为大家,并不是没有道理的,从读书方法这样的细小之处就能窥见一二。

社会的开发者

谷歌、苹果和微软这三个巨型科技公司都有一个特点,那就是他们都特别重视开发者,这是很显然的,我们从各方面都可以印证这一点。不说别的,就编译器、IDE、开发框架、发布平台等各类开发者工具都是被他们视为重中之重的业务的。因为他们非常清楚,一个平台上的开发者越多,那么这个平台就会越繁荣,其对应的生态就会越焕发生机,其背后的公司就能越值钱。

Chrome 就是一个非常典型的例子,它占领浏览器市场的第一步就是先占领开发者们,让开发者率先爱上它,之后才有那么多优质的插件被开发出来,Chrome 才有了那么可怕的市场占有率。

所以,开发者是兵家必争之地,得开发者者得天下。

这样的逻辑放到现实社会也是一样的,想一下,什么样的人是社会的“开发者”?以我愚见,这首当其要的就是企业家。但不要误会了,我所说的企业家,并不只是指那些少数功成名就的企业家们,更多的是指千千万万挤在小办公室里(或小商铺里、或小工作间里)、怀揣着理想、承担着风险的小企业家们。只有让这些企业家群体得到鼓励和保护,让企业家精神得到发扬,我们的社会才能繁荣富庶起来。

这周每天都会拿出一两个小时的时间来看这本《深入学习入门:基于 Python 的理论与实现》,每天学到了什么东西都会简单的记几笔,以下就是我这些略显零散的笔记的汇总。

第一二章

第一章基本上就是在复习第一本书的内容。主要谈了数学和 Python——矩阵的运算、多维数组的广播、矩阵形状的检查,以及如何在 Python 中做这些操作……其中值得注意的是提到了 Sum 节点、MatMul 节点的反向传播,这个是第一本书里没有提到的。

第二章开始就正式进入自然语言处理(NPL)的世界。自然语言处理是非常有难度的课题,它是人工智能皇冠上的明珠。在 NLP 领域,深度学习占据了非常重要的地位。搜索引擎和机器翻译等大量应用中都采用了 NPL 技术。

自然语言处理第一个要解决的问题就是让计算机理解单词的含义,有三种方法:同义词词典、计数(基于统计)、推理(基于深度学习)。

同义词词典就是根据单词的相似度把它们联系起来生成的图,最著名的同义词词典是 WordNet。同义词词典的问题:难以顺应时代、人力成本高、无法表示单词的微妙差别。

最有名的语料库应该是维基百科,文学作品、论文等各类出版物也经常被用作语料库。

分布式假设所表达的理念非常简单,主要是两点核心思想,第一,单词本身没有含义,单词含义由它所在的上下文(语境)形成,第二,含义相同的单词经常出现在相同的语境中。

通过共现矩阵将单词表示为了向量,然后就可以通过数学的方法对单词进行相关的计算了,比如说计算两个单词的关联度。

PPMI 矩阵是对共现矩阵的一种改进。但 PPMI 矩阵还是存在一个很大的问题,那就是随着语料库的词汇量增加,各个单词向量的维数也会增加。如果语料库的词汇量达到 10 万,则单词向量的维数也同样会达到 10 万。实际上,处理 10 万维向量是不现实的。并且向量中的绝大多数元素并不重要,也就是说,每个元素拥有的“重要性”很低。这就需要降维处理了。

降维的方法有很多,这本书上用到的是奇异值分解(Singular Value Decomposition,SVD)。 这一块是线性代数的知识,可以去看看 3b1b 的线性代数教程,通过可视化的方式更好地理解线性代数。

第三四章

基于计数的方法在处理大规模语料库时会出现问题。

神经网络可以分批次学习数据,而不用一股脑全部学了。

这一章主要讲了 word2vec 与其 CBOW 模型。

第四章

第四章讲了神经网络的学习,这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。

我们需要发现数据里的模式,那么就需要发现其中的特征量(也就是神经网络的中间层的节点),什么是特征量呢,比如说一句话的情感特征(生气、冷漠、好奇、礼貌、开心)、一个狗的图像的特征部位(眼睛、鼻子、耳朵、腿、尾巴)。一个例子是图二里的数字手写字,9 的特征就是有一个圈和一个竖,这个就是特征,将其量化后就叫特征量。

第一种,先从数据中提取特征量,再用机器学习技术学习这些特征量的模式。 在计算机视觉领域,常用的特征量包括 SIFT、SURF 和 HOG 等,使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的 SVM、KNN 等分类器进行学习。

第二种,神经网络中,连重要特征量也都是由机器来学习的。(可能是我们人类无法理解的一些特征量)

另外讲到了损失函数的概念,损失函数用于衡量神经网络模型的性能。通过将模型的预测值与应该输出的实际值进行比较,该函数将实质上计算出模型的执行效果。一般有均方误差 、交叉熵误差。神经网络训练的过程就是最小化损失函数的过程。(图三描绘地很清楚了)

这一章剩下的部分基本上都在讲高数(微分、导数、偏导、梯度),下一次打卡再总结。

第五章

第五章只讲了一个事情——误差反向传播(Back-propagation, BP)。

误差反向传播算法的出现是神经网络发展的重大突破,也是现在众多深度学习训练方法的基础。

我们先要理解,反向传播算法解决了什么问题。在我的上一个读书笔记里,我说到过,神经网络的训练过程就是一个不断求解梯度,然后下降,然后求解梯度,然后下降……循环往复直到找到最优解的一个过程。而到目前为止,求解梯度的方法当目前为止只说了数值微分的方法,这种方法的计算太耗时了,而反向传播就是用来快速求解梯度(导数)的。

反向传播不是那么容易理解,除了书本之外,我还推荐 3b1b 的视频:

这两个视频好好看一遍,基本上就理解了反向传播的原理,实际上本质就是对微分链式法则的一个应用。

第五章另外一大任务就是用代码(Python)实现反向传播的算法。

它分成了几种层去单独实现:简单层(加法和乘法)、激活函数层(sigmoid 函数、ReLU 函数)、Affine/Softmax 层。每种类型层的实现中都包括了一个 forward 函数和 backward 函数,分别表示正向传播和反向传播。最终再把这些层组合起来,就获得了误差反向传播法的实现。

理解了反向传播算法后,整个实现也是非常容易理解的。

第六章

打卡第六章,这一章说了很多实战经验,值得细读。主要说了几个问题:如何更新参数?如何设置参数初始值?如何避免过拟合?一个一个来看。

如何更新参数?

前面说过,深度学习就是梯度下降的过程,反向传播提高了「求梯度」的效率,那么接下来就要说「下降」的事情。「下降」其实就是更新参数的过程,参数的更新由 optimizer 负责完成。我们在这里需要做的只是将参数和梯度的信息传给 optimizer,由它来决定如何更新参数。

SGD 的缺点是如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效。为了改正 SGD 的缺点,书里介绍了 Momentum、AdaGrad、Adam 这 3 种更新参数的方法。 目前并不存在能在所有问题中都表现良好的方法。这 4 种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题。

很多深度学习框架都实现了各种 optimizer 方法,比如说 PyTorch 官网里就有关于这些方法文档。(下图)

如何设置参数初始值?

参数初始值的设置非常重要,因为这直接决定了学习是否能成功。初始值可以随便你设置,但是也是有讲究的,初始值不能设置为 0,不然无法学习!

偏向 0 和 1 的数据分布会造成反向传播中梯度的值不断变小,最后消失。这个问题称为梯度消失(gradient vanishing)。层次加深的深度学习中,梯度消失的问题可能会更加严重。

如果 100 个神经元都输出几乎相同的值,那么也可以由 1 个神经元来表达基本相同的事情。因此,激活值在分布上有所偏向会出现“表现力受限”的问题。

总结一下,当激活函数使用 ReLU 时,权重初始值使用 He 初始值,当激活函数为 sigmoid 或 tanh 等 S 型曲线函数时,初始值使用 Xavier 初始值。 这是目前的最佳实践。

这一章后面的内容等到下次打卡。

继续打卡第六章

通过使用 Batch Normalization(批处理归一化),可以调整激活的分布。Batch Norm 的思路是调整各层的激活值分布使其拥有适当的广度。为此,要向神经网络中插入对数据分布进行正规化的层,即 Batch Normalization 层。Batch Norm 层的反向传播的推导有些复杂,可以自行学习。

防止过拟合?

权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是因为权重参数取值过大才发生的。

Dropout 是一种在学习的过程中随机删除神经元的方法。训练时,随机选出隐藏层的神经元,然后将其删除。被删除的神经元不再进行信号的传递。

超参数的优化

逐渐缩小“好值”存在的范围是搜索超参数的一个有效方法。

这里介绍的超参数的最优化方法是实践性的方法。如果需要更精炼的方法,可以使用贝叶斯最优化(Bayesian optimization)。贝叶斯最优化运用以贝叶斯定理为中心的数学理论,能够更加严密、高效地进行最优化。

参数的更新方法、 权重初始值的赋值方法、Batch Normalization、Dropout 等,这些都是现代神经网络中不可或缺的技术,这些技巧在最先进的深度学习中也被频繁使用。

这一章有很多内容还不够理解,值得后续再研究一番。

打卡第七章——卷积神经网络(CNN)!!!

CNN 非常适合于图像识别、语音识别等场合,这里我要继续推荐 3b1b 的卷积神经网络的视频:https://www.youtube.com/watch?v=KuXjwB4LzSA

CNN 诞生的原因是全连接层(Affine 层) 忽视了数据的形状,但是数据的现状里蕴含了很多数据的特征。比如说图像有三维的数据:长、宽、颜色值。(如图所示)

卷积层可以保持形状不变,所以特别适合用来寻找图像的特征。

CNN 中有时将卷积层的输入输出数据称为特征图(feature map)。

关于 3 维数据卷积的运算,并不是很好理解,除了书本里的描述外,你还可以看这篇文章:https://blog.csdn.net/fu18946764506/article/details/88185434

池化是缩小高、长方向上的空间的运算。 除了 Max 池化之外,还有 Average 池化等。相对于 Max 池化是从目标区域中取出最大值,Average 池化则是计算目标区域的平均值。 在图像识别领域,主要使用 Max 池化。

通过 CNN 的可视化,可以更好地理解 CNN。如下图,第一层的神经元对边缘或斑块有响应,第三层的神经元对纹理有响应,第五层对物体部件有响应,最后的全连接层对物体的类别有响应。

具有代表性的 CNN 有 LeNet 和 AlexNet。LeNet 和 AlexNet 没有太大的不同。但围绕它们的环境和计算机技术有了很大的进步。具体地说,现在任何人都可以获得大量的数据。而且,擅长大规模并行计算的 GPU 得到普及,高速进行大量的运算已经成为可能。大数据和 GPU 已成为深度学习发展的巨大的原动力!

伟大的程序员都是产品经理

他们也许不懂产品设计的那些术语,但是不管是代码、API、协议、架构、文档、UI/UX,他们心里都总会有装着「人」,他们会思考别人会如何使用这些东西、为什么某个东西对别人来说很重要……

他们会持续不懈地追求更好的用户体验。

尽量构建小而美的东西

你总会有很多机会去做「添加」,但是却很难有机会去做「减少」。你要尽量抵挡这种趋势,专注于核心功能。无论是小到编写一个函数还是大到设计一个架构,尽量让它简单而优雅地完成任务。

要记得魔数 7

《Unix 编程艺术》里也提到过,人们无法一下子处理超过 7 个问题,因为人的短期记忆跨度是非常有限的。

这个数字可以让你避免构建过于复杂的东西,比如说你最好不要让一个函数调用别的函数超过 7 个,文档的一个小结最好不要超过 7 个要点……

The Magical Number Seven, Plus or Minus Two[1]

永远学习

哪怕你是个老炮,你也永远有不懂的地方,唯一的办法就是持续学习。实际上,不管是程序员还是什么职业,学习都是终身的事情,请成为一个终身学习者。

勇敢承认自己的无知,乐于向他人学习和请教。

永远重视基础知识

基础知识是有长期价值的,尽量多花时间掌握基础知识而不是上层那些花里胡哨的东西。因为基础知识才能让你「以不变应万变」,才可以让你理解事物的本质,当你遇到难题时才会更有自信。

成长是一个艰辛的过程

你或多或少要经历一些迷茫、痛苦、挣扎,最终才能走到更高的地方。认清楚这个现实,然后再稳扎稳打、踏踏实实地把路走出来。

这是长期主义者应该有的心态。

保持写作的习惯

程序员应该保持写作的习惯,不管是博客、文档,还是论文,不管你发布还是不发布这些东西,你都要持续地写、写、写。写作可以让人的思维变得清晰,让交流变得高效。

写作也是一件超高杠杆的事情。

数据往往比代码更值钱

永远重视数据,不要把它们搞丢了,让它们保持干净有序,长期来看,数据会给你带来源源不断的收益。

自己的数据资料也要管理好、备份好,因为这些都是你多年的劳动成果,千万不要把安全感寄托在一块磁盘上。

不妨多花点时间做决策

我们总是希望马上行动起来,马上看到产出,但这样很容易盲目。我们应该把大部分时间都花在思考问题和调研上,让自己尽量能做出好的决策。你待在越高的位置就应该把越多的时间花在做决策上。

好的决策可以让整个团队甚至是社区少走很多弯路。

赚钱并不寒碜

自信地为自己的劳动收取费用。免费做贡献是没问题的,它给你带来了荣誉,但是你为你的劳动收费也是理所应当的。我们为别人创造了价值,那么我们就应该获得应有的报酬。

这也是你尊重自己的知识、技能、时间的一种表现。

放下大厂和名校光环

你应该为自己的作品(做的事情)而感到骄傲而不是头衔。不要被那些光环给迷住了双眼,学会客观认识自己,想想看自己刨除那些光环后还剩下什么。

寻找合作伙伴或者员工的时候也是如此,不要被那些光环给眩晕了。

接受不完美

工程上很少有一个完美的解决方案,你学会理解那些不完美背后的权衡。尽量避免完美主义,因为在追求完美之外,还有很多更有价值的事情,它们更值得你付出时间和精力。

创建项目简单,维护项目难

我们或多或少都有「达芬奇综合征」:不停地开启新项目但是很少有真正完成的。

我们会沉迷在开新项目和收获新知识的快乐之中,但是真正要把一个项目做好是需要长年累月地迭代的,这个过程中大部分时候是枯燥的。

是否为科班出身并不重要

很多人会因为自己不是科班出身的而不自信,其实这并不重要,重要的是你是否有兴趣和热情。这个世界上已经有无数的例子向我们证明了这一点。


最后我想说,知易行难。懂得道理是几秒钟的事情,但是实践却需要长期地努力。

不过我们要坚信一句话:努力终有回报。

Rework 这本书的中文翻译叫做《重来》,作者是 37signals 的两个创始人。

第一次阅读这本书是在我在读本科的时候,当时就在学校图书馆里借的。

在这本书里,作者表达了上百个关于工作和创业的犀利观点,作者的很多观点都非常具有洞察力,让我经常有一种恍然大悟的感觉。虽然第一次看的时候我还没有工作,很多观点都没能够深刻体会。但我还是感受到了那种简洁有力的思想力量。

后来进入社会之后,又把这本书拿来读了几遍,也有了很多更深刻的体会。

为了加深我对这本书的理解,我打算写一写这本书的读书笔记,当然了,这本书里的内容很多东西可谈的,所以我并不打算一口气把这个写完,而是会按照时不时发一些的形式来写。有一些可能甚至可能只是对作者观点的简单总结。如下:

与其做个半成品,不如做好半个产品

我们要控制住自己的野心,集中自己的精力,把一个小事情做好。虽然理想很重要,但是也要客观地认识自己的能力,知道自己可以把握什么样的事情。

这一点我的感触比较深,我遇到的很多公司和团队都有这个问题,总是想做远超自己能力的事情。在外人看来就是没有收住自己扩张的 ego。

不要过早关注细节

应该关注整体。整体决定的性质而细节决定的深度,所以应该先关注一个事情的性质。

关注那些不变的事物

不变的才是值得去花精力去关注的,那些多变的事物,往往只是一副空壳。

所以,我们在学习计算机知识时,要多学习基础知识,多理解原理,不要浪费太多时间在各种框架里。因为那些基础知识都是几十年没变过的,可能预计未来几十年还是不会再改变。

招聘笔杆子

当你面试到两个差不多的候选人时,招募那个写作好的。因为写作技能是一个很大的杠杆,可以撬动很多资源。

你不能创造文化

公司的文化是它是自己生成的,它不会因为你说了什么就变成什么。

文化是持续行为的副产品。如果要培养公司的文化,那就要持续鼓励公司的员工去做某些事情,时间一长,自然就有了文化。

该睡觉就好好睡觉

不要在疲惫的时候工作,状态不好的情况下工作成果都是很不可靠的。

有时候我自己也会在很疲惫的时候工作,但是长期的事实证明,这样做是愚蠢的。累的时候做的工作都是负功,不一定能带来贡献,反而可能还要花更多的时间去修复这些负功。

亲力亲为

只有亲自做过某个职位,才能深刻理解这个职位,光听别人说是没法切实感受的。

Update

2023.09.09 Update: 最近有了一些新的思考。37 Signals 是一家非常特别的公司,以至于他们可以落地很特别的理念,并且取得成功。一般的公司还是不要盲目去模仿,要想清楚一个理念是否能真正帮助到自己,不然容易东施效颦,陷入形式主义。

经过小小的调查和统计,我发现几个关于图灵奖的有趣事实:

  • 获奖者中,美国人、英国人和加拿大人合计占了九成。
  • 获奖者中,本科学习数学、物理和电子工程的合计占了九成。
  • 获奖者中,有六十几位男性,但只有三位女性。
  • 最年轻的获奖者是 1974 年 36 岁的 Donald E. Knuth,因其在算法领域的贡献而获奖。
  • 表彰最多的主题是编译理论和编程语言设计(一大半以上)、人工智能(数个)。
  • 1974 年的获得者 Donald E. Knuth 是 Tex 和《计算机程序设计艺术》的作者。
  • 1975 年的获得者 Herbert A. Simon 还获得了 1978 年的诺贝尔奖,他和自己的学生 Allen Newell 共享当年的图灵奖,Simon 是一个通才,其研究领域涉及认知心理学、计算机科学、公共行政、经济学、管理学和科学哲学等多个方向。
  • 1998 年的获得者 Jim Gray 因其在数据库事务方面的贡献而获奖,没错,数据库事务值一个图灵奖。2007 年,他消失在海上,至今未被找到。
  • 1999 年的获得者 Frederick Brooks 是《人月神话》和《没有银弹》的作者,他还领导了 OS/360 的开发,他的研究领域也十分广泛。
  • 2013 年的获得者 Leslie Lamport 是 LaTex 和 Paxos 的作者,他奠定了分布式系统领域的基础。
  • 2014 年的获得者 Michael Stonebraker 是 PostgreSQL 的初始作者。
  • 2018 年的获得者 Yoshua Bengio 是花书《深度学习》的作者。
  • 2019 年的获得者 Edwin Earl Catmull 是皮克斯的创始人之一,同年的获得者 Patrick M. Hanrahan 也是皮克斯的员工并拿过三座奥斯卡金奖。
  • ALGOL 60 语言、C 语言、Smalltalk 语言、TCP/IP、HTTP、Unix、RSA、OS/360、Xerox Alto 这些都分别价值一个图灵奖。

最后!获得图灵奖并不难,只要成为一个领域的奠基人或者重大贡献者就行了,朋友们要加油了嗷。

糟糕的 2022 在历史上留下了浓墨重彩的一笔,还没反应过来,2023 已然启程。在一年的伊始之际,我们也该和过去做个告别,拍拍身上的尘土,整顿粮马再出发了。

前进时方向感尤其重要,所以我感觉有必要在此刻对世上我关注的事物做一点基本的判断。这篇 post 会对未来一年做一些趋势上的判断,但这些判断包含了很多我的个人臆测,仅供各位读者参考。

战争

从 2022 年开始,战争已经是一个无法被忽视的因素了。俄乌战争的结束遥遥无期,台海、中东等地的冲突一触即发,非洲那边也是长年战乱不止。这些战争对世界产生的影响难以估量。俄乌战争彻底改变了地缘政治格局,也顺带搅翻了能源和原油市场,对我们生活的方方面面产生了影响。战争将会是未来几年最大的灰犀牛。

OpenAI / DeepMind

今年 GitHub Copilot 和 ChatGPT 给我们带来了许多惊喜,在未来一年,深度学习将会继续从实验室走进工业界,其将会带来更多应用的落地,给我们带来更多惊喜,其中酝酿着许多危和机。

OpenAI 和 DeepMind 是两家在这个领域里值得关注的公司。

数据隐私 / 反垄断 / 出海

数据隐私将会继续受到关注,隐私产业还会继续发展,将会有更多的主打隐私保护的应用出现。

欧洲是反垄断最激烈的战场,欧盟取得的反垄断成果将会重塑科技行业,这不但有益于欧盟,还会惠及全世界。

互联网科技产业在国内的存量厮杀将会加剧,国内企业将会继续疯狂出海。

VR / AR

虚拟现实设备将会被持续改进,更多消费级的应用将会出现在虚拟现实平台。其作为下一代交互设备的定位没有变,长期看好。

新能源 / 环保产业

新能源依旧是资本宠儿,在国有资本和民间资本的双重推动下全速发展。环保产业还将持续发展,生存环境将会得到持续改善。

远程办公

远程办公还会加速流行,但个人依旧不看好纯远程办公,而是更看好混合办公,实质上是看好公司采用更宽松的管理制度。严抓考勤的管理方式已经不适合新生代的劳动力。

人力资源

随着优质的教育资源越来越容易获取,年轻的高水平人才将会变多,人才的竞争格局将会变得非常激烈。中年人将会越来越焦虑。

公司的招聘成本会继续上升,优秀的人才将会越来越难挖。人才越来越看重公司的氛围,越来越看重工作的体验感,考虑的因素不再只是工资的多寡。

和数据打交道的工作依旧很不错。

Web3 / Crypto

2022 是币圈历史上最动荡的一年,没有之一,经历了数次大暴雷,以及一整年的资本逃离。区块链技术依旧没有孕育出金融服务之外真正的应用,但金融服务类应用有其存在的合理性。未来几年,币圈乱象将会持续被整顿,相关法律法规会继续完善。NFT、GameFi 等叙事的吸资效果将会进一步降低。

内容产业

好的内容依旧稀缺,内容依旧为王。未来一年,内容创作者可以安心创作好内容,不用担心市场饱和的问题。

短视频依旧是内容行业中最有潜力、竞争也最为激烈的领域。

编程语言

持续看好 C / C++ / Rust 这几个高性能编程语言,尤其是 C++,鉴于其强大的兼容性、表现力和性能,其将继续为最前沿的科技提供动力。

数据库 / 分布式系统

由 Snowflake 所带来的数据库资本热将会进一步冷却,数据库创业潮将逐渐平息。Oracle 依旧最专业,PostgreSQL 依旧优雅并会更加受欢迎,MySQL 加速上云,SQLite 在其领域的垄断地位不可撼动。

由于成本降低和性能的进一步优化,分布式系统的优势会越发明显,业界对分布式系统的需求量会增多。

政体 / 移民

2022 年打醒了很多岁静党,专治体制丧失了一大波民心,更多人转而看好“低效率”的民主宪政制度。

移民产业将会持续火热。

新冠

全世界的人们将会继续与新冠战斗,但战斗烈度减弱。国内民众的生活将会重新回归 19 年之前。闹剧即将落幕,新剧缓缓开幕。


后记:2024 年再来看当时写的这篇文章,连我自己都惊叹于自己对将来一年的预测的准确性!

CMU 课程 + 《数据库系统概念》

  • Database system

    • storage manager
    • query processor component
    • transaction management component
  • Indexing

    • Ordered Indices
    • B+ Tree Indices
      • Queries
      • Updates
        • Insertion
        • Deletion
    • Hash Indices
      • Linear Probe Hashing
      • Robin Hood Hashing
      • Cuckoo Hashing
      • Chained Hashing
      • Extendible Hashing
        • Extendible Hash Table
      • Linear Hashing
    • Bitmap Indices
  • Query Processing

    • Selection
    • Sorting
      • External Sort-Merge Algorithm
    • Aggregations
    • Join
      • Nested-Loop Join
      • Block Nested-Loop Join
      • Indexed Nested-Loop Join
      • Merge Join
      • Hash Join
    • Modifications
      • Inserts
      • Updates
      • Deletes
  • Query Optimization

  • Concurrency Control

    • Lock-Based Protocols

      • 2PL/S2PL/R2PL
        • grow phase
        • shrink phase
      • Multiple-granularity Locking Protocol
        • IS/IX/S/SIX/X locks
    • Deadlock Handling

      • prevention
      • detection
      • recovery
    • Timestamp-Based Protocols

      • Ordering the transaction requests
    • Validation-Based Protocols

    • Multiversion Schemes

      • Combining with other protocols
    • Snapshot isolation

  • Transaction Concurrency Problems

    • Dirty Read
      • Read a uncorrect data.
    • Unrepeated Read
      • A transaction get two different value between two read operations.
    • Phantom Read
      • A transaction insert between B transaction’s two times of read operation, then B transaction get two different amount of ouputs.
  • Transaction isolation levels (high->low)

    • Serializable
    • Repeatable Read
    • Read Committed
    • Read Uncommitted
  • Isolation implementation

    • Locking
      • likes 2PL.
    • Timestamps
      • Ensure data items access order by timestamp (or serial number). Each data item holds two timestamps, read timestamp and write timestamp, and each transaction get a timestamp when it start.
    • Multi-version and snapshots
      • No lock, no waiting, good performance, widely adopted.
  • Advanced topics on Transaction

    • Concurrenency control ofern become bottlenecks in main-memory database.
    • Online Index Creation
    • B+ Tree index-concurrency control
      • crabbing protocols
      • B-link trees
    • Concurrency Control in Main-Memory Databases
      • lock-free data structure
  • Recovery System

    • Log Records
    • Checkpointing
    • ARIES
      • log sequence numbers
        • pageLSN
        • flushedLSN
      • recovery
        • Analysis pass
        • Redo pass
        • Unfo pass
  • Server System Architectures

    • Transaction-Server Architecture
      • Server processes
      • Lock manager process
      • Database writer process
      • Log writer process
      • Checkpoint process
      • Process monitor process
      • Buffer pool
      • Lock table
      • Log buffer
      • Query plan cache
    • Data servers
  • Distributed Databases

    • Partitioning Schemes
    • Distributed Concurrency Control
      • Centralized coordinator
      • Middleware
      • Decentralized coordinator
    • Partitioning
      • Naive Table Partitioning
      • Horizontal Table Partitioning

以前遇到有意思的东西就会有捡垃圾的心态:“点一下关注吧,反正就是动动手指,万一以后用得上呢?”,或者是“加一个好友吧,万一以后有事能寻求帮助呢?”……

后来当自己的世界被越来越多东西充满的时候,才明白,我们的时间和注意力都是有限且珍贵的,所以不应该浪费一刻在不值得的地方。于是我就针对线上生活,做了一些断舍离,大概如下:

  • YouTube:退订各种不值得浪费时间看的频道。
  • Twitter:大批量取关那些毫无营养的推特号,添加一些关键词屏蔽。以我观察,关注数最好是保持在 150 以下。
  • GitHub:unstar 一些对我来说已再无收藏价值的 repo,unfollow 一些不想再关注的账号。
  • Telegram:退掉对我没有太多意义的频道和群聊,尤其是一些几千人的大混战讨论群。
  • RSS:删掉某些我很长时间都不看的订阅。
  • 微信:批量删除再无联系意义的好友,我做得比较狠,大概删掉了 75%的好友。退掉各种无意义的微信群和公众号。
  • 草稿箱:平时写了很多半成品的文字,该删的删,该归纳的归纳。
  • 邮箱:删掉某些用邮箱注册的服务,屏蔽掉一些广告邮件地址。
  • App:删掉一些几个月都不打开的 App,删掉一些注意力吞噬 App。

等等,还有很多就不一一列举了。

这样删删删的过程持续了好几天,一波操作下来,效果一点一点地堆积起来,最后明显感觉到世界变安静了,我也更专注了。

断舍离的过程很有意思,它实际上就是在反复审问自己:“某某真的是我需要的吗?”不停地问这样的问题,促使自己去找到当下最应该专注的地方。

不过在断舍离的过程中,也会遇到一些心理上的障碍:

  • “某某万一以后我还用得上呢?”

不会的,某个东西一旦一段时间用不上,可能之后就永远用不上了,果断舍弃掉吧。

  • “没法立马收到最新消息怎么办?”

不必焦虑于新消息的获取,因为真正重要的消息会自己找上门来的,并且没有什么消息是真的非要立马知晓不可。如果你真的要自己去筛有效信息,那将会付出极高的注意力成本,不值得。

我觉得以后每隔一段时间就可以来一次这样的断舍离,主动积极地去抵抗各种互联网产品带来的信息过载。

之前看《穷查理宝典》里面有一段说到巴菲特曾经这样形容价值投资,他说“提高投资水平很容易,你只需要拿一张标记卡,上面有 12 个空白位置,你每投资一次就在一个空白上划个勾,12 次就是你一生中可以投资的总次数,这样,你就会全力对待每一次投资。”

同样的道理放在我们投资时间和注意力上也是适用的,我们要非常慎重地分配一天醒着的十几个小时时间。

创业或者说当独立开发者的一个比较好的思路是去做改进(而不是造新)。

很多伟大的产品都是在改进现有产品的基础上诞生的,比如说在谷歌搜索引擎推出之前,市面上已经有十几款搜索引擎了,而谷歌通过反链的思路对页面进行排序(这是一种改进),从而获得极大的产品优势,最终抢占市场。这样的例子你可以找出无数个。

改进的好处是你不需要费功夫去证明市场上有没有某个需求的存在。金矿就在那里躺着,就看你能不能挖走一块。

那么再说说可以从哪些方面去实践这种改进的思路。

第一类改进的思路就是去做视觉上的改进。

许多产品它的功能逻辑其实是大同小异的,如果在现有功能不变的情况下,你能把一个产品的外观做得很漂亮,那么我愿意为它买单,道理很简单,我喜欢漂亮的东西,每个人都喜欢漂亮的东西。这样的改进需要你有比较强的审美素养,比如最基本的能够理解不同字号字重对排版的影响……这方面有一个比较好的学习资料就是苹果官方一直在维护的 Human Interface Guidelines,如果你做的产品是这种软件类型的,它有一个用户界面,那么这将会是非常非常有用的一份学习资料。

第二类改进的思路就是去看看某一类产品有没有功能上的痛点和痒点。

有的话可以再进一步看看有没有方法(技术)可以解决这些痛点和痒点,让它用起来更舒服或者让它更适合于某些用户群体。最典型例子的就是百家争鸣的 ToDo 应用。另外一个例子是前段时间比较火的 Figma,它相对于老牌原型设计软件 Sketch 做的改进就是允许多人在线编辑及其便携性。

我认为不管是创业还是做独立开发,正确的思路(大体上来说但不绝对)是从市场推导出战略(打法),再从战略推导出产品,再从产品推导出技术,而不是我们某些朋友所想的从技术推导出产品,再从产品推导出战略,再从战略推导出市场。

逻辑一定不能弄反了,不然很容易出现这样的情况:你辛辛苦苦把一个你自认为很牛逼的东西搞出来了,但是可悲地发现没有任何人为愿意为它付费,那结果就相当惨烈了。

广度和深度本身就是两个矛盾的点。在资源有限、精力有限的情况下,要追求广度就必定会牺牲深度,要追求深度也必定会牺牲广度。放到我们发展自生的角度来说也是一样的,如果要往某个领域深度发展,那么就会可能陷入知识体系单一的境地,反之如果想尽可能熟悉更多的领域,那么就可能会出现啥都懂一些却啥都不精通的情况。

为了更好地分配有限的时间和精力,我们不得不思考上述这样的问题。就自我发展而言,广度和深度的矛盾,如何破?

这个问题我其实也思索良久。之前不知道在哪儿看到的一个说法,叫做“T 型人才”,这个思路我是比较赞同的。“T 型”的意思是有一个深入的领域和多个熟悉的领域,非常形象。如果成为某个领域的专家要花费 100%的时间,那么成为半个专家可能只需要花费 20%的时间,因为二八定律在这方面也是适用的,我们可以只用 20%时间去掌握某个领域 80%的知识(并且只挑那种最主要最重要的知识去掌握)。 这样的客观规律的存在让我们有了能把自己打造为“T 型人才”的可能性。

成为“T 型人才”的好处就像我上面说的,既可以避免知识体系的单一又可以避免无一精通的窘境。而且,复合型人才在各行各业都是非常紧俏的,知识这种东西和别的东西不一样,它是 1+1 远远大于 2 的,纵观历史,无数前沿创新和好创意都是来自于不同领域的复合,现在也是如此。

WRITING, BRIEFLY

原文地址,写于:2005 年 3 月

我认为,擅于写作比大部分人想象地要重要。写作不仅仅是交流想法,而且还产生想法。如果你不太会写作而且不喜欢写作,那么你会错失大部分在写作过程中产生的想法。

至于如何写好,这里有一些简单的建议:

以最快速度写下糟糕的第一个版本,再反复地改写它。缩减掉所有没必要的,并用对话的语气写。

培养对糟糕写作的嗅觉,这样你就可以察觉并修改自己的文章。

模仿你喜欢的作家。

如果你无法开始写作,那就告诉别人你打算写什么,然后再写下你说的话。

请接受一点,一篇文章 80%的内容会在你开始写之后才想到,并且你一开始写的 50%的内容都会是错的,所以请自信地缩减文章。

让你信得过的朋友读你的写的东西,让他们告诉你哪些地方写得不清晰的或者是多余的。

不要需要总是描绘出详细的轮廓。

在写之前几天把想法想清楚,随身携带一个小笔记本或者一张废纸。当你想到第一个句子的时候就开始写,如果 ddl 迫使你早点写,那就先写下最重要的句子。

写你喜欢的事物,不要试图让人印象深刻,随时可以改变话题。用脚注来包含一些离题的内容,用前后呼应来连接一些句子。

大声地读出你的文章,第一,看看哪些地方读起来不顺畅,第二,看看哪些地方是很无聊的(读这些段落让你害怕)。尽量告诉读者一些新的或者有用的东西,多花点时间来写。

当你重新开始时,再来读读你写的文章。当你完成后,给自己留一些容易的话题来开始。

在文件的底部为你计划涉及的主题积累笔记,但是不用覆盖所有的主题。你是在为一个不会像你读得那么认真的读者而写作,就像流行音乐一样,为了在一个糟糕的汽车收音机上听起来还 OK 而创作的。如果你写了什么不对的,那就快速修改它。

问你朋友你最后悔的一句话,看看他们的反应。然后回去,缓和某些措辞。

在网上发布你的东西,因为有听众会促使你写更多,并产生更多想法。

把草稿打印下来,而不仅仅是在屏幕上盯着看。

使用简单的语言,学会区分哪些是创意,哪些是离题。

学会判断结尾的临近,当出现一个时,那就抓住它。(知道什么时候结束一篇文章。)

原文标题:When Does Your OS Run?

原文链接:https://manybutfinite.com/post/when-does-your-os-run/

原文发布时间:Oct 28th, 2014

这有个问题:在你读到这一段话的时候,你的 OS 在运行吗?或者只是你的浏览器在运行?或者它们可能都在空闲状态,只是等你来点什么?

这个问题很简单,但却贯穿了软件的运行本质。为了准确回答这些问题,我们需要一个良好的操作系统行为心理模型,这反过来会影响性能、安全性和故障排除决策。我们将在本系列文章中使用 Linux 作为主要的操作系统(OS X 和 Windows 客串)来构建的模型。我会想深入研究的读者提供 Linux 内核源代码的链接。

这里的基本都原理是在任何给定时刻,CPU 上只有一个进程处于活动状态。任务通常是一个程序,比如浏览器或者音乐播放器,也可以是操作系统线程,但就一个任务。不是两个或者更多的任务。也永远不是 0 个任务。永远都是一个任务。

这听起来是问题的。比如说,如果你的音乐播放器占用了 CPU,不让其他任务运行怎么办?你无法打开一个工具来杀死它,甚至鼠标点击也是徒劳的,因为 OS 无法处理。它可能会不停地卡在“What does the fox say?”这里,引起办公室的一场骚乱。

这时就需要中断了。就像神经系统会中断大脑来引入外部刺激——一声巨响、触碰肩膀——计算机主板上的芯片组会中断 CPU 以传递外部事件的消息——按键盘、网络数据包到达、磁盘完成读取等。硬件外设、主板上的中断控制器和 CPU 它自己这几个一起协作来实现这些中断(interrupts)。

为了追踪最重要的东西——时间,中断也是必不可少的。在启动的过程中,内核会启动硬件定时器来让它定时发送定时器中断,比如说每 10 微秒发送一次。每次定时结束时,CPU 给内核有机会让它更新系统数据并评估当前状况:当前程序是否运行太久了?是否有 TCP 超时?中断让内核有机会思考这些问题,并采取适当的行动。就像你在一天中设置周期性的闹钟,每次闹钟响了就检查:我应该做我现在在做的事情吗?还有更紧急的事情吗?有一天你发现 10 年过去了。

内核把 CPU 这种周期性的劫持叫做时钟(ticks),所以中断实际上让你的 OS 跳动。不过还有:中断也用来处理一些软件事件,比如说整数溢出和页面异常,这些都不涉及到外部的硬件。中断是进入操作系统内核最频繁的和最关键的入口点。它们并不是 EE 人员需要担心的什么奇怪东西,这只是你的操作系统运行的机制。

说得够多了,我们实际看看吧。下面是 Intel Core i5 系列的网卡的中断。这个图是可以点击的,链接到具体的页面。

先看看这个。首先,由于中断的来源很多,如果硬件只是简单地告诉 CPU“嘿,发生了一些事情!”,这样没有什么帮助。悬念会让人难以忍受。因此在计算机通电之后,每个设备都分配了一个中断请求线(interrupt request line, IRQ)。这些 IRQ 依次由中断控制器映射为中断向量(interrupt vector),一个介于 0 和 255 之间的数字。当一个中断到达 CPU 时,它已经有了一个很好的、定义良好的数字,与不可预测的硬件隔离开。

CPU 有一个指针,指向由内核提供的 255 个函数组成的数组,其中每个函数都是特定中断向量的处理程序。稍后我们会更详细地介绍这个数组,即中断描述符表(Interrupt Descriptor Table, IDT)。

每当中断到达时,CPU 将其向量用作 IDT 的索引,并运行适当的处理程序。这是在当前运行的任务上下文中发生的特殊函数调用,允许操作系统以最小的开销快速响应外部事件。因此,网络服务器在向你发送数据时,会间接调用你 CPU 中的一个函数,这既酷又可怕。下面给出了一种情况,在中断到达时,CPU 正在忙于运行 Vim 命令:

请注意中断的到达是如何导致切换到核心态和零环权限的,但这并不改变活动进程。这就好像 Vim 直接对内核进行了一个魔术函数调用,但 Vim 仍然在那里,其地址空间完好无损,等待该调用返回。

令人兴奋的东西!唉,我需要保持这个 post-sized,所以让我们现在完成。我知道我们还没有回答开始的问题,实际上还提出了新的问题,但你现在怀疑,当你读这句话时,蜱虫正在发生。我们会在充实我们的动态操作系统行为模型时找到答案,浏览器场景也会变得清晰。如果你有问题,特别是在帖子发布的时候,尽管提出来,我会在帖子中或评论中回答它们。下一期将于明天在 RSS 和 Twitter 上播出。(这句是机翻)

数据结构和算法是编程的灵魂。

  • 函数的增长

    • 渐进记号
  • 复杂度分析

    • 摊还分析
      • 核算法
      • 势能法
  • 分治策略

    • 对递归式的求解
      • 主定理
  • 贪心算法

    • 霍夫曼编码
    • Dijkstra 算法
    • 最小生成树的 Prim 算法和 Kruskal 算法
    • 最佳优先搜索(BFS)
    • A*寻路算法(启发式的贪心算法)
    • 拟阵理论来证明贪心算法能找到最佳解
  • 动态规划

    • 动态规划问题的特征
    • 最长公共子序列问题(LCS)
  • 排序

    • 冒泡排序
    • 快速排序
      • pivot
      • 快速排序的优化
    • 归并排序
    • 堆排序
    • 桶排序
    • 计数排序
    • 基数排序
  • 中位数和顺序统计量

  • 散列表

    • 散列函数
    • 解决冲突
  • 平衡树

    • B 树
    • 红黑树
    • 斐波那契堆
  • van Emde Boas 树

  • 用于不相交集合的数据结构

  • 图算法

    • 遍历
      • 深度优先遍历
      • 广度优先遍历
    • 拓扑排序算法
    • 最小生成树算法
      • Prim 算法
      • Kruskal 算法
    • 最短路径问题
      • Dijkstra 算法(单源最短路径算法)
    • 所有结对点的最短路径问题
    • 寻路算法
      • 广度优先搜索
      • Dijkstra 算法
      • 贪婪最佳优先搜索
      • A*算法
      • B*算法
    • 最大流问题
  • 多线程算法

    • 多线程执行模型
    • 性能分析
    • 多线程矩阵乘法
    • 多线程归并排序
    • 为多线程算法设计的硬件
  • 线性规划

    • 把一个问题转化为线性规划问题
    • 线性规划的求解原理
  • 数论算法

    • 加密
  • 字符串匹配算法

    • 朴素算法
    • Robin-Karp 算法
    • 有限自动机算法
    • KMP 算法
  • NP 完全性

  • 近似算法

原文标题:Motherboard Chipsets and the Memory Map

原文链接:https://manybutfinite.com/post/motherboard-chipsets-memory-map/

原文发布时间:Jun 4th, 2008

我正要写一些关于计算机内部的帖子,目标是解释现代内核是如何工作的。我希望它们对于那些对感兴趣但是没有经验的爱好者和程序员有用。我们专注于 Linux、Windows 和 Intel 处理器。计算机内部是我的一个小爱好,我已经写了相当量的内核态代码,但是这段时间没有写了。第一个帖子描述了基于 Intel CPU 的现代主板,以及 CPU 是如何访问内存和系统内存映射。

开始之前我们先看一下一个 Intel 计算机是如何连接在一起。下图展示了主板里的主要组件:


现代主板的图。北桥和南桥组成了芯片组。

当你看到这个,最重要的事情是记住,CPU 不会真正知道它连接了什么。它通过pins和外部世界进行联系。它可能是一台计算机的主板,但也可能是一个烤面包机、网络路由器、大脑植入物或 CPU 测试台。CPU 和外部通信的主要方式有三种:内存地址空间、I/O 地址空间和中断。我们现在只关注主板和内存。

在主板上,CPU 通向世界的网关是连接它和北桥的前端总线。CPU 通过这个总线来读写内存。它使用一些引脚来传输它想要写入或读取的物理内存地址,而其他引脚发送要写入的值或接收要读取的值。一个 Intel Core 2 QX6600 有 33 个引脚来传输物理内存地址(因此有 2^33 个内存地址)和 64 个引脚来发送或接收数据(因此数据在一个 64 位的数据路径上传输,也就是 8 字节的数据块)。这允许 CPU 物理寻址 64GB 的内存(2^33 个地址 * 8 字节),尽管大多数芯片组只能处理 8GB 的 RAM。

现在问题来了。我们习惯于只从 RAM 的角度来考虑内存,也就是程序一直都在读写的东西。事实上,大多数来自处理器的内存请求都是通过北桥路由到 RAM 模块的。但不是所有的。物理内存地址也用于与主板上的各种设备进行通信(这种通信称为内存映射 I/O)。这些设备包括显卡、大多数 PCI 卡(例如扫描仪或 SCSI 卡),以及存储 BIOS 的闪存

当北桥收到一个物理内存请求,它来决定把请求路由到哪里:应该路由到 RAM?显卡?这个路由由内存地址映射来决定的。对于物理内存地址的每个区域,内存映射知道拥有该区域的设备。大部分的地址被映射到 RAM,但是当它们不是映射到 RAM 时,内存映射会告诉芯片组哪个设备应该为这些请求地址服务。这些不是没有映射地址到 RAM 模块的导致内存在 640KB 到 1MB 之间出现经典的空洞。当内存地址为显卡和 PCI 设备预留内存地址时,这个空洞就更大了。这就是为什么 32 位的操作系统使用 4GB 的 RAM 会有问题。Linux 文件/proc/iomem整齐地列出了这些地址范围的映射。下图展示了一个典型的内存映射(为 Intel PC 的最开始的 4GB 的物理内存进行内存映射):


Intel 系统中最开始的 4GB 内存的布局。(译者注:这是物理内存的映射,不要和虚拟内存映射搞混了,例如最终转化出来的物理地址为 0xFFFFE,那就是在访问 System BIOS。)

实际的地址和范围取决于计算机中特定主板和设备,但大多数 Core 2 系统非常接近上述。所有的棕色区域都是 RAM 之外的映射。记住,这些是在主板总线上使用的物理地址。在 CPU 内部(例如,在我们运行和编写的程序中),内存地址是逻辑的,在总线上访问内存之前,它们必须被 CPU 转换成物理地址。

逻辑地址转化为物理地址的规则很复杂,它取决于 CPU 运行的模式(实模式、32 位保护模式和 64 位保护模式)。不管转换机制如何,CPU 模式决定可以访问多少物理内存。例如,如果 CPU 在 32 位模式下运行,那么它只能物理寻址 4GB(好吧,有一个例外称为物理地址扩展的,但现在忽略它)。由于前面 1GB 左右的物理地址被映射到主板设备,CPU 只能有效地使用 3GB 的 RAM(有时甚至更小 - 我有一个 Vista 机器只能用 2.4GB 的 RAM。)如果 CPU 处于实模式,那么它只能处理 1MB 的物理 RAM(这是早期 Intel 处理器的唯一模式)。另一方面,在 64 位模式下运行的 CPU 可以访问 64GB 的 RAM(少数芯片组可以支持那么多 RAM)。在 64 位模式下,可以使用系统中总 RAM 以上的物理地址来访问 RAM 区域,对应了主板设备上“偷去”的物理地址。这些被称为 reclaiming memory,它是在芯片组的帮助下完成的。

这就是我们下一篇文章所需要的全部内存,它描述了从电源启动到引导加载程序即将进入内核的引导过程。如果你想了解更多关于这方面的知识,我强烈推荐 Intel 手册。总的来说,我很喜欢第一手资料,但 Intel 的手册写得很好,也很准确。这里有一些:

  • Datasheet for Intel G35 Chipset documents a representative chipset for Core 2 processors. 这是这篇的文章的主要来源。
  • Datasheet for Intel Core 2 Quad-Core Q6000 Sequence is a processor datasheet. 它给出了处理器中的每个引脚(实际上并没有那么多,并且在你将它们分组之后,它真的没有很多内容)的文档。有趣的东西,尽管有些地方很神秘。
  • Intel Software Developer’s Manuals 非常出色。这些手册一点也不神秘,它们漂亮地解释了所有关于架构的事情。第 1 卷和第 3 卷有很多好东西(不要被名字所迷惑,“卷”很小,你可以有选择地阅读)。
  • Pádraig Brady 建议我放上 Ulrich Drepper 的出色的关于内存的论文。这是好东西,我想把它放到一篇关于内存的帖子里,但是这里些也可以放,毕竟越多越好。

原文标题:CPU Rings, Privilege, and Protection

原文链接:https://manybutfinite.com/post/cpu-rings-privilege-and-protection/

原文发布时间:Aug 20th, 2008

您可能在直觉上知道,在 Intel x86 计算机上应用程序的能力是有限的,只有操作系统代码才能执行某些任务,但您知道这是什么原理吗?在这篇文章里,我们来看看 x86 特权级别(privilege levels),操作系统和 CPU 会协作来形成保护机制,来限制用户态(user-mode)程序的执行。CPU 有 4 个特权级别,从 0 (最高特权)到 3 (最低特权),主要有 3 种资源被保护:内存、I/O 端口和执行某些机器指令的能力。在某一时刻,x86 CPU 都在特定的特权级别上运行,这决定了哪些代码可以做什么,哪些不能做什么。这些特权级别通常被描述为保护环,最里面的环对应最高特权。大多数现代 x86 内核只使用两个特权级别,0 和 3:

x86 Protection Rings
x86 保护环

在众多机器指令中,大约有 15 条指令被 CPU 限制为 ring 0 权限才能执行。其他许多指令也会对其操作数做限制。如果允许用户模式下运行止血指令,可能会破坏保护机制,引发混乱,所以这些指令保留给内核使用。试图在 ring 0 外运行它们会导致通用保护(general-protection)异常,比如一个程序使用无效内存地址时。同样,对内存和 I/O 端口的访问也会基于特权级别进行限制。但在我们了解保护机制之前,让我们先看看 CPU 是如何跟踪当前的特权级别的,这涉及到之前帖子中的段选择器(segment selectors)。它们是这样的:

Segment Selectors - Data and Code
数据段选择器和代码段选择器

数据段选择器的所有 16 位内容会被代码直接加载到各种段寄存器中,比如 ss(堆栈段寄存器)和 ds(数据段寄存器)。其中包括被 Requested Privilege Level (RPL) 字段,我们将稍微处理它的含义。然而,代码段寄存器(cs)是不可思议的。首先,它的内容不能直接由加载指令(如 mov)设置,而只能由改变程序执行流程的指令(如 call)设置。其次,对我们来说很重要的一点是,cs 不是一个可以通过代码设置的 RPL 字段,而是一个由 CPU 自身维护的 Current Privilege Level(CPL) 字段。代码段寄存器中的 2 位 CPL 字段总是等于 CPU 当前的特权级别。英特尔的文档在这个问题上有点摇摆不定,有时在线文件也会混淆这个问题,但事实如此。在任何时候,无论 CPU 中发生了什么,cs 中的 CPL 字段都会告诉你当前运行代码的特权级别。

请记住,CPU 特权级别与操作系统用户无关。 无论您是 root 用户、管理员、游客还是普通用户,都没有关系。所有的用户代码在 ring 3 特权级别下运行,所有的内核代码都在 ring 0 特权级别中运行,无论代码代表哪个操作系统用户使用。有时,某些内核任务可以被推到用户态下运行,例如 Windows Vista 中用户态设备驱动程序,但这些只是为内核执行任务的特殊进程,通常可以在没有重大后果的情况下被终止。

由于对内存和 I/O 端口的访问限制,在不调用内核的情况下,用户态程序几乎不能与外界做任何交互。它无法打开文件、发送网络数据包、打印到屏幕上或分配内存。用户进程运行在由零环之神(the gods of ring zero)设置的非常有限制的沙盒中。这就是为什么通过设计,可以保证一个进程不可能泄漏其使用之外的内存,或者在它退出后留下打开的文件。所有控制这些东西的数据结构——内存、打开的文件等,都不能被用户代码直接触及;一旦进程完成,沙盒就会被操作系统内核清除。这就是为什么我们的服务器可以一直正常运行 600 天——只要硬件和内核不出问题,里面的东西可以永远运行。这也是 Windows 95 / 98 经常崩溃的原因:这不是因为“M$ sucks”,而是因为为了兼容性的原因,重要的数据结构可以被用户态程序访问。尽管这种做法代价高昂,在当时这可能是一种很好的权衡。

CPU 在两个关键点上保护内存:当一个段选择器被加载时,以及当一个内存页被一个线性地址访问时。因此,当分段和分页都涉及到时,会内存地址转换来做保护。当一个数据段选择器被加载时,会进行以下检查:

x86 Segment Protection
x86 分段保护

由于更高的数字意味着更小的特权,因此上面的 MAX()选择 CPL 和 RPL 中特权最低的,并将其与描述符特权级别(DPL)进行比较。如果 DPL 更高或相等,则允许访问。RPL 背后的想法是允许内核代码使用降低的权限来加载一个段。例如,可以使用 RPL 为 3 来确保用户态代码可以访问该段。堆栈段寄存器 ss 例外,对于它,CPL、RPL 和 DPL 三者必须完全匹配。

事实上,段保护几乎不重要,因为现代内核使用一个平坦的地址空间,用户态的段可以达到整个线性地址空间。当一个线性地址转换成一个物理地址时,有用的内存保护是在分页单元中完成的。内存页是一个连续的字节块,由页表项(page table entry)描述,页表项里包含两个与保护相关的字段:一个 supervisor 标志位和一个 read/write 标志位。supervisor 标志位是内核主要是用的 x86 内存保护机制。当 supervisor 标志位为 1 时,该页不能从 ring 3 访问。虽然 read/write 标志位在限制特权方面没有那么重要,但它仍然很有用。当加载一个进程时,存储二进制 image(代码)的页面被标记为只读,因此如果程序试图写入这些页面,就会捕获一些指针错误。在 Unix 中,这个标志也用来实现在 fork 进程时的写时复制(copy on write) 机制。在 fork 时,父进程的页面被标记为只读,并与 fork 出的子进程共享。当子进程试图写入这个页面时,CPU 就会触发一个错误,让内核知道此时要复制这个页面,并将其标记为可写入。

最后,我们需要一种方法让 CPU 在特权级别之间切换。如果 ring 3 的代码可以将控制权转移到内核中的任意位置,那么就很容易通过跳转到错误的(对的?)位置来颠覆操作系统。有控制的控制权转移是必要的。这是通过门描述符(gate descriptors)sysenter 指令来实现的。门描述符一种类型的段描述符,它有四种子类型:call-gate 描述符、interrupt-gate 描述符、trap-gate 描述符 以及 task-gate 描述符。call-gate 提供了一个内核入口点,可以与普通的调用和 jmp 指令一起使用,但是它们使用的不多,所以我先忽略它们。task-gate 也不是那么热门(在 Linux 中,它们只用于由内核或硬件问题引起的双重错误)。

这样就剩下了两个:interrupt-gate 和 trap-gate,它们被用来处理硬件中断(例如,键盘,定时器,磁盘)和异常(例如,页面错误,除零)。我将两者都称为”中断”这些门描述符存储在中断描述符表 Interrupt Descriptor Table (IDT) 中。每个中断被分配一个 0 到 255 之间的数字,称为向量,当处理器在找应该用哪个 gate 描述符来处理中断时,处理器就是通过把这个数字作为索引到 IDT 中找到该 gate 描述符的。interrupt-gate 和 trap-gate 几乎相同。它们的格式如下所示,以及在中断发生时强制执行的特权检查。为了让这个例子更具体,我还放上了一些 Linux 内核里的变量名。

Interrupt Descriptor with Privilege Check
中断描述符及其特权检查

gate 里的 DPL 字段和段选择器都被用来控制访问,而且段选择器加上偏移量(offset)一起确定中断处理程序代码的入口点。内核通常在这些 gate 描述符中为内核代码段使用段选择器。一个中断永远不能将控制从高特权的环转移到低特权的环。特权要么保持不变(当内核本身被中断时),要么提高(当用户模式代码被中断时)。在这两种情况下,得到的 CPL 将等于目标代码段的 DPL;如果 CPL 发生切换时,堆栈也会发生切换。如果一个中断是由代码通过诸如int n这样的指令触发的,那么需要额外的一次检查: gate DPL 必须与 CPL 具有相同或更低的权限,这可以防止用户的代码随机触发中断。如果这些检查失败——你猜对了——就会发生一个通用保护(general-protection)异常。所有的 Linux 中断处理程序最终都会在 ring 0 中运行。

Linux 内核在初始化时会先在 setup_idt()中设置一个 IDT,设置时忽略中断。然后它使用 include/asm-x86/desc.h 中的函数来填充 arch/x86/kernel/traps_32.c 中的常见的 IDT 条目。在 Linux 中,名称中带有“system”的 gate 描述符可以从用户态中访问,它的 set 函数使用 DPL 为 3。“system gate”是用户态可访问的一种 Intel trap gate。另外,硬件中断门不是在这里设置的,而是在适当的驱动程序中设置的。

用户态可访问三个 gate: 中断向量 3 和 4 分别用于调试和检查数值溢出。然后为 SYSCALL_VECTOR 设置一个 system gate,对于 x86 架构来说是 0x80。这是进程将控制转移到内核、进行系统调用的机制。从 Pentium Pro 开始,sysenter 指令作为一种更快的系统调用方式而被引入。它依赖于特殊用途的 CPU 寄存器,用于存储内核系统调用处理程序的代码段、入口点和其他细节。当 sysenter 被执行时,CPU 不会进行特权检查,而是立即进入 CPL 0,并将新的值加载到代码寄存器和堆栈寄存器(cs, eip, ss, esp)。只有 ring 0 可以用 sysenter 来设置寄存器,这是在 enable_sep_cpu()中完成的。

最后,当需要返回到 ring 3 时,内核发出 iretsysexit 指令,分别从中断和系统调用返回,从而退出 ring 0,并恢复 CPL 为 3 的用户代码的执行。Vim 告诉我,我将接近 1900 个单词,所以 I/O 端口保护的事情我隔天再说。我们关于 x86 环和保护的旅程就此结束。感谢你的阅读!

我的翻译感想:

最近在学习 MIT 的一门研究生公开课——6.828 操作系统。关于这篇文章中提到的 IDT,这门课程也提供了很多资料,比如说这个x86 IDT。我在学习这门课程的过程想要搞懂 CPU 在特权级、保护方面的作用和原理,Intel 的技术手册是一个比较好的材料,但是不够简练,没法让我一下子就明白核心原理,而这篇文章关于这个话题写得还不错,故花了一个下午的时间把它翻译为中文。

一个比较有趣的地方在于(也是我突然想明白的),当前运行程序的特权级的切换是通过一个叫做 gate 的东西来实现的。为什么要把它叫做 gate 呢?因为只有通过一个“门”才能走进/走出“高特权”的世界,无比形象。

总结下来,CPU 提供了一系列如中断、特权检查、分段、分页等功能,以及一些特殊的寄存器,操作系统通过巧妙地利用这些功能和寄存器来对用户态的程序进行隔离。如果想要深入研究这些主题,可以阅读阅读 Intel 的技术手册。

有很多材料值得阅读,我也贴出来:

Protection ring
Call gate (Intel)
Global Descriptor Table
Protected mode

前言

2019 年末,新冠病毒席卷全球,大家都被迫隔离在家里,吃在家里、住在家里、工作也在家里,办公远程化的进程被新冠病毒快速推进着。

我作为一个程序员,在一家公司完全远程地工作了一段时间,在那期间,我对远程工作做了大量的思考。

所以,本文中很多观点是基于个人经历与思考,我建议你结合自己的实际情况,批判阅读。

远程工作是什么样的?

对于我自己来说,远程办公的一天大概是这样的:起床,到书桌前办公、午休到楼下吃个饭、到书桌前办公、下班到楼下吃饭、业余生活(玩玩电脑、玩玩手机、看看书、弹弹琴……)、睡觉,基本上每天都是如此,循环往复。每天除了出门吃饭外,其他时间都关在自己的房间里(事实证明,这样是非常不好的)。

从直觉上说,远程办公除了不用通勤之外和线下办公是一样的,但实际上差别还是很大的,请继续往下看。

远程工作的利弊?

说到远程办公,可能会在你的脑海里浮现一副美好的场景:不用忍受漫长的通勤、有放松的办公环境、有大把的业余时间……等等,先停止这样的幻想。远程办公确实会带来一些好处,但是它也有很多“隐藏的坏处”,没有远程工作经历的人,恐怕很难体会到。下面我们来详细分析一下远程办公的利弊:

远程办公的好处:

  • 居住地不受限:想住哪里都可以,只要通电通网就行了,如果你喜欢隔一段时间就换个地方住,那么远程工作欢迎你。这也意味着你可以住在一些生活成本低的地方。
  • 不需要通勤:这是远程办公最大的好处,不需要挤公交挤地铁,通勤的劳累也可以省去,这一点对通勤过的打工人来说一定非常有吸引力。
  • 有更多个人时间:远程工作把通勤的时间都省下来了,这些时间可以拿来陪家人或者做自己想的事情。有一些人是很看重自己的个人时间的。
  • 个人自由度更高:在办公室你可能无法摸鱼,但是远程办公的话,没有人会管得了你摸鱼的,只要你能保质保量把工作做完。

对于公司来说还有这些好处:

  • 可以更大范围内招聘员工:一旦办公地点不局限于某地的话,公司就可以在全国乃至全世界范围内招聘员工。并且,公司可以把这一点当作是一种竞争力,毕竟对一些求职者来说,远程是加分项。
  • 可以节约办公成本:办公室租赁费、办公器材费、水电费等一大堆的费用都可以节省下来了。
  • 可以留住一些员工:有些员工很需要上述的一些好处,所以对于他们而言,公司支持远程办公本身就更让他们更愿意留下来。

远程办公的坏处:

  • 忍受长时间的孤独!:远程办公中的心理问题是很多人都会忽视的。如果你孤身一人生活,远程工作就让你被迫忍受长时间的独处。警告,这很容易让人患上抑郁,如果你发现自己有这样的迹象,请马上做出改变。
  • 交流成本很高:有人会说,远程工作和面对面是一样的,只不过把对话放到了线上而已。但实际上不是的,在线会议远远比不上面对面的交流。同事们同步状态的滞后性也会带来很大的问题。并且更严重的是,长期远程交流会导致人与人之间信任感的丧失,这会导致合作的崩盘。
  • 工作和生活混在一起:居家办公会让人无法把工作和生活区分开来,这个非常好理解,你可能正在吃饭的时候,同事那边突然要找你开会了。久而久之真的会让工作把生活给侵蚀掉。
  • 逐渐丢失作息规律:虽然远程工作让个人感觉有更多的自由,但是大多数情况下不是好事,“放养”会导致懒惰滋生,作息规律混乱。
  • 人际关系无法拓展:每一份工作中遇到的同事都是并肩作战的人,你会在工作中收获友情。但是,远程工作会让你和同事成为网友关系,这种关系非常淡。甚至长期无法和同事老板见面,会让你产生疏离感。
  • 需要自己营造工作环境:如果你线下办公,那么直接去办公室工作就行了,但是远程办公的话公司不会给你提供办公的环境,你要自己去营造这种工作的环境。对于有家庭的人来说,这还真的不是一件简单的事情。

对公司来说的坏处:

  • 管理成本增高:在远程办公的情况下,员工对于管理者来说变得更加地“不可见”和“不可控”了,这会进一步地带来混乱,管理者将会付出更高的管理成本来对抗这种“熵增”。如果一个公司没有做好管理工作,那么失控就是一个超大概率的事件。

所以总结下来,你要看到的部分,也要看到的部分。

虽然远程办公是未来的一个大趋势,但这并不表示你当前的状态就适合远程办公。远程办公有它的好处和坏处,我们一定要根据实际情况多多分析,在某些实际情况下,上面说的有些好处和坏处都不存在了。在大多数情况下,我的建议是谨慎选择远程工作

在我看来,目前只有少部分工作真正适合远程办公。只有那种不需要员工到现场,而且不太需要团队协作的工作才真正适合远程。一个主流的观点是软件行业适合远程工作,但是我并不赞同,因为软件开发是一种要高度合作的工作,远程工作一定会因为交流上的问题而牺牲掉开发的速度和质量,最终甚至可能丢失掉市场。只有那些没有经营压力的开源项目才适合远程工作。

另外有人会问,创业公司可不可以远程工作?我给出的建议是 NO!原因有以下几点:

1、创业要求团队进行高密度的沟通和非常迅速的反应,远程办公很难满足这样的需求。
2、创业团队需要有凝聚力,远程办公就像是和网友合作,团队难有凝聚力。
3、几乎没有通过远程工作打造出优秀产品的案例。

你可能会说某些创业公司就是远程工作,比如说 37Signal。但是,你要知道幸存者偏差,很多能保持远程工作的创业公司本身就很特殊。要么是几个创始人本身就长时间一起线下工作过,已经建立了牢固的信任;要么就是这个创业公司不是真正的创业公司,它已经有稳定的业务了,只是规模还非常小。

另外一个常见的问题是应届生(刚参加工作的年轻人)适合远程办公吗?不好意思,我的建议还是 NO!我强烈不建议刚参加工作的人去远程办公。为什么呢?

因为年轻人尚处于一个高速的学习提升期,最好还是走入办公室,和别人面对面地交流,在职场中和他人建立友情(很多人都在职场中找到了知己)……而不是天天把自己关在家里。另外一点,观察别人(实体的榜样)的工作方式是提升自己的绝佳方式,但如果你选择了远程工作,那也就缺少了这种观察学习的机会,别人更没法纠正你或给你建议。

什么人适合远程工作?

所以到底什么人适合远程工作呢?

如果非要总结一个标准的话,那就是不需要被管理的人最适合远程办公。每个人都会觉得自己不需要被管理,但事实上,只有极少人真的不需要被管理。如果你非常喜欢自学,对自己有清晰的规划,不需要别人督促就知道要做什么,那么你可能就适合远程办公。

从另一个维度来看,已经成家的人比孤身的人更适合远程办公。因为远程办公可以更多地顾及到家庭,并且家人(伴侣)的陪伴也能让远程工作不那么容易带来心理健康的问题。

从个人能力上来看的话,非常擅于远程沟通的人,也比较适合远程办公。

如何更好地远程工作?

如果你不得不远程工作,那么接下来,我将从提出多个方面的建议,目的是尽量减少远程工作对你的负面影响。

当然,这下面说的很多都是知易行难的“大空话”,但我还是希望说出来,万一对你有帮助呢?

打造舒适的工作环境

选择一个好的办公地点,因为环境是真的可以影响工作效率的。

你可以在家里办公,但是一定要把家里打造地非常适合办公;你也可以到外面办公,有几个地方你可以选择:

  • 共享办公空间:如果你的公司给你开通了共享办公的位置,那么你可以选择去那儿。共享办公空间既然是共享的,那么一定不会非常安静,旁边会有各种各样的人走来走去,聊天、开会、喝茶,总之就是各种声音。但是共享办公空间的工作氛围会比较好,因为里面大部分都是职场人士。

  • 图书馆:如果你附近有图书馆,那么图书馆就是很好的一个选择。尤其是公立的大型图书馆,这种图书馆一般都会配备充足的桌椅,网络和电源也能覆盖。最重要的一点是图书馆一般非常安静,周围的人也都在埋头做自己的事,这种环境很棒。我有段时间就经常去广州图书馆办公,体验感非常好。

  • 咖啡馆:咖啡馆要分情况看待,有好有坏。但是我个人不建议到咖啡馆里远程工作,虽然有些咖啡馆比较安静,但是氛围始终是放松的,呆的时间长了就没有工作的感觉了。这样看人,有的人就很喜欢放松的环境。

养成良好的工作习惯

我个人对远程工作中养成良好工作习惯保持悲观的态度,坦诚来说实在是知易行难:

  1. 如何让自己更加专心?做好两件事情:有目标和不被打扰。
    • 目标一定要明确,一定要明白自己该干嘛,要非常专注!耐着性子把目标任务搞定。如果你有这种习惯,你会发现你的工作效率越来越高。
    • 工作的时候开启免打扰模式,不然手机会不停地打断你,毕竟每次打断你都要花一点时间重新恢复专注的状态。所以我建议把所有的群聊都给取消掉提醒。不要害怕会有人找你,有人想找你的话 TA 会单独私聊你的。如果你和家人住在一起,那么要和家人沟通好,让他们给你让出一个不被打扰的时间和空间。
  2. 做好时间规划。定好闹钟,该工作时工作,该休息时休息~这样才能保护好自己的工作热情。
  3. 尽量要保持健康规律的作息。远程工作不需要通勤,这很可能会让人变得懒散,晚上纵容自己熬夜,早上纵容自己晚起。

加强工作沟通

沟通问题是远程办公中一个非常大的问题,这也是阻碍很多企业远程化的主要原因。

如果你想认真对待你的工作,那你就要想尽一切办法加强工作上的沟通。

这里有一些小方法:

  • 主动且迅速地和同事进行沟通。很多时候不是物理条件上的困难让我们无法保持沟通,而是心理上的困难。我们不会想动不动就麻烦别人,所以一般遇到问题了就不太会愿意去找别人沟通,而是先尝试着自己解决问题。这是人的本性,但是我们要学会克服,远程工作中很重要的一点就是沟通,不要闷着头做事情。
  • 在找别人沟通之前,把问题想清楚,甚至可以通过文字描述一下问题先,这样才能保持更加高效的沟通。
  • 在工作期间随时保持状态更新,要让别人知道你当前在干什么。可以通过一些软件的“设置个人状态”的功能来达到这个目的。
  • 经常找人闲聊,闲聊可以促进团队的相互了解、感情和凝聚力。线下办公其实也是这样,没有人会一直专注工作,很多时候都会闲聊放松。
  • 尽量在沟通的时候打开摄像头,因为面部表情也能传达很多信息,尽量避免同事之间的信任感的逐渐丢失。

但无论如何都要明白一点,线上沟通远远达不到线下沟通的效果,所以这些做法只是补救,而不是替代。如果你发现自己就是不擅长远程沟通,那也不要太自责了。

做好心理建设

我个人在远程工作的心理建设上就没有做好,吃了很大的亏,我自己非常有感触,所以我想特别地说明这一点。

第一个大问题就是缺乏真实的社交。远程工作中缺乏和同事面对面地交流,所以不太容易体会到一个团队的感觉,常常会有孤独感和疏离感。另外,远程工作者也会因为长期没有和人接触,产生抑郁的倾向。

经常性地给自己做心理建设。业余时间多出去社交,周末的时候出去参加线下活动。比如说我就喜欢参与同城的桌游活动,这一定程度上缓解了远程工作的社交缺乏症。

为了缓解长期个人工作带来的孤独感,可以选择听播客、听音乐和玩游戏。我自己就特别喜欢听播客,聆听别人的声音能给我带来一点社交感。

记得每天都要出去运动一下,运动刺激身体产生“快乐激素”多巴胺,运动对抗心理问题的绝佳方式。每天出去晒晒太阳,晒太阳和运动能产生差不多的效果。总之就是不要一整天都待在屋子里。

如果条件允许,还可以养一些小动物(猫猫狗狗)来缓解社交缺乏症。

最后请注意,并非每个人都能克服远程工作中的心理问题,如果你确定了这一点,请马上做出改变(包括但不限于换成线下办公的工作)!你的心理健康和你的身体健康一样,都是无价的。

丰富业余生活

其实这一点不管是对于远程工作还是线下工作都是适用的。

我们要始终清晰地认识到一点,工作仅仅是实现我们目的的一种手段,它不应该反过来把你的生活给吞噬掉。

远程工作的情况下,人们非常容易把生活和工作搞混在一起。不管是从时间上的混乱,还是空间上的混乱,这基本上都是无法避免的。

所以我建议你给工作和生活划分一个明确的界限。比如说到了晚上六点就不要继续工作了,把所有和工作相关的东西都忘掉,去享受业余生活;或者比如说从图书馆回家之后就不要工作了,做点别的让你开心的事情。

值得一看

最后

随着时间的推移,我还会不断往这份文档里添加内容。

如果你也想为这份文档做补充,欢迎通过任何方式联系我。

我们在工作总会遇到了大小各异的问题,它们暴露了我自身的许多问题,作为对自身能力有追求的程序员来说,我们非常有必要定期做一些反思。

我想把工作中暴露出自己的问题分为两大类,一类是客观能力上的问题,这些问题是需要自己努力去提升的,另外一类是主观态度上的问题,主观问题往往是我们会选择性逃避的,这方面的问题更需要我们重视,并且勤加反思。

下面这些的都是我自己最近反思出的问题。

客观能力的问题

1、排查问题(debug)能力有所欠缺。工作中很多时间不是在做设计,也不是在写代码,而是在排查问题。所以这部分我也做了一个通用的小总结:如何 Trouble shooting?,希望未来随着工作年限的增长,我可以总结到很多更实用的 debug 技巧。

2、行动上缺乏章法。有时候,做一个事情或者遇到一个问题时,没有一套很清晰的逻辑去指导行动,我意识到这样子是无法成为一个优秀的程序员的。所以希望我未来在平时就要多思考,多做总结,形成完整的方法论,来指导行为。

3、基础工具使用不够熟练,比如说 vi/vim、shell,这些都用得很笨拙。我们人类的能力都是由工具所扩展出来的,如果可以熟练掌握一些强大且有用的工具的话,能力的提升自然是不必多说了,工作的效率也一定会提升很多。这一块没有别的技巧了,只好多学多用,提高熟练度。

4、对 Linux 程序理解不够深,比如说高频使用的 systemd。这一方面需要我多阅读文档,另外一方面多思考一个程序为什么这样设计。

5、计算机基础有所欠缺。这一块在长远来看是最重要的,所以要加强学习。计算机基础知识的学习应该贯穿整个职业生涯。

主观态度的问题

1、畏难情绪,遇到困难的事情就想逃避,就想要寻找轻松的解决方案。畏难情绪不是我们的缺点,而是一种人类进化出的天然特性。但是只有克服那种天性使然的恐惧,我们才能不断解决难题,从而不断到达更高的地方。

2、自负心,我们都希望被别人当作是个厉害的人,这种心理是非常不利于我们正视自己的缺点的。它同时也很影响合作精神,这对软件开发这种需要高度合作的职业来说是致命的。我们要承认自己的错误和无知,坦然接受别人的批评,这样才能不断进步。有一份很好的视频可以参考学习:Google I/O 2009 - The Myth of the Genius Programmer,SVN 的两个作者在谷歌 I/O 大会上讲到了程序员的自负心的话题,讲得非常好,可以帮我们理解这种心理。

3、不求甚解,不对一个问题刨根问底,只流于表面,了解到恰好够解决当前问题的程度就不再深入。这样的话,下次遇到差不多的问题,还是不知所措,因为没有深入理解本质。

最后:Stay hungry,stay foolish.

调试占据了我工作中大量的时间,在此有必要做一些总结。

Trouble shooting 是解决问题的第一步,从一个非预期的现象出发,发现产生这个现象的本质原因,发现根本原因后才能釜底抽薪,真正解决问题。

我们可以大致遵循这样的流程来 trouble shooting:

1、要对程序的运作有非常清晰的认识。比如说我们做的监控系统,至少要先理解整个系统的数据流向,每一个环节都对数据做了哪些处理,如何对数据做聚合,聚合出来的数据都表达了什么含义等等,要对自己在 trouble shooting 的程序达到非常了解的程度,不然有可能都不会想到出问题的地方。

2、如果有必要,可以把遇到的问题组织成文档,能帮助自己梳理思路,也便于复现问题。

3、通过一些技巧来缩小问题的范围。

  • 日志是非常重要的线索,大部分的问题查询日志就可以发现。请重视日志。
  • 排除干扰因素,比如说关掉不必要的 services,注释掉没必要的配置等后,再去排查问题就会更清晰。
  • 使用正确的工具,包括但不限于监控工具、调试工具、网络工具,比如说 strace 来追踪系统调用的情况、ps 来查看进程运行情况、tcpdump 来抓包等。
  • 如果要分析代码执行的问题,可以使用断点/单步跟踪调试,Java 可以在 IDEA 里很方便地断点调试,Go/C/C++ 可以使用 GDB 这样的 debugger。
  • ……

4、当获得初步的错误信息时,比如说一条报错的日志。大部分情况下就知道怎么解决问题了,不知道的话我们还可以去谷歌搜索这条信息,也许之前就有人讨论过这种问题,看看别人是怎么说的。也可以去查文档,很多问题在文档里都有清晰的描述。

5、实在找不到问题所在,就要查看源代码或者请求别人/社区的帮助。

6、问题被发现后要及时用文字总结,这样才能在工作中有所成长。

参考资料:

软件架构是关于如何组织软件的各个部分的学问,合适的架构是软件成功的最重要因素之一。

如果你去研究一些千万行代码量的项目,你会发现其架构都是很简洁的。只有懂架构方面的知识,我们才能排除那些杂七杂八的东西,对软件有一个整体性地认识。

在经典著作《Unix 编程艺术》中有一个观点,人类只能记住七个点,再多一点,人的思维就会开始混乱。同理,当我们去理解一个复杂的软件时,我们应该拨云见日,刨除那些不重要的,直击其架构。

本文会介绍常见的软件架构,并会在每种架构里,列举一些案例以供大家进一步了解学习。本文在撰写的过程中参考了很多文献,我会在文末列出。

分层架构

分层架构(Layered architecture)是最常见的软件架构。分层就是把不同的功能放到不同的层级中,层级之间互不干涉,它们通过交互来完成一个复杂的任务。

比如互联网应用的后端中最常见的展示层-逻辑层-数据层三层架构。

又比如网络协议栈,这也是一个典型的分层架构。

微内核架构

微内核架构(Microkernel architecture),又被称为插件架构(Plugin architecture)。

其实 Linux 就是微内核架构,微内核架构对内核的代码质量有极高的要求,因为内核作为“功能枢纽”的作用。看看 Linux 内核就知道了,开发者们持续对其进行了大量的优化。(但其实这个也是有争议的,Linux 这个项目已经大到难以区分是宏内核还是微内核了。)

很多操作系统内核都是典型的微内核架构。

例子:

有一个很出名屏幕录制开源软件叫做 OBS Studio,如果你想要获得某些额外功能,那你就需要下载插件,插件就是.so 文件,直接把这个文件放到对应目录中,那么 OBS 就会自动加载该插件,进而把插件的功能提供给用户。这就是典型的插件架构。

在这里我想说一下插件架构。你会发现,很多平台型的应用都是采用了插件架构,比如说 WordPress、Eclipse IDE、Visual Studio Code,这些应用已经超越了一般的专有应用转而成为一个平台,很多开发者专门为其开发功能插件。这也是插件架构的一个好处,那就是可以维护出一个开发者生态。

事件驱动架构

事件驱动架构(Event-driven architecture,缩写 EDA),也被叫做隐式调用。

顾名思义,就是通过事件来驱动整个软件的运作的。可能这样说并不好理解,但是你看一个图就明白了。

事件驱动架构的例子非常多:

另外,以太坊的本质也是一个由事件(交易)驱动的分布式状态机(账本)。这个是不是也算事件驱动架构呢?

再另外,如果你对操作系统比较熟悉,那么你会发现,操作系统本质上也是一个由事件(中断)驱动的死循环。

面向服务架构

面向服务架构(Service-oriented architecture,缩写 SOA)

适合于面向服务架构软件的消息传递方式:

  • SOAP:SOAP(以前称为“简单对象访问协议”)是用于在分散的分布式环境中交换信息的轻量级协议。
  • RPC:远程过程调用,像调用本地函数那样调用远程的服务,比较知名的gRPC就是基于HTTP来实现的。
  • REST

企业服务总线架构

企业服务总线架构(Enterprise Service Bus architecture,缩写 ESB)是面向服务架构的一种。

微服务架构

微服务架构(Microservices architecture)是面向服务架构的升级。

每一个服务就是一个独立的部署单元(separately deployed unit)。这些单元都是分布式的,互相解耦,通过远程通信协议(比如 REST、SOAP)联系。

微服务是近几年非常火的一种软件架构。因为它有很多优点,比如说软件之间的耦合性很低,团队可以分开工作,可以分不同的机器进行部署,大大扩展了服务的容量。

MVC 架构

MVC(Model–View–Controller)架构在图形应用中被大量采用。比如说运行在手机上的 App 客户端软件就大量采用这种架构模式。

MVC 还有一个改进的版本叫做 MVVM,它用 VM 来代替 C,这两者有一些细小的差别。

MVC 架构有很多例子:

云架构

云架构(Cloud architecture)主要解决扩展性和并发的问题,是最容易扩展的架构。

这种架构在云原生组件中经常可以看到。

单体应用

当然了,还有最简单的就是单体应用(Monolithic application),那就是把所有功能都放在一起。

参考文献

写作是一件很有趣的事情,也是我们一生中都不可避免的事情。

写作本身就是一种表达和创作的过程,无论是图文、音频还是视频,这些内容表达形式的本质还是文字创作,也可以统称为写作。

在现如今这个人人都需要表达自我的时代,写作实在是太重要。

接下来分享几条我总结的极简写作方法论。

1、写提纲

从小老师就教我们写提纲,写提纲是很自然而然的一件事情。

写提纲是一个辅助你思考的方式,它可以帮助你快速组织思路,检验文章的内容是否完整、逻辑是否合理。这其实也是在进行小成本试错。

提纲是文章的骨架,基本上,一篇提纲就能大概决定这篇文章的走向。

2、多线并进

以前我认为,写作应该专注于每一篇,写完一篇再开启下一篇,这样才不会造成烂尾。

实际操作就会发现这很理想化,当我长时间专注于写某一篇文章时,很容易就感到疲劳。

人无法对一件事情长期保持注意力,因为这根本就不符合人脑运作的规律!

我们人脑的思维活动是跳跃性的,人一天在不同时间会想不同的事情,林林总总千差万别。

所以一直对着某个主题去写容易让人疲乏,所以我不再这样做,转而变成多线并进的模式,同时开几个不同的主题进行写作,充分发挥人脑的潜力。

多线并进的另外一个好处就是便于捕捉生活中的灵感,因为我们同时写多个主题的文章,所以我们在生活中产生相关联的灵感就更多了,并且能把灵感运用下来的机会也就更多了。

所以,对于我来说最好的做法就是多篇文章同时进行。

3、多写多删

世界上真的有那些在写作上一气呵成并且写得很好的人,比如说鲁迅。根据鲁迅夫人的回忆,鲁迅在写文章之前喜欢泡一壶茶,躺在摇椅上闭目酝酿一上午后才下笔,下笔基本上一遍就过,不再修改。

但是要认识到,鲁迅写文章轻车熟路的背后,离不开他从小打下的文学功底,以及在下笔前的深思熟虑。像鲁迅这样的水平,我相信很少有人能达到。

所以我建议多写,脑子里很多想法,有很多灵感,不知道如何组织语言写下来?那就先动笔写写看,只要是能写的,想到的,都写下来,有好的例子,好的比喻,也赶紧写下来。很快你的文章就会有了大体的架构和行文思路。

另外一个建议就是多删,多写让你的文章变得充实饱满,而多删则是让你的文章变得精炼。多删就是一个打磨的过程,你想让笔下有精品,那么这个过程是不可缺少的。

删除的过程中,迫使你思考哪些部分是精华,哪些部分是废话,哪些部分客观,哪些部分偏离事实。

4、酿稿

“酿稿”这是我自己发明概念,意思是把稿子放上一段时间再去修改,如此往复,直到稿子“酿造而成”。

第一次把稿子的大体写好之后,就把它放一段时间,可能是几天,可能是十天半个月,总之要等到差不多忘记它为止。

当你再次打开稿子时,你会有全新的感觉,通读一遍感受。此时此刻你会发现有很多地方值得改进,OK,趁现在改进,把能改的地方都改了。

改完之后,再放一段时间,再去改,如此往复几次,你就能得到一篇还不错的文章。

“酿稿”的精髓在于等待一段时间,让记忆淡忘。

另外,当文章发布后,还是应该隔一段时间来读一读,一定会发现很多能改进的地方。

5、利用好科技

既然生活在二十一世纪,那就要与时俱进,利用好科技产品,把生产力提满。

有长远的写作计划?那一定要选择一款合适的文章管理工具,比如说印象笔记,Notion,Bear 等。除此之外,还有一些重要的科技要掌握,比如搜索引擎、语言输入、思维导图软件、剪切板工具等等。

你不但要知道使用这些工具,还要花点时间研究如何最高效地使用这些工具。

只有学会熟练使用科技,才能事半功倍,才能方便把想法转化成有价值的内容。

当我还是个学生的时候,我就明白了一个道理:学习要讲究方法。掌握了适合自己的学习方法,事半功倍。

在没有互联网的年代,知识是有屏障的,你想学个东西,但你求知无门。教材非常稀有,想要找到好的教材,非得费一番功夫不可。

但我们有了互联网。

在以前,你能不能在竞争中胜出的关键就是能不能找到那本“葵花宝典”,找到了你就比别人厉害,但现在人人皆可轻易获得“葵花宝典”,此时比的就是学习的能力,看谁能把宝典里的招式学得又快又好。也就是说,决定一个人竞争力的因素中,学习能力占据的比重是越来越大的。

所以我认为,学习能力,作为当代人的核心能力,值得我们用心总结和提高。况且,学习贯穿了我们的一生,掌握好的学习方法也能使我们受益终生。

这篇文章包含了我平时总结的学习方法,我写得并没有太明显的条理,只是一条条地列出来,以方便经常拿出来回顾。

兴趣是基座

主动学习和被动学习的效果天差地别。

如果发自内心地去学习某个东西,那么你会调动全身的细胞,投入到学习中去,你的神经会被充分调动起来。

没有兴趣,谈何学习?要把一个东西学好,兴趣是动力的根源,它就好像是火车上的的锅炉,没有了它,火车就没法跑起来。学习的第一要务就是激发(或培养)自己的兴趣。

但这并非那么有迹可循。

根据我的观察和总结,一般兴趣是和动机绑定在一起的。比如说很多人学习编程的动机是为了赚钱,这种动机也是可以很好地促进学习的兴趣的。要么就是一个人发自内心地认为自己要学的东西是很有用的,甚至是很有意义的,学习中能体会到很多获得感,这样也能促进学习兴趣。

做学习计划

这是时间管理的一部分。

对于那些需要长时间来实施的事情都需要一份计划,计划不但提醒我们瞄准重点,还让我们对事情的进展有清晰的认识。没有学习计划“脚踩西瓜皮,滑到哪里算到哪里”,这是很不好的。

可以先做一份粗略的计划,当然,在做事情前拟定的计划大概率不是一个好计划(因为必然有一些因素没有考虑进去),所以在开始做事之后,要经常性地、谨慎地调整计划。

计划不是纪律表,要灵活变通。

如果没有计划,很容易忘记学习的重点,也很容易半途而废。半途放弃是学习的大忌。

持之以恒与循序渐进

学习是一个长期且持续的过程。

学习最忌讳的就是“三天打鱼,两天晒网”。当我们学习一个稍微有难度的东西时,所需的时间甚至可能高达几百个小时,哪怕我们每天都学习两三个小时,时间一长我们也就自然就掌握了。

如何才能做到持之以恒呢?上面说的兴趣是一种,只要有兴趣才能持续学习下去。

费曼学习法

这是一种符合人脑运作规律的学习方法。

费曼认为,他如果不能把一个问题简化到新人的理解水平,那么就表示他自己也没有搞懂。

费曼学习法的核心理念是“教别人可以促进自己学”,因为你如果能把别人给教懂了,说明你自己也就真的弄懂了。当你学会一个新知识时,你可以试着去教别人,看看别人是否能听懂,以此检验自己的理解。如果在这个过程中发现了你也说不清楚的地方,那正好可以查漏补缺。

费曼学习法蕴含的另外一个建议是“学习了之后要马上输出,及时的输出会加深我们对刚学知识的理解”。当我们吸收新知识时,进入我们脑袋的知识都是零散的,并非成逻辑体系的。你可以用文字的方式去组织这些知识,把它们有条有理地写出来(其实和教别人是一样的,都是在做输出)。

关键就是记住一句话:学习要输入和输出相结合,不能只是一味地输入。

问题驱动学习

有一种高效的学习方法叫做基于问题学习,简称 PBL(Problem-based learning),我喜欢叫它问题驱动学习

PBL 是一种学习方法,更确切地说是一种思维。它的核心思想就一句话:围绕着解决问题地去学习。

设想一下,正常的学习方法是怎么样的?往往是先花很多时间学知识,然后做一些“小实验”感受一下知识的实际应用,之后,如果碰到了实际问题了再用这些知识去解决问题,如果没有碰到,那一段时间之后就忘掉。这是学校教给我们的,它没有什么问题,只是效率很低下。

学校教我们的学习方法是为了学习知识而学习,这样很容易会让学习者陷入漫无目的的境地。导致一些常见的问题:

• 我学了这些能干嘛?我有必要学这些吗?
• 看不到有什么收益,我一点学习动力都没有。
• 前两天才学的东西,怎么一下就忘了?
• 我感觉学得很慢,感觉自己很笨,一学习脑子就犯困。
• ……

更好的方式是 PBL,PBL 在一开始就是设定好要解决的问题,然后边学习知识边把问题解决掉,整个过程都能感受到强烈的收获感,还能把知识技能学得更加牢固。

举个学习语言的例子:正常的学习方式是学习语法,背单词,看文章,最后找人交流。而 PBL 是掌握基本的语法和词汇之后直接去找人交流,然后发现交流不太流畅之后,再来针对补充学习一些表达,再去和别人交流,再回来补充学习,如此往复。这样不但能很快感受学习的收益,还能充分调动学习的积极性。在很多情况下,你会发现,PBL 比正常的学习方法好多了。

其他也是一样,比如你要学习编程,可以先设定一个“开发具有 xx 功能的 xx”的目标,然后为了实现这个目标去学习,而不是一上来就啃“xx 语言教程”。你迟早会在实现“开发具有 xx 功能的 xx”的过程中去啃“xx 语言教程”,因为你发现你不得不啃“xx 语言教程”,这时的啃才又快又香。

另外,正好说到这个问题了,为什么创业者的成长非常快呢?就是因为创业者每天都在遇到实际问题,然后会被迫/主动地通过学习知识去解决这些问题。创业者不知不觉就在应用 PBL 思维了。

当然了,不要打着 PBL 的旗号,去功利地挑选知识去学习,PBL 的核心还是通过着眼于解决问题,激发学习主动性,感受学习带来的收获感,从而提升学习效率。

看视频与读书

在教材媒介的选择上,我大概发现了两类人,有些人喜欢通过看视频来学习,而有些人则喜欢通过看书来学习。

但我总结了一条适合大多数人的规律:入门的时候适合看视频(或听课),进阶的时候适合看书(或结构性的长文)。

看视频和学校上课很像,它们都是最直观的学习方式。让懂的人教你,他们知道哪些地方是重点,哪些地方是难点,要注意哪些问题等等。这些很小的点拨对初学者来说是很关键的,可以让他节省很多不必要浪费的时间。而对于需要进阶的人来说,他已经不是那么容易找到老师给他讲课了,这种情况下就要去看书了,在书中去和那些最专家的人对话,这样才能把自己知识的深度继续延伸。

对于有一些人来说,学习就等于啃书,但这种学习方式或许不适合新手。因为啃书会遇到很多新的概念,这不断让新手受挫。最好还是听懂的人给你讲解,或者找一些提炼性的文章来阅读,再或者找一些入门视频,跟着视频动手做一做。这样你才能脱离繁杂的文字,抓住事物的核心,先对事物有一个大概的认知。这叫做“只见森林,不见树木”。

等到你需要精进的时候再去阅读一些专业的书籍或文档。

读书的要点

阅读书籍自古以来都是人们学习知识的主要手段。书籍非常便于记录成体系的知识。

我总结了一些关于读书的思考:

读最好的书

人生苦短,要读就读最好的书,别把时间浪费在那些次等的书上。

读最经典的书,尤其是思想的提出者/科学技术发明者的书,如果要读科普书的话,也要读科普大家的书。

各种各样二次解读的书,可以不看。

华罗庚读书法

在读书的流程上,我比较推荐华罗庚读书法,也就是“从厚到薄”读书法。(当然,华罗庚说的书更多的是科学研究方面的书。)

大家可以参考这篇文章,看看他本人是怎么说的:

一本书,当未读之前,你会感到,书是那么厚,在读的过程中,如果你对各章各节又作深入的探讨,在每页上加添注解,补充参考材料,那就会觉得更厚了。但是,当我们对书的内容真正有了透彻的了解,抓住了全书的要点,掌握了全书的精神实质以后,就会感到书本变薄了。愈是懂得透彻,就愈有薄的感觉。这是每个科学家都要经历的过程。这样,并不是学得的知识变少了,而是把知识消化了。青年同学读书要学会消化。我常见有些同学在考试前要求老师指出重点,这就反映了他们读书还没有抓住重点,还没有消化。靠老师指出重点不是好办法,主要的应当是自己抓重点。

“厚”在于我们在读的时候不停往里面做注解、补充参考材料;“薄”在于我们对全书有了透彻的理解之后,心中觉得这个书薄了。

系统性阅读

我们现代人的阅读习惯是越来越碎片化了,一天中大部分的知识可能都是源自网上零零碎碎的文章。

究其原因,一方面是我们的时间已经被各种事情切分成碎片,自然就适合碎片化的阅读;另外一方面是我们在这种快节奏的生活下,心态略有浮躁,缺乏耐心。

然后,碎片化的阅读无法让人构建成体系的知识,所以,我推荐系统性地阅读,找几本该领域或该主题的好书,花一段固定的时间来读一读,如果每天的时间不允许,那就把这些书拆分为碎片,填充到生活的空闲中去。比方说我每天只读书中的一个章节,日积月累也能达到系统性阅读的效果。

好书反复读

许多人(包括我自己)对读书有一个误解,认为读书就像吃饭一样,读一本书就能吸收一本书的营养。这就导致一种心态,那就是非常在意自己“读了多少本书”,非常在意这个具体的数量。经常在朋友圈就能看到有人说,“这个周末又读了两本书”、“今年的计划是读 50 本书”,这就是这种心态的典型表现。

但是我们好好想一下就知道了,读书并非吃饭,我们每次一次的读书都只能消化书中的一小部分,知识在传递到过程中会大比例地衰减,真正化为读者所有的,恐怕寥寥无几。

所以,读书要反复阅读,反复吸收。把一本好书阅读一百遍,比读一百本书可能都更有收获。当然,值得我们反复读的一定要是好书,那些经得起反复咀嚼的书。这个反复读的过程也是需要经历时间的沉积的,并不是说读完一本书又马上再读一遍,机械地去累积次数,而是读完之后放一段时间,在生活工作中品味一下书里的内容,然后一段时间后再去读,这样才好读出另外的味道。所以我推荐,可以时不时把好的书拿出来翻一翻,温故而知新。

总结起来就是,我们应该追求读书的质量而不是数量,丢掉“吃饭式”的读书思维。

找最棒的教材

与上所说“挑最好的书来读”一个道理,这也我秉持的理念:拿最棒的教材来学,事半功倍。

尽量在你力所能及的范围里,找到最好的教材。

感谢互联网,现在只要你稍微做一些检索工作,就能比较轻松地找到世界级的优质教材。

有时候想学习一个东西,于是上网找了资料来看,看了半天发现很吃力,看不懂,产生抵触心理了。运气好会话能找到很好的教材,这种教材都会循序渐进地安排内容,编写教材的人也很专业,跟着这样的教材来学,会学得很有成就感。

技术的原作者写的书,理论开创者的书或者论文,反正不管怎么样,秉持着一个原则:找到第一手的资料。

除了第一手的资料外,还有一些非常好的书,比如说一些经典的科普书籍,虽然作者不是理论的开创者,但是他却能把一个复杂的事情说清楚,那么这种书也是非常好的书。

重视基础知识

作为程序员,我就拿我熟悉的领域来举例子。

软件开发的基础知识大概有这些:操作系统、网络协议、编译原理、算法与数据结构、硬件原理等。

如果一个程序员对这些基础知识掌握扎实,那么他将会很快上手应用层的开发,做事情也会又快又好,也不用担心技术发展太快而跟不上。但是许多程序员更愿意花时间去琢磨编程语言特性、开发框架这些东西……这些都不是编程中最本质的东西。这些东西只要稍微一变化,你可能就搞不懂了,付出的时间也就相当于浪费掉了。

所以,我们要充分学习那些经年不变的东西,也就是那些基础知识。

从投入和产出比来看,学习基础知识其实是一笔非常好的投资。

连接现有知识

《贪婪的荷尔蒙》里说过一句很好的话,它说:创意就是把脑子里不同的事物联系在一起。

这是一种对创意的本质的解释:创意就是把不同的事物联系起来。

大部分的创新都是对现有事物进行组合产生的。

久闻此书大名,恰值春节假期,找出这本书来学习学习。

我在此简单地记录读书笔记,便于经常回顾:

  • 程序是写给人看的不是写给计算机看的。我们在写代码的时候不要只是满足功能,而是要尽量让人可以很快地理解程序。毕竟,几个月后维护代码的那个人很可能是你自己。

  • 在写代码之前,要做充分的设计,看看自己能不能用自然语言把程序要做的事情给描述清楚,如果可以的话再写代码,这样设计出来的程序可维护性更高。

  • 遵循可扩展的原则,在做程序设计的时候,尽量给程序留有升级改造的余地。不要高估自己当时的设计。

  • 遵循 KISS 原则,这是 Unix 编程艺术的灵魂,设计要简洁,复杂度能低则低。

  • Unix 的成功源于对一些原则的遵守,这些原则这和禅道一样,把千言万语浓缩成几句话。

  • 尽量用程序去生成程序,就像编译器所做的那样。因为每次都让人去写代码是不可靠的,人是会犯错误的。

  • 保持软件的透明性,容易让人理解的代码才更不容易出错,才更可能优雅地向前发展。优雅的代码不仅将算法传达给计算机,同时也把见解和信心传递给阅读代码的人。

  • 编写透明、可显的系统而节省的精力,将来完全可能就是自己的财富。

  • 宁愿抛弃、重建代码也不愿修补那些憋脚的代码。

  • 尽管线程没有快速转换进程上下文的开销,但是锁定共享数据结构以防互相干涉的开销同样昂贵。也就是说,“线程比进程更快”这个表述并不一定是正确的。

  • 在优化程序之前要有证据证明它确实可以优化,不要依赖于直觉而是依赖于 Profiler 工具。

还在更新中……

前言

大家或多或少都掌握了一些学习英语的方法,但可能还是学不好英语。其实没别的,主要的原因是没有体会到学习英语的乐趣

我注意到身边一个现象,有些朋友的日语比英语还好,甚至可以做到无障碍看日本综艺。据我分析,他们之所以能把日语学得那么好,其实别无法法唯兴趣二字而已。很多人学习日语的动机是喜欢日本文化,希望学习日语后能畅快地看日漫、看日剧、玩日本游戏……这就是兴趣促进学习的巨大力量,同样地道理也可以放到英语上。

所以我希望这篇文章可以激发大家对英语的兴趣,以及意识到学习英语给自己能带来的巨大收益,而不仅仅是告诉大家一个学习的方法。

我的故事

我是中国应试教育体系中的一员,应试教育不但没有激发我学习英语的乐趣,反而让对英语很反感。在我还是个学生的时候,我常常宁可学习一个小时的数学也不愿意学习十分钟的英语。当时最喜欢抱怨的一句话就是“等老子当了教育部长后一定要把英语给取消掉”。

后来要高考了实在是没有办法,英语那么差去不了理想的大学,只好强迫自己去采用应试教育的那套:背单词、背语法、刷题,就这样强行地去提升英语成绩。这样虽然能在卷子上拿个不错的分数,但我明白,我根本不会使用英语,我说不出一句流畅的英语句子,阅读起来还是磕磕碰碰,写作就更不用谈了,好久才能憋出一两句语法正确的话来,非常糟糕。我依旧讨厌英语,因为那时候的我根本意识不到英语的重要性,这也是应试教育中的败笔,无法让学生真切体会到学习英语的重要性。

我对英语的态度转变发生在大学时。

我获得了一个去国外大学参观学习的机会,目的地是纽约。那段时间的安排比较充实,哥伦比亚大学、康奈尔和雪城大学三所美国的顶级院校都被安排参观学习了。(见下图)


哥伦比亚大学


康奈尔大学


雪城大学

在当地,我被迫用憋足的英语去和当地人交流,去超市买东西、和大学老师交流、看英语的课程资料。在任何地方所见皆为英文……

虽然在使用英语方面我还是那样磕磕碰碰,但这是我第一次欣喜地感受到英语如此有用。那段时间的我的英语水平提高了不少,但是对我来说更重要的是我意识到了“我应该好好学习英语“这件事情。

站在曼哈顿的街头,我感受到了一种我生活的地方所远远比不上的文化繁荣和包容。你无法在地球上任何一个角落看到那么多人种的人在街头行走。我慢慢感受到这个国家在各个领域超前的发展,意识到别人还有那么多的好东西值得我来学习。在美国的这种开放包容的文化氛围下,会让人产生一种近乎本能的学习欲望,就好像看见了“好东西”自己也非常想拥有的那种感觉。


曼哈顿某路口


布鲁克林大桥


纽约时代广场


尼亚加拉大瀑布

总结下来,这次美国之行,在语言方面我有两个最大的收获:意识到英语的重要性,以及感受到英语背后的文化的魅力

回到中国后,我就有意识地练习自己的外语。在外网注册了各种社交媒体的账号,Facebook、Twitter、Telegram、Reddit,开始关注国外的新闻,了解国外的人每天都在做什么,参与到国外的线上社区里……也喜欢上了油管,当时第一次接触感觉很有意思,从来没有在一个地方能接触到那么多优质内容。

我也喜欢上了一些英语世界的文艺作品,比如记得很清楚的是 Coldplay 的一个专辑,叫做 A Head Full of Dreams。这张专辑在我刚回到中国那段时间整整听了两个月,现在依旧很喜欢。


Coldplay:A Head Full of Dreams

除此之外,我还养成了用英文进行检索的习惯,这这个习惯让我收获颇丰。一开始搜索出来一些英语文章,根本看不懂,但总会耐着性子把文章看完。随着这样的次数增多,我从害怕阅读英语文章状态,逐渐转变为了主动找英语文章来看的状态。甚至更进一步地,我会找英语书籍来阅读……我发现好像这个转变过程没有想象中的那么难,也没有我想象中的那么乏味……

学习语言是一件简单快乐且充满魅力的事情。

从我的故事看来,我之所以后来愿意拥抱这个我曾经讨厌的学科,这个转变的背后主要有两点原因,我在前面也提到了: 我体会到了学习英语的重要性,以及我感受到了英语背后文化的魅力。

体会学习英语的重要性

不管你是否喜欢英语,你都无法否认它的重要性。英语好的人将会有更多的机会。人做一件事情是需要动机的,学习也是一样。在学好英语之前,你要充分地去感受学好英语带来的好处,当你那种学习英语的强烈渴望被激发出来后,你才能畅快地把一门语言给学习好。

我自己是一名程序员,学好英语对我来说就可以直接阅读大量一手的技术资料,光是这一点对我来说就足够有吸引力了。学好英语的另外好处还有很多,我随便举几个例子:可以融入世界上最好的技术社区、可以参与到世界上最顶级的开源项目中、可以和全世界的各行各业的人交朋友、可以无障碍地了解全世界正在发生什么。

如果你对自己的个人能力有一定的追求,那么英语绝对是你值得好好投资时间和精力的。

感受英语背后的文化的魅力

我是幸运的,能够亲身到英语国家里体会过他们的文化。

就像我在我的故事里所说的,我们学习语言不仅仅只是学习语言本身,而且还是在学习这门语言背后的文化,而且文化才是重点。英语世界的文化是很丰富的,不缺少有意思的东西,其中也一定有能让你感兴趣的一些东西。把英语看作是一种工具,用它来开阔自己的视野,用它来扩展自己的兴趣,用它来交朋友。

如果暂时没有机会到英语国家体验,完全没有关系,我们还有互联网,互联网把全世界连在一起。这里我不再多说,大家可以自己想象。

听说读写

好了,说完了我自己的故事,接下来会结合我前面所说的理念来聊点实际的学习方法。

我不是专业的英语教育人员,但我亲身经历了整个学习的过程,可以站在初学者的角度给出一些接地气的、可以实际操作的、不同应试教育的“野路子”方法。

学习英语要注意训练自己听、说、读、写四方面的能力,缺了哪一项都不能称之为是英语学地很好,所以我就按照这个分类来逐个说。

阅读

要想熟练地阅读英语文章,前提是词汇量要达量。

应试教育告诉我们要直接背课本后面的单词,一天二十个,一年六千个。貌似很好,但实际上很低效,因为背的单词没有被用到阅读里,背了就忘,无效做功。

所以我推荐通过阅读驱动的词汇量增长法

我的做法是直接找想要阅读的文章(一定是想要阅读的文章),比如说我对费曼这个人非常感兴趣,那么我就会找到他的维基百科词条。接下来直接开始读,没错,直接开始读。读着读着你就会发现很多单词看不懂,阻碍你对某个句子的理解。这个时候就把这句话中不懂的词汇挑出来,然后查看它的中文翻译,并把它给记录到生词本里。知道单词的意思后,你应该就理解这句话的意思了。就按照这个方式,要有耐心地一点一点地把这篇文章给读完。可能你会发现,一篇文章读下来之后,你能收集到不少的生词。

这些生词对于你来说就是最高频的词汇,可以偶尔拿出来扫一遍作为复习。

然后,就按照这个方式大量阅读文章,同时记录下生词,你会发现你反复遇到同一批类似的生词,上次已经记录到生词本里了,但是这次遇到了还是不知道什么意思,没关系,翻出生词本看两眼,印象就能加深了。这样反复几次,你就会完全记录某个单词。对了,记得把生词本里一些非常熟悉的词汇给删掉,这个被删掉的词汇就是完全内化为你所有的单词。按照这个方法,要不了多久你就会发现自己的阅读量已经上去了,同时词汇量也见长。

同样地,你也可以把固定搭配(尤其是介词短语)、俚语这些记录到生词本上,用同样的方法内化它。

词汇量的事情说完了,再来看看阅读本身。

我自己的理解就是要有耐心。阅读的过程中逼着自己不要使用翻译软件,耐着性子去读英文,一行一行地把句意完全理解掉。在阅读的时候,尽量忘掉自己的母语中文,不要在脑海中把英文翻译成中文后再去理解,而是直接理解英文。

阅读的材料很多,关键是选择自己感兴趣的。我个人推荐维基百科、原著书籍和杂志,以及所有你所浏览的英文网站(下次打开一个英文网站先不要关掉,不妨读读看先)。维基百科是非常好的阅读材料,因为它涵盖的范围很广泛,一定会有你喜欢的内容,其次维基百科的词条里有大量外链,你可以顺这这些链接找到很多你感兴趣的主题。原著书籍和杂志的话难度会高一些,但是也是值得去挑战的,前期肯定会磕磕绊绊的,但是慢慢地就熟练了,当你可以很流畅地阅读一本英文杂志后,你会非常有成就感的。

差点忘了说,还有一些阅读材料也很好,那就是可以提升你工作能力的书籍。我是一个程序员,我会直接看很多原版技术书籍,我能在学到知识的同时提高阅读能力。

口语和听力

口语和听力是相辅相成的,把它们放在一起说。

其实真没别的,提高口语和听力能力的唯一诀窍就是逼着自己去用

英语口语并不难,只要掌握了几百个基本的词汇,就可以涵盖日常的交流,关键在于要有英语的思维。比如说你想表达自己流鼻涕,你可以说“I have a running nose”,如果没有英语的思维,很难能想出这样的表达。

我们应该借助互联网的力量,给自己创造锻炼英语的环境。比如说我自己就很喜欢用 Discord 和 Clubhouse 这两款语音聊天软件,每天上面都有大量的母语者在聊天,我只要混进去聊就行了。反正就是千万不要害羞,不要怕自己英语不好被人嘲笑,反正隔着网线没有人知道你是谁,怎么折腾都行。也不用过分在意自己的语法是否正确,让对方明白你的意思就行。

再来说说听力,和阅读是一样的道理,应该在使用中提高水平。锻炼听力主要有两个途径:听播客和看视频。

这个虽然看起来比较简单,但是在实际的操作过程中也是非常容易劝退人。有一些注意点:

  • 一定要找感兴趣的播客或视频,把注意力放到内容本身,不要知识为了学英语,不然会很无聊的。
  • 找到和自己能力匹配的播客或视频,保证自己可以理解大部分内容。
  • 看视频可以开字幕,听播客可以放慢速,保证自己可以理解大部分内容。

写作

写作的学问很大,我就说一个我的理解,我认为写作的诀窍就是模仿

模仿英语母语者组织语言的方式,模仿母语者的表达习惯,模仿他们阐述一件事情的方式,模仿他们对不同词汇的使用。

然后就是多混国外社区,注册一个 facebook 账号,注册一个 reddit 账号,逛逛各类垂直社区,看到自己感兴趣的东西就可以多参与评论。在评论的过程中,写作能力自然而然地就会提高了。

总结

上面所说的不管是阅读、口语还是听力,这些方法的核心只有一点:抛弃应试教育那种为了学习英语而学习的理念,转为学习英语是为了使用它,让它给我带来收益的理念。

我们不要成为研究英语的专家,我们要成为使用英语的人。

工具/资源推荐

  • Youtube:上面有很多优质的英文视频。
  • Apple Podcast/Google Podcast:上面有丰富的英语听力资源。
  • Discord/Clubhouse:很多英语母语者在上面聊天。
  • 备忘录:可以用来记录生词。
  • 有道云翻译:日常翻译使用,也有生词本功能。
  • Hacker News:科技创业类垂直社区,可以参与评论。

2022.04.12 补充

上面说到 Discord 这款聊天软件用来练习口语还是非常不错的。上面有一些专门学习英语的频道,里面有来自世界各地的人,他们都想学英语,大家都很能聊,氛围相当不错。唯一的缺点是这种频道里真正的英语母语者不太多,倒是能听到天南地北的口音,一不注意就会被带偏。不过我们可以剑走偏锋,去中文学习频道,那里反而有一些对中文非常感兴趣的英语母语者,本着“互惠互利”的原则,往往都能聊得很开心。

感谢 TG 群友提供的思路,来源

今天在 Twitter 上看到一篇文章:假新闻在社交媒体的传播速度是真实新闻的 6 倍,且假新闻更容易得到转发。这篇报告发表在美国《科学》杂志。研究人员在社交媒体 “推特” 选取 12.6 万则新闻,并由 6 家独立机构核实这些新闻的真实性。研究人员发现,以向 1500 名用户传播消息为标准,假新闻平均传播时间为 10 小时,而真实新闻传播需 60 小时。

我不禁思考,在这个信息爆炸的时代,我们如何才能避免被错误的信息(假新闻)所误导呢?

我认为除非完全不上网,否则很难完全避免被误导。不过,运用一些技巧,可以大幅度降低被误导的可能性。

下面我说一些我的方案(技巧):

1️、看权威性

这一点很好理解:权威性越高的机构,其发布的信息可信度越高。

这也就是为什么我一直一来都推荐阅读权威书籍,订阅权威机构、关注权威人士的原因。

同样的道理,能获得一手的信息,就坚决不要获得二手的信息。比如你要了解一项技术,最好直接看技术发明人写的文档,再不济也要看行业公认专家的文档。又比如你要了解一项政策,直接看政府发布的政策公告,或者看官方的解读。

2️、看引用

很多时候我们会看到一些文章,图文并茂,还列举了很多数据,让人觉得很有信服力。

但其实不然,这些数据可能来源不明,看到这样的文章之前要保持怀疑,因为伪造数据实在是太简单了,如果你轻易相信的话,那么你就很可能成为虚假信息受害者,甚至被别人利用。

比较好的做法是直接翻到文章最底下,看看有没有引用链接,一般来说,一篇真实性强的文章里的数据都是有引用的,尤其是那些支持一篇文章观点的关键数据。

一篇文章的引用量越多,其越靠近事实。

因为一个观点往往需要很多例子、数据、研究来做支撑,这也是检验一篇文章最好的方法——看看引用。

3️、看利益方

文章都蕴含了作者的动机,理解作者的动机,可以帮助你做出判断。

比如说一篇文章在讲获得顶尖公司的 offer 很容易,但是一看文章作者是某培训机构,这个时候你就很容易做出判断了,它讲的事情很可能并不是事实,而是它想让你去参加他们的技能培训。

人的立场、观点,会在不知不觉中被他们的身份影响,这是所有人的无法避免的。

所以当你理解了作者的动机之后,你会看得更加客观,而不是所有内容无脑地全盘接受。

4️、看逻辑

一般来说,随意看上一小段,就能知道一篇文章的逻辑是否经得起推敲。

逻辑错误的种类是很多的,比如:

1、用特例来证明一般性的事实。

2、两个事物存在相关性,就得出一个事物是造成另一个事物的原因。

3、通过激发读者的感情来让读者接受某种观点。

4、用模糊的语义(词语)来混淆事实。

……

逻辑经不起推敲的文章,不能说它都是错的,但是请小心阅读。

5️、看不同人的说法

偏听则暗,兼听则明。

对于一个事物,我们要保持开放的心态,去聆听更多人的看法。

这样才不容易陷入偏执,进而做出正确的决策。

最后,我建议你不断增进对基本原理的认识,比如自然科学原理,经济学原理等。避免相信“水可以驱动汽车”、“天天吃 xx 酸奶就会长命百岁”、“只要努力就能成功”、“属虎的人今年要 XX”这样的无脑信息。

斯坦福创业课:http://startupclass.samaltman.com

中文版视频(B 站):https://www.bilibili.com/video/BV1Zs411x7q7

Lecture 1 - How to Start a Startup (Sam Altman, Dustin Moskovitz)

  • 创业四要素:创意、产品、团队和执行。如果把这几个要素都做好,那就可以说是创业成功了。
  • 为了创业而创业是行不通的,想要致富的话,方法非常多,不一定通过需要创业来实现。创业应该成为你的最后选项。
  • 创业大多数时候不比去打工好,如果你有天赋,完全可以进入一家高增长且低风险的公司工作,然后获得丰厚的回报。
  • 创始人不要光埋着头写代码,而是走出去寻找客户,和陌生人沟通,虽然有可能会被拒绝,但是也要在销售和市场上花时间。
  • 这就是做小公司的一个优势:你可以提供大公司无法提供的服务水平
  • 真正伟大的不是产品,而是作为用户的体验。产品只是其中的一个组成部分。
  • 你从直接与你最早的用户接触中得到的反馈将是你所得到的最好的。
  • 创业人员必须要有长远的打算,一定要会做长远的计划。
  • 确保你的公司没有办法被复制,这一点是非常重要的。
  • 最好的公司都是有明确的使命驱动的。如果创业的点子不够好,很难产生那样的使命。并且,创业是需要长时间(数年甚至上十年)的坚持的,如果没有那种使命感,很难坚持下来。有使命感的话,不是公司的人也会给你提供帮助。
  • 很多好的创业创意在一开始都是不被看好的。
  • 很多刚创业的人都会认为自己的第一款产品必须非常宏大,其实不必,一开始只需要针对特定的市场开始。
  • 大家一定要潜心研究市场发展变化的规律.
  • Why now?为什么这个创意的时机是现在而不是过去也不是将来?
  • 如果你还是学生,你可以关注这两件事情:想一个好的创意和寻找潜在的合伙人,后者更加重要。
  • 好创意->好产品->好公司
  • 先获取一批忠实的用户比获取一大批没那么忠实的用户要好。所以要把目标定位到一小部分消费者上。
  • 很多成功的创始人都会把订单系统和通知软件绑定,如果客户在半夜发邮件过来,也要爬起来快速回复。
  • Pinterest 的创始人在咖啡馆里让陌生人使用他的网站,他还把苹果零售店里的电脑的首页改成了 Pinterest 主页。这就是不按常理出牌的例子。
  • 关注于用户增长,坦然接受用户增长方面的问题,然后找到问题并改进。
  • Do things that don’t scale,这篇文章值得看一下。

Lecture 2 - Team and Execution (Sam Altman)

  • 不一定非要改变世界,可以做一些部分人喜欢的东西出来。
  • 合伙人关系是创业中十分重要的,合伙人之间需要非常地了解对方,找到合伙人最好的方式就是在大学里,或者公司里。
  • 找到一个差的合伙人,还不如不要合伙人。
  • 什么样的合伙人比较好呢?YC 里有一句话叫做 relentlessly resourceful,意思就是镇定坚强、足智多谋的人。比起某个领域的专家,你更需要一个詹姆斯·邦德那样的人。
  • 在早期,尽量不要招聘,努力把员工规模缩减到最小。如果要招聘,尽量招聘最优秀的人。所以经常要花一整年去招聘一个人。
  • 所以建议先有产品再去招聘,因为大家知道这家公司马上要步入正轨了。如果你要找工作,也建议去那种步入正轨即将起飞的公司。
  • 要么不招人,要么就花大功夫去找人。花大概 25%的时间去招聘人,这已经是非常多的时间了。
  • 招人要关注的三个点:他是否聪明?他是否能完成任务?我是否愿意花时间和他相处?如果三个问题的回答都是肯定的,那么这个人大概率是 OK 的。
  • 是否有良好的沟通技巧与招到的人最终是否能胜任工作有很大的关系。
  • 应该计划把公司百分之 10%的股份给前十位员工,这样才能发挥股份的意义。创业者一般都对员工吝啬,但对投资人很大方,这完全搞反了。
  • 你要确定让员工感到高兴并且感到到被重视,要让员工感觉受到公司公正的对待。
  • 你不应该告诉员工他们做得很糟糕,除非你想让他们离开。你下意识地认为大家都应该做得很好,但事实并非如此。这是一个管理上的技巧。你要在每一件好事上都让员工感受到称赞。你不能管得太细,要让员工自己慢慢承担责任。
  • 尽快解雇那些对公司没什么用的员工,对公司好,对员工也好。还要尽快解雇那些搞办公室政治的和持续消极的人。
  • 分股份这件事情应该在决定共事之后尽快决定。一个公司必要要有股份分成的计划,股份成熟制度是还不错的做法,避免合伙人关系破裂而带走过多股份。
  • 合伙人团队应该避免远程办公,视频会议和电话会议的沟通效果是非常差的,而创业阶段的沟通效率是非常重要的。很少有优秀产品是通过远程协作搞出来的。另外一个原因是只有真正的沟通才能带来专注。
  • 创始人是公司的行为模范,创始人可以作为公司的即时文化。创始人应该身体力行,别无他法。
  • 有好想法的人很多,但是愿意把付出努力去把想法实现的人很少。
  • 早期创业公司 CEO 至少有五大工作:制定目标、筹集资金、宣传公司、招聘和管理团队、确保公司执行力。
  • 专注(Focus)是至关重要的。一定要找出最重要的两三件事情,然后专注地去做这两三件事情。
  • 如何专注?三点:经常说 NO,经常重申目标,真诚的沟通。
  • 高强度:只有极度高强度的工作才能使得创业公司成功。除了照顾家庭之外,不能有其他的兴趣爱好。要让公司形成一种对什么事情都保持高质量标准同时还做得很快的文化。如果你不想让他们写出劣质的代码,那就不要给他们买劣质的电脑,让员工在每一件事情上都贯彻高标准。
  • 优秀的创业者很快做出决定,并且非常快速地行动。速度是非常关键的,在不同方面的速度都要非常快,哪怕是回复邮件。
  • 优秀的创业者能够一步一步地解决新的问题。
  • 保持工作节奏:发布产品、发布新特性、周期性地回顾。
  • 不要让公司因为竞争者或媒体而变得焦虑和低落。

Lecture 3 - Before the Startup (Paul Graham)

  • 创业的思维经常与直觉相悖,创业中千万不要一直相信自己的直觉。只要记住这一条,在你犯错误的时候就可以悬崖勒马。
  • 商业中选人和选朋友是共通的,此时要相信自己的直觉。
  • 与自己喜欢和尊重的人共事,最好这个人你已经长期相处过了,不然会被某些人暂时表现出的品质给欺骗。
  • 成功创业需要的不是创业知识,而是懂你的用户。
  • 创业者走过场的情况太多了,也就是形式主义太多了。
  • 不要去想如何说服投资人注资,而是去想如何把企业发展好、如何做出顾客想要的产品。
  • 找诀窍(trick)在创业中是行不通的。
  • 创业是很耗人的,创业会以你难以想象的程度占据你的生活,你要每天处理一大堆麻烦事。
  • 成为一名成功的创业者要经历很多苦难,而且无处倾诉。
  • 作为一个学生去创业是不合适的,因为你要放弃学业去做一件很难的事情。创业很难!
  • 如果你对创业心存恐惧,那就不要去创业!如果你不确定自己是否需要创业,那也不要去创业!
  • 创业之前你需要做两件事情:寻找创意(idea)和寻找合伙人(co-founder)。
  • 获得创业创意的方法就是不要刻意去想创业创意。
  • 找到创意的好方法是让创意无意间闯入脑海,你甚至无法意识到这是创意。
  • 如何让好的创业闯入脑海?第一、学习重要的知识;第二、解决感兴趣的问题,第三、和喜欢与尊重的人一起共事。这适用于找到创意和合伙人。
  • 比懂某项技术更重要的是要擅长组织众人完成任务。
  • 我能做的就是告诉你,不断地提出问题,解决问题。
  • 最初招聘的员工不是上下级关系,而是合伙创业关系。
  • 无论开什么样的公司,其实都会面临一些差不多的问题。

Lecture 4 - Building Product, Talking to Users, and Growing (Adora Cheung)

  • 一旦你有了一个创意之后,你要问一下自己:这个创意是在解决什么问题(一句话说明白)?我和问题有什么关系?别人是否也在受此问题所困扰?
  • 如果你想创办服务类型的公司,那么你要非常了解这个行业,了解这个行业的人都在干什么,同时你要体验整个服务,亲自参与其中。
  • 关注行业竞争对手的信息,使用他们的产品,阅读他们的官网,查看他们公布的一切文件,参与他们的财务发布等等,你总会发现一些有用的信息。
  • 你应该成为一个行业的专家,这样别人才不会对你的产品或服务产生质疑。
  • 定位你的客户群体,为特定客户提供特殊的定制产品,这通常是一种可行的策略。
  • MVP,最小可行性产品,重点是 viable,而不是 minimum。
  • 用简洁的语言来表达产品的优势,比如说:“20 美元一小时,让你的房间洁净如新!”,这样客户才能秒懂,我们也能赢得客户。
  • 谁是你的第一批用户?你自己,你的同事,你的朋友,你的家人,这些人可以成为你的第一批用户。再进一步,在线社区、KOL、当地社区,这些也可以成为你的第一批用户。关键是你要去有潜在客户的地方做推销。
  • 家政平台 Homejoy 的创始人在线下集市,通过发放冰镇汽水来赢得一些早期客户,这也是一个不按常理出牌的好例子。
  • 关于获取用户反馈,首先一定要让客户能够联系到你。但是更好的方式是和客户面对面地交流,注意谈话技巧,要让客户放松下来,这样才能获取到最真实的反馈。
  • 另外一个值得关注的点是用户留存度,可以侧面反应用户对产品的评价,也可以直接看用户的评论,不过要注意评论的真实性,不能把所有的评论都当真。
  • 在自动化之前你要亲手去做。
  • 在创业初期,不要过度追求完美。
  • 尝试不同的宣传渠道,如果可行那就不断迭代这个宣传渠道。同时经常回顾不同的渠道。
  • 不同的增长模式:粘性增长(需要关注用户留存度)、病毒式增长(需要极高的用户满意度)、付费增长(需要资金充裕)、螺旋式增长

Lecture 5 - Competition is for Losers (Peter Thiel)

  • 要创立一个有价值的公司,你必须创造出价值,并且获取该价值的一部分。
  • 这个世界上存在两种截然不同的公司,一种是完全竞争的公司,一种是完全垄断的公司,很少有公司介于两者之间。
  • 用时髦的词语堆砌起来的公司通常不会成功。
  • 很多事实上垄断的大型公司从来不承认自己垄断了,因为不想被政府监管。
  • 如果想让公司垄断市场,从一个非常小的市场开始做起会更好。比如亚马逊一开始只是个书店、ebay 一开始只卖糖果盒,Facebook 一开始只有哈佛大学生用。
  • 小的市场的价值往往被低估了,大的市场往往存在很多竞争者,意味着更难以脱颖而出。
  • 只有那些做着前无古人的事情的公司,才有成为垄断企业的潜力。
  • 成功的公司各有各的特色,失败的公司往往相似。
  • 第一名的公司要比第二名超出一个数量级的优势,不管是技术优势还是什么其他的优势,这样才难成为一个垄断公司。
  • 软件的复制成本为 0,所以软件公司的扩张速度会非常夸张。
  • 成功的公司非常善于把各方面的创新整合起来,虽然没有在单一方向做出巨大的创新,但是在各个方向小的创新汇聚在一起就会形成巨大的竞争优势。
  • 创业的核心思想就是竞争与垄断。
  • 垂直整合对于技术进步的意义还没有被充分重视。
  • 有一种错误的观点:通过创造一个东西赚的钱越多这个东西就越有价值。显然不是,比如说爱因斯坦并没有靠发明相对论而赚到钱,但是它的价值是难以估量的。
  • 但很多人却一直为输赢所困,从而错过了重要的、有价值的东西。我想对大家说的是,不要拘泥于大家都盯着的弹丸之地,也许下一个转角,就会遇到没有人发现的巨大机遇。
  • 精益创业并不是万能药,一切都是看情况使用。很多伟大的公司并没有做太多的用户调研。
  • 谷歌不是第一个做搜索引擎的,但是它做得比之前的好太多。

Lecture 6 - Growth (Alex Schultz)

  • 如果你的产品的用户留存度非常高,那么这个一定是非常好的产品。
  • 如何你没有好的产品,执行增长策略是没有任何意义的。
  • 公司类型不同,判断其用户留存率的标准也不一样。
  • 不同产品的关键指标不一样,社交产品的指标是活跃用户数,电商平台的指标是成交额。团队领导者要让所有人明确自己的关键指标是什么。
  • 创业团队不需要增长团队,整个团队就是增长团队。
  • 在某些产品里,注册用户不重要,长期用户才重要。
  • 如果想在全球范围内发布产品,要尽早做国际化。
  • 强烈推荐一本关于病毒营销的书《病毒循环》,作者的另外一本书《奥格威谈广告》也不错。
  • 病毒营销可以分为三个要素:负载、频率、转化率。负载是一次触达了多少人,频率是单位时间内触达的人,转化率是触达的人有多少个转化为了用户。
  • hotmail 和 paypal 是两个典型的病毒营销例子。
  • 病毒营销要保证 R0 大于 1,这样才能保证有传播效果。
  • 在优化某个关键词之前,要做好调查,这个词是不是和你的网站密切相关的。也就是说搜索这个词的人是不是符合你网站的用户。

Lecture 7 - How to Build Products Users Love (Kevin Hale)

  • 我们如何使用户无条件地喜欢我们的产品、希望我们的产品大获成功(基于对产品的热爱)?
  • 增长率就是转化率和流失率之间的差值,这个差值越大越好。
  • 找到用户就像是约会,留存住用户就像是婚姻。
  • 约会的时候,第一印象非常重要,第一印象关系到成败。第一封邮件、注册流程、登陆页面、第一眼广告、第一次客服服务,这些都是打造良好第一印象的好机会。
  • 日本人用两个概念来描述产品体验:良好的功能性、有魅力的品质。比如一支钢笔写出来的字和给使用者带来的愉悦感。
  • 如果你要吸引别人,你不一定非要从外观设计上做。
  • 如何开发出好的软件?让每个人都做客服工作。
  • 导致创业团队解散的几个原因:批评、轻视、防御、闭关锁国。在婚姻上,冷战是导致散伙的重要原因,合伙人关系中也是一样,对待客户也是如此。
  • 有两种方法可以让用户使用好软件:增加用户对软件的了解,降低软件的使用门槛。
  • We made everyone say thank you. 整个团队一起给用户手写感谢邮件,不但提高了用户的体验,而且还凝聚了团队。
  • 三种不同占领市场的策略:Best price, best product, best overall solution.
  • 早期重点关注那些极度喜爱这个产品的人。
  • 产品的发展方向太多是很危险的,需要很多时间去做验证
  • 抽签让某个员工成为一周的「国王」,让他提出公司里任何可以改进的地方,所有人都要去配合他的工作。(这个玩法挺有意思的,有点民主决策的感觉)
  • 很多问题都没有必要一下子去解决,不妨停下来休息一下,说不定会有很好的思路。

Lecture 8 - How to Get Started, Doing Things that Don’t Scale, Press

  • 不要小瞧小规模市场,小规模市场对创业公司来说反而是好事。
  • 顾客增长没有捷径可走。
  • 创始人需要投入很多精力去找到第一批用户。
  • 支持者是那些会向别人推荐你的产品的那些用户。请把用户转化为支持者。比如说位用户提供难忘的使用体验。
  • 做客服的体验非常不好,但是可以和客户直接沟通。
  • 你也要关注社交媒体,看看用户如何公开讨论你的产品,你要主动出击来维护好产品的声誉。一个诋毁者足以摧毁十个支持者。
  • 对于创业公司来说,一个月就是一年。
  • 不要任性地放弃,直到最后一刻,不然就便宜了还在坚持的其他小公司。
  • 利用媒体宣传自己之前,要想清楚自己的目标是谁,这样才能知道从哪些媒体入手宣传自己。
  • 很多方式可以宣传自己:产品发布、获得投资、里程碑、炒作话题、招募、写行业文章
  • 有一点要明白,作为创始人可能认为你做的事情很有意思,但是从别人的视角来看或许不会这么认为。在宣传自己之前,先想想看别人是否有兴趣去阅读你的故事。

Lecture 9 - How to Raise Money (Marc Andreessen, Ron Conway, Parker Conrad)

  • “这个人是领导者吗?”,“这个人是不是正直、专注、痴迷于产品?”,“是什么让你想做这个产品的?”,“这个人是否有良好的沟通能力?”
  • 一个公司拥有极端优势,通常也有严重缺陷。
  • 努力让一句话介绍变得完美,要让投资人一听就知道这个产品是做什么的。
  • 尽可能地自给自足,如果能不依赖于投资那就再好不过了。
  • 成功的关键就是做到让别人无法无视你的优秀。
  • 你要做的关于产品的事情大多数都比拿投资要难。融资只是微不足道的一步。
  • 洋葱风险模型。创业的过程就是逐步剥离风险的过程。
  • 投资人和创业者之间需要高度的信任
  • 不要再早期出售太多的股份,因为这样留给自己和团队的股份就会很少,这很影响团队的士气。
  • 当你创建公司的时候,你需要找到一个和你一样棒,或者比你更棒的人做合伙人。
  • 如果这个投资人不熟悉你的行业,或者这个投资人没有很好的人脉来帮助你,你很可能不应该拿这个投资人的钱。尤其是那些急切着想着赚钱的投资人。
  • 选择投资人和选择婚姻对象一样重要。好的投资人是在投资人,他会继续投资你做的所有的公司。
  • 有一些投资公司的冲突策略是一个门类只投资一家公司,我个人觉得这个是非常尊重创业者的做法。
  • 拿投资就意味着创业者接受了一些限制条款,也意味着让步出管理公司的一些权利。
  • 如果一切顺利,创始人掌握一切;如果不顺利,投资人会介入。
  • 董事会投票的情况是非常非常罕见的,因为在做决策之前,会经过非常多的讨论和决议,最终的结果是大家一致认可的。

Lecture 10 - Culture (Brian Chesky, Alfred Lin)

  • 什么是文化?为什么文化很重要?有哪些打造文化上的最佳实践?
  • 创始人就是企业文化的代表。
  • 那些可以生存很久的公司都有一个清晰的使命和价值观。产品可以改变,但是使命是不会改变的。
  • 核心价值观。
  • 最初的一批人就是公司的 DNA。所以创始人的招聘要非常谨慎。
  • 我们希望因为使命而来的人,而不是为别的而来的人。
  • 企业文化有可能在短期内导致决策进展缓慢。要权衡是照顾文化还是照顾速度?
  • 在所有地方都不断强调我们的愿景。重复上万次。
  • 有一百个真正爱你们产品的用户远胜一百万个只是有点喜欢你们产品的人。再退回去说,有一个真爱用户那也是很棒的开始。

Lecture 11 - Hiring and Culture, Part 2 (Patrick and John Collison, Ben Silbermann)

  • 招聘那些你喜欢与之共事和与之相处的人。
  • 前十名员工是非常难招到的。要花很多时间去“劝”一些人才去加入。
  • 创业公司招聘和创投很像,就是去发现那些被低估的人才。
  • 一开始招的人里有两个特质是很重要的,诚恳和直率。
  • 要看该人是否能把事情做完而不总是半途而废。
  • 面试的时候要坦率地把该工作的好处和坏处都和面试者沟通清楚了。
  • 有一种招人的方法是叫 ta 过来一起共事一周,在这一周内观察 ta,看其是否可以胜任工作。

Lecture 12 - Building for the Enterprise (Aaron Levie)

  • 要不断寻找变化的要素,如果市场有潜在的大变化,无论是原材料还是促成要素,就预示着市场要有大变化。
  • 普通用户和企业用户的价值衡量方面是不同的。
  • 打造企业软件的过程很漫长,而且你不能有闪失。
  • (在这个演讲之时)上云是一个很大的趋势。
  • 你可以随便选择一个行业,然后放大来看,未来哪些科技要素会发生变化,导致近一步可以用哪些软件来带来更好的用户体验?
  • 世界上每一个公司都需要更好的科技来让他们在工作上变得更智能、更快、更安全。
  • 一个公司如果不能擅用科技、数据以及各种工具,那么它在未来就无法生存。
  • 要刻意保持公司的「小」和产品的「小」。
  • 寻找那个大公司不想去做的,或者不屑于去做的,或者技术上做不到的方向。
  • 你可以看大公司的成本结构,找出他们在哪个环节无法降低成本

Lecture 13 - How to be a Great Founder (Reid Hoffman)

  • 两到三个人员配置的创业公司最容易成功。因为创业成功需要很多优秀的品质,大家相互之间可以互补。
  • 创始人之间要有高度的信任感。
  • 如何为公司选址是衡量创始人是否优秀的一个指标。
  • 创业九死一生,你只有抓住你可以抓住的一切机会才能够成功。

未完待续……

迷茫是个很大话题,也是个恒久流传的话题,所以当我开动第一笔的时候,我承认我有忐忑和心虚的成分。

我始终认为文字承载着责任,也惶恐给读者带来不必要的烦恼,所以我措辞表达比较谨慎和提炼。

人们迷茫的原因大不相同,我无法涵盖所有情况,本文中的内容,望读者们依据自己的情况批判择取。

我算比较年轻吧,大的迷茫还真的没经历过,但是小的迷茫却时而有。

在持续的思考和实践后,我给自己总结了几点通用的建议。希望能帮到你!

1 理解迷茫、承认迷茫

不可置否,人这种生物常常会感到迷茫,尤其是年轻人。

总结起来就是不知道该干嘛,也不知道自己能干嘛。

哪个年轻的灵魂不迷茫呢?迷茫就对了,迷茫代表着不确定,不确定则意味着机会和希望。这是迷茫好的一面。

要和迷茫和谐相处,不但要进行心理建设,还要有实际操作理解和承认迷茫就属于心理建设中的一个,它也是所有建议的基础,所以我放到最开头来说。

不过说实在的,迷茫这个东西并不容易给出确切定义,但每个人也都能切切实实能感受到,它像风一样说不清道不明。

人们总有一个根深蒂固的观念,认为该时时有方向,该时时有目标。

观念本身是很积极的没错,可是这种观念也同时带领人们走向负面。

带着这个观念的人,如果其一时失去了方向,陷入了迷茫,那么就会有负罪感,进而感到沮丧,感到自责,甚至自我怀疑,自我否定。

但实际上,这些观念脱离了客观现实。迷茫不好,但迷茫本身就是一种常态,不要将其罪恶化和上升化。如果你觉得迷茫,不要有太多负罪感,也不要放大它,因为你要知道,这就是一件再正常不过的事情了,就像有时突然忘记该说什么一样的正常。

人生就是一个未知接着另一个未知的旅程,所以迷茫是在正常不过的了。

我们要做的无他,仅仅是找到现阶段要完成的事情,或者确立更长远的人生追求,然后踏踏实实享受人生就行了。

2 明确目标、分解目标、开始行动

第一,明确目标。

有的人说,我要想好再做出决定。但是要认清楚一点,无论如何规划未来,都是在冒风险,规划总有赌的成分。这种事情你永远无法想清楚,除非你能预见未来。说真的,人到中年四五十岁还没想清楚的多的是。

大多数人的问题是是想得太多,做得太少!

最好的做法是尽早做决定,快快行动起来,如遇变数再图后谋。

明确目标并不容易,这是一个不断做减法的过程,要放下无关紧要的,要排除当前的杂念去窥探本质,看看什么对于自己来说是真的无法割舍的,那或许就是你的目标所在。

正如庄子说过,“吾生也有涯,而知也无涯;以有涯随无涯,殆矣”。

人的力量始终是有限,无法什么都去追求,一定要懂得聚焦。

我在生活中常常遇到这样的人:东一榔头西一锤,今天对这个感兴趣,明天又对另一个东西充满热情。

如果发现自己什么都想要,那么正好说明没有目标和方向。这最容易让精力分散,最后有可能让你一直在原地打转。

哪怕朝着一个方向缓慢地前进,也不要一直在原地打转。

所以,静下心来思考一下,哪些对你而言是最重要的事情。其他的,勇敢地舍弃掉吧,有舍才有得。

另外,不要有太多不切实际的目标。有梦想是一方面,但是也要认清楚现实,不要沉浸在幻想中。

抛弃幻想,实事求是地设定目标,踏踏实实地实现。

在此,只要记住一句话:认准一个方向,然后大力出奇迹

第二,分解目标

年轻人特别容易产生一事无成的心理,进而妄自菲薄。感觉自己能力太小,什么都做不好。

你或许有一个很高远的目标,但是你感觉你离那个目标好远好远。

此时要分解目标,把大目标分解为几个小目标,小目标再分解为几个更小的目标,不断分解,直到每一个小目标都是可以差不多胜任的。

如此,当你努力实现一个小目标后,你会收获一些成就,也收获一些自信。在完成许多个小目标后,你就更有自信和意志力去完成大的目标。

总而言之,分解目标,在完成一个个小目标时不断建立起自信。

第三,开始行动!

行动起来,做一个行动派。

3 审视并重造失败观

不管是从小到大接受的教育,还是社会主流的价值观,都把失败看作是耻辱。

事实上,相当多的人迷茫的原因是害怕失败。

他知道自己想要什么,但是却没有追求的勇气,恐惧于被烙上失败者的印记。

重新审视自己对失败是看法,失败没有想的那么恐怖,没有那么不可接受。

失败没啥,失败是成功之母。在这次失败的经历中,足以学到让下次成功的经验。

硅谷是这个世界上最有科技含量、最有创新力的地方,同时其也是这个世界上最包容失败的地方。在硅谷,人们不把失败当回事儿,只是看作一次尝试,甚至会十分积极地看待:“哦!老兄,失败没事儿,这不过是一次伟大的尝试!”对失败的包容,是硅谷创业文化中很重要的一点,也是其不畏失败勇敢尝试的基础。硅谷的创业文化和风险投资行业,共同把这个荒野变成了科技中心。

重造失败观,最大的阻碍就是自己。把自己内心这一关过了,其他的都好说。

4 去书中寻找答案

去书中寻找答案,没错,书中真的有黄金屋。

阅读是一个深度单向交流的过程,是一个钻研前辈们最好成果的过程

我们现在经历的迷茫,千百年来的人们一定也经历过。他们有些处于动荡的时代,面对着艰巨的挑战。他们对于挫折,对于迷茫,乃至对于人生一定有很深刻的见解。

放下眼前碎片化的信息,沉下心来,去阅读吧。尤其是阅读那些伟大前辈们的思考和见解,听听他们的说法,看看他们的做法,接受他们的启发。

另外,还要一类含金量很高的书也值得多阅读,也就是与自己本行业相关的经典书籍,比如说教科书(对,就是那些在二手书店卖不出去的书)。说实在的,有些经典的教课书可能是数代行业领军人穷极一生心血编撰而成的,其中的含金量远远被大家所低估。

当你在你工作上感到迷茫的时候,不妨去看看这些书。它们会让你的工作能力提升一大个台阶。

最后送给大家一句箴言:得意时平心,失意时静气。

2022.01.22 重读后添加

刘慈欣《球状闪电》里有一段非常有价值的话:

“其实,儿子,过一个美妙的人生并不难,听爸爸教你:你选一个公认的世界难题,最好是只用一张纸和一只铅笔的数学难题,比如歌德巴赫猜想或费尔马大定理什么的,或连纸笔都不要的纯自然哲学难题,比如宇宙的本源之类,投入全部身心钻研,只问耕耘不问收获,不知不觉的专注中,一辈子也就过去了。人们常说的寄托,也就是这么回事。或是相反,把挣钱作为惟一的目标,所有的时间都想着怎么挣,也不问挣来干什么用,到死的时候像葛朗台一样抱者一堆金币说:啊,真暖和啊……所以,美妙人生的关键在于你能迷上什么东西。比如我——”爸爸指指房间里到处摆放着的那些小幅水彩画,它们的技法都很传统,画得中规中矩,从中看不出什么灵气来。这些画映着窗外的电光,像一群闪动的屏幕,“我迷上了画画,虽然知道自己成不了梵高。”

“是啊,理想主义者和玩世不恭的人都觉得对方很可怜,可他们实际都很幸运”。妈妈若有所思地说。

以 WordPress 为例,聊聊开源项目的赚钱之道

开源的赚钱之道

开源,顾名思义开放源代码。

程序员为什么要把辛辛苦苦写下来的代码开放出去呢?

原因很多,主要的一点是可以占领市场,但这个问题不是今天讨论的重点。

那源代码都开放出去了,又怎么从中获取收益呢?这就是我接下来要说的。

下面,我会以 WordPress 为例,来聊一聊朋友们比较关心的话题——开源项目的赚钱之道。

如果你是一个开源项目开发者,或者打算成为一个开源项目开发者,又或者仅仅是对开源本身感兴趣,那么没错,这篇文章就是为你而写的。

WordPress

WordPress 是一个用 PHP 语言编写的免费开源的内容管理系统(CMS)。

它创建之初只是一个博客发布系统,不过随着时间发展和项目迭代,WordPress 已经能支持多种形式的内容管理,它也早已被广泛用于图片展示、学习管理系统和在线商店。

WordPress 被超过 6000 万个网站使用,截止至 2019 年 4 月,前一千万个最大的网站中有 33.6% 都使用了 WordPress,这意味着你每看一个网站就有至少有一个是用 WordPress 搭建的。

WordPress 不但深刻地影响了整个互联网,而且它在商业上也是十分成功的。

因为这些前辈开发者们想出了一些非常好的盈利模式,这些盈利模式能在很大程度上代表开源项目的盈利模式,十分值得开源社区的后继者们学习体会。

相关组织

在了解 WordPress 的盈利模式之前,先来简短地认识一下 WordPress 相关的组织。

WordPress.org:WordPress 基金会,这是一个非盈利组织,它存在的目的很简单:维护并不断改造 WordPress 项目,并保障它是免费的,可以被任何人下载使用的。整个非营利组织的收入来源也含简单,就是靠捐赠,这些捐赠来自于全世界的使用 WordPress 的个人和组织。

WordPress.com:它隶属于一个叫做 Automattic 的私人软件公司,而这个公司就是 WordPress 的创建者 Matt Mullenweg 的公司(他还是 WordPress 基金会的董事,这意味他对整个项目有掌控权),Automattic 除了拥有 WordPress.com 之外,其还拥有 WordPress 商标的使用权。

前者 WordPress.com 是一个非盈利组织,靠捐赠自然不必多说。

Automattic 作为 WordPress 创建者的公司,开发者们只需要研究它的做法,就能大概知道如何让自己的开源项目赚钱。

插件

WordPress 提供了作为一个内容管理系统最基本的功能,即内容的创作、管理和展示三大功能,这些功能用来搭建一个博客,完全足够了。

但是如果你要加点其他功能的话,那就要用到插件了。


(插件商店)

插件是 WordPress 的一大特色,你想要一个特殊的功能,可以去内置的插件安装页面查找安装插件,如果实在没有你想要的插件,自己写一个也不是什么大问题。

毕竟,WordPress 专门设计了插件架构。WordPress 的插件架构是非常非常聪明的设计,它并没有锁定项目的功能,而是给其他开发者留下发挥的空间。

这里就催生了很多生意,许多开发者借此创建插件、售卖插件,通过开发插件来赚钱。

实际上,凭借其庞大的使用量,WordPress 的背后孕育一个庞大的插件市场。

到目前为止,光是登录官方平台的插件就多达 52326 个,更不用说其他非官方平台了,那更是数不胜数。

从这一点就可以看出,WordPress 已经不单单是一个开源项目了,而是一个平台,一个全世界都在使用的平台,这一切都来自于它们的插件架构的设计,这就是为什么我说这是非常非常聪明的设计。

WordPress 中有很多优秀的付费插件,它们往往采取订阅制,这就是它们主要的赚钱方式。

以 Ankimet 为例,这是 Automattic 开发的一款插件,用于检测管理你网站里出现的垃圾评论,这几乎是一个稍微大点的网站必须的功能。

结合 WordPress 的使用量,可想而知这个插件的顾客量,可以源源不断的给 Automattic 提供现金流。

主题

和插件一样,WordPress 的外观是完全可以定制的,它们把这个叫做主题。

如果说插件提供了不同的功能,那么主题可以说提供了不同的外观。

在这个颜值之上的时代,网站的外观设计是相当重要的,所以很多用户会愿意在外观上花费精力、财力。

因此,设计师们可以通过提供漂亮的主题模板来赚钱。(虽然 Automattic 在这主题部分并没有赚钱,但是仍值得一提。)

在主题部分,WordPress 也给用户留足了发挥的空间,这也是它另外一个聪明的设计。

托管服务

我在前面提到过 WordPress.com,这是 Automattic 旗下的业务,提供自动化的 WordPress 网站托管服务。


(wordpress.com 托管的页面)

我们知道,如果仅仅获得了 WordPress 的源代码还不足以搭建网站,你还必须购买域名、购买服务器,配置服务器……

这一套下来,真的挺有门槛的。而 WordPress.com 做的事情就是消除这个门槛,让没有技术背景的人也可以直接使用 WordPress。

在 WordPress.com 上,你不需要买域名和服务器,也不需要花时间学习服务器配置,只需要几个步骤,即可获得一个 WordPress 个人网站。

它们的收费也是订阅制的,按月/年收费,当然它也提供免费版,不过你的网站会被插入广告。

考虑到非技术背景的人更多,使用该服务的人不计其数,这也成为了 Automattic 的一项十分重要的现金流来源。

商标与品牌

WordPress 从诞生到现在,已经被无数的人所熟知,在形成一个巨大社区的同时,也产生了共同的文化符号(亚文化群体)。

在 WordPress 的这个例子里,商标已经被捐给了 WordPress 基金会,所以它们可以利用 WordPress 这个商标来进行发售周边物品,甚至是做一些授权、联名。

如果你是一个开源项目的作者,那么你最好掌握住你项目的商标,这对于你来说是一笔很大的财富。

因为在项目不断发展的过程中,商标背后的品牌也是在不断被塑造成形。

流量

在互联网上,有流量者有天下。对于很多事情来说,流量有点石成金的奇效。

如果你的开源项目被很多人用,那么它本身就是自带流量的,怎么用流量变现,根本不需要我多说,方式太多。

甚至都不需要你想怎么变现,自然会有人来找你合作。

从这一点出发,开发者要思考如何把握住自己项目的流量,这个相当重要!

总结

其实说了那么多,总结一下开源项目赚钱的底层逻辑:

0、开源证书表示了你的权力发放的尺度,对一个项目的商业模式影响很大(参考 Android)。所以请根据你的商业模式,选择最适合的开源证书。

1、你虽然开放了源代码,但是你可以选择只开放基础功能部分的源代码。高级功能以订阅付费的方式提供,就像 WordPress 的付费插件一样。

2、你比别人更懂你的项目,这一点很重要。你可以给使用者(尤其是企业用户)提供咨询、技术支持服务等附加服务。另外还有一点很有商业潜力的是制作教你更好地使用该项目的课程进行售卖(和官方文档不一样,这样的课程更多是提供一个具体的案例教学)。

3、以官方的名义,建立一套自动化流程,给用户提供一套付费的解决方案,就像 WordPress.com 的自动托管服务一样,让没有技术背景或者懒得花时间研究的用户得以使用你的项目。

4、在设计项目的架构时,做一些可拔插设计,就像 WordPress 的插件架构和模板系统一样,因为正是这些可拔插设计促成了 WordPress 生态的繁荣,让它成为一个平台级的项目。

5、你拥有商标和品牌的所有权,一旦你的项目用的人多了,你的商标就会自带流量和价值,你可以围绕它做很多事情。

机智的朋友可能已经发现了,开源项目的赚钱之道完全可以直接用在闭源项目上,因为他俩是包含和被包含的关系。

一个开源项目要持续不断地维持下去,必定要有一定的盈利方式。诚然,现在已经有很多值得参考的盈利模式,但更多的盈利模式仍然有待探索。

内容输出的方式多种多样,上至著书,下至聊天。

我接下来所说的内容输出是指互联网上的内容输出,主要包括说图文、音频、视频等载体的内容输出。

为什么要在互联网上做内容输出呢?我来说几点我的理解。

1、帮助自己学习

最有效果的学习方式就是学完之后马上去教别人,而内容输出实际上也是在“教别人”。

既然要输出内容,那么作者一定会思考如何把一堆东西串联起来,然后用自己朴素的语言表达出来。

在这个思考、整理和表达的过程中,作者快速地内化了知识。

就好比大学老师边教书边做研究,教学促使老师总结,总结提升理解,理解激发更好地研究。

所以做输出很重要,第一点就是帮助自己学习。

另外,做内容输出可以收获很多他人的留言和反馈,以此吸收许多先进观点,改进自己。这也是内容输出即学习的另一个方面。

2、扩大曝光量

为什么要扩大曝光量呢?总结下来有两个原因。

第一个原因是获得更多机会。

如果你是一个优秀的人,你要展示自己,让更多的人看到你的优秀,你才能获得更多的机会。

借助互联网的内容输出就是一个绝佳的自我曝光方式。

你如果分享的东西好,你一定会被大佬看到,得到大佬的赏识后,你一定会有比别人多得多的机会。

比如说对于一个创业者来说,你输出的内容是投资者们了解你的途径,提高了得到投资人赏识的概率,也就提高了获得投资的可能,这也是在为自己未来的项目融资做的准备。

第二个原因是以文会友。

内容输出后可以结识很多志同道合的朋友,往往这些朋友对你来说是很优质的人脉资源。

因为这样的朋友认可你的想法,赏识你的观点,你们未来会有很多的合作机会,也可以迸发出很多的想象力。

3、商业上的回报

虽然商业上的回报永远不是做内容输出的最终目的,但是这又是不可忽视的一点。

如果你分享的内容很有价值,能够帮助到别人,那么你能迟早能获得一些金钱上的奖励。

除此之外,你还收获了流量,流量是互联网的经济下血液,有了流量,无论你未来做什么项目,都是大有裨益的。

如果做得不错,你还能在输出内容的同时,打造一个 IP,这对于个人来说绝对是很值的商业回报。

4、给别人提供价值

前三点都是对于作者自己的好处,这一点这是对他人的好处。

输出优质内容本身可以提供价值,这一方面满足了作者的成就感,另外一方面还帮助了他人,两全其美,何乐而不为?

这一点在互联网上的表现尤其明显,在互联网上,内容的传播几乎没有成本,这意味着你的内容的价值总量可以被轻松翻倍(给你带来的回报也可以轻松翻倍),并且内容可以在很长一段时间里发光发热,造福他人。

在外文互联网中,有一只名叫 DuckDuckGo 的鸭子。

名字奇怪就算了,它的 logo 也相当奇幻:一个人畜无害的鸭子,天真无邪的眼神,头上竖着呆毛,系着违和的绿色领结。

当我第一次在火狐的搜索引擎列表看到时,还以为乱入了一个童装品牌。

谁会将可爱的鸭鸭与科技产品联系起来呢?

实际上,它就是一个货真价实的搜索引擎。

拿鸭鸭做 logo,或许创始人 Gabriel Weinberg 是想塑造一个平易近人的科技产品形象。

搜索引擎是如何赚钱的?

在聊这只鸭子之前,先来探讨一个问题:搜索引擎是怎么赚钱的?

我就拿全球最大的搜索引擎谷歌来说明。

谷歌搜索引擎赚钱基本上离不开两个字——广告。

这里就不得不提到谷歌最赚钱的业务—— AdWords 和 AdSense 了。


(Google AdSense)


(Google AdWords,现改名为 Google Ads)

广告主通过 AdWords 投广告,流量主通过 AdSense 接广告。(一个对接广告主,一个对接流量主。)

通过 AdWords,谷歌拥有了大量需要被展示的广告,这些广告主要通过两个途径被展示出去。

第一,各类大中小网站的站长会通过 AdSense 向谷歌接广告,广告收入的部分就被谷歌纳入囊中。(这就是为什么你总能在各种网页里见到谷歌的广告,因为谷歌的广告已经被嵌入在了数百万网页之中。)

第二,大部分广告将通过竞价排名等方式,被展示在谷歌搜索的结果页面。因为搜索词条的数量不胜估量,所以竞价排名是一项庞大的收入来源。

就这样,AdWords 和 AdSense 实现了广告投放与展示的自动化,我愿称之为广告流水线

正是这条“流水线”,为谷歌提供了源源不断的现金流。毫不夸张地说,这就是一台全天连轴转的印钞机。

当然,这一套广告系统也在一定程度上促进了互联网内容生态的积极发展。


(数据来源:美国证交会提供的 Google 10-K 表)

从历年谷歌披露的数据来看,虽然其广告收入占比在逐年下降,但不可置否这一直都是其最主要的收入,从这一点来看,谷歌也能被称作广告公司

为什么要这样说呢?因为我想提醒一下,作为广告公司,谷歌一定会有偏向广告而忽视用户的倾向。

广告公司谷歌最关心一个问题:如何让广告主爸爸投更多广告,给更多钱?

广告主打广告的目的很简单,增加品牌知名度+增加产品购买量,总之要达到一定的转化目的。

我投了一块钱的广告,如果只有获得了一分钱的效果,那么我就不会再通过此途径打广告了。相反地,我投了一块钱的广告,却达到了两块钱的效果,那么我还会继续在这里打广告。没有人比谷歌更懂广告主的心思了。

实际操作中,广告收益一般是按照点击率转化率来进行结算。

接下来,谷歌要做的就是利用技术手段来提高这两个指标。谷歌是怎么做的呢?

在这里就涉及了谷歌商业模式的基础,同时隐私问题也将呼之欲出。

首先,收集用户数据,包括但不限于搜索记录、点击记录、浏览记录。

其次,以收集到的你的个人数据为依据,对搜索结果进行过滤、排序、塞广告。例子如下图:

(87 个人同时在谷歌上搜索同一个词条,并且还是在隐私模式下,他们得到了不同的搜索结果。)

这样一套操作下来,就实现了精准投放的目的,从而实现了让你更愿意点击,更愿意购买的目的。

这里就举一个简单的例子:如果你最近浏览过某个电动车的页面,当你再次搜索电动车时,谷歌就会给你推送这个电动车厂商的广告。因为当你第二次看到时,你会相对更能接受,也更有可能点进去进行购买。

事实证明,精准投放比随意投放的转化率要高好几倍不止,广告主当然愿意给更多的钱,这就是谷歌在广告市场的竞争力所在。

总结下来,谷歌实现广告的精准投放是建立在用户的搜索记录、浏览记录等隐私信息之上的。

这些信息反应了一个人的日常活动,它们就像一条条线索一样,谷歌通过无数条这样小巧的线索来拼凑出你的人物画像,从而从你身上挖掘更多的商业价值。

同理,百度等各大搜索引擎也是这个模式,全都离不开收集数据和打广告。

除了广告之外,你的数据还会被搜索引擎出售、分享给其他公司。于是乎你的隐私就像是“不要告诉别人”的秘密,一传十,十传百,如病毒般在业界不断传播,直到成为“公开”信息为止。

收集数据+打广告,成了目前搜索引擎们的标准商业模式。

这里不包括一位“反叛者”——开头说的那只鸭子。

DuckDuckGo 及其差异化

2008 年,Gabriel Weinberg 开启了这个项目,其最初是建立在众多供应商提供的 API 基础之上的。这个搜索引擎是用 Perl 语言写的,运行在 nginx,FreeBSD 和 Linux 之上。

2010 年,Weinberg 开启了 DuckDuckGo 社区网站,用于和用户讨论改进项目。

2012 年,DuckDuckGo 的每日搜索量到达 150w。

2014 年,Apple 宣布 DuckDuckGo 成为其 Safari 浏览器的内置搜索引擎之一。紧接着,Mozilla(火狐浏览器)也做了同样的事情。主打私密浏览的 Tor 更进一步,将其作为默认搜索引擎。2014 对于 DuckDuckGo 来说,是飞越的一年。

2016 年,DuckDuckGo 开发了一款浏览器插件,该插件用于阻止搜索引擎按照搜索记录排序,也防止搜索引擎进行广告追踪。

2019 年,谷歌将 DuckDuckGo 加入到 Chrome 73 的默认搜索引擎列表中。

接着就是一路不断发展,不断积累,到了现在已经挤入了主流搜索引擎之列。

截至今日,DuckDuckGo 每日搜索量已经达到了 6700w 次,2020 全年搜索量将突破 200 亿次。


来源:duckduckgo.com/traffic)

尽管如此,其体量和谷歌比起来,亦不亚于蚂蚁比大象(不是用体量换算,只是形象比喻)。

来源:StatCounter Global Stats – Search Engine Market Share)

根据上图的数据,谷歌占据了美国搜索市场份额的 87.6%,而 DuckDuckGo 只有 1.6%,连谷歌的零头都不到。谷歌将会在很长的时间里,坐稳搜索引擎的头把交椅。

从市场份额看来,谷歌不会害怕 DuckDuckGo 的挑战,如果有那也仅仅是微乎其微的。

当 Weinberg 2008 年开始这个项目的时候,谷歌已经是一个千亿级市值的公司,已经能够提供了稳定成熟的搜索服务。

人们有理由抛弃谷歌,转而去用一个奇怪 logo 的搜索引擎吗?没有理由。

事实上也是如此,一开始没有人注意 Weinberg 的奇思妙想。

要不是一个叫 Hacker News 的地方,估计这项目得流产。

在 Hacker News 那个充满了创业精神、极客信仰的社区,Weinberg 收获了 DuckDuckGo 最早期的一批用户。

从这个社区开始,DuckDuckGo 走出了一条它的成长之路……如前文所述。

搜索引擎经过二十年的发展,其标准化程度已经高山仰止,在大部分用户看来,各大搜索引擎提供的服务如出一辙,至少没有哪个搜索引擎服务存在数量级上的优势了。

高度标准化意味着,留给后来者做差异化的地方真的所剩无几。

一个产品或服务如果做不出真正的差异化,将难以立足于激烈的竞争市场中,更不用说长久地盈利,其也不会成为一门好生意。

关于做不出差异化最好的例子就是航空业:用户无论乘坐哪家航空公司都能飞到同一个目的地,很少有人会为了机餐好吃而多给钱,所以用户会优先考虑最便宜的机票。

DuckDuckGo 能杀出谷歌的围追堵截,主要靠两条差异化的路线:

第一,极力保护用户隐私,承诺不收集用户数据。这是他们最大的特色,也是他们所有宣传的重点。

第二,改善搜索结果,减少广告,优化页面,给用户更好的搜索体验。比如可以改变页面主题,展示结果更加清晰明了等。如下图:


(比较权威的解释单独列在右边,官网放到最上面。)


(输入“weather”直接显示当地的天气预报。)


(输入“coffee in New York”,出来一张地图,显示一些推荐的咖啡馆。)

这两张牌,尤其是第一张,是 DuckDuckGo 较之于其他搜索引擎的最大差异化,也是其核心竞争力。

DuckDuckGo 官方表示从来没有收集、储存或分享用户的个人数据,正如创始人 Weinberg 所说:“没有监视的商业模式,就像我们的产品一样,照样也能带来盈利。”

换言之,Weinberg 认为收集用户数据不是互联网公司实现盈利的必要条件。

并且他一直坚信,减少用户数据的收集,依旧不会损伤太多的商业利益,但是可以让用户更舒适。

2014 年,在这种特立独行的信念之下,他们意料之外情理之中地盈利了。

在不把用户数据当作产品的前提下,DuckDuckGo 是如何获得收入的呢?有两条途径:

第一,基于搜索关键词的广告。这里的广告展示只是基于搜索框里的关键词,不会根据你个人进行特别优化。值得一提的是,广告是可以在设置里面关掉的,想不想看广告完全取决于用户。

除了基于关键词的广告之外,他们一直在寻找不靠用户数据的盈利方式,以此来减少对广告的依赖。于是就有了第二条。

第二,导购分成。电商导购是很老的盈利模式了,如果用户通过 DuckDuckGo 搜索页进入电商平台购买了商品,那么 DuckDuckGo 就能获得一定平台分成。

靠着这两种途径带来的收入,DuckDcukGo 团队已经扩大到将近百人。

在 DuckDuckGo 一路发展的道路上,我注意到一个很有意思的地方,Weinberg 很早就开始为 DuckDuckGo 搭建社区和博客。


(DuckDuckGo 社区)


(DuckDuckGo 博客)

通过社区和博客,不断聚合了一群拥有相同价值观的用户,还促进了网民保护隐私的意识。

我的观点与此不谋而合。如果条件允许,每一个创业者都要用心打造社区和博客,都是能在受益良多的东西,同时它们也是最好的广告,因为成本低、收益却相当可观。社区和博客的作用总结起来有三点,一来沉淀用户,二来收集反馈迭代产品,三来传递价值。这个话题说来话长,不在此详述,未来会专门出一些对其进行深入探讨的内容,敬请期待。

本着个人对互联网重视用户隐私的期望,我会义无反顾地支持 DuckDuckGo,毕竟他们作为一股不可忽视的力量,正在切切实实地向“作恶”的巨头施压,也在不断推进隐私权保护的进程。

商业上的思考

DuckDuckGo 是一个值得盘一盘的商业案例。

思索一番,你会发现其颇具革命色彩:一只弱小的鸭子,为了心中的一个信念,在互联网这种赢家通吃的环境下,试图对抗一个巨人,螳臂当车的既视感,有没有?然而鸭子不但没有被打败,反而还在抗争中不断壮大……

虽然 DuckDuckGo 的竞争之路还远远没有结束,但是这对我们进一步理解商业竞争有很大的启发。

当你处于一个被巨头占据的市场时,除了将市场细分外,还可以针对对手的弱点,做出差异化,在夹缝里生存并寻求机会。

DuckDcukGo 抓住的一点叫做隐私保护,这恰好戳中了谷歌的软肋。

巨头们看似不可挑战,但是真的有那么不可挑战吗?当然不是,巨头是笨重的,是有软肋的。

我们要始终保持勇于怀疑权威敢于挑战权威的精神,这是 DuckDuckGo 给我们传达的启示。

最后

DuckDuckGo 建立在人们对隐私的重视之上的,隐私越是得到重视,DuckDuckGo 的竞争力就越强,所以可以理解为什么 DuckDuckGo 社区持之以恒地宣传隐私安全。

在隐私问题日益突出的当下,可以预见会出现更多与 DuckDuckGo 类似的,在现有服务下针对隐私问题进行改进的产品。包括电子邮件应用、聊天应用、网购应用等,未来可期也。

诚然,中国互联网的出现距今只有二三十年,其目前仍处于一种相对狂放的发展阶段。其诸如隐私保护、数据归属等众多问题依旧有待解答。实际上,这些问题多是结构性的难题,想要解决这些问题,我们依然有多从上到下的工作要做。

《射雕英雄传》里有个充满喜剧色彩的角色名叫周伯通。他被黄药师困在桃花岛上多年,出于无聊地开发了一门武功用来自己和自己玩,即为左右互搏:左手和右手对打,相互拆招,相互掣肘。

这看似可笑但却是一种很高的境界:左手和右手分别施展不同的招式,要一心二用,这并不容易,同时还要对两边的招式烂熟于心,就难上加难了。

郭靖向周伯通学会此左右互博之术之后,便可同时使出两式降龙十八掌。本就独步天下的降龙十八掌,经左右互博之术的加成后,更是所向披靡。

话说回来,周伯通和郭靖毕竟是小说里的人物,他们终究停留在我们想象里。

可在现实生活确实也有那么一个人达到了左右互博的境界,他叫 Nir Eyal(尼尔·埃亚尔)。


(Nir Eyal)

不得不承认,这哥们儿或多或少地改变了我们的生活,包括正在使用手机的你我他。

左手:写给产品人

(以下“产品”均指“互联网产品”,“应用”均指“互联网应用”。)

2014 年,Nir 完成了一本书的写作,此书一经出版,便迅速出现在了各大互联网公司的书架上,成为了名副其实的畅销书。其至今都还在亚马逊产品类别排行榜中占据着一席之地。

不但如此,此书还受到各大科技公司 CEO 的一致称赞。微软某员工告诉 Nir,Satya Nadella(微软 CEO)曾经在办公室里举着这本书,向公司的员工推荐。

这本书叫 _Hooked: How to Build Habit-Forming Products_,中文译版叫《上瘾:让用户养成使用习惯的四大产品逻辑》。


(《上瘾》)

时间回到 2008 年。

当时,Nir 大学毕业后和几个同学开始创业,业务是在 Facebook 上发布广告(类似于网络营销)。

这次创业的缘故,Nir 开始研究起了当时几款主流的互联网应用。2008 年,Facebook 已经四岁了,Twitter 也有两岁了……当时正是一大众互联网产品迅猛发展的时期。人们为何对这些应用如此着迷,它们是有什么魔力吗?Nir 对此也颇为很好奇,他认为这些应用的背后,一定有一些共同的特性在改变用户的行为,他决定找出来。

经过数年的研究,包括对用户心理、用户行为、人性的特征等进行研究,Nir 最后总结出了一个很简单的模型。

这个模型在解释这些应用是如何让人上瘾的基础上,还高屋建瓴地总结了四条产品设计理论,通过这个四条理论就可以设计出让人为之沉迷的产品。

于是乎,Nir 把他的研究成果都汇集到一起,著成了 Hooked

对于一个产品控来说,我当然第一时间就找来了原书拜读。

当前,互联网在很大程度上还是一个注意力产业,这就直接塑造了许多互联网产品的商业模式,都是得益于用户的投入的关注:用户使用的次数越多,使用得越频繁,这些产品背后的公司收益就越多

并且,能让用户积极地参于产品还有另外一个好处,就是在竞争中以更快的发展速度超越对手,有时这种竞争是摧枯拉朽的。我们可以从很多互联网巨头的成长之路中看出这一点,他们无一不是开发出了洞悉用户心理且让用户迷恋不已的产品。

所以,通过对产品进行全方位地设计,让用户爱上这款产品,并最终达到上瘾的状态,是很多互联网公司费尽心思所追求的目标。

那要怎么设计产品才能达到这个目的呢?

当然了,产品设计是一个很大的课题,要真正把这个问题研究清楚,估计得上几门大课。但是我可以明确地说,其中一个显而易见的答案是培养用户使用习惯

这也是 Hooked 这本书里给出的答案。书中的四条设计理论,其背后的逻辑也是通过对产品进行设计来引导用户形成使用习惯。

在此,我会结合作者所传授,加上自己的理解,简单来说说这四条逻辑:

第一,引发用户使用该产品

书里把这一步叫做触发。

培养忠诚用户的第一步是让用户进入你的产品,我喜欢把这说成让用户进入你的产品的“圈子”

引导用户使用产品的方式多种多样。最主要的是宣传,比如广告宣传、邮件宣传、社交媒体宣传、口碑宣传、甚至是一次公关宣传。

做广告是很费钱的,但并不代表所有的宣传都是很费钱的。像社交媒体宣传、公关宣传这些,其实就是四两拨千斤的途径,不需要花费多少就能达到极佳的宣传目的。


(关于公关宣传,可以看看罗永浩团队的做法)

在所有的宣传中,最为有效的是口碑宣传,个人的宣传力量总是有限的,如果能发动广大用户自发进行宣传,那将是有燎原之势的。但是,前提是你的产品做得足够的好,诚意足够……

(关于宣传方面的方法论不宜在此赘述,未来我们再来深聊。)

这一条逻辑说直白一些,就是结合目标用户的特点,通过恰当的多个途径进行宣传,让用户开始使用产品。

第二,促进用户进行使用

如果第一步做得好,那么这时应该慢慢有一些用户尝试使用你的产品了。

不过这还不够,想要真正留住用户,还要扫除用户使用过程中一切可以扫除的障碍,力求给用户丝滑的使用体验。

越是简单的事情,越能让人形成习惯。 所以想要培养用户的使用习惯,一定要从简设计。

如何理解这里的呢?我总结就是两点:界面的简洁使用逻辑的简单。实际上,这两者也是相辅相成的。

苹果公司最让我影响深刻的一点是其几十年来始终贯彻“开箱即用”的理念。

苹果的产品,从硬件到操作系统,再到应用软件,都专注于简洁设计。这就让它的产品很容易让人接受,哪怕是没有用过电子产品的人也能很快上手。

这一点真的值得每一个产品人好好学习体会。

关于使用逻辑的简单,推特就是一个很好的例子。推特的核心功能简单得无以加复:140 字符以内的微博客。

写出长篇博客对于大多数人来说或许并不简单,但是发一小段文字却几乎没有门槛。正是因为这样简单的使用逻辑,用户才用随心所欲,用得得心应手。


(Twitter 原来限制 140 字符,现在限制 280 字符)

专注核心功能,砍掉不必要的环节。这一点不仅仅适用于互联网产品,还适用于所有的产品。不妨现在好好想一下,你的产品或业务中有哪些地方可以简化,哪些地方可以去掉。

总结起来,这一条就是通过各种办法让用户顺利使用到你提供的服务,最好上手即用。

做产品是减少的艺术。

第三,给用户多变的酬赏

这里倒不是给用户撒钱的意思,而是让用户获得不确定的满足。

比如短视频,奖励是看有趣的视频带来的愉悦。下一个视频也许有趣,也许无趣,在没有看到之前,用户会处于期待的状态。这就像开宝箱一样,用户只有在不断地刷才能知道下一个到底是不是奖励。

这个同样适用于社交应用上。比如微信,不管是别人给你发消息,还是你去看朋友圈,对你来说都是一种可变的酬赏。你怀着对酬赏的期待,在第一时间打开微信进行查看。

同样的逻辑可以应用在游戏、健身应用等各类产品中。

现实生活中也有类似的,那就是彩票。时不时让你中个小奖,给你多变的酬赏,让你慢慢喜欢买彩票,甚至是上瘾。

简而言之就是让用户永远保持未知,保持好奇,用户才会不断地使用,永远没有底。

并且!用户在欲望不断得到满足的过程中,心理依赖会不断加强,最终形成一种心理映射。比如说只要感到无聊,就会不自觉地打开抖音。

这一条实际上利用了人类大脑的特点。不过这一条对于用户使用习惯的培养也是至关重要的。

第四,引导用户进行投入

这里的投入包括时间,精力,甚至是金钱。

用户对产品投入得越多,那么用户粘性就会越大,因为离开成本对用户而言已经很高了。

如果哪一天,当用户考虑离开的时候会感到肉疼,那么这个产品就很成功了。

其实,不管做的是互联网产品还是实体产品,用户的离开成本都是经营者值得考虑的问题。

这也可以解释为什么理发店、健身房、咖啡馆、洋快餐等各行各业会不断地向你推销办卡,因为这实际上就是在提高用户的离开成本。

另外还有很多方法可以提高用户的离开成本,比如说给用户提供个性化的服务,将用户的利益和应用绑定等等。

引导用户投入,是培养回头客的有效方法,也是加强用户对产品依赖的有效方法。

这就是 Hooked 这本书的大致逻辑。如果把这四条都做得好,且配合地紧密,则会形成一个不断强化的循环,并最终培养出用户的使用的习惯。

我们知道,一个习惯是需要几十上百次不断地重复才能形成的,所以别小看这个循环,正是这个循环让用户在不知不觉间重复一个行为,并最终形成习惯的。

虽然这个模型很简单,但是书中引用了很多例子,并且做了更加全面深入的讲解,真心推荐大家去阅读。

右手:写给每个人

在 2020 年的今天,互联网已经深度地参于到我们生活的点滴之中了。

智能手机,这台安装了许多应用且可以连接互联网的设备,正在成为我们身体的一部分:我们工作需要它,买东西需要它,联络亲朋好友需要它,了解世界需要它,打发无聊时光需要它……

使用手机可能是我们早上睁眼做的第一件事,也是我们在睡觉前做的最后一件事。说真的,我们已经离不开手机,离不开其背后的互联网了。

据调查显示,我们每天解锁手机屏幕上百次,每天使用时间合计超过三小时……对于年轻人来说,这些数据只会更高不会更低。

目前,手机应用正在以前所未有的速度迭代着,越发精巧的设计,越发使人难以抵抗。它们不但占据我们大量的时间,还频繁地打断我们的注意力,让我们分心。

想想看自己:

是不是起床第一件事情就是拿起手机查看微信?

是不是无聊时会下意识打开抖音/B 站?

是不是刷抖音/B 站时根本停不下来?

是不是特别想把图标上的小红点都消除掉?

是不是使用一些 App 能熟练到近乎本能?

是不是会经常不假思索地打开某个 App?

是不是永远也不愿意卸载掉某些 App?

如果这些问题的答案是肯定的,那么就对了,我们都被手机应用培养出了使用习惯,并且是深入潜意识里的习惯。

正如Hooked中所说的,那些精心设计的产品已经在不知不觉间改变了我们。

这些近乎本能的习惯可并不是什么好事儿,它们每天浪费掉你大量的时间,消耗你宝贵的注意力。很多时候,我们会不知不觉地打开手机应用,或者不停地查看消息,查看邮件,只要停止这个行为我们就开始变得焦虑。但手机真正需要我们打开的时候很少很少。

如今,到底是我们掌控手机应用,还是手机应用在掌控我们呢?

Hooked这本书出版之后,许多产品人仿佛一下子顿悟了,原来这样就能做出让人上瘾的产品。确确实实地,这本书间接地指导了许多互联网产品的设计开发。让产品能利用人性的特点,使人上瘾,得以不断从中挖掘商业利益。

当然这一点也被 Nir 所看到,这或许是他当初写这本书时所没有料想到的。正如那本书的副标题所写:打造习惯养成产品。他本来是想指导产品的设计者们做出习惯养成的产品,但是没想到却参于了这些消耗人们大量时间的互联网产品的设计。

事情的发展已经偏离了 Nir 的本意,于是在出版了Hooked五年之后的 2019 年,他又出版了一本好书,一本教你如何克服被产品打断注意力的书。

他希望通过这本书,来帮助用户抵制那些成瘾产品的诱惑,让用户学会更好地掌握自己的生活。

这本书几乎是对Hooked发出了猛烈的挑战。

这本书的名字叫做:Indistractable: How to Control Your Attention and Choose Your Life

翻译过来就是,永不分心:如何控制你的注意力并掌控你的生活

中文版暂时没有找到,于是我又花了一个星期的空闲时间把英文原版的Indistractable通读一遍,这次看英文版本的,且书中的内容比Hooked多很多,所以读得比较慢,但是却收获良多。

这本书已经不光是教读者如何克服互联网应用带来的分心,还全方位地分析了生活中所能碰到的分心的情况。

包括电子邮件、聊天应用、网络文章、手机、电脑、人际关系等带来的分心。

Indistractable不但从心理层面提供了很多建议,还给出了很多实际操作的方法。

让我印象最深刻的一条是把某些手机应用卸载掉,需要的时候去使用网页版。太对了!手机随身写带在身边,应用会推送即时消息,使得我们不得不被动去处理消息。但是如果只使用网页版,那我们就不会随时随地被消息打扰,转而是主动去处理消息。并且没了手机端,就直接杜绝了时不时打开应用的机会。从源头杜绝了Hooked中提到的第一条产品设计逻辑:触发,进而从循环中脱离出来,慢慢改掉习惯。

另外还有一个章节也让我记忆深刻,探讨的是如何帮助孩子脱离电子设备沉迷。对于孩子沉迷于电子设备,家长们都简单地甩锅给电子设备和互联网应用。

家长们要对此现象有一个更深入的认识,尤其是心理层面的。无论是大人还是孩子,都有三种精神层面的需求,分别是自我掌控感获得感人际连接感,但是往往现在的孩子没有办法在现实生活中满足这些心理需求:生活一切被家长掌管了没有自我掌控感,学习很无聊得不到获得感,家长不放心孩子出去和其他孩子玩也就得不到人际连接感。

孩子的这些心理需求都得不到满足,所以会转而沉入互联网世界中,因为在这个虚拟的世界里,恰恰这些心理需求都能得到很好的满足。这才是孩子容易沉迷电子设备的原因。

有了这个基本的认知之后,才能提出建设性的方案,正如书中也给出了一些具体的操作。

……

有一说一,确实写得十分全面,在随时随地容易被各种事物打断注意力的当下,这是一本十分值得阅读的书,推荐给大家。

总结

Nir 出版的这两本书我都认真地阅读了,真的可以算作左右互博之举吧。

借着介绍这两本书的机会,同时也分享了一些自己的心得体会,关于做产品的,关于对抗分心的。

尤其是写给产品人那部分,我前前后后写了半个月,本来写了上万字的长文稿,但还是害怕太长了读者读不下去,所以我提取了其中的重点,并且以精炼的语言,写就现在这个版本。

1、重视产品骨架的设计,因为这决定了产品的逻辑和基调。

2、多听用户的意见,少听用户的建议。

3、不依赖于产品调研。

4、重视界面设计,重视交互设计,颜值能决定很多很多很多很多的东西。

5、保持克制,不要什么都往里塞,专注做好核心功能。

6、做 A/B 测试,从用户不知情的实际反应中得到反馈。(后来想想,其实这一条并不好,因为 A/B 测试往往只能看到短期效果,而忽视了长期效果。)

7、不做产品功能的长期规划,走一步,想一步,做一步。

8、快速迭代,持续发布,保持机动性。

9、保持简单,不要给用户留选择,不要让用户思考。

10、想方设法满足用户的心理需求。

技术人文

  • 失控
  • 数学之美
  • 人月神化
  • 奇点临近
  • 浪潮之巅
  • 重来
  • 哥德尔、艾舍尔、巴赫——集异璧之大成

人文社科

  • 穷查理宝典
  • 人类简史
  • 未来简史
  • 如何阅读一本书
  • 论幸福 —— 罗素
  • 洞穴奇案

数学

  • Linear algebra done right

历史

  • 万历十五年
  • 叫魂:1768 年中国妖术大恐慌
  • 潜规则:中国历史中的真实游戏
  • 血酬定律:中国历史中的生存游戏

经济/金融

  • 经济学原理 —— 格里高利·曼昆
  • 证券分析 —— 本杰明·格雷厄姆
  • 聪明的投资者 —— 本杰明·格雷厄姆

创业/管理

  • 从 0 到 1
  • 创业者手册
  • 创业维艰
  • 不拘一格

传记

  • 乔布斯传
  • 富兰克林自传
  • 李光耀观天下
  • 邓小平时代(港版)

平面设计/交互设计

  • Human Interface Guidelines

计算机

  • 汇编
    • 汇编语言 —— 王爽
  • C
    • C 程序设计语言
  • C++
    • The C++ Programming Language
    • C++ Core Guidelines
    • Effective C++
    • More Effective C++
  • Java
    • Effective Java
    • 深入理解 Java 虚拟机
  • 运维
    • SRE:Google 运维解密
    • Kubernetes in Action
  • 计算机网络
    • 计算机网络
    • TCP/IP 详解(卷一:协议,卷二:实现)
    • HTTP 权威指南
  • 数据库
    • Database System Concepts
    • MongoDB in Action
  • 性能
    • Systems Performance
  • 操作系统
    • 现代操作系统
    • 操作系统设计与实现(上册设计、下册实现)
    • Linux 内核完全注释
    • Linux 内核设计与实现
    • Understanding the Linux Kernel
    • 程序员的自我修养:链接、装载与库
  • 分布式系统
    • 数据密集型应用设计
  • 数据结构与算法
    • 算法导论
  • 软件工程/计算机系统
    • Software Engineering at Google
    • 深入理解计算机系统
    • Unix 编程艺术
  • Kubernetes
    • 深入剖析 Kubernetes