背景介绍:
1. 数据包输入:
以非NAPI 11 内核收包为例,
1)驱动收包中断回调e1000_intr,会调用
e1000_clean_rx_irq
2) e1000_clean_rx_irq从硬件获取网络数据,填充skb后,通过netif_rx(skb);将其放入接受数据缓冲队列:
即本cpu的softnet_data结构里的input_pkt_queue队列。等待下一步软中断对缓冲进行处理。
3) 链接skb到队列的模式是,如果input_pkt_queue队列未满,则将skb链到队尾,返回;如果input_pkt_queue队列是空,则先通过netif_rx_schedule激活
收包软中断net_rx_action,再把skb链进队列。
3.1) tricky : 这里我们埋下了一个伏笔,为什么如果接受数据缓冲队列未满,就只是把skb链接进去,而不去尝试激活软中断?
后面我们看了软中断的处理策略再表。
4) 好,我们回来进入收包软中断net_rx_action。
此函数的作用就是上传数据到协议栈。
具体的就是取当前cpu的softnet_data结构, 遍历此softnet_data上的poll_list链表,找出需要被轮询的网卡设备,然后根据轮询到的dev,调用dev->poll尝试将
缓冲里的数据上传至协议栈。
这一步有2点提请注意,
4.1) softnet_data上的poll_list链表里dev的来源,是在第3步网卡驱动收包时通过netif_rx_schedule将dev添加到链表里的;
4.1) softnet_data是所有网卡设备共享的结构,假如eth0是e1000,eth1是3c59x,如果中断都在同一个cpu,那么他们第2步收到的
数据都放在这个cpu的队列上,net_rx_action并不做区分
5) dev->poll在非NAPI下,默认被初始化为process_backlog,该函数遍历本cpu上的softnet_data的input_pkt_queue,将skb挨个取出,
通过netif_receive_skb(skb)上传给协议栈。
此函数运行也有时限,当队列已经为空,或设备配额已经用完,或函数运行了足够长的时间,则退出到net_rx_action。
在net_rx_action的循环里,很可能再次将此设备插入到poll_list,等待下一次轮询。
在net_rx_action里,如果单次遍历poll_list的时间不超过1个tick,或者单次process_backlog里取出的skb不超过netdev_max_backlog个,则会将此dev再次
加入poll_list的尾端。那么,什么时候会将poll_list里的dev删除? 当然是process_backlog将input_pkt_queue里的skb都处理完毕后。
5.1) 这里我们再回来考虑3.1,如果接受数据缓冲队列input_pkt_queue未满,且不为0,
则说明收包软中断正在遍历此链表进行处理,或者已经处理了一遍但还没有处理完,等待下次处理,因此不需要去激活软中断。
上传到协议栈之后,如何处理? 假设我们上传的是一个TCP报文,则回溯如下:
[<ffffffff8101d5e7>] tcp_rcv_established+0x37/0x9d4
[<ffffffff8103f58e>] tcp_v4_do_rcv+0x2e/0x338
[<ffffffff8102a811>] tcp_v4_rcv+0x971/0xa14
[<ffffffff81037f0e>] ip_local_deliver+0x193/0x275
[<ffffffff810392c5>] ip_rcv+0x4c5/0x51b
[<ffffffff81022d0b>] netif_receive_skb+0x38b/0x426
[<ffffffff81033fb0>] process_backlog+0x9a/0x10a
[<ffffffff8100c908>] net_rx_action+0xd1/0x209
[<ffffffff810a1737>] ___do_softirq+0xc7/0x1b7
[<ffffffff81013209>] __do_softirq+0x3e/0x65
[<ffffffff810642ac>] call_softirq+0x1c/0x28
[<ffffffff81072bc7>] do_softirq+0x34/0x9d
[<ffffffff810a1a68>] irq_exit+0x3f/0x41
[<ffffffff8107fc1f>] smp_apic_timer_interrupt+0x3f/0x51
[<ffffffff81063d56>] apic_timer_interrupt+0x66/0x70
tcp_rcv_established里 通过__skb_queue_tail(&sk->sk_receive_queue, skb);
将skb放到sk_receive_queue,之后通过sk->sk_data_ready(sk, 0);唤醒等待的读者
读者此时阻塞在sk_wait_data,等待软中断通知他sk_receive_queue,非空,
之后读者会从sk_receive_queue,里peek一个skb来处理。
[<c0008b7c>] __switch_to+0xbc/0x114
[<c02925e0>] sk_wait_data+0x8c/0xec
[<c02e4ac8>] tcp_recvmsg+0x520/0x838
[<c0290d2c>] sock_common_recvmsg+0x3c/0x60
[<c028ebec>] sock_aio_read+0x110/0x118
[<c00a784c>] do_sync_read+0xc4/0x138
[<c00a83a4>] vfs_read+0x14c/0x154
[<c00a877c>] sys_read+0x4c/0x90
[<c0010a48>] ret_from_syscall+0x0/0x3c
关于网络包的接受,就讨论到这里。
2. 数据包的输出:
不管外层是sendto,send,还是sendmsg,走的都是sys_sendmsg(socket.c)
首先将用户态填充的msghdr结构复制到内核态。
这个msghdr是个啥东西?msghdr里包含要发送的数据,目的地址等信息。
也就是说,msghdr是只和发送数据有关,而和接受数据无关。
struct msghdr{
void* msg_name; // sock_addr结构目的地址指针
int msg_namelen; //目的地址或者源地址缓冲区大小
msg_iov =
struct iovec[msg_iovlen]{ //发送数据缓冲区数组,可以看出一个报文可以由多个缓冲区构成
void* iov_base;
int iov_len;
}
msg_control =
struct cmsghdr[msg_controllen]{
int cmsg_level; //SOL_SOCKET,SOL_IP等
cmsg_type; //IP_RETOPTS,IP_TTL
}
}
以udp发送为例,sys_sendmsg经过一系列检查后走udp_sendmsg
此函数先查路由,再根据查到的路由信息发送数据。
查路由的函数为ip_route_output_flow,此函数先查路由缓存(位于rt_hash_table)
如果没有查到,则走慢速查找ip_route_output_slow
对于发送缓冲,不得不多说两句。缓冲大小主要反映在sock的sk_wmem_alloc字段。对于TCP类型的包来说,可以通过
/proc/sys/net/ipv4/tcp_wmem_default以及/proc/sys/net/ipv4/tcp_wmem_max来控制
而对于packet类型的包来说,则是通过/proc/sys/net/core/wmem_default和/proc/sys/net/core/wmem_max来控制。
以packet为例:
[<c0007078>] __switch_to+0x40/0x8c
[<c025d34c>] sock_alloc_send_skb+0x160/0x260
[<c02df0b8>] packet_sendmsg+0x100/0x274
[<c025aae0>] sock_sendmsg+0x10c/0x13c
[<c025bfd8>] sys_sendto+0xcc/0x100
[<c025c370>] sys_socketcall+0x150/0x1f0
[<c000e6d4>] ret_from_syscall+0x0/0x3c
packet_sendmsg里,sock_alloc_send_skb会判断当前sock,已发送但没回收的sk_wmem_alloc缓存,
是否比上限sk->sk_sndbuf小,如果小的话,就允许发送,并在skb_set_owner_w里将sk_wmem_alloc递增
skb->len长度,在接下来的任意一步流程失败,就走到out_free调用kfree_skb(skb);将skb释放,并递减sk_wmem_alloc以
skb->len长度。
在继续之前,先说明一下路由的作用。
1) 对于网络上的每台主机,他都会收到局域网内不及其数的报文,只有报文里的mac字段和本机
mac相等的报文,主机网卡才会接收。这个是网卡硬件保证。
2) 当内网的一个主机A ,给主机B发送报文时,会先查路由表,发现是同一个网段,
于是为直连路由,根据arp表找到(B.ip,B.mac),将数据填充为(B.ip,B.mac)发送出去,
B就会自动过滤到此报文
3) 当内网的一个主机A,给外网C发送报文时,查A本机路由表的各个条目发现没有匹配的,则把数据报文发送给
网关路由R。数据报文的格式为(C.ip,R.mac), 网关路由器R根据报文的C.ip去查自己的路由表(数量巨大),
找到下一条路径,需要把报文发送给另外一个承载路由器r1, 于是把报文格式改为(C.ip,r1.mac),往下依次
发送。
具体的路由查找可以见 http://blog.csdn.net/qy532846454/article/details/6423496。