《计算机网络基础 — 以太网》
《计算机网络基础 — 物理网络》
《计算机网络基础 — TCP/IP 网络模型》
本文主要记录 Linux 内核网络协议栈的运行原理,为学习记录,仅做参考,大量内容来自网络,详见参考文章列表。
NOTE:本文涉及到两个 Linux kernel 版本 1.2.13 以及 2.6.32。
封装:当应用程序用 TCP 协议传送数据时,数据首先进入内核网络协议栈中,然后逐一通过 TCP/IP 协议族的每层直到被当作一串比特流送入网络。对于每一层而言,对收到的数据都会封装相应的协议首部信息(有时还会增加尾部信息)。TCP 协议传给 IP 协议的数据单元称作 TCP 报文段,或简称 TCP 段(TCP segment)。IP 传给数据链路层的数据单元称作 IP 数据报(IP datagram),最后通过以太网传输的比特流称作帧(Frame)。
分用:当目的主机收到一个以太网数据帧时,数据就开始从内核网络协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议都会检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称作分用。
协议栈实现层级:
read()
、write()
、open()
、ioctl()
等。这需要从内核启动流程说起。当内核完成自解压过程后进入内核启动流程,这一过程先在 arch/mips/kernel/head.S 程序中,这个程序负责数据区(BBS)、中断描述表(IDT)、段描述表(GDT)、页表和寄存器的初始化,程序中定义了内核的入口函数 kernel_entry()
、kernel_entry()
函数是体系结构相关的汇编代码,它首先初始化内核堆栈段为创建系统中的第一过程进行准备,接着用一段循环将内核映像的未初始化的数据段清零,最后跳到 start_kernel()
函数中初始化硬件相关的代码,完成 Linux Kernel 环境的建立。
start_kenrel()
定义在 init/main.c 中,真正的内核初始化过程就是从这里才开始。函数 start_kerenl()
将会调用一系列的初始化函数,如:平台初始化,内存初始化,陷阱初始化,中断初始化,进程调度初始化,缓冲区初始化,完成内核本身的各方面设置,目的是最终建立起基本完整的 Linux 内核环境。
start_kernel()
的过程中会执行 socket_init()
来完成协议栈的初始化,实现如下:
void sock_init(void)//网络栈初始化
{
int i;
printk("Swansea University Computer Society NET3.019\n");
/*
* Initialize all address (protocol) families.
*/
for (i = 0; i < NPROTO; ++i) pops[i] = NULL;
/*
* Initialize the protocols module.
*/
proto_init();
#ifdef CONFIG_NET
/*
* Initialize the DEV module.
*/
dev_init();
/*
* And the bottom half handler
*/
bh_base[NET_BH].routine= net_bh;
enable_bh(NET_BH);
#endif
}
rc = proto_register(&udp_prot, 1);
:注册 INET 层 UDP 协议,为其分配快速缓存。(void)sock_register(&inet_family_ops);
:向 static const struct net_proto_family *net_families[NPROTO]
结构体注册 INET 协议族的操作集合(主要是 INET socket 的创建操作)。inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0;
:向 externconst struct net_protocol *inet_protos[MAX_INET_PROTOS]
结构体注册传输层 UDP 的操作集合。static struct list_head inetsw[SOCK_MAX]; for (r = &inetsw[0]; r < &inetsw[SOCK_MAX];++r) INIT_LIST_HEAD(r);
:初始化 SOCKET 类型数组,其中保存了这是个链表数组,每个元素是一个链表,连接使用同种 SOCKET 类型的协议和操作集合。for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
:
inet_register_protosw(q);
:向 sock 注册协议的的调用操作集合。arp_init();
:启动 ARP 协议支持。ip_init();
:启动 IP 协议支持。udp_init();
:启动 UDP 协议支持。dev_add_pack(&ip_packet_type);
:向 ptype_base[PTYPE_HASH_SIZE];
注册 IP 协议的操作集合。socket.c
提供的系统调用接口。协议栈初始化完成后再执行 dev_init()
,继续设备的初始化。
硬件层与设备无关层:硬件监听物理介质,进行数据的接收,当接收的数据填满了缓冲区,硬件就会产生中断,中断产生后,系统会转向中断服务子程序。在中断服务子程序中,数据会从硬件的缓冲区复制到内核的空间缓冲区,并包装成一个数据结构(sk_buff),然后调用对驱动层的接口函数 netif_rx()
将数据包发送给设备无关层。该函数的实现在 net/inet/dev.c 中,采用了 bootom half 技术,该技术的原理是将中断处理程序人为的分为两部分,上半部分是实时性要求较高的任务,后半部分可以稍后完成,这样就可以节省中断程序的处理时间,整体提高了系统的性能。
NOTE:在整个协议栈实现中 dev.c 文件的作用重大,它衔接了其下的硬件层和其上的网络协议层,可以称它为链路层模块,或者设备无关层的实现。
网络协议层:就以 IP 数据报为例,从设备无关层向网络协议层传递时会调用 ip_rcv()
。该函数会根据 IP 首部中使用的传输层协议来调用相应协议的处理函数。UDP 对应 udp_rcv()
、TCP 对应 tcp_rcv()
、ICMP 对应 icmp_rcv()
、IGMP 对应 igmp_rcv()
。以 tcp_rcv()
为例,所有使用 TCP 协议的套接字对应的 sock 结构体都被挂入 tcp_prot 全局变量表示的 proto 结构之 sock_array 数组中,采用以本地端口号为索引的插入方式。所以,当 tcp_rcv()
接收到一个数据包,在完成必要的检查和处理后,其将以 TCP 协议首部中目的端口号为索引,在 tcp_prot 对应的 sock 结构体之 sock_array 数组中得到正确的 sock 结构体队列,再辅之以其他条件遍历该队列进行对应 sock 结构体的查询,在得到匹配的 sock 结构体后,将数据包挂入该 sock 结构体中的缓存队列中(由 sock 结构体中的 receive_queue 字段指向),从而完成数据包的最终接收。
NOTE:虽然这里的 ICMP、IGMP 通常被划分为网络层协议,但是实际上他们都封装在 IP 协议里面,作为传输层对待。
协议无关层和系统调用接口层:当用户需要接收数据时,首先根据文件描述符 inode 得到 socket 结构体和 sock 结构体,然后从 sock 结构体中指向的队列 recieve_queue 中读取数据包,将数据包 copy 到用户空间缓冲区。数据就完整的从硬件中传输到用户空间。这样也完成了一次完整的从下到上的传输。
sock_write()
会调用 INET socket 层的 inet_wirte()
。INET socket 层会调用具体传输层协议的 write 函数,该函数是通过调用本层的 inet_send()
来实现的,inet_send()
的 UDP 协议对应的函数为 udp_write()
。udp_write()
调用本层的 udp_sendto()
完成功能。udp_sendto()
完成 sk_buff 结构体相应的设置和报头的填写后会调用 udp_send()
来发送数据。而在 udp_send()
中,最后会调用 ip_queue_xmit()
将数据包下放的网络层。ip_queue_xmit()
的功能是将数据包进行一系列复杂的操作,比如是检查数据包是否需要分片,是否是多播等一系列检查,最后调用 dev_queue_xmit()
发送数据。dev->hard_start_xmit(skb, dev);
。具体设备的发送函数在协议栈初始化的时候已经设置了。这里以 8390 网卡为例来说明驱动层的工作原理,在 net/drivers/8390.c 中函数 ethdev_init()
的设置如下:/* Initialize the rest of the 8390 device structure. */
int ethdev_init(struct device *dev)
{
if (ei_debug > 1)
printk(version);
if (dev->priv == NULL) { //申请私有空间
struct ei_device *ei_local; //8390 网卡设备的结构体
dev->priv = kmalloc(sizeof(struct ei_device), GFP_KERNEL); //申请内核内存空间
memset(dev->priv, 0, sizeof(struct ei_device));
ei_local = (struct ei_device *)dev->priv;
#ifndef NO_PINGPONG
ei_local->pingpong = 1;
#endif
}
/* The open call may be overridden by the card-specific code. */
if (dev->open == NULL)
dev->open = &ei_open; // 设备的打开函数
/* We should have a dev->stop entry also. */
dev->hard_start_xmit = &ei_start_xmit; // 设备的发送函数,定义在 8390.c 中
dev->get_stats = get_stats;
#ifdef HAVE_MULTICAST
dev->set_multicast_list = &set_multicast_list;
#endif
ether_setup(dev);
return 0;
}
https://blog.csdn.net/zxorange321/article/details/75676063
https://blog.csdn.net/geekcome/article/details/8333011