文章目录
内容主要参考:《深入理解Linux网络》,公众号「开发内功修炼」
一、内核源码结构
Linux 内核源码树的根目录描述如下表:
目录 | 含义描述 |
---|---|
arch | 此目录的所有子目录的文档数据都是体系结构特有的代码。有3个子目录: Kernel(不同体系结构内核所有的实现方式,比如计时器、SMP、信号量等等);lib(不同体系结构下的高性能通用代码实现,比如memcpy等等);mm(不同体系结构特有的内存管理程序的实现) |
block | 块设备I/O层 |
crypto | 加密API |
Documentation | 内核源码技术文档 |
drivers | 设备驱动程序。包括显卡、网卡、PCI等外围设备驱动代码 |
firmware | 某些驱动程序需要的设备固件 |
fs | VFS和各种文件系统代码。Ext2、Ext3、Ext4等本地文件系统 |
include | 内核头文件(.h) |
init | 内核引导和初始化代码 |
ipc | 进程间通信代码 |
kernel | 调度程序的核心子系统。主要包括进程创建、销毁和调度等代码 |
lib | 通用内核函数 |
LICENSES | 内核相关的license文件 |
mm | 内存管理子系统和VM |
net | 内核网络协议栈代码。netfilter内核防火墙框架,灵活、可实现安全策略,如数据包过滤、数据包处理、透明代理、动态网络地址转换等。抽象及通用化框架,作为中间件,为每种网络协议(ipV4/IPv6等)定义整套钩子函数 |
samples | 示例,示范代码 |
scripts | 编译内核所用的脚本 |
security | Linux内核安全模块 |
sound | 语音子系统 |
tools | 在Linux开发中有用的工具 |
usr | 早期用户空间代码 |
virt | 虚拟化基础结构 |
- TCP网络编程流程:socket()---->bind()---->listen()---->accept()---->send()---->recv()---->关闭socket()
- 网络数据在内核中处理过程主要是在网卡和协议栈之间进行
- 协议栈将需要发送的数据通过网络发送出去
- 应用层输出数据时自上而下
- 有数据到达时自下而上到达应用程序
二、网络栈总体架构
目录 | 含义描述 |
---|---|
物理层 | 提供电信号和一些底层的细节 |
数据链接层 | 处理端点间的数据传输 |
网络层 | 负责数据包转发和主机编址 |
协议层/传输层 | 完成结点间的数据发(TCP/UDP) |
传话层 | 处理端点的传话 |
表示层 | 处理数据传送和格式设置 |
应用层 | 向最终用户应用程序提供网络服务 |
注:
-
linux内核涉及3层,L2,L3,L4三层,分别对应OSI七层模型中的数据链路层、网络层、传输层。内核栈的任务就是将接收到的数据包L2(网络设备驱动程序)传递给L3(网络层,通常为IPV4/IPV6)
-
若数据包目的地为当前设备,Linux内核网络栈将其传递给L4(传输层,应用TCP/UCP协议侦听套接字);若数据包需要转发,则将其交还给L2传输;本地生成的出站数据包,将从L4依次传递给L3,L2,再由网络设备驱动程序传输。
-
Linux内核不涉及L4之上的会话层、表示层、应用层的任务由用户空间应用程序实现。
-
网络设备驱动程序位于数据链路层层。net_device结构体相当庞大,包括设备参数、设备MAC地址。设备名称,如eth0/eth。设备标志,如状态是up还是down。(netdevice.h)
三、SK_BUFF内部原理及协议栈
内核层和用户层在网络方面差别比较大,在内核的网络层中sk_buff结构占有重要地位。存储网络报文。include/linux/skbuff.h
代码核心点注释如下:
tail、end、head这些指针是对网络报文的部分描述。网络报文存储空间是在应用层发送网络数据或者网络设备收到网络数据时动态分配的,分配成功之后,将接收或者发送的网络数据填充到这个储存空间中(skbuff)
结构 sk_ buff 以 sk_ buff head 构成一个环状的链,next变量指向下一个 sk_ buff 结构,prev变量指向前一个 sk_ buff 结构,内核程序通过访问其中的各个单元来遍历整个协议栈中的网络数据。
四、数据包的收发
网络设备驱动程序主要任务:
接受目的地为当前主机的数据包,并将其传递给网络层(L3),之后将其传递给传输层(L4)。传输当前主机生成的外出数据包或转发当前主机收到的数据包。
内核收包路径示意:
1. 启动过程
模块在具备接收网卡数据包之前,要做很多的准备工作才行。比如要提前创建好ksoftirqd内核线程,要注册好各个协议对应的处理函数,网卡设备子系统要提前初始化好,网卡要启动好。
1.1 创建ksoftirqd内核进程
Linux 的软中断都是在专⻔的内核线程(ksoftirqd)中进⾏的。系统初始化的时候在 kernel/smpboot.c中调⽤了 smpboot_register_percpu_thread, 该函数进⼀步会执⾏到 spawn_ksoftirqd(位于kernel/softirq.c)来创建出 softirqd 进程。
//kernel/softirq.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)
{
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
takeover_tasklets);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
return 0;
}
early_initcall(spawn_ksoftirqd);
ksoftirqd被创建出来以后,进入循环函数ksoftirqd_should_run与run_ksoftirqd,判断有没有软中断需要被处理。
1.2 网络子系统初始化
linux 内核通过调⽤ subsys_initcall 来初始化各个⼦系统,⽹络⼦系统的初始化,会执⾏到net_dev_init 函数。
// net/core/dev.c
static int __init net_dev_init(void)
{
···
for_each_possible_cpu(i) {
struct work_struct *flush = per_cpu_ptr(&flush_works, i);
struct softnet_data *sd = &per_cpu(softnet_data, i);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
···
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 数据结构,在这个数据结构⾥的poll_list 是等待驱动程序将其 poll 函数注册进来。
-
open_softirq注册软中断NET_TX_SOFTIRQ发送与NET_RX_SOFTIRQ接收。
1.3 协议栈注册
内核实现了网络层的 ip 协议,也实现了传输层的 tcp 协议和 udp 协议。 这些协议对应的实现函数分别是 ip_rcv(), tcp_v4_rcv()和 udp_rcv()。和平时写代码的⽅式不⼀样的是,内核是通过注册的方式来实现的。 Linux 内核中的 fs_initcall 和 subsys_initcall 类似,也是初始化模块的入口。 fs_initcall 调⽤ inet_init 后开始网络协议栈注册。 通过 inet_init ,将这些函数注册到了 inet_protos 和 ptype_base 数据结构中。
//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,
};
static struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.early_demux_handler = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
.icmp_strict_tag_validation = 1,
};
static struct net_protocol udp_protocol = {
.early_demux = udp_v4_early_demux,
.early_demux_handler = udp_v4_early_demux,
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
static int __init inet_init(void)
{
//注册tcp_port
rc = proto_register(&tcp_prot, 1);
//注册udp_port
rc = proto_register(&udp_prot, 1);
...
//注册udp协议
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
//注册tcp协议
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
dev_add_pack(&ip_packet_type);
}
inet_add_protocol 函数将 tcp 和 udp 对应的处理函数都注册到了 inet_protos 数组中。
struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;
inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
...
return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
NULL, prot) ? 0 : -1;
}
在 dev_add_pack(&ip_packet_type); 处,ip_packet_type 结构体中的 type是协议名,func 是 ip_rcv 函数,在dev_add_pack 中会被注册到 ptype_base 哈希表中
//net/core/dev.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 pt->dev ? &pt->dev->ptype_all : &ptype_all;
else
return pt->dev ? &pt->dev->ptype_specific :
&ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
至此inet_protos 记录着 udp,tcp 的处理函数地址,ptype_base 存储着ip_rcv() 函数的处理地址。
1.4网卡驱动初始化
每⼀个驱动程序(不仅仅只是⽹卡驱动)会使⽤ module_init 向内核注册⼀个初始化函数,当驱动被加载时,内核会调⽤这个函数。⽐如igb⽹卡驱动的代码
//drivers/net/ethernet/intel/igb/igb_main.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)
{
int ret;
ret = pci_register_driver(&igb_driver);
return ret;
}
module_init(igb_init_module);
驱动的 pci_register_driver 调用完成后,Linux 内核就知道了该驱动的相关信息,比如igb 网卡驱动的 igb_driver_name 和 igb_probe 函数地址等等。当网卡设备被识别以后,内核会调用其驱动的 probe方法(igb_driver 的 probe方法是 igb_probe)。
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
//DMA初始化
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
//ethools注册igb_netdev_ops
netdev->netdev_ops = &igb_netdev_ops;
igb_set_ethtool_ops(netdev);
err = igb_sw_init(adapter);
//注册netdev
err = register_netdev(netdev);
dev_pm_set_driver_flags(&pdev->dev, DPM_FLAG_NEVER_SKIP);
}
igb_sw_init -> igb_init_interrupt_scheme -> igb_alloc_q_vectors -> igb_alloc_q_vector
static int igb_alloc_q_vector(struct igb_adapter *adapter,
int v_count, int v_idx,
int txr_count, int txr_idx,
int rxr_count, int rxr_idx)
{
netif_napi_add(adapter->netdev, &q_vector->napi,
igb_poll, 64);
...
}
NAPI 机制所必须的 poll 函数,对于 igb 网卡驱动来说是 igb_poll。
1.5启动网卡
启用⼀个网卡时(例通过 ifconfig eth0 up ),net_device_ops 中的 igb_open 方法会被调用,通常完成以下事情
// drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming)
{
/* 分配TX队列内存 */
err = igb_setup_all_tx_resources(adapter);
/* 分配RX队列内存 */
err = igb_setup_all_rx_resources(adapter);
//处理中断
err = igb_request_irq(adapter);
//使能napi
for (i = 0; i < adapter->num_q_vectors; i++)
napi_enable(&(adapter->q_vector[i]->napi));
...
return 0;
}
// drivers/net/ethernet/intel/igb/igb_main.c
static int __igb_open(struct net_device *netdev, bool resuming)
{
/* 分配TX队列内存 */
err = igb_setup_all_tx_resources(adapter);
/* 分配RX队列内存 */
err = igb_setup_all_rx_resources(adapter);
err = igb_request_irq(adapter);//处理中断
for (i = 0; i < adapter->num_q_vectors; i++)//使能napi
napi_enable(&(adapter->q_vector[i]->napi));
...
return 0;
}
中断处理igb_request_irq
static int igb_request_irq(struct igb_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
struct pci_dev *pdev = adapter->pdev;
int err = 0;
if (adapter->flags & IGB_FLAG_HAS_MSIX) {
err = igb_request_msix(adapter);
...
}
...
}
static int igb_request_msix(struct igb_adapter *adapter)
{
for (i = 0; i < adapter->num_q_vectors; i++) {
struct igb_q_vector *q_vector = adapter->q_vector[i];
vector++;
err = request_irq(adapter->msix_entries[vector].vector,
igb_msix_ring, 0, q_vector->name,
q_vector);
}
igb_configure_msix(adapter);
return 0;
...
}
igb_request_msix 中对于多队列的⽹卡,为每⼀个队列都注册了中断,其对应的中断处理函数为 igb_msix_ring