Redian新闻
>
如何学习 Linux 内核网络协议栈

如何学习 Linux 内核网络协议栈

公众号新闻
↓推荐关注↓

来源:https://segmentfault.com/a/1190000021227338



协议栈的细节


 

下面将介绍一些内核网络协议栈中常常涉及到的概念。


sk_buff

内核显然需要一个数据结构来表示报文,这个结构就是 sk_buff ( socket buffer 的简称),它等同于在<TCP/IP详解 卷2>中描述的 BSD 内核中的 mbuf。


sk_buff 结构自身并不存储报文内容,它通过多个指针指向真正的报文内存空间:


sk_buff 是一个贯穿整个协议栈层次的结构,在各层间传递时,内核只需要调整 sk_buff 中的指针位置就行。


net_device

内核使用 net_device 表示网卡。网卡可以分为物理网卡和虚拟网卡。物理网卡是指真正能把报文发出本机的网卡,包括真实物理机的网卡以及VM虚拟机的网卡,而像 tun/tap,vxlan、veth pair 这样的则属于虚拟网卡的范畴。


如下图所示,每个网卡都有两端,一端是协议栈(IP、TCP、UDP),另一端则有所区别,对物理网卡来说,这一端是网卡生产厂商提供的设备驱动程序,而对虚拟网卡来说差别就大了,正是由于虚拟网卡的存在,内核才能支持各种隧道封装、容器通信等功能。


socket & sock

用户空间通过 socket()、bind()、listen()、accept() 等库函数进行网络编程。而这里提到的 socket 和 sock 是内核中的两个数据结构,其中 socket 向上面向用户,而 sock 向下面向协议栈。


如下图所示,这两个结构实际上是一一对应的。


注意到,这两个结构上都有一个叫 ops 的指针, 但它们的类型不同。socket 的 ops 是一个指向 struct proto_ops 的指针,sock 的 ops 是一个指向 struct proto 的指针, 它们在结构被创建时确定。


回忆网络编程中 socket() 函数的原型:

#include <sys/socket.h>

sockfd = socket(int socket_family, int socket_type, int protocol);

实际上, socket->ops 和 sock->ops 由前两个参数 socket_family 和 socket_type 共同确定。


如果 socket_family 是最常用的 PF_INET 协议簇, 则 socket->ops 和 sock->ops 的取值就记录在 INET 协议开关表中:

static struct inet_protosw inetsw_array[] =
{
    {
        .type =     SOCK_STREAM,
        .protocol = IPPROTO_TCP,
        .prot =     &tcp_prot,                 // 对应 sock->ops
        .ops =      &inet_stream_ops,          // 对应 socket->ops
        .flags =    INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
    },

    {
        .type =     SOCK_DGRAM,
        .protocol = IPPROTO_UDP,
        .prot =     &udp_prot,                 // 对应 sock->ops
        .ops =      &inet_dgram_ops,           // 对应 socket->ops
        .flags =    INET_PROTOSW_PERMANENT,
    },
}
.......


L3->L4

我们知道网络协议栈是分层的,但实际上,具体到实现,内核协议栈的分层只是逻辑上的,本质还是函数调用。发送流程(上层调用下层)通常是直接调用(因为没有不确定性,比如TCP知道下面一定IP),但接收过程不一样了,比如报文在 IP 层时,它上面可能是 TCP,也可能是 UDP,或者是 ICMP 等等,所以接收过程使用的是注册-回调机制。


还是以 INET 协议簇为例,注册接口是:

int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol);


在内核网络子系统初始化时,L4 层协议(如下面的 TCP 和 UDP)会被注册:

static struct net_protocol tcp_protocol = {
    ......
    .handler = tcp_v4_rcv,
    ......
};

static struct net_protocol udp_protocol = {
    .....
    .handler = udp_rcv,
    .....
};
.......
而在IP层,查询过路由后,如果该报文是需要上送本机的,则会根据报文的 L4 协议,送给不同的 L4 处理:
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    ......
    ipprot = rcu_dereference(inet_protos[protocol]);
    ......
    ret = ipprot->handler(skb);     
    ......
}
.......


L2->L3

L2->L3 如出一辙。只不过注册接口变成了:

void dev_add_pack(struct packet_type *pt)


谁会注册呢?显然至少 IP 会:

static struct packet_type ip_packet_type = {
    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,
}
.......


而在报文接收过程中,设备驱动程序会将报文的 L3 类型设置到 skb->protocol,然后在内核 netif_receive_skb 收包时,会根据这个 protocol 调用不同的回调函数:
__netif_receive_skb(struct sk_buff *skb)
{
    ......
    type = skb->protocol;
    ......
    ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
.......


Netfilter

Netfilter 是报文在内核协议栈必然会通过的路径,我们从下面这张图就可以看到,Netfilter 在内核的 5 个地方设置了 HOOK 点,用户可以通过配置 iptables 规则,在 HOOK 点对报文进行过滤、修改等操作。


在内核代码中,我们时常可见 NF_HOOK 这样的调用。我的建议是,如果你暂时不考虑 Netfilter,那么就直接跳过, 跟踪 okfn 就行。
static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, 
    struct sk_buff *skb, struct net_device *in, struct net_device *out,
    int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
    int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);
    if (ret == 1)
        ret = okfn(net, sk, skb);
    return ret;
}
.......


