原文链接:https://mp.weixin.qq.com/s/jiY6uApi0OmwTHSmLB-j8g
本文要点
《TCP/IP详解 卷2》原书章节简介
第一个网络编程示例
网络层次结构
描述符
mbuf
网络输出与输入
中断与并发
源码组织结构
《TCP/IP详解 卷2》原书章节简介
原书共32个章节(如图1所示),详细的介绍了TCP/IP协议栈的实现原理与细节。从网络层次结构来讲分为接口层、协议层、插口层;而协议来讲分有以太网、ARP、SLIP、IP、ICMP、IGMP、UDP、TCP等协议。
图1 《TCP/IP详解 卷2》章节概要第一个网络编程示例
按照学习一门新编程语言的惯例,首先学会第一个"hello world"程序,下面将介绍第一个网络程序入门示例,如图2所示。
图2 源码示例1. 创建一个数据报接口
19-20 socket函数创建一个UDP插口,并且给进程返回一个描述符,保存在变量sockfd中。
2. 将服务器地址存入socketaddr_in
21-24 设置服务器ip地址(140.252.1.32)和端口(13),htons将一个主机字节序列(可能是低字节在后)转换为网络字节序列(高字节在后),不理解的可以搜索大端/小端相关问题。
3. 发送数据报给服务器
25-27 程序调用sendto发送一个150字节的数据报给服务器,150字节的数据是未知的,因为服务器不管收到什么数据都返回当时时间和日期。
4. 读取从服务器返回的数据报
程序通过调用recvfrom来读取从服务器发回的数据,然后输出。
网络层次结构
内核网络代码组织成三层结构,如图3所示:
图3 网络层次结构插口层 是一个到下层协议相关层的协议无关接口。所有的系统调用从协议无关的插口层开始。例如:在插口层中的bind系统调用的协议无关代码包含几十行代码,它们验证的第一个参数是一个有效的插口描述符,第二个参数是一个进程中的有效指针。然后调用下层协议相关代码,协议相关代码可能包含几百行代码。
协议层 包括四种协议族(TCP/IP, XNS, OSI和Unix域)的实现。每个协议话可能包含自己的内部结构。例如,在Internet协议族中,IP网络层在最低层,TPC和UDP两个运输层在IP层之上。
接口层 包括网络设备通信的设备驱动程序,属于数据链路层。
描述符
在图2的示例中,一开始调用socket,要求指定插口类型,Internet协议族为PF_INET和数据报插口为SOCK_DGRAM,组合成一个UDP协议插口。
socket函数的返回值就是一个描述符,它具有其它unix描述符的所有特性:可以通过它调用read和write;可能调用fcntl来改变它的属性;还可以作为sento和recvfrom函数的第一个参数。
下面将介绍描述符在一个进程中所处位置以及相关数据结构,如下图所求:
图4 socket在进程中的位置每个进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,每一项表示一个描述符项,例如图4中的p_fd,它指向进程的filedesc结构。在这个结构中包含fd_ofileflags(一个字符数组指针,每个描述符有一个描述符标志)和fd_ofiles(一个指向文件表结构的指针数组的指针)。
项fd_ofiles指向的数据结构用*file{ }[ ]来表示。它是一个指向file结构的指针数组。这个数组及描述符标志数组的下标就是描述符本身:0,1,2等(好好理解一下)。
结构file的成员f_type指示描述符的类型是DTYPE_SOCKET和DTYPE_VNODE。v-node是一个通用机制,允许内核支持不同类型的文件系统,本文中关心的不是v-node,因为TCP/IP插口的类型总是DTYPE_SOCKET。
结构file的成员f_data指向一个socket结构或者vnode结构,根据描述符类型而定。成员f_ops指向一个有5个函数指针的向量。这些函数指针用在read、readv、write、writev、ioctl、select和close系统调用中,这些系统调用需要一个插口描述符或者非插口描述符。图4中fileops中的fo_read等函数表示结构成员名称,左边对应的soo_read/vn_read等函数表示结构成员的实际内容。
接下来我们来看一下在执行一个网络系统调用时的大致查找流程(假设ftype为DTYPE_SOCKET):
1. 当进程执行一个系统调用时,如sendto,内核从描述符值开始,使用fd_ofiles索引到file结构指针向量到描述符所对应的file结构,file结构指向socket结构,结构socket带有指向结构inpcb(Internet协议控制块)的指针。
2. 当一个UDP数据报达到一个网络接口时,内核搜索所有UDP协议控制块,寻找一个合适的,至少要根据目标UDP端口号,可能还要根据目标IP地址、源IP地址和源端口号。一旦定位所找的inpcb,内核就能通过inp_socket指针来找到相应的socket结构。
其中,成员inp_faddr和inp_laddr包含远程和本地IP地址,成员inp_fport和inp_lport包含远程和本地端口号。IP地址和端口号组成一个插口。图54中udp是一个全局结构,它是所有UDP PCB(协议控制块)组成的链表表头。成员inp_next和inp_pre把所有的UDP PCB 组成一个双向环形链表。
mbuf
mbuf是存储器缓存,在整个网络代码中用于存储各种信息,是很核心的一个数据结构,我们将在下一章节详细介绍它,在这里我们先简单了解下它的常见用法。
1. 包含插口地址结构的mbuf
图5 包含socketf地址的mbuf在图5中,mbuf的前20个字节是首部,它包含关于这个mbuf的一些基础信息,由四个4字节字段和两个2字节字段组成,mbuf总长为128个字节。
m_next和m_nextpkt用于将多个mbuf连接起来;m_data指向mbuf中的数据,本例中数据为插口地址(sockaddr_in{}),m_len指示它的长度;m_type指示mbuf中的数据类型,本例中为MT_SONAME(插口名称),m_flags为标志位。
2. 包含数据的mbuf
图6 包含数据的mbuf图6中的结构由两个mbuf组成,通过m_next指针指向下一个mbuf,这种方式叫作mbuf链表。与图5中的mbuf最大的差别是第一个mbuf首部增加了两个成员:m_pkthdr.len和m_pkthdr.rcvif。这两个成员组成了一个分组首部并且只用在链表的第一个mbuf中。m_flags的值为M_PKTHDR,指示这个mbuf包含一个分组首部。m_pkthdr.len包含整个mbuf链中的数据总长度(本例中为150),m_pkthdr.rcvif包含了一个指向接收分组的接收接口结构的指针。
3. 添加IP和UDP首部
图7 添加ip和udp首在插口层,通常将目标插口地址结构复制到一个mbuf中,并把数据复制到mbu链中,然后与此插口描述符对应的协议层被调用。明确地说,UDP输出例程被调用,指向mbuf的指针被作为一个参数传递。这个例程在这150个字节数据前添加一个IP首部和一个UDP首部,然后将这个mbuf链传递给IP输出例程。
在图7中,IP首部和UDP首部放置在第一个mbuf中的最后部分,同样的第一个mbuf中包含了m_pkthdr.len和m_pkthdr.rcvif,同时设置M_PKTHDR标志;而原来的分组首部(第2个mbuf中)空间现在未用,同时清除M_PKTHDR标志。
网络输出与输入
1. 输出过程
图8给出了一个进程调用 sendto传输一个UDP数据报时的大致处理过程。
IP输出例程要填写IP首部中剩余字段,包括IP检验和;确定数据报应由哪个输出接口发出;必要时对IP报文分片。假设输出接口是一个以太网接口,再次把mbuf链的指针作为参数传递给以太网输出函数。
以太网输出函数第一个功能就是通过ARP地址解析将32位IP地址转换为48位以太网地址。然后将14字节(6字节的以太网目标地址、6字节的以太网源地址和2字节的以太网帧类型)的以太网首部添加到链表的第一个mbuf中,紧接在IP首部的前面,然后将mbuf链表加到此接口的输出队列中等待被发送。
图8 三层处理一个简单UDP输出执行过程2. 输入过程
与输出处理不同,因为输入处理是异步的,是通过中断的方式来接收一个分组,而不同输出处理是通过进程的系统调用。
当数据到达接口时(假设是以太网)产生中断,以太网设备驱动程序处理将数据从设备读取到mbuf链表中,同时根据帧类型将mbuf链表向上层协议提交,假设类型字段为IP数据报,则将mbuf链加入到IP输出队列中。
此时,会产生一个软中断来执行IP输入例程,可见IP输入也是异步的。IP输入例程会循环处理它的输入队列中的每一个IP数据报,直到处理完后返回。
IP输入例程会根据首部协议字段继续将数据传递到上层协议的缓存队列中,调用上层协议的输入例程处理数据,最终将数据提交给应用进程。
中断与并发
输入处理是异步的中断驱动的方式。首先,一个设备中断引发接口层代码执行,然后它产生一个软中断引发协议层代码执行。当内核完成这些级别的中断后,执行插口代码。
在这里给每个硬件和软件中断分配一个优先级。如下图9所示:
图9 中断优先级对于不同优先级,比较关心的问题就是如何处理那些在不同级别的进程间共享的数据结构。在图3中显示三种不同优先级进程间共享的数据结构——插口队列、接口队列和协议队列。例如,当IP输入例程正在从它的输入队列中取出一个接收分组时,一个设备中断发生,抢占了协议层,并且那个设备驱动程序可能添加另外一个分组到IP输入队列。如果不协调它们对数据的访问,可能会破坏数据完整性。
那么怎么解决这个问题呢?Net/3代码经常通过调用函数splimp和splnet。这两个调用总是与splx成对出现,splx使处理器返回到原来的优先级。
如下面代码所示:
struct mbuf *m
int s;
s = splimt();
IF_DEQUEUE(&ipintrq, m);
splx(s);
if (m == 0):
return
当协议层IP输入函数执行时,先调用splimp把CPU优先级升高到网络设备驱动程序级,防止任何网络设备驱动程序中断发生。原来的优先级存储到变量s中。然后执行宏IF_DEQUEUE把IP输入队列ipintrq头部的第二个分组删去,并将指向mbuf链表的指针存储在变量m中。最后通过splx恢复CPU优先级。由于在splimp和splx之间所有的网络设备驱动程序的中断被禁止,所以在这两个调用间代码应尽量少。
源码组织结构
本系列文章所有源码均是基于Net/3版本,其源代码组织结构如图10所示:
图10 源代码组织结构本系列文章的重点在目录netinet,它包含了所有TCP/IP源代码
更多最新文章尽在公众号:大白爱爬山,欢迎关注!
网友评论