TCP/IP协议栈是Linux内核中负责网络通信的核心模块,它实现了从数据链路层到应用层的各种协议和功能,如IP、ARP、ICMP、TCP、UDP、Socket等。本文将从源代码的角度,分析TCP/IP协议栈的主要结构和流程,以及一些重要的函数和数据结构。本文基于Linux 5.10版本的源代码,部分代码可能与其他版本有所不同。
1、inet_init是如何被调用的?从start_kernel到inet_init调用路径
inet_init是TCP/IP协议栈的初始化函数,它在内核启动时被调用,完成各种协议和数据结构的注册和初始化。inet_init的调用路径如下:
-
start_kernel:内核的入口函数,位于init/main.c中,完成内核的基本初始化工作。
-
rest_init:在start_kernel中被调用,位于init/main.c中,创建内核初始化线程kernel_init。
-
kernel_init:内核初始化线程的主函数,位于init/main.c中,完成内核的后续初始化工作。
-
do_basic_setup:在kernel_init中被调用,位于init/main.c中,调用所有的initcall函数,这些函数用于初始化各种内核模块和子系统。
-
do_initcalls:在do_basic_setup中被调用,位于init/main.c中,遍历所有的initcall函数,并依次执行它们。
-
inet_init:在do_initcalls中被调用,位于net/ipv4/af_inet.c中,是一个__initcall函数,用于初始化TCP/IP协议栈。
2、跟踪分析TCP/IP协议栈如何将自己与上层套接口与下层数据链路层关联起来的?
TCP/IP协议栈与上层套接口和下层数据链路层的关联主要通过以下几个方面实现:
-
套接口层和传输层的关联:套接口层提供了一组通用的操作函数,如socket、bind、connect、send、recv等,这些函数通过调用传输层的相应函数,实现了套接口层和传输层的关联。例如,当用户调用socket函数创建一个TCP套接口时,套接口层会调用inet_create函数,该函数位于net/ipv4/af_inet.c中,它会根据传入的协议参数,调用相应的传输层协议的create函数,如tcp_create,该函数位于net/ipv4/tcp.c中,它会为套接口分配一个sock结构,并初始化其相关字段和操作函数。这样,套接口就与传输层的sock结构关联起来了。
-
传输层和网络层的关联:传输层和网络层的关联主要通过协议注册和分发实现。传输层协议,如TCP和UDP,会在初始化时,通过inet_add_protocol函数,将自己的协议号和处理函数注册到网络层的inet_protos数组中,该函数位于net/ipv4/protocol.c中。当网络层接收到一个IP数据包时,会根据其协议号,从inet_protos数组中查找相应的传输层协议的处理函数,并调用它。例如,当网络层调用ip_local_deliver_finish函数处理一个本地目的的IP数据包时,该函数位于net/ipv4/ip_input.c中,它会根据IP数据包的协议号,从inet_protos数组中查找相应的传输层协议的处理函数,如tcp_v4_rcv,该函数位于net/ipv4/tcp_ipv4.c中,它会处理TCP数据包,并调用相应的sock结构的操作函数。这样,网络层就与传输层的协议和sock结构关联起来了。
-
网络层和数据链路层的关联:网络层和数据链路层的关联主要通过网络设备和数据包类型实现。网络设备是一种抽象的数据结构,用于表示各种物理或虚拟的网络接口,如以太网卡、回环设备等,它们都有一个net_device结构,该结构位于include/linux/netdevice.h中,它包含了网络设备的各种属性和操作函数。数据包类型是一种用于表示不同协议的数据包的结构,如IP数据包、ARP数据包等,它们都有一个packet_type结构,该结构位于include/linux/if_packet.h中,它包含了数据包类型的协议号和处理函数。当网络层发送一个IP数据包时,会调用ip_output函数,该函数位于net/ipv4/ip_output.c中,它会根据目的IP地址,查找路由表,找到相应的网络设备和下一跳地址,然后调用网络设备的hard_header函数,该函数位于net/core/dev.c中,它会根据网络设备的类型,如eth_header,该函数位于net/ethernet/eth.c中,为IP数据包添加一个以太网帧头,然后调用网络设备的xmit函数,如dev_queue_xmit,该函数位于net/core/dev.c中,它会将IP数据包加入到网络设备的发送队列中,等待发送。当数据链路层接收到一个以太网帧时,会调用netif_receive_skb函数,该函数位于net/core/dev.c中,它会根据以太网帧头的协议号,从ptype_all链表中查找相应的数据包类型的处理函数,并调用它。例如,当数据链路层接收到一个IP数据包时,会调用ip_rcv函数,该函数位于net/ipv4/ip_input.c中,它会处理IP数据包,并调用相应的传输层协议的处理函数。这样,网络层就与数据链路层的网络设备和数据包类型关联起来了。
3、TCP的三次握手源代码跟踪分析,跟踪找出设置和发送SYN/ACK的位置,以及状态转换的位置
TCP的三次握手是TCP建立连接的过程,它涉及到客户端和服务器端的交互,以及TCP状态的变化。下面分别从客户端和服务器端的角度,跟踪分析TCP的三次握手的源代码,找出设置和发送SYN/ACK的位置,以及状态转换的位置。
客户端流程
客户端的三次握手流程如下:
-
发送SYN报文,向服务器发起TCP连接
-
收到服务器的SYN+ACK报文,发送ACK报文,完成TCP连接的建立
发送SYN报文,向服务器发起TCP连接
当客户端调用connect函数连接服务器时,套接口层会调用inet_stream_connect函数,该函数位于net/ipv4/af_inet.c中,它会调用传输层的connect函数,如tcp_v4_connect,该函数位于net/ipv4/tcp_ipv4.c中,它会完成以下几个步骤:
-
为连接分配一个本地端口号,如果没有指定,会调用inet_hash_connect函数,该函数位于net/ipv4/inet_hashtables.c中,它会从ephemeral端口范围中选择一个未使用的端口号,并将其与sock结构关联。
-
根据目的IP地址,查找路由表,找到相应的网络设备和下一跳地址,然后调用网络设备的hard_header函数,该函数位于net/core/dev.c中,它会根据网络设备的类型,如eth_header,该函数位于net/ethernet/eth.c中,为IP数据包添加一个以太网帧头,然后调用网络设备的xmit函数,如dev_queue_xmit,该函数位于net/core/dev.c中,它会将IP数据包加入到网络设备的发送队列中,等待发送。这样,就完成了SYN报文的发送,同时将TCP状态从CLOSED变为SYN_SENT。
-
设置和发送SYN报文的位置:tcp_v4_connect -> ip_output -> dev_queue_xmit
-
状态转换的位置:tcp_v4_connect -> tcp_set_state -> inet_sk_state_store
收到服务器的SYN+ACK报文,发送ACK报文,完成TCP连接的建立
当客户端收到服务器的SYN+ACK报文时,数据链路层会调用netif_receive_skb函数,该函数位于net/core/dev.c中,它会根据以太网帧头的协议号,从ptype_all链表中查找相应的数据包类型的处理函数,并调用它。例如,当数据链路层接收到一个IP数据包时,会调用ip_rcv函数,该函数位于net/ipv4/ip_input.c中,它会处理IP数据包,并调用相应的传输层协议的处理函数。对于TCP协议来说,会调用tcp_v4_rcv函数,该函数位于net/ipv4/tcp_ipv4.c中,它会完成以下几个步骤:
-
根据TCP报文头的源端口号和目的端口号,从hash表中查找相应的sock结构,如果找不到,就丢弃该报文,如果找到,就将该报文加入到sock结构的接收队列中,然后唤醒等待该sock结构的进程,即connect函数。
-
调用tcp_rcv_established函数,该函数位于net/ipv4/tcp_input.c中,它会根据TCP报文头的标志位,执行相应的操作,对于SYN+ACK报文来说,会调用tcp_ack函数,该函数位于net/ipv4/tcp_input.c中,它会完成以下几个步骤:
-
检查ACK序号是否有效,如果无效,就丢弃该报文,如果有效,就更新sock结构的相关字段,如snd_una,snd_wnd等。
-
检查SYN标志位是否有效,如果无效,就丢弃该报文,如果有效,就更新sock结构的相关字段,如rcv_nxt,rcv_wnd等。
-
调用tcp_send_ack函数,该函数位于net/ipv4/tcp_output.c中,它会创建一个ACK报文,并发送给服务器,以确认收到了SYN+ACK报文。
-
将TCP状态从SYN_SENT变为ESTABLISHED,表示TCP连接已经建立。
-
-
设置和发送ACK报文的位置:tcp_v4_rcv -> tcp_ack -> tcp_send_ack
-
状态转换的位置:tcp_v4_rcv -> tcp_ack -> tcp_set_state -> inet_sk_state_store
服务器端流程
服务器端的三次握手流程如下:
-
收到客户端的SYN报文,发送SYN+ACK报文,回应TCP连接请求
-
收到客户端的ACK报文,完成TCP连接的建立
收到客户端的SYN报文,发送SYN+ACK报文,回应TCP连接请求
当服务器端收到客户端的SYN报文时,数据链路层会调用netif_receive_skb函数,该函数位于net/core/dev.c中,它会根据以太网帧头的协议号,从ptype_all链表中查找相应的数据包类型的处理函数,并调用它。例如,当数据链路层接收到一个IP数据包时,会调用ip_rcv函数,该函数位于net/ipv4/ip_input.c中,它会处理IP数据包,并调用相应的传输层协议的处理函数。对于TCP协议来说,会调用tcp_v4_rcv函数,该函数位于net/ipv4/tcp_ipv4.c中,它会完成以下几个步骤:
-
根据TCP报文头的源端口号和目的端口号,从hash表中查找相应的sock结构,如果找不到,就丢弃该报文,如果找到,就判断该sock结构是否处于LISTEN状态,如果不是,就丢弃该报文,如果是,就继续处理。
-
调用tcp_v4_do_rcv函数,该函数位于net/ipv4/tcp_ipv4.c中,它会根据TCP报文头的标志位,执行相应的操作,对于SYN报文来说,会调用tcp_v4_conn_request函数,该函数位于net/ipv4/tcp_ipv4.c中,它会完成以下几个步骤:
-
检查SYN报文是否有效,如果无效,就丢弃该报文,如果有效,就继续处理。
-
为连接分配一个新的sock结构,称为子sock结构,它会继承父sock结构的一些属性,如本地地址,本地端口号等,同时为子sock结构分配一个新的本地序号,作为ISN。
-
将子sock结构加入到父sock结构的accept队列中,等待被accept函数接受。
-
调用tcp_make_synack函数,该函数位于net/ipv4/tcp_output.c中,它会创建一个SYN+ACK报文,并发送给客户端,以回应TCP连接请求,同时将TCP状态从LISTEN变为SYN_RECV。
-
-
设置和发送SYN+ACK报文的位置:tcp_v4_rcv -> tcp_v4_conn_request -> tcp_make_synack
-
状态转换的位置:tcp_v4_rcv -> tcp_v4_conn_request -> tcp_set_state -> inet_sk_state_store
收到客户端的ACK报文,完成TCP连接的建立
当服务器端收到客户端的ACK报文时,数据链路层会调用netif_receive_skb函数,该函数位于net/core/dev.c中,它会根据以太网帧头的协议号,从ptype_all链表中查找相应的数据包类型的处理函数,并调用它。例如,当数据链路层接收到一个IP数据包时,会调用ip_rcv函数,该函数位于net/ipv4/ip_input.c中,它会处理IP数据包,并调用相应的传输层协议的处理函数。对于TCP协议来说,会调用tcp_v4_rcv函数,该函数位于net/ipv4/tcp_ipv4.c中,它会完成以下几个步骤:
-
根据TCP报文头的源端口号和目的端口号,从hash表中查找相应的sock结构,如果找不到,就丢弃该报文,如果找到,就判断该sock结构是否处于SYN_RECV状态,如果不是,就丢弃该报文,如果是,就继续处理。
-
调用tcp_rcv_established函数,该函数位于net/ipv4/tcp_input.c中,它会根据TCP报文头的标志位,执行相应的操作,对于ACK报文来说,会调用tcp_ack函数,该函数位于net/ipv4/tcp_input.c中,它会完成以下几个步骤:
-
检查ACK序号是否有效,如果无效,就丢弃该报文,如果有效,就更新sock结构的相关字段,如snd_una,snd_wnd等。
-
将TCP状态从SYN_RECV变为ESTABLISHED,表示TCP连接已经建立。
-
将sock结构从父sock结构的accept队列中移动到已完成连接的队列中,等待被accept函数接受。
-
状态转换的位置:tcp_v4_rcv -> tcp_ack -> tcp_set_state -> inet_sk_state_store
-
4、send在TCP/IP协议栈中的执行路径
当用户调用send函数发送数据时,套接口层会调用inet_sendmsg函数,该函数位于net/ipv4/af_inet.c中,它会调用传输层的sendmsg函数,如tcp_sendmsg,该函数位于net/ipv4/tcp.c中,它会完成以下几个步骤:
-
检查sock结构的状态是否为ESTABLISHED,如果不是,就返回错误,如果是,就继续处理。
-
检查用户传入的数据长度是否合法,如果超过了sock结构的发送缓冲区大小,就返回错误,如果合法,就继续处理。
-
调用tcp_write_xmit函数,该函数位于net/ipv4/tcp_output.c中,它会将用户的数据拷贝到sk_buff结构中,并将sk_buff结构加入到sock结构的发送队列中,然后调用tcp_transmit_skb函数,该函数位于net/ipv4/tcp_output.c中,它会根据sock结构的相关字段,如snd_nxt,snd_wnd等,判断是否可以发送sk_buff结构,如果可以,就调用ip_queue_xmit函数,该函数位于net/ipv4/ip_output.c中,它会为sk_buff结构添加一个IP数据包头,并调用ip_output函数,该函数位于net/ipv4/ip_output.c中,它会根据目的IP地址,查找路由表,找到相应的网络设备和下一跳地址,然后调用网络设备的hard_header函数,该函数位于net/core/dev.c中,它会根据网络设备的类型,如eth_header,该函数位于net/ethernet/eth.c中,为IP数据包添加一个以太网帧头,然后调用网络设备的xmit函数,如dev_queue_xmit,该函数位于net/core/dev.c中,它会将IP数据包加入到网络设备的发送队列中,等待发送。
-
send在TCP/IP协议栈中的执行路径:send -> inet_sendmsg -> tcp_sendmsg -> tcp_write_xmit -> tcp_transmit_skb -> ip_queue_xmit -> ip_output -> dev_queue_xmit。
5、recv在TCP/IP协议栈中的执行路径
当用户调用recv函数接收数据时,套接口层会调用inet_recvmsg函数,该函数位于net/ipv4/af_inet.c中,它会调用传输层的recvmsg函数,如tcp_recvmsg,该函数位于net/ipv4/tcp.c中,它会完成以下几个步骤:
-
检查sock结构的状态是否为ESTABLISHED,如果不是,就返回错误,如果是,就继续处理。
-
检查sock结构的接收队列是否为空,如果为空,就判断是否是非阻塞模式,如果是,就返回错误,如果不是,就进入睡眠状态,等待被唤醒,如果不为空,就继续处理。
-
调用tcp_recv_urg函数,该函数位于net/ipv4/tcp.c中,它会检查是否有紧急数据,如果有,就将其拷贝到用户的缓冲区中,然后返回,如果没有,就继续处理。
-
调用skb_copy_datagram_iovec函数,该函数位于net/core/datagram.c中,它会将sock结构的接收队列中的第一个sk_buff结构的数据拷贝到用户的缓冲区中,然后返回。
-
recv在TCP/IP协议栈中的执行路径:recv -> inet_recvmsg -> tcp_recvmsg -> skb_copy_datagram_iovec
6、路由表的结构和初始化过程
路由表是一种用于存储网络目的地址和下一跳地址的映射关系的数据结构,它在网络层的转发过程中起到重要的作用。Linux内核中的路由表有两种形式,一种是基于三层的路由表,另一种是基于二层的路由表。本文主要介绍基于三层的路由表的结构和初始化过程。
路由表的结构
基于三层的路由表的主要数据结构是struct rtable,该结构位于include/net/route.h中,它包含了以下几个重要的字段:
-
rt_dst:目的IP地址,用于匹配路由表项。
-
rt_gateway:下一跳IP地址,用于转发数据包。
-
rt_dev:网络设备的名称,用于指定发送数据包的接口。
-
rt_flags:路由标志,用于表示路由的一些属性,如RTF_GATEWAY,表示该路由是一个网关路由,RTF_HOST,表示该路由是一个主机路由,RTF_LOCAL,表示该路由是一个本地路由等。
-
rt_genid:路由生成号,用于表示路由表的更新情况,每当路由表发生变化时,该值就会增加,用于通知缓存的路由表项失效。
-
rt_hash:路由表项的哈希值,用于将路由表项插入到哈希表中,提高查找效率。
-
rt_next:指向下一个路由表项的指针,用于将路由表项链接成链表,方便遍历和删除。
Linux内核中的路由表是一个由多个哈希桶组成的哈希表,每个哈希桶是一个rtable结构的链表,哈希表的大小由RT_HASH_DIVISOR宏定义,该宏位于include/net/route.h中,其值为256。哈希表的入口是rt_hash_table数组,该数组位于net/ipv4/route.c中,它包含了RT_HASH_DIVISOR个元素,每个元素是一个rtable结构的指针,指向相应的哈希桶的头部。当需要查找一个路由表项时,会根据目的IP地址的低8位,计算出哈希值,然后从rt_hash_table数组中找到对应的哈希桶,然后遍历该哈希桶的链表,比较目的IP地址是否匹配,如果匹配,就返回该路由表项,如果不匹配,就继续查找,直到找到或者遍历完毕。
路由表的初始化过程
路由表的初始化过程是在网络层的初始化函数ip_init中完成的,该函数位于net/ipv4/ip.c中,它会调用ip_rt_init函数,该函数位于net/ipv4/route.c中,它会完成以下几个步骤:
-
初始化rt_hash_table数组,将所有的元素都置为NULL,表示哈希表为空。
-
调用devinet_init函数,该函数位于net/ipv4/devinet.c中,它会遍历所有的网络设备,为每个网络设备创建一个inet_device结构,该结构位于include/linux/inetdevice.h中,它包含了网络设备的IP相关的信息,如IP地址,子网掩码,广播地址等,然后将inet_device结构加入到inet_dev_list链表中,方便查找和管理。
-
调用ip_fib_init函数,该函数位于net/ipv4/fib_frontend.c中,它会初始化一个名为fib_info_hash的哈希表,该哈希表用于存储路由信息,如目的地址,下一跳地址,网络设备等,然后调用fib4_rules_init函数,该函数位于net/ipv4/fib_rules.c中
-
调用fib4_rules_init函数,该函数位于net/ipv4/fib_rules.c中,它会初始化一个名为fib4_rules_ops的结构,该结构位于net/ipv4/fib_rules.c中,它包含了IPv4路由规则的相关操作函数,如fib4_rule_match,用于匹配路由规则,fib4_rule_action,用于执行路由规则的动作等,然后调用fib_default_rule_add函数,该函数位于net/core/fib_rules.c中,它会为每个网络命名空间添加两条默认的路由规则,一条是优先级为0的本地路由规则,用于匹配本地地址,另一条是优先级为32767的主路由规则,用于匹配其他地址。
-
调用ip_rt_init_sockets函数,该函数位于net/ipv4/route.c中,它会为每个网络命名空间创建一个名为rt_cache的套接口,该套接口用于接收和发送路由缓存的控制消息,如RTM_NEWROUTE,RTM_DELROUTE等,这些消息用于通知用户空间的路由缓存的变化,或者由用户空间发送给内核,用于修改路由缓存。
-
调用ip_route_input_init函数,该函数位于net/ipv4/route.c中,它会初始化一个名为ip_rt_bug_ops的结构,该结构位于net/ipv4/route.c中,它包含了一个名为ip_rt_bug的函数,该函数用于处理路由错误的情况,如无法找到匹配的路由表项,或者路由表项的状态不正确等,该函数会打印错误信息,并发送一个ICMP目的不可达的消息给源地址。
-
调用ip_route_init_special_entries函数,该函数位于net/ipv4/route.c中,它会为每个网络命名空间创建一些特殊的路由表项,如local_route,用于匹配本地地址,broadcast_route,用于匹配广播地址,martian_route,用于匹配非法地址等,这些路由表项都有特殊的标志位和处理函数,用于区分和处理这些特殊的情况。
-
调用fib_netdev_event函数,该函数位于net/ipv4/fib_frontend.c中,它会注册一个网络设备事件的回调函数,用于处理网络设备的变化,如网络设备的注册,注销,上线,下线等,当这些事件发生时,会调用相应的函数,如fib_add_ifaddr,fib_del_ifaddr,fib_disable_ip等,这些函数用于更新路由表和路由缓存,以适应网络设备的变化。
7、通过目的IP查询路由表的到下一跳的IP地址的过程
当网络层需要转发一个IP数据包时,会调用ip_route_input函数,该函数位于net/ipv4/route.c中,它会完成以下几个步骤:
-
检查IP数据包是否有效,如果无效,就丢弃该数据包,如果有效,就继续处理。
-
根据IP数据包的目的IP地址,源IP地址,传输层协议,传输层端口等参数,创建一个名为fl4的结构,该结构位于include/net/flow.h中,它包含了路由查询的关键信息,然后调用fib_lookup函数,该函数位于net/ipv4/fib_frontend.c中,它会根据fl4结构,从fib_info_hash哈希表中查找匹配的路由信息,如果找不到,就返回错误,如果找到,就返回一个名为res的结构,该结构位于include/net/flow.h中,它包含了路由查询的结果,如网络设备,下一跳地址等,然后调用ip_route_input_slow函数,该函数位于net/ipv4/route.c中,它会根据res结构,创建一个名为rth的结构,该结构是rtable结构的指针,它包含了路由表项的信息,然后将rth结构加入到rt_hash_table哈希表中,方便下次查找,最后返回rth结构给ip_route_input函数。
-
如果rth结构的网络设备和IP数据包的网络设备相同,表示该数据包是本地目的的,就调用ip_local_deliver函数,该函数位于net/ipv4/ip_input.c中,它会处理本地目的的IP数据包,并调用相应的传输层协议的处理函数。
-
如果rth结构的网络设备和IP数据包的网络设备不同,表示该数据包需要转发,就调用ip_forward函数,该函数位于net/ipv4/ip_forward.c中,它会处理转发的IP数据包,并调用ip_output函数,该函数位于net/ipv4/ip_output.c中,它会根据rth结构中的下一跳地址,查找ARP缓存,找到相应的MAC地址,然后调用网络设备的hard_header函数,该函数位于net/core/dev.c中,它会根据网络设备的类型,如eth_header,该函数位于net/ethernet/eth.c中,为IP数据包添加一个以太网帧头,然后调用网络设备的xmit函数,如dev_queue_xmit,该函数位于net/core/dev.c中,它会将IP数据包加入到网络设备的发送队列中,等待发送。
-
通过目的IP查询路由表的到下一跳的IP地址的过程的位置:ip_route_input -> fib_lookup -> ip_route_input_slow -> ip_local_deliver 或 ip_forward -> ip_output -> dev_queue_xmit。
8、ARP缓存的数据结构及初始化过程,包括ARP缓存的初始化
ARP缓存是用来存储IP地址和MAC地址的映射关系的表,它可以加快数据的转发,避免每次都进行ARP请求。ARP缓存的数据结构在不同的系统中可能有所不同,但一般都包含以下几个字段:
-
IP地址:表示网络层的地址,用于匹配目的IP地址或源IP地址。
-
MAC地址:表示数据链路层的地址,用于封装帧头或解析帧头。
-
状态:表示ARP缓存项的状态,如空闲,等待,已完成等,用于控制ARP缓存项的更新和删除。
-
接口:表示ARP缓存项所属的网络接口,用于确定数据的出入方向。
-
老化时间:表示ARP缓存项的有效期,用于删除过期的ARP缓存项。
ARP缓存的初始化过程一般分为以下几个步骤:
-
分配内存空间:根据系统的配置,分配一定数量的内存空间,用于存储ARP缓存项,一般采用哈希表或链表的方式组织ARP缓存项,以提高查找效率。
-
初始化ARP缓存项:将所有的ARP缓存项的状态设置为空闲,表示可以被使用,同时将其他字段设置为默认值,如IP地址为0,MAC地址为0,接口为NULL,老化时间为0等。
-
注册ARP协议:将ARP协议的处理函数注册到网络层,以便接收和发送ARP报文,同时设置ARP协议的参数,如超时时间,重传次数,老化时间等。
-
启动ARP定时器:设置一个定时器,用于定期检查ARP缓存项的状态,如果发现有过期或无效的ARP缓存项,就将其删除,以释放内存空间和保持ARP缓存的正确性。
9、如何将IP地址解析出对应的MAC地址
在网络层发送一个IP数据包时,需要知道目的IP地址对应的MAC地址,以便在数据链路层封装一个以太网帧。如果目的IP地址和源IP地址在同一个子网内,那么就可以直接通过ARP协议来解析出MAC地址,如果不在同一个子网内,那么就需要通过路由表找到下一跳的IP地址,然后再通过ARP协议来解析出MAC地址。ARP协议的主要功能是通过发送ARP请求和接收ARP应答,来实现IP地址和MAC地址的映射关系。下面分别介绍ARP请求和应答的过程。
ARP请求
当网络层需要解析一个IP地址的MAC地址时,会调用arp_find函数,该函数位于net/ipv4/arp.c中,它会完成以下几个步骤:
-
根据IP地址,从ARP缓存中查找是否有对应的MAC地址,如果有,就直接返回,如果没有,就继续处理。
-
为IP地址分配一个arp_table结构,该结构位于include/net/neighbour.h中,它包含了IP地址和MAC地址的映射关系,以及一些状态和操作函数,然后将该结构加入到ARP缓存的哈希表中,方便查找和管理。
-
调用arp_solicit函数,该函数位于net/ipv4/arp.c中,它会创建一个ARP请求报文,并发送给目的IP地址,以询问其MAC地址,同时将arp_table结构的状态设置为NUD_INCOMPLETE,表示正在等待ARP应答。
-
ARP请求的位置:arp_find -> arp_solicit
ARP应答
当一个主机收到一个ARP请求报文时,会调用arp_rcv函数,该函数位于net/ipv4/arp.c中,它会完成以下几个步骤:
-
检查ARP请求报文是否有效,如果无效,就丢弃该报文,如果有效,就继续处理。
-
根据ARP请求报文中的源IP地址和源MAC地址,更新ARP缓存,如果已经存在对应的arp_table结构,就更新其MAC地址和状态,如果不存在,就创建一个新的arp_table结构,并加入到ARP缓存中。
-
判断ARP请求报文中的目的IP地址是否是自己的IP地址,如果不是,就丢弃该报文,如果是,就继续处理。
-
调用arp_send函数,该函数位于net/ipv4/arp.c中,它会创建一个ARP应答报文,并发送给源IP地址,以告知自己的MAC地址,同时将arp_table结构的状态设置为NUD_REACHABLE,表示已经收到ARP应答。
-
ARP应答的位置:arp_rcv -> arp_send
10、跟踪TCP send过程中的路由查询和ARP解析的最底层实现
在之前节中,我们介绍了send在TCP/IP协议栈中的执行路径,其中涉及到了两个重要的过程,一是路由查询,即根据目的IP地址,查找路由表,找到相应的网络设备和下一跳地址,二是ARP解析,即根据下一跳地址,查找ARP缓存,找到相应的MAC地址。这两个过程都是在网络层的ip_output函数中完成的,该函数位于net/ipv4/ip_output.c中,它会调用以下几个函数:
-
ip_route_output_flow函数,该函数位于net/ipv4/route.c中,它会根据目的IP地址,源IP地址,传输层协议,传输层端口等参数,创建一个名为fl4的结构,该结构位于include/net/flow.h中,它包含了路由查询的关键信息,然后调用fib_lookup函数,该函数位于net/ipv4/fib_frontend.c中,它会根据fl4结构,从fib_info_hash哈希表中查找匹配的路由信息,如果找不到,就返回错误,如果找到,就返回一个名为res的结构,该结构位于include/net/flow.h中,它包含了路由查询的结果,如网络设备,下一跳地址等,然后调用ip_route_create_rcu函数,该函数位于net/ipv4/route.c中,它会根据res结构,创建一个名为rth的结构,该结构是rtable结构的指针,它包含了路由表项的信息,然后将rth结构加入到rt_hash_table哈希表中,方便下次查找,最后返回rth结构给ip_output函数。
-
arp_bind_neighbour函数,该函数位于net/ipv4/arp.c中,它会根据rth结构中的网络设备和下一跳地址,从ARP缓存中查找是否有对应的MAC地址,如果有,就直接返回,如果没有,就调用arp_find函数,该函数位于net/ipv4/arp.c中,它会为下一跳地址分配一个arp_table结构,并将其加入到ARP缓存中,然后调用arp_solicit函数,该函数位于net/ipv4/arp.c中,它会发送一个ARP请求报文,以询问下一跳地址的MAC地址,同时将arp_table结构的状态设置为NUD_INCOMPLETE,表示正在等待ARP应答,最后返回arp_table结构给arp_bind_neighbour函数。