【图解】Linux网络数据包发送与接收过程
1 一、接收:UDP报文为例,以太网物理网卡
意义:了解收包流程,可以明白在哪些地方监控、修改数据包,哪些情况下数据包被丢弃。
同时对了解netfilter、iptables有利。
1.1 1.1 网卡到内存(硬中断,同时启用软中断)
1、数据包从网络传到NIC物理网卡
2、网卡数据包通过DMA写入指定内存地址,该地址由网卡驱动分配并初始化
3、网卡通过硬件中断唤醒CPU,有数据要处理了
4、CPU根据中断表,调用注册过的硬中断处理函数
5、禁用网卡的硬中断,下次收到数据直接写入内存就行了,不要去打扰CPU了
6、硬中断处理过程是不能被中断,执行时间长会导致CPU不能执行其他中断。启用软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
1.2 1.2 内核在进入协议栈前对网络报文的优化、处理(生成skb、GRO、RPS、原始套接字)
7、ksoftirqd专门负责软中断处理,网卡驱动抛出软中断,就会执行net_rx_action函数
8、net_rx_action函数调用网卡驱动里面的poll函数来逐个处理数据包
9、poll函数中网卡驱动读取内存中的数据包,数据包格式只有网卡知道
10、为了统一成内核能够设备的格式,驱动程序将数据包转换成skb格式
11、对于开启了GRO功能的内核来说,napi_gro_receive会将数据包聚合为gro包,提搞cpu处理效率
12、开启了RPS功能的话,napi_gro_receive会调用enqueue_to_backlog,将将数据包放入相应CPU核心的处理队列中(softnet_data.input_pkt_queue)
13、CPU在软中断上下文处理softnet_data.input_pkt_queue中的数据
14、没有开启RPS功能,napi_gro_receive直接让CPU调用处理数据包
15、检查数据包是否为原始套接字,如果是则拷贝一份
16、进入后续IP、UDP协议栈处理函数(后续会讲)
17、当内存中所有数据包处理完后,开启网卡硬中断,下次网卡收到数据时再通知cpu
1.3 1.3 IP协议栈(netfilter、routing)
1、ip_rcv,是IP模块的入口,首先将垃圾数据包丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
2、NF_INET_PRE_ROUTING是netfilter的钩子,可以通过iptables过滤、修改、丢弃一些数据包
3、routing是进行路由,目的地址不是本地ip,且没有开启ip转发功能,丢弃;反之,进入ip_forward函数
4、ip_forward调用netfilter的NF_INET_FORWARD钩子,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk
5、dst_output_sk使用ip层的函数将数据包发送出去
6、ip_local_deliver,如果routing目的地址是本地ip,调用NF_INET_LOCAL_IN的钩子,通过则发给UDP
1.4 1.4 UDP协议栈(找skb,检查socket,数据包加入队列)
1、udp_rcv是UDP模块的入口,调用__udp4_lib_lookup_skb,该函数根据目的IP和端口找到对应的socket,如果没有找到就丢弃
2、sock_queue_rcv_skb,(1)检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包 (2)调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃
3、__skb_queue_tail,将数据包放在socket接收队列的末尾
4、sk_data_ready,通知socket数据包已经ok
5、 调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。
1.5 1.5 socket
应用层两种方法接收数据:
1、recvfrom阻塞,等待数据,socket收到通知后,recvfrom被唤醒
2、epoll、select监听对应的socket,收到通知后调用recvfrom函数去读取接收队列的数据
2 二、发送:以Kernel3.13.0,UDP报文为例,以太网物理网卡
2.1 2.1 用户态socket发包
1、创建socket,初始化相应的操作函数,这里初始化UDP相关函数
2、应用程序调用sendto发送数据包
3、sendto调用inet_sendmsg,检查当前socket是否绑定了源端口,没有的话就就用inet_autobind自动分配一个
4、inet_autobind函数调用socket上绑定的get_port获取一个可用端口,get_port调用UDP代码里的相应函数
2.2 2.2 UDP层
1、UDP模块的数据包发送入口 udp_sendmsg,该函数较长
2、ip_route_output_flow,获取路由信息(源IP、网卡)
如果socket没有绑定源IP,函数会根据路由表找到一个最合适的源IP;
如果socket绑定了源IP,根据路由表,无法源IP对应网卡无法达到目的地址,报文就会被丢弃,sendto发送失败
该函数最后会将设备、源ip,写入flowi4结构体
3、ip_make_skb,构造skb包,IP包头的源IP在这里被设置进去
调用__ip_append_dat,如果需要分片,就在这里面分片
该函数还会检查socket的send buffer是否用光,如果用光则返回ENOBUFS
4、udp_send_skb(skb,fl4),往skb中填充UDP头,处理checksum,进入IP层
2.3 2.3 IP层
1、IP模块的数据包发送入口,ip_send_skb,该函数只是为了调用后续函数
2、__ip_local_out_sk,设置IP报文头长度和checksum
3、NF_INET_LOCAL_OUT,用于处理本地发出的网络数据包的函数链,根据不同的协议(ipv4、ipv6、原始套接字)调用不同的处理函数
4、dst_output_sk,根据skb信息,调用其中的output函数,UDP_IPv4情况下,会调用ip_output
5、ip_output,将udp_sendmsg网卡信息写入skb
6、NF_INET_POST_ROUTING,用户配置了SNAT网络地址转换,可能导致skb路由信息变化
7、ip_finish_output,判断路由信息是否变化
变化了,重新调用(4)dst_output_sk,重新调用这个函数时,可能就不会再走到ip_output,而是走到被netfilter指定的output函数里,这里有可能是xfrm4_transport_output
8、ip_finish_output2,为了获取目标主机的MAC地址,以便正确地发送数据包到目标主机:路由表找下一跳,再调用__ipv4_neigh_lookup_noref去arp表中找下一跳的neigh信息(网络设备邻居信息),没找到的话会调用__neigh_create构造一个空的neigh结构体
9、dst_neigh_output
如果上一步ip_finish_output2没得到neigh信息,走到函数neigh_resolve_output中
如果得到了,直接走到neigh_hh_output,在该函数中,会将neigh信息里面的mac地址填到skb中
10、neigh_resolve_output,发送arp请求,得到下一跳的mac地址,填充到skb中并进入netdevice层
2.4 2.4 设备无关层
1、dev_queue_xmit,网络设备层的的入口,首先获取与设备相关联的队列调度器(qdisc)。
如果设备没有队列调度器,比如loopback设备、IP隧道设备,就直接调用dev_hard_start_xmit发包
2、Traffic Control,对网络流量进行控制和调度,例如进行流量控制、限速、队列管理等操作,如果队列满了包会被丢掉
3、dev_hard_start_xmit,直接将数据包发送到设备驱动程序
首先是
拷贝一份skb给“packet taps”(tcpdump),随后调用ndo_start_xmit
如果dev_hard_start_xmit返回错误的话(大部分情况可能是NETDEV_TX_BUSY)
调用它的函数会把skb放到一个地方,然后抛出软中断NET_TX_SOFTIRQ,交给软中断处理程序net_tx_action稍后重试(如果是loopback或者IP tunnels的话,失败后不会有重试的逻辑)
4、ndo_start_xmit, 这是一个函数指针,会指向具体驱动发送数据的函数
2.5 2.5 驱动层
网卡驱动的处理大致流程如下:
1、将skb放入网卡发送队列
2、通知网卡发送数据包
3、网卡发送完成后发送中断给CPU
4、CPU收到中断后清理skb
网卡队列如果满了,会告诉上层(驱动无关层)不需要再发了,空闲的时候再通知上层继续发
网卡驱动注册
网卡是PCI设备,采用PCI注册方法进行注册,主要数据结构定义如下
struct pci_driver {
struct list_head node;
const char *name; ——驱动程序的名称,用于对其进⾏唯一标识。当驱动程序在内核中时,它会出现在“/sys/bus/pci/driver/”下⾯
const struct pci_device_id *id_table; ——用于存储设备的描述信息,包括设备的ID、生产⼚商、PCI类型等信息
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); ——probe 是PCI驱动程序的探测函数。当新添加一个PCI设备时,该函数会被调用
void (*remove)(struct pci_dev *dev);
int (*suspend)(struct pci_dev *dev, pm_message_t state); ——suspend 当设备被挂起时,该函数会被调用。其他 resume 、 shutdown 等函数与其类似
int (*suspend_late)(struct pci_dev *dev, pm_message_t state);
int (*resume_early)(struct pci_dev *dev);
int (*resume) (struct pci_dev *dev); /* Device woken up */
void (*shutdown) (struct pci_dev *dev);
int (*sriov_configure) (struct pci_dev *dev, int num_vfs); /* On PF */
const struct pci_error_handlers *err_handler;
const struct attribute_group **groups;
struct device_driver driver;
struct pci_dynids dynids;
};
PCI驱动程序通过调用 pci_register_driver 函数来进行驱动程序的注册,该函数的参数为 struct pci_driver *drv 和 struct module *owner ,其中 owner 为注册该驱动的模块。
系统启动时,PCI模块会检测PCI总线上的PCI设备,并为每个设备创建 pci_device_id 变量。
PCI模块通过对比PCI设备的 pci_device_id 和注册的驱动程序的 pci_device_id 来调用匹配的驱动程序的 probe 函数。对于网卡驱动,该函数会创建并注册相应的网络设备。