Linux内核TCP/IP协议栈

inet_init是如何被调用的?从start_kernel到inet_init调用路径

在 Linux 内核启动过程中,inet_init 函数是通过以下路径被调用的:

1.start_kernel 函数是内核的入口点,它位于 init/main.c 文件中。
2.在 start_kernel 函数中,会调用 rest_init 函数来初始化系统的剩余部分。
3.rest_init 函数中会调用 kernel_init 函数,该函数位于 init/main.c 文件中。
4.在 kernel_init 函数中,会调用 do_basic_setup 函数来进行一些基本的系统设置。
5.在 do_basic_setup 函数中,会调用 device_initcall 宏,该宏会依次调用一系列设备初始化函数。
6.其中,device_initcall 宏会调用 do_initcalls 函数。
7.在 do_initcalls 函数中,会调用 __do_initcall_level 函数来执行各个级别的初始化函数。
8.在初始化级别为 SUBSYS_FINAL 的阶段,__do_initcall_level 函数会调用 inet_init 函数。
9.在 inet_init 函数中进行了网络子系统的初始化。

asmlinkage __visible void __init start_kernel(void)
{
	char *command_line;
	char *after_dashes;

	set_task_stack_end_magic(&init_task);
	smp_setup_processor_id();
	debug_objects_early_init();

	cgroup_init_early();

	local_irq_disable();
	early_boot_irqs_disabled = true;

	/*
	 * Interrupts are still disabled. Do necessary setups, then
	 * enable them.
	 */
	boot_cpu_init();
	page_address_init();
	pr_notice("%s", linux_banner);
	setup_arch(&command_line);
	mm_init_cpumask(&init_mm);
	setup_command_line(command_line);
	setup_nr_cpu_ids();
	setup_per_cpu_areas();
	smp_prepare_boot_cpu();	/* arch-specific boot-cpu hooks */
	boot_cpu_hotplug_init();

	build_all_zonelists(NULL);
	page_alloc_init();

	pr_notice("Kernel command line: %s\n", boot_command_line);
	/* parameters may set static keys */
	jump_label_init();
	parse_early_param();
	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);
	if (!IS_ERR_OR_NULL(after_dashes))
		parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
			   NULL, set_init_arg);

	/*
	 * These use large bootmem allocations and must precede
	 * kmem_cache_init()
	 */
	setup_log_buf(0);
	vfs_caches_init_early();
	sort_main_extable();
	trap_init();
	mm_init();

	ftrace_init();

	/* trace_printk can be enabled here */
	early_trace_init();

	/*
	 * Set up the scheduler prior starting any interrupts (such as the
	 * timer interrupt). Full topology setup happens at smp_init()
	 * time - but meanwhile we still have a functioning scheduler.
	 */
	sched_init();
	/*
	 * Disable preemption - early bootup scheduling is extremely
	 * fragile until we cpu_idle() for the first time.
	 */
	preempt_disable();
	if (WARN(!irqs_disabled(),
		 "Interrupts were enabled *very* early, fixing it\n"))
		local_irq_disable();
	radix_tree_init();

	/*
	 * Set up housekeeping before setting up workqueues to allow the unbound
	 * workqueue to take non-housekeeping into account.
	 */
	housekeeping_init();

	/*
	 * Allow workqueue creation and work item queueing/cancelling
	 * early.  Work item execution depends on kthreads and starts after
	 * workqueue_init().
	 */
	workqueue_init_early();

	rcu_init();

	/* Trace events are available after this */
	trace_init();

	if (initcall_debug)
		initcall_debug_enable();

	context_tracking_init();
	/* init some links before init_ISA_irqs() */
	early_irq_init();
	init_IRQ();
	tick_init();
	rcu_init_nohz();
	init_timers();
	hrtimers_init();
	softirq_init();
	timekeeping_init();
	time_init();

	/*
	 * For best initial stack canary entropy, prepare it after:
	 * - setup_arch() for any UEFI RNG entropy and boot cmdline access
	 * - timekeeping_init() for ktime entropy used in random_init()
	 * - time_init() for making random_get_entropy() work on some platforms
	 * - random_init() to initialize the RNG from from early entropy sources
	 */
	random_init(command_line);
	boot_init_stack_canary();

	perf_event_init();
	profile_init();
	call_function_init();
	WARN(!irqs_disabled(), "Interrupts were enabled early\n");

	early_boot_irqs_disabled = false;
	local_irq_enable();

	kmem_cache_init_late();

	/*
	 * HACK ALERT! This is early. We're enabling the console before
	 * we've done PCI setups etc, and console_init() must be aware of
	 * this. But we do want output early, in case something goes wrong.
	 */
	console_init();
	if (panic_later)
		panic("Too many boot %s vars at `%s'", panic_later,
		      panic_param);

	lockdep_init();

	/*
	 * Need to run this when irqs are enabled, because it wants
	 * to self-test [hard/soft]-irqs on/off lock inversion bugs
	 * too:
	 */
	locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD
	if (initrd_start && !initrd_below_start_ok &&
	    page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
		pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
		    page_to_pfn(virt_to_page((void *)initrd_start)),
		    min_low_pfn);
		initrd_start = 0;
	}
