Skip to content

Latest commit

 

History

History
248 lines (209 loc) · 13.8 KB

gvisor-tcpip.org

File metadata and controls

248 lines (209 loc) · 13.8 KB

gvisor-tcpip

1 简介

yes! gvisor里竟然包含了tcpip的golang实现。本文来撸一撸。

本人读的版本commit: 28291a5a5d25633c8bdf45ed5affe90f779c74b4

2 PacketBuffer之buf介绍

PacketBuffer是一个报文的结构封装贯穿于整个流程中。所以先把 PacketBuffer 了解一下。这里先了解一下 PacketBuffer 的底层buf细节。因为整个流程都用到这个结构,所以每一个field都可谓别有洞天,需要一个一个来消化,比如网卡、路由、conntrack等。

2.1 gvisor/pkg/buffer

这个package抽象了内存表示。其中buffer.View是离散的内存封装,内存最小的单位是buffer:

///home/coder/go/src/github.com/google/gvisor/pkg/buffer/buffer.go:24
type buffer struct {
  data  []byte
  read  int
  write int
  bufferEntry
}

两个指针read/write表示读取的长度和写的长度。函数 Full/ReadSize/WriteSize 可以看出来用意。 bufferEntry用于把buffer串起来。

封装了个函数可以去抠掉一部分data的接口: Remove,其他的都很简洁,这个Remove说一下。

为了表示区间增加了个Range的表示

// A Range specifies a range of buffer.
type Range struct {
  begin int
  end   int
}
//应该还是这种范围[begin, end)

Range的Intersect方法实在是骚。

///home/coder/go/src/github.com/google/gvisor/pkg/buffer/buffer.go:51
// Remove removes r from the unread portion. It returns false if r does not
// fully reside in b.
func (b *buffer) Remove(r Range) bool {
  sz := b.ReadSize()
  switch {
  case r.Len() != r.Intersect(Range{end: sz}).Len():// 确保可读的区间要 >= range
    return false
  case r.Len() == 0: // 抠掉的区间长度为空啥也不干
    // Noop
  case r.begin == 0: // 从read的开始的地方抠,假装抠掉的数据被读走了
    b.read += r.end
  case r.end == sz: // 从write地方往前抠,假装写的少了
    b.write -= r.Len()
  default:
    // Remove from the middle of b.data.
    // 从中间抠掉,读不改变,假装写的少了
    // |--------|
    // b.r |---|
    //     r.b r.e
    copy(b.data[b.read+r.begin:], b.data[b.read+r.end:b.write])
    b.write -= r.Len()
  }
  return true
}

好,buffer清楚之后就是bufferList了。 bufferList是个有头尾指针的双链表,用于串buffer,buffer里面的bufferEntry属性就是被它调用设置next/prev。作为数据结构的内容这里不描述了。 接下来是pool

///home/coder/go/src/github.com/google/gvisor/pkg/buffer/pool.go:37
type pool struct {
  bufferSize      int
  avail           []buffer              `state:"nosave"`
  embeddedStorage [embeddedCount]buffer `state:"wait"`
}

avail最先指向embeddedStorage的某个index,后面用满了之后指向新make的slice。buffer在pool这个结构中仅关心位置,里面的buffer里面的data另外初始化。注意从pool里拿一个buf的初始大小也会被设置(get 方法体现)

///home/coder/go/src/github.com/google/gvisor/pkg/buffer/pool.go:51
// get gets a new buffer from p without initializing it.
func (p *pool) getNoInit() *buffer {
  //最一开始的情况, avail没有初始化,先绑定
  if p.avail == nil {
    p.avail = p.embeddedStorage[:]
  }
  // 这个case是avail已经被切片用完了,此时avail不为nil,但是len为0
  // 需要重新开辟空间
  if len(p.avail) == 0 {
    p.avail = make([]buffer, embeddedCount)
  }
  if p.bufferSize <= 0 {
    p.bufferSize = defaultBufferSize
  }
  buf := &p.avail[0]
  // 配合的是上面第二个判断
  p.avail = p.avail[1:]
  return buf
}

接下来就是View了。

// /home/coder/go/src/github.com/google/gvisor/pkg/buffer/view.go:31
type View struct {
  data bufferList
  size int64 // size表示的所有buffer加起来的长度,而不是分片buffer的个数
  pool pool
}

方法就不贴代码了,要贴得贴满了。

构造函数: 静态创建即可,没有New…的pattern。

