404频道

学习笔记

sync.Cond类似于pthread中的条件变量,但等待的为goroutine,而不是线程。比较难理解的为Wait函数,在调用该函数时必须L为Lock状态,调用Wait函数后,goroutine会自动解锁,并等待条件的到来,等条件到来后会重新加锁。

代码量并不多,下面是去掉注释后的代码。

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
package sync

import (
"sync/atomic"
"unsafe"
)

type Cond struct {
noCopy noCopy

// L is held while observing or changing the condition
L Locker

notify notifyList
checker copyChecker
}

func NewCond(l Locker) *Cond {
return &Cond{L: l}
}

func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}

func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}

func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}

// copyChecker holds back pointer to itself to detect object copying.
type copyChecker uintptr

func (c *copyChecker) check() {
if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
uintptr(*c) != uintptr(unsafe.Pointer(c)) {
panic("sync.Cond is copied")
}
}

// noCopy may be embedded into structs which must not be copied
// after the first use.
//
// See https://github.com/golang/go/issues/8005#issuecomment-190753527
// for details.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}

具体的使用例子如下:

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
package main

import (
"fmt"
"sync"
)

func main() {
mutex := &sync.Mutex{}
cond := sync.NewCond(mutex)

wg := &sync.WaitGroup{}

wait := func(i int, c chan int) {
defer wg.Done()
fmt.Println("start chan ", i)
cond.L.Lock()
defer cond.L.Unlock()
fmt.Printf("chan %d wait before\n", i)
c <- i
// Wait是理解起来稍微麻烦的点,Cond.Wait会自动释放锁等待信号的到来,当信号到来后,第一个获取到信号的Wait将继续往下执行并从新上锁
cond.Wait()
fmt.Printf("chan %d wait end\n", i)
}

signal := func(count int, c chan int) {
defer wg.Done()
for i := 0; i < count; i++ {
fmt.Printf("read chan %d ready\n", <-c)
}
fmt.Println("call signal")
cond.Signal()
}

broadcast := func(count int, c chan int) {
defer wg.Done()
for i := 0; i < count; i++ {
fmt.Printf("read chan %d ready\n", <-c)
}
fmt.Println("call broadcast")
cond.Broadcast()
}

c := make(chan int)
wg.Add(2)
go wait(0, c)
go signal(1, c)
wg.Wait()
fmt.Println("signal test finished\n\n")

count := 3
for i := 0; i < count; i++ {
wg.Add(1)
go wait(i, c)
}
wg.Add(1)
go broadcast(count, c)
wg.Wait()
}

北京青龙峡

自从阮一峰的博客中增加了每周分享栏目,自己每周五都是主动的浏览一下阮老师的每周分享,一来阮老师的涉猎非常广泛,可以提高自己的视野;二来,阮老师的文章都特别容易懂,给人一种一直想看下去的冲动。

我个人平常也会看很多的技术类文章,也会遇到各种工具或者特别不错的文章,也有分享的冲动,也想搞一些分享的文章,当然我没有阮老师勤奋和涉猎广泛,但也期望能够对他人有所帮助,哪怕文中的一条分享能够让读者觉得有价值,那么也是值的做的一件事情。

具体的分享版块可能不会特别固定,分享的间隔也不会特别勤快,很难做到阮老师的一周一次的频次。

教程

1. The Go Memory Model

Golang的内存模型,建议Golang开发者读一遍。

2. 《极客时间》-左耳听风

知名博客酷壳的作者陈皓的技术专栏,花钱购买一下专栏还是非常值得的,尤其是最近写的程序员练级攻略系列,能提供大量有价值的学习资料及方向指导,非常赞。


http://

3. 深入解析 kubernetes 资源管理,容器云牛人有话说

对kubernetes的资源管理讲解的非常到位和深入,文章略长,需要花点时间才能读完,值的一看。

4. 《TCP/IP详解 卷1: 协议》

网络方面的经典著作,每个工程师必读,虽然是写给工程师看的,但很多的学术著作中引用到了该书中内容。

5. 《深入解析GO》

对于go的内部实现原理讲解的挺到位,对于理解go的原理挺有帮助。涉及少量汇编,我不太懂汇编,涉及汇编的地方直接跳过了。

6. Red Hat Enterprise Linux Document

RedHat官方的Linux文档,我个人还没怎么读过。

资源

1. 腾讯大学-CEO来了

货真价实的互联网CEO的视频分享,谈创业、谈感悟,目前已经有蔚来汽车、VIPKID、每日优鲜、快手、Keep、知乎的CEO的分享。

2. nginx-upsync-module

新浪微博开源的nginx module,用于动态更改upstream server。

3. 语义化版本规范SemVer

软件版本在取名上会比较混乱,有的使用1.0.1,有的使用1.0等,SemVer用于规范软件版本的命名。

4. 《见识》

吴军老师的最新图书,内容整理自吴军的专栏《硅谷来信》,每篇文章一个主题,值的一读。

5. 嗨!济南

又听到了一首关于济南的歌曲,曾在济南生活多年,必须要分享一下。

6. termtosvg

Github上的开源项目,将命令行工具单独保存为SVG动画。

7. teleport

提供了ssh的审计和回放,基于SSH的RBAC管理,同时还有一个带管理功能的ui界面,目的是用于取代系统自带的sshd。

工具

1. cloc

统计代码行数的工具,下面是kubernetes项目的v1.11.2版本的代码行数统计,go的代码行数已经超过了100万行。

2. SpaceVim

我个人不是vim工具党,刚毕业那会曾经一度热衷于将vim打造成为一个开发C++的IDE,但经过复杂的配置后仍然难以达到CLion这种IDE的水平。最近偶然看到SpaceVim,心中为之一振,这就是我想要的vim,虽达不到IDEA的高度,但已经可以跟vscode的易用度差不多了。

SpaceVim的强大之处在于Space键的使用,默认情况下按下空格键会给出快捷键的提示,类似于桌面系统中的菜单功能。

macvlan的原理是在宿主机物理网卡上虚拟出多个子接口,每个子接口有独立的mac地址,通过不同的MAC地址在数据链路层(Data Link Layer)进行网络数据转发的。达到的效果类似,一块物理网卡上有多个IP地址,多个IP地址有自己的mac地址。

它是比较新的网络虚拟化技术,需要较新的内核支持(Linux kernel v3.9–3.19 and 4.0+)。

macvlan设备跟物理设备之间并不直接互通。

macvlan不以交换机端口来划分vlan,一个交换机端口可接收来自多个mac地址的数据。

一个交换机端口要处理多个vlan的数据,需要开启trunk模式。

四种模式

以下四种模式为每个macvlan设备单独配置,而不是一个物理设备就只有一个配置。

VEPA

所有发送出去的报文都经过交换机,交换机再发送到对应的目标地址。默认模式。物理网卡接收到macvlan设备的数据后,总是将数据发送出去,即使是发往本设备上其他macvlan设备的数据包。这样在交换机设备上可以看到所有网络的流量。如果是本机的macvlan设备流量仍然是发往本机的macvlan设备,可能会被交换机的生成树协议阻止。需要交换机开启hairpin模式或者reflective relay模式,该模式在目前的交换机上未广泛支持,vepa模式的应用较少。

linux的网桥支持hairpin模式。

bridge

最常用,同一个物理设备上的不同macvlan设备间的通讯可以直接转发,不再需要经过外部的交换机。转发非常快速,macvlan设备相对固定,不需要生成树协议。

Private

本质上是VEPA模式,但同一个物理设备上的macvlan设备之间无法直接通讯,不常用。

Passthru

后来增加的模式,比较少用