#endif
	kmemleak_init();
	debug_objects_mem_init();
	setup_per_cpu_pageset();
	numa_policy_init();
	acpi_early_init();
	if (late_time_init)
		late_time_init();
	sched_clock_init();
	calibrate_delay();

	arch_cpu_finalize_init();

	pid_idr_init();
	anon_vma_init();
#ifdef CONFIG_X86
	if (efi_enabled(EFI_RUNTIME_SERVICES))
		efi_enter_virtual_mode();
#endif
	thread_stack_cache_init();
	cred_init();
	fork_init();
	proc_caches_init();
	uts_ns_init();
	buffer_init();
	key_init();
	security_init();
	dbg_late_init();
	vfs_caches_init();
	pagecache_init();
	signals_init();
	seq_file_init();
	proc_root_init();
	nsfs_init();
	cpuset_init();
	cgroup_init();
	taskstats_init_early();
	delayacct_init();


	acpi_subsystem_init();
	arch_post_acpi_subsys_init();
	sfi_init_late();

	if (efi_enabled(EFI_RUNTIME_SERVICES)) {
		efi_free_boot_services();
	}

	/* Do the rest non-__init'ed, we're now alive */
	rest_init();

	prevent_tail_call_optimization();
}
static noinline void __ref rest_init(void)
{
	struct task_struct *tsk;
	int pid;

	rcu_scheduler_starting();
	/*
	 * 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);
	/*
	 * Pin init on the boot CPU. Task migration is not properly working
	 * until sched_init_smp() has been run. It will set the allowed
	 * CPUs for init to the non isolated CPUs.
	 */
	rcu_read_lock();
	tsk = find_task_by_pid_ns(pid, &init_pid_ns);
	set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
	rcu_read_unlock();

	numa_default_policy();
	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
	rcu_read_lock();
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
	rcu_read_unlock();

	/*
	 * Enable might_sleep() and smp_processor_id() checks.
	 * They cannot be enabled earlier because with CONFIG_PREEMPT=y
	 * kernel_thread() would trigger might_sleep() splats. With
	 * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
	 * already, but it's stuck on the kthreadd_done completion.
	 */
	system_state = SYSTEM_SCHEDULING;

	complete(&kthreadd_done);

	/*
	 * The boot idle thread must execute schedule()
	 * at least once to get things moving:
	 */
	schedule_preempt_disabled();
	/* Call into cpu_idle with preempt disabled */
	cpu_startup_entry(CPUHP_ONLINE);
}

跟踪分析TCP/IP协议栈如何将自己与上层套接口与下层数据链路层关联起来的

TCP/IP协议栈通过套接字接口(Socket API)与上层应用程序进行交互,并通过网络设备驱动程序与下层的数据链路层进行通信。具体的关联过程如下:

上层套接口和协议栈的关联:应用程序通过套接字API(如socket、bind、listen等)与协议栈进行交互。协议栈提供了一组函数和数据结构,使应用程序能够发送和接收网络数据。

协议栈和传输层的关联:协议栈使用传输层协议(如TCP或UDP)将数据从应用层传递到网络层。协议栈会相应地解析和处理传输层的头部信息,并提供相应的函数和数据结构供传输层使用。

协议栈和网络层的关联:协议栈使用网络层协议(如IP)来封装数据,并确定数据包的路由。协议栈会解析和处理网络层的头部信息,并提供相应的函数和数据结构供网络层使用。

协议栈和数据链路层的关联:协议栈通过网络设备驱动程序与下层的数据链路层进行通信。数据链路层负责将数据帧从一个网络接口发送到另一个网络接口。协议栈会使用适当的数据链路层协议(如以太网)来封装数据,并提供相应的函数和数据结构供数据链路层使用。

通过这些关联,TCP/IP协议栈能够在不同层级之间传递和处理网络数据。

TCP的三次握手源代码跟踪分析,跟踪找出设置和发送SYN/ACK的位置,以及状态转换的位置

TCP的三次握手是指在建立TCP连接时,客户端和服务器端进行的一系列交互过程。具体来说,三次握手的过程如下:

