Linux 网络 —— 数据包的发送

背景 Linux 网络–数据包的接收 一文中分析了 Linux 是如何接收一个数据包的,那么 Linux 在发送数据包的时候又是什么样的呢?本文聚焦于 Linux 数据包的发送,结合内核源码分析数据包的发送过程。 关于 Linux 网络数据包的接收可以阅读前文: Linux 网络 —— 数据包的接收 日期: 2024-08-24   标签: #Linux  #net  背景 最初学习计算机网络的时候,通常都是从 TCP/IP 网络分层模型入手,学习各种协议,如 TCP、IP等,以及相关的原理,并未过多关注整个协议栈具体是如何实现的。在开发的过程中,通过高级语言往往只需要几行就能实现一个简单的网络程序,并从网络接收数据。然而数据具体是如何从网卡达到我们的进程中的呢?在这个过程中网卡和内核到底又做了些什么呢?数据在这个过程中是如何流转、复制的呢?带着这些问题,笔者最近学习了下 Linux 网络数据包的接收,并总结如下。 通过阅读本文应该能够了解: Linux 网络栈是如何接收数据的,数据从网卡到内核各个部分是如何进行流转的; 网络栈在正式工作之前需要经过哪些初始化,这些初始化工作的目的是什么; 数据包从网卡到内核需要经过几次复制; 网络相关的硬中断、软中断分别是如何实现的,二者是如何配合的; ethtool、tcpdump、iptables 等分别工作在何处,原理是什么。 本文基于内核 3.10 版本,网卡举例使用的是 igb 网卡。 <!DOCTYPE html> 系统初始化 Linux 系统在接收数据包之前需要做很多的准备工作,比如创建内核 ksoftirqd 线程,便于后续处理软中断;网络子系统初始化,注册各个协议的处理函数,便于后面的协议栈处理;网卡设备子系统初始化,便于后面从网卡接收数据包。这里首先对这些初始化的过程进行记录,其中重点是网卡设备的初始化。 内核线程 ksoftirqd 初始化 Linux 在接收网络数据的时候需要用到中断机制,包括硬中断和软中断。 其中 Linux 的软中断都是在内核线程 ksoftirq 中进行的,该线程在系统中有 N 个 (N=机器核数),线程被 CPU 调度。 系统初始化的时候创建了这一线程,之后它就进入到这自己的线程循环函数 ksoftirqd_should_run 和 run_ksoftirqd,判断有无中断需要处理。这里的中断不仅仅包括网络相关的软中断,还有其他的中断类型, 具体可以查看 Linux 的定义。 关于Linux中断相关的原理与实现,推荐阅读下面这篇文章: Linux 中断(IRQ/softirq)基础:原理及内核实现(2022) 网络子系统初始化 Linux 内核通过 subsys_initcall调用来初始化各个子系统,这里我们来看一看网络子系统的初始化 net_dev_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 28 29 30 31 32 33 34 35 36 static int __init net_dev_init(void) { ... ... for_each_possible_cpu(i) { struct softnet_data *sd = &per_cpu(softnet_data, i); memset(sd, 0, sizeof(*sd)); skb_queue_head_init(&sd->input_pkt_queue); skb_queue_head_init(&sd->process_queue); sd->completion_queue = NULL; INIT_LIST_HEAD(&sd->poll_list); sd->output_queue = NULL; sd->output_queue_tailp = &sd->output_queue; #ifdef CONFIG_RPS sd->csd.func = rps_trigger_softirq; sd->csd.info = sd; sd->csd.flags = 0; sd->cpu = i; #endif sd->backlog.poll = process_backlog; sd->backlog.weight = weight_p; sd->backlog.gro_list = NULL; sd->backlog.gro_count = 0; } ... open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); out: return rc; } subsys_initcall(net_dev_init); 网络子系统初始化的核心是为每个 CPU 都申请了一个 softnet_data数据结构,并且为网络中断注册了对应的处理函数,其中 NET_TX_SOFTIRQ 的处理函数是 net_tx_action, NET_RX_SOFTIRQ的处理函数是net_rx_action。中断和其对应的处理函数之间是如何对应的可以跟踪阅读 open_softirq。 ...... 通过阅读本文应该能够知悉: ...

