Go netpoll大解析(上)

简介: Go netpoll大解析

开篇


之前简单看过一点go原生netpoll,没注意太多细节。最近从头到尾看了一遍,特写篇文章记录下。文章很长,请耐心看完,一定有所收获。


内核空间和用户空间


在linux中,经常能看到两个词语:User space(用户空间)和Kernel space (内核空间)。


简单说, Kernel space是linux内核运行的空间,User space是用户程序运行的空间。它们之间是相互隔离的。


现代操作系统都是采用虚拟存储器。那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。


操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。


为了保证用户进程不能直接操作内核,保证内核的安全,系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。


针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。空间分配如下图所示:


1668515813323.jpg


Kernel space可以调用系统的一切资源。User space 不能直接调用系统资源,在 Linux系统中,所有的系统资源管理都是在内核空间中完成的。


比如读写磁盘文件、分配回收内存、从网络接口读写数据等等。应用程序无法直接进行这样的操作,但是用户程序可以通过内核提供的接口来完成这样的任务。


1668515827238.jpg


像下面这样,


1668515840149.jpg


应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:”我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态。


在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。


具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。


此时应用程序已经从系统调用中返回并且拿到了想要的数据,继续往下执行用户空间执行逻辑。


这样的话,一旦涉及到对I/O的处理,就必然会涉及到在用户态和内核态之间来回切换。


io模型


网上有太多关于I/O模型的文章,看着看着有可能就跑偏了,所以我还是从 <<UNIX 网络编程>> 中总结的5中I/O模型说起吧。


Unix可用的5种I/O模型。

  • 阻塞I/O
  • 非阻塞I/O
  • I/O复用
  • 信号驱动式I/O(SIGIO)
  • 异步I/O(POSIX的aio_系列函数)


阻塞I/O


1668515861988.jpg


阻塞式I/O下,进程调用recvfrom,直到数据到达且被复制到应用程序的缓冲区中或者发生错误才返回,在整个过程进程都是被阻塞的


非阻塞I/O


1668515905770.jpg


从图中可以看出,前三次调用recvfrom中没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。


第四次调用recvfrom时已有一个数据报准备好,它被复制到应用程序缓冲区,于是recvfrom成功返回。


当一个应用程序像这样对一个非阻塞描述符循环调用recvfrom时,我们通常称为轮询(polling),持续轮询内核,以这种方式查看某个操作是否就绪。


I/O多路复用


1668515950829.jpg


有了I/O多路复用(I/O multiplexing),我们就可以调用 select 或者 poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。


上面这句话难理解是吧。


说白了这里指的是,在第一步中,我们只是阻塞在select调用上,直到数据报套接字变为可读,返回可读条件,这里并没有发生I/O事件,所以说这一步,并没有阻塞在真正的I/O系统调用上。


其他两种就不过多介绍了。还有一点,我们会经常提到同步I/O和异步I/O。


POSIX 把这两种术语定义如下:

  • 同步I/O操作(synchronous I/O opetation) 导致请求进程被阻塞,直到I/O操作完成。
  • 异步I/O(asynchronous opetation) 不导致请求进程被阻塞。


基于上面的定义,


1668515969645.jpg


异步I/O的关键在于第二步的recrfrom是否会阻塞住用户进程,如果不阻塞,那它就是异步I/O。从上面汇总图中可以看出,只有异步I/O满足POSIX中对异步I/O的定义。


Go netpoller


Go netpoller 底层就是对I/O多路复用的封装。不同平台对I/O多路复用有不同的实现方式。比如Linux的select、poll和epoll。


在MacOS则是kqueue,而Windows是基于异步I/O实现的icop......,基于这些背景,Go针对不同的平台调用实现了多版本的netpoller。


1668516022079.jpg


下面我们通过一个demo开始讲解。


1668516037196.jpg


很简单一个demo,开启一个tcp服务。然后每来一个连接,就启动一个g去处理连接。处理完毕,关闭连接。


而且我们使用的是同步的模式去编写异步的逻辑,一个连接对应一个g处理,极其简单和易于理解。go标准库中的http.server也是这么干的。


1668516048559.jpg


针对上面的tcp服务demo,我们需要关注这段代码底层都发生了什么。


上面代码中主要涉及底层的一些结构。


1668516059060.jpg