1.客户端向服务器端发送SYN包,表示请求建立连接。
2.服务器端收到SYN包后,向客户端发送SYN/ACK包,表示确认建立连接。
3.客户端收到SYN/ACK包后,向服务器端发送ACK包,表示连接已经建立。

下面我们通过跟踪TCP协议栈的源代码,来分析三次握手的具体实现过程。

首先,在Linux系统中,TCP协议栈的相关代码位于net/ipv4/tcp_ipv4.c文件中。在该文件的tcp_v4_connect函数中,实现了TCP连接的建立过程。具体来说,当客户端调用connect函数向服务器端发起连接请求时,会调用tcp_v4_connect函数。在该函数中,会执行以下代码:

sk = tcp_v4_syn_recv_sock(net, saddr, daddr, dport, O_NONBLOCK, &err);
if (!sk)
    goto out;
        
inet_sk(sk)->daddr = daddr;
inet_sk(sk)->dport = dport;
        
tcp_connect(sk);

其中,tcp_v4_syn_recv_sock函数用于创建一个用于接收SYN/ACK包的套接字,tcp_connect函数用于执行三次握手过程。

接下来,我们来看一下tcp_connect函数的实现。在该函数中,会执行以下代码:

tcp_send_syn(sk, GFP_KERNEL);
tcp_write_wakeup(sk);
        
set_bit(TCPF_SYN_SENT, &sk->sk_tcp_state);
        
sk->sk_sndbuf = tcp_initial_cwnd(sk);

其中,tcp_send_syn函数用于发送SYN包,并设置SYN包的相关信息,如序列号、窗口大小等;set_bit函数用于将TCP状态设置为SYN_SENT状态,表示已经发送了SYN包;tcp_initial_cwnd函数用于计算初始的拥塞窗口大小。

接下来,我们来看一下tcp_send_syn函数的实现。在该函数中,会执行以下代码:

tcp_init_nondata_skb(skb, sk);
        
tcp_syn_build_options(skb, sk);
                
tcp_set_skb_tso_segs(skb, 1);

tcp_transmit_skb(skb, sk, 1, GFP_ATOMIC);

其中,tcp_init_nondata_skb函数用于初始化SKB,并设置TCP头部的一些基本信息,如源IP地址、目的IP地址、源端口号、目的端口号等;tcp_syn_build_options函数用于设置TCP协议选项,如MSS、SACK等;tcp_set_skb_tso_segs函数用于设置TCP分段的数量;tcp_transmit_skb函数用于将SKB发送出去。

当服务器端收到客户端发送的SYN包后,会向客户端发送SYN/ACK包。具体来说,在服务器端的tcp_v4_do_rcv函数中,会执行以下代码:

if (req != TCP_CONN_ESTABLISHED)
    sk = tcp_check_req(sk, req, skb);
        
tcp_v4_send_synack(sk, skb, tcp_time_stamp(skb));

其中,tcp_check_req函数用于检查连接请求,返回一个新的套接字;tcp_v4_send_synack函数用于发送SYN/ACK包,并设置SYN/ACK包的相关信息,如序列号、窗口大小等。

当客户端收到服务器端发送的SYN/ACK包后,会向服务器端发送ACK包,表示连接已经建立。具体来说,在客户端的tcp_v4_do_rcv函数中,会执行以下代码:

if (th->syn && th->ack) {
    if (sk->sk_state == TCP_SYN_SENT) {
        inet_sk(sk)->inet_dport = th->source;
        tcp_finish_connect(sk, skb);
    } else
        goto discard;
} else
    goto discard;

其中,tcp_finish_connect函数用于完成TCP连接建立过程,并将TCP状态设置为ESTABLISHED状态,表示连接已经建立成功。

综上所述,三次握手过程中,客户端发送SYN包的位置在tcp_send_syn函数中,服务器端发送SYN/ACK包的位置在tcp_v4_send_synack函数中,客户端发送ACK包的位置在tcp_finish_connect函数中。同时,TCP状态的转换也可以通过在代码中设置标志位来实现,如在客户端发送SYN包时,需要将TCP状态设置为SYN_SENT状态。

send在TCP/IP协议栈中的执行路径

send操作涉及应用程序调用套接字接口发送数据,并通过协议栈将数据传递到网络层。

