Linux网络收报原理
Linux启动阶段
软中断处理线程启动
创建内核线程ksoftirqd。
c
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u"
}
static __init int spawn_ksoftirqd(void)
{
register_cpu_notifier(&cpu_nfb);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
}
early_initcall(spawn_ksoftirqd);
网络子系统初始化
网络子系统初始化将做的事情
- 为每个CPU初始化softnet_data;
- 为软中断RX_SOFTIRQ和TX_SOFTIRQ注册处理函数;
具体执行
Linux内核初始化通过调用subsys_initcall实现,目前关心的网络子系统初始化通过函数net_dev_init实现。
c
static int __init net_dev_init(void)
{
......
for_each_possible_cpu(i){
struct softnet_data *sd = &per_cup(softnet_data,i);
memset(sd, 0, sizeof(*sd));
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
sd->completion_queue = NULL;
INIT_LIST_HEAD(&sd->poll_list);
......
}
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
}
subsys_initcall(net_dev_init);
这个函数的功能是为每个cpu申请一个softnet_data数据。每个softnet_data里面都有一个poll_list,网卡驱动在起来以后会将网卡的poll函数注册进来。14和15行的open_softirq会为每个软中断注册一个处理函数(理解起来感觉就像是用户空间的信号处理函数)。
c
void open_softirq(int nr, void (*action)(struct softirq_action*))
{
softirq_vec[nr].action = action;
}
软中断的枚举在include/linux/interrupt.h中有定义,如下:
c++
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
NR_SOFTIRQS
}
我们就可以看到open_softirq的实现就很简单,就是在softirq_vec这个数组中信号对应序号的位置写上软中断处理函数的指针,仅此而已。
协议栈注册
Linux系统的协议栈注册是通过fs_initcall(inet_init)实现的。这个fs_initcall调用传入的inet_init函数进行协议栈初始化,将协议注册到inet_protos和ptype_base数据结构中。
c
static struct packet_type ip_packet_type __read_mostly ={
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};
static const struct net_protocol udp_protocol = {
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
static const struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
};
static int __init inet_init(void)
{
......
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
......
dev_add_pack(&ip_packet_type);
}
看到static struct packet_type ip_packet_type __read_mostly
这个变量声明我也是很懵逼的。后来查了资料,__read_mostly
实际是一个关键字,表示这个变量主要是用来被读取的。
!tip
这里就有一个不太理解的,系统是怎么知道先调用ip的解包函数ip_rcv,然后再根据需要调用udp或者tcp的解包函数的咧?
应用协议注册函数inet_add_protocol
以上那些协议解包函数都是通过inet_add_protocol这个函数注册进去的,这个函数实现如下:
c
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
if (!prot->netns_ok){
pr_err("Protocol %u is not namespace aware, cannot register.\n", protocol);
return -EINVAL;
}
return !cmpxchg((const struct net_protocol**)&inet_protos[protocol], NULL, prot)?0:-1;
}
这里面就是用了一个原子操作将inet_protos下标为对应协议号的结构体指针替换为传入的指针,很简洁。
IP注册函数
ip是通过注册函数dev_add_pack注册进去的,看一下实现:
c
void dev_add_pack(struct packet_type* pt)
{
......
struct list_head *head = ptype_head(pt);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
实际就是调用了ptype_head并且返回了head,这个ptype_head主要做的就是在ptype_base中找到packet_type.type字段(IP协议传入的是cpu_to_be16(ETH_P_IP))这个转换成本机short类型的序号。
!tip
总结一下,IP协议的处理函数会放到ptype_base这个数组中,索引和类型ETH_P_IP相关;应用协议会放到inet_protos数组中,索引就是IPPROTO_TCP/IPPROTO_UDP/IPPROTO_ICMP这种宏。
网卡驱动
网卡驱动初始化
每个驱动程序(不仅仅是网卡驱动)会使用module_init向内核注册一个初始化函数,当驱动程序被加载时,内核会调用这个函数。比如igb网卡驱动程序的代码:
c
static struct pci_driver igb_driver = {
.name = igb_driver_name,
.id_table = igb_pci_tbl,
.probe = igb_probe,
.remove = igb_remove,
......
};
static int __init igb_init_module(void)
{
......
ret = pci_register_driver(&igb_driver);
return ret;
}
网卡设备被识别之后就会调用注册的probe函数,也就是igb_probe。这个函数主要做以下一些事情来使网卡进入ready状态:
- 获取硬件的MAC地址;
- DMA初始化;
- 注册ethtool实现函数(相当于ethtool命令对客户提供虚函数,由网卡驱动进行了实现);
- 注册net_device_ops、netdev等变量;(ops变量包含igb_open函数,用于在网卡启动时调用)
- NAPI初始化,注册poll函数;
net_device_ops对于igb网卡来说是实例化了一个全局变量igb_netdev_ops
如下:
c
static const struct net_device_ops igb_netdev_ops = {
.ndo_open = igb_open,
.ndo_stop = igb_close,
.ndo_start_xmit = igb_xmit_frame,
.ndo_get_stats64 = igb_get_stats64,
.ndo_set_rx_mode = igb_set_rx_mode,
.ndo_set_mac_address = igb_set_mac,
.ndo_change_mtu = igb_change_mtu,
.ndo_do_ioctl = igb_ioctl,......
};
igb_open函数在初始化的过程中会调用igb_alloc_q_vector
函数,这个函数用来注册一个NAPI机制
必须的poll函数,对于igb网卡来讲就是igb_poll。
c
static int igb_alloc_q_vector(...)
{
......
netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);
}
网卡启动
在igb_probe这一步中,设置了net_device_ops变量。这个变量包含启动、发包、设置MAC地址等回调函数。启动算法时候会调用net_device_ops.ndo_open方法,对于igb网卡来讲就是igb_open函数:
c
static int __igb_open(struct net_device *netdev, bool resuming)
{
// 分配传输描述符数组
err = igb_setup_all_tx_resources(adapter);
// 分配接收描述符数组
err = igb_setup_all_rx_resources(adapter);
// 注册中断处理函数
err = igb_request_irq(adapter);
if (err)
goto err_req_irq;
// 启用NAPI
for (i = 0; i < adapter->num_q_vectors; i++)
napi_enable(&(adapter->q_vector[i]->napi));
......
}
igb_setup_all_rx_resources这一步会分配RingBuffer并且建立内存和RX队列的映射关系:
c
static int igb_setup_all_rx_resources(struct igb_adapter *adapter)
{
...
for (i = 0; i < adapter->num_rx_queues; i++){
err = igb_setup_rx_resources(adapter->rx_ring[i]);
...
}
return err;
}
对adapter(适配器)中的rx_ring中的每一个,调用igb_setup_rx_resources进行分配资源。每个rx_ring的元素是一个环形接收对接。下面来看一下分配资源的函数igb_setup_rx_resources:
c
int igb_setup_rx_resources(struct igb_ring *tx_ring)
{
//1. 申请igb_rx_buffer数组内存
size = sizeof(struct igb_rx_buffer) * rx_ring->count;
rx_ring->rx_buffer_info = vzalloc(size);
//2. 申请e1000_adv_rx_desc DMA数组内存
rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc);
rx_ring->size = ALIGN(rx_ring->size, 4096);
rx_ring->desc = dma_alloc_coherent(dv, rx_ring->size, &rx_ring->dma, GFP_KERNEL);
//3. 初始化队列成员
rx_ring->next_to_alloc = 0;
rix_ring->next_to_clean = 0;
rx_ring->next_to_user = 0;
return 0;
}
可以看到一个igb_ring中即有rx_buffer_info,同时也有dma数组。下面看看__igb_open中的下一步igb_request_irq(注释是"注册中断处理函数",这里是驱动,注册的是硬中断的处理函数):
c
static int igb_request_irq(struct igb_adapter *adapter)
{
if (adapter->msix_entries)
{
err = igb_request_msix(adapter);
if (!err)
goto request_done;
......
}
}
static int igb_request_msix(struct igb_adapter *adapter)
{
......
for (i = 0; i < adapter->num_q_vectors; i++)
{
......
err = request_irq(adapter->msix_entries[vector].vector, igb_msix_ring, 0, q_vector->name,)
}
}
adapter->num_q_vectors是个老演员了,__igb_open函数最后一步启用NAPI也用到了这个。那么这个注册到底干了什么事情,就是给硬中断增加了一个处理函数igb_msix_ring吗?
网卡接收数据
网卡驱动干活
网卡在接收到数据帧以后会将数据DMA到系统内存,然后触发硬中断。网卡驱动注册了硬中断的处理函数是igb_msix_ring,那么这个硬中断的处理细节到底是如何呢?如下:
c
static irqreturn_t igb_msix_ring(int irq, void * data)
{
struct igb_q_vector *q_vector = data;
/* write the ITR value calculated from the previous interrupt. */
igb_write_itr(q_vector);
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
第二个函数____napi_schedule中我们看到了老演员softnet_data.poll_list,这个变量是per-cpu的,是在网络子系统初始化时候调用net_dev_init函数初始化的。
ksoftirqd内核线程处理软中断
ksoftirqd会运行函数run_ksoftirqd,如下:
c
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()){
__do_softirq();
...
}
local_irq_enable();
}
asmlinkage void __do_softirq(void)
{
do{
if (pending & 1){
unsigned int vec_nr = h - softirq_vec;
int pre_count = preempt_count();
...
trace_softirq_entry(vec_nr);
h->action(h);
trace_softirq_exit(vec_nr);
...
}
h++;
pending >>= 1;
}while (pending);
}
看到第19行大家应该还记得在net_dev_init函数中,我们调用了open_softirq为每个信号注册了处理的函数,其中就包含软中断NET_RX_SOFTIRQ的处理函数net_rx_action,就是放在softirq_vec数组NET_RX_SOFTIRQ对应索引的action函数指针中的。
c
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = &__get_cpu_var(softnet_data);
unsigned long time_limit = jiffies + 2;
int budget = netdev_budget;
void *have;
local_irq_disable();
while (!list_empty(&sd->poll_list)){
......
n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)){
work = n->poll(n, weight);
trace_napi_poll(n);
}
budget -= work;
...
}
}
在这个接收消息的处理函数中我们看到了很多老演员,复习一下:
- softirq_data.poll_list:在Linux网络子系统初始化时候通过net_dev_init函数创建的,是一个per-cpu的变量;在接收到报文的时候通过硬中断处理函数igb_msix_ring-->____napi_schedule-->list_add_tail添加的变量;
这个net_rx_action的poll函数实际是在网卡启动时候的igb_open-->igb_alloc_q_vector-->netif_napi_add增加进去的igb_poll函数。读到这里,我也能够理解____napi_schedule函数的第二个入参是哪里来的了,实际就是q_vector->napi,也就是adapter->q_vector[i]->napi。
接收函数会调用每个网卡的poll函数即n->poll(n,weight)
这句。对于igb网卡来讲,就是igb_poll函数:
c
static int igb_poll(struct napi_struct *napi, int budget)
{
...
if (q_vector->tx.ring)
clean_complete = igb_clean_tx_irq(q_vector);
if (q_vector->rx.ring)
clean_complete &= igb_clean_rx_irq(q_vector, budget);
...
}
读取操作中,igb_poll的重点工作是对igb_clean_rx_irq的调用。
c
static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget){
...
do{
/* retrieve a buffer from the ring */
skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);
/* fetch next buffer in frame if non-eop */
if (igb_is_non_eop(rx_ring, rx_desc))
continue;
/* verify the packet layout is correct */
if (igb_cleanup_headers(rx_ring, rx_desc, skb))
{
skb = NULL;
continue;
}
/* populate checksum, timestamp, VLAN, and protocol */
igb_process_skb_fields(rx_ring, rx_desc, skb);
napi_gro_receive(&q_vector->napi, skb);
...
} while (likely(total_packets < budget));
}
这个函数通过igb_fetch_rx_buffer和igb_is_non_eop来收取完整的数据帧。这里出于的考虑是一个数据帧可能会包含多个RingBuffer,所以需要每次通过igb_is_non_eop来判断一下是不是已经到了数据帧的结尾了,如果没有就continue,不进行以下的操作。
当数据帧收取完毕之后会进行一个头校验,然后是通过igb_process_skb_fields函数设置skb的timestamp、VLAN id、protocol等字段。然后是关键的igb_process_skb_fields函数了:
c
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
skb_gro_reset_offset(skb);
return napi_skb_finish(dev_gro_receive(napi, skb), skb);
}
static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
switch (ret) {
case GRO_NORMAL:
if (netif_receive_skb(skb))
ret = GRO_DROP;
break;
......
}
}
在以上函数的netif_receive_skb函数中,数据包将被发送到协议栈中进行处理。以UDP来举例,netif_receive_skb函数会首先调用ip_rcv函数进行IP层的解包(这个是在协议栈注册的inet_init函数中写进ptype_base数组的);然后再通过udp_rcv函数解析udp协议。看一下代码,书接上文netif_receive_skb实现如下:
c
int netif_receive_skb(struct sk_buff *skb)
{
// RPS处理逻辑,先忽略
......
return __netif_receive_skb(skb);
}
static int __netif_receive_skb(struct sk_buff *skb)
{
......
ret = __netif_receive_skb_core(skb, false);
}
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
......
// pcap逻辑,这里会将数据送入抓包点。tcpdump就是从这个入口取包
list_for_each_entry_rcu(ptype, &ptype_all, list){
if (!ptype->dev || ptype->dev == skb->dev){
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
......
list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list){
if (ptype->type == type
&& (
ptype->dev == null_or_dev
|| ptype->dev == skb->dev
|| type->dev == orig_dev
)
){
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
}
tcpdump命令会把抓包函数以虚拟协议的方式挂到ptype_all上。所以说tcpdump可以抓到ip层的头。
来个急转弯,讨论一下tcpdump。tcpdump会执行到packet_create:
c
static int packet_create(struct net *net, struct socket *sock, ...)
{
...
po->prot_hook.func = packet_rcv;
if (sock->type == SOCK_PACKET)
po->prot_hook.func = packet_rcv_spkt;
po->prot_hook.af_packet_priv = sk;
register_prot_hook(sk);
}
register_prot_hook函数会把tcpdump用到的"协议"挂到ptype_all上。至此急转弯结束,回归正常协议解析过程。
还是看__netif_receive_skb_core函数,33行调用了deliver_skb是个关键:
c
static inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev)
{
......
return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
可以看到这个函数调用了pt_prev->func。这个pt_pre是ptype_base上面取下来的。我们在协议栈注册时候通过inet_init-->dev_add_pack将ip层协议解包的结构体加进去了,其中func就是ip_rcv函数指针。也就是说deliver_skb的4行就是在调用ip_rcv这个函数进行ip层的解包!
IP层的处理
我们都知道了ip_rcv是用来处理IP协议的,那么我们从关注流程的角度看一下ip_rcv函数的实现:
c
int ip_rcv(struct sk_buff *skb, ...)
{
......
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
}
NF_HOOK是一个钩子函数,ip_rcv在解析完成后就会把结果挂在钩子上,告诉操作系统发生了NFPROTO_IPV4-->NF_INET_PRE_ROUTING事件。这个时候操作系统就可以执行一些和这个事件相关的操作了。在注册完钩子之后呢,就会执行ip_rcv_finish函数:
c
static int ip_rcv_finish(struct sk_buff *skb)
{
......
if (!skb_dst(skb)){
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, skb->dev);
...
}
......
return dst_input(skb);
}
跟踪ip_route_input_noref函数就会发现,它又调用了ip_route_input_mc。在ip_route_input_mc中,函数ip_local_deliver被赋值给了dst.input。
c
static int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr, u8 tos, struct net_device *dev, int our)
{
if (our) {
rth->dst.input = ip_local_deliver;
rth->rt_flags |= RTCF_LOCAL;
}
}
回到ip_rcv_finish函数,最后执行了dst_input(skb):
c
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
skb_dst(skb)->input调用的input方法就是路由子系统赋予的ip_local_deliver。
c
int ip_local_deliver(struct sk_buff *skb)
{
if (ip_is_fragment(ip_hdr(skb))){
if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);
}
static int ip_local_deliver_finish(struct sk_buff *skb)
{
......
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
ipprot = rcv_dereference(inet_protos[protocol]);
if (ipprot != NULL) {
ret = ipprot->handler(skb);
}
}
到了这里,我们又看到了一位人民艺术家"inet_protos"。这个数组是以协议为下表的数组,那么连起来就是找到了对应的协议,并且调用了协议结构体的handler方法。以udp举例,那么调用的就是udp_rcv函数。从这里也可以看出,这个就是使用函数指针+结构体在C语言的基础上实现了多态。
总结
Linux启动阶段

- 启动软中断处理内核线程ksoftirqb;
- 注册软中断回调函数;
- 发送软中断回调
net_tx_action
; - 接收软中断回调
net_rx_action
;
- 发送软中断回调
- 注册协议栈;
- IP协议放在ptype_base数组中;
- 应用层协议放在inet_protos数组中;
其余阶段
其余阶段包含网卡初始化、硬中断触发处理、软中断触发处理、IP解包、应用层解包。这部分画图太复杂了。有兴趣可以按照之前的文字描述自己进行整理。
引用:
《深入理解Linux网络》 张彦飞