首页 > 编程知识 正文

协议栈工作流程,通信协议栈基础

时间:2023-05-06 02:24:47 阅读:224099 作者:3822

写在前面

这是我第一次公开发表自己的笔记,内容是我学习过程中摘录或者总结的学习资料,如果有引用原作者的话、图片,希望作者能及时提醒我删除。有错误希望有大佬指出,毕竟我还是个初学者。
文章会随着我的学习深入而不断修改。

一、协议栈架构介绍

Linux TCP/IP协议栈按照tcp/ip分层结构可以分为四层,应用层、传输层、网络层和链路层(上图的网络访问层)。简要的说,网络数据在应用层,使用套接字,加上三元数据(IP、PORT、协议)建立起客户端或者服务器,并在此基础上组织协议(HTTP、SMTP等)收发数据,然后用户数据被Socket送到内核空间,交给内核协议栈处理,最终通过互联网发到指定设备。内核协议栈处理的工作主要是一、注册每一层需要处理的协议(例如ip层注册tcp和udp协议)、协议对应的操作集合(接收、发送、检查等操作) 二、根据注册的协议内容操作用户数据(添加、卸载头部数据等)三、将数据包抛掷到合适的协议层。
可以看到,整个协议栈的框架如上图所示。先看协议栈发流程,设备通过驱动将数据转换成skb数据,并通过netif_receive_skb获取链路层需要的数据,比如说源Mac地址、目的Mac地址以及协议类型(type)。在数据不需要经过vlan或者bridge时,选择接收函数处理(注册在ptype_all,ptype_base链表中),就以ip协议为例,ipv4的协议号是0x8000,在遍历链路层注册好的协议后,选择了ip_rcv()函数处理数据。Ip_rcv函数后的下一级函数的头部信息(Tcp头部信息、Udp头部信息等等),函数接收链路层数据,期间处理路由信息,判断是否需要转发或者丢弃的情况,最后根据头部信息中的type值(tcp是0x06 udp是0x17)遍历链表中符合的结构体,调用handle函数接收数据(tcp是tcp_v4_recv,udp是udp_rcv)。
以tcp为例,ip层调用tcp_v4_recv函数处理接收数据(获取原始数据和地址信息),数据最终存入与socket同时生成的接收队列,然后通知socket有数据。最后socket调用recv或者recvfrom函数间接调用xxx_recvmsg函数从内核拿数据到用户空间,给socket使用。
协议栈发流程与之相反。Socket调用send或者sendto函数,间接使用xx_sendmsg函数(Raw类型是直接发送IP层的)将发送数据复制到内核空间中的skb发送队列,并添加协议头,最终通过ip_queue_xmit(TCP使用)等函数打包成ip层数据包,交由ip层处理,经过netfilter子系统后,使用dev_queue_xmit函数发送至链路层,交由设备驱动发送。

可以看到每经过一层协议都在前面数据的基础上加上协议的头部。解包的过程正好相反,每过一层便卸载一层头部。
应用层的各种网络数据都是通过Linux Socket来与内核空间的协议栈通信的(socket结构体包含了文件描述符、inet层操作等结构体)。从层次上讲,它属于应用层,系统给程序员提供用户API方便应用程序向传输层发送数据。
Socket屏蔽不同网络协议之间的差异,提供统一的操作函数。对于用户来讲,Socket网络通信可以向操作文件一样方便。
网络应用可以通过改变参数来实现TCP、UDP连接,甚至可以直接构造原始的IP数据报、链路数据包。如下图所示的简易流程,展示了Socket文件的创建、使用流程。

用户经过上述部分后来到了内核协议栈的主要处理部分,每一层的处理逻辑如下图所示。

二、协议栈初始化

在协议栈可以使用前,内核需要对网络环境进行初始化。协议栈注册初始化涉及到两个函数 sock_init和 inet_init。(net/ipv4/af_net.c)