用户空间调用send函数;
send函数通过系统调用进入内核空间,调用sock_sendmsg函数;
sock_sendmsg函数中会调用sock_sendmsg_nosec函数进行实际的发送操作;
sock_sendmsg_nosec函数首先会调用sock_sendmsg_check_size函数判断发送的数据是否超出了网络拥塞窗口大小和socket缓冲区大小的限制;
如果发送数据量过大,sock_sendmsg_check_size函数会返回一个错误码,否则继续执行;
sock_sendmsg_nosec函数会调用inet_sendmsg函数对待发送数据进行封装(添加TCP头部、IP头部等),并将数据送到tcp_sendmsg函数中;
tcp_sendmsg函数中会判断当前TCP连接的状态,如果处于CLOSED或LISTEN状态,则返回错误码;
如果处于SYN_SENT状态,表示正在进行三次握手,此时发送的数据会被放到SYN包里一起发送;
如果处于SYN_RECEIVED、ESTABLISHED或CLOSE_WAIT状态,则可以正常发送数据;
tcp_sendmsg函数中会调用tcp_transmit_skb函数将封装好的数据包交给网络设备发送;
网络设备将数据包发送出去后,会触发中断处理程序,中断处理程序会调用tcp_ack函数对发送的数据包进行确认。

recv在TCP/IP协议栈中的执行路径

recv操作涉及从网络接收数据,并通过协议栈将数据传递给应用程序。

用户空间调用recv函数;
recv函数通过系统调用进入内核空间,调用sock_recvmsg函数;
sock_recvmsg函数会调用sock_recvmsg_nosec函数进行实际的接收操作;
sock_recvmsg_nosec函数首先会调用skb_recv_datagram函数从接收缓冲区中获取一个数据报(包);
如果接收缓冲区为空,则进入等待状态,等待数据到达或超时;
当有数据到达时,sock_recvmsg_nosec函数会继续执行,调用skb_copy_datagram_msg函数将数据从内核空间复制到用户空间;
复制完成后,sock_recvmsg_nosec函数返回接收到的数据的长度给recv函数;
recv函数返回接收到的数据给用户程序。

路由表的结构和初始化过程

Linux内核的路由表使用一些数据结构来表示每个路由项。这些结构定义在include/net/route.h和net/ipv4/route.c中。

struct rtable {
	struct dst_entry	dst;

	int			rt_genid;
	unsigned int		rt_flags;
	__u16			rt_type;
	__u8			rt_is_input;
	__u8			rt_uses_gateway;

	int			rt_iif;

	/* Info on neighbour */
	__be32			rt_gateway;

	/* Miscellaneous cached information */
	u32			rt_mtu_locked:1,
				rt_pmtu:31;

	struct list_head	rt_uncached;
	struct uncached_list	*rt_uncached_list;
};

初始化过程如下:

初始化路由缓存:在内核启动时,会创建一个全局的路由缓存(route cache)。路由缓存用于加速查找过程,避免每次都需要进行完整的路由表查找。路由缓存的初始化通过调用ip_rt_init()函数完成。

注册路由协议:内核支持多种路由协议,如IPv4的RIP、OSPF、BGP等。在初始化过程中,通过调用register_pernet_device()函数注册路由协议。

添加路由项:用户可以通过命令行工具(如ip route add)或系统调用(如rtnetlink)来添加路由项。添加路由项时,会调用ip_rt_insert()函数,该函数会调用fib_table_lookup()函数查找目标路由项,并将其插入到路由表中。

路由查找:当内核需要发送数据包时,需要根据目标IP地址查找对应的路由项。路由查找通过调用ip_route_output_slow()函数完成,该函数会依次查找本地路由表、主机路由表、默认路由表等,直到找到匹配的路由项。

路由更新:当网络拓扑发生变化时,内核需要更新路由表。路由更新通过调用fib_table_lookup()函数和fib_table_update()函数完成。fib_table_lookup()函数用于查找目标路由项,fib_table_update()函数用于更新路由项的下一跳信息。

通过目的IP查询路由表的到下一跳的IP地址的过程

通过目的IP地址查询路由表的到下一跳IP地址的过程主要涉及以下几个步骤:

调用ip_route_output_slow()函数:这是一个路由查找的慢路径函数,用于查找目标IP地址对应的路由项。该函数定义在net/ipv4/route.c文件中。

调用fib_lookup函数执行路由查找操作。

fib_lookup函数内部会调用fib_table_lookup函数来执行实际的路由表查找操作。fib_table_lookup函数位于net/ipv4/fib_semantics.c文件中

在fib_table_lookup函数中,它将根据flowi4结构体中的信息从路由表中查找匹配的路由项。关键的查找逻辑包括以下步骤:
根据flp->daddr和flp->flowi4_scope选择要查询的路由表。
使用fib_lookup_1函数从选定的路由表中查找匹配的路由项。fib_lookup_1函数会逐级查找路由表,并根据目的IP地址和掩码进行匹配。
如果找到匹配的路由项,将相关信息填充到res结构体中,包括下一跳IP地址和出接口。
返回查找结果。

