部门有新伙伴,领导说要我做一个关于linux内核网络协议栈的presentation,所以就有了这篇文章。为什么是文字而不是PPT?因为我真的不喜欢PPT!准备工作对于没有学习过Linux内核网络的人来说,可能是向往,也可能是害怕。但当你在深入了解和验证后得到正面的反馈时,那种豁然开朗的感觉会让你心满意足,你就可以相信可口可乐了。回想刚进入这个话题时,有以下疑问:Q1:内核网络子系统那么大,应该从哪里入手?掉进去会晕倒吗?Q2:内核网络代码更新这么快,应该从哪个版本开始学习?Q3:有什么好的资料和教程吗?Q4:如何验证我的理解是否正确?所以现在,我可以简单的主观回答一下:Q1:内核网络子系统那么大,我应该从哪里入手呢?掉进去会晕倒吗?虽然内核网络子系统的代码看起来很多,但是核心进程和侧分支还是分开的。并且,我认为它的水平超过了很多开源代码。评论的地方够多,炫技的地方就少了。每次修改都能从社区GIT仓库找到修改原因,已经很不错了。Q2:内核网络代码更新这么快,应该从哪个版本开始学习?使用您需要的任何版本。如果在作业中指定了版本,则选择该版本。如果教程书是基于某个版本的,就选择它。如果你只是自己研究,那么我建议提前几个版本的代码:比如2.6、3.7、4.4、4.9、5.3(这些版本号是我写的)。或者https://elixir.bootlin.com/可以在线浏览各种版本的代码Q3:有什么好的资料和教程吗?没有看到完整的教程,我想可能是因为内容太多太复杂了。但是网上几乎各个方面都有相关内容的讨论和分析。这里推荐以下三本书,都是比较全面的。本文的内容在很大程度上也受到了这些书籍的影响。UnderstandinglinuxnetworkinternalsTCP/IPArchitecture,Design,andImplementationinLinuxLinuxKernelNetworking:ImplementationandTheory这些书都有中文翻译,但是我觉得看英文原版可以避免翻译导致理解偏差...Q4:如何验证自己你的理解正确吗?验证很重要,不然怎么知道对不对。除了使用printk和调试信息重新编译内核的原始方法外,还有更智能的工具可以提供帮助。Systemtap:几乎无所不能,可以在内核中放置探测点,执行自己的代码。kprobe:一个简单的工具,可以快速检查一个函数是否被执行。Packetdrill:它可用于验证TCP协议的行为。协议栈的细节。下面将介绍内核网络协议栈中经常涉及到的一些概念。sk_buff内核显然需要一个数据结构来表示消息,这个结构就是sk_buff(socketbuffer的简称),相当于中描述的BSD内核中的mbuf。sk_buff结构本身不存储消息的内容,它通过多个指针指向真正的消息内存空间:sk_buff是一个贯穿整个协议栈层的结构,在层与层之间传递时,内核只需要调整sk_buff中的指针位置就可以了。net_device内核使用net_device来代表一个网卡。网卡可以分为物理网卡和虚拟网卡。物理网卡是指能够真正向本机发送消息的网卡,包括真实物理机的网卡和VM虚拟机的网卡,如tun/tap、vxlan、vethpair属于虚拟网卡的范畴。如下图所示,每个网卡都有两端,一端是协议栈(IP、TCP、UDP),另一端不一样。对于物理网卡,这一端是网卡厂商提供的设备驱动程序。对于虚拟网卡来说,差别还是挺大的。正是因为虚拟网卡的存在,内核才能支持隧道封装、容器通信等各种功能。Socket&sock用户空间通过socket()、bind()、listen()、accept()等库函数进行网络编程。这里说的socket和sock是内核中的两个数据结构,socket向上面向用户,而sock向下面向协议栈。如下图所示,这两个结构体其实是一一对应的。请注意,这两个结构体上都有一个名为ops的指针,但它们的类型不同。socket的ops是structproto_ops的指针,sock的ops是structproto的指针。它们是在创建结构时确定的。回忆一下网络编程中socket()函数的原型#includesockfd=socket(intsocket_family,intsocket_type,intprotocol);实际上,socket->ops和sock->ops是由前两个参数socket_family和socket_type共同决定的。如果socket_family是最常用的PF_INET协议簇,socket->ops和sock->ops的值记录在INET协议开关表staticstructinet_protoswinetsw_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协议簇为例,注册接口为intinet_add_protocol(conststructnet_protocol*prot,unsignedcharprotocol);当内核网络子系统初始化时,会注册L4层协议(比如下面的TCP和UDP)staticstructnet_protocoltcp_protocol={.......handler=tcp_v4_rcv,......};staticstructnet_protocoludp_protocol={......handler=udp_rcv,.....};而在IP层,查询完路由后,如果报文需要发送到本机,则根据报文的L4协议发送到不同的L4进行处理staticintip_local_deliver_finish(structnet*net,structsock*sk,structsk_buff*skb){......ipprot=rcu_dereference(inet_protos[协议]);......ret=ipprot->handler(skb);......}L2->L3L2->L3完全一样。只是注册界面变成voiddev_add_pack(structpacket_type*pt)谁来注册?显然至少IP会是staticstructpacket_typeip_packet_type={.type=cpu_to_be16(ETH_P_IP),.func=ip_rcv,}并且在消息接收过程中,设备驱动会设置消息的L3类型为skb->protocol,然后当内核netif_receive_skb收到数据包时,会根据这个协议调用不同的回调函数__netif_receive_skb(structsk_buff*skb){......type=skb->protocol;......ret=pt_prev->func(skb,skb->dev,pt_prev,orig_dev);}NetfilterNetfilter是包在内核协议栈中必须经过的路径。从下图我们可以看出Netfilter在内核的5处设置了HOOK点,用户可以通过配置iptables规则在HOOK点过滤和修改数据包。在内核代码中,我们经常会有NF_HOOK这样的调用。我的建议是,暂时不考虑Netfilter,直接跳过,关注okfn。staticinlineintNF_HOOK(uint8_tpf,unsignedinthook,structnet*net,structsock*sk,structsk_buff*skb,structnet_device*in,structnet_device*out,int(*okfn)(structnet*,structsock*,structsk_buff*)){intret=nf_hook(pf,hook,net,sk,skb,in,out,okfn);如果(ret==1)ret=okfn(net,sk,skb);returnret;}dst_entry内核需要判断接收到的报文是本地投递(localdeliver)还是转发(forward),本地发送的报文(localout)需要确定从哪个网卡发送出去。这是内核通过查询fib(前向信息库,前向信息表)OK。fib可以理解为一个数据库,数据来源是用户配置或者内核自动生成的路由。fib查询的输入是消息sk_buff,输出是dst_entry。dst_entry会被设置为skbstaticinlinevoidskb_dst_set(structsk_buff*skb,structdst_entry*dst){skb->_skb_refdst=(unsignedlong)dst;}而dst_entry最重要的是一个输入指针和输出指针structdst_entry{。.....int(*input)(structsk_buff*);int(*output)(structnet*net,structsock*sk,structsk_buff*skb);......}-对于消息需要本机发送rth->dst.input=ip_local_deliver;-对于需要转发的报文rth->dst.input=ip_forward;-对于本机发送的报文Packetrth->dst。输出=ip_output;