sock_init的目的是分配socket文件系统空间和初始化收发的skb结构体空间。下面的注册流程图,展示了tcp/ip协议栈 文件系统和协议功能的初始化。
在注册协议时,inet_init 使用(proto_register)先注册了传输层协议操作集,然后(sock_register)注册了Inet层操作集合,接着(inet_add_protocol)注册了一些协议(tcp,udp,icmp,igmp)的接收函数。注册完这些,就初始化了指针数组(inetsw),有了存放空间后,使用inet_register_protosw函数,以type为索引把inetsw_array结构中的节点添加到inetsw表中,方便以后使用。完成这些后逐个初始化协议需要的变量,最终使用dev_add_pack() 处理链路层数据)将钩子函数挂到 ptype_base链表上。至此,整个协议栈环境可用。

/*net_protocol 结构定义了传输层协议(包含icmp igmp协议)以及传输层的报文接收例程,此结构是网络层和传输层之间的桥梁,定义了协议族中支持的传输层协议以及传输层的报文接收实例。此结构是网络层和 传输层之间的桥梁,当网络数据包从网络层流向传输层时,会调用此结构中的传输层协议数据时,会调用此结构中的传输层协议数据报接收处理函数(handler)。*/struct net_protocol { void (*early_demux)(struct sk_buff *skb); int (*handler)(struct sk_buff *skb); void(*err_handler)(struct sk_buff *skb, u32 info);Unsigned int no_policy:1,icmp_strict_tag_validation:1;};

linux目前支持多种协议族,每个协议族用一个net_porto_family结构实例来表示,在初始化时,会调用sock_register函数初始化注册到net_families[]中去。主要是协议族的创建方法inet_create。

三、协议栈处理流程 1、链路层数据处理

首先协议栈要从设备那里拿到数据,这里调用了netif_receive_skb函数(net/core/dev.c),检查完时间戳后调用_netif_receive_skb函数开始处理skb协议包数据。然后判断数据是否要走桥、vlan等。得到协议类型后最终如下图所示遍历所有ptype(协议的type),然后上传到上一层协议。
对于ipv6协议来说,其有独有的type值,ipv6的钩子函数是ip6_rcv()(在ipv6文件夹里),与ipv4的过程一致,不在多赘述。
IP报文结构体示例。 Type是协议类型,func是注册的钩子函数。
下图是链路层发数据的流程图,得到skb数据后,内核分析得到协议的type是何种类型,就遍历了ptype_base数组得到注册好的协议,调用func钩子函数处理数据。也就是ip_rcv();
netif_receive_skb()的主要作用体现在两个遍历链表的操作中,其中之一为遍历 ptype_all 链( ETH_P_ALL 被单独的放到了 ptype_all 这个表中,用于 sniffer 中),这些为注册到内核的一些 sniffer,将上传给这些 sniffer,另一个就是遍历 ptype_base,这个就是具体的协议类型(宏定义在 include/linux/if_ether.h)。当以太网接收到一个类型为 ETH_P_IP 的类型,它由 ip_rcv处理。如果这个链中还注册有其它 IP 层的协议,它也会同时发送一个副本给它。
做了初步处理后就调用deliver_skb执行相应 packet_type 里的 func 函数,如对于ETH_P_IP 类型,由上面可以看到,它执行的就是 ip_rcv 了。
数据包的发送为接收的反过程,发送过程较之接收过程的复杂性在于它有一个流量控制层(Trafficing Control Layer),用于实现QoS.当内核有数据包等待发送时,它会间接调用__netif_schedule ()去处理这些数据包。

dev_hard_start_xmit(skb, dev)只是一个包装函数,它首先看有没有注册的 sniffer,要是存在的话(netdev_nit 不等于0),便将一个副本通过 dev_queue_xmit_nit(skb, dev)发送给它,再之后,就是调用驱动程序的 hard_start_xmit 完成最后的发送工作了。hard_start_xmit()只要是跟硬件打交道,一般是通知DMA完成数据的发送工作。如果有发送队列的话,就要考Qos控制发送,qdisc_run(dev)会选择“合适”的 skb 然后传递给 dev_hard_start_xmit(skb, dev)。