static int ip_route_input_slow(struct sk_buff *skb, __be32 daddr, __be32 saddr,
			       u8 tos, struct net_device *dev,
			       struct fib_result *res)
{
	struct in_device *in_dev = __in_dev_get_rcu(dev);
	struct flow_keys *flkeys = NULL, _flkeys;
	struct net    *net = dev_net(dev);
	struct ip_tunnel_info *tun_info;
	int		err = -EINVAL;
	unsigned int	flags = 0;
	u32		itag = 0;
	struct rtable	*rth;
	struct flowi4	fl4;
	bool do_cache = true;

	/* IP on this device is disabled. */

	if (!in_dev)
		goto out;

	/* Check for the most weird martians, which can be not detected
	   by fib_lookup.
	 */

	tun_info = skb_tunnel_info(skb);
	if (tun_info && !(tun_info->mode & IP_TUNNEL_INFO_TX))
		fl4.flowi4_tun_key.tun_id = tun_info->key.tun_id;
	else
		fl4.flowi4_tun_key.tun_id = 0;
	skb_dst_drop(skb);

	if (ipv4_is_multicast(saddr) || ipv4_is_lbcast(saddr))
		goto martian_source;

	res->fi = NULL;
	res->table = NULL;
	if (ipv4_is_lbcast(daddr) || (saddr == 0 && daddr == 0))
		goto brd_input;

	/* Accept zero addresses only to limited broadcast;
	 * I even do not know to fix it or not. Waiting for complains :-)
	 */
	if (ipv4_is_zeronet(saddr))
		goto martian_source;

	if (ipv4_is_zeronet(daddr))
		goto martian_destination;

	/* Following code try to avoid calling IN_DEV_NET_ROUTE_LOCALNET(),
	 * and call it once if daddr or/and saddr are loopback addresses
	 */
	if (ipv4_is_loopback(daddr)) {
		if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
			goto martian_destination;
	} else if (ipv4_is_loopback(saddr)) {
		if (!IN_DEV_NET_ROUTE_LOCALNET(in_dev, net))
			goto martian_source;
	}

	/*
	 *	Now we are ready to route packet.
	 */
	fl4.flowi4_oif = 0;
	fl4.flowi4_iif = dev->ifindex;
	fl4.flowi4_mark = skb->mark;
	fl4.flowi4_tos = tos;
	fl4.flowi4_scope = RT_SCOPE_UNIVERSE;
	fl4.flowi4_flags = 0;
	fl4.daddr = daddr;
	fl4.saddr = saddr;
	fl4.flowi4_uid = sock_net_uid(net, NULL);

	if (fib4_rules_early_flow_dissect(net, skb, &fl4, &_flkeys)) {
		flkeys = &_flkeys;
	} else {
		fl4.flowi4_proto = 0;
		fl4.fl4_sport = 0;
		fl4.fl4_dport = 0;
	}

	err = fib_lookup(net, &fl4, res, 0);
	if (err != 0) {
		if (!IN_DEV_FORWARD(in_dev))
			err = -EHOSTUNREACH;
		goto no_route;
	}

	if (res->type == RTN_BROADCAST) {
		if (IN_DEV_BFORWARD(in_dev))
			goto make_route;
		/* not do cache if bc_forwarding is enabled */
		if (IPV4_DEVCONF_ALL(net, BC_FORWARDING))
			do_cache = false;
		goto brd_input;
	}

	if (res->type == RTN_LOCAL) {
		err = fib_validate_source(skb, saddr, daddr, tos,
					  0, dev, in_dev, &itag);
		if (err < 0)
			goto martian_source;
		goto local_input;
	}

	if (!IN_DEV_FORWARD(in_dev)) {
		err = -EHOSTUNREACH;
		goto no_route;
	}
	if (res->type != RTN_UNICAST)
		goto martian_destination;

make_route:
	err = ip_mkroute_input(skb, res, in_dev, daddr, saddr, tos, flkeys);
out:	return err;

brd_input:
	if (skb->protocol != htons(ETH_P_IP))
		goto e_inval;

	if (!ipv4_is_zeronet(saddr)) {
		err = fib_validate_source(skb, saddr, 0, tos, 0, dev,
					  in_dev, &itag);
		if (err < 0)
			goto martian_source;
	}
	flags |= RTCF_BROADCAST;
	res->type = RTN_BROADCAST;
	RT_CACHE_STAT_INC(in_brd);

local_input:
	do_cache &= res->fi && !itag;
	if (do_cache) {
		rth = rcu_dereference(FIB_RES_NH(*res).nh_rth_input);
		if (rt_cache_valid(rth)) {
			skb_dst_set_noref(skb, &rth->dst);
			err = 0;
			goto out;
		}
	}