dst_entry

内核需要确定收到的报文是应该本地上送(local deliver)还是转发(forward),对本机发送(local out)的报文需要确定是从哪个网卡发送出去,这都是内核通过查询 fib (forward information base, 转发信息表) 确定。fib 可以理解为一个数据库,数据来源是用户配置或者内核自动生成的路由。


fib 查询的输入是报文 sk_buff,输出是 dst_entry. dst_entry 会被设置到 skb 上:

static inline void skb_dst_set(struct sk_buff *skb, struct dst_entry *dst)
{
    skb->_skb_refdst = (unsigned long)dst;
}


而 dst_entry 中最重要的是一个 input 指针和 output 指针:

struct dst_entry 
{
    ......
    int (*input)(struct sk_buff *);
    int (*output)(struct net *net, struct sock *sk, struct sk_buff *skb);
    ......
}


对于需要本机上送的报文:

rth->dst.input = ip_local_deliver;


对需要转发的报文:

rth->dst.input = ip_forward;


对本机发送的报文:

rth->dst.output = ip_output;

版权归原作者所有,如有侵权,请联系删除。


- EOF -


加主页君微信,不仅Linux技能+1

主页君日常还会在个人微信分享Linux相关工具资源精选技术文章,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

加个微信,打开一扇窗


推荐阅读  点击标题可跳转

1、Linux 内核源码中最常见的数据结构之【mutex】

2、Linus:是时候从内核移除对 i486 CPU 的支持了

3、小米工程师提交优化补丁被批,Linux 内核维护者:太疯狂!


看完本文有收获?请分享给更多人

推荐关注「Linux 爱好者」,提升Linux技能

点赞和在看就是最大的支持❤️

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Arch Linux 2023.01.01 版本 ISO 镜像发布:采用 Linux 内核 6.1如何在最小安装的 CentOS、RHEL、Rocky Linux 中设置互联网 | Linux 中国硬核观察 #848 Linux 6.1 发布,拉开 Rust 进入 Linux 内核的大幕网络协议:TCP和UDP什么区别?(附视频)如何在 Arch Linux 中安装 Cinnamon 桌面 | Linux 中国大规模GNN如何学习?北邮最新《分布式图神经网络训练》综述,35页pdf阐述分布式GNN训练算法和系统如何在 Ubuntu Linux 上更新谷歌 Chrome | Linux 中国如何在 Arch Linux 中安装 elementary OS 的 Pantheon 桌面 | Linux 中国我如何使用现场 USB 设备恢复我的 Linux 系统 | Linux 中国Rosalía 登意大利版《VOGUE》封面!如何提高 Ubuntu 和其他 Linux 系统中的扬声器音量 | Linux 中国张沛超:如何学习精神分析?带你开启140+分钟学习之旅关于 Linux 和 Git 的创造者 Linus Torvalds 的 20 件趣事 | Linux 中国华为开发者贡献 Linux 内核补丁,将核心内核函数速度提升 715 倍如何在 Linux 中找到一个进程 ID 并杀死它 | Linux 中国如何在 Ubuntu 等 Linux 中安装 Python 3.11 | Linux 中国Linux 内核 6.1 发布,包含初始 Rust 支持 | Linux 中国【庭院种菜】自家菜地,别用这种有机肥!如何在 Arch Linux 中启用 Snap 支持 | Linux 中国如何在 Linux 中使用媒体传输协议访问安卓设备的内部存储和 SD 卡 | Linux 中国毛泽东是第一个与官国决裂的人在 Mac 上运行 Linux 更进一步,Apple SoC CPUFreq 驱动即将并入 Linux 主线内核如何在 Arch Linux 中安装 OpenOffice(新手指南) | Linux 中国如何在 Silverblue 上变基到 Fedora Linux 37 | Linux 中国硬核网站,适合老师和学生!如何在 Linux 中确定运行的是那种初始化系统 | Linux 中国伊丽莎白一世的性别之谜2-四郎和嬛嬛(多图)在 Linux 中如何从命令行查找默认网关的 IP 地址 | Linux 中国操作系统人机对话!Linux OS大谈Windows与macOS:值得学习,但被Linux碾压试试这个 Linux 网络浏览器作为你的文件管理器 | Linux 中国如何通过 chroot 恢复 Arch Linux 安装 | Linux 中国全职妈妈招惹谁了?如何在 Ubuntu 和其他 Linux 中检查 CPU 和硬盘温度 | Linux 中国李克强内部讲话(有待核实)如何在 Ubuntu 和其他相关 Linux 中安装 Python 3.10 | Linux 中国
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。