vepa和passthru都会将不同macvlan接口之间的数据发送到交换机,然后发回,对性能的影响比较明显。

物理网卡收到包后,根据包的mac地址来判断这个包交给哪个虚拟接口。

image

手工创建实践

以下实验为在virturalbox虚拟机下

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
# 创建两个network namespace net1和net2
ip netns add net1
ip netns add net2

# 创建macvlan接口
# enp0s8相当于物理网卡eth0
ip link add link enp0s8 mac1 type macvlan

# 可以看到创建了接口mac1@enp0s8
[root@localhost vagrant]# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:6c:3e:95 brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
link/ether 08:00:27:cf:b0:b4 brd ff:ff:ff:ff:ff:ff
4: docker_gwbridge: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT
link/ether 02:42:8e:cf:a9:da brd ff:ff:ff:ff:ff:ff
5: br-b3e83aa45886: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT
link/ether 02:42:bf:b4:e5:36 brd ff:ff:ff:ff:ff:ff
6: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT
link/ether 02:42:25:e7:40:32 brd ff:ff:ff:ff:ff:ff
19: br-ec6c4e77321d: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT
link/ether 02:42:e0:d8:38:6d brd ff:ff:ff:ff:ff:ff
24: mac1@enp0s8: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT
link/ether 3e:b8:e9:1d:3b:c8 brd ff:ff:ff:ff:ff:ff

创建macvlan接口的格式为:ip link add link <PARENT> <NAME> type macvlan是macvlan接口的父接口名称,name是新创建的macvlan接口名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 将mac1放入到net1 namespace中
[root@localhost vagrant]# ip link set mac1 netns net1
[root@localhost vagrant]# ip netns exec net1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
24: mac1@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT
link/ether 3e:b8:e9:1d:3b:c8 brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 在net1中将mac1接口命名为eth0
[root@localhost vagrant]# ip netns exec net1 ip link set mac1 name eth0
[root@localhost vagrant]# ip netns exec net1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
24: eth0@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT
link/ether 3e:b8:e9:1d:3b:c8 brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 在net1中分配eth0网卡的ip地址为192.168.8.120
[root@localhost vagrant]# ip netns exec net1 ip addr add 192.168.8.120/24 dev eth0
[root@localhost vagrant]# ip netns exec net1 ip link set eth0 up
[root@localhost vagrant]# ip netns exec net1 ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
24: eth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT
link/ether 3e:b8:e9:1d:3b:c8 brd ff:ff:ff:ff:ff:ff link-netnsid 0

在docker上的实践

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost vagrant]# docker network create -d macvlan --subnet=10.0.2.100/24 --gateway=10.0.2.2 -o parent=enp0s3 mcv
5c637798d559471bd8d1036cdd947d3949e1973f724568da066d9c60c00fb5e6

[root@localhost vagrant]# docker network ls
NETWORK ID NAME DRIVER SCOPE
b6a128f1730e bridge bridge local
a65957cd6c3f docker_gwbridge bridge local
540bb390028a host host local
b3e83aa45886 isolated_nw bridge local
ec6c4e77321d local_alias bridge local
5c637798d559 mcv macvlan local
3d247d0414d0 none null local

reference

Some notes on macvlan/macvtap

pushd和popd命令的用法

在编写shell的时候,经常会在目录之间进行切换,如果使用cd命令经常会切换错误,pushd和popd使用栈的方式来管理目录。

dirs

用于显示当前目录栈中的所有记录。

pushd

将目录加入到栈顶部,并将当前目录切换到该目录。若不加任何参数,该命令用于将栈顶的两个目录进行对调。

popd

删除目录栈中的目录。若不加任何参数,则会首先删除目录栈顶的目录,并将当前目录切换到栈顶下面的目录。

命令格式:pushd [-N | +N] [-n]

  • +N 将第N个目录删除(从左边数起,数字从0开始)
  • -N 将第N个目录删除(从右边数起,数字从0开始)
  • -n 将目录出栈时,不切换目录

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost tmp]# mkdir /tmp/dir{1,2,3,4}
[root@localhost tmp]# pushd /tmp/dir1
/tmp/dir1 /tmp
[root@localhost dir1]# pushd /tmp/dir2
/tmp/dir2 /tmp/dir1 /tmp
[root@localhost dir2]# pushd /tmp/dir3
/tmp/dir3 /tmp/dir2 /tmp/dir1 /tmp
[root@localhost dir3]# pushd /tmp/dir4
/tmp/dir4 /tmp/dir3 /tmp/dir2 /tmp/dir1 /tmp
# dirs的显示内容跟pushd完成后的输出一致
[root@localhost dir4]# dirs
/tmp/dir4 /tmp/dir3 /tmp/dir2 /tmp/dir1 /tmp

[root@localhost dir4]# popd
/tmp/dir3 /tmp/dir2 /tmp/dir1 /tmp

# 带有数字比较容易出错
[root@localhost dir3]# popd +1
/tmp/dir3 /tmp/dir1 /tmp

# 清除目录栈
[root@localhost dir3]# dirs -c
[root@localhost dir3]# dirs
/tmp/dir3

docker image结构

docker可以通过命令docker image inspect ${image}来查看image的详细信息,其中包含了所使用的底层文件系统及各层的信息。

docker container的存储结构分为了只读层、init层和可读写层。

只读层跟docker image的层次结构恰好对应,主要包含操作系统的文件、配置、目录等信息,不包含操作系统镜像。

init层在只读层和读写层中间,用来专门存放/etc/hosts /etc/resolv.conf等信息,这些文件往往需要在启动的时候写入一些指定值,但不期望docker commit命令对其进行提交。

可读写层为容器在运行中可以进行写入的层。

overlay

image

采用了两层结构,lowerdir为镜像层,只读。upperdir为容器层。

每层都会在/var/run/docker/overlay创建一个文件夹,文件夹中为实际层的内容,文件采用硬链接的方式链接到真实层中的文件,每一层都包含该层该拥有的所有文件,而该文件的真实存储可能是采用硬链接的方式链接到上层中的真实文件,因此比较耗费inode。

创建一个容器时,会新增两个目录,一个为读写层,一个为初始层。初始层中保存了容器初始化时的环境信息,如hostname、hosts文件等。读写层用于记录容器的所有改动。

overlay2

为了规避overlay消耗inode节点过多的问题,overlay2采用在每层中增加lower文件的方式来记录所有底层的信息,类似于链表的形式。

docker pull ubuntu

1
2
3
4
5
6
7
8
9
10
[root@localhost runc]# docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
c64513b74145: Pull complete
01b8b12bad90: Pull complete
c5d85cf7a05f: Pull complete
b6b268720157: Pull complete
e12192999ff1: Pull complete
Digest: sha256:3f119dc0737f57f704ebecac8a6d8477b0f6ca1ca0332c7ee1395ed2c6a82be7
Status: Downloaded newer image for ubuntu:latest