2.网络层数据处理

数据包到了网络层,需要根据type找到对应的处理函数,以Ip协议为例,skb数据传到了ip_rcv函数处。(net/ipv4/ip_input.c)。网络层的任务就是选择合适的网间路由和交换结点,确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括 (1)路由处理,即选择下一跳 (2)添加 IP header(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错 (4)可能的话,进行 IP 分片(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。对于上层需要发送的数据,仍然需要经过netfilter系统处理路由信息,添加ip头。还有处理其他钩子函数,直接将ip信息发给钩子函数的创建者。下面是ip层数据流路径。
链路层将数据包上传到 IP 层时,由 IP 层相关协议的处理例程处理。对于IP 协议,这个注册的处理例程是 ip_rcv(),它处理完成后交给 NETFILTER(PRE-ROUTING)过滤,再上递给 ip_rcv_finish(), 这个函数根据 skb 包中的路由信息,决定这个数据包是转发还是上交给本机,由此产生两条路径,一为 ip_local_deliver(),它首先检查这个包是否是一个分 片 包 , 如 果 是 , 它 要 调 动 ip_defrag() 将 分 片 重 装 , 然 后 再 次 将 包 将 给 NETFILTER(LOCAL_IN)过滤后,再由 ip_local_deliver_finish()将数据上传到传输层层,这样就完成了 IP 层的处理;它负责将数据上传,另一路径为 ip_forward(),它负责将数据转发,经由 NETFILTER(FORWARD)过滤后将给 ip_forward_finish(),然后调用 dst_output()将数据包发送出去。
当上一层有数据需要发送时,它将调用ip_append_data、ip_push_pending_frams(udp,icmp, Raw IP), 或 ip_append_page(UDP),ip_queue_xmit (TCP,SCTP), 或者 raw_send_hdrinc(Raw IP, IGMP),它们将这些包交由NETFILTER(LOLACL_OUT)处理后,然后交给 dst_output,这会根据是多播或单播选择合适的发送函数。如果是单播,它会调用 ip_output(),然后是 ip_finish_output(),这个函数主要是检查待发送的数据包大小是否超过 MTU,如果是,则要首先调用 ip_fragment()将其分片,然后再传给 ip_finish_output2(),由它交给链路层处理了

3、传输层数据处理

数据包在找到注册的协议后,使用handler函数(上文提到)交给上层协议处理(Tcp、UDP、etc)。下图是收包处理。传输层为用户提供数据传输服务,在socket创建后,首先出发的便是握手程序,等待双方连接建立后(存在结构体中),即可以互相通信。作为数据载体的skb buf会加入存储队列,以供socket查阅

![在这里插入图片描述](https://img-blog.csdnimg.cn/2020Net/ipv4/udp.c
发包处理流程:
到此位置数据包的收工作就到了一定阶段,skb数据被存放在收队列中,并触发中断通知socket有数据需要接收。流程如下图所示。1103193452879.png#pic_center)

4、应用层数据处理

可以看到应用层的数据处理符合服务器/客户端模型,即更上层的协议围绕socket来建立自己的服务,上图展示了socket收发数据的示意图。

四、Socket的创建与使用
Socket 创建和使用
(函数位置 在linux-3.4rt/net目录下的socket.c中)

可以看到创建一个socket文件的用户函数包含四个方面:文件句柄、协议族、协议类型和协议号。可以看到在内核中首先调用了sock_create()和sock_map_fd()。功能分别是根据协议创建sock结构体和分配文件描述符。然后使用create函数创建协议族。
网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个队列中保存要发送或者接收的skb实例(贯穿整个协议栈)。
可以看到最终生成如上图的结构类型,每一次新建socket都会生成上图结构,只需要维护好这个结构就可以实现正常的socket运行。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。