以下函数介绍按出场顺序介绍:

  1. TrimFront(count int64) ==> 从前面砍掉多少个字节,核心实现在 advanceRead 里面。实现方法就是从双链表的头开始一个一个的切。 当前的这个buf还分两种case,够砍的和不够砍的,够砍的砍完结束(break),不够砍的这个buf直接砍掉(从链表里Remove),相应更新下一轮数据和全局的data长度size。 最后还判断一下进来的场景是不是砍掉的字节比总长度小,如果不满足就panic。这里也发现整个框架在不可能出现的case地方都是直接panic的。k8s里面的代码panic的数量远小于这里的。
  2. Remove(offset, length int) bool ==> 从某个位置开始抠掉一些数据显然就比上面直接从头砍要细节很多了。 offset,length基于全局的。
    • 首先确保区间的正确: 待抠的range要在整个数据区间之内
    • 抠的时候还要考虑区间跨buf的case。甚至是跨多个buf的情况。用的方法是一个curr区间,每次遍历bufferList的时候先更新curr.end为当前end,当然表示还是全局的表示,当和input比较时,有交集就清理这个交集,没有交集继续跳。curr.begin在当前buf比较结束时更新。区间更新的时机巧妙。删除的时候要把全局位移转变成当前buff的位移,所以有个设置Offset的行为。
  3. ReadAt(p []byte, offset int64) (int, error) ==> 从offset位置开始read,并且read满。
    • 要判断offset是否在当前区间上。用的方法是offset减去前面的偏移是否大于当前buf的长度,如果大于等于则说明开始位置不在当前这个区间上。忽略

      ./include/images/buf-read-at.png

    • 注意一旦追上,只要开始copy,那么offset - skipped 就为0了,后面的buffer就是一直cp到p满为止或者EOF
  4. Truncate(length int64) ==> 强制缩到这个大小,不会长的,要求length必须 < size
    • 从后面开始往前遍历
    • 看看删除后的效果是否满足 < length,满足了, 这是最后一个了,干完收工,怎么干? 假装写少了,更新write指针到满足条件处。不满足继续删,注意这里的判断条件不包括等于,等于的case删整个当前buffer留在下面做了。

      ./include/images/buf-truncat.png

  5. Grow(length int64, zero bool) ==> 设置View的大小至length,zero表示是否用0填充
    • 判断最后一个是否为空或者还有空间可写?满足的话就从pool里拿个新的buffer。
    • 对这个可写的buf(最后一个或者是新拿的)进行写操作(更新buf.write指针),稍微判断一下写空间是否绰绰有余,多的话就按照left要求来
  6. Prepend(data []byte) ==> 将data塞到前面去
    • 第一个还有空间吗?有就塞一点,动的是buf.read指针,读过的就不要了,放在这些位置上,还分情况:
      • 第一个空间足够如下图:

        ./include/images/buf-has-space.png

      • 第一个空间不够,data写一点,buf写满

        ./include/images/buf-not-enough.png

  7. Append(data []byte) ==> 一直写,写到data尽头
  8. AppendOwned(data []byte) ==> data包裹上一个buf放到最后
  9. PullUp(offset, length int) ([]byte, bool) ==> offset开始length长度放到连续空间并返回
    • 保证区间正确
    • 判断区间是否跨buf了,下图展示的是跨buf的情况:

      ./include/images/buf-pullup.png

      不跨buf的情况就是在单buf之内,那么直接return就好了。如果交集为空跳过此buf即可。

    • 跨buf处理: 开始的buf已经记下了,然后统计下总共横跨多少个buf,然后要merge这些buf放到一个buf上。待merge的buf除了第一个不删除之外其余的都删掉。第一个的buf要用新产生的汇总data去替换。
    • 减去当前begin的偏移在当前data中返回对应需要的slice。
  10. Flatten() []byte ==> 就是打平所有的buf到一个上面并返回
  11. Size() int64 ==> 返回size属性,好轻松, phew~
  12. Copy() (other View) ==> 将当前view插入other后面
  13. Apply(fn func([]byte)) ==> 对每一个buf的data apply fn
  14. SubApply(offset, length int, fn func([]byte)) ==> 取offset处length长度的byte来apply fn

    ./include/images/buf-subapply.png

    需要判断offset是否是当前起始位置以及length是否超出当前buf,注意两个条件判断即可。

  15. Merge(other *View) ==> 把other吸溜过来放到最后
  16. WriteFromReader(r io.Reader, count int64) (int64, error) ==> 从r里读count过来,然后写到后面去
    • 小细节: buf还剩空间不到一个指针大小时,读一个指针大小的data然后调用Append来写,怎么扩容由Append搞定。
    • 操作如下图:

      ./include/images/buf-writ-from-reader.png

  17. ReadToWriter(w io.Writer, count int64) (int64, error) ==> View中从头开始读count字节写到w中。
    • offset什么意思很重要,理解了就理解了实现。offset是指上一此的读在当前buf中造成的偏移,下一次读要减去这个偏移。

      ./include/images/buf-read-to-write.png

      上图说明一下:

      1. offset是当前的buf的偏移,为了方便理解故意画的不为0的场景,造成这样的场景是上一轮按照最小批次的读也吸溜到了这个buf。
      2. sz - offset为当前可读的空间
      3. 方便理解故意画出了if条件中的case: 当前读又是按照最小批次来读,注意offset的更新: n - sz就是下一个buf的offset了。
      4. 还有一种情况是当前buf整个被前面吸溜完了,此时offset >= sz,直接continue,并且更新offset

