Contents

操作系统-小林coding-网络系统

本系列笔记为作者在跟随小林coding学习的时候做的笔记。感谢小林大大。

什么是零拷贝

减少「用户态与内核态的上下文切换」和「内存拷贝」的次数

两种实现方式

  1. mmap+write
https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D/mmap%20%2B%20write%20%E9%9B%B6%E6%8B%B7%E8%B4%9D.png
零拷贝1
  1. sendfile
https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D/senfile-3%E6%AC%A1%E6%8B%B7%E8%B4%9D.png
零拷贝2
  1. 真正的零拷贝技术:网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同)
https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E9%9B%B6%E6%8B%B7%E8%B4%9D/senfile-%E9%9B%B6%E6%8B%B7%E8%B4%9D.png
零拷贝3

I/O 多路复用:select/poll/epoll

I/O 多路复用:select/poll/epoll

go修养之路 流?I/O操作?阻塞?epoll?

最基本的 Socket 模型

创建 Socket 的时候,可以指定网络层使用的是 IPv4 还是 IPv6,传输层使用的是 TCP 还是 UDP

udp很简单这里只讲tcp

服务端调用 socket() 函数,创建网络协议为 IPv4,以及传输协议为 TCP 的 Socket ,接着调用 bind() 函数,给这个 Socket 绑定一个 IP 地址和端口,最后调用 listen() 函数进行监听

netstat 或 ss 查看正在被监听的端口号

客户端创建 Socket 后,调用 connect() 函数指明服务端的 IP 地址和端口号发起连接,然后开始 TCP 三次握手

服务端 TCP 连接的过程中每个 Socket 维护了两个队列:

  • TCP 半连接队列,syn_rcvd状态
  • TCP 全连接队列,established状态

TCP 全连接队列不为空时,服务端的 accept() 函数从内核中的 TCP 全连接队列里拿出一个已经完成连接的 Socket 返回应用程序,后续数据传输都用这个 Socket

监听的 Socket 和真正用来传数据的 Socket 是不同的

连接建立后,客户端和服务端都可以通过 read() 和 write() 函数来读写数据

Socket 也是以「文件」的形式存在的,也是有对应的文件描述符,Socket 文件的 inode 指向了内核中的 Socket 结构,它有发送队列和接收队列字段,保存链表形式的 sk_buff 结构体

sk_buff 结构体表示各层的数据包,为了不发生拷贝,发送和接收数据包时通过修改 skb->data 的值,来修改头部范围

https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/sk_buff.jpg
接收数据包

多进程模型

https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/%E5%A4%9A%E8%BF%9B%E7%A8%8B.png
多进程模型

多线程模型

https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/%E7%BA%BF%E7%A8%8B%E6%B1%A0.png
多线程模型

I/O 多路复用

select/poll/epoll使得进程可以通过一个系统调用函数从内核中获取多个事件

select/poll

select 将已连接的 Socket 都放到一个文件描述符集合,通过遍历文件描述符集合检查是否有事件产生,如果有,将此 Socket 标记为可读或可写,把整个文件描述符集合拷贝回用户态里,用户态需要再通过遍历的方法找到可读或可写的 Socket 对其处理。2 次「遍历」文件描述符集合,2 次「拷贝」文件描述符集合

select 使用固定长度的 BitsMap,支持的文件描述符的个数有限制,默认最大值为 1024,只能监听 0~1023 的文件描述符

poll 使用动态数组以链表形式组织,突破了 select 的文件描述符个数限制

它们都使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合

epoll

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

while(1) {
    int n = epoll_wait(...);
    for(接收到数据的socket){
        //处理
    }
}

epoll通过2个方法优化了select/poll

  • 红黑树来跟踪进程所有待检测的文件描述字,通过 epoll_ctl() 函数加入内核中的红黑树里,增删改一般时间复杂度是 O(logn),只需要传入一个待检测的 socket 而不是所有 socket 集合
  • 事件驱动,维护了一个链表来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序

epoll 被称为解决 C10K 问题的利器

边缘触发和水平触发

  • 边缘触发:服务器端只会从 epoll_wait 中苏醒一次
  • 水平触发:服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束

select/poll 只有水平触发

epoll 支持边缘触发和水平触发,默认是水平触发

什么是一致性哈希

https://cdn.xiaolincoding.com//mysql/other/d528bae6fcec2357ba2eb8f324ad9fd5.png
一致性哈希

提高均衡度:将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系

https://cdn.xiaolincoding.com//mysql/other/dbb57b8d6071d011d05eeadd93269e13.png
一致性哈希2
 |