Linux网络协议源码分析(一):网络协议栈

前沿

网络协议源码系列

在本系列中,将重点学习linux 1.2.13版本的网络协议源代码,可以同步学习Linux 内核网络协议栈源码剖析

  • 为啥还要学习Linux老版本的代码呢?纯粹的心灵,纯粹的代码,才能有更纯粹的理解

  • 为啥还要学习Linux老版本的代码呢?有指路仙人,避免迷失在code forest之中

七层模型

下三层是数据通信(通信子网),上三层是数据处理(资源子网);传输层是通信子网、资源子网的接口,承上启下

  • 物理层(device):利用传输介质为数据链路层提供物理连接,实现比特流的透明传输

  • 数据链路层(packet_type):建立和管理节点间的链路;通过差错控制/流量控制,使有差错的物理线路变为无差错的数据链路;分为介质控制访问(MAC)/逻辑链路控制(LLC)

    1. MAC(medium access control)子层(IEEE802.3):解决共享型网络中多用户对信道竞争问题,完成网络介质的访问控制

      媒介访问控制:确定链路两端是否使用同一种链路层协议;

      MAC地址:二层硬件地址

    2. LLC(Logical Link Control)子层(IEEE802.2):建立和维护网络连接,执行差错校验,流量控制,链路控制

      LLC1:默认用来封装广播型链路;服务访问协议(SAP), 子网访问协议(SNAP–>arp)

      LLC2: 默认封装点对点链路

  • 网络层:通过路由算法,为报文或分组通过通信子网选择最适当的路径;控制数据链路层与传输层之间的信息转发、建立、维持和终止网络的连接,IP、ICMP、IGMP(internel组管理协议)、RIP,该层要解决如下问题:

    1. 寻址:数据链路层使用物理地址(MAC地址)仅解决网络内部的寻址问题,不同子网间通信使用逻辑地址(IP地址) IP协议

    2. 交换:规定不同信息交换方式;线路交换、存储转发交换(报文交换,分组交换)

    3. 路由算法:源地址与目的地址间存在多条路径,由路由算法决定最佳路径 RIP(Routing information protocal)

    4. 连接服务:数据链路层控制网络相邻节点间流量;本层控制源节点到目的节点的流量,并防阻塞、差错检测 ICMP(Internet control message protocal)

  • 传输层:向用户提供可靠的端到端差错、流量控制,保证报文传输正确,TCP、UDP、spx、NetBIOS/NetBEUI

    1. 从会话层中获取数据,必要是会对数据进行分割

    2. 确保将数据正确无误的传送到网络层

    3. 如果是”面向连接”服务,则在此层如果指定时间内未收到确认信息,数据将被重发

  • 会话层:用户应用程序、网络之间的接口,向两个实体表示层提供建立和使用连接的方法;INET socket层(af_inet.c)

    1. 建立连接、保持连接、断开连接
  • 表示层:对来自应用层的命令和数据进行解释,并按照一定的格式传送给会话层;BSD socket层(socket.c)

    1. 数据格式处理:协商和建立数据交换的格式,解决各应用程序之间在数据格式表示上的差异

    2. 数据的编码:处理字符集和数字的转换

    3. 压缩与解压缩;加密与解密

  • 应用层:为用户提供服务、协议;ftp,telnet,email,dns,whois,smtp,http…..

本篇主要内容

  • 网络协议栈初始化及报文收发流程

代码分析

内核网络栈初始化

start_kernel