	rth = rt_dst_alloc(l3mdev_master_dev_rcu(dev) ? : net->loopback_dev,
			   flags | RTCF_LOCAL, res->type,
			   IN_DEV_CONF_GET(in_dev, NOPOLICY), false, do_cache);
	if (!rth)
		goto e_nobufs;

	rth->dst.output= ip_rt_bug;
#ifdef CONFIG_IP_ROUTE_CLASSID
	rth->dst.tclassid = itag;
#endif
	rth->rt_is_input = 1;

	RT_CACHE_STAT_INC(in_slow_tot);
	if (res->type == RTN_UNREACHABLE) {
		rth->dst.input= ip_error;
		rth->dst.error= -err;
		rth->rt_flags 	&= ~RTCF_LOCAL;
	}

	if (do_cache) {
		struct fib_nh *nh = &FIB_RES_NH(*res);

		rth->dst.lwtstate = lwtstate_get(nh->nh_lwtstate);
		if (lwtunnel_input_redirect(rth->dst.lwtstate)) {
			WARN_ON(rth->dst.input == lwtunnel_input);
			rth->dst.lwtstate->orig_input = rth->dst.input;
			rth->dst.input = lwtunnel_input;
		}

		if (unlikely(!rt_cache_route(nh, rth)))
			rt_add_uncached_list(rth);
	}
	skb_dst_set(skb, &rth->dst);
	err = 0;
	goto out;

no_route:
	RT_CACHE_STAT_INC(in_no_route);
	res->type = RTN_UNREACHABLE;
	res->fi = NULL;
	res->table = NULL;
	goto local_input;

	/*
	 *	Do not cache martian addresses: they should be logged (RFC1812)
	 */
martian_destination:
	RT_CACHE_STAT_INC(in_martian_dst);
#ifdef CONFIG_IP_ROUTE_VERBOSE
	if (IN_DEV_LOG_MARTIANS(in_dev))
		net_warn_ratelimited("martian destination %pI4 from %pI4, dev %s\n",
				     &daddr, &saddr, dev->name);
#endif

e_inval:
	err = -EINVAL;
	goto out;

e_nobufs:
	err = -ENOBUFS;
	goto out;

martian_source:
	ip_handle_martian_source(dev, in_dev, skb, daddr, saddr);
	goto out;
}
static void nl_fib_lookup(struct net *net, struct fib_result_nl *frn)
{

	struct fib_result       res;
	struct flowi4           fl4 = {
		.flowi4_mark = frn->fl_mark,
		.daddr = frn->fl_addr,
		.flowi4_tos = frn->fl_tos,
		.flowi4_scope = frn->fl_scope,
	};
	struct fib_table *tb;

	rcu_read_lock();

	tb = fib_get_table(net, frn->tb_id_in);

	frn->err = -ENOENT;
	if (tb) {
		local_bh_disable();

		frn->tb_id = tb->tb_id;
		frn->err = fib_table_lookup(tb, &fl4, &res, FIB_LOOKUP_NOREF);

		if (!frn->err) {
			frn->prefixlen = res.prefixlen;
			frn->nh_sel = res.nh_sel;
			frn->type = res.type;
			frn->scope = res.scope;
		}
		local_bh_enable();
	}

	rcu_read_unlock();
}

ARP缓存的数据结构及初始化过程,包括ARP缓存的初始化

ARP(Address Resolution Protocol)缓存是用于存储IP地址与MAC地址之间映射关系的数据结构。在Linux内核中,ARP缓存使用struct neighbour结构来表示。

初始化ARP缓存的过程通常在设备初始化的时候完成。以以太网设备为例,以下是ARP缓存的初始化过程:

在以太网设备的初始化函数中,调用neigh_table_init函数来初始化邻居表。neigh_table_init函数位于net/core/neighbour.c文件中,用于初始化邻居表。它会创建用于存储邻居项的哈希表和相关的数据结构。

在设备初始化过程中,通过调用neigh_create函数创建ARP缓存项。neigh_create函数位于net/core/neighbour.c文件中,用于创建一个新的ARP缓存项。该函数会为缓存项分配内存,并对缓存项的字段进行初始化。

初始化ARP缓存项的各个字段,例如设置dev指针指向所属的网络设备、设置硬件层地址缓存等。

通过调用neigh_add函数将ARP缓存项添加到邻居表中。neigh_add函数位于net/core/neighbour.c文件中,用于将ARP缓存项添加到邻居表中并建立映射关系。

当需要使用ARP缓存项时,可以通过调用neigh_lookup函数来查找缓存项。neigh_lookup函数位于net/core/neighbour.c文件中,用于根据目标IP地址查找对应的ARP缓存项。

