go build 编译时的附加参数
go build 还有一些附加参数,可以显示更多的编译信息和更多的操作,详见下表所示。
go build 编译时的附加参数
附加参数 | 备 注 |
---|---|
-v | 编译时显示包名 |
-p n | 开启并发编译,默认情况下该值为 CPU 逻辑核数 |
-a | 强制重新构建 |
-n | 打印编译时会用到的所有命令,但不真正执行 |
-x | 打印编译时会用到的所有命令 |
-race | 开启竞态检测 |
1. 【初级】下面属于关键字的是()
A. func
B. def
C. struct
D. class
参考答案:AC
2. 【初级】定义一个包内全局字符串变量,下面语法正确的是()
A. var str string
B. str := “”
C. str = “”
D. var str = “”
参考答案:AD
3. 【初级】通过指针变量 p 访问其成员变量 name,下面语法正确的是()
A. p.name
B. (*p).name
C. (&p).name
D. p->name
参考答案:AB
4. 【初级】关于接口和类的说法,下面说法正确的是()
A. 一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口
B. 实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理
C. 类实现接口时,需要导入接口所在的包
D. 接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口
参考答案:ABD
5. 【初级】关于字符串连接,下面语法正确的是()
A. str := ‘abc’ + ‘123’
B. str := “abc” + “123”
C. str := ‘123’ + “abc”
D. fmt.Sprintf(“abc%d”, 123)
参考答案:BD
6. 【初级】关于协程,下面说法正确是()
A. 协程和线程都可以实现程序的并发执行
B. 线程比协程更轻量级
C. 协程不存在死锁问题
D. 通过 channel 来进行协程间的通信
参考答案:AD
7. 【中级】关于 init 函数,下面说法正确的是()
A. 一个包中,可以包含多个 init 函数
B. 程序编译时,先执行导入包的 init 函数,再执行本包内的 init 函数
C. main 包中,不能有 init 函数
D. init 函数可以被其他函数调用
参考答案:AB
8. 【初级】关于循环语句,下面说法正确的有()
A. 循环语句既支持 for 关键字,也支持 while 和 do-while
B. 关键字 for 的基本使用方法与 C/C++中没有任何差异
C. for 循环支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环
D. for 循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量
参考答案:CD
9. 【中级】对于函数定义:
}
下面对 add 函数调用正确的是()
A. add(1, 2)
B. add(1, 3, 7)
C. add([]int{1, 2})
D. add([]int{1, 3, 7}…)
参考答案:ABD
A. 16. type MyInt int 16. var i int = 1
var jMyInt = i
B.
type MyIntint
var i int= 1
var jMyInt = (MyInt)i
C.
type MyIntint
var i int= 1
var jMyInt = MyInt(i)
D.
type MyIntint
var i int= 1
var jMyInt = i.(MyInt)
参考答案:C
A. var i int = 10
B. var i = 10
C. i := 10
D. i = 10
参考答案:ABC
A. 20. const Pi float64 = 3.14159265358979323846
const zero= 0.0
B.
const (
size int64= 1024
eof = -1
)
C.
const (
ERR_ELEM_EXISTerror = errors.New(“element already exists”)
ERR_ELEM_NT_EXISTerror = errors.New(“element not exists”)
)
D.
const u, vfloat32 = 0, 3
const a,b, c = 3, 4, “foo”
参考答案:ABD
A. b = true
B. b = 1
C. b = bool(1)
D. b = (1 == 2)
参考答案:BC
}
A. 321
B. 32
C. 31
D. 13
参考答案:C
A. 条件表达式必须为常量或者整数
B. 单个 case 中,可以出现多个结果选项
C. 需要用 break 来明确退出一个 case
D. 只有在 case 中明确添加 fallthrough 关键字,才会继续执行紧跟的下一个 case
参考答案:BD
A. 方法施加的对象显式传递,没有被隐藏起来
B. golang 沿袭了传统面向对象编程中的诸多概念,比如继承、虚函数和构造函数
C. golang 的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达
D. 方法施加的对象不需要非得是指针,也不用非得叫 this
参考答案:ACD
A. 数组切片
B. map
C. channel
D. interface
参考答案:ABCD
A. 可以对指针进行自增或自减运算
B. 可以通过“&”取指针的地址
C. 可以通过“*”取指针指向的数据
D. 可以对指针进行下标运算
参考答案:BC
A. main 函数不能带参数
B. main 函数不能定义返回值
C. main 函数所在的包必须为 main 包
D. main 函数中可以使用 flag 包来获取和解析命令行参数
参考答案:ABCD
A. var x = nil
B. var x interface{} = nil
C. var x string = nil
D. var x error = nil
参考答案:BD
A. s := make([]int)
B. s := make([]int, 0)
C. s := make([]int, 5, 10)
D. s := []int{1, 2, 3, 4, 5}
参考答案:BCD
A. 38. func (s *Slice)Remove(value interface{})error { 38. for i, v := range *s { 38. if isEqual(value, v) { 38. if i== len(*s) - 1 { 38. _s = (_s)[:i] 38. }else { 38. _s = append((_s)[:i],(*s)[i + 2:]…) 38. } 38. return nil 38. } 38. } 38. return ERR_ELEM_NT_EXIST
}
B.
func (s*Slice)Remove(value interface{}) error {
for i, v:= range *s {
if isEqual(value, v) {
_s =append((_s)[:i],(*s)[i + 1:])
return nil
}
}
returnERR_ELEM_NT_EXIST
}
C.
func (s*Slice)Remove(value interface{}) error {
for i, v:= range *s {
if isEqual(value, v) {
delete(*s, v)
return nil
}
}
returnERR_ELEM_NT_EXIST
}
D.
func (s*Slice)Remove(value interface{}) error {
for i, v:= range *s {
if isEqual(value, v) {
_s =append((_s)[:i],(*s)[i + 1:]…)
return nil
}
}
returnERR_ELEM_NT_EXIST
}
参考答案:D
A. 51. x := []int{ 51. 1, 2, 3, 51. 4, 5, 6,
}
B.
x :=[]int{
1, 2, 3,
4, 5, 6
}
C.
x :=[]int{
1, 2, 3,
4, 5, 6}
D.
x :=[]int{1, 2, 3, 4, 5, 6,}
参考答案:ACD
A. 55. i := 1
i++
B.
i := 1
j = i++
C.
i := 1
++i
D.
i := 1
i–
参考答案:AD
A. func f(a, b int) (value int, err error)
B. func f(a int, b int) (value int, err error)
C. func f(a, b int) (value int, error)
D. func f(a int, b int) (int, int, error)
参考答案:C
}
则 Add 函数定义正确的是()
A.
typeInteger int
func (aInteger) Add(b Integer) Integer {
return a + b
}
B.
typeInteger int
func (aInteger) Add(b *Integer) Integer {
return a + *b
}
C.
typeInteger int
func (a*Integer) Add(b Integer) Integer {
return *a + b
}
D.
typeInteger int
func (a_Integer) Add(b _Integer) Integer {
return _a + _b
}
参考答案:AC
}
则 Add 函数定义正确的是()
A.
typeInteger int
func (a Integer)Add(b Integer) Integer {
return a + b
}
B.
typeInteger int
func (aInteger) Add(b *Integer) Integer {
return a + *b
}
C.
typeInteger int
func (a*Integer) Add(b Integer) Integer {
return *a + b
}
D.
typeInteger int
func (a_Integer) Add(b _Integer) Integer {
return _a + _b
}
参考答案:A
}
A. var fragment Fragment =new(GetPodAction)
B. var fragment Fragment = GetPodAction
C. var fragment Fragment = &GetPodAction{}
D. var fragment Fragment = GetPodAction{}
参考答案:ACD
A. GoMock 可以对 interface 打桩
B. GoMock 可以对类的成员函数打桩
C. GoMock 可以对函数打桩
D. GoMock 打桩后的依赖注入可以通过 GoStub 完成
参考答案:AD
A. 只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等价的,可以相互赋值
B. 如果接口 A 的方法列表是接口 B 的方法列表的子集,那么接口 B 可以赋值给接口 A
C. 接口查询是否成功,要在运行期才能够确定
D. 接口赋值是否可行,要在运行期才能够确定
参考答案:ABC
A. var ch chan int
B. ch := make(chan int)
C. <- ch
D. ch <-
参考答案:ABC
A. 当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖的等待,除非该 goroutine 释放这个 Mutex
B. RWMutex 在读锁占用的情况下,会阻止写,但不阻止读
C. RWMutex 在写锁占用情况下,会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占
D. Lock()操作需要保证有 Unlock()或 RUnlock()调用与之对应
参考答案:ABC
A. 指针
B. channel
C. complex
D. 函数
参考答案:BCD
A. 基本思路是将引用的外部包的源代码放在当前工程的 vendor 目录下面
B. 编译 go 代码会优先从 vendor 目录先寻找依赖包
C. 可以指定引用某个特定版本的外部包
D. 有了 vendor 目录后,打包当前的工程代码到其他机器的$GOPATH/src 下都可以通过编译
参考答案:ABD
A. if flag == 1
B. if flag
C. if flag == false
D. if !flag
参考答案:BD
A. if value == 0
B. if value
C. if value != 0
D. if !value
参考答案:AC
A. 如果失败原因只有一个,则返回 bool
B. 如果失败原因超过一个,则返回 error
C. 如果没有失败原因,则不返回 bool 或 error
D. 如果重试几次可以避免失败,则不要立即返回 bool 或 error
参考答案:ABCD
A. 在程序开发阶段,坚持速错,让程序异常崩溃
B. 在程序部署后,应恢复异常避免程序终止
C. 一切皆错误,不用进行异常设计
D. 对于不应该出现的分支,使用异常处理
参考答案:ABD
A. 91. var s []int
s =append(s,1)
B.
var mmap[string]int
m[“one”]= 1
C.
var s[]int
s =make([]int, 0)
s =append(s,1)
D.
var mmap[string]int
m =make(map[string]int)
m[“one”]= 1
参考答案:ACD
A. 给一个 nil channel 发送数据,造成永远阻塞
B. 从一个 nil channel 接收数据,造成永远阻塞
C. 给一个已经关闭的 channel 发送数据,引起 panic
D. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
参考答案:ABCD
A. 无缓冲的 channel 是默认的缓冲为 1 的 channel
B. 无缓冲的 channel 和有缓冲的 channel 都是同步的
C. 无缓冲的 channel 和有缓冲的 channel 都是非同步的
D. 无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的
参考答案:D
A. 空指针解析
B. 下标越界
C. 除数为 0
D. 调用 panic 函数
参考答案:ABCD
A. array
B. slice
C. map
D. channel
参考答案:ABD
A. beego 是一个 golang 实现的轻量级 HTTP 框架
B. beego 可以通过注释路由、正则路由等多种方式完成 url 路由注入
C. 可以使用 bee new 工具生成空工程,然后使用 bee run 命令自动热编译
D. beego 框架只提供了对 url 路由的处理,而对于 MVC 架构中的数据库部分未提供框架支持
参考答案:ABC
A. goconvey 是一个支持 golang 的单元测试框架
B. goconvey 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到 web 界面
C. goconvey 提供了丰富的断言简化测试用例的编写
D. goconvey 无法与 go test 集成
参考答案:ABC
A. go vet 是 golang 自带工具 go tool vet 的封装
B. 当执行 go vet database 时,可以对 database 所在目录下的所有子文件夹进行递归检测
C. go vet 可以使用绝对路径、相对路径或相对 GOPATH 的路径指定待检测的包
D. go vet 可以检测出死代码
参考答案:ACD
100. 【中级】关于 map,下面说法正确的是()
A. map 反序列化时 json.unmarshal 的入参必须为 map 的地址
B. 在函数调用中传递 map,则子函数中对 map 元素的增加不会导致父函数中 map 的修改
C. 在函数调用中传递 map,则子函数中对 map 元素的修改不会导致父函数中 map 的修改
D. 不能使用内置函数 delete 删除 map 的元素
参考答案:A
101. 【中级】关于 GoStub,下面说法正确的是()
A. GoStub 可以对全局变量打桩
B. GoStub 可以对函数打桩
C. GoStub 可以对类的成员方法打桩
D. GoStub 可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为
参考答案:ABD
102. 【初级】关于 select 机制,下面说法正确的是()
A. select 机制用来处理异步 IO 问题
B. select 机制最大的一条限制就是每个 case 语句里必须是一个 IO 操作
C. golang 在语言级别支持 select 关键字
D. select 关键字的用法与 switch 语句非常类似,后面要带判断条件
参考答案:ABC
103. 【初级】关于内存泄露,下面说法正确的是()
A. golang 有自动垃圾回收,不存在内存泄露
B. golang 中检测内存泄露主要依靠的是 pprof 包
C. 内存泄露可以在编译阶段发现
D. 应定期使用浏览器来查看系统的实时内存信息,及时发现内存泄露问题
参考答案:BD
填空题
1. 【初级】声明一个整型变量 i**__**
参考答案:var i int
2. 【初级】声明一个含有 10 个元素的整型数组 a**__**
参考答案:var a [10]int
3. 【初级】声明一个整型数组切片 s**__**
参考答案:var s []int
4. 【初级】声明一个整型指针变量 p**__**
参考答案:var p *int
5. 【初级】声明一个 key 为字符串型 value 为整型的 map 变量 m**__**
参考答案:var m map[string]int
6. 【初级】声明一个入参和返回值均为整型的函数变量 f**__**
参考答案:var f func(a int) int
7. 【初级】声明一个只用于读取 int 数据的单向 channel 变量 ch**__**
参考答案:var ch <-chan int
8. 【初级】假设源文件的命名为 slice.go,则测试文件的命名为**__**
参考答案:slice_test.go
9. 【初级】 go test 要求测试函数的前缀必须命名为**__**
参考答案:Test
}
参考答案:4 3 2 1 0
}
参考答案:21
【中级】下面的程序的运行结果是**__**
func main() {
strs := []string{“one”,”two”, “three”}
for _, s := range strs {
go func() {
time.Sleep(1 * time.Second)
fmt.Printf(“%s “, s)
}()
}
time.Sleep(3 * time.Second)
}
参考答案:three threethree
}
参考答案:012
}
参考答案:abc
}
参考答案:21
}
参考答案:2
参考答案:go
}
参考答案:132
判断题
1. 【初级】数组是一个值类型()
参考答案:T
2. 【初级】使用 map 不需要引入任何库()
参考答案:T
3. 【中级】内置函数 delete 可以删除数组切片内的元素()
参考答案:F
4. 【初级】指针是基础类型()
参考答案:F
5. 【初级】 interface{}是可以指向任意对象的 Any 类型()
参考答案:T
6. 【中级】下面关于文件操作的代码可能触发异常()
7. file, err := os.Open(“test.go”)
8. defer file.Close()
9. if err != nil {
…
参考答案:T
参考答案:F
参考答案:T
参考答案:F
json:"x"
json:"y"
json:"z"
}
参考答案:T
参考答案:T
参考答案:F
参考答案:F
}
参考答案:F
参考答案:T
}
参考答案:F
参考答案:T
参考答案:T
参考答案:T
参考答案:T
参考答案:F
参考答案:F
参考答案:T
参考答案:F
参考答案:T
参考答案:T
参考答案:T
参考答案:T
参考答案:T
【中级】当函数 deferDemo 返回失败时,并不能 destroy 已 create 成功的资源()
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
return nil
}
参考答案:F
参考答案:F
参考答案:F
最近在很多地方看到了 golang 的面试题,看到了很多人对 Golang 的面试题心存恐惧,也是为了复习基础,我把解题的过程总结下来。
面试题
1 写出下面代码输出内容。
package main
import (
“fmt”
)
funcmain() {
defer_call()
}
funcdefer_call() {
deferfunc() {fmt.Println(“打印前”)}()
deferfunc() {fmt.Println(“打印中”)}()
deferfunc() {fmt.Println(“打印后”)}()
panic(“触发异常”)
}
考点:defer 执行顺序
解答:
defer 是后进先出。
panic 需要等 defer 结束后才会向上传递。 出现 panic 恐慌时候,会先按照 defer 的后入先出的顺序执行,最后才会执行 panic。
打印后
打印中
打印前
panic: 触发异常
2 以下代码有什么问题,说明原因。
type student struct {
Name string
Age int
}
funcpasestudent() {
m := make(map[string]\student)
stus := []student{
{Name: “zhou”,Age: 24},
{Name: “li”,Age: 23},
{Name: “wang”,Age: 22},
} for *,stu := range stus {
m[stu.Name] =&stu
}
}
考点:foreach
解答:
这样的写法初学者经常会遇到的,很危险! 与 Java 的 foreach 一样,都是使用副本的方式。所以 m[stu.Name]=&stu 实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个 struct 的值拷贝。 就像想修改切片元素的属性:
for _, stu := rangestus {
stu.Age = stu.Age+10}
也是不可行的。 大家可以试试打印出来:
func pasestudent() {
m := make(map[string]\student)
stus := []student{
{Name: “zhou”,Age: 24},
{Name: “li”,Age: 23},
{Name: “wang”,Age: 22},
}
// 错误写法
for *,stu := range stus {
m[stu.Name] =&stu
}
fork,v:=range m{
println(k,”=>”,v.Name)
}
// 正确
for i:=0;i<len(stus);i++ {
m[stus[i].Name] = &stus[i]
}
fork,v:=range m{
println(k,”=>”,v.Name)
}
}
3 下面的代码会输出什么,并说明原因
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20) for i := 0; i < 10; i++ {
gofunc() {
fmt.Println(“A: “, i)
wg.Done()
}()
}
for i:= 0; i < 10; i++ {
gofunc(i int) {
fmt.Println(“B: “, i)
wg.Done()
}(i)
}
wg.Wait()
}
考点:go 执行的随机性和闭包
解答:
谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 但是 A:均为输出 10,B:从 0~9 输出(顺序不定)。 第一个 go func 中 i 是外部 for 的一个变量,地址不变化。遍历完成后,最终 i=10。 故 go func 执行时,i 的值始终是 10。
第二个 go func 中 i 是函数参数,与外部 for 中的 i 完全是两个变量。 尾部(i)将发生值拷贝,go func 内部指向值拷贝地址。
4 下面代码会输出什么?
type People struct{}func (p People)ShowA() {
fmt.Println(“showA”)
p.ShowB()
}
func(pPeople)ShowB() {
fmt.Println(“showB”)
}
typeTeacher struct {
People
}
func(t*Teacher)ShowB() {
fmt.Println(“teachershowB”)
}
funcmain() {
t := Teacher{}
t.ShowA()
}
考点:go 的组合继承
解答:
这是 Golang 的组合模式,可以实现 OOP 的继承。 被组合的类型 People 所包含的方法虽然升级成了外部类型 Teacher 这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 此时 People 类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者 Teacher 类型的功能。
showAshowB
5 下面代码会触发异常吗?请详细说明
func main() {
runtime.GOMAXPROCS(1)
int_chan := make(chanint, 1)
string_chan := make(chanstring, 1)
int_chan <- 1
string_chan <- “hello”
select {
case value := <-int_chan:
fmt.Println(value)
casevalue := <-string_chan:
panic(value)
}
}
考点:select 随机性
解答:
select 会随机选择一个可用通用做收发操作。 所以代码是有肯触发异常,也有可能不会。 单个 chan 如果无缓冲时,将会阻塞。但结合 select 可以在多个 chan 间等待执行。有三点原则:
select 中只要有一个 case 能 return,则立刻执行。
当如果同一时间有多个 case 均能 return 则伪随机方式抽取任意一个执行。
如果没有一个 case 能 return 则可以执行”default”块。
6 下面代码输出什么?
funccalc(indexstring, a, bint) int {
ret := a+ b
fmt.Println(index,a, b, ret)
return ret
}
funcmain() {
a := 1
b := 2
defer calc(“1”, a,calc(“10”, a, b)) a = 0
defer calc(“2”, a,calc(“20”, a, b)) b = 1
}
考点:defer 执行顺序
解答:
这道题类似第 1 题 需要注意到 defer 执行顺序和值传递 index:1 肯定是最后执行的,但是 index:1 的第三个参数是一个函数,所以最先被调用
calc(“10”,1,2)>10,1,2,3 执行 index:2 时,与之前一样,需要先调用 calc(“20”,0,2)>20,0,2,2 执行到 b=1 时候开始调用,index:2==>calc(“2”,0,2)>2,0,2,2 最后执行 index:1>calc(“1”,1,3)==>1,1,3,4
10 1 2 320 0 2 22 0 2 21 1 3 4
7 请写出以下输入内容
funcmain() {
s := make([]int,5)
s = append(s,1, 2, 3)
fmt.Println(s)
}
考点:make 默认值和 append
解答:
make 初始化是由默认值的哦,此处默认值为 0
[00000123]
大家试试改为:
s := make([]int, 0)
s = append(s, 1, 2, 3)
fmt.Println(s)//[1 2 3]
8 下面的代码有什么问题?
type UserAges struct {
ages map[string]int
sync.Mutex
}
func(uaUserAges)Add(name string, age int) {
ua.Lock()
deferua.Unlock()
ua.ages[name] = age
}
func(uaUserAges)Get(name string)int {
ifage, ok := ua.ages[name]; ok {
return age
}
return-1
}
考点:map 线程安全
解答:
可能会出现
fatal error: concurrent mapreadandmapwrite.
修改一下看看效果
func (ua *UserAges)Get(namestring)int {
ua.Lock()
deferua.Unlock()
ifage, ok := ua.ages[name]; ok {
return age
}
return-1
}
9. 下面的迭代会有什么问题?
func (set *threadSafeSet)Iter()<-chaninterface{} {
ch := make(chaninterface{})
gofunc() {
set.RLock()
for elem := range set.s {
ch <- elem
}
close(ch)
set.RUnlock()
}()
return ch
}
考点:chan 缓存池
解答:
看到这道题,我也在猜想出题者的意图在哪里。 chan?sync.RWMutex?go?chan 缓存池?迭代? 所以只能再读一次题目,就从迭代入手看看。 既然是迭代就会要求 set.s 全部可以遍历一次。但是 chan 是为缓存的,那就代表这写入一次就会阻塞。 我们把代码恢复为可以运行的方式,看看效果
package main
import (
“sync”
“fmt”)//下面的迭代会有什么问题?type threadSafeSet struct {
sync.RWMutex
s []interface{}
}
func(set*threadSafeSet)Iter() <-chaninterface{} {
//ch := make(chan interface{}) // 解除注释看看!
ch := make(chaninterface{},len(set.s))
gofunc() {
set.RLock()
forelem,value := range set.s {
ch <- elem
println(“Iter:”,elem,value)
} close(ch)
set.RUnlock()
}()
return ch
}
funcmain() {
th:=threadSafeSet{
s:[]interface{}{“1”,”2”},
}
v:=<-th.Iter()
fmt.Sprintf(“%s%v”,”ch”,v)
}
10 以下代码能编译过去吗?为什么?
package main
import ( “fmt”)
typePeople interface {
Speak(string) string
}
typeStduent struct{}
func(stu*Stduent)Speak(think string)(talk string) {
ifthink == “bitch” {
talk = “Youare a good boy”
} else {
talk = “hi”
}
return
}
funcmain() {
var peoPeople = Stduent{}
think := “bitch”
fmt.Println(peo.Speak(think))
}
考点:golang 的方法集
解答:
编译不通过! 做错了!?说明你对 golang 的方法集还有一些疑问。 一句话:golang 的方法集仅仅影响接口实现和方法表达式转化,与通过实例或者指针调用方法无关。
11 以下代码打印出来什么内容,说出为什么。
package main
import ( “fmt”)
typePeople interface {
Show()
}
typeStudent struct{}
func(stuStudent)Show() {
}
funclive()People {
var stuStudent
return stu
}
funcmain() { if live() == nil
{
fmt.Println(“AAAAAAA”)
} else {
fmt.Println(“BBBBBBB”)
}
}
考点:interface 内部结构
解答:
很经典的题! 这个考点是很多人忽略的 interface 内部结构。 go 中的接口分为两种一种是空的接口类似这样:
varininterface{}
另一种如题目:
type People interface {
Show()
}
他们的底层结构如下:
type eface struct { //空接口
type __type //类型信息
data unsafe.Pointer //指向数据的指针(go 语言中特殊的指针类型 unsafe.Pointer 类似于 c 语言中的 void)}
typeiface struct { //带有方法的接口
tab itab //存储 type 信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go 语言中特殊的指针类型 unsafe.Pointer 类似于 c 语言中的 void)}
type_type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据 hash 值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值 kind 等于 0 是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte str nameOff
ptrToThis typeOff
}type itab struct {
inter _interfacetype //接口类型
_type __type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小方法集合}
可以看出 iface 比 eface 中间多了一层 itab 结构。 itab 存储_type 信息和[]fun 方法集,从上面的结构我们就可得出,因为 data 指向了 nil 并不代表 interface 是 nil, 所以返回值并不为空,这里的 fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口 结果:
BBBBBBB 12.是否可以编译通过?如果通过,输出什么?
func main() {
i := GetValue() switch i.(type) {
caseint:
println(“int”)
casestring:
println(“string”)
caseinterface{}:
println(“interface”)
default:
println(“unknown”)
}
}
funcGetValue()int {
return1
}
解析
考点:type
编译失败,因为 type 只能使用在 interface
13.下面函数有什么问题?
func funcMui(x,y int)(sum int,error){
returnx+y,nil
}
解析
考点:函数返回值命名
在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有 sum 名称,第二个未命名,所以错误。
14.是否可以编译通过?如果通过,输出什么?
package mainfunc main() { println(DeferFunc1(1)) println(DeferFunc2(1)) println(DeferFunc3(1))
}func DeferFunc1(i int)(t int) {
t = i deferfunc() {
t += 3
}() return t
}
funcDeferFunc2(i int)int {
t := i deferfunc() {
t += 3
}() return t
}
funcDeferFunc3(i int)(t int) { deferfunc() {
t += i
}() return2}
解析
考点:defer 和函数返回值
需要明确一点是 defer 需要在函数结束前执行。 函数返回值名字会在函数起始处被初始化为对应类型的零值并且作用域为整个函数 DeferFunc1 有函数返回值 t 作用域为整个函数,在 return 之前 defer 会被执行,所以 t 会被修改,返回 4; DeferFunc2 函数中 t 的作用域为函数,返回 1;DeferFunc3 返回 3
15.是否可以编译通过?如果通过,输出什么?
funcmain() { list := new([]int)
list = append(list,1)
fmt.Println(list)
}
解析
考点:new
list:=make([]int,0)
16.是否可以编译通过?如果通过,输出什么?
package mainimport “fmt”funcmain() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1,s2)
fmt.Println(s1)
}
解析
考点:append
append 切片时候别漏了’…’
17.是否可以编译通过?如果通过,输出什么?
func main() {
sn1 := struct {
age int
name string
}{age: 11,name: “qq”}
sn2 := struct {
age int
name string
}{age: 11,name: “qq”} if sn1== sn2 {
fmt.Println(“sn1== sn2”)
}
sm1 := struct {
age int
m map[string]string
}{age: 11, m:map[string]string{“a”: “1”}}
sm2 := struct {
age int
m map[string]string
}{age: 11, m:map[string]string{“a”: “1”}}
if sm1 == sm2 {
fmt.Println(“sm1== sm2”)
}
}
解析
考点:结构体比较
进行结构体比较时候,只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。
sn3:= struct {
name string
age int
}
{age:11,name:”qq”}
sn3 与 sn1 就不是相同的结构体了,不能比较。 还有一点需要注意的是结构体是相同的,但是结构体属性中有不可以比较的类型,如 map,slice。 如果该结构属性都是可以比较的,那么就可以使用“==”进行比较操作。
可以使用 reflect.DeepEqual 进行比较
if reflect.DeepEqual(sn1, sm) {
fmt.Println(“sn1==sm”)
}else {
fmt.Println(“sn1!=sm”)
}
所以编译不通过: invalid operation: sm1 == sm2
18.是否可以编译通过?如果通过,输出什么?
func Foo(x interface{}) { if x== nil {
fmt.Println(“emptyinterface”)
return
}
fmt.Println(“non-emptyinterface”)
}
funcmain() {
var x *int = nil
Foo(x)
}
解析
考点:interface 内部结构
non-emptyinterface
19.是否可以编译通过?如果通过,输出什么?
func GetValue(m map[int]string, id int)(string, bool) {
if _,exist := m[id]; exist {
return”存在数据”, true
}
returnnil, false}funcmain() {
intmap:=map[int]string{
1:”a”,
2:”bb”,
3:”ccc”,
}
v,err:=GetValue(intmap,3)
fmt.Println(v,err)
}
解析
考点:函数返回值类型
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。报:cannot use nil as type string in return argument.
20.是否可以编译通过?如果通过,输出什么?
const (
x = iota
y
z = “zz”
k
p = iota)
funcmain()
{
fmt.Println(x,y,z,k,p)
}
解析
考点:iota
结果:
0 1 zz zz 4
21.编译执行下面代码会出现什么?
package mainvar(
size :=1024
max_size = size*2)
funcmain() {
println(size,max_size)
}
解析
考点:变量简短模式
变量简短模式限制:
定义变量同时显式初始化
不能提供数据类型
只能在函数内部使用
结果:
syntaxerror: unexpected :=
22.下面函数有什么问题?
package main
const cl = 100
var bl = 123
funcmain() {
println(&bl,bl)
println(&cl,cl)
}
解析
考点:常量
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用,
cannot take the address of cl
23.编译执行下面代码会出现什么?
package main
funcmain() {
for i:=0;i<10;i++ {
loop:
println(i)
} gotoloop
}
解析
考点:goto
goto 不能跳转到其他函数或者内层代码
goto loop jumps intoblock starting at
24.编译执行下面代码会出现什么?
package main
import”fmt”
funcmain() {
typeMyInt1 int
typeMyInt2 = int
var i int =9
var i1MyInt1 = i
var i2MyInt2 = i
fmt.Println(i1,i2)
}
解析
考点:**Go 1.9 新特性 Type Alias **
基于一个类型创建一个新类型,称之为 defintion;基于一个类型创建一个别名,称之为 alias。 MyInt1 为称之为 defintion,虽然底层类型为 int 类型,但是不能直接赋值,需要强转; MyInt2 称之为 alias,可以直接赋值。
结果:
cannot use i (typeint) astype MyInt1 in assignment
25.编译执行下面代码会出现什么?
package main
import”fmt”
typeUser struct {
}
typeMyUser1 User
typeMyUser2 = User
func(iMyUser1)m1(){
fmt.Println(“MyUser1.m1”)
}
func(iUser)m2(){
fmt.Println(“User.m2”)
}
funcmain() {
var i1MyUser1
var i2MyUser2
i1.m1()
i2.m2()
}
解析
考点:**Go 1.9 新特性 Type Alias **
因为 MyUser2 完全等价于 User,所以具有其所有的方法,并且其中一个新增了方法,另外一个也会有。 但是
i1.m2()
是不能执行的,因为 MyUser1 没有定义该方法。 结果:
MyUser1.m1User.m2
26.编译执行下面代码会出现什么?
package main
import”fmt”
type T1 struct {
}
func(tT1)m1(){
fmt.Println(“T1.m1”)
}
type T2= T1
typeMyStruct struct {
T1
T2
}
funcmain() {
my:=MyStruct{}
my.m1()
}
解析
考点:**Go 1.9 新特性 Type Alias **
是不能正常编译的,异常:
ambiguousselectormy.m1
结果不限于方法,字段也也一样;也不限于 type alias,type defintion 也是一样的,只要有重复的方法、字段,就会有这种提示,因为不知道该选择哪个。 改为:
my.T1.m1()
my.T2.m1()
type alias 的定义,本质上是一样的类型,只是起了一个别名,源类型怎么用,别名类型也怎么用,保留源类型的所有方法、字段等。
27.编译执行下面代码会出现什么?
package main
import (
“errors”
“fmt”)
varErrDidNotWork = errors.New(“did not work”)
funcDoTheThing(reallyDoItbool)(errerror) {
ifreallyDoIt {
result, err:= tryTheThing()
if err!= nil || result != “it worked” {
err = ErrDidNotWork
}
} return err
}
functryTheThing()(string,error) {
return””,ErrDidNotWork
}
funcmain() {
fmt.Println(DoTheThing(true))
fmt.Println(DoTheThing(false))
}
解析
考点:变量作用域
因为 if 语句块内的 err 变量会遮罩函数作用域内的 err 变量,结果:
改为:
func DoTheThing(reallyDoIt bool)(errerror) {
varresult string
ifreallyDoIt {
result, err =tryTheThing()
if err!= nil || result != “it worked” {
err = ErrDidNotWork
}
} return err
}
28.编译执行下面代码会出现什么?
package main
functest() []func() {
varfuns []func()
fori:=0;i<2;i++ {
funs = append(funs,func() {
println(&i,i)
})
} returnfuns
}
funcmain(){
funs:=test()
for_,f:=range funs{
f()
}
}
解析
考点:闭包延迟求值
for 循环复用局部变量 i,每一次放入匿名函数的应用都是想一个变量。 结果:
0xc042046000 2
0xc042046000 2
如果想不一样可以改为:
func test() []func() {
varfuns []func()
fori:=0;i<2;i++ {
x:=i
funs = append(funs,func() {
println(&x,x)
})
} returnfuns
}
29.编译执行下面代码会出现什么?
package main
functest(x int)(func(),func()) {
returnfunc() {
println(x)
x+=10
}, func() {
println(x)
}
}
funcmain() {
a,b:=test(100)
a()
b()
}
解析
考点:闭包引用相同变量*
结果:
100
110
package main
import ( “fmt”
“reflect”)
funcmain1() {
deferfunc() {
iferr:=recover();err!=nil{
fmt.Println(err)
}else {
fmt.Println(“fatal”)
}
}()
deferfunc() {
panic(“deferpanic”)
}()
panic(“panic”)
}
funcmain() {
deferfunc() {
iferr:=recover();err!=nil{
fmt.Println(“++++”)
f:=err.(func()string)
fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())
}else {
fmt.Println(“fatal”)
}
}()
deferfunc() {
panic(func()string {
return “defer panic”
})
}()
panic(“panic”)
}
解析
考点:panic 仅有最后一个可以被 revover 捕获
触发 panic(“panic”)后顺序执行 defer,但是 defer 中还有一个 panic,所以覆盖了之前的 panic(“panic”)
————————————————
版权声明:本文为 CSDN 博主「尹成」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/itcastcpp/java/article/details/80462619
对于技术来说,每一个系统都少不了最重要的一个环节,系统设计。
系统设计的成功与否直接决定了后续的系统开发能否成功。
这次的分享,我们将系统设计的方法简化为三板斧,简单易懂,居家旅行学习进修必备。
三包承诺:包记住,包理解,包会用。如有问题,请再看一遍。如想快速浏览,可以先看总论
The process of defining the 定义(产出)
architecture, 架构
components, 组件
modules, 模块
interfaces, 接口
and data 数据
for a system to satisfy specified requirements. 需求(目标)
软件开发流程,详细内容可以参考: https://baike.baidu.com/item/软件开发流程
需求分析就是一个很专业的工作,对于大型的软件开发,也有专门的需求分析师,所以这里面的方法和流程也还有很多细分的内容,有兴趣的同学可以继续挖掘。
系统设计主要就是 2、3 两部分,大学毕业设计的时候,应该也都有深刻的印象,自己的文档是怎么写出来,怎么组织和编排的吧。
系统设计的内容可以用 16 或者 32 课时来专门开一课,大学时候应该也有这样的课,可能是选修吧。
详细内容可以参考: 从 0 到 1 教你设计业务系统 https://36kr.com/p/5105245
上面的软件开发流程和系统设计流程,看着就很复杂,分工很细,要完全遵守这些方法来做互联网产品开发的话,很多人是无法想象的,2 周上线,1 天 1 个版本?
所以,上面的流程都是来自传统的企业软件开发方法。而传统软件的开发周期,最少也是半年、一年的周期,交付物也都会比较严格,也不会有持续迭代一说。
传统企业软件开发,有很多是外包公司,规模很大,类似富士康的工厂。而规模的基础就需要标准化,遵守各种认证体系的要求,这样也更能被企业认可和信任。
但是,这些流程、方法,对于我们互联网研发就很不合适,系统设计也是。所以,才有了本文的核心方法。
实体模型,数据结构
组件模块,功能需求
系统规模,开发周期,架构设计
宏观上的面,提炼和定义,不拘泥细节。
从繁杂的系统中剥离、提炼,抽象出来我们系统的核心,定义好系统的人、事、物,也就有了系统的大致样貌。
抽象,这是最重要的一步,也是最厉害的一招。
对象的属性,数据表
功能模块,任务清单
性能、并发、伸缩、扩展、安全
微观实现的点,为后续的编码扫平模糊。
系统设计在这一招完成后,基本上就要全面完工了,交付给开发来编码。如果这时候还有不清晰的地方,还有遗留的点,那么后续就会出现很严重的需求变更、技术变更、甚至重构,会严重影响项目开发的周期和质量。
具象,这是需要细致且耐心的一步,也是势大力沉的一招。
让功能细节发生互动(业务恋爱)
让对象之间发生关系(数据怀孕)
模拟的运转和系统压力评估(成长烦恼)
流程不通,操作不顺,系统瓶颈,及早发现和补充完善。
纯脑力运算的过程,可能是设计者来做,也可能是要系统相关人一起来做。
这里用时 2 小时或者一起开个会,把系统设计的方方面面都模拟的运转起来,各个环节,各个角色带入进来,发现问题马上调整前面的抽象和具象的设计。
演算是一个快速查缺补漏的环节,也是对系统设计进行方案优化、调整的最后环节,这里投入的每一分钟都是十倍百倍于后续研发阶段的调整时间效率。
演算,这是最容易忽略的一步,也是最难的一招。
围绕系统设计中的人、事、物,我们通过宏观层面的抽象,提炼和定义出来核心内容,在通过微观层面的具象,把各个细节补充完善,最后,运用演算,让这个系统在我们的脑子里面活起来,重复这三板斧,完善我们的系统设计。
介绍:权限系统管理着应用和服务之间的调用关系。应用要调用服务的接口,必须先申请对应的服务接口权限才可以。
应用,服务
接口,应用请求服务
权限,接口权限
网关,系统平台,第三方服务等
提炼权限系统内部的主体对象,确定系统的数据模型。
应用端申请,服务端审批,查询
服务信息和扩展权限的配置
消息通知,权限推送
定义必须的应用的全后端功能,扩展的与外部系统的交互功能。
系统并发量很有限,性能要求低
系统的安全和稳定性高
扩展性要求高
系统的架构设计不必过于复杂,保证应用程序的安全和稳定性,定义好系统的可扩展性。
应用相关
服务相关
接口相关
申请相关
审批相关
标准字段
服务信息
推送相关
外部相关
扩展相关
标准字段
数据模型
新旧数据
标准字段
细化每一个功能的处理过程,要结合交互页面,操作流程,数据输入和产出等。
前端展现和交互,后端数据和逻辑,前后端耦合度低
定制化多端推送,扩展能力强
资源伸缩性强
架构演化:单机 -> 分布式,单库 -> 主从库 -> 缓存层 -> 数据分拆(水平、垂直) -> 应用分拆(微服务)
架构选择,只有合适与否,没有好坏之分。
方便查找服务,方便申请接口权限
方便查找服务,方便申请接口权限
申请、审批和推送、通知,把开发者、服务方、网关、服务都串联起来
流程,让整个系统运转起来,是一个活的系统,不再是冷冰冰的数据结构和系统框架。
考虑开发者和服务方的用户,带入用户的立场和操作,更好地模拟整个系统的交互体验。
这里的实例比较简单,但毕竟是最近设计开发的一个系统。
而对于更加复杂的系统,方法还是互通的。
定义好,写下来,画下来,这就是一个系统设计和项目文档。
当然系统设计不仅仅是上面呈现出来的内容,还有关于接口,任务清单等内容。
一句话形容,从宏观到微观,互动更长久。
抽象,先从宏观层面做好抽象;
具象,再从微观层面补充完善细节;
演算,全流程的模拟,查缺补漏;
重复上述三板斧,不断完善系统设计。
注意:不用纠结未知和差异化太大的部分。
系统设计的三板斧,相信不仅仅可以用在系统设计中,在其他的很多方面应该也是类似,比如:写作。
主题 -> 目标
创意 -> 抽象(人、事、物)
细节 -> 具象(有血有肉)
故事 -> 演算(活灵活现)
在最后,强调一遍,抽象和推理是数学的重要能力,数学绝对是极其重要的学科,掌握好数学,能够更好地掌握科学方法,而不仅仅只有个人的经验和直觉。
【数据库】
一个典型的互联网应用,前端服务器可以利用负载均衡服务,组成一个集群,但是只有一个 mysql 主库,这时候,mysql 服务就是系统中依赖的关键服务。
关键服务的性能和波动将成为整个系统的性能瓶颈和主要问题源。
为了减轻只有一个 mysql 主库的依赖,我们引入了一主多从的架构,让更多从库也来提供查询服务,减轻主库的查询依赖。
这样升级改造后,系统的性能和稳定性还是有明显的提升,但是 mysql 查询的复杂度和数量增加后,mysql 的性能瓶颈依然会很突出。
【缓存层】
于是,继续升级,引入 redis 或者 memcache,将大量的结果缓存起来,把应用尽量从 mysql 隔离,减少对数据库的依赖。
这时候,我们会欣喜的发现,应用的性能比之前完全依赖 mysql 的时候要强好多,稳定性也响应的提高很多。
随着我们的系统越来越复杂,访问量越来越大,缓存层的压力也会越来越大。
一方面是内存不足的问题,另一方面数据更新更复杂,还有就是访问压力以及内网带宽的瓶颈也增加了。
这时候又要开始对缓存层继续升级改造了,于是分布式缓存集群也就出现了。
通过一致性 hash 算法或者简单的散列方法,都可以很容易的增加 redis/memcache 服务来构建更大规模的集群。
这样一来,随着服务器增加,单机的内存瓶颈减轻了,访问压力以及带宽压力也降低了,但是数据更新的问题依然存在。
数据更新的问题,就是数据不一致的问题,本质上就是因为一个数据的多次写入,中间出现异常的话,就导致出现不一致了。
关于数据一致性问题,我们后面再来详细分析和讲解。
随着缓存层集群的构建,整个系统架构就变成了多前端服务器,多缓存服务器,一个主库,多个从库。
主库主要承载数据写入的负载,在大部分的互联网应用中,写入的数量相对还是少很多的,所以大部分时候,这样的架构也就可以安枕无忧啦。
【更大规模】
我们的追求不仅是眼前的苟且,还有更强大的系统和更多的用户。
当我们的应用,用户数、访问量过千万之后,以前的架构还是会遇到越来越多的挑战。
这里面,可能数据库还是会第一个出现瓶颈,毕竟一个主库,再强大也是没法做到一秒钟数万次的更新,哪怕只是小小的点击数的增加。
这时候,就要开始考虑对数据库做分拆了,把一个数据库分拆为好几个数据库(分表分库)。
而分拆的方式,大家应该能想到的,比如:水平分拆和垂直分拆。
【水平分拆】
典型的水平分拆就是对数据做归档,按照日期(每年归档),把旧的数据迁移到归档库里面,访问量少,也不会有写入的压力。
水平分拆对整个系统的改造和变化相对来说是影响比较小的,毕竟数据结构没有变化,只是数据源增加了。
而这种分拆带来的明显效果就是,数据规模减少,数据库的读写性能都会有明显的提升,当然对整个应用的性能和稳定性都会有很大提高。
【垂直分拆】
如果只是对数据库做垂直分拆,只是把一部分数据表迁移到其他独立数据库中,比如:把用户相关的信息表迁移到用户库,把商品相关的产品表迁移到商品库,那这个分拆也不算难。
但是,往往伴随着系统规模的变大,应用的复杂度也会不断提高。
所以,这个时候垂直分拆,很可能会同时进行应用的分拆。
比如:把用户的登录注册、信息维护单独部署和维护,把商品信息的管理和维护单独部署。
这时候,应用也就同时进行了垂直分拆,即:把大的应用进行组件化、服务化。
【微服务】
到这个阶段,大家应该就开始感觉大一个系统的复杂了吧。
一开始说好的做一个互联网应用,而现在却出来了很多个应用和服务,他们之间又有很多关联,组成一个大型的系统。
这时候,系统中的关键服务依赖已经不仅仅是缓存层、数据库服务,而变成了一个个拆分之后的应用、微服务了。
这时候,系统的性能和稳定性就完全依赖各个微服务的性能和稳定性了。
如果,再把每个微服务按照上面的架构升级路线演练一遍,貌似又不那么困难。
但是,全部组合在一起,新的难点已经是对这些服务的监控、运维、故障排除等治理工作。
【畅想未来】
那么,系统继续升级的话,我想,可能就是自动化运维的工作会更多了吧。
一两个数据库宕机不用怕,主从自动切换,数据库集群秒级自动恢复。
几个缓存服务器网络不稳定也没关系,有备份的缓存可以先用着。
微服务的一些服务器不稳定,服务自动降级,并且在微服务稳定后自动恢复以及同步数据。
甚至一个机房断网、断电了,其他机房依然正常的提供全面的服务,不影响用户使用。
【总结】
关键服务依赖总是最重要的环节,也是最容易出问题的地方。
系统架构升级,正是对这些关键依赖的瓶颈进行针对性优化升级和改造。
应用从小变大,再分拆变小,从一个应用到很多个微服务,这些都是技术不断变革的过程。
规模化带来了带来了的总体容量和总体性能的提高,同时也带来了关于服务治理的新挑战。
那么,是不是开发系统都要用这么复杂的架构呢?
其实不是,上面的架构升级过程,其实是对线上问题不断发现和解决的过程,也是一个不得已的过程。如果一个系统的用户量、业务量不大,一开始就复用一套庞大的系统架构,那真的就费时费力,累死自己,完全没必要。所以,合适就好。
我们在程序设计时,有一个极其重要的非功能性指标:性能,总是无时无刻不缠绕在程序员的脑海,尤其是我们开发的面向大众的 Web 服务,网络接口等程序。
高性能的程序可以使用更少的服务器资源提供同样规模的用户请求(成本低),也可以更快的响应用户请求(体验好)。
当然,高性能的程序设计也会更加复杂,开发也有更大难度。
这次的内容,我们面向高性能程序设计方向,来讲一讲其中最核心最重要的缓存。
希望能够帮助大家更好的理解缓存为王的含义,也能更好的利用缓存,设计出高性能的程序。
头
颈椎
胸腹
关节
脚
哪种地铁闸机,占用空间小、过关快、体验好、可靠性好、安全性好?还有更多类型的闸机可以比较的。
关注程序性能,首先要关注单次请求的执行时间,10ms 的等待时长肯定是要比 100ms 的执行时间要更好。然后就是在压力测试下(并发&集群),我们会关注上面的吞吐率、吞吐量、TPS 这些关键指标。
CPU 密集型,如:数据排序
假设:单次请求耗时 Tms,服务器 CPU 数量 C 核,集群的服务器数量 S 台
QPS=1000/T_C_S (公式是理想状态,单机、分布式并发中无共享无状态)
IO 密集型,如:依赖大量网络 API/数据库/文件(IO 耗时)
假设:单次请求耗时 Tms,服务器 CPU 数量 C 核,集群的服务器数量 S 台,IO 耗时 1/2Tms
QPS=1000/(T-1/2T)_C_S(理想状态下,API 不是瓶颈)
服务线程数量预估
CPU 密集型,线程数量与 CPU 数量一致(redis)
IO 密集型,要考虑 IO 的开销,适当放大线程数量,如:1/2 时间在 IO 中,那么线程数量可以是 CPU 的 2 倍(Java Web 的线程数,PHP-fpm 的进程数)
增加 CPU 数量,涉及到并发编程。
增加服务器数量,涉及到分布式系统设计。
所以,提高系统性能,还需要提高并发编程的能力,提高分布式系统设计的能力。
减少 CPU 运算量
简化运算逻辑,优化算法(少循环,少编解码等)
简化数据结构,降低时间复杂度,减少内存复制
减少 IO 耗时
减少 API/数据库/文件的依赖
优化 API/数据库/文件的性能
利用缓存
缓存复杂运算后的结果
缓存 IO 的返回值
最好的优化手段就是砍需求,没有代码就有最好的性能。
增加的缓存空间
缓存 IO 返回值
缓存运算结果
缓存 IO 返回值以及运算结果
增加的处理逻辑
缓存数据的读取和验证
数据更新到缓存
减少的处理时间
减少 IO 耗时
减少大量的 CPU 运算
离 CPU 越近的数据,处理越快;减少的处理逻辑就是优化的时间。缓存就是这个法宝。
下面三种情况建议尽量使用缓存来做优化。
减少的处理时间显著(性能差异明显)
原来的逻辑太复杂,性能很低下,如:超过 50ms
原来的 IO 耗时太长,如:网络延时超过 50ms,或者 IO 处理耗时超过 50ms
增加空间有限(成本提高)
缓存的数据空间尽量小,如果实在很大,可以考虑把数据压缩后缓存,如:博文正文页(计算换空间)
缓存数据的位置,可以在进程内,外部服务进程,甚至文件、数据库中(缓存后速度比缓存前的性能提高明显才有益)
单个实例进程的容量尽量别太大(超过 16G,32G),以减小迁移、重启、故障造成的影响(运维的负担也不能忽视)
增加的处理有限(开发难度,运算次数)
避免缓存频繁失效(命中率太低)
避免缓存频繁更新(数据一致性复杂)
高性能程序设计,重点关注
方法一,减少单个请求的处理时间(程序优化)
方法二,增加 CPU,线性提高系统的吞吐率(并发编程)
方法三,增加集群的服务器,线性提高系统的吞吐率(分布式系统设计)
空间换时间,缓存的优势
场景一,缓存前的处理速度太慢,IO 耗时太长(超过 50ms)
场景二,缓存数据具有极高的命中率(超过 90%,理想是 100%)
避免缓存的陷阱
场景一,程序没有高性能需求,程序原本性能已经非常高(不要为缓存而优化)
场景二,缓存容量爆炸性增长(成本太高)
场景三,缓存数据更新太频繁(命中率低,数据一致性差)
CPU 内的寄存器/L1/L2/L3
计算机内存
更多参考: 并发编程与锁的底层原理
SATA 传来的数据和盘片的实际操作间加一个缓冲
缓存容量增加,带来的成本提高,突然掉电导致数据丢失的风险增大
发送缓存
接收缓存
缓冲文件系统
网络相关缓存设置
操作系统磁盘缓存,可以减少磁盘机械操作。
更多参考:
PHP 的缓存
Java 的缓存
编程语言的版本升级,我们最关注的除了语言特性的变化,还有就是关于性能的提升。其中有优化数据结构的,也有优化 GC 的,当然也有引入缓存/JIT 这些技术。
nginx 中的缓存
mysql 中的缓存
数据库缓存,减少文件系统 I/O。
更多参考:mysql 缓冲和缓存设置详解
分布式网络
Web 内容缓存
CDN,加速终端连接和请求速度,减少源站点压力
DNS 服务是典型的分布式分层缓存系统,高效可靠,当然也是非常核心的系统,大面积断网的事件就跟 DNS 故障有关。
更多参考:浏览器的 DNS 缓存
客户端浏览器缓存,减少对网站的访问。
Web 内容缓存
Cookie/LocalStorage/SessionStorage
更多参考:
缓冲区 buffer
缓冲队列
多级缓存,分布式缓存
问:在浏览器中输入一个网址 http://www.imooc.com/ ,接下来会发生什么?
先来看看 2 个典型的常见的软件系统。
数据模型
页面
操作
缓存数据
数据模型
页面
操作
缓存数据
数据只读,极少更新
数据占用空间有限
数据结构简单,容易快速查找
数据读多写少,读取速度慢
数据占用空间较大
保证较高的命中率,90%以上
读写过于频繁的时候
排序方式太多,索引效率下降的时候
数据表分拆太细,连表查询效率低的时候
内容更新太频繁,命中率太低
数据写入超过读取次数
系统自身性能很好
系统访问量很有限
提高缓存命中率
规划缓存容量
缓存性能优势
高性能程序设计与缓存的效果(连蒙带猜)
如果没有缓存的情况下,100 亿的客户端请求,最后落到数据服务器上会有上万亿的 IO 操作。
老司机箴言:
数据结构和算法(应用优化)
并发编程的问题(增加 CPU)
分布式系统设计(增加服务器)
缓存优化(空间换时间)
高性能程序设计,使用缓存来优化可能会是第一选择。只是,方法虽然简单,过程还是曲折的。每一次缓存设计,都还是要针对具体场景和需求,制定最合适的方案,要考虑的地方也还是有很多。
1 |
|
你可以通过试验来确定这个问题的答案。例如:先在一个源码文件中导入一个在你的机器上并不存在的代码包,然后编译这个代码文件。最后,将输出的编译错误信息与 GOPATH 的值进行对比。
如果在多个工作区中都存在导入路径相同的代码包会产生冲突吗?
答:不会产生冲突。因为代码包的查找是按照已给定的顺序逐一地在多个工作区中进行的。
默认情况下,我们可以让命令源码文件接受哪些类型的参数值?
答:这个问题通过查看 flag 代码包的文档就可以回答了。概括来讲,有布尔类型、整数类型、浮点数类型、字符串类型,以及 time.Duration 类型。
我们可以把自定义的数据类型作为参数值的类型吗?如果可以,怎样做?
答:狭义上讲是不可以的,但是广义上讲是可以的。这需要一些定制化的工作,并且被给定的参数值只能是序列化的。具体可参见 flag 代码包文档中的例子。
如果你需要导入两个代码包,而这两个代码包的导入路径的最后一级是相同的,比如:dep/lib/flag 和 flag,那么会产生冲突吗? 答:这会产生冲突。因为代表两个代码包的标识符重复了,都是 flag。
如果会产生冲突,那么怎样解决这种冲突?有几种方式?
答:接上一个问题。很简单,导入代码包的时候给它起一个别名就可以了,比如: import libflag “dep/lib/flag”。或者,以本地化的方式导入代码包,如:import . “dep/lib/flag”。
如果与当前的变量重名的是外层代码块中的变量,那么意味着什么?
答:这意味着这两个变量成为了“可重名变量”。在内层的变量所处的那个代码块以及更深层次的代码块中,这个变量会“屏蔽”掉外层代码块中的那个变量。
如果通过 import . XXX 这种方式导入的代码包中的变量与当前代码包中的变量重名了,那么 Go 语言是会把它们当做“可重名变量”看待还是会报错呢?
答:这两个变量会成为“可重名变量”。虽然这两个变量在这种情况下的作用域都是当前代码包的当前文件,但是它们所处的代码块是不同的。
当前文件中的变量处在该文件所代表的代码块中,而被导入的代码包中的变量却处在声明它的那个文件所代表的代码块中。当然,我们也可以说被导入的代码包所代表的代码块包含了这个变量。
在当前文件中,本地的变量会“屏蔽”掉被导入的变量。
除了《程序实体的那些事儿 3》一文中提及的那些,你还认为类型转换规则中有哪些值得注意的地方?
答:简单来说,我们在进行类型转换的时候需要注意各种符号的优先级。具体可参见 Go 语言规范中的转换部分。
你能具体说说别名类型在代码重构过程中可以起到的哪些作用吗?
答:简单来说,我们可以通过别名类型实现外界无感知的代码重构。具体可参见 Go 语言官方的文档 Proposal: Type Aliases。
如果是,那么问一下自己,这是你想要的结果吗?无论如何,通过这种方式来组织或共享数据是不正确的。你需要做的是,要么彻底切断这些切片的底层联系,要么立即为所有的相关操作加锁。
怎样沿用“扩容”的思想对切片进行“缩容”?
答:关于切片的“缩容”,可参看官方的相关 wiki。不过,如果你需要频繁的“缩容”,那么就可能需要考虑其他的数据结构了,比如:container/list 代码包中的 List。
container/ring 包中的循环链表的适用场景都有哪些?
答:比如:可重用的资源(缓存等)的存储,或者需要灵活组织的资源池,等等。
container/heap 包中的堆的适用场景又有哪些呢?
答:它最重要的用途就是构建优先级队列,并且这里的“优先级”可以很灵活。所以,想象空间很大。
字典类型的值是并发安全的吗?如果不是,那么在我们只在字典上添加或删除键-元素对的情况下,依然不安全吗?
答:字典类型的值不是并发安全的,即使我们只是增减其中的键值对也是如此。其根本原因是,字典值内部有时候会根据需要进行存储方面的调整。
通道的长度代表着什么?它在什么时候会通道的容量相同?
通道的长度代表它当前包含的元素值的个数。当通道已满时,其长度会与容量相同。
元素值在经过通道传递时会被复制,那么这个复制是浅表复制还是深层复制呢?
答:浅表复制。实际上,在 Go 语言中并不存在深层次的复制,除非我们自己来做。
如果在 select 语句中发现某个通道已关闭,那么应该怎样屏蔽掉它所在的分支?
答:很简单,把 nil 赋给代表了这个通道的变量就可以了。如此一来,对于这个通道(那个变量)的发送操作和接收操作就会永远被阻塞。
在 select 语句与 for 语句联用时,怎样直接退出外层的 for 语句?
答:这一般会用到 goto 语句和标签(label),具体请参看 Go 语言规范的这部分。
complexArray1 被传入函数的话,这个函数中对该参数值的修改会影响到它的原值吗?
答:文中 complexArray1 变量的声明如下:
1 | complexArray1 := [3][]string{ |
这要看怎样修改了。虽然 complexArray1 本身是一个数组,但是其中的元素却都是切片。如果对 complexArray1 中的元素进行增减,那么原值就不会受到影响。但若要修改它已有的元素值,那么原值也会跟着改变。
函数真正拿到的参数值其实只是它们的副本,那么函数返回给调用方的结果值也会被复制吗?
答:函数返回给调用方的结果值也会被复制。不过,在一般情况下,我们不用太在意。但如果函数在返回结果值之后依然保持执行并会对结果值进行修改,那么我们就需要注意了。
我们可以在结构体类型中嵌入某个类型的指针类型吗?如果可以,有哪些注意事项?
答:当然可以。在这时,我们依然需要注意各种“屏蔽”现象。由于某个类型的指针类型会包含与前者有关联的所有方法,所以我们更要注意。
另外,我们在嵌入和引用这样的字段的时候还需要注意一些冲突方面的问题,具体请参看 Go 语言规范的这一部分。
因此,我们可以把这样的值作为占位值来使用。比如:在同一个应用场景下,map[int] [int]bool 类型的值占用更少的存储空间。
如果我们把一个值为 nil 的某个实现类型的变量赋给了接口变量,那么在这个接口变量上仍然可以调用该接口的方法吗?如果可以,有哪些注意事项?如果不可以,原因是什么?
答:可以调用。但是请注意,这个被调用的方法在此时所持有的接收者的值是 nil。因此,如果该方法引用了其接收者的某个字段,那么就会引发 panic!
引用类型的值的指针值是有意义的吗?如果没有意义,为什么?如果有意义,意义在哪里?
答:从存储和传递的角度看,没有意义。因为引用类型的值已经相当于指向某个底层数据结构的指针了。当然,引用类型的值不只是指针那么简单。
用什么手段可以对 goroutine 的启用数量加以限制?
答:一个很简单且很常用的方法是,使用一个通道保存一些令牌。只有先拿到一个令牌,才能启用一个 goroutine。另外在 go 函数即将执行结束的时候还需要把令牌及时归还给那个通道。
更高级的手段就需要比较完整的设计了。比如,任务分发器+任务管道(单层的通道)+固定个数的 goroutine。又比如,动态任务池(多层的通道)+动态 goroutine 池(可由前述的那个令牌方案演化而来)。等等。
runtime 包中提供了哪些与模型三要素 G、P 和 M 相关的函数?
答:关于这个问题,我相信你一查文档便知。在线文档在这里。不过光知道还不够,还要会用。
在类型 switch 语句中,我们怎样对被判断类型的那个值做相应的类型转换?
答:其实这个事情可以让 Go 语言自己来做,例如:
1 | switch t := x.(type) { |
当流程进入到某个 case 子句的时候,变量 t 的值就已经被自动地转换为相应类型的值了。
在 if 语句中,初始化子句声明的变量的作用域是什么?
答:如果这个变量是新的变量,那么它的作用域就是当前 if 语句所代表的代码块。注意,后续的 else if 子句和 else 子句也包含在当前的 if 语句代表的代码块之内。
请列举出你经常用到或者看到的 3 个错误类型,它们所在的错误类型体系都是怎样的?你能画出一棵树来描述它们吗?
答:略。这需要你自己去做,我代替不了你。
请列举出你经常用到或者看到的 3 个错误值,它们分别在哪个错误值列表里?这些错误值列表分别包含的是哪个种类的错误?
答:略。这需要你自己去做,我代替不了你。
一个函数怎样才能把 panic 转化为 error 类型值,并将其作为函数的结果值返回给调用方?
答:可以这样编写:
1 | func doSomething() (err error) { |
注意结果声明的写法。这是一个带有名称的结果声明。
除了本文中提到的,你还知道或用过 testing.T 类型和 testing.B 类型的哪些方法?它们都是做什么用的?
答:略。这需要你自己去做,我代替不了你。
在编写示例测试函数的时候,我们怎样指定预期的打印内容?
答:这个问题的答案就在 testing 代码包的文档中。
-benchmem 标记和-benchtime 标记的作用分别是什么?
答:-benchmem 标记的作用是在性能测试完成后打印内存分配统计信息。-benchtime 标记的作用是设定测试函数的执行时间上限。
具体请看这里的文档。
你知道互斥锁和读写锁的指针类型都实现了哪一个接口吗?
答:它们都实现了 sync.Locker 接口。
怎样获取读写锁中的读锁?
答:sync.RWMutex 类型有一个名为 RLocker 的指针方法可以获取其读锁。
*sync.Cond 类型的值可以被传递吗?那 sync.Cond 类型的值呢?
答:sync.Cond 类型的值一旦被使用就不应该再被传递了,传递往往意味着拷贝。拷贝一个已经被使用的 sync.Cond 值会引发 panic。但是它的指针值是可以被拷贝的。
sync.Cond 类型中的公开字段 L 是做什么用的?我们可以在使用条件变量的过程中改变这个字段的值吗?
答:这个字段代表的是当前的 sync.Cond 值所持有的那个锁。我们可以在使用条件变量的过程中改变该字段的值,但是在改变之前一定要搞清楚这样做的影响。
如果要对原子值和互斥锁进行二选一,你认为最重要的三个决策条件应该是什么?
答:我觉得首先需要考虑下面几个问题。
被保护的数据是什么类型的?是值类型的还是引用类型的?
操作被保护数据的方式是怎样的?是简单的读和写还是更复杂的操作?
操作被保护数据的代码是集中的还是分散的?如果是分散的,是否可以变为集中的?
在搞清楚上述问题(以及你关注的其他问题)之后,优先使用原子值。
在使用 WaitGroup 值实现一对多的 goroutine 协作流程时,怎样才能让分发子任务的 goroutine 获得各个子任务的具体执行结果? 答:可以考虑使用锁+容器(数组、切片或字典等),也可以考虑使用通道。另外,你或许也可以用上 golang.org/x/sync/errgroup 代码包中的程序实体,相应的文档在这里。
Context 值在传达撤销信号的时候是广度优先的还是深度优先的?其优势和劣势都是什么?
答:它是深度优先的。其优势和劣势都是:直接分支的产生时间越早,其中的所有子节点就会越先接收到信号。至于什么时候是优势、什么时候是劣势还要看具体的应用场景。
例如,如果子节点的存续时间与资源的消耗是正相关的,那么这可能就是一个优势。但是,如果每个分支中的子节点都很多,而且各个分支中的子节点的产生顺序并不依从于分支的产生顺序,那么这种优势就很可能会变成劣势。最终的定论还是要看测试的结果。
最后,我们应该保证它的 New 字段所代表的值是可用的。虽然 New 函数返回的临时对象并不会被放入池中,但是起码能够保证池的 Get 方法总能返回一个临时对象。
关于保证并发安全字典中的键和值的类型正确性,你还能想到其他的方案吗?
答:这是一道开放的问题,需要你自己去思考。其实怎样做完全取决于你的应用场景。不过,我们应该尽量避免使用反射,因为它对程序性能还是有一定的影响的。
判断一个 Unicode 字符是否为单字节字符通常有几种方式?
答:unicode/utf8 代码包中有几个可以做此判断的函数,比如:RuneLen 函数、EncodeRune 函数等。我们需要根据输入的不同来选择和使用它们。具体可以查看该代码包的文档。
strings.Builder 和 strings.Reader 都分别实现了哪些接口?这样做有什么好处吗?
答:strings.Builder 类型实现了 3 个接口,分别是:fmt.Stringer、io.Writer 和 io.ByteWriter。
而 strings.Reader 类型则实现了 8 个接口,即:io.Reader、io.ReaderAt、io.ByteReader、io.RuneReader、io.Seeker、io.ByteScanner、io.RuneScanner 和 io.WriterTo。
好处是显而易见的。实现的接口越多,它们的用途就越广。它们会适用于那些要求参数的类型为这些接口类型的地方。
// String returns the accumulated string. func (b _Builder) String() string { return _(*string)(unsafe.Pointer(&b.buf)) } 数组值和字符串值在底层的存储方式其实是一样的。所以从切片值到字符串值的指针值的转换可以是直截了当的。又由于字符串值是不可变的,所以这样做也是安全的。
不过,由于一些历史、结构和功能方面的原因,bytes.Buffer 的 String 方法却不能这样做。
io.Pipe 函数会返回一个 io.PipeReader 类型的值和一个 io.PipeWriter 类型的值,并将它们分别作为管道的两端。而这两个值在底层其实只是代理了同一个*io.pipe 类型值的功能而已。
io.pipe 类型通过无缓冲的通道实现了读操作与写操作之间的同步,并且通过互斥锁实现了写操作之间的串行化。另外,它还使用原子值来处理错误。这些共同保证了这个同步内存管道的并发安全性。
比如,我们可以自定义每次扫描的边界,或者说内容的分段方法。我们在调用它的 Scan 方法对目标进行扫描之前,可以先调用其 Split 方法并传入一个函数来自定义分段方法。
在默认情况下,扫描器会以行为单位对目标内容进行扫描。bufio 代码包提供了一些现成的分段方法。实际上,扫描器在默认情况下会使用 bufio.ScanLines 函数作为分段方法。
又比如,我们还可以在扫描之前自定义缓存的载体和缓存的最大容量,这需要调用它的 Buffer 方法。在默认情况下,扫描器内部设定的最大缓存容量是 64K 个字节。
换句话说,目标内容中的每一段都不能超过 64K 个字节。否则,扫描器就会使它的 Scan 方法返回 false,并通过其 Err 方法给予我们一个表示“token too long”的错误值。这里的“token”代表的就是一段内容。
关于 bufio.Scanner 类型的更多特点和使用注意事项,你可以通过它的文档获得。
这两者都会返回一个*os.Process 类型的值。该类型提供了一些方法,比如,用于杀掉当前进程的 Kill 方法,又比如,可以给当前进程发送系统信号的 Signal 方法,以及会等待当前进程结束的 Wait 方法。
与此相关的还有 os.ProcAttr 类型、os.ProcessState 类型、os.Signal 类型,等等。你可以通过积极的实践去探索更多的玩法。
这三个方法的签名是一模一样的,只是名称不同罢了。它们都接受一个 time.Time 类型的参数,并都会返回一个 error 类型的结果。其中的 SetDeadline 方法是用来同时设置读操作超时和写操作超时的。
有一点需要特别注意,这三个方法都会针对任何正在进行以及未来将要进行的相应操作进行超时设定。
因此,如果你要在一个循环中进行读操作或写操作的话,最好在每次迭代中都进行一次超时设定。
否则,靠后的操作就有可能因触达超时时间而直接失败。另外,如果有必要,你应该再次调用它们并传入 time.Time 类型的零值来表达不再限定超时时间。
它会先关闭所有的空闲连接,并一直等待。只有活动的连接变为空闲之后,它才会关闭它们。当所有的连接都被平滑地关闭之后,它会关闭当前的服务器并返回。当有错误发生时,它还会把相应的错误值返回。
另外,你还可以通过调用 Server 值的 RegisterOnShutdown 方法来注册可以在服务器即将关闭时被自动调用的函数。
更确切地说,当前服务器的 Shutdown 方法会以异步的方式调用如此注册的所有函数。我们可以利用这样的函数来通知长连接的客户端“连接即将关闭”。
通过它们生成的跟踪记录可以通过 go tool trace 命令来查看。更具体的说明可以参看 runtime/trace 代码包的文档。
有了 runtime/trace 代码包,我们就可以为 Go 程序加装上可以满足个性化需求的跟踪器了。Go 语言标准库中有的代码包正是通过使用该包实现了自身的功能,例如 net/http/pprof 包。
好了,全部的思考题答案已经更新完了,你如果还有疑问,可以给我留言。祝你新春快乐,学习愉快。再见。
为了便于理解,本文从常用操作和概念开始讲起。虽然已经尽量做到简化,但是涉及到的内容还是有点多。在面试中,Linux 知识点相对于网络和操作系统等知识点而言不是那么重要,只需要重点掌握一些原理和命令即可。为了方便大家准备面试,在此先将一些比较重要的知识点列出来:
指令的基本用法与选项介绍。
man 是 manual 的缩写,将指令的具体信息显示出来。
当执行 man date
时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下:
代号 | 类型 |
---|---|
1 | 用户在 shell 环境中可以操作的指令或者可执行文件 |
5 | 配置文件 |
8 | 系统管理员可以使用的管理指令 |
info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以跳转。
/usr/share/doc 存放着软件的一整套说明文件。
在关机前需要先使用 who 命令查看有没有其它用户在线。
为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘,因此关机之前需要先进行 sync 同步操作。
-k : 不会关机,只是发送警告信息,通知所有在线的用户
-r : 将系统的服务停掉后就重新启动
-h : 将系统的服务停掉后就立即关机
-c : 取消已经在进行的 shutdown
可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin
sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。
RPM 和 DPKG 为最常见的两类软件包管理工具:
Linux 发行版是 Linux 内核及各种应用软件的集成版本。
基于的包管理工具 | 商业发行版 | 社区发行版 |
---|---|---|
RPM | Red Hat | Fedora / CentOS |
DPKG | Ubuntu | Debian |
在指令列模式下,有以下命令用于离开或者保存文件。
命令 | 作用 |
---|---|
:w | 写入磁盘 |
:w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 |
:q | 离开 |
:q! | 强制离开不保存 |
:wq | 写入磁盘后离开 |
:wq! | 强制写入磁盘后离开 |
GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议(GNU General Public License),包含了以下内容:
IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。
SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能。SATA-II 的接口速度为 300MiB/s,而 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。
SCSI 全称是 Small Computer System Interface(小型机系统接口),SCSI 硬盘广为工作站以及个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。
SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也通过缩小连接线改善系统内部空间等。
Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下:
其中文件名后面的序号的确定与系统检测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。
磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。
MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。
分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区来记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。
Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。
扇区是磁盘的最小存储单位,旧磁盘的扇区大小通常为 512 bytes,而最新的磁盘支持 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。
GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。
GPT 没有扩展分区概念,都是主分区,每个 LBA 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。
MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 2 TB = 8 ZB。
BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。
BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。
主要开机记录(MBR)中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。
下图中,第一扇区的主要开机记录(MBR)中的开机管理程序提供了两个选单:M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。
安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录(MBR),而 Linux 可以选择将开机管理程序安装在主要开机记录(MBR)或者其它分区的启动扇区,并且可以设置开机管理程序的选单。
BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。
最主要的几个组成部分如下:
除此之外还包括:
对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中查找文件内容所在的所有 block,然后把所有 block 的内容读出来。
而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。
指一个文件内容所在的 block 过于分散,导致磁盘磁头移动距离过大,从而降低磁盘读写性能。
在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。
大小 | 1KB | 2KB | 4KB |
---|---|---|---|
最大单一文件 | 16GB | 256GB | 2TB |
最大文件系统 | 2TB | 8TB | 16TB |
一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。
inode 具体包含以下信息:
inode 具有以下特点:
inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用让 inode 记录的引用 block 块记录引用信息。
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。
可以看到文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的写权限有关。
如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。
ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。
挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。
为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下:
用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。
使用 ls 查看一个文件时,会显示一个文件的信息,例如 drwxr-xr-x 3 root root 17 May 6 00:14 .config
,对这个信息的解释如下:
常见的文件类型及其含义有:
9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。
文件时间有以下三种:
列出文件或者目录的信息,目录的信息就是其中包含的文件。
-a :列出全部的文件
-d :仅列出目录本身
-l :以长数据串行列出,包含文件的属性与权限等等数据
更换当前目录。
1 | cd [相对路径或绝对路径] |
创建目录。
1 | # mkdir [-mp] 目录名称 |
删除目录,目录必须为空。
rmdir [-p] 目录名称
-p :递归删除目录
更新文件时间或者建立新文件。
-a : 更新 atime
-c : 更新 ctime,若该文件不存在则不建立新文件
-m : 更新 mtime
-d : 后面可以接更新日期而不使用当前日期,也可以使用 –date=”日期或时间”
-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm]
复制文件。如果源文件有两个以上,则目的文件一定要是目录才行。
cp [-adfilprsu] source destination
-a :相当于 -dr –preserve=all
-d :若来源文件为链接文件,则复制链接文件属性而非文件本身
-i :若目标文件已经存在时,在覆盖前会先询问
-p :连同文件的属性一起复制过去
-r :递归复制
-u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制
–preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了
删除文件。
-r :递归删除
移动文件。
-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖
可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。
示例:将 .bashrc 文件的权限修改为 -rwxr-xr–。
也可以使用符号来设定权限。
可以通过 umask 设置或者查看默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r–。
文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。
目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。
-s :默认是实体链接,加 -s 为符号链接
-f :如果目标文件存在时,先删除目标文件
在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。
删除任意一个条目,文件还是存在,只要引用数量不为 0。
有以下限制:不能跨越文件系统、不能对目录进行链接。
34474855 -rw-r–r–. 2 root root 451 Jun 10 2014 crontab
34474855 -rw-r–r–. 2 root root 451 Jun 10 2014 /etc/crontab
符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。
当源文件被删除了,链接文件就打不开了。
因为记录的是路径,所以可以为目录建立符号链接。
34474855 -rw-r–r–. 2 root root 451 Jun 10 2014 /etc/crontab
53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab
取得文件内容。
-n :打印出行号,连同空白行也会有行号,-b 不会
是 cat 的反向操作,从最后一行开始打印。
和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。
和 more 类似,但是多了一个向前翻页的功能。
取得文件前几行。
-n :后面接数字,代表显示几行的意思
是 head 的反向操作,只是取得是后几行。
以字符或者十六进制的形式显示二进制文件。
指令搜索。
-a :将所有指令列出,而不是只列第一个
文件搜索。速度比较快,因为它只搜索几个特定的目录。
文件搜索。可以用关键字或者正则表达式进行搜索。
locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。
-r:正则表达式
文件搜索。可以使用文件的属性和权限进行搜索。
example: find . -name “shadow*“
① 与时间有关的选项
-mtime n :列出在 n 天前的那一天修改过内容的文件
-mtime +n :列出在 n 天之前 (不含 n 天本身) 修改过内容的文件
-mtime -n :列出在 n 天之内 (含 n 天本身) 修改过内容的文件
-newer file : 列出比 file 更新的文件
+4、4 和 -4 的指示的时间范围如下:
② 与文件拥有者和所属群组有关的选项
-uid n
-gid n
-user name
-group name
-nouser :搜索拥有者不存在 /etc/passwd 的文件
-nogroup:搜索所属群组不存在于 /etc/group 的文件
③ 与文件权限和名称有关的选项
-name filename
-size [+-]SIZE:搜寻比 SIZE 还要大 (+) 或小 (-) 的文件。这个 SIZE 的规格有:c: 代表 byte,k: 代表 1024bytes。所以,要找比 50KB 还要大的文件,就是 -size +50k
-type TYPE
-perm mode :搜索权限等于 mode 的文件
-perm -mode :搜索权限包含 mode 的文件
-perm /mode :搜索权限包含任一 mode 的文件
Linux 底下有很多压缩文件名,常见的如下:
扩展名 | 压缩程序 |
---|---|
*.Z | compress |
*.zip | zip |
*.gz | gzip |
*.bz2 | bzip2 |
*.xz | xz |
*.tar | tar 程序打包的数据,没有经过压缩 |
*.tar.gz | tar 程序打包的文件,经过 gzip 的压缩 |
*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 |
*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 |
gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。
经过 gzip 压缩过,源文件就不存在了。
有 9 个不同的压缩等级可以使用。
可以使用 zcat、zmore、zless 来读取压缩文件的内容。
$ gzip [-cdtv#] filename
-c :将压缩的数据输出到屏幕上
-d :解压缩
-t :检验压缩文件是否出错
-v :显示压缩比等信息
-# : # 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6
提供比 gzip 更高的压缩比。
查看命令:bzcat、bzmore、bzless、bzgrep。
$ bzip2 [-cdkzv#] filename
-k :保留源文件
提供比 bzip2 更佳的压缩比。
可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。
查看命令:xzcat、xzmore、xzless、xzgrep。
$ xz [-dtlkc#] filename
压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gzip、bzip2、xz 将打包文件进行压缩。
$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename… ==打包压缩
$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看
$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
-z :使用 zip;
-j :使用 bzip2;
-J :使用 xz;
-c :新建打包文件;
-t :查看打包文件里面有哪些文件;
-x :解打包或解压缩的功能;
-v :在压缩/解压缩的过程中,显示正在处理的文件名;
-f : filename:要处理的文件;
-C 目录 : 在特定目录解压缩。
使用方式 | 命令 |
---|---|
打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 |
查 看 | tar -jtv -f filename.tar.bz2 |
解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 |
可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。
对一个变量赋值直接使用 =。
对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式;
输出变量使用 echo 命令。
$ x=abc
$ echo $x
$ echo ${x}
变量内容如果有空格,必须使用双引号或者单引号。
可以使用 指令
或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。
可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。
Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令:
$ declare [-aixr] variable
-a : 定义为数组类型
-i : 定义为整数类型
-x : 定义为环境变量
-r : 定义为 readonly 类型
使用 [ ] 来对数组进行索引操作:
$ array[1]=a
$ array[2]=b
$ echo ${array[1]}
重定向指的是使用文件代替标准输入、标准输出和标准错误输出。
1 | 代码 | 运算符 |
---|---|---|
标准输入 (stdin) | 0 | < 或 << |
标准输出 (stdout) | 1 | > 或 >> |
标准错误输出 (stderr) | 2 | 2> 或 2>> |
其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。
可以将不需要的标准输出以及标准错误输出重定向到 /dev/null,相当于扔进垃圾箱。
如果需要将标准输出以及标准错误输出同时重定向到一个文件,需要将某个输出转换为另一个输出,例如 2>&1 表示将标准错误输出转换为标准输出。
$ find /home -name .bashrc > list 2>&1
管道是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管道。
在命令之间使用 | 分隔各个管道命令。
$ ls -al /etc | less
cut 对数据进行切分,取出想要的部分。
切分过程一行一行地进行。
$ cut
-d :分隔符
-f :经过 -d 分隔后,使用 -f n 取出第 n 个区间
-c :以字符为单位取出区间
示例 1:last 显示登入者的信息,取出用户名。
$ last
root pts/1 192.168.201.101 Sat Feb 7 12:35 still logged in
root pts/1 192.168.201.101 Fri Feb 6 12:13 - 18:46 (06:33)
root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
$ last | cut -d ‘ ‘ -f 1
示例 2:将 export 输出的信息,取出第 12 字符以后的所有字符串。
$ export
declare -x HISTCONTROL=”ignoredups”
declare -x HISTSIZE=”1000”
declare -x HOME=”/home/dmtsai”
declare -x HOSTNAME=”study.centos.vbird”
…..(其他省略)…..
$ export | cut -c 12-
sort 用于排序。
$ sort [-fbMnrtuk] [file or stdin]
-f :忽略大小写
-b :忽略最前面的空格
-M :以月份的名字来排序,例如 JAN,DEC
-n :使用数字
-r :反向排序
-u :相当于 unique,重复的内容只出现一次
-t :分隔符,默认为 tab
-k :指定排序的区间
示例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。
$ cat /etc/passwd | sort -t ‘:’ -k 3
root:x:0:0:root:/root:/bin/bash
dmtsai:x:1000:1000:dmtsai:/home/dmtsai:/bin/bash
alex:x:1001:1002::/home/alex:/bin/bash
arod:x:1002:1003::/home/arod:/bin/bash
uniq 可以将重复的数据只取一个。
$ uniq [-ic]
-i :忽略大小写
-c :进行计数
示例:取得每个人的登录总次数
$ last | cut -d ‘ ‘ -f 1 | sort | uniq -c
1
6 (unknown
47 dmtsai
4 reboot
7 root
1 wtmp
输出重定向会将输出内容重定向到文件中,而 tee 不仅能够完成这个功能,还能保留屏幕上的输出。也就是说,使用 tee 指令,一个输出会同时传送到文件和屏幕上。
$ tee [-a] file
tr 用来删除一行中的字符,或者对字符进行替换。
$ tr [-ds] SET1 …
-d : 删除行中 SET1 这个字符串
示例,将 last 输出的信息所有小写转换为大写。
$ last | tr ‘[a-z]’ ‘[A-Z]’
col 将 tab 字符转为空格字符。
$ col [-xb]
-x : 将 tab 键转换成对等的空格键
expand 将 tab 转换一定数量的空格,默认是 8 个。
$ expand [-t] file
-t :tab 转为空格的数量
join 将有相同数据的那一行合并在一起。
$ join [-ti12] file1 file2
-t :分隔符,默认为空格
-i :忽略大小写的差异
-1 :第一个文件所用的比较字段
-2 :第二个文件所用的比较字段
paste 直接将两行粘贴在一起。
$ paste [-d] file1 file2
-d :分隔符,默认为 tab
split 将一个文件划分成多个文件。
$ split [-bl] file PREFIX
-b :以大小来进行分区,可加单位,例如 b, k, m 等
-l :以行数来进行分区。
g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。
$ grep [-acinv] [–color=auto] 搜寻字符串 filename
-c : 统计匹配到行的个数
-i : 忽略大小写
-n : 输出行号
-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行
–color=auto :找到的关键字加颜色显示
示例:把含有 the 字符串的行提取出来(注意默认会有 –color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串)
$ grep -n ‘the’ regular_express.txt
8:I can’t finish the test.
12:the symbol ‘*‘ is represented as start.
15:You are the best is mean you are the no. 1.
16:The world Happy is the same with “glad”.
18:google is the best tools for search keyword
示例:正则表达式 a{m,n} 用来匹配字符 a m~n 次,这里需要将 { 和 } 进行转义,因为它们在 shell 是有特殊意义的。
$ grep -n ‘a{2,5}‘ regular_express.txt
用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
$ printf ‘%10s %5i %5i %5i %8.2f \n’ $(cat printf.txt)
DmTsai 80 60 92 77.33
VBird 75 55 80 70.00
Ken 60 90 70 73.33
是由 Alfred Aho,Peter Weinberger 和 Brian Kernighan 创造,awk 这个名字就是这三个创始人名字的首字母。
awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:$n,n 为字段号,从 1 开始,$0 表示一整行。
示例:取出最近五个登录用户的用户名和 IP。首先用 last -n 5 取出用最近五个登录用户的所有信息,可以看到用户名和 IP 分别在第 1 列和第 3 列,我们用 $1 和 $3 就能取出这两个字段,然后用 print 进行打印。
$ last -n 5
dmtsai pts/0 192.168.1.100 Tue Jul 14 17:32 still logged in
dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22)
dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12)
dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14)
dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15)
$ last -n 5 | awk ‘{print $1 “\t” $3}’
可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。
$ awk ‘条件类型 1 {动作 1} 条件类型 2 {动作 2} …’ filename
示例:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。
1 | $ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' |
awk 变量:
变量名称 | 代表意义 |
---|---|
NF | 每一行拥有的字段总数 |
NR | 目前所处理的是第几行数据 |
FS | 目前的分隔字符,默认是空格键 |
示例:显示正在处理的行号以及每一行有多少字段
$ last -n 5 | awk ‘{print $1 “\t lines: “ NR “\t columns: “ NF}’
dmtsai lines: 1 columns: 10
dmtsai lines: 2 columns: 10
dmtsai lines: 3 columns: 10
dmtsai lines: 4 columns: 10
dmtsai lines: 5 columns: 9
查看某个时间点的进程信息。
示例:查看自己的进程
示例:查看系统所有进程
示例:查看特定的进程
查看进程树。
示例:查看所有进程树
实时显示进程信息。
示例:两秒钟刷新一次
查看占用端口的进程
示例:查看特定端口的进程
状态 | 说明 |
---|---|
R | running or runnable (on run queue) |
正在执行或者可执行,此时进程位于执行队列中。 | |
D | uninterruptible sleep (usually I/O) |
不可中断阻塞,通常为 IO 阻塞。 | |
S | interruptible sleep (waiting for an event to complete) |
可中断阻塞,此时进程正在等待某个事件完成。 | |
Z | zombie (terminated but not reaped by its parent) |
僵死,进程已经终止但是尚未被其父进程获取信息。 | |
T | stopped (either by a job control signal or because it is being traced) |
结束,进程既可以被作业控制信号结束,也可能是正在被追踪。 |
当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中:
其中子进程发送的 SIGCHLD 信号包含了子进程的信息,比如进程 ID、进程状态、进程使用 CPU 的时间等。
在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
pid_t wait(int *status)
父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。
如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。
参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。
pid_t waitpid(pid_t pid, int *status, int options)
作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。
pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。
options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。
孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。
僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。
系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。
进程是资源分配的基本单位。
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
下图显示了 4 个程序创建了 4 个进程,这 4 个进程可以并发地执行。
线程是独立调度的基本单位。
一个进程中可以有多个线程,它们共享进程资源。
QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
Ⅰ 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
Ⅱ 调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
Ⅲ 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
应该注意以下内容:
不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
1.1 先来先服务 first-come first-serverd(FCFS)
非抢占式的调度算法,按照请求的顺序进行调度。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
1.2 短作业优先 shortest job first(SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
1.3 最短剩余时间优先 shortest remaining time next(SRTN)
最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
2.1 时间片轮转
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系:
2.2 优先级调度
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
2.3 多级反馈队列
一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
实时系统要求一个请求在一个确定时间内得到响应。
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。
// entry section
// critical section;
// exit section
信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。
typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 临界区
up(&mutex);
}
void P2() {
down(&mutex);
// 临界区
up(&mutex);
}
使用信号量实现生产者-消费者问题
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer() {
while(TRUE) {
int item = produce_item();
down(∅);
down(&mutex);
insert_item(item);
up(&mutex);
up(&full);
}
}
void consumer() {
while(TRUE) {
down(&full);
down(&mutex);
int item = remove_item();
consume_item(item);
up(&mutex);
up(∅);
}
}
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
c 语言不支持管程,下面的示例代码使用了类 Pascal 语言来描述管程。示例代码的管程提供了 insert() 和 remove() 方法,客户端代码通过调用这两个方法来解决生产者-消费者问题。
monitor ProducerConsumer
integer i;
condition c;
procedure insert();
begin
// …
end;
procedure remove();
begin
// …
end;
end monitor;
管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。
管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
使用管程实现生产者-消费者问题
// 管程
monitor ProducerConsumer
condition full, empty;
integer count := 0;
condition c;
procedure insert(item: integer);
begin
if count = N then wait(full);
insert_item(item);
count := count + 1;
if count = 1 then signal(empty);
end;
function remove: integer;
begin
if count = 0 then wait(empty);
remove = remove_item;
count := count - 1;
if count = N -1 then signal(full);
end;
end monitor;
// 生产者客户端
procedure producer
begin
while true do
begin
item = produce_item;
ProducerConsumer.insert(item);
end
end;
// 消费者客户端
procedure consumer
begin
while true do
begin
item = ProducerConsumer.remove;
consume_item(item);
end
end;
生产者和消费者问题前面已经讨论过了。
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。
下面是一种错误的解法,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。
#define N 5
void philosopher(int i) {
while(TRUE) {
think();
take(i); // 拿起左边的筷子
take((i+1)%N); // 拿起右边的筷子
eat();
put(i);
put((i+1)%N);
}
}
为了防止死锁的发生,可以设置两个条件:
#define N 5
#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N // 右邻居
#define THINKING 0
#define HUNGRY 1
#define EATING 2
typedef int semaphore;
int state[N]; // 跟踪每个哲学家的状态
semaphore mutex = 1; // 临界区的互斥,临界区是 state 数组,对其修改需要互斥
semaphore s[N]; // 每个哲学家一个信号量
void philosopher(int i) {
while(TRUE) {
think(i);
take_two(i);
eat(i);
put_two(i);
}
}
void take_two(int i) {
down(&mutex);
state[i] = HUNGRY;
check(i);
up(&mutex);
down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去
}
void put_two(i) {
down(&mutex);
state[i] = THINKING;
check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了
check(RIGHT);
up(&mutex);
}
void eat(int i) {
down(&mutex);
state[i] = EATING;
up(&mutex);
}
// 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行
void check(i) {
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
state[i] = EATING;
up(&s[i]);
}
}
允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。
typedef int semaphore;
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
void reader() {
while(TRUE) {
down(&count_mutex);
count++;
if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
up(&count_mutex);
read();
down(&count_mutex);
count–;
if(count == 0) up(&data_mutex);
up(&count_mutex);
}
}
void writer() {
while(TRUE) {
down(&data_mutex);
write();
up(&data_mutex);
}
}
以下内容由 @Bandi Yugandhar 提供。
The first case may result Writer to starve. This case favous Writers i.e no writer, once added to the queue, shall be kept waiting longer than absolutely necessary(only when there are readers that entered the queue before the writer).
int readcount, writecount; //(initial value = 0)
semaphore rmutex, wmutex, readLock, resource; //(initial value = 1)
//READER
void reader() {
down(&readLock); // reader is trying to enter
down(&rmutex); // lock to increase readcount
readcount++;
if (readcount == 1)
down(&resource); //if you are the first reader then lock the resource
up(&rmutex); //release for other readers
up(&readLock); //Done with trying to access the resource
//WRITER
void writer() {
down(&wmutex); //reserve entry section for writers - avoids race conditions
writecount++; //report yourself as a writer entering
if (writecount == 1) //checks if you’re first writer
down(&readLock); //if you’re first, then you must lock the readers out. Prevent them from trying to enter CS
up(&wmutex); //release entry section
1 | int readCount; // init to 0; number of readers currently accessing resource |
进程同步与进程通信很容易混淆,它们的区别在于:
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。
#include <unistd.h>
int pipe(int fd[2]);
它具有以下限制:
也称为命名管道,去除了管道只能在父子进程中使用的限制。
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
相比于 FIFO,消息队列具有以下优点:
它是一个计数器,用于为多个进程提供对共享数据对象的访问。
允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。
需要使用信号量用来同步对共享存储的访问。
多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。
与其它通信机制不同的是,它可用于不同机器间的进程通信。
并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。
并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。
操作系统通过引入进程和线程,使得程序能够并发运行。
共享是指系统中的资源可以被多个并发进程共同使用。
有两种共享方式:互斥共享和同时共享。
互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。
虚拟技术把一个物理实体转换为多个逻辑实体。
主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。
多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一小个时间片并快速切换。
虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。
异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
进程控制、进程同步、进程通信、死锁处理、处理机调度等。
内存分配、地址映射、内存保护与共享、虚拟内存等。
文件存储空间的管理、目录管理、文件读写管理和保护等。
完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。
主要包括缓冲管理、设备分配、设备处理、虛拟设备等。
如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。
Linux 的系统调用主要有以下这些:
Task | Commands |
---|---|
进程控制 | fork(); exit(); wait(); |
进程通信 | pipe(); shmget(); mmap(); |
文件操作 | open(); read(); write(); |
设备操作 | ioctl(); read(); write(); |
信息维护 | getpid(); alarm(); sleep(); |
安全 | chmod(); umask(); chown(); |
大内核是将操作系统功能作为一个紧密结合的整体放到内核。
由于各模块共享信息,因此有很高的性能。
由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。
因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。
由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。
在用户程序中使用系统调用。
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true