会在/var/run/docker/overlay2目录下创建如下文件:

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
[root@localhost overlay2]# tree -L 2
.
|-- 664ae13f1c21402385076025d68476eb8d1cc4be6c6a218b24bd55217ac62672 // 第0层
| |-- diff
| `-- link // MZUEUOFHBNVTRCJYJEG7QY4VWT
|-- 783ad02709b67ac47b55198e9659c4592f0972334987ab97f42fd10f1784cbba // 第2层
| |-- diff
| |-- link // HXATFASQ4E2JBG434DUEN54EZZ
| |-- lower // l/WUZC5WSTQTPJUJ4KFAYCUT5IPD:l/MZUEUOFHBNVTRCJYJEG7QY4VWT
| |-- merged
| `-- work
|-- 89f7a20dda3d868840e20d9e8f1bfe20c5cca51c27b07825f100da0f474672f6 // 第3层
| |-- diff
| |-- link // 5PHT7S3MCZTTQOXVPA4CKJRRFD
| |-- lower // l/HXATFASQ4E2JBG434DUEN54EZZ:l/WUZC5WSTQTPJUJ4KFAYCUT5IPD:l/MZUEUOFHBNVTRCJYJEG7QY4VWT
| |-- merged
| `-- work
|-- backingFsBlockDev
|-- bad073a2d1f79a03af6caa0b3f51a22e6762cebbc0c30e45458fe6c1ff266f68 // 第4层
| |-- diff
| |-- link // QMKHIPSDT4JTPE4FLT7QGJ33ND
| |-- lower // l/5PHT7S3MCZTTQOXVPA4CKJRRFD:l/HXATFASQ4E2JBG434DUEN54EZZ:l/WUZC5WSTQTPJUJ4KFAYCUT5IPD:l/MZUEUOFHBNVTRCJYJEG7QY4VWT
| |-- merged
| `-- work
|-- cb40b5b47c699050305676b35b1cea1ce08b38604dd68243c4be48934125b1a3 // 第1层
| |-- diff
| |-- link // WUZC5WSTQTPJUJ4KFAYCUT5IPD
| |-- lower // l/MZUEUOFHBNVTRCJYJEG7QY4VWT
| |-- merged
| `-- work
`-- l
|-- 5PHT7S3MCZTTQOXVPA4CKJRRFD -> ../89f7a20dda3d868840e20d9e8f1bfe20c5cca51c27b07825f100da0f474672f6/diff
|-- HXATFASQ4E2JBG434DUEN54EZZ -> ../783ad02709b67ac47b55198e9659c4592f0972334987ab97f42fd10f1784cbba/diff
|-- MZUEUOFHBNVTRCJYJEG7QY4VWT -> ../664ae13f1c21402385076025d68476eb8d1cc4be6c6a218b24bd55217ac62672/diff
|-- QMKHIPSDT4JTPE4FLT7QGJ33ND -> ../bad073a2d1f79a03af6caa0b3f51a22e6762cebbc0c30e45458fe6c1ff266f68/diff
`-- WUZC5WSTQTPJUJ4KFAYCUT5IPD -> ../cb40b5b47c699050305676b35b1cea1ce08b38604dd68243c4be48934125b1a3/diff

l目录下为超链接,缩短后的目录,为了避免mount时超出页大小限制。

每一层中的diff文件夹包含实际内容。

每一层中都有一个link文件,内容为l目录中的超链接,超链接实际指向当前层目录中的diff文件夹。

除去最底层的目录外,其余每一层中包含一个lower文件,包含了该层的所有更底层名称和顺序,可以根据该文件构建出整个镜像的层次结构。

work目录用于OverlayFS内部使用。

最底层只有link文件,无lower文件,因此664ae13f1c21402385076025d68476eb8d1cc4be6c6a218b24bd55217ac62672为最底层。

以上五层为lower,只读。

当使用docker run -it ubuntu:latest /bin/bash启动一个容器后,在overlay2目录下会多出两个文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@localhost overlay2]# tree -L 1 0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08-init 0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08 l
0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08-init
|-- diff
|-- link // ZJVMGTB2IOJ6QF57TYM5O7EWXW
|-- lower // l/QMKHIPSDT4JTPE4FLT7QGJ33ND:l/5PHT7S3MCZTTQOXVPA4CKJRRFD:l/HXATFASQ4E2JBG434DUEN54EZZ:l/WUZC5WSTQTPJUJ4KFAYCUT5IPD:l/MZUEUOFHBNVTRCJYJEG7QY4VWT
|-- merged
`-- work
0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08
|-- diff
|-- link
|-- lower // l/ZJVMGTB2IOJ6QF57TYM5O7EWXW:l/QMKHIPSDT4JTPE4FLT7QGJ33ND:l/5PHT7S3MCZTTQOXVPA4CKJRRFD:l/HXATFASQ4E2JBG434DUEN54EZZ:l/WUZC5WSTQTPJUJ4KFAYCUT5IPD:l/MZUEUOFHBNVTRCJYJEG7QY4VWT
|-- merged
`-- work
l
|-- 5PHT7S3MCZTTQOXVPA4CKJRRFD -> ../89f7a20dda3d868840e20d9e8f1bfe20c5cca51c27b07825f100da0f474672f6/diff
|-- AOLYGFOHIAHWU5CBAJFULNAXI7 -> ../0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08/diff
|-- HXATFASQ4E2JBG434DUEN54EZZ -> ../783ad02709b67ac47b55198e9659c4592f0972334987ab97f42fd10f1784cbba/diff
|-- MZUEUOFHBNVTRCJYJEG7QY4VWT -> ../664ae13f1c21402385076025d68476eb8d1cc4be6c6a218b24bd55217ac62672/diff
|-- QMKHIPSDT4JTPE4FLT7QGJ33ND -> ../bad073a2d1f79a03af6caa0b3f51a22e6762cebbc0c30e45458fe6c1ff266f68/diff
|-- WUZC5WSTQTPJUJ4KFAYCUT5IPD -> ../cb40b5b47c699050305676b35b1cea1ce08b38604dd68243c4be48934125b1a3/diff
`-- ZJVMGTB2IOJ6QF57TYM5O7EWXW -> ../0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08-init/diff

0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08-init用于存放容器初始化时的信息,通过下面查看更直观。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@localhost overlay2]# tree 0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08-init
0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08-init
|-- diff
| |-- dev
| | `-- console
| `-- etc
| |-- hostname
| |-- hosts
| |-- mtab -> /proc/mounts
| `-- resolv.conf
|-- link
|-- lower
|-- merged
`-- work
`-- work

0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08的直接底层为init层,更详细的目录结构如下。

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
[root@localhost overlay2]# tree -L 2  0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08
0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08
|-- diff
|-- link
|-- lower
|-- merged
| |-- bin
| |-- boot
| |-- dev
| |-- etc
| |-- home
| |-- lib
| |-- lib64
| |-- media
| |-- mnt
| |-- opt
| |-- proc
| |-- root
| |-- run
| |-- sbin
| |-- srv
| |-- sys
| |-- tmp
| |-- usr
| `-- var
`-- work
`-- work

merged文件夹中内容较多,为overlay2的直接挂载点,对容器的修改会反应到该目录中。例如在容器中增加/root/hello.txt文件,在merged目录下会增加root/hello.txt文件。

1
2
3
[root@localhost overlay2]# mount  | grep overlay2
/dev/mapper/centos-root on /var/lib/docker/overlay2 type xfs (rw,relatime,attr2,inode64,noquota)
overlay on /var/lib/docker/overlay2/0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/ZJVMGTB2IOJ6QF57TYM5O7EWXW:/var/lib/docker/overlay2/l/QMKHIPSDT4JTPE4FLT7QGJ33ND:/var/lib/docker/overlay2/l/5PHT7S3MCZTTQOXVPA4CKJRRFD:/var/lib/docker/overlay2/l/HXATFASQ4E2JBG434DUEN54EZZ:/var/lib/docker/overlay2/l/WUZC5WSTQTPJUJ4KFAYCUT5IPD:/var/lib/docker/overlay2/l/MZUEUOFHBNVTRCJYJEG7QY4VWT,upperdir=/var/lib/docker/overlay2/0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08/diff,workdir=/var/lib/docker/overlay2/0326c1da0af912a6ea5efda77b65b04e796993e0f111ed8f262c55b2716f1c08/work)