通过以上步骤,ARP缓存项的初始化和添加到邻居表的过程完成了。在使用ARP缓存时,可以根据目标IP地址查找对应的缓存项,从而获取相应的MAC地址。

struct neighbour {
	struct neighbour __rcu	*next;
	struct neigh_table	*tbl;
	struct neigh_parms	*parms;
	unsigned long		confirmed;
	unsigned long		updated;
	rwlock_t		lock;
	refcount_t		refcnt;
	struct sk_buff_head	arp_queue;
	unsigned int		arp_queue_len_bytes;
	struct timer_list	timer;
	unsigned long		used;
	atomic_t		probes;
	__u8			flags;
	__u8			nud_state;
	__u8			type;
	__u8			dead;
	seqlock_t		ha_lock;
	unsigned char		ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
	struct hh_cache		hh;
	int			(*output)(struct neighbour *, struct sk_buff *);
	const struct neigh_ops	*ops;
	struct rcu_head		rcu;
	struct net_device	*dev;
	u8			primary_key[0];
} __randomize_layout;
void neigh_table_init(int index, struct neigh_table *tbl)
{
	unsigned long now = jiffies;
	unsigned long phsize;

	INIT_LIST_HEAD(&tbl->parms_list);
	list_add(&tbl->parms.list, &tbl->parms_list);
	write_pnet(&tbl->parms.net, &init_net);
	refcount_set(&tbl->parms.refcnt, 1);
	tbl->parms.reachable_time =
			  neigh_rand_reach_time(NEIGH_VAR(&tbl->parms, BASE_REACHABLE_TIME));

	tbl->stats = alloc_percpu(struct neigh_statistics);
	if (!tbl->stats)
		panic("cannot create neighbour cache statistics");

#ifdef CONFIG_PROC_FS
	if (!proc_create_seq_data(tbl->id, 0, init_net.proc_net_stat,
			      &neigh_stat_seq_ops, tbl))
		panic("cannot create neighbour proc dir entry");
#endif

	RCU_INIT_POINTER(tbl->nht, neigh_hash_alloc(3));

	phsize = (PNEIGH_HASHMASK + 1) * sizeof(struct pneigh_entry *);
	tbl->phash_buckets = kzalloc(phsize, GFP_KERNEL);

	if (!tbl->nht || !tbl->phash_buckets)
		panic("cannot allocate neighbour cache hashes");

	if (!tbl->entry_size)
		tbl->entry_size = ALIGN(offsetof(struct neighbour, primary_key) +
					tbl->key_len, NEIGH_PRIV_ALIGN);
	else
		WARN_ON(tbl->entry_size % NEIGH_PRIV_ALIGN);

	rwlock_init(&tbl->lock);
	INIT_DEFERRABLE_WORK(&tbl->gc_work, neigh_periodic_work);
	queue_delayed_work(system_power_efficient_wq, &tbl->gc_work,
			tbl->parms.reachable_time);
	timer_setup(&tbl->proxy_timer, neigh_proxy_process, 0);
	skb_queue_head_init_class(&tbl->proxy_queue,
			&neigh_table_proxy_queue_class);

	tbl->last_flush = now;
	tbl->last_rand	= now + tbl->parms.reachable_time * 20;

	neigh_tables[index] = tbl;
}

如何将IP地址解析出对应的MAC地址

在Linux内核中,IP地址解析出对应的MAC地址是通过ARP(Address Resolution Protocol)实现的。ARP是一种广泛用于解析IP地址到对应MAC地址的协议。

内核中与ARP相关的代码位于net/ipv4/arp.c文件中。可以在这个文件中找到处理ARP请求和响应的函数。

当发送ARP请求时,调用arp_send()函数,该函数会构造一个ARP请求包并通过网络设备发送出去。

当接收到ARP请求或ARP响应时,会调用arp_rcv()函数进行处理。该函数会解析接收到的ARP包,更新ARP缓存表。

ARP缓存表的数据结构定义在include/net/arp.h文件中,主要包括IP地址、MAC地址、更新时间等字段。

当需要获取目标IP地址对应的MAC地址时,会调用arp_find()函数,在ARP缓存表中查找匹配的条目。如果找到则返回对应的MAC地址,否则返回NULL。

如果ARP缓存表中没有匹配的条目,则需要发送ARP请求进行解析。

通过这个过程,系统能够根据目的IP地址解析出对应的MAC地址。

