TCP/IP协议栈在Linux内核中的运行时序分析

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,如下图所示

TCP/IP协议栈在Linux内核中的运行时序分析

越靠近内核区域,需要的权限也就越高,操作系统很好地利用了x86的分层权限机制:

  • 将可以访问的关键资源的代码放在Ring0,将其称为内核态(Kernel Mode)
  • 将普通的程序代码放在Ring3,将其称为用户态(User Mode)

如果用户态的代码需要访问核心资源,需要通过系统调用来进行访问

系统调用

操作系统中的状态切换如下图所示:

TCP/IP协议栈在Linux内核中的运行时序分析

在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号进程接手内核进程的创建
  • 实现文件系统的事务日志

内核线程主要有两种类型

  1. 线程启动后一直等待,直至内核请求线程执行某一特定操作。
  2. 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制时采取行动。

内核线程由内核自身生成,其特点在于

  1. 它们在CPU的管态执行,而不是用户态。
  2. 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户空间

内核线程是一种只运行在内核地址空间的线程。所有的内核线程共享内核地址空间,所以也共享同一份内核页表,内核线程只运行在内核地址空间中,只会访问 3-4GB (32位系统)的内核地址空间,不存在虚拟地址空间,因此每个内核线程的 task_struct 对象中的 mm 为 NULL。普通线程虽然也是同主线程共享地址空间,但是它的 task_struct 对象中的 mm 不为空,指向的是主线程的 mm_struct 对象,普通进程既可以运行在内核态,也可以运行在用户态而内核线程只运行在内核态。

TCP/IP协议栈

TCP/IP协议栈在Linux内核中的运行时序分析

基于TCP/IP协议栈的send/recv在应用层,传输层,网络层和链路层中具体函数调用过程的一张比较完善的图如下:

TCP/IP协议栈在Linux内核中的运行时序分析

socket的基本使用和创建过程

应用层接口

应用层位于TCP/IP协议的最上层,工作于用户空间,使用内核提供的API对底层数据结构进行操作,其提供的函数比较多样,如:

  • read()/write()
  • send()/recv()
  • sendto()/recvfrom()
  • bind()
  • connect()
  • accept()
  • etc...

其中,应用层socket套接字遵循unix"万物皆文件"的思想,这些接口函数,最终都将使用系统调用sys_socketcall()对socket套接字进行各种各样的操作(如:申请套接字fd,发送、接受数据。)
TCP/IP协议栈在Linux内核中的运行时序分析

而后,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()

具体流程如下图所示:
TCP/IP协议栈在Linux内核中的运行时序分析

在socket创建完毕后,用户和应用程序在用户空间所能看到的只是socket对应的fd,而在内核空间中,内核保存了复杂的数据结构,并建立起他们之间的关系:
TCP/IP协议栈在Linux内核中的运行时序分析

随后,用户和应用程序,通过对socket fd进行数据的读写操作,完成网络通讯。
套接口层的主要流程图如下:
TCP/IP协议栈在Linux内核中的运行时序分析

完成套接口的创建后,即可对相应socket文件描述符进行相应的操作。

TCP/IP处理流程

应用层

上面已经提到了socket结构,应用层的各种网络应用程序都是通过Linux Socket编程接口来和内核空间的网络协议通信的。具体处理流程为:

  1. 网络应用调用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。
  2. 对于 TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
  3. 应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端
  4. sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message
  5. _sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数
    • 对于 TCP ,调用 tcp_sendmsg 函数
    • 对于 UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数

下图为socket的三个队列

TCP/IP协议栈在Linux内核中的运行时序分析

传输层

传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,TCP 协议栈的大致处理过程如下图所示:

TCP/IP协议栈在Linux内核中的运行时序分析

TCP发送数据的简要过程:

  1. tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程
  2. 构造 TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer
  3. 构造 TCP header
  4. 计算 TCP 校验和(checksum)和 顺序号 (sequence number)
  5. 发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程

接收数据:

  1. 传输层 TCP 处理入口在 tcp_v4_rcv 函数,它会做 TCP header 检查等处理
  2. 调用 _tcp_v4_lookup,查找该 package 的 open socket。如果找不到,该 package 会被丢弃。接下来检查 socket 和 connection 的状态
  3. 如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment

IP网络层

网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。下图为网络层处理信息的基本流程:

TCP/IP协议栈在Linux内核中的运行时序分析

发送流程:

  1. 首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由
  2. 接着,填充IP包的各个字段,比如版本、包头长度、TOS等
  3. 进行一些分片操作
  4. 接下来就用ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取

接收流程:

  1. IP 层的入口函数在ip_rcv函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达ip_rcv_finish 函数

  2. 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_sendtosys_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件事:

  1. 通过fd获取了对应的struct socket
  2. 创建了用来描述要发送的数据的结构体struct msghdr
  3. 调用了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);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。

TCP/IP协议栈在Linux内核中的运行时序分析

网络层分析

网络层的调用流程为:

  1. 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;
    }
    
  2. 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()

  3. ip_output

    处理单播数据报,设置数据报的输出网络设备以及网络层协议类型参数

  4. ip_finish_output

    观察数据报长度是否大于MTU,若大于,则调用ip_fragment分片,否则调用ip_finish_output2输出

  5. ip_finish_output2

    在该函数中会检测skb的前部空间是否还能存储链路层首部。如果不够,就会申请更大的存储空间,最终会调用邻居子系统的输出函数neigh_output进行输出,输出分为有二层头缓存和没有两种情况,有缓存时调用neigh_hh_output进行快速输出,没有缓存时,则调用邻居子系统的输出回调函数进行慢速输出

TCP/IP协议栈在Linux内核中的运行时序分析

数据链路层分析

网络层最终会通过调用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来发送一到多个数据包

TCP/IP协议栈在Linux内核中的运行时序分析

recv过程的Linux内核实现

数据链路层分析

在数据链路层接受数据并传递给上层的步骤如下所示:

  1. 一个 package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
  2. 网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
  3. 终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
  4. 内核 2.5 中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule函数,进入软中断处理流程,再调用net_rx_action函数。
  5. 该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入netif _receive_skb处理流程。
  6. netif_receive_skb是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。

TCP/IP协议栈在Linux内核中的运行时序分析

网络层分析

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 将会被发到本机还是会被转发还是丢弃:

  1. 如果是发到本机的话,调用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 栈。
  2. 如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用dst_input函数。该函数会 (1)处理 Netfilter Hook (2)执行 IP fragmentation (3)调用 dev_queue_xmit,进入链路层处理流程。

TCP/IP协议栈在Linux内核中的运行时序分析

传输层分析

对于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;
	}

TCP/IP协议栈在Linux内核中的运行时序分析

上一篇:[转]python 网络篇(网络编程) - 超天大圣 - 博客园


下一篇:TCP/IP协议栈在Linux内核中的运行时序分析