先简单解释一波。

  • TCPListener:我们开启的是一个TCP服务,那当然就是TCP服务的网络监听器。
  • netFD:网络描述符。Go中所有的网络操作都是以netFD实现的,它和底层FD做绑定。
  • FD:文件描述符。net和os包把这个类型作为一个网络连接或者操作系统文件。其中里面一个字段Sysfd就是具体文件描述符值。
  • pollDesc:I/O轮询器。说白了它就是底层事件驱动的封装。其中的runtimeCtx是一个指针类型,具体指向runtime/netpoll 中的pollDesc.


当然图上面结构字段都是阉割版的,但是不影响我们这篇文章。


还有一个问题,为什么结构上需要一层一层嵌入呢?我的理解是每下一层都是更加抽象的一层。它是可以作为上一层具体的一种应用体现。


是不是跟没说一样?哈哈。


举例,比如这里的netFD表示网络描述符。


它的上一层可以是用于TCP的网络监听器TCPListener,那么对应的接口我们能想到的有两个Accept以及close。


对于Accept 动作,一定是返回一个连接类型 Conn ,针对这个连接,它本身也存在一个自己的netFD,那么可想而知一定会有 Write和Read两个操作。


而所有的网络操作都是以netFD实现的。这样,netFD在这里就有两种不的上层应用体现了。


好了,我们需要搞清楚几件事:

  • 一般我们用其他语言写一个tcp服务,必然会写这几步:socket->bind->listen,但是Go就一个Listen,那就意味着底层包装了这些操作。它是在哪一步完成的?
  • Go是在什么时候初始化netpoll的,比如linux下初始化epoll实例。
  • 当对应fd没有可读或者可写的IO事件而对应被挂起的g,是如何知道fd上的I/O事件已ready,又是如何唤醒对应的g的?


Listen解析


带着这些问题,我们接着看流程。


1668516107423.jpg


上图已经把当你调用Listen操作的完整流程全部罗列出来了。


就像我上面列出的结构关系一样,从结构层次来说,每调用下一层,都是为了创建并获取下一层的依赖,因为内部的高度抽象与封装,才使得使用者往往只需调用极少数简单的API接口。


现在我们已经知道事例代码涉及到的结构以及对应流程了。


在传统印象中,创建一个网络服务。需要经过:创建一个socket、bind 、listen这基本的三大步。


前面我们说过,Go中所有的网络操作都是以netFD实现的。go也是在这一层封装这三大步的。所以我们直接从netFD逻辑开始说。


上图是在调用socket函数这一步返回的netFD,可想而核心逻辑都在这里面。


1668516127547.jpg


我们可以把这个函数核心点看成三步。

  • 调用sysSocket函数创建一个socket,返回一个文件描述符(file descriptor),简称fd下文。
  • 通过sysSocket返回的fd,调用newFD函数创建一个新的netFD。
  • 调用netFD自身的方法listenStream函数,做初始化动作,具体详情下面再说。


在sysSocket函数中,首先会通过socketFunc来创建一个socket,通过层层查看,最终是通过system call来完成这一步。


当获取到对应fd时,会通过syscall.SetNonblock函数把当前这个fd设置成非阻塞模式,这样当这个Listener调用accept函数就不会被阻塞了。


1668516140740.jpg


第二步,通过第一步创建socket拿到的fd,创建一个新的netFD。这段代码没啥好解释的。


第三步,也就是最核心的一步,调用netFD自身的listenStream方法。


1668516156140.jpg