ref

本文绝大多数题目来源于网络,部分题目为原创。

slice相关

以下代码有什么问题,说明原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type student struct {
Name string
Age int
}

func pase_student() {
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
}
}

每次遍历的时候stu变量为值拷贝,stu变量的地址未改变,即&stu未改变,遍历结束后stu指向stus中的最后一个元素。

使用reflect.TypeOf(str)打印出的类型为main.student,如果使用stu.Age += 10这样的语法是不会修改stus中的值的。

可修改为如下形式:

1
2
3
for i, _ := range stus {
m[stus[i].Name] = &stus[i]
}

有一个slice of object, 遍历slice修改name为指定的值

1
2
3
4
5
6
7
8
type foo struct {
name string
value string
}

func mutate(s []foo, name string) {
// TODO
}

意在考察range遍历的时候是值拷贝,以及slice的内部数据结构,slice的数据结构如下:

1
2
3
4
5
6
struct    Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};

执行append函数后会返回一个新的Slice对象,新的Slice对象跟旧Slice对象共用相同的数据存储,但是len的值并不相同。

该题目中,可以通过下面的方式来修改值:

1
2
3
4
5
6
7
8
9
// range方式
for i, _ := range s {
s[i].name = name
}

// for i形式
for i:=0; i<len(s); i++ {
s[i].name = name
}

从slice中找到一个元素匹配name,并将该元素的指针添加到一个新的slice中,返回新slice

1
2
3
4
func find(s []foo, name string) []*foo {
// TODO

}

仍旧是考察range是值拷贝的用法,此处使用for i 循环即可

1
2
3
4
5
6
7
8
9
10
11
func find(s []foo, name string) []*foo {
res := []*foo{}

for i := 0; i < len(s); i++ {
if s[i].name == name {
res = append(res, &(s[i]))
break
}
}
return res
}

下面输出什么内容

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

import "fmt"

func m(s []int) {
s[0] = -1
s = append(s, 4)
}

func main() {
s1 := []int{1, 2, 3}
m(s1)
s2 := make([]int, 3, 6)
m(s2)
s2 = append(s2, 7)
s3 := [3]int{1, 2, 3}
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
}

slice的函数传递为值拷贝方式,在函数m中对下标为0的元素的修改会直接修改原slice中的值,因为slice中的指针指向的地址是相同的。

append之后的slice虽然可能是在原数组上增加了元素,但原slice中的len字段并没有变化。

make([]int, 3, 6)虽然指定了slice的cap,但对于append没有影响,还是会在slice中最后一个元素的下一个位置增加新元素。

数组由于是值拷贝,对新数组的修改不会影响到原数组。

输出内容如下:

1
2
3
[-1 2 3]
[-1 0 0 7]
[1 2 3]

下面输出什么内容

该题目为我自己想出来的,非来自于互联网,意在考察对slice和append函数的理解。

1
2
3
4
5
6
7
8
9
func f() {
s1 := make([]int, 2, 8)
fmt.Println(s1)
s2 := append(s1, 4)
fmt.Println(s2)
s3 := append(s1, 5)
fmt.Println(s3)
fmt.Println(s2)
}

输出结果如下,在执行第二个append后,第一个append在内存中增加的元素4会被5覆盖掉。执行结果可以通过fmt.Println(s1, cap(s1), &s1[0])的形式将第一个元素的内存地址打印出来查看。

1
2
3
[0 0 4]
[0 0 5]
[0 0 5]

goroutine

以下代码输出内容:

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

import (
"fmt"
"runtime"
)

func main() {
runtime.GOMAXPROCS(1)
go func() {
fmt.Println(1)
}()
for {
}
fmt.Println(1)
}

不会有任何输出

下面输出的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type People struct{}

func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}

type Teacher struct {
People
}

func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}

func main() {
t := Teacher{}
t.ShowA()
}

输出

1
2
showA
showB

有点出乎意料,可以举个反例,如果ShowA()方法会调用到Teacher类型的ShowB()方法,假设People和Teacher并不在同一个包中时,编译一定会出现错误。

Go中没有继承机制,只有组合机制。

下面代码会触发异常吗?请详细说明

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
runtime.GOMAXPROCS(1)
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
int_chan <- 1
string_chan <- "hello"
select {
case value := <-int_chan:
fmt.Println(value)
case value := <-string_chan:
panic(value)
}
}

会间歇性触发异常,select会随机选择。

以下代码能编译过去吗?为什么?

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
package main

import (
"fmt"
)

type People interface {
Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}

func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}

不能编译过去,提示Stduent does not implement People (Speak method has pointer receiver),将Speak定义更改为func (stu Stduent) Speak(think string) (talk string)即可编译通过。

main的调用方式更改为如下也可以编译通过var peo People = new(Stduent)

func (stu *Stduent) Speak(think string) (talk string)*Student类型的方法,不是Stduent类型的方法。

下面输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"time"
"runtime"
)

func main() {
runtime.GOMAXPROCS(1)
arr := [10000]int{}
for i:=0; i<len(arr); i++ {
arr[i] = i
}
for _, a := range arr {
go func() {
fmt.Println(a)
}()
}
for {
time.Sleep(time.Second)
}
}

一直输出9999.涉及到goroutine的切换时机,仅系统调用或者有函数调用的情况下才会切换goroutine,for循环情况下一直没有系统调用或函数切换发生,需要等到for循环结束后才会启动新的goroutine。

以下代码打印出来什么内容,说出为什么。。。

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
package main

import (
"fmt"
)

type People interface {
Show()
}

type Student struct{}

func (stu *Student) Show() {

}

func live() People {
var stu *Student
return stu
}

func main() {
if live() == nil {
fmt.Println("AAAAAAA")
} else {
fmt.Println("BBBBBBB")
}
}

打印BBBBBBB

byte与rune的关系

  • byte alias for uint8
  • rune alias for uint32,用来表示unicode
1
2
3
4
5
6
7
8
func main() {
// range遍历为rune类型,输出int32
for _, w:=range "123" {
fmt.Printf("%T", w)
}
// 取数组为byte类型,输出uint8
fmt.Printf("%T", "123"[0])
}

写出打印的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type People struct {
name string `json:"name"`
}

func main() {
js := `{
"name":"11"
}`
var p People
p.name = "123"
err := json.Unmarshal([]byte(js), &p)
if err != nil {
fmt.Println("err: ", err)
return
}
fmt.Println("people: ", p)
}

打印结果为people: {123}

下面函数有什么问题?

1
2
3
func funcMui(x,y int)(sum int,error){
return x+y,nil
}

函数返回值命名 在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个为命名,所以错误。

以下函数输出什么

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
package main

func main() {
println(DeferFunc1(1))
println(DeferFunc2(1))
println(DeferFunc3(1))
println(DeferFunc4(1))
}

func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}

func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}

func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}

func DeferFunc4(i int) (t int) {
t = 10
return 2
}

输出结果为: 4 1 3 2

return语句不是一个原子指令,分为两个阶段,执行return后面的表达式和返回表达式的结果。defer函数在返回表达式之前执行。

  1. 执行return后的表达式给返回值赋值
  2. 调用defer函数
  3. 空的return

DeferFunc1在第一步执行表达式后t=1,执行defer后t=4,返回值为4

DeferFunc2在第一步执行表达式后t=1,执行defer后t=4,返回值为第一步表达式的结果1

DeferFunc3在第一步表达式为t=2,执行defer后t=3,返回值为t=3

DeferFunc4在第一步执行表达式后t=2,返回值为t=2

是否可以编译通过?如果通过,输出什么?

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
package main

import (
"fmt"
)