于是乎,View的结构我们看完了,这根桩我们打完了,看后面的。

2.2 tcpip/buffer

除了顶级的buffer之外tcpip package里面还有一个buffer package。这里就相对简单一点了,主要是两个结构:

  1. View ==> []byte
  2. VectorisedView ==> [][]byte,不涉及到诸多指针操作,相对简单,不赘述API
    • 也有个PullUp 意思相近,返回连续空间的byte,不过是从头开始,没有offset。
  3. Prependable ==> 倒着长的buf,方便协议栈的前插,从data往前插tcp/network/link层的头
    • 里面有个属性: usedIdx表示从最后到 usedIdx 都被占了。所以初始化一个空的 Prependable 这个指针应该停在buf的len(buf)上,注意看 UsedLength 以及 AvailableLength 就好理解了。

2.3 一些全局属性

/home/coder/go/src/github.com/google/gvisor/pkg/tcpip/stack/registration.go 以及 /home/coder/go/src/github.com/google/gvisor/pkg/tcpip/tcpip.go 存放了一些全局属性,用到的时候再切过来

2.4 PacketBuffer之buf介绍

铺垫差不多,现在看关键结构里面的buf实现。

按照代码中的文档描述,buf结构配备了三个指针来分别在inbound和outbound中都可以方便的使用这个buf。

///home/coder/go/src/github.com/google/gvisor/pkg/tcpip/stack/packet_buffer.go:97
// buf is the underlying buffer for the packet. See struct level docs for
// details.
buf      *buffer.Buffer
reserved int
pushed   int
consumed int
  • 出向时要构造报文,所以会有reserve字段。入向时 reserved 字段为0,整个生命周期 reserved 字段值保持不变。
  • pushed用于标记push header,下文会结合代码进行描述
  • consumed用于parse header,同上。

下文对相关的代码进行说明:

  1. NewPacketBuffer(opts PacketBufferOptions) *PacketBuffer ==> 构造函数:传入的option会指出是保留reserve字段,有就创建指定的头部长度。
  2. 两个位移函数: push/consume

    ./include/images/packet-buf.png

    需要注意push标记的是与 reserved 之间的距离。往左, 而consume则往右。理解了这个方向一些API的操作才会更清楚。另外还有头的push并不一定是按照link/network/transport这样的顺序来的,可以是任何顺序,而怎么访问则定义了一个 headerInfo 每一个头信息在 PacketBuffer这个结构里都缓存着。字段名称叫: headers [numHeaderType]headerInfo ,但是从 PayloadSince 函数又看出,其实头还是按顺序排放的。(link/network/transport)

  3. HeaderSize() int ==> header长度
    • pushed + consumed ? 一个方向中总有一个值为0, 出入向复用 PacketBuffer 的体现之一。
  4. dataOffset() int ==> data的起始位置
    • reserved + consumed 也是一样的操作,一个方向中总有一个值为0
  5. PacketHeader/PacketData ==> 外围封装,提供对应的API,侧重点分别在header以及data部分的处理

其实夯下buf的基础看这些就比较接近api的调用了。比较简单,这里就不展开了。

3 tcp 伪首部:

12 字节 = 源IP + 目的IP + tcpprotocol + tcptotal length 4 4 2 2 checksum 计算方式:

  1. checksum的初始值自动被设置为0
  2. 然后,以16bit为单位,两两相加,对于该例子,即为:E34F + 2396 + 4427 + 99F3 = 1E4FF
  3. 若计算结果大于0xFFFF,则将,高16位加到低16位上,对于该例子,即为,0xE4FF + 0x0001 = E500
  4. 然后,将该值取反,即为~(E500)=1AFF