/* 内核入口 */
asmlinkage void start_kernel(void)
{
	char * command_line;
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
	/* 平台初始化 */
	setup_arch(&command_line, &memory_start, &memory_end);
	/* 内存初始化 */
	memory_start = paging_init(memory_start,memory_end);
	trap_init();/* 陷阱初始化 */
	init_IRQ();/* 中断初始化 */
	sched_init();/* 进程调度初始化 */
	parse_options(command_line);/* uboot参数解析 */
	init_modules();
#ifdef CONFIG_PROFILE
	prof_buffer = (unsigned long *) memory_start;
	/* only text is profiled */
	prof_len = (unsigned long) &etext;
	prof_len >>= CONFIG_PROFILE_SHIFT;
	memory_start += prof_len * sizeof(unsigned long);
#endif
	memory_start = console_init(memory_start,memory_end);
	memory_start = bios32_init(memory_start,memory_end);
	memory_start = kmalloc_init(memory_start,memory_end);
	sti();
	calibrate_delay();
	memory_start = chr_dev_init(memory_start,memory_end);
	memory_start = blk_dev_init(memory_start,memory_end);
	sti();
#ifdef CONFIG_SCSI
	memory_start = scsi_dev_init(memory_start,memory_end);
#endif
#ifdef CONFIG_INET
	memory_start = net_dev_init(memory_start,memory_end);
#endif
	memory_start = inode_init(memory_start,memory_end);
	memory_start = file_table_init(memory_start,memory_end);
	memory_start = name_cache_init(memory_start,memory_end);
	mem_init(memory_start,memory_end);
	buffer_init();/* 缓冲区初始化 */
	time_init();/* 时间初始化 */
	sock_init();/* 网络初始化 */
#ifdef CONFIG_SYSVIPC
	ipc_init();
#endif
	sti();
	check_bugs();

	printk(linux_banner);

	if (!fork())		/* we count on this going ok */
		init();
/*
 * task[0] is meant to be used as an "idle" task: it may not sleep, but
 * it might do some general things like count free pages or it could be
 * used to implement a reasonable LRU algorithm for the paging routines:
 * anything that can be useful, but shouldn't take time from the real
 * processes.
 *
 * Right now task[0] just does a infinite idle loop.
 */
	for(;;)
		idle();
}

sock_init

/* 网络初始化 */
void sock_init(void)
{
	int i;

	printk("Swansea University Computer Society NET3.019\n");

	/*
	 * 初始化协议族,全局结构体 pops ;默认支持配置16个协议族;
	 * 主要为协议族编号、协议操作方法
	 */
	 
	for (i = 0; i < NPROTO; ++i) pops[i] = NULL;

	/*
	 *	初始化kernel默认支持的协议,调用各个协议默认的初始化函数
	 * SNAP(子网络访问协议): IEEE802.2;数据链路层
	 * 802.2:高层协议与MAC子层的接口;数据链路层
	 * AX.25:?;数据链路层协议
	 * INET:
	 * IPX: 分组交换协议,提供分组寻址、选择路由的功能;网络层协议
	 * DDP:
	 */

	proto_init();

#ifdef CONFIG_NET
	/* 
	 * 往dev_base设备链表中加入配置的设备;所有配置的设备在space.c文件中;
	 * 初始化该链表中只有loopback_dev设备;通过next,一个接一个加入到dev_base中
	 * 对于NE设备,初始化函数均为ethif_probe->ne_probe->ne_probe1->init_etherdev->ether_setup
	 * 初始化过程中会注册网卡内存地址到 iolist中,并注册open、发送、接受、关闭、查询状态等函数
	 * 中断处理函数在open中会通过 request_irq 进行注册
	 * 有个中断号和device的映射表;中断产生后,根据该表格找到对应的device
	 */
	dev_init();
  
	/*
	 * linux将完整中断切成2部分
	 * top half(硬中断): 在呼叫 request_irq()时指定的interrupt handler函数
		1. 真正接受中断请求的task,避免执行过久,主要任务是根据硬件中断排程bottom half
		2. 具有原子性:top half期间,中断关闭,CPU无法接收新的中断;
	 * bottom half(软中断):由top half排程,负责真正响应中断的task
		1. 执行执行中断的task,是硬中断服务程序对内核的中断
		2. 不具原子性,而且可能将多个中断的bottom half合并一起做
	 * 注册NET bottom Half handle函数;
	 */
	bh_base[NET_BH].routine= net_bh;
	/* 在中断向量中使能NET中断 */
	enable_bh(NET_BH);
#endif  
}