func main() {
sn1 := struct {
age int
name string
}{age: 11, name: "qq"}

sn2 := struct {
age int
name string
}{age: 11, name: "qq"}

sn3 := struct {
name string
age int
}{age: 11, name: "qq"}

if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}

if sn1 == sn3 {
fmt.Println("sn1 == sn3")
}


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")
}
}

结构体比较 进行结构体比较时候,只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。

还有一点需要注意的是结构体是相同的,但是结构体属性中有不可以比较的类型,如map,slice。 如果该结构属性都是可以比较的,那么就可以使用“==”进行比较操作。

是否可以编译通过?如果通过,输出什么?

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

import (
"fmt"
)

func Foo(x interface{}) {
if x == nil {
fmt.Println("empty interface")
return
}
fmt.Println("non-empty interface")
}

func main() {
var x *int = nil
Foo(x)
}

输出“non-empty interface”

交替打印数字和字母

使用两个 goroutine 交替打印序列,一个 goroutine 打印数字, 另外一个 goroutine 打印字母, 最终效果为: 12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

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
package main

import (
"fmt"
"sync"
)


func main() {
number, letter := make(chan bool), make(chan bool)
wait := new(sync.WaitGroup)

go func () {
num := 1
for {
<-number
fmt.Printf("%d%d", num, num+1)
num += 2
letter <- true
if num > 28 {
break
}
}
wait.Done()
}()

go func () {
begin := 'A'
for {
<- letter
if begin < 'Z' {
fmt.Printf("%c%c", begin, begin+1)
begin+=2
number <- true
} else {
break
}
}

wait.Done()
}()

number <- true

wait.Add(2)

wait.Wait()
}

struct类型的方法调用

假设T类型的方法上接收器既有T类型的,又有T指针类型的,那么就不可以在不能寻址的T值上调用T接收器的方法。

请看代码,试问能正常编译通过吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
)
type Lili struct{
Name string
}
func (Lili *Lili) fmtPointer(){
fmt.Println("poniter")
}
func (Lili Lili) fmtReference(){
fmt.Println("reference")
}
func main(){
li := Lili{}
li.fmtPointer()
}

能正常编译通过,并输出”poniter”

请接着看以下的代码,试问能编译通过?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
)
type Lili struct{
Name string
}
func (Lili *Lili) fmtPointer(){
fmt.Println("poniter")
}
func (Lili Lili) fmtReference(){
fmt.Println("reference")
}
func main(){
Lili{}.fmtPointer()
}

不能编译通过。
“cannot call pointer method on Lili literal”
“cannot take the address of Lili literal”

其实在第一个代码示例中,main主函数中的“li”是一个变量,li的虽然是类型Lili,但是li是可以寻址的,&li的类型是Lili,因此可以调用Lili的方法。

golang context包的用法

  1. goroutine之间的传值
  2. goroutine之间的控制

在单核cpu的情况下,下面输出什么内容?

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

import (
"fmt"
"sync"
)

func main() {
wg := sync.WaitGroup{}
for _, i:=range []int{1, 2, 3, 4, 5} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(i)
} ()
}
wg.Wait()
}

考察golang的runtime机制,goroutine的切换时机只有在有系统调用或者函数调用时才会发生,本例子中的for循环结束之前不会发生goroutine的切换,所以最终输出结果为5.

下面输出什么

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
package main

import (
"fmt"
)

type People interface {
Speak(string) string
}

type Stduent struct{}

func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}

func main() {
var peo People = Stduent{}
think := "bitch"
fmt.Println(peo.Speak(think))
}

编译不通过,仅*Student实现了People接口,更改为var peo People = &Student{}即可编译通过。

下面输出什么

1
2
3
4
5
6
7
8
9
10
package main

const cl = 100

var bl = 123

func main() {
println(&bl, bl)
println(&cl, cl)
}

编译失败,常量cl通常在预处理阶段会直接展开,无法取其地址。

以下代码是否存在问题,请解释你的判断和理由

1
2
3
4
5
6
7
8
import "sync"

func f(m sync.Mutex) {
m.Lock()
defer m.Unlock()

// Do something...
}

Mutex对象不能被值拷贝,后续传递需要使用指针的形式

以下代码输出是什么 解释一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func    main()  {
case1()
case2()
}

func case1() {
s1 := make([]string, 1, 20)
s1[0] = "hello"
p1 := &s1[0]
s1 = append(s1, "world")
*p1 = "hello2"
fmt.Printf("value of p1 is %s, value of s1[0] is %s \n", *p1, s1[0])
}

func case2() {
s1 := make([]string)
s1[0] = "hello"
p1 := &s1[0]
s1 = append(s1, "world")
*p1 = "hello2"
fmt.Printf("value of p1 is %s, value of s1[0] is %s \n", *p1, s1[0])
}

本题意在考察string和slice的数据结构,string的数据结构如下:

http://research.swtch.com/godata2.png

case1的内存结构变化情况如下:

case2由于s1默认长度为0,直接使用s1[0]复制会出现panic错误。

ref

通过使用Google的登陆二步验证(即Google Authenticator服务),我们在登陆时需要输入额外由手机客户端生成的一次性密码。大大提高登陆的安全性。

实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。

google实现了基于时间的TOTP算法(Time-based One-time Password),客户端实现包括了android和ios。

算法为公开算法,google没有提供服务端的实现,各个语言都有单独的实现。自己系统使用可以直接使用网上的代码。

linux下有libpam-google-authenticator模块,可以使用yum或者源码编译安装,github上有源码,编译出来的为so文件,可以加到sshd的配置文件中,用于给sshd提供二次认证机制。

客户端和服务端存在时间差的问题,google authenticator的超时时间为30s,服务端可以使用两个30s的时间来验证,包括当前和上一个30s。

曾经使用多说和网易云评论作为博客的评论系统,不幸都相继倒闭后,博客就一直没有评论系统。虽博客的访问量可以忽略不计,但本着折腾和好奇的原则,还是折腾一下gitment。

更新hexo-theme-next主题

最新版本的next主题已经默认支持gitment,需要将next主题升级到最新版本。

我的hexo-theme-next使用单独的git项目进行管理,git地址为:https://github.com/kuring/hexo-theme-next。接下来需要将fork出来的git项目跟next的git项目进行同步。

  1. 在本地创建名字为upstream的remote,指向地址为:git remote add upstream https://github.com/theme-next/hexo-theme-next.git

  2. 拉取next项目到本地分支,本地的分支,执行git fetch upstream

  3. 将upsteam/master分支合并到master分支上

1
2
3
4
5
6
7
8
9
10
git checkout master

# 由于修改了_config.yml文件,存在冲突,合并失败
lvkai@osx:~/blog/kuring/themes/hexo-theme-next% git merge upstream/master 128 ↵
Removing source/css/_common/components/third-party/gentie.styl
Removing layout/_third-party/comments/gentie.swig
Auto-merging _config.yml
CONFLICT (content): Merge conflict in _config.yml
Removing README.en.md
Automatic merge failed; fix conflicts and then commit the result.
  1. 解决冲突后提交并将master分支push到github仓库

注册gitment

前往:https://github.com/settings/profile

Developer settings -> Register a new application

在界面中输入如下内容:

image

获取到Client ID和Client Secret.

新建github repo

创建新的github项目:https://github.com/kuring/gitment-comments

在next主题中设置gitment

next主题的配置文件为theme/next/_config.yml,修改其中的gitment设置如下,

  1. client_id为在github中注册所获取到的client id
  2. client_secret为在github中注册所获取到的client secret
  3. github_repo为上面新创建的github repo名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Gitment
