TCP/IP协议栈源代码分析
1、inet_init是如何被调用的?从start_kernel到inet_init调用路径
inet_init
通常是在Linux内核网络子系统初始化过程中被调用的函数。它在网络子系统启动时执行,负责初始化网络相关的数据结构、协议栈和网络设备等。
在Linux内核中,网络子系统的初始化过程通常是由net_init
函数开始的。net_init
函数会调用inet_init
函数,大致流程是net_init
-> inet_init
,这样来完成网络子系统的初始化。
// 网络子系统初始化的入口函数
void __init net_init(void) {
// 其他网络子系统初始化操作
// 调用 inet_init 函数来初始化
inet_init();
// 其他网络子系统初始化操作
}
// inet_init 函数定义在 net/ipv4/af_inet.c 文件中
void __init inet_init(void) {
// 初始化相关数据结构和功能
// 其他初始化操作
}
start_kernel
是Linux内核的启动入口函数,它调用rest_init
来执行内核的进一步初始化。rest_init
函数中,执行了一些其他的初始化操作后,调用了kernel_init
。在kernel_init
函数中,各种子系统都会初始化,其中就包括网络子系统的初始化,net_init
函数就是网络子系统初始化的入口。在net_init
中调用inet_init
来完成初始化。
asmlinkage __visible void __init start_kernel(void) {
// 初始化操作
// 执行内核初始化
rest_init();
}
static noinline void __init_refok rest_init(void) {
// 其他初始化操作
// 内核初始化
kernel_init();
}
static noinline void __init kernel_init(void) {
// 其他初始化操作
// 网络子系统的初始化
net_init();
}
2、跟踪分析TCP/IP协议栈如何将自己与上层套接口与下层数据链路层关联起来的?
在Linux内核中,协议簇注册和套接口等多个结构和函数来实现。以下是一个示例,展示了TCP/IP协议栈中不同层次之间的关系以及数据的传递过程。
static struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.obj_size = sizeof(struct tcp_sock),
};
static int __init tcp_init(void) {
inet_register_protosw(&inet_stream_ops, &tcp_prot);
return 0;
}
tcp_prot
是一个struct proto
结构体,其中包含了协议的名称、模块信息等。通过inet_register_protosw
函数将TCP协议注册到协议列表中。
-
传输层与套接口关联:
- 传输层协议与上层套接口之间的关联通过
struct proto
结构体中的函数指针集合来实现。 - 当用户空间的应用程序通过
socket()
系统调用创建套接口时,使得套接口与传输层建立关联。
- 传输层协议与上层套接口之间的关联通过
struct proto {
// 其他字段
int (*init)(struct sock *sk); // 初始化传输层协议
int (*connect)(struct sock *sk, struct sockaddr *uaddr, int addr_len, int flags); // 建立连接
// 其他函数指针
};
在实际的TCP模块中,这些函数指针会指向TCP协议相关的操作函数,如tcp_init
、tcp_connect
等。这样,当套接口调用特定的操作时,就会与对应的传输层函数进行关联。
-
传输层与网络层关联:
- 传输层协议与网络层协议的关联通常在数据包封装的过程中实现。在TCP协议中,将数据发送给IP层进行路由和封装。
- 在TCP发送数据的路径中,数据将传递给IP层,IP层负责将TCP数据包封装为IP数据报并添加IP头部信息。
int tcp_v4_send_check(struct sk_buff *skb) {
// 其他代码
ip_send_check(ip_hdr(skb));
// 其他代码
}
在这个示例中,TCP层调用了ip_send_check
函数,将TCP数据包传递给IP层进行检查和封装。
-
网络层与数据链路层关联:
- 在Linux内核中,网络层与数据链路层的关联通常在数据包的发送路径中完成。IP层封装数据后,会将数据包传递给数据链路层。
- 数据链路层负责将IP数据包封装为特定的数据链路层帧,并通过设备驱动程序发送到物理介质上。
static int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev) {
// 其他代码
dev_queue_xmit(skb);
// 其他代码
}
在这个示例中,数据链路层通过调用dev_queue_xmit
函数将封装好的数据包发送到物理介质上。
3、TCP的三次握手源代码跟踪分析,跟踪找出设置和发送SYN/ACK的位置,以及状态转换的位置
-
设置和发送SYN:
客户端发起连接:
当客户端应用程序调用
connect()
函数时,会触发TCP连接的建立。sys_connect()
是系统调用的入口函数,在net/socket.c
文件中。sock->ops->connect()
会调用协议族对应的连接函数。
SYSCALL_DEFINE3(connect, int, sockfd, struct sockaddr __user *, addr, int, addrlen) {
// 其他代码
err = sock->ops->connect(sock, (struct sockaddr *)addr, addrlen, flags);
// 其他代码
}
发送SYN:
tcp_v4_connect()
是处理IPv4 TCP连接的函数,位于net/ipv4/tcp_ipv4.c
文件中。
tcp_send_syn()
用于设置SYN标志位并发送SYN包。
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) {
// 其他代码
tcp_send_syn(sk);
// 其他代码
}
服务器端处理SYN请求:
服务器端接收到SYN请求后,会进行处理并回复SYN/ACK。
在net/ipv4/tcp_ipv4.c
文件中,接收SYN请求的函数是tcp_v4_syn_recv_sock()
。
tcp_send_synack()
函数发送SYN/ACK响应。
void tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb, struct request_sock *req) {
// 其他代码
tcp_send_synack(sk);
// 其他代码
}
状态转换处理:
状态转换通常在接收到SYN/ACK后进行,以及在不同的状态下处理不同情况。
在net/ipv4/tcp_input.c
文件中,状态转换的函数是tcp_rcv_state_process()
。
tcp_rcv_synsent_state_process()
处理了在SYN_SENT状态下接收到SYN/ACK的情况,完成状态的转换。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) {
// 其他代码
tcp_rcv_synsent_state_process(sk, skb);
// 其他代码
}
4、send在TCP/IP协议栈中的执行路径
-
应用程序调用
send()
:应用程序调用
send()
发送数据,传递套接字描述符、数据缓冲区和数据长度。 -
系统调用:
系统调用
sys_send()
或sys_sendto()
等。在net/socket.c
文件中实现。SYSCALL_DEFINE3(send, int, sockfd, const void __user *, buff, size_t, len) { // 其他代码 return sock_sendmsg(sock, &msg, len); // 其他代码 }
-
调用
sock_sendmsg()
:sock_sendmsg()
是一个转发函数,它调用了特定协议族的sendmsg()
函数。int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) { // 其他代码 error = sock->ops->sendmsg(sock, msg, size); // 其他代码 }
-
TCP 协议族的
sendmsg()
:对于 TCP 协议族,
sock->ops->sendmsg()
调用了特定协议族(如TCP)的sendmsg()
函数。int tcp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) { // 其他代码 sk = sock->sk; tcp_write_xmit(sk, skb, clone_it, gfp_mask); // 其他代码 }
-
tcp_send_skb()
函数:tcp_send_skb()
函数是 TCP 发送数据包的关键函数,负责准备和发送 SKB(Socket Buffer)。int tcp_send_skb(struct sock *sk, struct sk_buff *skb) { // 其他代码 tcp_transmit_skb(skb); // 准备并发送 SKB // 其他代码 }
-
tcp_transmit_skb()
:tcp_transmit_skb()
函数用于准备和发送 SKB(Socket Buffer)。void tcp_transmit_skb(struct sock *sk, struct sk_buff *skb) { // 其他代码 tcp_transmit_skb(skb); // 准备并发送 SKB // 其他代码 }
-
tcp_write_xmit()
:tcp_write_xmit()
是 TCP 发送数据的核心函数,位于net/ipv4/tcp_output.c
文件中。void tcp_write_xmit(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask) { // 其他代码 tcp_transmit_skb(skb); // 其他代码 }
-
数据包处理和发送:
在 tcp_write_xmit()
中,skb 被发送到网络层和数据链路层,最终被发送到目标主机。
5、recv在TCP/IP协议栈中的执行路径
应用程序调用 recv()
,传递套接字描述符和接收数据的缓冲区等参数。
系统调用最终被转发到套接字层,对于 recv()
调用,在 Linux 内核的 net/socket.c
文件中,有以下实现:
SYSCALL_DEFINE3(recv, int, sockfd, void __user *, buff, size_t, len) {
// 其他代码
return sock_recvmsg(sock, &msg, len, flags);
// 其他代码
}
sock_recvmsg()
函数在套接字层处理消息接收,它调用对应协议族的 recvmsg()
函数。
int sock_recvmsg(struct socket *sock, struct msghdr *msg, size_t size, int flags) {
// 其他代码
return sock->ops->recvmsg(sock, msg, size, flags);
// 其他代码
}
对于 TCP 协议族,recvmsg()
函数调用了 TCP 的 tcp_recvmsg()
函数。
int tcp_recvmsg(struct socket *sock, struct msghdr *msg, size_t size, int flags) {
// 其他代码
sk = sock->sk;
tcp_recv_skb(sk, skb); // 接收并处理 SKB
// 其他代码
}
tcp_recv_skb()
函数是 TCP 接收数据包的函数,用于接收并处理 SKB(Socket Buffer)。
int tcp_recv_skb(struct sock *sk, struct sk_buff *skb) {
// 其他代码
tcp_rcv_state_process(sk, skb); // 处理接收到的 SKB
// 其他代码
}
tcp_rcv_state_process()
函数用于处理接收到的数据包,根据 TCP 连接的状态进行不同的处理。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) {
// 其他代码
tcp_rcv_established(sk, skb); // 在 ESTABLISHED 状态下处理接收到的 SKB
// 其他代码
}
tcp_rcv_established()
函数处理在 ESTABLISHED 状态下接收到的数据包。
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb) {
// 其他代码
tcp_queue_rcv_skb(sk, skb); // 将 SKB 添加到接收队列
// 其他代码
}
tcp_queue_rcv_skb()
将接收到的 skb 添加到接收队列中,并最终将数据传递给应用程序。
6、路由表的结构和初始化过程
Linux 内核中的路由表结构主要由路由缓存和路由表组成。路由表主要用于确定数据包从源地址到目的地址的传输路径。在 Linux 内核中,这些数据结构在 include/net/route.h
和 net/ipv4/route.c
文件中定义和实现。以下是路由缓存结构定义。
// 路由缓存结构
struct rtable {
// 路由表条目的相关信息
// 这里只是一个简化的示例,实际结构更为复杂
struct dst_entry dst;
__u32 gw4; // 网关地址
// 其他字段
};
// 简化的 dst_entry 结构
struct dst_entry {
// 一些路由相关的字段
struct net_device *dev; // 目标设备
// 其他字段
};
在 net/ipv4/route.c
文件中有路由表初始化的函数 ip_rt_init()
,它用于初始化路由表。
void __init ip_rt_init(void) {
// 创建和初始化路由表条目
struct rtable *rt = alloc_route();
if (!rt)
return;
// 设置默认路由信息
rt->dst.addr = DEFAULT_GATEWAY_ADDRESS;
rt->gw4 = DEFAULT_GATEWAY_ADDRESS;
rt->dst.dev = DEFAULT_INTERFACE;
// ...其他初始化...
// 将条目添加到路由表中
rt_add_uncached_entry(rt);
}
这个示例中的 ip_rt_init()
函数是在内核启动时被调用的,在这个函数中,它创建并初始化了一个路由表条目,并将其添加到路由表中。
7、通过目的IP查询路由表的到下一跳的IP地址的过程
当数据包需要发送到目的地时,内核需要确定下一跳 IP 地址以便正确地路由数据包。
内核会调用路由查找函数,比如 ip_route_input()
,该函数位于 net/ipv4/route.c
中。
struct rtable *ip_route_input(struct sk_buff *skb, __be32 daddr, __be32 saddr, u8 tos, struct net_device *dev) {
// 其他代码
rt = ip_route_input_slow(skb, daddr, saddr, tos, dev);
// 其他代码
}
在 ip_route_input()
中,会调用 ip_route_input_slow()
函数,该函数实现了路由查找的具体过程。
struct rtable *ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr, u8 tos, struct net_device *dev) {
struct rtable *rt;
// 查询路由表
rt = ip_route_lookup(skb->dst.dev, daddr, saddr, tos, RT_CONN_FLAGS(rt->rt_flags));
return rt;
}
ip_route_lookup()
函数用于在路由表中查找目的 IP 地址的路由条目。
struct rtable *ip_route_lookup(struct net_device *dev, __be32 daddr, __be32 saddr, u8 tos, int flags) {
struct rtable *rt;
// 在路由表中查找目的 IP 地址的路由条目
rt = __ip_route_output_key(dev_net(dev), &fl);
return rt;
}
__ip_route_output_key()
函数是一个重要的路由查找函数,用于查询路由表。
struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {
struct rtable *rt;
// 其他代码.
// 查询路由表
rt = ip_route_output_key(net, fl4);
return rt;
}
ip_route_output_key()
函数是用于查找路由表的核心函数。
struct rtable *ip_route_output_key(struct net *net, struct flowi4 *fl4) {
struct rtable *rt;
// 其他代码
// 实际的路由查找过程
rt = __mkroute_output(net, fl4, &res);
return rt;
}
rt
变量即是路由查找的结果,包含了下一跳 IP 地址以及其他路由相关信息。通过这个结果,内核就可以确定数据包的下一跳地址并进行发送。
8、ARP缓存的数据结构及初始化过程,包括ARP缓存的初始化
在 Linux 内核中,ARP 缓存用于存储 IP 地址到 MAC 地址的映射关系,在需要发送数据包时能够快速地查找目标设备的 MAC 地址。
ARP 缓存的数据结构主要由 struct neighbour
和 struct neigh_table
组成。
struct neighbour {
struct neighbour *next;
struct neigh_table *tbl;
struct dst_entry *arp_entry;
// 其他字段
};
struct neigh_table {
//其他字段
struct neighbour **hash_buckets;
// 其他字段
};
-
ARP 初始化过程:
1.ARP 表格的创建与初始化:
在 Linux 内核中,ARP 表格的初始化在
net/ipv4/arp.c
文件中完成。以下是其中的部分代码示例:#include <net/arp.h> struct neigh_table arp_tbl; // ARP 表格 // ARP 表格的初始化 void __init arp_init(void) { int err; memset(&arp_tbl, 0, sizeof(struct neigh_table)); // 其他初始化 err = neigh_table_init(&arp_tbl); // 初始化 ARP 表格 if (err) { // 处理错误 } // 其他初始化 }
2.初始化 ARP 缓存表:
在初始化过程中,通过
neigh_table_init()
函数初始化 ARP 缓存表,该表格用于存储 IP 地址到 MAC 地址的映射关系。int __init neigh_table_init(struct neigh_table *tbl) { // 其他初始化 tbl->hash_buckets = neigh_hash_alloc(tbl); // 分配哈希桶 if (!tbl->hash_buckets) return -ENOMEM; // 其他初始化 return 0; }
3.启动 ARP 子系统:
在内核初始化过程中,通过调用
arp_init()
函数启动 ARP 子系统,进行 ARP 表格的创建和初始化,以便正确地进行地址解析。 -
ARP 缓存初始化过程:
ARP 缓存的初始化过程涉及内核初始化和网络子系统的启动阶段。
1.ARP 缓存初始化函数:
ARP 缓存的初始化主要通过 neigh_table_init()
函数完成,该函数在 net/core/neighbour.c
文件中定义。
int __init neigh_table_init(struct neigh_table *tbl) {
// 其他初始化
tbl->hash_buckets = neigh_hash_alloc(tbl);
if (!tbl->hash_buckets)
return -ENOMEM;
// 其他初始化
return 0;
}
2.创建ARP缓存表:
内核初始化过程中会创建并初始化 ARP 缓存表,通常在网络子系统初始化的早期阶段完成。
struct neigh_table arp_tbl; // 创建 ARP 缓存表
// 初始化 ARP 缓存表
void __init arp_init(void) {
int err;
memset(&arp_tbl, 0, sizeof(struct neigh_table));
// 其他初始化
err = neigh_table_init(&arp_tbl);
if (err) {
// 处理错误
}
// 其他初始化
}
9、如何将IP地址解析出对应的MAC地址
在 Linux 内核中,将 IP 地址解析为对应的 MAC 地址是通过 ARP协议实现的。
在 Linux 内核中,ARP 相关的函数和数据结构主要在 net/ipv4/arp.c
文件中。以下是涉及到 ARP 解析 IP 地址的主要函数。
arp_find
函数用于查找 ARP 缓存中是否存在指定的 IP 地址对应的 MAC 地址。neigh_lookup()
位于 net/core/neighbour.c
,这个函数用于查找邻居缓存。如果需要解析的 IP 地址不在邻居缓存中,则会调用 ARP 请求,并等待响应。
struct neighbour *arp_find(struct net_device *dev, __be32 sip, __be32 tip, int nud_state) {
struct neighbour *n;
// 从 ARP 缓存中查找对应的条目
n = neigh_lookup(&arp_tbl, &tip, dev, 0);
if (n && n->nud_state == nud_state)
return n;
return NULL;
}
若ARP缓存表中没有对应的MAC地址条目,内核将发送ARP请求以获取对应的MAC地址。发送ARP请求通过arp_create()
函数。
struct neighbour *arp_create(struct net_device *dev, __be32 ip) {
struct neighbour *n;
n = neigh_create(&arp_tbl, &ip, dev);
if (n)
n->ops = &arp_direct_ops;
return n;
}
发送ARP请求是通过arp_send()
函数完成的,该函数位于 net/ipv4/arp.c
文件中。
int arp_send(int type, int ptype, __be32 dest_ip, struct net_device *dev) {
struct sk_buff *skb;
skb = arp_create(dev, type, ptype, dest_ip);
if (!skb)
return -ENOMEM;
dev_queue_xmit(skb);
return 0;
}
一旦目标设备收到ARP请求,会返回对应的MAC地址。内核收到响应后,会更新ARP缓存表中的相关条目。接收和处理ARP响应通常在 arp_rcv()
函数中完成。
int arp_rcv(struct sk_buff *skb) {
// 从数据包中解析 ARP 响应
struct arphdr *arp = (struct arphdr *)skb->data;
// 根据 ARP 响应信息更新 ARP 缓存表中的条目
// ...
}
10、跟踪TCP send过程中的路由查询和ARP解析的最底层实现
1.TCP 发送数据:
当应用程序调用 send()
发送数据时,TCP 协议栈接收数据并准备发送。
2.构建数据包:
TCP 协议栈构建数据包,包括设置 IP 头部信息。在这个过程中,需要确定数据包的路由。
3.检查和发送 TCP 包
tcp_v4_send_check()
函数负责检查和发送 TCP 包,它位于 net/ipv4/tcp_ipv4.c
中。
int tcp_v4_send_check(struct sk_buff *skb, ...)
{
// 准备发送TCP数据包,调用路由查找函数
int err = ip_route_output_flow(skb);
if (err)
return err;
// ...
}
4.IP 路由查找:
在数据包准备发送之前,调用 ip_route_output_flow()
函数进行 IP 路由查找。这个函数位于 net/ipv4/route.c
中,它会根据目标 IP 地址查找路由表。它调用了 ip_route_output_key()
来进行实际的路由查找。
struct rtable *ip_route_output_flow(struct net *net, struct flowi4 *fl4, struct sock *sk) {
struct rtable *rt;
// 路由表查询过程
rt = __ip_route_output_key(net, fl4);
return rt;
}
5.路由表查询过程
在 __ip_route_output_key()
函数中进行路由表的查询和匹配,位于 net/ipv4/route.c
,是路由查找的核心,用来查找目标 IP 地址对应的路由表项。
struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {
// 路由查找过程,涉及缓存和最长前缀匹配等
// ...
struct rtable *rt = fib_lookup(&init_net, fl4);
return rt;
}
6.查找路由表项
fib_lookup()
函数用于查找匹配目标 IP 地址的路由表项。它是路由查找过程的一部分,涉及到 IP 地址匹配和路由表项的选择。
struct rtable *fib_lookup(struct net *net, struct flowi4 *fl4)
{
// 查找匹配目标IP地址的路由表项
// ...
return rt;
}
7.确定路由信息
ip_route_output_flow()
函数会返回一个 struct rtable
结构体,其中包含了确定的路由信息。
struct rtable {
struct dst_entry dst; // 用于存储目标地址的信息
// 其他路由信息
// ...
};
neigh_resolve_output()
函数位于 net/core/neighbour.c
中,用于执行 ARP 解析。当路由表项中缺少目标 IP 地址对应的 MAC 地址时,会调用这个函数进行解析。
int neigh_resolve_output(struct net *net, struct dst_entry *dst, struct flowi4 *fl4, ...)
{
struct neighbour *neigh = neigh_lookup(net, dst, &fl4->daddr);
if (!neigh) {
// 如果邻居缓存中没有对应的条目,则创建并发送ARP请求
neigh = arp_create(&fl4->daddr, dev);
if (!neigh)
return -ENOMEM;
}
// ...
}
neigh_lookup()
位于 net/core/neighbour.c
,这个函数用于查找邻居缓存。如果需要解析的 IP 地址不在邻居缓存中,则会调用 ARP 请求,并等待响应。
struct neighbour *neigh_lookup(struct net *net, struct dst_entry *dst, const void *pkey)
{
// 在邻居缓存中查找对应IP地址的邻居条目
// ...
return neigh;
}
arp_create()
在 net/ipv4/arp.c
中,这个函数用于创建并发送 ARP 请求。
struct neighbour *arp_create(const struct in_addr *sip, struct net_device *dev)
{
// 创建并发送ARP请求
// ...
}
发送ARP请求是通过arp_send()
函数完成的,该函数位于 net/ipv4/arp.c
文件中。
int arp_send(int type, int ptype, __be32 dest_ip, struct net_device *dev) {
struct sk_buff *skb;
skb = arp_create(dev, type, ptype, dest_ip);
if (!skb)
return -ENOMEM;
dev_queue_xmit(skb);
return 0;
}
一旦目标设备收到ARP请求,会返回对应的MAC地址。内核收到响应后,会更新ARP缓存表中的相关条目。接收和处理ARP响应通常在 arp_rcv()
函数中完成。
int arp_rcv(struct sk_buff *skb) {
// 从数据包中解析 ARP 响应
struct arphdr *arp = (struct arphdr *)skb->data;
// 根据 ARP 响应信息更新 ARP 缓存表中的条目
// ...
}
总结
之前对于计算机网络的了解仅停留在书本和应试,通过对本课程的学习,我从代码的层面更加深入地理解了网络应用在底层是怎样实现通信的,通过阅读相关底层代码,学习了 Linux 内核源代码中对于网络协议栈相关的实现。
最后感谢孟宁老师为我们精心挑选的各个实验,这些实验都让我有所收获,为我的学习提供了方向。