inet_proto_init

/* 初始化INET协议族 */
void inet_proto_init(struct net_proto *pro)
{
	struct inet_protocol *p;
	int i;
	printk("Swansea University Computer Society TCP/IP for NET3.019\n");

	/* 在协议族全局结构体在中寻找空闲位置;并注册INET协议族方法结构体到其中*/
  	(void) sock_register(inet_proto_ops.family, &inet_proto_ops);

  	seq_offset = CURRENT_TIME*250;
	 
	for(i = 0; i < SOCK_ARRAY_SIZE; i++) 
	{
		tcp_prot.sock_array[i] = NULL;
		udp_prot.sock_array[i] = NULL;
		raw_prot.sock_array[i] = NULL;
  	}
	tcp_prot.inuse = 0;
	tcp_prot.highestinuse = 0;
	udp_prot.inuse = 0;
	udp_prot.highestinuse = 0;
	raw_prot.inuse = 0;
	raw_prot.highestinuse = 0;

	printk("IP Protocols: ");
	/* 初始化INET协议族中的子协议,ICMP,IGMP,TCP,UDP;并加入INET协议族结构体中 */
	for(p = inet_protocol_base; p != NULL;) 
	{
		struct inet_protocol *tmp = (struct inet_protocol *) p->next;
		inet_add_protocol(p);/* 将各个子协议加入到inet_protos中 */
		printk("%s%s",p->name,tmp?", ":"\n");
		p = tmp;
	}
	/* 初始化arp模块 */
	arp_init();
  	/* 初始化IP模块;类似arp,不再详细分析 */
	ip_init();
}

总结

linux网络栈初始化

  1. 内核完成自解压过程后进入内核启动;heads.S: 负责BBS,中断描述表、段描述表(GDT)、页表、寄存器初始化后进入kernel_entry()

  2. kernel_entry: 体系结构相关汇编,初始化内核堆栈段,将内核映像区未初始化的数据段清零–>start_kernel

  3. start_kernel->sock_init->proto_init(->inet_proto_init)->dev_init->bottom_half_init

  • proto_init: 初始化目前linux支持的域,目前支持UNIX、802.2、SNAP、AX.25、INET、IPX、DDP;并将域操作函数结构体通过sock_register注册到pops

  • inet_proto_init:初始化INET域,将INET协议族的操作函数注册到全局协议族数组 pops(BSD层到inet_sock层接口) 中;该协议族包含 inet_create,inet_bind,inet_connect,inet_socketpair…属于会话层接口函数

    1. 通过sock_register()将会话层的接口注册到表示层

    2. 初始化传输层协议操作函数结构体tcp_prot, udp_prot, raw_prot;同时所有的会话层sock资源都注册在对应的传输层协议结构体中 proto->sock_array

    3. inet_protocol_add:将传输层收报结构体注册到inet_protos

    4. arp_init,ip_init

  • arp_init:arp(网络层协议)协议初始化函数

    1. 为了从链路层接受数据包,其必须定义一个packet_type结构,调用dev_add_pack函数向链路层struct packet_type *ptype_base注册arp协议收报函数

    2. 注册arp缓存刷新任务到timer

    3. 注册网络设备状态变更handler导致内核通知链netdev_chain(维护arp表项有效性)

  • ip_init:IP协议初始化

    1. 调用dev_add_pack函数向链路层struct packet_type *ptype_base注册IP协议收报函数

    2. 注册网络设备状态变更handler到内核通知链netdev_chain(维护路由表项有效性)

IP、ARP协议

都为网络层协议,初始化函数功能基本相同

  • 向链路层注册数据包接受函数,完成网络层和链路层之间的衔接

  • 由于arp表项以及路由表项都与某个网络设备绑定,故为了保证表项有效性,二者都必须对网络设备状态变化事件进行监听

网络栈初始化

