一、inet_init是如何被调用的?从start_kernel到inet_init调用路径
start_kernel
函数位于init/main.c
文件中,是内核启动的入口函数。
asmlinkage __visible void __init start_kernel(void)
{
// ...
// 调用 rest_init 函数
rest_init();
// ...
}
rest_init
函数位于kernel/init/main.c
文件中,完成一些初始化工作,并通过kernel_thread
函数创建一个新的内核线程。
static void __init rest_init(void)
{
// ...
// 创建一个新的内核线程,并指定其执行函数为 kernel_init
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
// ...
}
kernel_init
函数位于init/main.c
文件中,是内核初始化的第一个阶段。
static int __init kernel_init(void *unused)
{
// ...
// 调用 do_basic_setup 函数
do_basic_setup();
// ...
// 调用 do_initcalls 函数执行内核初始化的第二个阶段
do_initcalls();
// ...
}
do_basic_setup
函数位于init/main.c
文件中,完成一些基本的系统初始化工作,并调用do_initcalls
函数执行内核初始化的第二个阶段。
static void __init do_basic_setup(void)
{
// ...
// 调用 do_initcalls 函数执行内核初始化的第二个阶段
do_initcalls();
// ...
}
do_initcalls
函数位于init/main.c
文件中,会调用do_one_initcall
函数多次,以执行一系列的初始化函数。
static void __init do_initcalls(void)
{
// ...
// 调用 do_one_initcall 函数,执行一系列的初始化函数
do_one_initcall(inet_init);
// ...
}
inet_init
函数位于net/ipv4/af_inet.c
文件中,是网络子系统中 IPv4 协议族的初始化函数。它执行了一些与 IPv4 相关的初始化工作。
static int __init inet_init(void)
{
// ...
// 执行 IPv4 相关的初始化工作
// ...
return 0;
}
因此,start_kernel
-> rest_init
-> kernel_init
-> do_basic_setup
-> do_initcalls
-> do_one_initcall
-> inet_init
是 inet_init
函数从 start_kernel
到 inet_init
的调用路径。
二、跟踪分析TCP/IP协议栈如何将自己与上层套接口与下层数据链路层关联起来的?
在 Linux 内核中,TCP/IP 协议栈通过网络套接口(socket)与上层应用程序和下层数据链路层进行关联。
- 上层套接口与协议关联(以 TCP 为例):
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 绑定套接字到本地地址和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
// 监听连接请求
listen(sockfd, SOMAXCONN);
socket
系统调用创建了一个 TCP 套接字,并返回相应的文件描述符。bind
系统调用将套接字绑定到本地地址和端口。之后,通过 listen
系统调用开始监听连接请求。
- 数据传输和处理:
// 接受连接请求
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);
int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
// 发送和接收数据
char buffer[1024];
ssize_t num_bytes = recv(connfd, buffer, sizeof(buffer), 0);
send(connfd, buffer, num_bytes, 0);
accept
系统调用接受连接请求,并返回一个新的文件描述符 connfd
,用于与客户端进行通信。通过 recv
和 send
系统调用,可以在套接字之间传输数据。
- 下层数据链路层处理
// 发送数据帧
struct ethhdr eth_header;
struct iphdr ip_header;
struct tcphdr tcp_header;
// 构建以太网头部、IP 头部和 TCP 头部
struct sk_buff *skb = alloc_skb(total_len, GFP_KERNEL);
// 分配 sk_buff 结构体
// 将数据帧封装到 skb
memcpy(skb_put(skb, sizeof(eth_header)), ð_header, sizeof(eth_header));
memcpy(skb_put(skb, sizeof(ip_header)), &ip_header, sizeof(ip_header));
memcpy(skb_put(skb, sizeof(tcp_header)), &tcp_header, sizeof(tcp_header));
// 发送 skb
dev_queue_xmit(skb);
在上述示例中,通过构建以太网头部、IP 头部和 TCP 头部,将数据帧封装到一个 sk_buff 结构体中。然后,使用 dev_queue_xmit
函数将 skb 发送到网络设备队列中,以便进一步处理和发送到物理链路。
三、TCP的三次握手源代码跟踪分析,跟踪找出设置和发送SYN/ACK的位置,以及状态转换的位置
- 设置和发送 SYN/ACK 的位置:
在 TCP 的三次握手过程中,服务器端(被动打开方)负责设置和发送 SYN/ACK 报文。
// 文件:net/ipv4/tcp_ipv4.c
// 函数:tcp_v4_rcv
// 位置:约在 3500 行附近
// 处理接收到的 SYN 报文段
static void tcp_v4_rcv(struct sk_buff *skb)
{
// ...
// 检查报文段是否是 SYN 报文
if (th->syn) {
// ...
// 创建一个新的连接请求
struct tcp_sock *newtp = tcp_create_openreq_child(sk, skb);
if (newtp) {
// 设置新连接请求的状态和序列号等参数
tcp_init_sock(newtp, req->rsk_listener);
// 设置 SYN/ACK 报文的标志
tcp_initialize_rcv_mss(newtp);
tcp_synack_rtt_meas(newtp, req);
// 构建 SYN/ACK 报文
tcp_send_synack(newtp, req);
// 发送 SYN/ACK 报文
tcp_event_data_recv(newtp, skb);
// ...
}
}
// ...
}
在上述代码中,tcp_v4_rcv
函数用于处理接收到的 TCP 报文。当接收到的报文段的 SYN 标志被设置时,表示这是一个连接请求的 SYN 报文。在这种情况下,内核会根据接收到的报文创建一个新的连接请求,并通过调用 tcp_send_synack
函数设置并发送 SYN/ACK 报文。
- 状态转换的位置:
在 TCP 的三次握手过程中,涉及到多个状态的转换,其中包括 SYN_SENT、SYN_RECV、ESTABLISHED 等。
// 文件:net/ipv4/tcp_ipv4.c
// 函数:tcp_v4_syn_recv_sock
// 位置:约在 3600 行附近
// 创建 SYN_RECV 状态的套接字
static struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
// ...
// 创建 SYN_RECV 状态的套接字
newsk = tcp_create_openreq_child(sk, skb);
if (newsk) {
// 设置套接字的状态为 SYN_RECV
newtp = tcp_sk(newsk);
newtp->rx_opt.tstamp_ok = sysctl_tcp_timestamps;
newtp->advmss = tcp_mtu_to_mss(newtp, dst_mtu(dst));
// ...
// 触发状态转换
inet_csk(sk)->icsk_accept_queue.rskq_defer_accept = 1;
inet_csk(newsk)->icsk_ulp_data = req->ulp_info;
// ...
}
// ...
return newsk;
}
在上述代码中,tcp_v4_syn_recv_sock
函数用于创建 SYN_RECV 状态的套接字。在创建套接字后,通过设置套接字的状态和其他相关参数来触发状态转换。这里使用了 inet_csk(sk)->icsk_accept_queue.rskq_defer_accept
标志来延迟接受连接,从而将套接字状态从 SYN_SENT 转换为 SYN_RECV。
四、send在TCP/IP协议栈中的执行路径
send
函数用于将数据发送到 TCP/IP 协议栈中的 TCP 套接字
- 用户层调用
send
函数:
// 文件:net/socket.c
// 函数:sock_sendmsg
// 位置:约在 3500 行附近
ssize_t sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
// ...
// 调用协议族的 sendmsg 函数
ssize_t ret = sock->ops->sendmsg(sock, msg, len);
// ...
return ret;
}
sock_sendmsg
函数是用户层调用 send
函数的入口。它将调用协议族的 sendmsg
函数来处理数据的发送。
- 调用 TCP 协议族的
sendmsg
函数:
// 文件:net/ipv4/tcp.c
// 函数:tcp_sendmsg
// 位置:约在 8000 行附近
ssize_t tcp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
// ...
// 将用户层的数据复制到发送缓冲区
ret = memcpy_from_msg(data, msg, size);
// ...
// 调用核心的发送函数
ret = tcp_sendmsg_locked(sk, msg, size);
// ...
return ret;
}
tcp_sendmsg
函数被调用来处理 TCP 协议族的数据发送。它首先将用户层的数据复制到发送缓冲区,然后调用核心的发送函数 tcp_sendmsg_locked
进行实际的发送操作。
- 核心的发送函数
tcp_sendmsg_locked
:
// 文件:net/ipv4/tcp_output.c
// 函数:tcp_sendmsg_locked
// 位置:约在 1200 行附近
ssize_t tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
// ...
// 创建发送的 sk_buff
skb = sock_alloc_send_skb(sk, size, msg->msg_flags & MSG_DONTWAIT, &err);
if (!skb) {
// 处理内存不足的情况
goto failure;
}
// ...
// 将用户层的数据复制到 sk_buff
ret = memcpy_from_msg(skb_put(skb, size), msg, size);
// ...
// 调用 TCP/IP 协议栈的数据发送函数
ret = tcp_transmit_skb(sk, skb, msg->msg_flags);
// ...
return ret;
}
tcp_sendmsg_locked
函数首先通过调用 sock_alloc_send_skb
函数创建一个发送的 sk_buff 结构体,然后将用户层的数据复制到 sk_buff 中。最后,它调用 TCP/IP 协议栈的数据发送函数 tcp_transmit_skb
来实际发送数据。
五、recv在TCP/IP协议栈中的执行路径
recv
函数用于从 TCP/IP 协议栈中的 TCP 套接字接收数据。
- 用户层调用
recv
函数
// 文件:net/socket.c
// 函数:sock_recvmsg
// 位置:约在 4000 行附近
ssize_t sock_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
// ...
// 调用协议族的 recvmsg 函数
ssize_t ret = sock->ops->recvmsg(sock, msg, size, flags);
// ...
return ret;
}
sock_recvmsg
函数是用户层调用 recv
函数的入口。它将调用协议族的 recvmsg
函数来处理数据的接收。
调用 TCP 协议族的 recvmsg
函数:
// 文件:net/ipv4/tcp.c
// 函数:tcp_recvmsg
// 位置:约在 10000 行附近
ssize_t tcp_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
// ...
// 调用核心的接收函数
ret = tcp_recvmsg_locked(sk, msg, size, flags);
// ...
return ret;
}
tcp_recvmsg
函数被调用来处理 TCP 协议族的数据接收。它调用核心的接收函数 tcp_recvmsg_locked
进行实际的接收操作。
- 核心的接收函数
tcp_recvmsg_locked
:
/ 文件:net/ipv4/tcp.c
// 函数:tcp_recvmsg_locked
// 位置:约在 9800 行附近
ssize_t tcp_recvmsg_locked(struct sock *sk, struct msghdr *msg, size_t size,
int flags)
{
// ...
// 从套接字接收数据
copied = tcp_read_sock(sk, msg, size, flags);
// ...
return copied;
}
tcp_recvmsg_locked
函数通过调用 tcp_read_sock
函数从套接字接收数据。
- 调用
tcp_read_sock
函数:
// 文件:net/ipv4/tcp.c
// 函数:tcp_read_sock
// 位置:约在 9100 行附近
static int tcp_read_sock(struct sock *sk, struct msghdr *msg, int len,
int nonblock)
{
// ...
// 从接收缓冲区读取数据
copied = skb_copy_datagram_msg(skb, 0, msg, len);
// ...
return copied;
}
tcp_read_sock
函数通过调用 skb_copy_datagram_msg
函数从接收缓冲区读取数据,并将数据复制到 msg
结构体中。
六、路由表的结构和初始化过程
在 Linux 内核中,路由表用于确定将网络数据包从源地址传输到目标地址的路径。
- 路由表的结构:
// 文件:include/net/route.h
// 位置:约在 200 行附近
struct rtable {
// ...
struct net_device *dst.dev; // 目标设备
__be32 rt_dst; // 目标 IP 地址
__be32 rt_gateway; // 网关 IP 地址
unsigned int rt_pmtu; // 最大传输单元
// ...
struct fib_nh_common nhc;
// ...
};
rtable
结构体表示路由表的一条路由项。其中包含了目标设备、目标 IP 地址、网关 IP 地址、最大传输单元等信息。
-
路由表的初始化过程:
// 文件:net/ipv4/route.c
// 函数:ip_route_input_noref
// 位置:约在 1000 行附近
struct rtable *ip_route_input_noref(struct net *net, __be32 daddr,
__be32 saddr, u8 tos, struct net_device *dev)
{
struct rtable *rt;
// ...
// 查找匹配的路由项
rt = __ip_route_output_key(net, daddr, saddr, tos, dev);
if (IS_ERR(rt)) {
// 处理无法找到匹配路由项的情况
return NULL;
}
// ...
return rt;
}
在上述代码中,ip_route_input_noref
函数用于在输入路径上查找匹配的路由项。它调用 __ip_route_output_key
函数来查找路由表中与目标 IP 地址最匹配的路由项。
- 查找路由项的具体实现:
// 文件:net/ipv4/route.c
// 函数:__ip_route_output_key
// 位置:约在 2000 行附近
struct rtable *__ip_route_output_key(struct net *net, __be32 daddr,
__be32 saddr, u8 tos, struct net_device *dev)
{
struct rtable *rt;
// ...
// 遍历路由缓存进行查找
rt = ip_route_cache_lookup(net, daddr, saddr, tos, dev);
if (rt) {
// 找到匹配的路由项
return rt;
}
// ...
// 如果缓存中没有找到匹配项,则调用路由查找函数
rt = ip_route_output_key_hash(net, daddr, saddr, tos, dev, 0, 0);
// ...
return rt;
}
在上述代码中,__ip_route_output_key
函数首先尝试从路由缓存中查找匹配的路由项。如果路由缓存中没有找到匹配项,则调用 ip_route_output_key_hash
函数进行实际的路由查找。
七、通过目的IP查询路由表的到下一跳的IP地址的过程
通过目的 IP 查询路由表获取下一跳 IP 地址的过程,可以通过以下步骤完成:
-
获取目的 IP 地址。
-
遍历路由表的条目,逐个比较目的 IP 地址与路由表中的目标地址。
-
对于每个路由表条目,检查是否匹配目的 IP 地址。匹配通常涉及子网掩码和最长前缀匹配。
-
如果匹配成功,获取路由表条目中的下一跳 IP 地址。
// 文件:net/ipv4/route.c
// 函数:ip_route_output_key_hash
// 位置:约在 2400 行附近
struct rtable *ip_route_output_key_hash(struct net *net, __be32 daddr,
__be32 saddr, u8 tos,
struct net_device *dev,
int oif, int hash)
{
// ...
struct fib_result res;
// 查询路由表
fib_lookup(net, &fl, &res);
// 检查是否匹配目的 IP 地址
if (!res.fi) {
// 没有匹配的路由项
return NULL;
}
// 获取下一跳 IP 地址
if (res.type == RTN_UNICAST) {
// 单播路由
struct fib_nh *nh = fib_info_nh(res.fi, res.table);
next_hop = nh->nh_gw;
} else {
// 非单播路由,如多播、广播等
// 处理方法根据需要进行调整
// ...
}
// ...
return rt;
}
在上述代码中,ip_route_output_key_hash
函数调用了 fib_lookup
函数查询路由表,并根据查询结果判断是否匹配到目的 IP 地址。如果匹配成功,根据路由类型获取下一跳 IP 地址。
八、ARP缓存的数据结构及初始化过程,包括ARP缓存的初始化
ARP(Address Resolution Protocol)缓存用于存储IP地址与MAC地址之间的映射关系,以便在网络通信中进行地址解析。下面是一个简化的示例源代码,展示了ARP缓存的数据结构及初始化过程。
- ARP缓存的数据结构:
// 文件:include/net/arp.h
// 位置:约在 100 行附近
struct neighbour {
// ...
struct hh_cache hh;
// ...
u8 nud_state;
// ...
u8 ha[HARDWARE_ADDRESS_LEN]; // MAC地址
// ...
__be32 primary_key; // IP地址
// ...
struct timer_list timer;
// ...
};
在上述代码中,neighbour
结构体表示ARP缓存的一项。其中包含了MAC地址、IP地址等信息。
- ARP缓存的初始化过程:
// 文件:net/ipv4/arp.c
// 函数:arp_init
// 位置:约在 200 行附近
void arp_init(void)
{
// ...
// 创建ARP缓存
arp_tbl = neigh_table_init(&arp_tbl_parm);
if (!arp_tbl) {
printk(KERN_INFO "ARP: Failed to allocate memory for ARP table\n");
return;
}
// ...
return;
}
在上述代码中,arp_init
函数用于初始化ARP缓存。它调用 neigh_table_init
函数创建ARP缓存,并将参数传递给该函数进行初始化。
九、如何将IP地址解析出对应的MAC地址
在Linux内核中,将IP地址解析为对应的MAC地址主要通过ARP(Address Resolution Protocol)实现。大致的执行过程如下:
-
用户空间调用套接字API发送一个IP数据包。
相关函数:sendto()
、connect()
等。
相关文件:net/socket.c
、net/ipv4/raw.c
等。
-
数据包到达网络层,内核检查目标IP地址是否在本地ARP缓存中,ARP缓存中存储了IP地址和对应的MAC地址。
相关数据结构:struct neighbour
、struct neighbour_table
等。
相关函数:neigh_lookup()
等。
相关文件:net/core/neighbour.c
、include/net/neighbour.h
等。
- 如果目标IP地址不在本地ARP缓存中,内核创建并发送一个ARP请求消息。
相关函数:arp_create()
、arp_send()
等。
相关文件:net/ipv4/arp.c
等。
- ARP请求消息封装到数据链路层帧中,通过网络接口发送出去。
相关函数:neigh_xmit()
等。
相关文件:net/core/neighbour.c
、net/ipv4/devinet.c
等。
-
目标主机收到ARP请求帧后,根据请求中的IP地址判断是否为自己的IP地址,并构建一个ARP响应消息发送回请求方。
相关函数:arp_rcv()
等。
相关文件:net/ipv4/arp.c
等。
-
请求方收到ARP响应帧,提取MAC地址并将其存储到本地ARP缓存中。
相关函数:neigh_update()
等。
相关文件:net/core/neighbour.c
、include/net/neighbour.h
等。
-
请求方将数据包发送到目标MAC地址。
相关函数:dev_queue_xmit()
等。
相关文件:net/core/dev.c
等。
十、跟踪TCP send过程中的路由查询和ARP解析的最底层实现
- send过程中的路由查询
这件事情主要由 ip_route_output_flow 函数完成,调用链 为:ip_route_output_flow->__ip_route_output_key->ip_route_output_key_hash->ip_route_output_key_hash_rcu
ip_route_output_key_hash_rcu
是 Linux 内核中用于根据给定的散列键值进行路由查找和输出的函数。它是 IPv4 路由子系统的一部分,用于确定给定目标 IP 地址的下一跳。
struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
// ...
err = fib_lookup(net, fl4, res, 0);
// ...
make_route:
rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
// ...
}
fib_lookup
是 Linux 内核中用于进行 FIB(Forwarding Information Base,转发信息库)查找的函数之一。它用于根据给定的目标地址查找匹配的路由表项。
static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
{ struct fib_table *tb;
// ...
tb = fib_get_table(net, RT_TABLE_MAIN);
if (tb)
err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
// ...
}
- ARP解析的最底层实现
__ipv4_neigh_lookup_noref
函数用于在本地 ARP 表中查找下一跳的 MAC 地址。该函数接收两个参数:dev
表示网络设备,dst
表示目标 IP 地址。它通过调用__neigh_lookup_noref
函数在 ARP 表中查找与目标 IP 地址匹配的邻居项(neighbour entry)。它接收邻居表 tbl
、目标 IP 地址 pkey
和网络设备 dev
作为参数,返回一个指向邻居项结构体的指针。
若查找失败,则会调用 __neigh_create
函数。
void create_neigh_entry(struct net_device *dev, __be32 dst_ip, unsigned char *dst_mac)
{
struct neighbour *neigh;
// 创建邻居项
neigh = __neigh_create(&arp_tbl, &dst_ip, dev, false);
if (!neigh) {
// 邻居项创建失败,处理错误情况
return;
}
// 设置邻居项的 MAC 地址
memcpy(neigh->ha, dst_mac, ETH_ALEN);
// 将邻居项添加到邻居表
neigh_add(neigh, false);
}
它接收网络设备 dev
、目标 IP 地址 dst_ip
和目标 MAC 地址 dst_mac
作为参数。函数首先调用 __neigh_create
函数创建一个新的邻居项,并指定邻居表 arp_tbl
、目标 IP 地址和网络设备。然后,它将目标 MAC 地址拷贝到邻居项的 ha
字段中,并通过调用 neigh_add
函数将邻居项添加到邻居表中。
在 __neigh_create 之后,会调用 neigh_output 发送网络包。neigh_output调用neigh_resolve_output。neigh_resolve_output
函数是用于发送网络包的关键函数之一,它用于解析目标 IP 地址的邻居项,并将网络包发送到相应的邻居。
neigh_resolve_output
函数的主要作用是将网络包发送给指定的邻居。它通常会执行以下步骤:
-
验证邻居项的状态:检查邻居项的状态,例如是否是有效的、是否已解析等。如果邻居项状态不满足发送条件,可能需要进行邻居探测或其他操作来解析邻居项。
-
设置网络包的目标 MAC 地址:从邻居项中获取目标 MAC 地址,并将其设置为网络包的目标 MAC 地址,以便正确路由和传递给下一跳。
-
发送网络包:将网络包发送到网络设备的发送队列,以便进行实际的数据传输。发送过程可能涉及网络设备驱动程序的调用,通过底层的网络协议栈将数据封装成数据帧并发送出去。
int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb) {
if (neigh->nud_state != NUD_VALID) {
// 邻居项状态不满足发送条件,可能需要进行邻居探测或其他操作
// ...
}
// 设置网络包的目标 MAC 地址
skb->mac_dest = neigh->ha;
// 发送网络包
int ret = dev_queue_xmit(skb);
return ret;
}