void arp_send(int type, int ptype, __be32 dest_ip,
	      struct net_device *dev, __be32 src_ip,
	      const unsigned char *dest_hw, const unsigned char *src_hw,
	      const unsigned char *target_hw)
{
	arp_send_dst(type, ptype, dest_ip, dev, src_ip, dest_hw, src_hw,
		     target_hw, NULL);
}
static int arp_rcv(struct sk_buff *skb, struct net_device *dev,
		   struct packet_type *pt, struct net_device *orig_dev)
{
	const struct arphdr *arp;

	/* do not tweak dropwatch on an ARP we will ignore */
	if (dev->flags & IFF_NOARP ||
	    skb->pkt_type == PACKET_OTHERHOST ||
	    skb->pkt_type == PACKET_LOOPBACK)
		goto consumeskb;

	skb = skb_share_check(skb, GFP_ATOMIC);
	if (!skb)
		goto out_of_mem;

	/* ARP header, plus 2 device addresses, plus 2 IP addresses.  */
	if (!pskb_may_pull(skb, arp_hdr_len(dev)))
		goto freeskb;

	arp = arp_hdr(skb);
	if (arp->ar_hln != dev->addr_len || arp->ar_pln != 4)
		goto freeskb;

	memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));

	return NF_HOOK(NFPROTO_ARP, NF_ARP_IN,
		       dev_net(dev), NULL, skb, dev, NULL,
		       arp_process);

consumeskb:
	consume_skb(skb);
	return NET_RX_SUCCESS;
freeskb:
	kfree_skb(skb);
out_of_mem:
	return NET_RX_DROP;
}

跟踪TCP send过程中的路由查询和ARP解析的最底层实现

在Linux内核中,TCP send过程中的路由查询和ARP解析是通过网络协议栈中的不同模块实现的。具体来说,路由查询是在IP层实现的,而ARP解析是在数据链路层实现的。

路由查询:
在TCP send过程中,当应用程序调用send函数发送数据时,数据首先被传递到TCP层。TCP层会将数据封装成TCP报文段,并添加TCP报头信息。然后,TCP层将TCP报文段传递给IP层。IP层会根据目标IP地址查找路由表,以确定数据报文段应该通过哪个网络接口发送。

在Linux内核中,路由查询是通过ip_route_output函数实现的。该函数位于net/ipv4/route.c文件中,其定义如下:

struct rtable *ip_route_output(struct net *net, struct flowi4 *fl4,
                               const struct sock *sk, int flags);

其中,net参数是指向网络命名空间的指针,fl4参数是指向IPv4流量标识符(flow identifier)的指针,sk参数是指向套接字的指针,flags参数是控制路由查询行为的标志。该函数会根据目标IP地址和流量标识符查找路由表,并返回一个指向路由表项的指针。如果没有找到合适的路由表项,则返回NULL。

在ip_route_output函数中,路由表的查找是通过调用fib_lookup函数实现的。fib_lookup函数位于net/ipv4/fib_frontend.c文件中,其定义如下:

struct fib_result *fib_lookup(struct net *net, const struct flowi *flp,
                              int flags);

其中,net参数是指向网络命名空间的指针,flp参数是指向流量标识符的指针,flags参数是控制路由查询行为的标志。该函数会根据流量标识符查找路由表,并返回一个指向路由表项的指针。

ARP解析:
在TCP send过程中,当IP层确定了数据报文段应该通过哪个网络接口发送后,数据报文段会被传递到数据链路层。数据链路层会将数据报文段封装成以太网数据帧,并添加以太网帧头信息。然后,数据链路层将以太网数据帧发送到网络接口上。

在发送以太网数据帧之前,需要将目标主机的MAC地址解析出来。如果ARP缓存中已经有了目标主机的MAC地址,则可以直接使用该地址。否则,需要先发送ARP请求,获取目标主机的MAC地址。在Linux内核中,ARP解析是通过neigh_resolve_output函数实现的。该函数会等待收到ARP响应,并在收到响应后更新邻居项中的MAC地址。

neigh_resolve_output函数位于net/core/neighbour.c文件中,其定义如下:

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb);

其中,neigh参数是指向邻居项的指针,skb参数是指向要发送的数据包的指针。该函数会等待ARP响应,并在收到响应后更新邻居项中的MAC地址。如果成功获取到MAC地址,则返回0;否则返回负数。

在neigh_resolve_output函数中,ARP请求的创建和发送是通过arp_create函数和dev_queue_xmit函数实现的。具体来说,arp_create函数用于创建一个新的ARP请求,而dev_queue_xmit函数用于将ARP请求发送到网络接口上。

综上所述,TCP send过程中的路由查询和ARP解析是在Linux内核的IP层和数据链路层中实现的。路由查询是通过ip_route_output函数和fib_lookup函数实现的,而ARP解析是通过neigh_resolve_output函数、arp_create函数和dev_queue_xmit函数实现的。

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值