经过inet_proto_init, arp_init, ip_init函数后,系统完成了由下而上的各层接口之间的衔接

  • inet_proto_init:

    1. 会话层向表示层注册接口(下层到上层的注册), struct proto_ops *pops[NPROTO]中每个结构体代表一个域(会话层协议)

    2. 传输层向网络层注册接口(上层到下层注册),每个元素代表一个传输层协议(tcp, udp, icmp, igmp);其中icpm/igmp介于网络层和传输层之间,比较特殊

  • arp_init, ip_init:

    1. 网络层往链路层注册接口(上层到下层注册),每个packet_type代表一种网络层协议

数据包接收通道解析

当我们open一个网卡,会触发内核将网卡的接收驱动函数(struct irqaction)通过request_irq注册到内核的中

  1. 物理层–>驱动(3c501.c):硬件监听物理传输介质,进行数据接受,当完全接受一个数据包后,产生中断,调用对应中断的irqaction->handler(xx_interrupt)函数

  2. 驱动–>链路层(dev.c):xx_interrupt判断是rx报文中断后,会调用对应的xx_receive()函数,在其中完成sk_buff结构对报文的封装,并通过insb完成从硬件缓冲区到内核缓冲区的复制,最终调用netif_rx,将数据包由驱动层传递给链路层

2.1. 链路层:在netif_rx中,为了减少中断执行时间,会将数据包丢到sk_buff链表(struct sk_buff_head backlog)中,等待软中断从队列中取包处理;并在软中断向量表bh_active中注册,do_bottom_half()会检测该向量,并调用对应的软中断函数

  1. 链路层–>网络层(ip.c,arp.c):在网络软中断net_bh()函数中,首先会尝试对所有dev_base链表中硬件设备发包;再遍历backlog队列,则找到该包对应的struct packet_type,调用packet_type->func()将包发给网络层

  2. 网络层–>传输层(protocal.c):在ip_rcv()中,根据ipheader->protocal(传输层协议号),找到对应的inet_protocal结构,调用结构中的接收函数,将报文送到传输层,例如 tcp_rcv()

  3. 传输层–>会话层(tcp.c,udp.c):根据tcp_rcv()中,根据tcp->dest(目标端口号)从tcp_prot->sock_array数组中找到本会话的套接字struct sock;通过tcp_data()将数据包挂入该sock结构的缓存队列中sock->receive_queue,完成数据的读取

  4. 会话层–>用户态(数据拷贝):根据收件描述符(sockfd)得到对应的节点(inode)结构,由节点得到对应的socket结构(作为inode结构中union类型字段存在),进而得到对应的sock结构

数据包发送通道解析

  1. 物理层:提供硬件发送函数注册到对应device->hard_start_xmit(ei_start_xmit函数)中,该函数会将内核缓冲区发送数据拷贝至NE网卡设备硬件缓冲区

  2. 链路层:提供dev_queue_xmit()发送函数供网络层使用,调用device->hard_start_xmit指针指向的具体硬件发送函数

  3. 网络层:提供ip_queue_xmit()发送函数供传输层使用,将数据包缓存域sock->send_head队列

  4. 传输层:在不同协议的 inet_prot 中提供了不同的发包封装函数(例如 tcp_write()),在本层完成数据封装到sk_buff结构中

  5. 会话层/表示层:sock_write()

  6. 应用层:write()

附录

socket

/* 表示层(BSD层)描述结构体 */
struct socket {
  ...
  /* inet会话层协议向BSD层注册的结构,不同协议对应不同操作函数
   * 在proto_init中通过 sock_register()注册,且有个全局结构体数组struct proto_ops *pops[NPROTO]
   * 例如 INET域的 inet_proto_ops 操作函数集, inet_proto_init->inet_proto_ops
   */
  struct proto_ops	*ops;		
  ...
}

sock

/* 会话层描述结构体,linux 1.2.13版本该结构体杂糅了太多东西,我们只看关心部分 */
struct sock {
  ...
  /* 传输层向会话层注册的接口;不同的传输层协议,接口指针不同 */
  struct proto			*prot
  ...
  /* 指向所属的BSD层 */
  struct socket			*socket;
  ...
}