# Introduction: https://imsun.net/posts/gitment-introduction/
gitment:
enable: true
mint: true # RECOMMEND, A mint on Gitment, to support count, language and proxy_gateway
count: true # Show comments count in post meta area
lazy: false # Comments lazy loading with a button
cleanly: false # Hide 'Powered by ...' on footer, and more
language: # Force language, or auto switch by theme
github_user: kuring # MUST HAVE, Your Github ID
github_repo: gitment-comments # MUST HAVE, The repo you use to store Gitment comments
client_id: xxx # MUST HAVE, Github client id for the Gitment
client_secret: xxxx # EITHER this or proxy_gateway, Github access secret token for the Gitment
proxy_gateway: # Address of api proxy, See: https://github.com/aimingoo/intersect
redirect_protocol: # Protocol of redirect_uri with force_redirect_protocol when mint enabled

执行hexo clean && hexo g && hexo s重新生成页面并在本地运行,可以看到gitment组件已经可以显示了,但是提示Error: Comments Not Initialized错误,点击login,然后允许认证,即可消除该错误。

在界面上添加评论后,可以在github repo的issuse中看到,整个搭建完毕。

概要

netfilter与iptables的关系

linux在内核中对数据包过滤和修改的模块为netfilter,netfilter模块本身并不对数据包进行过滤,只是允许将过滤数据包或修改数据包的函数hook到内核网络协议栈的适当位置。

iptables是用户态的工具,用于向netfilter中添加规则从而实现报文的过滤和修改等功能,工作在ip层。ebtables工作在数据链路层,用于处理以太网帧。

图中绿色代表iptables的表,可以看到有部分位于了数据链路层,之所以产生这种奇怪的架构,原因是bridge_nf模块,因为bridge工作在数据链路层,不一定会经过网络层,但仍然需要iptables的功能。详细信息可以在ebtables/iptables interaction on a Linux-based bridge中了解。

概念:tables -> chains -> rules

iptabels介绍

chain

每个表都由一组内置的链,还可以添加用户自定义链,只是用户自定义链没有钩子可以触发,需要从其他链通过-j即JUMP进行触发。

  • INPUT 链:发往本机的报文
  • OUTPUT 链:由本机发出的报文
  • FORWARD 链:经由本机转发的报文
  • PREROUTING 链:报文到达本机,进行路由决策之前
  • POSTROUTING 链:报文由本机发出,进行路由决策之后

image

从chain的角度考虑数据包的流向:

  • 到本机某进程的报文:PREROUTING -> INPUT
  • 由本机转发的报文:PREROUTING -> FORWARD -> POSTROUTING
  • 由本机某进程发出的报文:OUTPUT -> POSTROUTING

当一个网络包进入一台机器的时候,首先拿下 MAC 头看看,是不是我的。如果是,则拿下 IP 头来。得到目标 IP 之后呢,就开始进行路由判断。在路由判断之前,这个节点我们称为 PREROUTING。如果发现 IP 是我的,包就应该是我的,就发给上面的传输层,这个节点叫作 INPUT。如果发现 IP 不是我的,就需要转发出去,这个节点称为 FORWARD。如果是我的,上层处理完毕完毕后,一般会返回一个处理结果,这个处理结果会发出去,这个节点称为 OUTPUT,无论是 FORWARD 还是 OUTPUT,都是路由判断之后发生的,最后一个节点是 POSTROUTING。

table

有了chain的概念后,为了便于chain中rule的管理,又引入了table的概念,用于存放相同功能的rule,不同功能的rule放到不同的table中。

包括:filter nat mangle raw

filter

默认表,管理本机数据包的进出,用于实现包的过滤,对应内核模块iptables_filter

input:想要进入linux主机的包
output:linux主机要发送的包
forward:传递包到后端计算机,与nat table关联较多

nat

管理后端主机进出,与linux主机没有关系,与linux后的主机有关

prerouting:进行路由判断之前的规则(dnat/redirect)
postrouting:路由判断之后执行的规则(snat/masquerade)
output:与发出去的包有关

mangle

较少使用,用于拆解报文,修改数据包,并重新封装。

raw

raw表的主要作用是允许我们给某些特定的数据包打上标记。

rule

包含了匹配条件和处理动作。

匹配条件包括:source ip、destination ip、source port、destination port

处理动作包括:

  • accept: 将包交给协议栈
  • drop:直接丢弃数据包,不给任何回应
  • reject:拒绝数据包通过,并给一个响应信息,客户端会收到拒绝消息
  • queue: 交个某个用户态进程处理
  • dnat:目的地址转换
  • snat:源地址转换,必须要指定SNAT地址,即–to-source参数,可以是单个ip,也可以是网段。用在POSTROUTING链上。
  • masquerade: 源地址伪装,跟snat类似,不需要指定SNAT地址,会自动从服务器上获取SNAT的ip地址。如果有多个网卡的情况下,会使用路由选择算法。
  • mark: 对数据包进行打标签操作

table filter rule的关系

这三者之间的关系还是相当的绕。

链中的规则存在的表

chain中存放了rule,某些chain中注定不包含某些rule。例如prerouting链中的rule仅存在于nat raw mangle三张表中。

prerouting链中的规则存在的表:raw mangle nat
input链中的规则存在的表:mangle filter nat
forward链中的规则存在的表:mangle filter
output链中的规则存在的表:raw mangle filter nat
postrouting链中的规则存在的表:mangle nat

表中的规则可以被哪些链使用

raw表中的规则可以被链使用:prerouting output

image

表的名字为小写,链的名字为大写

常用操作

查询

iptables 查询默认的表为filter,默认会列出表中所有链的规则

  • -t 用于指定要操作的表,支持raw mangle filter nat,省略-t选项,默认使用filter表
  • -L 列出rule
  • -v 可查看更详细的信息
  • -n 规则中以ip地址的形式进行显示
  • –line-number 显示规则的编号
  • -x 包的计数以精确数字显示

iptables -t filter -L:从表的角度查询规则,用于查看filter表中的所有规则

iptables -L INPUT: 从链的角度查询规则,用于查看INPUT链中的所有规则

iptables -vL INPUT: 从链的角度查询规则,用于查看INPUT链中的所有规则,可查看更详细信息,包含了规则的匹配信息

iptables -nvL:以精确数字显示

修改

  • -F: 清空规则
  • -I: 表示插入规则
  • -A: 表示以追加的方式插入规则
  • --dport: 目的端口
  • --sport: 源端口
  • -s: 源ip
  • -d: 目的ip
  • --match-set:匹配ipset

iptables -F INPUT:清空filter表中的INPUT链中的所有规则。

删除

  • -D: 删除规则,iptables -D 链名 规则编号,其中规则编号可以通过--line-number查看到。
  • -F: 清空规则,iptables -F 链名 -t 表名
  • -X: 删除链 iptables -X 链名 -t 表名

trace

开启trace功能

1
2
3
4
5
# centos7 系统下有效,centos6下内核模块为ipt_LOG
$ modprobe nf_log_ipv4

# 用来验证module是否加载成功
$ sysctl net.netfilter.nf_log.2

要开启icmp协议的追踪,执行如下的命令

1
2
iptables -t raw -A OUTPUT -p icmp -m comment --comment "TRACE" -j TRACE
iptables -t raw -A PREROUTING -p icmp -m comment --comment "TRACE" -j TRACE

可以通过如下的命令看到插入的iptabels规则:

1
iptables -t raw -nvL --line-number

追踪日志最终会在/var/log/message或者/var/log/kern下看到:

1
Feb  6 11:22:04 c43k09006.cloud.k09.am17 kernel: TRACE: raw:PREROUTING:policy:3 IN=docker0 OUT= PHYSIN=bond0.9 MAC=02:42:30:fb:43:94:5c:c9:99:de:c4:8b:08:00 SRC=10.45.8.10 DST=10.45.4.99 LEN=84 TOS=0x00 PREC=0x00 TTL=62 ID=25550 DF PROTO=ICMP TYPE=0 CODE=0 ID=24191 SEQ=2

格式这块的含义如下:

“TRACE: tablename:chainname:type:rulenum “ where type can be “rule” for plain rule, “return” for implicit rule at the end of a user defined chain and “policy” for the policy of the built in chains.

环境清理,删除刚刚创建的规则即可,其中1为规则的编号:

1
2
3
4
5
# 可以通过此来查询之前创建的规则编号
iptables -t raw --line-number -nvL
# 删除规则
iptables -t raw -D PREROUTING 1
iptables -t raw -D OUTPUT 1

实战

试验1 基本规则管理

插入规则

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
# 清空filter表中的input链规则
[vagrant@localhost ~]$ sudo iptables -F INPUT

# 查看filter表中的详细规则,此时从其他机器上ping该ip是通的
[vagrant@localhost ~]$ sudo iptables -nvL INPUT
Chain INPUT (policy ACCEPT 7 packets, 388 bytes)
pkts bytes target prot opt in out source destination

# 增加规则,拒绝192.168.33.1上的请求
# -I:表示插入
# INPUT为要插入的链
# -s:表示源ip地址
# -j:表示要执行的动作
[vagrant@localhost ~]$ sudo iptables -t filter -I INPUT -s 192.168.33.1 -j DROP

# 再次查询filter表中的规则,此时192.168.33.1上的报文已经不通
[vagrant@localhost ~]$ sudo iptables -t filter -nvL
Chain INPUT (policy ACCEPT 107 packets, 6170 bytes)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * * 192.168.33.1 0.0.0.0/0

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 56 packets, 4355 bytes)
pkts bytes target prot opt in out source destination

# appent一条接收192.168.33.1的请求规则
[vagrant@localhost ~]$ sudo iptables -t filter -A INPUT -s 192.168.33.1 -j ACCEPT
# 新增加的序号为2,192.168.33.1的包匹配到1后就停止往下走,因此192.168.33.1还是ping不通当前主机
[vagrant@localhost ~]$ sudo iptables -nvL INPUT --line-number
Chain INPUT (policy ACCEPT 65 packets, 3572 bytes)
num pkts bytes target prot opt in out source destination
1 9 756 DROP all -- * * 192.168.33.1 0.0.0.0/0
2 0 0 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0

