1.net/core/dev.c
在net_dev_init中初始化两个链表:ptype_base和ptype_all
struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
struct list_head ptype_all __read_mostly; /* Taps */
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);
2.net/ipv4/af_inet.c
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
.list_func = ip_list_rcv,
};
在inet_init中初始化ip协议packet_type结构体根据ETH_P_IP类型挂载全局的ptype_base链表中
dev_add_pack(&ip_packet_type);
dev_add_pack在---net/core/dev.c中定义
ptype_head
if (pt->type == htons(ETH_P_ALL)) 放入ptype_all
else 放入ptype_base中
netif_receive_skb函数中根据收到数据包的类型为索引来遍历ptype_base数组,找到挂接的ip协议类型,然后就能把数据包交给注册的L3回调函数:ip_rcv来处理了netif_receive_skb的主要作用体现在两个遍历链表的操作中,其中之一为遍历ptype_all双向链表,这些为注册到内核的一些sniffer,将上传给这些sniffer,另一个就是遍历ptype_base hash表,这个就是具体的协议类型
当eth1接收到一个IP数据包时,它首先分别发送一份副本给每个ptype_all链表中的packet_type,它们都由package_rcv处理,然后在根据HASH值,再遍历另一个HASH表时,发送一份给
类型为ETH_P_IP的类型,它由ip_rcv处理。如果这个链表中还注册有其他IP层的协议,它也会同时发送一个副本给它.
3.net/core/dev.c
netif_receive_skb ---根据ptype_base的hash表,来选择网络层处理函数
netif_receive_skb_internal
enqueue_to_backlog if (static_branch_unlikely(&rps_needed)) ---将数据包skb放入接收队列:input_pkt_queue中
__netif_receive_skb ---上送协议栈
__netif_receive_skb_one_core
__netif_receive_skb_core
if (likely(!deliver_exact)) {deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type, &ptype_base[ntohs(type) & PTYPE_HASH_MASK]);}
*ppt_prev = pt_prev; ---就是ptype_base中的ip_packet_type(.func = ip_rcv),根据skb->protocol,协议类型获取对应数组元素
ret = INDIRECT_CALL_INET(pt_prev->func, ipv6_rcv, ip_rcv, skb, skb->dev, pt_prev, orig_dev);
4.net/ipv4/ip_input.c
ip_rcv
ip_rcv_core
ip_rcv_finish
l3mdev_ip_rcv
ip_rcv_finish_core ---实现skb与rth->dst关联
ip_route_input_noref ---net/ipv4/route.c 查找路由表,其结果会决定报文的去向
ip_route_input_rcu ---net/ipv4/route.c
ip_route_input_slow ---net/ipv4/route.c
dst_input ---include/net/dst.h
return skb_dst(skb)->input(skb); ---include/net/dst.h
skb_dst ---include/linux/skbuff.h (return (struct dst_entry *)(skb->_skb_refdst & SKB_DST_PTRMASK);)
| struct dst_entry ---include/net/dst.h
|
|
struct rtable { | ---include/net/route.h
struct dst_entry dst; ---ip_local_deliver
...
};
5.net/ipv4/route.c
ip_route_input_rcu
ip_route_input_slow
设置路由:res->type == RTN_BROADCAST --> ip_mkroute_input
ip_mkroute_input
__mkroute_input
rt_dst_alloc设置:rt->dst.output和rt->dst.input
rth->dst.input = ip_forward; 覆盖rt->dst.input
skb_dst_set(skb, &rth->dst);
本地路由:res->type == RTN_LOCAL --> rt_dst_alloc
rt_dst_alloc 设置路由表的出口函数和入口函数:rt->dst.output和rt->dst.input
rt->dst.input = ip_local_deliver; --- 本地包
skb_dst_set(skb, &rth->dst); --> 设置skb和rth下的dst关联起来
ip_local_deliver
ip_defrag 使得IP分片重组 ---net/ipv4/ip_fragment.c
ip_local_deliver_finish (netfilter:NF_INET_LOCAL_IN) ---net/ipv4/ip_input.c
ip_protocol_deliver_rcu ---net/ipv4/ip_input.c
ipprot = rcu_dereference(inet_protos[protocol]);
static struct net_protocol tcp_protocol = { ---net/ipv4/af_inet.c 在static int __init inet_init(void)中使用inet_add_protocol注册
.handler = tcp_v4_rcv,
}
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb); ---net/ipv4/ip_input.c
tcp_v4_rcv ---net/ipv4/tcp_ipv4.c
th = (const struct tcphdr *)skb->data;//tcp header
iph = ip_hdr(skb);//获取ip header
tcp_v4_do_rcv 接收数据 ---net/ipv4/tcp_ipv4.c
tcp_rcv_established ESTABLISH状态的数据包处理 ---net/ipv4/tcp_input.c
---快路径 eaten = tcp_queue_rcv(sk, skb, &fragstolen); ---数据加入接收列队,添加数据到 sk_receive_queue 中
tcp_data_ready(sk);
---慢路径 tcp_data_queue ---net/ipv4/tcp_input.c
tcp_data_ready if (!sock_flag(sk, SOCK_DEAD)) ---net/ipv4/tcp_input.c
sk->sk_data_ready(sk); ---net/ipv4/tcp_input.c
inet_create net/ipv4/af_inet.c,在socket的pf->create时调用
sock_init_data ---net/core/sock.c
sk->sk_data_ready = sock_def_readable; ---net/core/sock.c
sock_def_readable 当软中断上收到数据包时会通过调 sk_data_ready 函数指针 来唤醒在 sock 上等待的进程
rcu_dereference 获取wait queue
if (skwq_has_sleeper(wq)) 有进程在此socket的等待队列
wake_up_interruptible_sync_poll 唤醒等待队列上的进程
__wake_up_sync_key
__wake_up_common_lock
__wake_up_common
ret = curr->func(curr, mode, wake_flags, key); 此回调函数唤醒,在初始化的时候有定义DEFINE_WAIT_FUNC(wait, woken_wake_function);
inet_recvmsg ---net/ipv4/af_inet.c
err = INDIRECT_CALL_2(sk->sk_prot->recvmsg, tcp_recvmsg, udp_recvmsg, sk, msg, size, flags & MSG_DONTWAIT, flags & ~MSG_DONTWAIT, &addr_len);
tcp_recvmsg ---net/ipv4/tcp.c
skb_queue_walk(&sk->sk_receive_queue, skb 来遍历接收队列接收数据
sk_wait_data 等待队列处理 ---net/core/sock.c
DEFINE_WAIT_FUNC(wait, woken_wake_function);
.private = current, 把当前进程描述符current关联到其.private成员上
.func = function, 注册回调函数function(woken_wake_function)
.entry = LIST_HEAD_INIT((name).entry), 定义了一个等待队列wait
add_wait_queue(sk_sleep(sk), &wait); 把新定义的等待队列项wait,插入到sock对象的等待队列下
sk_sleep 获取sock对象下的等待队列列表头wait_queue_head_t
rc = sk_wait_event(sk, timeo, skb_peek_tail(&sk->sk_receive_queue) != skb, &wait); 进程将进入睡眠状态
inet_add_protocol
cmpxchg
cmpxchg(ptr, o, n)宏的作用就一目了然了。 如果 ptr 值和 old相等, 则将new赋值给ptr且返回old, 否则返回new
struct proto tcp_prot = {
tcp_v4_do_rcv
tcp_rcv_established
6.net/core/dev.c
netif_rx
netif_rx_internal
enqueue_to_backlog ---将数据包skb放入接收队列:input_pkt_queue中
__skb_queue_tail(&sd->input_pkt_queue, skb); --> 将skb添加到CPU的输入队列:input_pkt_queue
7.net/core/dev.c
net_dev_init
skb_queue_head_init(&sd->input_pkt_queue);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
net_rx_action
8.net/core/dev.c
netif_rx_ni
do_softirq
网络设备子系统的初始化:net_dev_init
1. 在proc文件系统上创建此设备相关的目录,调用:dev_proc_init
2. 在sys目录下创建网络设备的目录,调用:netdev_kobject_init
3. 为每个CPU创建一个struct softnet_data sd变量
从网卡收包到上送协议栈的两种模式:
1) 一种是传统的中断模式:即收到一个数据包,执行一次中断处理函数,在此函数中分配skb,替换有数据的skb(DMA已经将数据拷贝到初始化的skb),调用netif_rx将有
数据的skb放在percpu的队列上(如果开启了RPS,这个队列有可能是本地cpu的,也有可能是其他cpu的),最后激活软中断。之后的软中断处理函数net_rx_action中调用poll函数
process_backlog(如果将skb放在其他cpu队列上了,还需要通过ipi激活其他cpu的软中断),处理percpu队列上的数据包,上送协议栈__netif_receive_skb。中断模式会触发很多中断,
影响性能。
2) napi模式,这种模式下,一次中断可以poll收多个数据包(配额64)。具体的为收到一个中断,执行中断处理函数(比如ixgbe_msix_clean_rings),在此函数中只是激活软中断,并不处
理skb,在之后的软中断处理函数net_rx_action中调用驱动注册的poll函数,比如ixgbe_poll,来收包,上送协议栈netif_receive_skb_internal(如果开启了RPS,就会按照non-napi的
处理方式,将skb放在percpu的队列上,这个队列有可能是本地cpu的,也有可能是其他cpu的),再经过软中断处理才会将skb上送协议栈__netif_receive_skb。
虚拟网络设备中:老式的napi的poll函数就是process_backlog函数:
报文接收过程中的三个成员:
1)poll_list,网络设备dev的队列。其中的设备接收到了报文,需要被处理;
2) input_pkt_queue,skb报文结构的队里,保存已接收并需要被处理的报文;
3) struct napi_struct *napi,网络设备
内核在中断下半部中调用process_backlog函数处理数据包,其中使用process_queue处理队列,来做input_pkt_queue接收队列的中间桥梁,使在处理数据包时不必关闭本地CPU的中断。
而对input_pkt_queue队列的处理又做到了精简,提高了效率,关闭中断的时间缩短了