proto

/* 相当于一个传输层协议描述结构体,每个传输层协议都对应一个proto
 * tcp->tcp_prot; udp->udp_prot
 */
struct proto {
  ...
  unsigned short	max_header;
  unsigned long		retransmits;
  /* 所有会话层sock在对应的传输层注册结构体 
   * 例如所有tcp协议的套接字对应sock结构都被划入tcp_prot全局变量表示的proto结构
   */
  struct sock *		sock_array[SOCK_ARRAY_SIZE];
  char			name[80];
  int			inuse, highestinuse;
};

inet_protocol

/* 传输层向网络层注册的收包接口结构体,在protocol.c中定义, tcp,udp
 * 所有协议初始化后在全局结构体inet_protos中
 */
struct inet_protocol {
/* 传输层协议报文处理函数;在此会将报文传给上层协议
 * 一般通过get_sock*方法,获取上层协议封装结构体
 */
  int			(*handler)(struct sk_buff *skb, struct device *dev,
				   struct options *opt, unsigned long daddr,
				   unsigned short len, unsigned long saddr,
				   int redo, struct inet_protocol *protocol);
  int			(*frag_handler)(struct sk_buff *skb, struct device *dev,
				   struct options *opt, unsigned long daddr,
				   unsigned short len, unsigned long saddr,
				   int redo, struct inet_protocol *protocol);
/* 传输层协议异常报文处理函数 */
  void			(*err_handler)(int err, unsigned char *buff,
				       unsigned long daddr,
				       unsigned long saddr,
				       struct inet_protocol *protocol);
/* 所有传输层协议作为一个单向链表存在 */
  struct inet_protocol *next;
/* 协议号 */
  unsigned char		protocol;
  unsigned char		copy:1;
  void			*data;
  char 			*name;
};
static struct inet_protocol udp_protocol = {
  udp_rcv,		/* UDP handler		*/
  NULL,			/* Will be UDP fraglist handler */
  udp_err,		/* UDP error control	*/
  &tcp_protocol,	/* next			*/
  IPPROTO_UDP,		/* protocol ID		*/
  0,			/* copy			*/
  NULL,			/* data			*/
  "UDP"			/* name			*/
}

packet_type

/* 网络层描述结构体,在netdevice.h中定义, IP,ICMP、IGMP
 * 所有协议初始化后在全局结构体inet_protos中
 */
struct packet_type {
  unsigned short	type;	/* This is really htons(ether_type). */
  struct device *	dev;
  int			(*func) (struct sk_buff *, struct device *,
				 struct packet_type *);
  void			*data;
  struct packet_type	*next;
};
static struct packet_type arp_packet_type =
{
	0,	/* Should be: __constant_htons(ETH_P_ARP) - but this _doesn't_ come out constant! */
	NULL,		/* All devices */
	arp_rcv,
	NULL,
	NULL
}

arp_table

/* ARP缓存中的每一个由arp_table结构表示,将这些表项串联起来构成链表,就构成了ARP缓存 */
struct arp_table
{
	struct arp_table		*next;			/* Linked entry list 		*/
	unsigned long			last_used;		/* For expiry 			*/
	unsigned int			flags;			/* ATF_PERM 永不过期 		*/
	unsigned long			ip;			/* ip address of entry 		*/
	unsigned long			mask;			/* netmask - used for generalised proxy arps (tridge) 		*/
	unsigned char			ha[MAX_ADDR_LEN];	/* Hardware address		*/
	unsigned char			hlen;			/* Length of hardware address 	*/
	unsigned short			htype;			/* Type of hardware in use	*/
	struct device			*dev;			/* Device the entry is tied to 	*/

	/*
	 *	The following entries are only used for unresolved hw addresses.
	 */
	
	struct timer_list		timer;			/* expire timer 		*/
	int				retries;		/* remaining retries	 	*/
	struct sk_buff_head		skb;			/* list of queued packets 	*/
};

参考资料

电子版下载

OSI七层模型

内核通知链

linux内核协议栈详解