# 插入一条ACCEPT rule,此时192.168.33.1可以ping通当前主机,新插入的规则优先
[vagrant@localhost ~]$ sudo iptables -t filter -I INPUT -s 192.168.33.1 -j ACCEPT
[vagrant@localhost ~]$ sudo iptables -nvL INPUT --line-number
Chain INPUT (policy ACCEPT 7 packets, 388 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0
2 10 840 DROP all -- * * 192.168.33.1 0.0.0.0/0
3 0 0 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0

# 新插入一条accept 192.168.33.2的规则,插入位置为2,可以看到插入到2的位置了
[vagrant@localhost ~]$ sudo iptables -t filter -I INPUT 2 -s 192.168.33.2 -j ACCEPT
[vagrant@localhost ~]$ sudo iptables -nvL INPUT --line-number
Chain INPUT (policy ACCEPT 7 packets, 388 bytes)
num pkts bytes target prot opt in out source destination
1 1 84 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0
2 0 0 ACCEPT all -- * * 192.168.33.2 0.0.0.0/0
3 10 840 DROP all -- * * 192.168.33.1 0.0.0.0/0
4 0 0 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0

删除规则

接下在上面实验的基础上测试删除规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 删除刚刚创建的规则2
[vagrant@localhost ~]$ sudo iptables -t filter -D INPUT 2
[vagrant@localhost ~]$ sudo iptables -nvL INPUT --line-number
Chain INPUT (policy ACCEPT 7 packets, 388 bytes)
num pkts bytes target prot opt in out source destination
1 1 84 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0
2 10 840 DROP all -- * * 192.168.33.1 0.0.0.0/0
3 0 0 ACCEPT all -- * * 192.168.33.1 0.0.0.0/0

# 删除source为192.168.33.1,动作为ACCEPT的规则,实际此时执行一次命令仅能删除一条
[vagrant@localhost ~]$ sudo iptables -t filter -D INPUT -s 192.168.33.1 -j ACCEPT
[vagrant@localhost ~]$ sudo iptables -nvL INPUT --line
Chain INPUT (policy ACCEPT 13 packets, 736 bytes)
num pkts bytes target prot opt in out source destination
1 11 936 DROP all -- * * 192.168.33.1 0.0.0.0/0

修改规则

在上面实验的基础上修改规则

1
2
3
4
5
6
7
8
9
10
# 将规则动作从REJECT更改为REJECT
[vagrant@localhost ~]$ sudo iptables -t filter -R INPUT 1 -s 192.168.33.1 -j REJECT
[vagrant@localhost ~]$ sudo iptables -nvL INPUT --line
Chain INPUT (policy ACCEPT 7 packets, 388 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT all -- * * 192.168.33.1 0.0.0.0/0 reject-with icmp-port-unreachable

# 每个链都有一个默认规则,当前INPUT链中的默认为ACCEPT
# 以下可以修改INPUT链的默认规则为DROP,远程连接慎用,不要问我为什么
[vagrant@localhost ~]$ sudo iptables -t filter -P INPUT DROP

保存规则

防火墙的所有修改都是临时的,重启系统后会失效。iptables会读取/etc/sysconfig/iptables中的规则。

1
2
3
4
5
# iptables-save命令仅会打印当前的规则,需要使用重定向当前规则到文件中
[root@localhost system]# iptables-save > /etc/sysconfig/iptables

# 可以从规则文件中载入规则
[root@localhost system]# iptables-restore < /etc/sysconfig/iptables

实验二 各类匹配条件的使用

匹配条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 可一次性插入两条规则
[vagrant@localhost ~]$ sudo iptables -t filter -I INPUT -s 192.168.33.1,192.168.33.2 -j DROP
[vagrant@localhost ~]$ sudo iptables -t filter -nvL INPUT --line
Chain INPUT (policy ACCEPT 31 packets, 1744 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * * 192.168.33.2 0.0.0.0/0
2 0 0 DROP all -- * * 192.168.33.1 0.0.0.0/0

# 可指定ip网段
[vagrant@localhost ~]$ sudo iptables -t filter -F INPUT
[vagrant@localhost ~]$ sudo iptables -t filter -I INPUT -s 192.168.33.0/24 -j DROP
[vagrant@localhost ~]$ sudo iptables -t filter -nvL INPUT --line
Chain INPUT (policy ACCEPT 7 packets, 388 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * * 192.168.33.0/24 0.0.0.0/0

[vagrant@localhost ~]$ sudo iptables -t filter -I INPUT ! -s 192.168.33.0/24 -j DROP
[vagrant@localhost ~]$ sudo iptables -t filter -nvL INPUT --line
Chain INPUT (policy ACCEPT 19 packets, 1048 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * * !10.0.2.0/24 0.0.0.0/0

协议类型

使用-p来指定协议类型,支持tcp udp icmp等,不指定时默认匹配所有协议

网卡接口

-i来指定从某个网卡进入的流量,仅使用于PREROUTING INPUT FORWARD三条链。

-o来指定从某个网络流出的流量,仅适用于FORWARD OUTPUT POSTROUTING三条链。

实验三 扩展模块

端口

使用了扩展模块tcp udp,默认可以省略

--dport来匹配报文的目的端口,使用时必须指定协议,即-p选项。
--sport来匹配报文的源端口,使用时必须指定协议,即-p选项。

端口可以指定范围,例如22:25表示22-25之间的所有端口,22,25表示22和25端口,还可以配合起来使用,比如22,80:88表示22和80-88之间的端口。

1
2
3
4
5
6
# 可以指定目的端口的范围
[root@localhost vagrant]# iptables -t filter -I INPUT -s 192.168.33.1 -p tcp --dport 22:25 -j REJECT
[root@localhost vagrant]# iptables -t filter -nvL INPUT --line
Chain INPUT (policy ACCEPT 70 packets, 4024 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT tcp -- * * 192.168.33.1 0.0.0.0/0 tcp dpts:22:25 reject-with icmp-port-unreachable

iprange扩展模块

iprange扩展模块可以指定一段连续的ip地址范围。

--src-range--dst-range用来指定源地址和目的范围。

1
2
3
4
5
[root@localhost vagrant]# iptables -t filter -I INPUT -m iprange --src-range 192.168.33.1-192.168.33.10 -j DROP
[root@localhost vagrant]# iptables -t filter -nvL INPUT --line
Chain INPUT (policy ACCEPT 17 packets, 968 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 source IP range 192.168.33.1-192.168.33.10

string扩展模块

匹配报文中包含的字符串

1
2
3
4
5
6
# 匹配报文中包含XXOO的报文
[root@localhost vagrant]# iptables -t filter -I INPUT -m string --algo bm --string "XXOO" -j REJECT
[root@localhost vagrant]# iptables -t filter -nvL INPUT --line
Chain INPUT (policy ACCEPT 15 packets, 852 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 STRING match "XXOO" ALGO name bm TO 65535 reject-with icmp-port-unreachable

其他扩展

time扩展用来根据时间段进行匹配

connlimit用来对ip的并发连接数进行限制

limit模块限制单位时间内进出包的数量

tcp扩展中可以使用--tcp-flags可根据tcp flag进行匹配

state扩展可根据tcp的连接状态进行匹配

实验四 自定义链

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
# 创建自定义链 IN_WEB
[root@localhost vagrant]# iptables -t filter -N IN_WEB
[root@localhost vagrant]# iptables -nvL
Chain INPUT (policy ACCEPT 31 packets, 1780 bytes)
pkts bytes target prot opt in out source destination

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 16 packets, 1216 bytes)
pkts bytes target prot opt in out source destination

Chain IN_WEB (0 references)
pkts bytes target prot opt in out source destination

[root@localhost vagrant]# iptables -t filter -I IN_WEB -s 192.168.33.1 -j REJECT
[root@localhost vagrant]# iptables -t filter -I IN_WEB -s 192.168.33.2 -j REJECT

[root@localhost vagrant]# iptables -t filter -nvL IN_WEB --line
Chain IN_WEB (0 references)
num pkts bytes target prot opt in out source destination
1 0 0 REJECT all -- * * 192.168.33.2 0.0.0.0/0 reject-with icmp-port-unreachable
2 0 0 REJECT all -- * * 192.168.33.1 0.0.0.0/0 reject-with icmp-port-unreachable

# 将IN_WEB自定义链添加到INPUT链上
[root@localhost vagrant]# iptables -t filter -I INPUT -p tcp --dport 80 -j IN_WEB
# 可以看到INPUT链中多出了IN_WEB链
[root@localhost vagrant]# iptables -nvL
Chain INPUT (policy ACCEPT 35 packets, 2012 bytes)
pkts bytes target prot opt in out source destination
0 0 IN_WEB tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 18 packets, 1408 bytes)
pkts bytes target prot opt in out source destination

Chain IN_WEB (1 references)
pkts bytes target prot opt in out source destination
0 0 REJECT all -- * * 192.168.33.2 0.0.0.0/0 reject-with icmp-port-unreachable
0 0 REJECT all -- * * 192.168.33.1 0.0.0.0/0 reject-with icmp-port-unreachable

# 重新定义自定链名字
[root@localhost vagrant]# iptables -E IN_WEB WEB
[root@localhost vagrant]# iptables -nvL
Chain INPUT (policy ACCEPT 39 packets, 2244 bytes)
pkts bytes target prot opt in out source destination
0 0 WEB tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 20 packets, 1520 bytes)
pkts bytes target prot opt in out source destination

Chain WEB (1 references)
pkts bytes target prot opt in out source destination
0 0 REJECT all -- * * 192.168.33.2 0.0.0.0/0 reject-with icmp-port-unreachable
0 0 REJECT all -- * * 192.168.33.1 0.0.0.0/0 reject-with icmp-port-unreachable

# 由于iptables有自定义链,不能删除
[root@localhost vagrant]# iptables -X WEB
iptables: Too many links.
# 将INPUT链引用的WEB链删除
[root@localhost vagrant]# iptables -D INPUT 1
# 此时仍不能删除自定义链,因为自定义链删除,需要上面没有任何规则
[root@localhost vagrant]# iptables -X WEB
iptables: Directory not empty.

# 先清空自定义链的规则后可以删除
[root@localhost vagrant]# iptables -F WEB
[root@localhost vagrant]# iptables -X WEB

ref

为了其他主机可访问docker registry,必须采用https协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
registry_data_dir=~/docker_registry/data
cert_dir=~/docker_registry/certs
signdomain=mycert

# 制作证书
mkdir -p ${cert_dir}
openssl req -nodes -subj "/C=CN/ST=BeiJing/L=BeiJing/CN=$signdomain" -newkey rsa:4096 -keyout ${cert_dir}/$signdomain.key -out ${cert_dir}/${signdomain}.csr
openssl x509 -req -days 3650 -in ${cert_dir}/$signdomain.csr -signkey ${cert_dir}/${signdomain}.key -out ${cert_dir}/$signdomain.crt

# 从docker hub拉取registry镜像,并启动镜像
mkdir -p ${registry_data_dir}
docker run -d -p 15000:5000 --restart=always --name registry \
-v ${registry_data_dir}:/var/lib/registry \
-v ${cert_dir}:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/${signdomain}.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/${signdomain}.key \
registry:2

停止registry镜像并删除的命令为:

1
docker stop registry && docker rm -v registry

下载最新的centos7镜像

1
docker pull centos:7.3.1611

将centos7镜像增加tag

1
2
3
4
5
6
7
8
docker tag centos:7.3.1611 127.0.0.1:15000/centos:7.3

# 可以看到列表中会多出一个镜像
[root@103-17-184-lg-201-k08 data]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/registry 2 047218491f8c 4 weeks ago 33.17 MB
103-17-184-lg-201-k08.yidian.com:5000/centos 7.3 67591570dd29 3 months ago 191.8 MB
docker.io/centos 7.3.1611 67591570dd29 3 months ago 191.8 MB

docker push命令仅支持https协议,签名已经启动了自签名的https协议的registry,为了能够让docker能够信任registry,需要在/etc/docker/certs.d/目录下增加相应的crt文件,增加后的目录结构为/etc/docker/certs.d/103-17-184-lg-201-k08.yidian.com:5000/103-17-184-lg-201-k08.yidian.com.crt,添加完成后需要重启docker服务。

将image push到registry

1
docker push 103-17-184-lg-201-k08.yidian.com:5000/centos:7.3 

api

可以直接通过curl命令来访问api:curl --cacert 103-17-184-lg-201-k08.yidian.com.crt -v https://103-17-184-lg-201-k08.yidian.com:5000/v2

ref

0%