发表于:2024-09-01 · 更新于: 2024-09-01

Linux 网络 —— 数据包的接收

背景 最初学习计算机网络的时候,通常都是从 TCP/IP 网络分层模型入手,学习各种协议,如 TCP、IP等,以及相关的原理,并未过多关注整个协议栈具体是如何实现的。在开发的过程中,通过高级语言往往只需要几行就能实现一个简单的网络程序,并从网络接收数据。然而数据具体是如何从网卡达到我们的进程中的呢?在这个过程中网卡和内核到底又做了些什么呢?数据在这个过程中是如何流转、复制的呢?带着这些问题,笔者最近学习了下 Linux 网络数据包的接收,并总结如下。 通过阅读本文应该能够了解: Linux 网络栈是如何接收数据的,数据从网卡到内核各个部分是如何进行流转的; 网络栈在正式工作之前需要经过哪些初始化,这些初始化工作的目的是什么; 数据包从网卡到内核需要经过几次复制; 网络相关的硬中断、软中断分别是如何实现的,二者是如何配合的; ethtool、tcpdump、iptables 等分别工作在何处,原理是什么。 本文基于内核 3.10 版本,网卡举例使用的是 igb 网卡。 <!DOCTYPE html> 系统初始化 Linux 系统在接收数据包之前需要做很多的准备工作,比如创建内核 ksoftirqd 线程,便于后续处理软中断;网络子系统初始化,注册各个协议的处理函数,便于后面的协议栈处理;网卡设备子系统初始化,便于后面从网卡接收数据包。这里首先对这些初始化的过程进行记录,其中重点是网卡设备的初始化。 内核线程 ksoftirqd 初始化 Linux 在接收网络数据的时候需要用到中断机制,包括硬中断和软中断。 其中 Linux 的软中断都是在内核线程 ksoftirq 中进行的,该线程在系统中有 N 个 (N=机器核数),线程被 CPU 调度。 系统初始化的时候创建了这一线程,之后它就进入到这自己的线程循环函数 ksoftirqd_should_run 和 run_ksoftirqd,判断有无中断需要处理。这里的中断不仅仅包括网络相关的软中断,还有其他的中断类型, 具体可以查看 Linux 的定义。 关于Linux中断相关的原理与实现,推荐阅读下面这篇文章: Linux 中断(IRQ/softirq)基础:原理及内核实现(2022) 网络子系统初始化 Linux 内核通过 subsys_initcall调用来初始化各个子系统,这里我们来看一看网络子系统的初始化 net_dev_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 28 29 30 31 32 33 34 35 36 static int __init net_dev_init(void) { ... ... for_each_possible_cpu(i) { struct softnet_data *sd = &per_cpu(softnet_data, i); memset(sd, 0, sizeof(*sd)); skb_queue_head_init(&sd->input_pkt_queue); skb_queue_head_init(&sd->process_queue); sd->completion_queue = NULL; INIT_LIST_HEAD(&sd->poll_list); sd->output_queue = NULL; sd->output_queue_tailp = &sd->output_queue; #ifdef CONFIG_RPS sd->csd.func = rps_trigger_softirq; sd->csd.info = sd; sd->csd.flags = 0; sd->cpu = i; #endif sd->backlog.poll = process_backlog; sd->backlog.weight = weight_p; sd->backlog.gro_list = NULL; sd->backlog.gro_count = 0; } ... open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); out: return rc; } subsys_initcall(net_dev_init); 网络子系统初始化的核心是为每个 CPU 都申请了一个 softnet_data数据结构,并且为网络中断注册了对应的处理函数,其中 NET_TX_SOFTIRQ 的处理函数是 net_tx_action, NET_RX_SOFTIRQ的处理函数是net_rx_action。中断和其对应的处理函数之间是如何对应的可以跟踪阅读 open_softirq。 ...

发表于:2024-08-24 · 更新于: 2024-08-24

Kudu 概览

为什么需要 Kudu 在 Hadoop 生态中结构化的数据通常使用两种方式进行存储:一是通过 Apache Parquet 等二进制数据格式,将静态数据集存储在 HDFS 上,二是将可变数据以半结构化的方式存储在 HBase 中。这里面临的问题是存储在 HDFS 上的数据无法提供单个记录的随机访问,HBase 中存储的数据尽管允许低延迟的读取和写入,但是基于 SQL 分析的应用中顺序读取的吞吐方面要远远落后于静态文件格式。 当需要在一个系统中需要这两种访问模式的时候,HDFS 上的静态数据集提供的分析性能力与 HBase 的低延迟行级随机访问能力之间的差距就需要复杂的架构设计,一种可以想到的方式就是采用类似于 Lambda 架构的方案:架构中通过 HBase 构建一个实时层,通过 HDFS 构建一个批处理层。数据流式的进入实时层,然后定期将数据导入到 Parquet 批处理层,以提供历史分析。这一架构看似能够解决这个差异性的问题,但是却有这样几个缺点: 由于要管理两个层的数据流之间的同步,这样应用层的架构会变得复杂; 需要跨两个系统管理一致性备份、安全和监控; 新数据达到数据在实时层和可用于分析的批处理层之间会表现出滞后; 现实世界中,系统需要容纳后到达的数据、还需要对迁移到不可变存储中数据进行删除,这一过程会设计到昂贵的分区重写以及手动干预。 为了解决上面的问题,Kudu 被设计以一种折中的方式解决这些问题。Kudu 是一种全新设计和实现的新存储系统,旨在填补 HDFS 等高吞吐量顺序访问存储系统与 HBase 或 Cassandra 等低延迟随机访问系统之间的空白。特别是,Kudu 为行级插入、更新和删除提供了一个简单的 API,同时以类似于 Parquet的吞吐量提供表扫描。 用户视角下的 Kudu 表与模式 在用户视角下,Kudu 是一个结构化数据表的存储系统,可以有任意数量的表,每个表都有一个由有限数量的列组成的明确定义的模式。每列具有列名和类型。对于列设置类型有这样两个好处: 特定的类型在进行列式存储的时候可以进行充分的压缩; 显式类型允许我们将类似 SQL 的元数据公开给其他系统。 Kudu 表有主键,主键负责唯一性约束,并充当更新和删除行的唯一索引。同样与关系型数据库类似的是,用户在创建表的时候需要定义表的架构,插入不存在的列或者违反主键唯一性约束都会引起错误,用户可以添加删除列但是不能删除主键列。 与关系型数据库不同的是 Kudu 表不提供二级索引。 写操作 创建表之后的写操作必须指定主键,Kudu 提供了多种语言的客户端。这些 API 允许精确控制批处理和异步错误处理,以在执行批量数据操作(例如数据加载或大型更新)时分摊往返成本。Kudu 不提供跨行事务。 读操作 Kudu 提供 Scan 操作从表中检索数据,扫描时,用户可以添加谓词进行过滤,支持列和常量值之间的比较以及主键范围,谓词操作会下推到 Kudu 后台,减少数据扫描传递时的磁盘和网络开销。 ...

发表于:2024-07-31 · 更新于: 2024-07-31

Python 中的元类

元类(metaclass)是 Python 中一个高级概念,它控制了类的创建过程。在 Python 的开源框架中,可以看到很多元类的相关身影,学习并理解元类也可以帮助理解 Python 中的类的创建过程。 一切皆对象 在 Python 中一切皆对象,类也不例外,类也是一个对象,可以像普通对象一样进行赋值、拷贝以及作为函数的参数使用。我们知道每个对象都有一个类型,在 Python 中可以通过 type() 函数进行查看,一个 Python 实例它的类型是 class 类名,那类这个对象的类型又是什么呢,可以通过下面的一组例子进行查看。 1 2 3 4 5 6 7 8 9 10 11 12 class Foo: def __init__(self): pass foo = Foo() # 也可以通过 obj.__class__ 查看类型 print(type(foo)) # <class '__main__.Foo'> print(type(Foo)) # <class 'type'> print(type(1)) # <class 'int'> print(type(int)) # <class 'type'> print(type(type)) # <class 'type'> 我们可以看到实例 foo 的类型是class Foo, 而 Foo (类本身) 的类型是 type ,更近一步地,查看内置类型以及 type 本身的类型。我们可以发现,类的类型是 type,这实际上就是元类。正如普通对象是类的实例一样,Python 中的任何类都是type元类的实例,我们可以用下面的图来表示这种关系。 ...

发表于:2024-05-15 · 更新于: 2024-05-15

Kubernetes 整体架构与核心组件

Kubernetes 整体架构 从整体来看 Kubernetes (k8s) 主要由两种节点组成,分别是 Master 节点和 Node 节点,其整体结构如图1 所示。Master 节点由三个紧密协作的独立组件组合而成,分别是:kube-apiserver、kube-scheduler 以及 kube-controller-manager;Node 节点上最核心的是名为 kubelet 的组件以及 kube-proxy。下面结合图1 对一些组件的功能进行说明。 <!DOCTYPE html> 图1. kubernetes (k8s) 整体架构图(来源于网络) 核心组件 kubectl & cliet-go kubectl 是 k8s 官方提供的命令行工具,它通过 HTTP/JSON 通信协议以命令行的方式来与 kube-apiserver 交互。 ...

发表于:2023-08-27 · 更新于: 2023-08-27

rsync 用法

在不同的机器和服务器之间进行数据同步是一个常见的场景,也有不同的命令工具(如 scp、FTP 等),甚至是不同的图形界面软件(如 Termius 等)可供我们选择操作。本文记录的 rsync (remote sync)也是用于文件同步的工具之一,其最大的特点是会检查发送方和接收已有的文件,并仅传输有变动的部分。rsync 不仅可以在本地不同的目录之间使用,同时也支持本地主机与远程服务器之间进行数据同步。由于其具有仅传输变动、增量备份、 断点续传等特点,在一些场景下非常实用。本文记录了其一些常见的用法,主要关注其在本地主机与远程主机之间同步的操作。 在使用 rysnc 之前需要确保本地和远程主机都安装了 rsync,可通过 rsync --version 检查是否安装,如果没有,可使用下面的命令进行安装: 1 2 sudo apt-get install rsync # ubuntu brew install rsync # macOS rsync 的基本使用如下: 1 2 3 4 rsync -av [source] [destination] -a: 表示递归同步,并同步元信息 -v: 输出到终端 如果目标位置不存在,执行上诉命令之后将会自动创建。 常用参数 -n 如果不确定执行 rsync 会有什么结果,可以使用该参数查看执行后的结果,并不真正执行命令。 --delete 默认情况下,rsync 只是将源目录的所有内容都复制到目标目录,并不会保证两个目录相同,也不会删除文件。因此,如果是想要构建源目录的镜像就需要使用 --delete 参数,这样保证目标目录和源目录相同。 --exclude --exclude参数用于排除文件或者目录,利用该参数指定排除模式。排除多个模式的文件时,可以使用 Bash 的扩展功能。如果需要排除的文件数量比较多,那么可以将其需要排除的文件所对应的模式写入文件,每个模式负责一行,然后利用 --exclude-from 参数指定文件。 与 --exclude 参数所类似的有 --include 参数,用于指定必须同步的文件,二者一般联合使用。 增量备份 rsync 的最大特点就是它可以完成增量备份。除了源目录与目标目录直接比较,rsync 还支持使用基准目录,即将源目录与基准目录之间变动的部分,同步到目标目录。 ...

发表于:2023-04-20 · 更新于: 2023-04-20

Linux 60秒性能分析

原文:Linux Performance Analysis in 60,000 Milliseconds 当你登陆到一台性能出现问题的 Linux 服务器:你在第一分钟会检查什么? 在 Netflix,我们拥有一个庞大的 EC2 Linux 云和大量的性能分析工具,用于监控和调查其性能。其中包括 Atlas 用于云范围的监控和 Vector 用于按需实例分析。虽然这些工具帮助我们解决了大多数问题,但有时我们需要登录到一台实例并运行一些标准的 Linux 性能工具。 60秒摘要 这篇文章将介绍 Netflix 性能工程团队在命令行下使用标准 Linux 工具进行优化性能调查的前60秒,你也可以使用这些工具。在60秒内,你可以通过运行以下十个命令了解系统资源使用情况和正在运行的进程。查找错误和饱和度指标,然后查看资源利用率。饱和是指资源的负载超过其处理能力,可表现为请求队列的长度或等待时间。 1 2 3 4 5 6 7 8 9 10 uptime dmesg | tail vmstat 1 mpstat -P ALL 1 pidstat 1 iostat -xz 1 free -m sar -n DEV 1 sar -n TCP,ETCP 1 top 其中一些命令需要安装 sysstat 软件包。这些命令输出的指标将有助于完成一些性能瓶颈的定位(USE 方法)。这涉及检查所有资源(CPU,内存,磁盘等)的利用率、饱和度和错误指标。还要注意何时检查和排除了资源,因为通过排除,可以缩小要研究的目标,并指导任何后续的分析。 ...

发表于:2023-03-19 · 更新于: 2023-03-19

Git 常用命令总结

配置 1 2 3 4 5 6 git config --list # 查看配置 git config -e [--global] # 编辑配置文件 # 设置提交代码时的用户信息 git config [--global] user.name "[name]" git config [--global] user.email "[email address]" 版本管理 版本回退 git reset 版本回退是指将版本库恢复到上个或者某个 git commit 之后。Git 中每次进行 commit 操作会在版本库中保存一个快照,如果把文件改乱了,或者误删了文件,可以从某个 commit 恢复,用到的操作命令如下: 1 2 3 4 5 6 7 8 git reset [--soft | --mixed | --hard] <commit-id | HEAD> 说明: --mixed: 默认参数,不删除工作空间改动代码,撤销 commit,并且撤销 git add . --soft: 不删除工作空间改动代码,撤销 commit,不撤销 git add . --hard: 删除工作空间改动代码,撤销 commit,撤销 git add . (即完全恢复到某次 git commit 之后) 如果使用 --mixed 或者 --soft 参数回到多个版本之前时,需要注意后面多次修改的内容都会留在工作区或者暂存区 其中 commit-id 可用命令 git log --pretty=oneline 查看。回退的版本较近时可以用 HEAD 表示,在 Git 中,用 HEAD 表示当前版本,上一个版本就是 HEAD^,上上一个版本就是 HEAD^^,依次类推。 ...

发表于:2023-01-14 · 更新于: 2024-01-28

Channel原理与实现

数据结构 channel内部的数据结构表示如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 type hchan struct { qcount uint // channel中元素的个数 dataqsiz uint // channel中循环队列的长度 buf unsafe.Pointer // channel缓冲数据指针 elemsize uint16 // 收发的元素大小 closed uint32 elemtype *_type // 收发的元素类型 sendx uint // channel的发送操作处理到缓冲位置 recvx uint // channel的接收操作处理到缓冲位置 recvq waitq // 当前因为缓冲区不足而阻塞的Gorontine列表 sendq waitq lock mutex } 阻塞的 GoRoutine 等待队列使用双向链表表示,链表中所有的元素是 runtime.sudog 结构: 1 2 3 4 type waitq struct { first *sudog last *sudog } 该结构中存储了两个分别指向前后 runtime.sudog 的指针构成的链表。 创建channel Go 语言中所有的 channel 都使用 make 关键字进行创建,并转换为 runtime.makechan 或者 runtime.makechan64 的调用,后者用于处理缓冲区大于 $2^{32}$ 的情况。 ...

发表于:2022-06-18 · 更新于: 2022-06-18

职业的乐趣与苦恼

摘自 FrederickP.Brooks.Jr 的《人月神话》第一章 焦油坑 职业的乐趣 编程为什么有趣?作为回报,它的从业者期望得到什么样的快乐? 首先是一种创建事物的纯粹快乐。如同小孩在玩泥巴时感到愉快一样,成年人喜欢创建事物,特别是自己进行设计。我想这种快乐是上帝创造世界的折射,一种呈现在每片独特、崭新的树叶和雪花上的喜悦。 其次,快乐来自于开发对其他人有用的东西。内心深处,我们期望其他人使用我们的劳动成果,并能对他们有所帮助。从这个方面,这同小孩用粘土为"爸爸办公室"捏制铅笔盒没有本质的区别。 第三是整个过程体现出魔术般的力量。将相互啮合的零部件组装在一起,看到它们精妙地运行,得到预先所希望的结果。比起弹珠游戏或点唱机所具有的迷人魅力,程序化的计算机毫不逊色。 第四是学习的乐趣,来自于这项工作的非重复特性。人们所面临的问题,在某个或其它方面总有些不同。因而解决问题的人可以从中学习新的事物:有时是实践上的,有时是理论上的,或者兼而有之。 最后,乐趣还来自于工作在如此易于驾驭的介质上。程序员,就像诗人一样,几乎仅仅工作在单纯的思考中。程序员凭空地运用自己的想象,来建造自己的"城堡"。很少有这样的介质–创造的方式如此得灵活,如此得易于精炼和重建,如此得容易实现概念上的设想。(不过我们将会看到,容易驾驭的特性也有它自己的问题)然而程序毕竟同诗歌不同,它是实实在在的东西;可以移动和运行,能独立产生可见的输出;能打印结果,绘制图形,发出声音,移动支架。神话和传说中的魔术在我们的时代已变成了现实。在键盘上键入正确的咒语,屏幕会活动、变幻,显示出前所未有的或是已经存在的事物。 编程非常有趣,在于它不仅满足了我们内心深处进行创造的渴望,而且还愉悦了每个人内在的情感。 职业的苦恼 然而这个过程并不全都是喜悦。我们只有事先了解一些编程固有的烦恼,这样,当它们真的出现时,才能更加坦然地面对。 首先,必须追求完美。因为计算机也是以这样的方式来变戏法:如果咒语中的一个字符、一个停顿,没有与正确的形式一致,魔术就不会出现。(现实中,很少的人类活动要求完美,所以人类对它本来就不习惯。)实际上,我认为学习编程的最困难部分,是将做事的方式往追求完美的方向调整。 其次,是由他人来设定目标,供给资源,提供信息。编程人员很少能控制工作环境和工作目标。用管理的术语来说,个人的权威和他所承担的责任是不相配的。不过,似乎在所有的领域中,对要完成的工作,很少能提供与责任相一致的正式权威。而现实情况中,实际(相对于正式)的权威来自于每次任务的完成。 对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而往往这些程序设计得并不合理,实现拙劣,发布不完整(没有源代码或测试用例),或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可靠完整的。 下一个烦恼–概念性设计是有趣的,但寻找琐碎的bug却只是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的劳动。程序编制工作也不例外。 另外,人们发现调试和查错往往是线性收敛的,或者更糟糕的是,具有二次方的复杂度 。结果,测试一拖再拖,寻找最后一个错误比第一个错误将花费更多的时间。 最后一个苦恼,有时也是一种无奈——当投入了大量辛苦的劳动,产品在即将完成或者终于完成的时候,却已显得陈旧过时。可能是同事和竞争对手已在追逐新的、更好的构思;也许替代方案不仅仅是在构思,而且已经在安排了。 现实情况比上面所说的通常要好一些。当产品开发完成时,更优秀的新产品通常还不能投入使用。事实上,只有实际需要时,才会用到最新的设想,因为所实现的系统已经能满足要求,体现了回报。 诚然,产品开发所基于的技术在不断地进步。一旦设计被冻结,在概念上就已经开始陈旧了。不过,实际产品需要一步一步按阶段实现。实现落后与否的判断应根据其它已有的系统,而不是未实现的概念。因此,我们所面临的挑战和任务是在现有的时间和有效的资源范围内,寻找解决实际问题的切实可行方案。 这,就是编程。一个许多人痛苦挣扎的焦油坑以及一种乐趣和苦恼共存的创造性活动。

发表于:2022-02-03 · 更新于: 2022-02-03