TCP/IP协议栈在Linux内核中的运行时序分析
目录调研要求
- 在深入理解Linux内核任务调度(中断处理、softirg、tasklet、wq、内核线程等)机制的基础上,分析梳理send和recv过程中TCP/IP协议栈相关的运行任务实体及相互协作的时序分析。
- 编译、部署、运行、测评、原理、源代码分析、跟踪调试等
- 应该包括时序图
Linux内核基础简介
内核初始化
Linux内核的启动从入口函数start_kernel开始,在init/main.c中,start_kernel就相当于内核的main函数。该函数中调用了很多的xxx_init函数。
创建0号进程
set_task_stack_end_magic(&init_task);
1
init_task是系统创建第一个进程,也称为0号进程,这也是唯一一个没有使用fork或者kernel_thread产生的进程,是进程列表的第一个,它的定义如下:
struct task_struct init_task{
.state = 0,
.stack = init_stack,
.usage = ATOMIC_INIT(2),
.flags = PF_KTHREAD,
.prio = MAX_PRIO - 20,
.static_prio = MAX_PRIO - 20,
.normal_prio = MAX_PRIO - 20,
.policy = SCHED_NORMAL,
.cpus_allowed = CPU_MASK_ALL,
.nr_cpus_allowed= NR_CPUS,
.mm = NULL,
.active_mm = &init_mm,
.restart_block = {
.fn = do_no_restart_syscall,
},
.se = {
.group_node = LIST_HEAD_INIT(init_task.se.group_node),
},
.rt = {
.run_list = LIST_HEAD_INIT(init_task.rt.run_list),
.time_slice = RR_TIMESLICE,
},
.tasks = LIST_HEAD_INIT(init_task.tasks),
.ptraced = LIST_HEAD_INIT(init_task.ptraced),
.ptrace_entry = LIST_HEAD_INIT(init_task.ptrace_entry),
.real_parent = &init_task,
.parent = &init_task,
.children = LIST_HEAD_INIT(init_task.children),
.sibling = LIST_HEAD_INIT(init_task.sibling),
.group_leader = &init_task,
RCU_POINTER_INITIALIZER(real_cred, &init_cred),
RCU_POINTER_INITIALIZER(cred, &init_cred),
.comm = INIT_TASK_COMM,
.thread = INIT_THREAD,
.fs = &init_fs,
.files = &init_files,
.signal = &init_signals,
.sighand = &init_sighand,
.nsproxy = &init_nsproxy,
.pending = {
.list = LIST_HEAD_INIT(init_task.pending.list),
.signal = {{0}}
},
.blocked = {{0}},
.alloc_lock = __SPIN_LOCK_UNLOCKED(init_task.alloc_lock),
.journal_info = NULL,
INIT_CPU_TIMERS(init_task)
.pi_lock = __RAW_SPIN_LOCK_UNLOCKED(init_task.pi_lock),
.timer_slack_ns = 50000, /* 50 usec default slack */
.thread_pid = &init_struct_pid,
.thread_group = LIST_HEAD_INIT(init_task.thread_group),
.thread_node = LIST_HEAD_INIT(init_signals.thread_head),
#ifdef CONFIG_PREEMPT_RCU
.rcu_read_lock_nesting = 0,
.rcu_read_unlock_special.s = 0,
.rcu_node_entry = LIST_HEAD_INIT(init_task.rcu_node_entry),
.rcu_blocked_node = NULL,
INIT_PREV_CPUTIME(init_task)
}
中断门
函数trap_init里面设置了很多的中断门,用于处理各种中断,其中的 SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),就是系统调用的中断门,用户态进程调用系统调用也是通过发送中断的方式进行的。
函数调用关系如下
trap_init
-- idt_setup_traps();
- idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
其中def_idts的详细定义如下,这里面就定义了各种中断门。
static const __initconst struct idt_data def_idts[] = {
INTG(X86_TRAP_DE, divide_error),
INTG(X86_TRAP_NMI, nmi),
INTG(X86_TRAP_BR, bounds),
INTG(X86_TRAP_UD, invalid_op),
INTG(X86_TRAP_NM, device_not_available),
INTG(X86_TRAP_OLD_MF, coprocessor_segment_overrun),
INTG(X86_TRAP_TS, invalid_TSS),
INTG(X86_TRAP_NP, segment_not_present),
INTG(X86_TRAP_SS, stack_segment),
INTG(X86_TRAP_GP, general_protection),
INTG(X86_TRAP_SPURIOUS, spurious_interrupt_bug),
INTG(X86_TRAP_MF, coprocessor_error),
INTG(X86_TRAP_AC, alignment_check),
INTG(X86_TRAP_XF, simd_coprocessor_error),
#ifdef CONFIG_X86_32
TSKG(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS),
#else
INTG(X86_TRAP_DF, double_fault),
#endif
INTG(X86_TRAP_DB, debug),
#ifdef CONFIG_X86_MCE
INTG(X86_TRAP_MC, &machine_check),
#endif
SYSG(X86_TRAP_OF, overflow),
#if defined(CONFIG_IA32_EMULATION)
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_compat),
#elif defined(CONFIG_X86_32)
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),
#endif
};
mm_init函数用于初始化内存管理模块。
sched_init函数用于初始化内核调度模块。主要代码如下:
for_each_possible_cpu(i){
struct rq *rq;
rq = cpu_rq(i);
raw_spin_lock_init(&rq->lock);
rq->nr_running = 0;
rq->calc_load_active = 0;
rq->calc_load_update = jiffies + LOAD_FREQ;
init_cfs_rq(&rq->cfs);
init_rt_rq(&rq->rt);
init_dl_rq(&rq->dl);
hrtick_rq_init(rq);
atomic_set(&rq->nr_iowait, 0);
}
1234567891011121314
上面的代码中首先取到每个CPU上的进程运行队列,初始化每一个进程的自旋锁,然后又一次初始化rq的字队列cfs_rq、rt_rq和dl_rq等。
文件系统初始化
vfs_caches_init();用来初始化基于内存的文件系统rootfs,该函数会一次调用mnt_init()->init_rootfs().
int __init init_rootfs(void)
{
int err = register_filesystem(&rootfs_fs_type);
if (err)
return err;
if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
(!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
err = shmem_init();
is_tmpfs = true;
} else {
err = init_ramfs_fs();
}
if (err)
unregister_filesystem(&rootfs_fs_type);
return err;
}
上面的register_filesystem在linux的虚拟文件系统里面注册了一种文件系统类型为rootfs_fs_type。
最后start_kernel调用了rest_init用来做其他方面的初始化工作。
创建1号进程
rest_init的第一个工作就是调用kernel_thread创建第二个进程也就是1号进程。
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
1号进程是系统运行的第一个用户进程,1号进程创建的时候还在内核态,那么又是怎么切换到用户态的呢?
Kernel_thread的第一个参数是kernel_init,进程创建后就会运行这个kernel_init函数。函数具体实现如下
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
ftrace_free_init_mem();
jump_label_invalidate_initmem();
free_initmem();
mark_readonly();
/*
* Kernel mappings are now finalized - update the userspace page-table
* to finalize PTI.
*/
pti_finalize();
system_state = SYSTEM_RUNNING;
numa_default_policy();
rcu_end_inkernel_boot();
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
}
其中在kernel_init_freeable函数有如下的调用代码
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
这里将ramdisk_execute_comman设置为"/init"
然后kernel_init后面还有下面的代码
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
此时ramdisk_execute_command不为空程序将执行run_init_process函数,在该函数内部调用do_execve了函数,这里就是系统调用execve函数的内核实现,他的作用就是运行一个文件,因此程序运行到这里就会尝试运行ramdisk的init进程或者是普通文件系统上的/sbin/init、"/etc/init"、"/bin/init"、"/bin/sh"。
创建2号进程
rest_init的第二个工作就是调用kernel_thread创建第三个进程 即2号进程。
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
这里有一次使用了kernel_thread函数创建进程,这里创建的kthreadd负责所有内核态的线程调度和管理,是内核态所有线程的祖先。Kthreadd的主要代码如下
For(;;){
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
spin_unlock(&kthread_create_lock);
create_kthread(create);
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
该函数的核心就是一个for循环和一个while循环,for循环中首先将线程状态设置为可中断的睡眠状态,然后判断线程链表是否为空,如果为空则执行一次线程调度让出CPU。如果变成链表不为空则进入while循环,最终会进入到create_kthread函数
create_kthread
- pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
- _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL, 0);
最终会调用到_do_fork函数完成线程创建。
用户态和内核态的划分
在原来只有0号进程的时候,操作系统所有的资源都可以使用,是没有竞争关系的,也无需担心被恶意破坏,有了1号进程之后,需要区分核心资源和非核心资源,x86提供了分层的权限机制,将操作系统中的资源分为了4个Ring,如下图所示
越靠近内核区域,需要的权限也就越高,操作系统很好地利用了x86的分层权限机制:
- 将可以访问的关键资源的代码放在Ring0,将其称为内核态(Kernel Mode)
- 将普通的程序代码放在Ring3,将其称为用户态(User Mode)
如果用户态的代码需要访问核心资源,需要通过系统调用来进行访问
系统调用
操作系统中的状态切换如下图所示:
在x86系统中,软件中断是由int $0x80
指令产生,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。
用户空间的程序无法直接执行内核代码,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。
延后中断
中断处理会有一些特点,其中最主要的两个是:
- 中断处理必须快速执行完毕
- 有时中断处理必须做很多冗长的事情
因此中断被切分为两部分:
- 前半部
- 后半部
中断处理代码运行于中断处理上下文中,此时禁止响应后续的中断,所以要避免中断处理代码长时间执行。但有些中断却又需要执行很多工作,所以中断处理有时会被分为两部分。第一部分中,中断处理先只做尽量少的重要工作,接下来提交第二部分给内核调度,然后就结束运行。当系统比较空闲并且处理器上下文允许处理中断时,第二部分被延后的剩余任务就会开始执行。
目前实现延后中断有如下三种途径:
- 软中断(softirq)
- tasklets
- 工作队列(wq)
Softirq(软中断)
伴随着内核对并行处理的支持,出于性能考虑,所有新的下半部实现方案都基于被称之为 ksoftirqd
。每个处理器都有自己的内核线程,名字叫做 ksoftirqd/n
,n是处理器的编号,由 spawn_ksoftirqd
函数启动这些线程。
软中断在 Linux 内核编译时就静态地确定了。open_softirq
函数负责 softirq
初始化,它的定义如下所示:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
这个函数有两个参数:
-
softirq_vec
数组的索引序号 - 一个指向软中断处理函数的指针
softirq_vec
数组包含了 NR_SOFTIRQS
(其值为10)个不同 softirq
类型的 softirq_action
。当前版本的 Linux 内核定义了十种软中断向量。其中两个 tasklet 相关,两个网络相关,两个块处理相关,两个定时器相关,另外调度器和 RCU 也各占一个。所有这些都在一个枚举中定义:
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
};
可以看到 softirq_vec
数组的类型为 softirq_action
。这是软中断机制里一个重要的数据结构,它只有一个指向中断处理函数的成员:
struct softirq_action
{
void (*action)(struct softirq_action *);
};
open_softirq
函数实际上用 softirq_action
参数填充了 softirq_vec
数组。由 open_softirq
注册的延后中断处理函数会由 raise_softirq
调用。这个函数只有一个参数 -- 软中断序号 nr
。下面是该函数的代码:
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
raise_softirq_irqoff
函数设置当前处理器上和nr参数对应的软中断标志位(__softirq_pending
)。这是通过以下代码做到的:
__raise_softirq_irqoff(nr);
然后,通过 in_interrupt
函数获得 irq_count
值,该函数可以用来检测一个cpu是否处于中断环境,如果处于中断上下文中,就退出 raise_softirq_irqoff
函数,装回 IF
标志位并允许当前处理器的中断。如果不在中断上下文中,就会调用 wakeup_softirqd
函数,wakeup_softirqd
函数会激活当前处理器上的 ksoftirqd
内核线程,其代码如下所示:
static void wakeup_softirqd(void)
{
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
ksoftirqd
内核线程都运行 run_ksoftirqd
函数来检测是否有延后中断需要处理,如果有的话就会调用 __do_softirq
函数。__do_softirq
读取当前处理器对应的 __softirq_pending
软中断标记,并调用所有已被标记中断对应的处理函数。
每个 softirq
都有如下的阶段:通过 open_softirq
函数注册一个软中断,通过 raise_softirq
函数标记一个软中断来激活它,然后所有被标记的软中断将会在 Linux 内核下一次执行周期性软中断检测时得以调度,对应此类型软中断的处理函数也就得以执行。
从上述可看出,软中断是静态分配的,这对于后期加载的内核模块将是一个问题。基于软中断实现的 tasklets
解决了这个问题。
tasklet
tasklets
构建于 softirq
中断之上,他是基于下面两个软中断实现的:
-
TASKLET_SOFTIRQ
; -
HI_SOFTIRQ
.
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
1)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
2)多个不同类型的tasklet可以并行在多个CPU上。
3)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。也就是说tasklet是软中断的一种特殊用法,即延迟情况下的串行执行。
相关描述符和数据结构定义如下:
1 //tasklet描述符
3 struct tasklet_struct
5 { struct tasklet_struct *next;//将多个tasklet链接成单向循环链表
7 unsigned long state;//TASKLET_STATE_SCHED(Tasklet is scheduled for execution) TASKLET_STATE_RUN(Tasklet is running (SMP only))
9 atomic_t count;//0:激活tasklet 非0:禁用tasklet
11 void (*func)(unsigned long); //用户自定义函数
13 unsigned long data; //函数入参
15 };
17 //tasklet链表
19 static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);//低优先级
21 static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);//高优先级
23 //相关API
25 //定义tasklet
27 #define DECLARE_TASKLET(name, func, data) \
29 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
31 //定义名字为name的非激活tasklet
33 #define DECLARE_TASKLET_DISABLED(name, func, data) \
35 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
37 //定义名字为name的激活
39 taskletvoid tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
41 //动态初始化tasklet
43 //tasklet操作
45 static inline void tasklet_disable(struct tasklet_struct *t)
47 //函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet被再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出
49 static inline void tasklet_enable(struct tasklet_struct *t)
51 //使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"
53 static inline void tasklet_schedule(struct tasklet_struct *t)
55 //调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己
57 tasklet_hi_schedule(struct tasklet_struct *t)
59 //和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期.
61 tasklet_kill(struct tasklet_struct *t)//确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执
wq(工作队列)
工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。这也就是的它具有自己的特性:
工作队列会在进程上下文中执行
1)可以阻塞
2)可以重新调度
3)缺省工作者线程(kthrerad worker && kthread work)
4)在工作队列和其它内核间用锁和其它进程上下文一样
5)默认允许响应中断
6)默认不持有任何锁
工作队列和tasklet的选择比较重要,由各自的工作特性可以得知如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。工作队列的源码描述如下:
1 //结构体代码:
3 struct workqueue_struct {
5 struct cpu_workqueue_struct *cpu_wq;
7 //指针数组,其每个元素为per-cpu的工作队列
9 struct list_head list; const char *name; int singlethread;
11 //标记是否只创建一个工作者线程
13 nt freezeable; /* Freeze threads during suspend */
15 int rt;
17 #ifdef CONFIG_LOCKDEP
19 struct lockdep_map lockdep_map;
21 #endif };
23 //相关API如下:
25 //静态创建
27 DECLARE_WORK(name,function);
29 //定义正常执行的工作项
31 DECLARE_DELAYED_WORK(name,function);
33 //定义延后执行的工作项
35 动态创建 INIT_WORK(_work, _func)
37 //创建正常执行的工作项
39 INIT_DELAYED_WORK(_work, _func
41 )//创建延后执行的工作项
内核线程
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程经常被称之为内核守护进程。
他们执行下列任务
- 周期性地将修改的内存页与页来源块设备同步
- 如果内存页很少使用,则写入交换区
- 管理延时动作, 如2号进程接手内核进程的创建
- 实现文件系统的事务日志
内核线程主要有两种类型
- 线程启动后一直等待,直至内核请求线程执行某一特定操作。
- 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。
内核线程由内核自身生成,其特点在于
- 它们在CPU的管态执行,而不是用户态。
- 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间
内核线程是一种只运行在内核地址空间的线程。所有的内核线程共享内核地址空间,所以也共享同一份内核页表,内核线程只运行在内核地址空间中,只会访问 3-4GB (32位系统)的内核地址空间,不存在虚拟地址空间,因此每个内核线程的 task_struct 对象中的 mm 为 NULL。普通线程虽然也是同主线程共享地址空间,但是它的 task_struct 对象中的 mm 不为空,指向的是主线程的 mm_struct 对象,普通进程既可以运行在内核态,也可以运行在用户态而内核线程只运行在内核态。
TCP/IP协议栈
基于TCP/IP协议栈的send/recv在应用层,传输层,网络层和链路层中具体函数调用过程的一张比较完善的图如下:
socket的基本使用和创建过程
应用层接口
应用层位于TCP/IP协议的最上层,工作于用户空间,使用内核提供的API对底层数据结构进行操作,其提供的函数比较多样,如:
- read()/write()
- send()/recv()
- sendto()/recvfrom()
- bind()
- connect()
- accept()
- etc...
其中,应用层socket套接字遵循unix"万物皆文件"的思想,这些接口函数,最终都将使用系统调用sys_socketcall()对socket套接字进行各种各样的操作(如:申请套接字fd,发送、接受数据。)
而后,sys_socketcall()函数根据传入的参数,对这些函数进行具体的分发处理,并转去调用真正执行这些操作的函数。
套接字的创建
主要作用:
1.为上层应用提供协议无关的通用网络编程接口。
2.为下层各种协议提供族接口和机制,使具体的协议族可以注册到系统中。
主要数据结构:
- struct net_protocol_family
- struct socket
- struct sock
- struct inet_protosw
- struct proto_ops
- struct proto
在应用程序使用套接口API进行数据的发送与接受前,需要先完成SOCKET的创建工作,所使用的是各个协议族对应的sock_creat()数。
- inet_creat()
- inet6_creat()
- unix_creat()
具体流程如下图所示:
在socket创建完毕后,用户和应用程序在用户空间所能看到的只是socket对应的fd,而在内核空间中,内核保存了复杂的数据结构,并建立起他们之间的关系:
随后,用户和应用程序,通过对socket fd进行数据的读写操作,完成网络通讯。
套接口层的主要流程图如下:
完成套接口的创建后,即可对相应socket文件描述符进行相应的操作。
TCP/IP处理流程
应用层
上面已经提到了socket结构,应用层的各种网络应用程序都是通过Linux Socket编程接口来和内核空间的网络协议通信的。具体处理流程为:
- 网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
- 对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
- 应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端
- sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message
- _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数
- 对于 TCP ,调用 tcp_sendmsg 函数
- 对于 UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数
下图为socket的三个队列
传输层
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,TCP 协议栈的大致处理过程如下图所示:
TCP发送数据的简要过程:
- tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程
- 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer
- 构造 TCP header
- 计算 TCP 校验和(checksum)和 顺序号 (sequence number)
- 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程
接收数据:
- 传输层 TCP 处理入口在 tcp_v4_rcv 函数,它会做 TCP header 检查等处理
- 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态
- 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment
IP网络层
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。下图为网络层处理信息的基本流程:
发送流程:
- 首先,
ip_queue_xmit(skb)
会检查skb->dst
路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()
选择一个路由 - 接着,填充IP包的各个字段,比如版本、包头长度、TOS等
- 进行一些分片操作
- 接下来就用
ip_finish_ouput2
设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output
,使用 ARP 获取
接收流程:
-
IP 层的入口函数在
ip_rcv
函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达ip_rcv_finish
函数 -
p_rcv_finish
函数会调用
ip_router_input
函数,进入路由处理环节。它首先会调用
ip_route_input
来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
- 如果是发到本机的话,调用
ip_local_deliver
函数,可能会做 de-fragment(合并多个 IP packet),然后调用ip_local_deliver
函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括tcp_v4_rcv
(TCP),udp_rcv
(UDP),icmp_rcv
(ICMP),igmp_rcv
(IGMP)。对于 TCP 来说,函数tcp_v4_rcv
函数会被调用,从而处理流程进入 TCP 栈 - 如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用
dst_input
函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用dev_queue_xmit
,进入链路层处理流程
- 如果是发到本机的话,调用
数据链路层
功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。
send过程的Linux内核实现
传输层分析
send的定义如下所示:
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
当在调用send
函数的时候,内核封装send()
为sendto()
,然后发起系统调用。其实也很好理解,send()
就是sendto()
的一种特殊情况,而sendto()
在内核的系统调用服务程序为sys_sendto
,sys_sendto
的代码如下所示:
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
struct sockaddr __user *addr, int addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg; //用来表示要发送的数据的一些属性
struct iovec iov;
int fput_needed;
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
err = sock_sendmsg(sock, &msg); //实际的发送函数
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
__sys_sendto
函数其实做了3件事:
- 通过fd获取了对应的
struct socket
- 创建了用来描述要发送的数据的结构体
struct msghdr
- 调用了
sock_sendmsg
来执行实际的发送
继续追踪sock_sendmsg
,发现其最终调用的是sock->ops->sendmsg(sock, msg, msg_data_left(msg));
,即socet在初始化时赋值给结构体struct proto tcp_prot
的函数tcp_sendmsg
,如下所示:
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
...
而tcp_send
函数实际调用的是tcp_sendmsg_locked
函数,该函数的定义如下所示:
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/
struct sk_buff *skb;
flags = msg->msg_flags;
......
if (copied)
tcp_push(sk, flags & ~MSG_MORE, mss_now,
TCP_NAGLE_PUSH, size_goal);
}
在tcp_sendmsg_locked
中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock
结构中的一个域sk_write_queue
,这个队列的每一个元素是一个skb
,里面存放的就是待发送的数据。在该函数中通过调用tcp_push()
函数将数据加入到发送队列中。
struct sock
的部分代码如下所示:
struct sock{
...
struct sk_buff_head sk_write_queue;/*指向skb队列的第一个元素*/
...
struct sk_buff *sk_send_head;/*指向队列第一个还没有发送的元素*/
}
tcp_push
的代码如下所示:
static void tcp_push(struct sock *sk, int flags, int mss_now,
int nonagle, int size_goal)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
skb = tcp_write_queue_tail(sk);
if (!skb)
return;
if (!(flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb);
tcp_mark_urg(tp, flags);
if (tcp_should_autocork(sk, skb, size_goal)) {
/* avoid atomic op if TSQ_THROTTLED bit is already set */
if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
}
/* It is possible TX completion already happened
* before we set TSQ_THROTTLED.
*/
if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
}
if (flags & MSG_MORE)
nonagle = TCP_NAGLE_CORK;
__tcp_push_pending_frames(sk, mss_now, nonagle); //最终通过调用该函数发送数据
}
在之后tcp_push调用了__tcp_push_pending_frames(sk, mss_now, nonagle);
来发送数据
__tcp_push_pending_frames
的代码如下所示:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
int nonagle)
{
if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
sk_gfp_mask(sk, GFP_ATOMIC))) //调用该函数发送数据
tcp_check_probe_timer(sk);
}
在__tcp_push_pending_frames
又调用了tcp_write_xmit
来发送数据,代码如下所示:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
int result;
bool is_cwnd_limited = false, is_rwnd_limited = false;
u32 max_segs;
/*统计已发送的报文总数*/
sent_pkts = 0;
......
/*若发送队列未满,则准备发送报文*/
while ((skb = tcp_send_head(sk))) {
unsigned int limit;
if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
/* "skb_mstamp_ns" is used as a start point for the retransmit timer */
skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache;
list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue);
tcp_init_tso_segs(skb, mss_now);
goto repair; /* Skip network transmission */
}
if (tcp_pacing_check(sk))
break;
tso_segs = tcp_init_tso_segs(skb, mss_now);
BUG_ON(!tso_segs);
/*检查发送窗口的大小*/
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota) {
if (push_one == 2)
/* Force out a loss probe pkt. */
cwnd_quota = 1;
else
break;
}
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
is_rwnd_limited = true;
break;
......
limit = mss_now;
if (tso_segs > 1 && !tcp_urg_mode(tp))
limit = tcp_mss_split_point(sk, skb, mss_now,
min_t(unsigned int,
cwnd_quota,
max_segs),
nonagle);
if (skb->len > limit &&
unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE,
skb, limit, mss_now, gfp)))
break;
if (tcp_small_queue_check(sk, skb, 0))
break;
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) //调用该函数发送数据
break;
......
tcp_write_xmit
位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)
传输数据,实际上调用的是__tcp_transmit_skb
。
__tcp_transmit_skb
的部分代码如下所示:
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
skb_push(skb, tcp_header_size);
skb_reset_transport_header(skb);
......
/* 构建TCP头部和校验和 */
th = (struct tcphdr *)skb->data;
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq = htonl(rcv_nxt);
tcp_options_write((__be32 *)(th + 1), tp, &opts);
skb_shinfo(skb)->gso_type = sk->sk_gso_type;
if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
th->window = htons(tcp_select_window(sk));
tcp_ecn_send(sk, skb, th, tcp_header_size);
} else {
/* RFC1323: The window in SYN & SYN/ACK segments
* is never scaled.
*/
th->window = htons(min(tp->rcv_wnd, 65535U));
}
......
icsk->icsk_af_ops->send_check(sk, skb);
if (likely(tcb->tcp_flags & TCPHDR_ACK))
tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);
if (skb->len != tcp_header_size) {
tcp_event_data_sent(tp, sk);
tp->data_segs_out += tcp_skb_pcount(skb);
tp->bytes_sent += skb->len - tcp_header_size;
}
if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
tcp_skb_pcount(skb));
tp->segs_out += tcp_skb_pcount(skb);
/* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */
skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);
/* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */
/* Cleanup our debris for IP stacks */
memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
sizeof(struct inet6_skb_parm)));
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); //调用网络层的发送接口
......
}
__tcp_transmit_skb
是位于传输层发送tcp数据的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
网络层分析
网络层的调用流程为:
-
ip_queue_xmit
将TCP传输过来的数据包打包成IP数据报,将数据打包成IP数据包之后,通过调用
ip_local_out
函数,在该函数内部调用了__ip_local_out
,该函数返回了一个nf_hook
函数,在该函数内部调用了dst_output
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl) { struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); struct ip_options_rcu *inet_opt; struct flowi4 *fl4; struct rtable *rt; struct iphdr *iph; int res; /* Skip all of this if the packet is already routed, * f.e. by something like SCTP. */ rcu_read_lock(); /* * 如果待输出的数据包已准备好路由缓存, * 则无需再查找路由,直接跳转到packet_routed * 处作处理。 */ inet_opt = rcu_dereference(inet->inet_opt); fl4 = &fl->u.ip4; rt = skb_rtable(skb); if (rt) goto packet_routed; /* Make sure we can route this packet. */ /* * 如果输出该数据包的传输控制块中 * 缓存了输出路由缓存项,则需检测 * 该路由缓存项是否过期。 * 如果过期,重新通过输出网络设备、 * 目的地址、源地址等信息查找输出 * 路由缓存项。如果查找到对应的路 * 由缓存项,则将其缓存到传输控制 * 块中,否则丢弃该数据包。 * 如果未过期,则直接使用缓存在 * 传输控制块中的路由缓存项。 */ rt = (struct rtable *)__sk_dst_check(sk, 0); if (!rt) { /* 缓存过期 */ __be32 daddr; /* Use correct destination address if we have options. */ daddr = inet->inet_daddr; /* 目的地址 */ if (inet_opt && inet_opt->opt.srr) daddr = inet_opt->opt.faddr; /* 严格路由选项 */ /* If this fails, retransmit mechanism of transport layer will * keep trying until route appears or the connection times * itself out. */ /* 查找路由缓存 */ rt = ip_route_output_ports(net, fl4, sk, daddr, inet->inet_saddr, inet->inet_dport, inet->inet_sport, sk->sk_protocol, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if); if (IS_ERR(rt)) goto no_route; sk_setup_caps(sk, &rt->dst); /* 设置控制块的路由缓存 */ } skb_dst_set_noref(skb, &rt->dst);/* 将路由设置到skb中 */ packet_routed: if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway) goto no_route; /* OK, we know where to send it, allocate and build IP header. */ /* * 设置IP首部中各字段的值。如果存在IP选项, * 则在IP数据包首部中构建IP选项。 */ skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0)); skb_reset_network_header(skb); iph = ip_hdr(skb);/* 构造ip头 */ *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff)); if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df) iph->frag_off = htons(IP_DF); else iph->frag_off = 0; iph->ttl = ip_select_ttl(inet, &rt->dst); iph->protocol = sk->sk_protocol; ip_copy_addrs(iph, fl4); /* Transport layer set skb->h.foo itself. */ /* 构造ip选项 */ if (inet_opt && inet_opt->opt.optlen) { iph->ihl += inet_opt->opt.optlen >> 2; ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0); } ip_select_ident_segs(net, skb, sk, skb_shinfo(skb)->gso_segs ?: 1); /* TODO : should we use skb->sk here instead of sk ? */ /* * 设置输出数据包的QoS类型。 */ skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; res = ip_local_out(net, sk, skb); /* 输出 */ rcu_read_unlock(); return res; no_route: rcu_read_unlock(); /* * 如果查找不到对应的路由缓存项, * 在此处理,将该数据包丢弃。 */ IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); kfree_skb(skb); return -EHOSTUNREACH; }
-
dst_output
dst_output()
实际调用skb_dst(skb)->output(skb)
,skb_dst(skb)
就是skb所对应的路由项。skb_dst(skb)
指向的是路由项dst_entry,它的input在收到报文时赋值ip_local_deliver()
,而output在发送报文时赋值ip_output()
。 -
ip_output
处理单播数据报,设置数据报的输出网络设备以及网络层协议类型参数
-
ip_finish_output
观察数据报长度是否大于MTU,若大于,则调用ip_fragment分片,否则调用ip_finish_output2输出
-
ip_finish_output2
在该函数中会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数
neigh_output
进行输出,输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output
进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出
数据链路层分析
网络层最终会通过调用dev_queue_xmit
来发送报文,在该函数中调用的是__dev_queue_xmit(skb, NULL);
,如下所示:
int dev_queue_xmit(struct sk_buff *skb)
{
return __dev_queue_xmit(skb, NULL);
}
直接调用__dev_queue_xmit
传入的参数是一个skb 数据包
__dev_queue_xmit
函数会根据不同的情况会调用__dev_xmit_skb
或者sch_direct_xmit
函数,最终会调用dev_hard_start_xmit
函数,该函数最终会调用xmit_one
来发送一到多个数据包
recv过程的Linux内核实现
数据链路层分析
在数据链路层接受数据并传递给上层的步骤如下所示:
- 一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
- 网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配
skb_buff
数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff
缓冲区中;从数据帧中提取出一些信息,并设置skb_buff
相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
- 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
- 内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数
netif_rx;
(2)通过NAPI机制。该中断处理程序调用 Network device的netif_rx_schedule
函数,进入软中断处理流程,再调用net_rx_action
函数。 - 该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入
netif _receive_skb
处理流程。 -
netif_receive_skb
是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。
网络层分析
ip层的入口在ip_rcv
函数,该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达ip_rcv_finish
函数。
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
ip_rcv_finish
函数如下所示:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int ret;
/* if ingress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_rcv(skb);
if (!skb)
return NET_RX_SUCCESS;
ret = ip_rcv_finish_core(net, sk, skb, dev, NULL);
if (ret != NET_RX_DROP)
ret = dst_input(skb);
return ret;
}
ip_rcv_finish 函数最终会调用ip_route_input
函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃:
- 如果是发到本机的话,调用
ip_local_deliver
函数,可能会做 de-fragment(合并多个 IP packet),然后调用ip_local_deliver
函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数tcp_v4_rcv
函数会被调用,从而处理流程进入 TCP 栈。 - 如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用
dst_input
函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用dev_queue_xmit
,进入链路层处理流程。
传输层分析
对于recv
函数,与send
函数类似,调用的系统调用是__sys_recvfrom
,其代码如下所示:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
......
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
.....
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Save some cycles and don't copy the address if not needed */
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
/* We assume all kernel code knows the size of sockaddr_storage */
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err = sock_recvmsg(sock, &msg, flags); //调用该函数接受数据
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
.....
}
__sys_recvfrom
通过调用sock_recvmsg
来对数据进行接收,该函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
,同样类似send函数中,调用的实际上是socet
在初始化时赋值给结构体struct proto tcp_prot
的函数tcp_rcvmsg
,如下所示:
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
...
tcp_rcvmsg
的代码如下所示:
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
......
if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) &&
(sk->sk_state == TCP_ESTABLISHED))
sk_busy_loop(sk, nonblock); //如果接收队列为空,则会在该函数内循环等待
lock_sock(sk);
.....
if (unlikely(tp->repair)) {
err = -EPERM;
if (!(flags & MSG_PEEK))
goto out;
if (tp->repair_queue == TCP_SEND_QUEUE)
goto recv_sndq;
err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out;
......
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used); //将接收到的数据拷贝到用户态
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop
函数内循环等待,知道接收队列不为空,并调用函数数skb_copy_datagram_msg
将接收到的数据拷贝到用户态,该函数内部实际调用的是__skb_datagram_iter
,其代码如下所示:
int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *, struct iov_iter *),
void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;
/* 拷贝tcp头部 */
if (copy > 0) {
if (copy > len)
copy = len;
n = cb(skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
/* 拷贝数据部分 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
const skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
WARN_ON(start > offset + len);
end = start + skb_frag_size(frag);
if ((copy = end - offset) > 0) {
struct page *page = skb_frag_page(frag);
u8 *vaddr = kmap(page);
if (copy > len)
copy = len;
n = cb(vaddr + frag->page_offset +
offset - start, copy, data, to);
kunmap(page);
offset += n;
if (n != copy)
goto short_copy;
if (!(len -= copy))
return 0;
}
start = end;
}