相关文章
|
2月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟蒋星熠Jaxonic,Go语言探索者。深耕云计算、微服务与并发编程,以代码为笔,在二进制星河中书写极客诗篇。分享Go核心原理、性能优化与实战架构,助力开发者掌握云原生时代利器。#Go语言 #并发编程 #性能优化
444 43
Go语言深度解析:从入门到精通的完整指南
|
4月前
|
数据采集 数据挖掘 测试技术
Go与Python爬虫实战对比:从开发效率到性能瓶颈的深度解析
本文对比了Python与Go在爬虫开发中的特点。Python凭借Scrapy等框架在开发效率和易用性上占优,适合快速开发与中小型项目;而Go凭借高并发和高性能优势,适用于大规模、长期运行的爬虫服务。文章通过代码示例和性能测试,分析了两者在并发能力、错误处理、部署维护等方面的差异,并探讨了未来融合发展的趋势。
375 0
|
8月前
|
算法 Go 索引
【LeetCode 热题100】45:跳跃游戏 II(详细解析)(Go语言版)
本文详细解析了力扣第45题“跳跃游戏II”的三种解法:贪心算法、动态规划和反向贪心。贪心算法通过选择每一步能跳到的最远位置,实现O(n)时间复杂度与O(1)空间复杂度,是面试首选;动态规划以自底向上的方式构建状态转移方程,适合初学者理解但效率较低;反向贪心从终点逆向寻找最优跳点,逻辑清晰但性能欠佳。文章对比了各方法的优劣,并提供了Go语言代码实现,助你掌握最小跳跃次数问题的核心技巧。
348 15
|
3月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟 蒋星熠Jaxonic,执着的星际旅人,用Go语言编写代码诗篇。🚀 Go语言以简洁、高效、并发为核心,助力云计算与微服务革新。📚 本文详解Go语法、并发模型、性能优化与实战案例,助你掌握现代编程精髓。🌌 从goroutine到channel,从内存优化到高并发架构,全面解析Go的强大力量。🔧 实战构建高性能Web服务,展现Go在云原生时代的无限可能。✨ 附技术对比、最佳实践与生态全景,带你踏上Go语言的星辰征途。#Go语言 #并发编程 #云原生 #性能优化
|
8月前
|
机器学习/深度学习 存储 算法
【LeetCode 热题100】347:前 K 个高频元素(详细解析)(Go语言版)
这篇文章详细解析了力扣热题 347——前 K 个高频元素的三种解法:哈希表+小顶堆、哈希表+快速排序和哈希表+桶排序。每种方法都附有清晰的思路讲解和 Go 语言代码实现。小顶堆方法时间复杂度为 O(n log k),适合处理大规模数据;快速排序方法时间复杂度为 O(n log n),适用于数据量较小的场景;桶排序方法在特定条件下能达到线性时间复杂度 O(n)。文章通过对比分析,帮助读者根据实际需求选择最优解法,并提供了完整的代码示例,是一篇非常实用的算法学习资料。
520 90
|
4月前
|
缓存 监控 安全
告别缓存击穿!Go 语言中的防并发神器:singleflight 包深度解析
在高并发场景中,多个请求同时访问同一资源易导致缓存击穿、数据库压力过大。Go 语言提供的 `singleflight` 包可将相同 key 的请求合并,仅执行一次实际操作,其余请求共享结果,有效降低系统负载。本文详解其原理、实现及典型应用场景,并附示例代码,助你掌握高并发优化技巧。
349 0
|
4月前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。
|
6月前
|
存储 设计模式 安全
Go 语言单例模式全解析:从青铜到王者段位的实现方案
单例模式确保一个类只有一个实例,并提供全局访问点,适用于日志、配置管理、数据库连接池等场景。在 Go 中,常用实现方式包括懒汉模式、饿汉模式、双重检查锁定,最佳实践是使用 `sync.Once`,它并发安全、简洁高效。本文详解各种实现方式的优缺点,并提供代码示例与最佳应用建议。
212 5
|
7月前
|
存储 算法 Go
【LeetCode 热题100】17:电话号码的字母组合(详细解析)(Go语言版)
LeetCode 17题解题思路采用回溯算法,通过递归构建所有可能的组合。关键点包括:每位数字对应多个字母,依次尝试;递归构建下一个字符;递归出口为组合长度等于输入数字长度。Go语言实现中,使用map存储数字到字母的映射,通过回溯函数递归生成组合。时间复杂度为O(3^n * 4^m),空间复杂度为O(n)。类似题目包括括号生成、组合、全排列等。掌握回溯法的核心思想,能够解决多种排列组合问题。
315 11
|
7月前
|
Go
【LeetCode 热题100】155:最小栈(详细解析)(Go语言版)
本文详细解析了力扣热题155:最小栈的解题思路与实现方法。题目要求设计一个支持 push、核心思路是使用辅助栈法,通过两个栈(主栈和辅助栈)来维护当前栈中的最小值。具体操作包括:push 时同步更新辅助栈,pop 时检查是否需要弹出辅助栈的栈顶,getMin 时直接返回辅助栈的栈顶。文章还提供了 Go 语言的实现代码,并对复杂度进行了分析。此外,还介绍了单栈 + 差值记录法的进阶思路,并总结了常见易错点,如 pop 操作时忘记同步弹出辅助栈等。
263 6

推荐镜像

更多
  • DNS