【网络编程系列_01 】Linux内核启动与收包分析
前言:
1. 最近在学习netty,由于源码中有很多和网络连接,读取数据等这些和网络编程密切相关的东西(因为netty本身定位就是网络编程框架)。
我自认为如果不去一探究竟,可能会对以后或者当前的学习造成障碍,同时我也深知,对底层实现理解的越深,上层的框架分析起来其实
就很简单了,有种 “透视” 的感觉。我觉得“透视” 这个词用在这里真的是在恰当不过了!
2. 因为Linux源码太多,所以在涉及到源码时,我们将去繁化简,紧抓要点,旁枝末节不再过多讨论(但是源码注释我一般都保留,同时文件路径我也会给出,这样方便查找)。
3. 本文对应的Linux源码版本为 Linux-3.10.1
4. 本文采用总分总的方式来展开,如此: 准备+概览 -> 展开 -> 理论+实际演示 -> 总结 。
5. 由于避免让字数过于长,代码片段占比过大,所以很多贴出来的代码是删减后的,只保留了和我们相关的,
想下载完整版本请移步 https://github.com/torvalds/linux 进行下载学习。
一、开篇前的准备工作
由于本文将是一篇很长的文章,而且内容比较多,比较杂,为了不使读者发懵,我们先做一下开篇前的准备工作
本文将结合linux源代码和几篇大佬的博客,来做分析。因为时间&技术有限,很难去真实的一行行调试linux源码,所以本篇不是一篇实战型文章,更多的,我们将从理论+源码的方式,来分析内核启动,以及收包的流程。
问题与一些说明
为了避免开篇显得很唐突,我们在这里先放出两个问题(注意:这俩问题会贯穿整个文章
)以及几张图,好知道我们接下来大概要说个什么。
- 问题1: 在收发数据包之前,底层系统做了哪些准备工作?
- 问题2: 接收到数据包后,内核是如何处理的?又是如何向上给到应用层的?其实对应的就是下图
(一张自己画的tcp工作示意图)
里边的节点R!!! (注:R代表receive)
- 说明: 系统分层与网络抽象模型(OSI)对应关系:
二、Linux收包总览
一些说明:
- 在Linux的源码中,网络设备驱动对应的逻辑位于
drivers/net/ethernet
, 其中intel系列的网卡驱动
在drivers/net/ethernet/intel
目录下。协议栈
模块代码位于kernel
和net
目录。 当设备上有数据到达的时候,先通过DMA将数据放到内存中
,然后给CPU的相关引脚上触发一个电压变化(其实就是发出硬中断通知
),以通知CPU来处理数据。对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。因此Linux中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放CPU
,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中
,可以慢慢从容处理。2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd
内核线程全权处理。和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化
,而软中断是通过给内存中的一个变量的二进制值
以通知软中断处理程序 (该程序是内核初始化时候就注册了的)。
何为中断,有哪几种类型?
- 何为中断: CPU 通过时分复用来处理很多任务,这其中包括一些硬件任务,例如磁盘读写、键盘输入,也包括一些软件任务,例如网络包处理。 在任意时刻,一个 CPU 只能处理一个任务。 当某个硬件或软件任务此刻没有被执行,但它希望 CPU 来立即处理时,就会给 CPU 发送一个中断请求 —— 希望 CPU 停下手头的工作,优先服务“我”。
中断是以事件的方式通知 CPU 的
,因此我们常看到 “XX 条件下会触发 XX 中断事件” 的表述。- 两种中断类型: 中断分为硬中断和软中断。 硬中断:(外部或硬件产生的中断,例如键盘按键,cpu引脚的电压变化等)。软中断:(软件产生的中断(比如标记一个变量值,有轮询函数去检测该值,符合某种条件后做一定的处理))
Linux收包总览【示意图】重要
下边我们画一张示意图,来概览一下数据是如何从(物理层)网卡到我们的用户空间的(或者说linux的收包过程)
我们对上图做个简单的解释(其实图中已经很详细了)
到此,我们的准备工作和概览基本就差不多了,有了这些准备工作,我想在后续的学习中,会更加容易一些。因为linux内核真的是太复杂了,如果开篇直述,可能很多人都不知道说了个啥。 ps: 概览图是很重要的,将图牢牢记住,对学习这些东西将会事半功倍。
在下边的文章中,我们将围绕上边目录
【问题与一些说明】
中的【问题1】
和【问题2】
来展开。过于细节的我们直接略过,不再纠缠。
接下来,让我们带着【问题与一些说明】
中的【问题1】
来展开本章节的内容!
三、Linux内核启动与初始化
Linux在收包之前,需要启动和初始化一些模块包括以下几个:
- 初始化/启动一些基本函数或者线程(如
ksoftirqd
内核线程 ) - 注册各协议对应的处理函数
- 网络子系统初始化
- 网卡启动
上边只是列了个主要,事实上准备工作远不止这些,在以上几个流程都走完后,Linux才可以进行收包。紧接着我们对上边这几项做一个展开。
内核启动入口总览
首先我们找到Linux内核启动入口
函数 start_kernel
该函数里边有100多个方法
调用,是一个初始化大杂烩
源代码有点长,我这里挑几个看一下:
文件路径: /linux-3.10.1/init/main.c
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern const struct kernel_param __start___param[], __stop___param[]
/*
* Need to run as early as possible, to initialize the
* lockdep hash:
*/
lockdep_init();
smp_setup_processor_id();
debug_objects_early_init();
/*
* Set up the the initial canary ASAP:
*/
boot_init_stack_canary();
cgroup_init_early();
boot_cpu_init();
...
page_address_init();
pr_notice("%s", linux_banner);
setup_arch(&command_line);
mm_init();
sched_init();
local_irq_disable();
console_init();
early_boot_irqs_disabled = true;
/* init some links before init_ISA_irqs() */
early_irq_init();
init_IRQ();
tick_init();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
time_init();
...
vfs_caches_init(totalram_pages);
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
捡几个函数进行简单说明:
- (setup_arch:系统架构初始化函数)
- (mm_init:内存初始化函数)
- (sched_init:内核系统调度器)
- (init_IRQ:中断初始化函数)
- (console_init:终端打印函数初始化)
- (vfs_caches_init:虚拟文件系统初始化)
- (rest_init:fork 出用户进程)
- .....
完成内核本身的各方面设置,目的是最终建立起基本完整的 Linux 内核环境
创建 ksoftirqd
内核线程
首先我们要说明,ksoftirqd函数非常重要(因为他是软中断的核心逻辑)
小提示:每个处理器都有自己的内核线程,名字叫做
ksoftirqd/n
,n是处理器的编号
内核初始化的入口是从start_kernel
开始的,文字描述不太直观,让我们直接看一个示意图来一览ksoftirqd
内核线程是如何创建的吧。
- 顺着上图,我们来简单看下源码:(源码绝对路径在上图中可以找到)
linux-3.10.1/init/main.c
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern const struct kernel_param __start___param[], __stop___param[];
.....
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
linux-3.10.1/init/main.c
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
...
}
linux-3.10.1/init/main.c
static int __ref kernel_init(void *unused)
{
kernel_init_freeable();
async_synchronize_full();
...
panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
linux-3.10.1/init/main.c
static noinline void __init kernel_init_freeable(void)
{
wait_for_completion(&kthreadd_done);
...
do_basic_setup();
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
pr_err("Warning: unable to open an initial console.\n");
}
linux-3.10.1/init/main.c
/*
* Ok, the machine is now initialized. None of the devices
* have been touched yet, but the CPU subsystem is up and
* running, and memory and process management works.
*
* Now we can finally start doing some real work..
*/
static void __init do_basic_setup(void)
{
cpuset_init_smp();
usermodehelper_init();
shmem_init();
driver_init();
init_irq_proc();
do_ctors();
usermodehelper_enable();
do_initcalls();
}
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));
return 0;
}
early_initcall(spawn_ksoftirqd);
小疑问:+重点: 为甚么从do_initcalls
就能直接调到early_initcall
? 是因为内核中有这么一段代码,他将会遍历这个数组,并将其值拼接到initcall上(注意:这个逻辑很重要,他是linux内核初始化各个模块的初始方法!
),然后执行拼接后的函数 (我们将拼接函数名然后挨个执行拼接后的函数的逻辑用logic A
来表示,后边我们可能会用到 ),(ps:我个人感觉这有点像java中的反射)
static char *initcall_level_names[] __initdata = {
"early",
"core",
"postcore",
"arch",
"subsys",
"fs",
"device",
"late",
};
注意:上边的 logic A 是一个重点:
比如初始化网络子系统就调用 subsys_initcall
, 初始化协议栈就进入 fs_initcall
, 一些最基本的东西就调用 early_initcall
(比如我们本小节ksoftirqd线程的创建
就是在early_initcall
中进行的) 等等, 事实上,我们可以根据这些单词,知道对应的 xxx_initcall 大概是做啥的!在下边的章节中,我们还会提到这个 xxx_initcall 所以这里需要重点关注一下!!!
...下边创建线程的逻辑不贴了 ,要不代码占用篇幅太多了
到这里看下我本机上的这些
ksoftirqd
线程:(可见每个cpu对应一个ksoftirqd
线程! )
小贴士: 当ksoftirqd被创建出来以后,它就会进入自己的线程循环函数
run_ksoftirqd
了 (当然前提是ksoftirqd_should_run
返回true )。run_ksoftirqd中有个do while循环,他将会不停地判断有没有软中断需要被处理。这里需要注意的一点是,软中断不仅仅只有网络软中断,还有其它类型。 如下:
文件位置:include/linux/interrupt.h
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/
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, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
总之到这里,我们心中已经有个大体流程了:原来ksoftirqd
线程是这么个创建时机和流程!
同时: 我们要明白,ksoftirqd
线程被创建后, 就启动了循环函数run_ksoftirqd
了 ,这个函数挺重要的,是处理软中断通知的入口。ps:(其实我感觉可以这个循环函数和Netty
中的NioEventLoop
的run()
函数很像有没有?)
我们看下软中断处理相关入口代码:(这里我们不做过多的展开,后续在收包过程中,会重温下边这块的代码逻辑)
/Users/hzz/myself_project/linux_source_code/linux-3.10.1/kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
//软中断逻辑入口
__do_softirq();
rcu_note_context_switch(cpu);
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
//重要的一个函数 是软中断处理逻辑的入口
asmlinkage void __do_softirq(void)
{
pending = local_softirq_pending();
account_irq_enter_time(current);
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) {
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action(h);
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %u %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", vec_nr,
softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
...
__local_bh_enable(SOFTIRQ_OFFSET);
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
网络子系统初始化
实际上网络子系统初始化是有很多的工作的,我们在这个环节只关注以下两点
- 注册软中断处理函数
- 注册poll函数的引用到
softnet_data
的poll_list
说起网络子系统初始化,我们在上边 logic A
处也提到过,就是在 subsys_initcall
函数中进行的。接着我们看下subsys_initcall
做了些什么,也就是网络子系统初始化时候做了哪些事,(ps:
这里我们只看我们关心的,其他的不做过多展开)
/Users/hzz/myself_project/linux_source_code/linux-3.10.1/net/core/dev.c
/*
* Initialize the DEV module. At boot time this walks the device list and
* unhooks any devices that fail to initialise (normally hardware not
* present) and leaves us with a valid list of present and active devices.
* This is called single threaded during boot, so no need
* to take the rtnl semaphore.
*/
static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;
BUG_ON(!dev_boot_phase);
if (dev_proc_init())
goto out;
if (netdev_kobject_init())
goto out;
INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);
INIT_LIST_HEAD(&offload_base);
if (register_pernet_subsys(&netdev_net_ops))
goto out;
/*
* Initialise the packet receive queues. //初始化网络数据包接收队列
*/
for_each_possible_cpu(i) {
//为当前循环中的cpu声请 softnet_data 类型的数据结构
struct softnet_data *sd = &per_cpu(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);
sd->output_queue = NULL;
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
sd->csd.func = rps_trigger_softirq;
sd->csd.info = sd;
sd->csd.flags = 0;
sd->cpu = i;
#endif
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
sd->backlog.gro_list = NULL;
sd->backlog.gro_count = 0;
}
dev_boot_phase = 0;
/* The loopback device is special if any other network devices
* is present in a network namespace the loopback device must
* be present. Since we now dynamically allocate and free the
* loopback device ensure this invariant is maintained by
* keeping the loopback device as the first device on the
* list of network devices. Ensuring the loopback devices
* is the first device that appears and the last network device
* that disappears.
*/
if (register_pernet_device(&loopback_net_ops))
goto out;
if (register_pernet_device(&default_device_ops))
goto out;
//注册 NET_TX_SOFTIRQ 类型的软中断 和注册 NET_RX_SOFTIRQ类型的软中断 NET_RX_SOFTIRQ其实就是收包
//(也就是和本文对应的)时候的软中断, 其中 net_tx_action 和 net_rx_action
// 分别是发包时候和收包时候的软中断处理逻辑!!!
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
hotcpu_notifier(dev_cpu_callback, 0);
dst_init();
rc = 0;
out:
return rc;
}
subsys_initcall(net_dev_init);
来说下上边这段逻辑的大致:
在上边这个 subsys_initcall
对应的 net_dev_init
函数里,会为每个CPU都申请一个softnet_data
数据结构,在这个数据结构里的poll_list
是等待驱动程序
将其poll
函数注册进来(poll_list里边保存的其实可以理解为poll函数的引用
),稍后网卡驱动初始化的时候我们可以看到这一过程。
另外open_softirq是为每一种软中断都注册一个处理函数。NET_TX_SOFTIRQ
类型的软中断处理函数为net_tx_action
,NET_RX_SOFTIRQ 类型的软中断的处理函数是net_rx_action
。继续跟踪open_softirq
后发现这个注册的方式是记录在softirq_vec
变量里的。后面ksoftirqd
线程收到软中断的时候,也会使用这个变量来找到每一种软中断对应的处理函数。代码示意如下:
/Users/hzz/myself_project/linux_source_code/linux-3.10.1/kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
说白了,这小节的网络子系统初始化,有两件事我们需要关注
- 第一件事: 为收发包类型的软中断注册了 各自对应的处理函数,好在后边接收到这种(出去 or 进来的)软中断通知时,来从数组(
softirq_vec
)中拿到对应的函数的引用
,来调用对应软中断处理函数来执行对应的软中断逻辑
! - 第二件事: 为每个CPU都申请一个
softnet_data
数据结构,在这个数据结构里的poll_list
作用是:等待各种类型的驱动程序
将其poll
函数注册进来用于后续使用,(poll
函数用于拉取RingBuffer
上的数据报
(不记得的话看开篇中的那个内核收包示意图。)
协议栈注册
说起协议栈初始化,我们在上边 logic A
处也提到过,就是在 fs_initcall
函数中进行的。
内核实现了网络层的ip
协议,也实现了传输层的tcp
协议和udp
协议。这些协议对应的实现函数分别是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我们平时写代码的方式不一样的是,内核是通过注册的方式来实现的。Linux内核中的fs_initcall
和subsys_initcall
类似,也是初始化模块的入口只不过 fs_initcall
是为了协议栈 而 subsys_initcall
是初始化网络子系统 。fs_initcall
调用inet_init
后开始网络协议栈注册。通过inet_init
,将这些函数的引用保存到了inet_protos
和ptype_base
数据结构中去,以便后续需要时拿来使用。
如下是初始化协议栈的相关代码:
文件路径:/linux-3.10.1/net/ipv4/af_inet.c
static int __init inet_init(void)
{
...
if (!sysctl_local_reserved_ports)
goto out;
rc = proto_register(&tcp_prot, 1);
if (rc)
goto out_free_reserved_ports;
rc = proto_register(&udp_prot, 1);
if (rc)
goto out_unregister_tcp_proto;
rc = proto_register(&raw_prot, 1);
if (rc)
goto out_unregister_udp_proto;
rc = proto_register(&ping_prot, 1);
if (rc)
goto out_unregister_raw_proto;
/*
* Tell SOCKET that we are alive...
*/
(void)sock_register(&inet_family_ops);
#ifdef CONFIG_SYSCTL
ip_static_sysctl_init();
#endif
tcp_prot.sysctl_mem = init_net.ipv4.sysctl_tcp_mem;
/*
* Add all the base protocols. 添加基础协议 icmp tcp udp
*/
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__);
#ifdef CONFIG_IP_MULTICAST
if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)
pr_crit("%s: Cannot add IGMP protocol\n", __func__);
#endif
/* Register the socket-side information for inet_create. */
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r);
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
/*
* Set the ARP module up --- ARP协议
*/
arp_init();
/*
* Set the IP module up --- IP协议
*/
ip_init();
tcp_v4_init(); --- TCP协议
/* Setup TCP slab cache for open requests. */
tcp_init();
/* Setup UDP memory threshold */ --- UDP协议
udp_init();
/* Add UDP-Lite (RFC 3828) */
udplite4_register();
ping_init();
/*
* Set the ICMP layer up
*/
if (icmp_init() < 0)
panic("Failed to create the ICMP control socket.\n");
/*
* Initialise the multicast router
*/
#if defined(CONFIG_IP_MROUTE)
if (ip_mr_init())
pr_crit("%s: Cannot init ipv4 mroute\n", __func__);
#endif
/*
* Initialise per-cpu ipv4 mibs
*/
if (init_ipv4_mibs())
pr_crit("%s: Cannot init ipv4 mibs\n", __func__);
ipv4_proc_init();
ipfrag_init();
dev_add_pack(&ip_packet_type);
rc = 0;
out:
...
}
fs_initcall(inet_init);
从上可以看出,这里初始化了 IP
ARP
ICMP
TCP
UDP
等等协议
我们这里关注下tcp是咋搞的 其他协议略过。tcp如下:
文件路径:/linux-3.10.1/net/ipv4/protocol.c
/*
* Add a protocol handler to the hash tables // 翻译: 将协议处理程序添加到哈希表
*/
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;
}
EXPORT_SYMBOL(inet_add_protocol);
我们也顺便看下这些协议(也就是调用 inet_add_protocol 方法时传入的第一个参数)里边都有哪些东西:
/linux-3.10.1/net/ipv4/af_inet.c
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv, (传输层)icmp类型的处理函数(其实他是一个回调函数)
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
static const struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.handler = tcp_v4_rcv, (传输层)tcp类型的处理函数(其实他是一个回调函数)
.err_handler = tcp_v4_err,
.no_policy = 1,
.netns_ok = 1,
};
static const struct net_protocol udp_protocol = {
.handler = udp_rcv, (传输层)udp类型的处理函数(其实他是一个回调函数)
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,
};
从上边代码和方法的注释(Add a protocol handler to the hash tables
)不难看出, inet_add_protocol
函数将tcp,udp, icmp 这些协议的处理函数都注册到了inet_protos (hash结构的) hash表中了。
这里我们需要记住inet_protos记录着icmp,udp,tcp的处理函数地址
注册了传输层的各个协议栈的实现( icmp_rcv, ucp_rcv , tcp_rcv)后,网络层的协议栈(ip暂且我们只关注他)也得注册下吧? 不然怎么玩呢?这里我们直接给出答案: 网络层协议ip的处理函数是在
inet_add_protocol
里的dev_add_pack(&ip_packet_type);
函数中进行的。下边我们简单看下 dev_add_pack函数以及其入参都是些啥。
/linux-3.10.1/net/ipv4/af_inet.c
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
spin_lock(&ptype_lock);
list_add_rcu(&pt->list, head);
spin_unlock(&ptype_lock);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
//将ip_rcv函数引用 保存到ptype_base 表中
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
补充:dev_add_pack函数入参:
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(0x0800),
.func = ip_rcv, (网络层)ip协议的处理函数 //是一个钩子函数
};
可以看出该函数就是将ip协议对应的处理函数和类型 即 (ip_packet_type) ,保存到了ptype_head中了。用于后续根据type
取出对应的处理函数 如ip_rcv
进行ip层的协议包处理
而ptype_head
里会把ip_packet_type
保存到ptype_base
哈希表中,ip_packet_type
存储着ip_rcv()
函数的处理地址。在后面,我们会看到软中断中会通过ptype_base找到ip_packet_type中的ip_rcv函数地址,进而将ip包正确地送到ip_rcv()中执行。在ip_rcv中将会通过inet_protos找到tcp或者udp的处理函数,再而把包转发给udp_rcv()或tcp_v4_rcv()函数。
提示:inet_init这个函数中的工作可远远不止上边说的那些内容。这里我们不再展开,感兴趣自行阅读相关文章
网卡初始化与启动
网卡初始化
注意:在本文,我们将以igb网卡举例
igb网卡初始化入口对应源码:
/Users/hzz/myself_project/linux_source_code/linux-3.10.1/drivers/net/ethernet/intel/igb/igb_main.c
/**
* igb_init_module - Driver Registration Routine
*
* igb_init_module is the first routine called when the driver is
* loaded. All it does is register with the PCI subsystem.
**/
static int __init igb_init_module(void)
{
int ret;
pr_info("%s - version %s\n",
igb_driver_string, igb_driver_version);
pr_info("%s\n", igb_copyright);
#ifdef CONFIG_IGB_DCA
dca_register_notify(&dca_notifier);
#endif
ret = pci_register_driver(&igb_driver);
return ret;
}
module_init(igb_init_module);
static struct pci_driver igb_driver = {
.name = igb_driver_name,
.id_table = igb_pci_tbl,
.probe = igb_probe, // 初始化时将会执行这个方法 **重要**
.remove = igb_remove,
#ifdef CONFIG_PM
.driver.pm = &igb_pm_ops,
#endif
.shutdown = igb_shutdown,
.sriov_configure = igb_pci_sriov_configure,
.err_handler = &igb_err_handler
};
上边这段代码中里边有一个至关重要的函数 pci_register_driver
其负责 注册设备驱动到PCI总线的设备队列上 至于pci 我们这里不做展开说明,请自行查阅 示例:
内核启动过程中,会为设备寻找驱动其实就是调用其 probe()
方法。 probe()
做的事情因厂商和设备而异,总体来说这个过程涉及到的东西都非常多, 最终目标也都是使设备 ready。典型过程包括:
- 获取网卡地址
- DMA初始化,设置 DMA 掩码
- 注册设备驱动支持的
ethtool
方法 - 注册各种网卡启动时需要的函数
- 注册
NAPI poll
方法 - 使得设备达到
ready
状态
我们简单看下 probe相关源码(其余我们略过,只看注册启动函数和poll函数)
/Users/hzz/myself_project/linux_source_code/linux-3.10.1/drivers/net/ethernet/intel/igb/igb_main.c
/**
* igb_probe - Device Initialization Routine
* @pdev: PCI device information struct
* @ent: entry in igb_pci_tbl
*
* Returns 0 on success, negative on failure
*
* igb_probe initializes an adapter identified by a pci_dev structure.
* The OS initialization, configuring of the adapter private structure,
* and a hardware reset occur.
**/
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev;
struct igb_adapter *adapter;
struct e1000_hw *hw;
...
netdev->netdev_ops = &igb_netdev_ops;//驱动向内核注册了 net_device_ops 变量
...
}
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,
.ndo_tx_timeout = igb_tx_timeout,
.ndo_validate_addr = eth_validate_addr,
.ndo_vlan_rx_add_vid = igb_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = igb_vlan_rx_kill_vid,
.ndo_set_vf_mac = igb_ndo_set_vf_mac,
.ndo_set_vf_vlan = igb_ndo_set_vf_vlan,
.ndo_set_vf_tx_rate = igb_ndo_set_vf_bw,
.ndo_set_vf_spoofchk = igb_ndo_set_vf_spoofchk,
.ndo_get_vf_config = igb_ndo_get_vf_config,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = igb_netpoll,
#endif
.ndo_fix_features = igb_fix_features,
.ndo_set_features = igb_set_features,
};
其中igb_open
是网卡启动时候需要调用的函数。
另外 在igb_probe
初始化过程中,还调用到了igb_alloc_q_vector
。他注册了一个NAPI机制所必须的poll
函数,对于igb网卡驱动来说,这个函数就是igb_poll
,如下代码所示
linux-3.10.1/drivers/net/ethernet/intel/igb/igb_main.c
igb_alloc_q_vector函数中的代码片段:
/* initialize NAPI */
netif_napi_add(adapter->netdev, &q_vector->napi,
igb_poll, 64);
igb的poll函数:
/**
* igb_poll - NAPI Rx polling callback
* @napi: napi polling structure
* @budget: count of how many packets we should handle
**/
static int igb_poll(struct napi_struct *napi, int budget)
{
struct igb_q_vector *q_vector = container_of(napi,
struct igb_q_vector,
napi);
bool clean_complete = true;
#ifdef CONFIG_IGB_DCA
if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
igb_update_dca(q_vector);
#endif
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);
/* If all work not completed, return budget and keep polling */
if (!clean_complete)
return budget;
/* If not enough Rx work done, exit the polling mode */
napi_complete(napi);
igb_ring_irq_enable(q_vector);
return 0;
}
到此,网卡的初始化就完成了,由于网卡初始化时工作太多,我们没对上边网卡初始化进行过度展开,只是挑两个后续我们会说到的(igb_open
, igb_poll
)来简单说明一下。
网卡启动
当上面的初始化都完成以后,就可以启动网卡了。在前面网卡驱动初始化时,我们提到了驱动向内核注册了 net_device_ops
变量,它包含着网卡启用、发包、设置mac 地址等回调函数(函数指针)。当启用一个网卡时(例如,通过 ifconfig eth0 up),它通常会做以下事
- 启动网卡
- 调用
net_device_ops
注册的open
函数 如igb_open
- 分配RX TX队列所需的内存
- 注册中断处理函数
- 打开硬中断,开始准备收包。
我们简单看下源码
/linux-3.10.1/drivers/net/ethernet/intel/igb/igb_main.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,
.ndo_tx_timeout = igb_tx_timeout,
.ndo_validate_addr = eth_validate_addr,
.ndo_vlan_rx_add_vid = igb_vlan_rx_add_vid,
.ndo_vlan_rx_kill_vid = igb_vlan_rx_kill_vid,
.ndo_set_vf_mac = igb_ndo_set_vf_mac,
.ndo_set_vf_vlan = igb_ndo_set_vf_vlan,
.ndo_set_vf_tx_rate = igb_ndo_set_vf_bw,
.ndo_set_vf_spoofchk = igb_ndo_set_vf_spoofchk,
.ndo_get_vf_config = igb_ndo_get_vf_config,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = igb_netpoll,
#endif
.ndo_fix_features = igb_fix_features,
.ndo_set_features = igb_set_features,
};
static int igb_open(struct net_device *netdev)
{
return __igb_open(netdev, false);
}
/**
* igb_open - Called when a network interface is made active
* @netdev: network interface device structure
*
* Returns 0 on success, negative value on failure
*
* The open entry point is called when a network interface is made
* active by the system (IFF_UP). At this point all resources needed
* for transmit and receive operations are allocated, the interrupt
* handler is registered with the OS, the watchdog timer is started,
* and the stack is notified that the interface is ready.
**/
static int __igb_open(struct net_device *netdev, bool resuming)
{
struct igb_adapter *adapter = netdev_priv(netdev);
/* 给发送队列分配资源 */
err = igb_setup_all_tx_resources(adapter);
if (err)
goto err_setup_tx;
/* 给接队列分配资源 */
err = igb_setup_all_rx_resources(adapter);
if (err)
goto err_setup_rx;
//注册硬中断处理函数
err = igb_request_irq(adapter);
if (err)
goto err_req_irq;
...
/* From here on the code is the same as igb_up() */
clear_bit(__IGB_DOWN, &adapter->state);
//开启 NAPI模式
for (i = 0; i < adapter->num_q_vectors; i++)
napi_enable(&(adapter->q_vector[i]->napi));
...
/* Clear any pending interrupts. */
rd32(E1000_ICR);
igb_irq_enable(adapter);
....
return err;
}
__igb_open
函数调用了igb_setup_all_tx_resources
和igb_setup_all_rx_resources
。在igb_setup_all_rx_resources
这一步操作中,为每个网卡都分配了RingBuffer,并建立内存和Receive
队列的映射关系。
igb_setup_all_tx_resources这个暂时我们不管,不在本文讨论之内。
下边我们看下硬中断函数是如何注册的:
/linux-3.10.1/drivers/net/ethernet/intel/igb/igb_main.c
注册硬中断处理函数
/**
* igb_request_irq - initialize interrupts
* @adapter: board private structure to initialize
*
* Attempts to configure interrupts using the best available
* capabilities of the hardware and kernel.
**/
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->msix_entries) {
//注册以及设置中断通知方式为 msi-x
err = igb_request_msix(adapter);
if (!err)
goto request_done;
...
igb_setup_all_tx_resources(adapter);
igb_setup_all_rx_resources(adapter);
igb_configure(adapter);
}
...
return err;
}
真正注册硬中断处理函数 和设置中断通知的方式
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++;
q_vector->itr_register = hw->hw_addr + E1000_EITR(vector);
if (q_vector->rx.ring && q_vector->tx.ring)
sprintf(q_vector->name, "%s-TxRx-%u", netdev->name,
q_vector->rx.ring->queue_index);
else if (q_vector->tx.ring)
sprintf(q_vector->name, "%s-tx-%u", netdev->name,
q_vector->tx.ring->queue_index);
else if (q_vector->rx.ring)
sprintf(q_vector->name, "%s-rx-%u", netdev->name,
q_vector->rx.ring->queue_index);
else
sprintf(q_vector->name, "%s-unused", netdev->name);
// 注册中断处理函数,并且设置触发中断的方式为 MSI-X
err = request_irq(adapter->msix_entries[vector].vector,
igb_msix_ring, 0, q_vector->name,
q_vector);
if (err)
goto err_free;
}
}
在上面的代码中用, __igb_open
-> igb_request_irq
-> igb_request_msix
, 在igb_request_msix
中我们看到了,对于多队列的网卡,为每一个队列(通过for循环)都注册了硬中断,其对应的中断处理函数是igb_msix_ring
。
igb网卡 使用的硬中断方式是 MSI-X。 当一个数据帧通过 DMA 写到内核内存 ringbuffer 后,网卡通过硬件中断(IRQ)通知其他系统。 他有多种方式触发一个中断,分别是:
MSI-X MSI legacy interrupts 三种 设备驱动的实现也因此而不同。驱动必须判断出设备支持哪种中断方式,然后注册相应的硬中断处理函数,这些函数在硬中断发生的时候会被执行。
- MSI-X 中断是比较推荐的方式 也是本文示例【igb】网卡所支持的,尤其是对于支持多队列的网卡。 另外提一句: 多队列模式下的同一个网卡的中断信号可以被不同的 CPU 处理(通过调整
irqbalance
,或者修改/proc/irq/IRQ_NUMBER/smp_affinity
的方式) 更多见此文 。这样的话,从网卡硬件中断的层面就可以设置让收到的包被不同的CPU处理,从而避免某个cpu负载过重的情况出现。 - 如果不支持 MSI-X,那 MSI 相比于传统中断方式仍然有一些优势,驱动仍然会优先考虑它。 关于MSI和MSI-X 更多参阅: wiki
在网卡启动完成并注册好硬中断处理函数后,该网卡就可以开始收包了!
至此 开篇时的
问题与一些说明
小节中的问题1
我们就讲完了。
下边,我们带着开篇时候的 问题与一些说明
小节中的 问题2
进行接下来的 收包
分析。
四、收包分析
将数据帧DMA到RingBuffer (接收环中)
在数据帧从外部网络到达网卡后,第一步是
- 网卡在分配给自己的RingBuffer(分配资源的函数是上边 __igb_open中 提到的:
igb_setup_all_rx_resources(adapter);
)中寻找可用的内存位置 - 找到后DMA引擎会把数据DMA到网卡之前关联的内存里
- 向cpu发出硬中断通知(告知cpu,你先停下其他的,抓紧处理下我的这个请求吧!)
关于DMA的解释: 一句话概况: 为RAM和IO设备开辟一条直接传输数据的通道,无需cpu参与,网卡数据即可直接到达内存。 更多请见:wiki : 什么是DMA?
说明: 1 和 2 CPU都是无感知的! 也就是说是硬件的直接行为! 这个设计通过DMA方式,大大减少了cpu的压力(尤其是网络这种高频,处理逻辑复杂的场景) ps: 最初时,可能是来一个请求cpu就停下手头工作去处理收数据包和处理相关逻辑,DMA这种方式,给了cpu缓冲的时间,减少了网络高峰期时cpu的压力。
当然,RingBuffer也不是无限长度的,他是一个环形队列,当达到阈值时,将会把新来的数据包丢掉 此时,我们通过ifconfig查看网卡的时候可以看到里边有 overruns的值不为0的话,代表出现了丢包现象,而此时,我们可以通过ethtool来调整环形队列(RingBufer)的大小
向cpu发起硬中断通知以及cpu处理硬中断
- 在将数据帧DMA到内存后,网卡将会发起一个硬中断,来通知cpu
- 随后cpu将会调用网卡启动时该网卡注册的硬中断处理函数也就是上边说的(
igb_request_irq
),开始处理硬中断,从这里我们可以得知,硬中断里边干了什么事,是根据设备不同而有差异的(但是,一般硬中断里边的逻辑都比较简单,而把一些复杂的逻辑都放到处理软中断的逻辑中去,这样便于减少cpu压力,提高其吞吐性能,因为硬中断的优先级是很高的,大量,耗时的硬中断处理,可能会导致cpu繁忙,效率下降,吞吐降低)。
至于硬中断都做了什么,我们这里通过源码看下igb网卡的硬中断是咋搞的(其实硬中断只是超级简单的逻辑,大部分收包的逻辑都在后边呢)
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;
}
/**
* __napi_schedule - schedule for receive
* @n: entry to schedule
*
* The entry's receive function will be scheduled to run
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(&__get_cpu_var(softnet_data), n);
local_irq_restore(flags);
}
EXPORT_SYMBOL(__napi_schedule);
/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
//将poll_list添加进链表中(具体做什么我们这里不做过多展开)
list_add_tail(&napi->poll_list, &sd->poll_list);
//触发软中断 !
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
void __raise_softirq_irqoff(unsigned int nr)
{
//真正 触发软中断!
trace_softirq_raise(nr);
//修改软中断标志
or_softirq_pending(1UL << nr);
}
发起软中断通知
其实,就是这段代码
void __raise_softirq_irqoff(unsigned int nr)
{
//真正 触发软中断!
trace_softirq_raise(nr);
//修改软中断标志
or_softirq_pending(1UL << nr);
}
那么发出软中断通知后,由谁来处理对应的逻辑呢?
这个就是我们前边说的ksoftirqd
线程中的循环函数run_ksoftirqd
了!
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
//软中断对应的处理逻辑
__do_softirq();
rcu_note_context_switch(cpu);
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
asmlinkage void __do_softirq(void)
{
//检测是否有软中断标志 ,其实就是读取上边 __raise_softirq_irqoff
//函数中 or_softirq_pending(1UL << nr);设置的软中断标记
pending = local_softirq_pending();//读取软中断标记
account_irq_enter_time(current);
__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
do {
//如果有软中断需要处理,将进行处理
if (pending & 1) {
unsigned int vec_nr = h - softirq_vec;
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action(h);
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %u %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", vec_nr,
softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
说明:其实软中断的设置和读取,都是有一个主体的,也就是某个cpu来进行的, 设置软中断标记的cpu和读取软中断标记的cpu是同一个, 通过下段代码中的: smp_processor_id (获取当前cpu_id) 我们可以看出来
/* arch independent irq_stat fields */
#define local_softirq_pending() \
__IRQ_STAT(smp_processor_id(), __softirq_pending)
在上段代码的 h->action(h) 处 , 最终会调用到
net_rx_action
中来,这才是真正的软中断的处理逻辑!
/linux-3.10.1/net/core/dev.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();
//遍历 poll_list ,拿到poll函数的引用,在while中执行poll函数,
//poll函数的逻辑一句话概况就是(从RingBuffer环形队列上获取数据帧)
while (!list_empty(&sd->poll_list)) {
struct napi_struct *n;
int work, weight;
/* If softirq window is exhuasted then punt.
* Allow this to run for 2 jiffies since which will allow
* an average latency of 1.5/HZ.
*/
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
goto softnet_break;
local_irq_enable();
/* Even though interrupts have been re-enabled, this
* access is safe because interrupts can only add new
* entries to the tail of this list, and only ->poll()
* calls can remove this head entry from the list.
*/
n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
have = netpoll_poll_lock(n);
weight = n->weight;
/* This NAPI_STATE_SCHED test is for avoiding a race
* with netpoll's poll_napi(). Only the entity which
* obtains the lock and sees NAPI_STATE_SCHED set will
* actually make the ->poll() call. Therefore we avoid
* accidentally calling ->poll() when NAPI is not scheduled.
*/
work = 0;
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
//触发poll函数(是在网卡初始化时候说过的,网卡会把poll函数的引用注册到poll_list中)
trace_napi_poll(n);
}
WARN_ON_ONCE(work > weight);
budget -= work;
local_irq_disable();
/* Drivers must not modify the NAPI state if they
* consume the entire weight. In such cases this code
* still "owns" the NAPI instance and therefore can
* move the instance around on the list at-will.
*/
if (unlikely(work == weight)) {
if (unlikely(napi_disable_pending(n))) {
local_irq_enable();
napi_complete(n);
local_irq_disable();
} else {
if (n->gro_list) {
/* flush too old packets
* If HZ < 1000, flush all packets.
*/
local_irq_enable();
napi_gro_flush(n, HZ >= 1000);
local_irq_disable();
}
list_move_tail(&n->poll_list, &sd->poll_list);
}
}
netpoll_poll_unlock(have);
}
out:
net_rps_action_and_irq_enable(sd);
#ifdef CONFIG_NET_DMA
/*
* There may not be any more sk_buffs coming right now, so push
* any pending DMA copies to hardware
*/
dma_issue_pending_all();
#endif
return;
softnet_break:
sd->time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}
而我们本文以igb网卡举例,所以我们看下igb
网卡对应的poll
函数的逻辑
igb_poll
函数的处理逻辑:
linux-3.10.1/drivers/net/ethernet/intel/igb/igb_main.c
/**
* igb_poll - NAPI Rx polling callback
* @napi: napi polling structure
* @budget: count of how many packets we should handle
**/
static int igb_poll(struct napi_struct *napi, int budget)
{
...
#ifdef CONFIG_IGB_DCA
if (q_vector->adapter->flags & IGB_FLAG_DCA_ENABLED)
igb_update_dca(q_vector);
#endif
if (q_vector->tx.ring)
clean_complete = igb_clean_tx_irq(q_vector);
if (q_vector->rx.ring)
//收包时候的poll逻辑
clean_complete &= igb_clean_rx_irq(q_vector, budget);
/* If all work not completed, return budget and keep polling */
if (!clean_complete)
return budget;
/* If not enough Rx work done, exit the polling mode */
napi_complete(napi);
igb_ring_irq_enable(q_vector);
return 0;
}
static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{
do {
...
/* retrieve a buffer from the ring */
// 从RingBuffer上拉取buffer (数据帧)
skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);
cleaned_count++;
/* 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;
}
/* probably a little skewed due to removing CRC */
total_bytes += skb->len;
/* populate checksum, timestamp, VLAN, and protocol */
igb_process_skb_fields(rx_ring, rx_desc, skb);
napi_gro_receive(&q_vector->napi, skb);
/* reset skb pointer */
skb = NULL;
/* update budget accounting */
total_packets++;
} while (likely(total_packets < budget));
if (cleaned_count)
igb_alloc_rx_buffers(rx_ring, cleaned_count);
return (total_packets < budget);
}
上边函数虽然比较多,但是流程还是很清晰的,大概就是
- 在循环中调用
igb_fetch_rx_buffer
来从RingBuffer
中拉取数据帧 - 如果RingBuffer没有数据帧可拉取,就跳出,否则就把刚拿到的数据,做一些校验工作
- 随后进入
napi_gro_receive
这个逻辑中,接下来我们看下这个逻辑(其实根据我们第四节开头的 **收包示意图** 也大概知道了,就是将拉取的数据包保存到skb中去
)
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);
}
EXPORT_SYMBOL(napi_gro_receive);
//
static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
switch (ret) {
case GRO_NORMAL:
//接收skb
if (netif_receive_skb(skb))
ret = GRO_DROP;
break;
case GRO_DROP:
kfree_skb(skb);
break;
case GRO_MERGED_FREE:
if (NAPI_GRO_CB(skb)->free == NAPI_GRO_FREE_STOLEN_HEAD)
kmem_cache_free(skbuff_head_cache, skb);
else
__kfree_skb(skb);
break;
case GRO_HELD:
case GRO_MERGED:
break;
}
return ret;
}
static int __netif_receive_skb(struct sk_buff *skb)
{
int ret;
if (sk_memalloc_socks() && skb_pfmemalloc(skb)) {
unsigned long pflags = current->flags;
/*
* PFMEMALLOC skbs are special, they should
* - be delivered to SOCK_MEMALLOC sockets only
* - stay away from userspace
* - have bounded memory usage
*
* Use PF_MEMALLOC as this saves us from propagating the allocation
* context down to all allocation sites.
*/
current->flags |= PF_MEMALLOC;
// 标记A
ret = __netif_receive_skb_core(skb, true);
tsk_restore_flags(current, pflags, PF_MEMALLOC);
} else
ret = __netif_receive_skb_core(skb, false);
return ret;
}
由于代码调用链有点长,我们不继续深跟,这里只给出关键的一步,也就是 上段代码 标记A : __netif_receive_skb_core
函数中的(看下图:)
这块代码中的 ptype_base
中的内容 其实就是ip协议的处理方法: ip_rcv()
的函数引用!(不记得可以去第三节的 协议栈注册中找下)
而deliver_skb
里边是做什么我们应该从函数名就可以看出来(递交skb)至于递交给谁,我们应该可以猜出来,就是我们在协议栈注册小节中讲到的 ip_rcv()
函数。 (ps:由于我们本文不关注 网络层的arp
协议,所以我们这里只讨论 ip_rcv
而不去关注arp_rcv
)。
协议栈处理数据包(skb)
网络层处理(IP协议举例)
我们接着上一节来说,上一节我们说到从RingBuffer环形环上扒到 数据后封装成skb(socket buffer) , 通过 deliver_skb
函数 来传递给了ip_rcv
, 所以我们看下ip_rcv
里边是个什么逻辑
linux-3.10.1/net/ipv4/ip_input.c
/*
* Main IP Receive routine.
*/
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
...
IP_UPD_PO_STATS_BH(dev_net(dev), IPSTATS_MIB_IN, skb->len);
//一些检查
...
...
/* Remove any debris in the socket control block */
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
/* Must drop socket now because of tproxy. */
skb_orphan(skb);
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);//ip_rcv_finish是主要的处理逻辑
csum_error:
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_CSUMERRORS);
inhdr_error:
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
drop:
kfree_skb(skb);
out:
return NET_RX_DROP;
}
从上边可以看到在ip_rcv
中做了这些工作:
检查报文首部和ip协议版本号,
检查ip首部的校验和,以及ip数据包的总长度,校验通过后,
数据包进入PREROUTING链,如果通过该链(说明一定通过了防火墙了),则将数据包传递给ip_rcv_finish
处理
PREROUTING的作用是 : 目的地 地址转换,要把别人的公网IP换成你们内部的IP,才可以访问到你们内部受防火墙保护的机器
接下来我们看下 ip_rcv_finish
linux-3.10.1/net/ipv4/ip_input.c
static int ip_rcv_finish(struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
...
/*
* Initialise the virtual path cache for the packet. It describes
* how the packet travels inside Linux networking.
//初始化数据包的虚拟路径缓存。 它描述了
//数据包如何在 Linux 网络中传输。
*/
if (!skb_dst(skb)) {
//通过路由子系统 查询路由函数 ,在下边的 dst_output函数其实就是执行的 :
//ip_local_deliver(可以看下图:)
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, skb->dev);
if (unlikely(err)) {
if (err == -EXDEV)
NET_INC_STATS_BH(dev_net(skb->dev),
LINUX_MIB_IPRPFILTER);
goto drop;
}
}
#ifdef CONFIG_IP_ROUTE_CLASSID
...
#endif
if (iph->ihl > 5 && ip_rcv_options(skb))
goto drop;
rt = skb_rtable(skb);
if (rt->rt_type == RTN_MULTICAST) {
IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INMCAST,
skb->len);
} else if (rt->rt_type == RTN_BROADCAST)
IP_UPD_PO_STATS_BH(dev_net(rt->dst.dev), IPSTATS_MIB_INBCAST,
skb->len);
//分发流转数据包, 这里边会决定是转发,还是递交给上层 (即传输层)。
return dst_input(skb);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
下图:(可以看到路由子系统(ip_route_input_noref
)中, 最终调的函数是 ip_route_input_mc
, 它里边可以看到 上段代码的 分发逻辑:dst_output
其实是 执行的 ip_local_deliver
里的逻辑)
在上边代码的 dst_input(skb) 处, 数据包将会进行转发,或者向上层(传输层)传递。
/* Input packet from network to transport. */
// 从网络层输入数据包到传输层!!!
static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}
由于在上边我们说了,dst_input
其实是执行的ip_local_deliver
里的逻辑,所以我们看下这个里边都是怎么处理的。
(注意:由于
dst_input
里可能是转发到其他机器,也可能是递交给传输层处理(也就是本机处理),在这里我们不对转发逻辑展开 (转发其实就不是ip_local_deliver
了而是ip_forward
了),只讨论向传输层递交的逻辑)
接下来我们直接进入ip_local_deliver
的逻辑,看看都做了哪些工作
/linux-3.10.1/net/ipv4/ip_input.c
/*
* Deliver IP Packets to the higher protocol layers.
* 将 IP 数据包传送到更高的协议层!!!
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
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);
}
可以从上边这个函数的注释看到,这个函数的作用就是向上层递交数据包!(这里的上层就是传输层)
但其实真正的逻辑是在这里边ip_local_deliver_finish
源码如下:
linux-3.10.1/net/ipv4/ip_input.c
static int ip_local_deliver_finish(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
__skb_pull(skb, ip_hdrlen(skb));
/* Point into the IP datagram, just past the header. */
skb_reset_transport_header(skb);
rcu_read_lock();
{
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
int raw;
resubmit:
raw = raw_local_deliver(skb, protocol);
//从这里我们可以看到,他会根据协议找到对应的函数引用,然后来执行对应的函数
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot != NULL) {
int ret;
if (!ipprot->no_policy) {
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
kfree_skb(skb);
goto out;
}
nf_reset(skb);
}
ret = ipprot->handler(skb);
if (ret < 0) {
protocol = -ret;
goto resubmit;
}
IP_INC_STATS_BH(net, IPSTATS_MIB_INDELIVERS);
} else {
...
}
}
out:
rcu_read_unlock();
return 0;
}
上边这个函数通过 ipprot = rcu_dereference(inet_protos[protocol]);
,这段代码,来从 inet_protos
找到对应的协议栈的函数引用(关于inet_protos
我们在协议栈注册那一节说过,忘记的可以翻翻,本质上他是一个哈希表,里边保存的是各个传输层协议(udp tcp icmp
)的协议处理方法(udp_rcv() 或 tcp_v4_rcv() 或 icmp_rcv()
)的引用)
之后开始执行找到的方法即 ret = ipprot->handler(skb);
这段代码。
由此,数据包完成了从网络层到传输层的流转工作,接下来的工作就是传输层的了!
传输层处理(TCP 协议举例)
这里我们将选择tcp来进行传输层处理数据包的讲解
由于在上边 网络层处理 结尾时,我们说到tcp是以tcp_v4_rcv()来处理数据包的。所以我们看下这个函数的内容,源码如下:
/*
* From tcp_input.c
*/
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
int ret;
struct net *net = dev_net(skb->dev);
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
//一些校验工作
/* Count it even if it's bad */
TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
th = tcp_hdr(skb);
if (th->doff < sizeof(struct tcphdr) / 4)
goto bad_packet;
if (!pskb_may_pull(skb, th->doff * 4))
goto discard_it;
/* An explanation is required here, I think.
* Packet length and doff are validated by header prediction,
* provided case of th->doff==0 is eliminated.
* So, we defer the checks. */
if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
goto csum_error;
th = tcp_hdr(skb);
iph = ip_hdr(skb);
//设置TCP_CB
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->when = 0;
TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)->sacked = 0;
//根据数据包 header 中的 ip、端口信息查找到对应的socket, 也就是 查找连接信息
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
process:
//根据上一步找到的socket连接的状态,来进行不同的处理逻辑
//TIME_WAIT 状态的处理
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;//time_wait的真正实现
if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
NET_INC_STATS_BH(net, LINUX_MIB_TCPMINTTLDROP);
goto discard_and_relse;
}
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
nf_reset(skb);
if (sk_filter(sk, skb))
goto discard_and_relse;
skb->dev = NULL;
bh_lock_sock_nested(sk);
ret = 0;
if (!sock_owned_by_user(sk)) {
#ifdef CONFIG_NET_DMA
struct tcp_sock *tp = tcp_sk(sk);
if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
tp->ucopy.dma_chan = net_dma_find_channel();
if (tp->ucopy.dma_chan)
//非time_wait 的逻辑,里边会有 ESTABLISHED 和 LISTEN 状态的对应处理逻辑
ret = tcp_v4_do_rcv(sk, skb);
else
#endif
{
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
}
} else if (unlikely(sk_add_backlog(sk, skb,
sk->sk_rcvbuf + sk->sk_sndbuf))) {
bh_unlock_sock(sk);
NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
goto discard_and_relse;
}
bh_unlock_sock(sk);
sock_put(sk);
return ret;
no_tcp_socket: //不是socket的情况处理
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
csum_error:
TCP_INC_STATS_BH(net, TCP_MIB_CSUMERRORS);
bad_packet:
TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(NULL, skb);
}
discard_it:
/* Discard frame. */ 丢弃掉数据帧
kfree_skb(skb);
return 0;
discard_and_relse:
sock_put(sk);
goto discard_it;
do_time_wait:
//进行time_wait相关处理
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
if (skb->len < (th->doff << 2)) {
inet_twsk_put(inet_twsk(sk));
goto bad_packet;
}
if (tcp_checksum_complete(skb)) {
inet_twsk_put(inet_twsk(sk));
goto csum_error;
}
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
&tcp_hashinfo,
iph->saddr, th->source,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
/* Fall through to ACK */
}
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
goto no_tcp_socket;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
从上源码可以看到,tcp_rcv函数大概流程为:
- 一些校验
- 根据数据包
header
中的ip
、端口
信息查找到对应的socket信息(连接信息) - 根据上一步找到的
socket
连接的状态,来进行不同的处理逻辑 time_wait
状态下的处理逻辑,是在tcp_rcv
中处理的, ESTABLISHED 和 LISTEN 状态的对应处理逻辑 是在tcp_v4_do_rcv
中进行的
在这里,我们对其他状态的处理逻辑不做过多研究,只看ESTABLISHED和LISTEN
下的处理是什么样的
于是找到tcp_v4_do_rcv
的源码, 如下:
linux-3.10.1/net/ipv4/tcp_input.c
/* The socket must have it's spinlock held when we get
* here.
*
* We have a potential double-lock case here, so even when
* doing backlog processing we use the BH locking scheme.
* This is because we cannot sleep with the original spinlock
* held.
*/
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
#ifdef CONFIG_TCP_MD5SIG
//一些校验
if (tcp_v4_inbound_md5_hash(sk, skb))
goto discard;
#endif
// 当前tcp连接状态是 ESTABLISHED 的处理逻辑
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
dst->ops->check(dst, 0) == NULL) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
//ESTABLISHED状态下 会进入此函数
if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
return 0;
}
if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb))
goto csum_err;
//当前tcp连接状态是 LISTEN 的处理逻辑
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) {
sock_rps_save_rxhash(nsk, skb);
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
}
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
从上边我们可以看出, ESTABLISHED
状态下进入的函数是:tcp_rcv_established
我们找下 tcp_rcv_established
的源码:
注意:由于
tcp_rcv_established
这里边的代码太多 我们把其他不关注的一律都删掉(但是我们尽量把注释留下,这样可能对理解代码逻辑起到一定帮助作用)
/*
* TCP receive function for the ESTABLISHED state.
*
* It is split into a fast path and a slow path. The fast path is
* disabled when:
* - A zero window was announced from us - zero window probing
* is only handled properly in the slow path.
* - Out of order segments arrived.
* - Urgent data is expected.
* - There is no buffer space left
* - Unexpected TCP flags/window values/header lengths are received
* (detected by checking the TCP header against pred_flags)
* - Data is sent in both directions. Fast path only supports pure senders
* or pure receivers (this means either the sequence number or the ack
* value must stay constant)
* - Unexpected TCP option.
*
* When these conditions are not satisfied it drops into a standard
* receive procedure patterned after RFC793 to handle all cases.
* The first three cases are guaranteed by proper pred_flags setting,
* the rest is checked inline. Fast processing is turned on in
* tcp_data_queue when everything is OK.
*/
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
struct tcp_sock *tp = tcp_sk(sk);
if (unlikely(sk->sk_rx_dst == NULL))
inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb);
/*
* Header prediction.
* The code loosely follows the one in the famous
* "30 instruction TCP receive" Van Jacobson mail.
*
* Van's trick is to deposit buffers into socket queue
* on a device interrupt, to call tcp_recv function
* on the receive process context and checksum and copy
* the buffer to user space. smart...
*
* Our current scheme is not silly either but we take the
* extra cost of the net_bh soft interrupt processing...
* We do checksum and copy also but from device to kernel.
*/
tp->rx_opt.saw_tstamp = 0;
/* pred_flags is 0xS?10 << 16 + snd_wnd
* if header_prediction is to be made
* 'S' will always be tp->tcp_header_len >> 2
* '?' will be 0 for the fast path, otherwise pred_flags is 0 to
* turn it off (when there are holes in the receive
* space for instance)
* PSH flag is ignored.
*/
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len;
......
#ifdef CONFIG_NET_DMA
....
#endif
if (
.........
/* Bulk data transfer: receiver */
//接收数据到队列中 !!! 重要的一个环节
eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
&fragstolen);
}
tcp_event_data_recv(sk, skb);
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* Well, only one small jumplet in fast path... */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk);
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
if (!copied_early || tp->rcv_nxt != tp->rcv_wup)
__tcp_ack_snd_check(sk, 0);
...
#endif
if (eaten)
kfree_skb_partial(skb, fragstolen);
//该条socket连接上的数据已经准备好(或者说有数据就绪),
//则唤醒 socket 上阻塞掉的用户进程 !!也是重要的一步
sk->sk_data_ready(sk, 0);
return 0;
}
}
csum_error:
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_CSUMERRORS);
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);
discard:
__kfree_skb(skb);
return 0;
}
从上边的 tcp_rcv_established
可以看出来,该函数内做了两个我们关注的事情
- 接收数据到队列(
sk_receive_queue
)中, 通过tcp_queue_rcv
函数 - 该条
socket
连接上的数据已经准备好(或者说有数据就绪),则唤醒在该socket上阻塞的用户进程 !
注意:唤醒用户进程后,还伴随着一次进程状态的切换
另外,唤醒阻塞的用户进程后的工作也有很多,但是我们在这里不准备展开,而是准备在后续的tcp详解时,做对应的展开分析。
到这里,内核收包的理论工作基本就差不多了,下边我们用个小例子,把理论和实际结合一下
上边讲的理论与实际结合
在本小节,我为了结合实际,所以做个小demo,将我们前几个小节讲的:
- 网卡收包
- DMA到RingBuffer
- 发出硬中断通知
- 处理硬中断后发出软中断
- 处理软中断(调用网卡初始化时候注册的poll函数从RingBuffer上获取数据包)
- 将获取到的数据包copy成skb
- 调用ip_rcv()进行ip层的处理
- ip层处理完后传递给传输层处理通过tcp_v4_rcv
- tcp层校验通过后将数据包放入sk_receive_queue接收队列
- 内核唤醒等待(阻塞)的用户进程,之后进行数据copy到用户空间同时进行进程状态切换
这些东西和实际编码结合起来!
BIOServer 源码如下:
/**
* @Author: hzz
* @Date: 2022/10/15 20:05:27
* @Description:
*/
public class BioServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocket并且监听6666端口
ServerSocket serverSocket = new ServerSocket(6666);
while (true) {
// 监听---一直等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("有客户端连接来了,此时已经是三次握手完成状态"+socket.toString());
try {
// 获取客户端发送过来的输入流
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int read = inputStream.read(bytes);
System.out.println("有数据报来了 read:"+read);
// 读取发送过来的信息并打印
if (read != -1) {
System.out.println(new String(bytes, 0, read));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 断开通讯
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
第一步:启动BIOServer
debug 启动main方法,没什么好说的,略。
第二步:telnet与本地 6666端口
建立一条tcp连接
第三步:阻塞在了socket.getInputStream().read()
方法
由于连接已经建立,所以accept不再阻塞,走到 int read = inputStream.read(bytes);
这行代码后,阻塞在了read
方法, 见下图:
第四步:(客户端)输入点什么,然后回车,使得数据发送到本机服务端
第五步:内核处理(收包分析章节的理论内容)
第五步是最关键的一步,也是我们上边讲了那么多的理论和实际(本demo)结合的一步。
注意:实际上,第五步整个的过程,在我们开发编码时,都是无感知的。可见写内核的大佬们是多么的煞费苦心 第五步细节如下:
- 网卡收到数据包
- 通过DMA将数据包保存到RingBuffer环形队列中去
- 向cpu发出硬中断通知
- 处理硬中断后发出软中断
- 处理软中断(调用网卡初始化时候注册的poll函数从RingBuffer上获取数据包)
- 将获取到的数据包copy成skb
- 调用ip_rcv()进行ip层的处理
- ip层处理完后传递给传输层
- 传输层通过tcp_v4_rcv对包进行处理:校验通过后将数据包放入sk_receive_queue接收队列
- 内核唤醒等待(阻塞)的用户进程(对应本demo的话就是调用read方法阻塞的main-java进程),之后进行数据copy到用户空间同时进行进程状态切换为用户态
在第10步骤完成后,inputStream.read(bytes) 方法将退出阻塞状态,返回读到的字节长度!于是我们来到了下边的第六步,如下:
第六步:可以看到服务端的read不再阻塞,而是到了下一行,即打印了一句话,见下图:
第七步:读到数据后,进行业务处理
do somthing .....
到此,我们第四节的收包分析就完成了。在本节最后,我们用了一个小demo,来将本节收包分析的理论内容和实际编码做了一个结合,希望此举措对理解本文有所帮助。
五、总结
到此,本文开篇中的两个问题:
- 问题1: 在收发数据包之前,底层系统做了哪些准备工作?
- 问题2: 接收到数据包后,内核是如何处理的?又是如何向上给到应用层的?
就算讲完了,问题1 对应的解答是第三小节(Linux内核启动与初始化)
;问题2 对应的解答是在第四节(收包分析)
下边,我们来总结一下本文所讲的内容:
内核启动和初始化总结:
注意:真正的内核初始化和启动工作远远不止我们下边总结的这些,我们关注的只是本文所涉及到的。
- 内核初始化
- 内核初始化和启动的总入口是
start_kernel
,在这个函数中会启动各个模块(大约100多个),以此来构建出最基础的内核环境。 - 在初始化内核这个环节,与本文联系密切的
ksoftirqd
内核线程被创建和启动
- 内核初始化和启动的总入口是
- 网络子系统初始化
- 第一件事: 为收发包类型的软中断注册了 各自对应的处理函数,好在后边接收到这种(出去 or 进来的)软中断通知时,来从数组(
softirq_vec
)中拿到对应的函数的引用
,来调用对应软中断处理函数来执行对应的软中断逻辑
! - 第二件事: 为每个CPU都申请一个
softnet_data
数据结构,在这个数据结构里的poll_list
作用是:等待各种类型的驱动程序
将其poll
函数注册进来用于后续使用,(poll
函数用于拉取RingBuffer
上的数据包
- 第一件事: 为收发包类型的软中断注册了 各自对应的处理函数,好在后边接收到这种(出去 or 进来的)软中断通知时,来从数组(
- 协议栈注册
- linux实现了很多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下(如网络层是存到了:
ip_packet_type
,传输层协议是存到了:inet_protos
中),方便包来了迅速找到对应的处理函数来执行。
- linux实现了很多协议,比如arp,icmp,ip,udp,tcp,每一个协议都会将自己的处理函数注册一下(如网络层是存到了:
- 网卡初始化
- 在网卡初始化过程中,逻辑其实很复杂,我们这里只关注他注册的两个函数
igb_open
和igb_poll
,因为igb_open
是网卡启动的入口,igb_poll
是从RingBuffer
上获取数据包的关键!
- 在网卡初始化过程中,逻辑其实很复杂,我们这里只关注他注册的两个函数
- 网卡启动
- 给
发送/接收
队列分配资源 - 开启
NAPI
机制 - 为不同网卡注册了
硬中断
处理函数 和设置硬中断通知的方式
- 给
到此,内核初始化和启动完成,可以收包了!
收包总结:
收包总结的话,我想下边这张图,会更直观,印象更深:
但是我们还是用文字简单来解释一下上图的流程:
- 网卡收到数据包
- 通过DMA将数据包保存到RingBuffer环形队列中去
- 向cpu发出硬中断通知
- 处理硬中断(网卡启动时注册的)后发出软中断
- 处理软中断(调用网卡初始化时候注册的poll函数从RingBuffer上获取数据包)
- 将获取到的数据包copy成skb
- 调用ip_rcv()进行ip层的处理
- ip层处理完后传递给传输层
- 传输层通过tcp_v4_rcv对包进行处理:校验通过后将数据包放入sk_receive_queue接收队列
- 内核唤醒等待(阻塞)的用户进程,之后进行数据copy到用户空间同时将进程状态切换为用户态
站在巨人的肩膀,才能看的更远!
本篇文章部分参考: 参考2到此,本文完,如有建议或指正欢迎留言,一起学习进步,扎实基础!
转载自:https://juejin.cn/post/7154997831118356493