参考教材《TCP/IP详解 卷2:实现》,文内示意图大多取自该教材。本文为复习时自行总结,难免存在不全或者错误,求求大伙轻喷。如有问题可以留言在文末评论区,或者直接私聊,我会及时更改文章内容。
基本框架如下图所示,我们会在后面详细讨论输入输出
这段C代码也充当后续我们分析输入输出时的例子
/*
* Send a UDP datagram to the daytime server on some other host,
* read the reply. and print the time and date on the server .
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFSIZE 150 /* arbitrary size */
int
main()
{
struct sockaddr_in serv;
char buff [BUFFSIZE] ;
int sockfd, n;
if ((sockfd = socket(PF_INET, SOCK_DGRAM,0)) < 0)
err_sys (" socket error") ;
bzero((char *) &serv, sizeof (serv));
serv.sin_family = AF_INET;
serv.sin_addr.s_addr = inet_addr("140.252.1.32");
serv.sin_port = htons(13);
if (sendto (sockfd, buff, BUFFSIZE,0,
(struct sockaddr *) &serv, sizeof(serv)) != BUFFSIZE)
err_sys ("sendto error");
if ((n = recvfrom(sockfd, buff, BUFFSIZE, 0,
(struct sockaddr *) NULL, (int *) NULL))< 2)
err_sys("recvfrom error");
buff[n - 2] = 0; /* null terminate */
printf("&s\n", buff);
exit(0);
}
下面考虑插口层调用sendto()
函数时发生的流程
如图是一个含有150字节的mbuf分组。宏观来看,可以发现mbuf
是由m_next
指针链接的单链表,这种安排方式叫做mbuf链表.
细节来看各个成员变量及其含义:
m_next
用来连接下一个mbuf
m_nextpkt
用来连接下一个mbuf分组m_len
标识所在的mbuf
包含的data数据长度m_type
标识所在mbuf
的类型m_data
指向所在mbuf
的数据开头地址m_flags
用来标识所在mbuf
的类型,但不同于m_type
以及两个特殊的成员,共同组成了mbuf分组首部,占8个byte。只有当mbuf的flags为M_PKTHDR时,才启用:
m_pkthdr.len
用来表示整个分组的data长度m_pkthdr.rcvif
整个mbuf的size为128 byte,除去前面的成员变量和首部的话可以存储100字节的数据,如果不包含mbuf分组首部,则可以存储108字节。
下图是以一个150字节数据的mbuf,加入IP和UDP首部后的mbuf情况
具体来说,当UDP输出例程被调用时:
m_next
域指向原有传入的mbuf指针另外需要注意,我们在第4步中将分组首部复制到了新的mbuf中,那么新旧mbuf的m_flags域也要进行相应的设置。
于是新的mbuf成为了新的分组首部,且mbuf链表的总长度由2变3,mbuf分组的数据长度增加了28,变成了178byte。可以看到,在分组首部和IP/UDP首部之间存在78字节的空缺,通过适当的调整m_data
和m_len
我们可以在其中加入之后的首部,而不需要新建mbuf.
之后由UDP例程填写UDP首部和IP首部的一部分内容,且UDP检验和计算后也会存储在这里。接着,UDP输出例程调用IP输出例程,并把此mbuf链表的指针传递给IP输出例程。UDP和IP首部剩余的部分由IP输出例程进行填写,如IP校验和等等。
在完成上述操作后,IP输出例程调用以太网接口并把mbuf链表继续传递,以太网输出函数流程如下:
14字节的以太网首部包含:6字节的以太网源地址,6字节的以太网目标地址,2字节的以太网帧类型
如下图所示,当进程调用sendto()
系统调用时:
输入处理与刚谈到的输出处理不同,因为输入时异步的。具体来讲,他是通过一个输入完成的中断使以太网设备驱动程序来接受一个输入分组,而不是通过进程的系统调用。内核处理这个设备中断,并调度设备驱动程序进入运行状态。
假设一个正常的接受已经完成,以太网设备驱动程序会处理这个中断。在下图的这个例子中,我们接收了54个字节的数据,并将其复制到了mbuf当中,其中包括:20字节的IP首部,8字节的UDP首部,26字节的数据
可以看到其中分配了16字节,但是并没有使用。这个空间是分配给接口层的首部,其他数据存放在剩余的84字节中。
接着设备驱动程序会根据以太网帧中的类型字段来决定这个分由那个协议层来接收。在这个例子中,将由IP输入例程进行接收。从而mbuf会被加入到IP输入队列当中,另外会产生一个软中断来执行IP输入例程。
IP输入是异步的,通过软中断来执行。这个软中断由接口层接收到IP数据报时触发。IP处理例程会循环处理IP输入队列上的每一个数据报,并在整个队列完成后返回。IP数据报处理流程包括:
在本例中,我们假设下一步输入处理例程为UDP
UDP输入例程会验证UDP首部中的各字段,然后确定是否一个进程接收次数据报。
一个进程可以接收到指定UDP端口的所有数据报,或让内核根据源与目标IP地址与目标端口号来限制数据报的接收。
可以看到,原有的mbuf被插入到了一个新的mbuf中。由于我们的数据报需要传给进程,所以这个新的mbuf包含了发送方的IP地址和端口号(从IP首部中拆分出来)。
另外,如果我们对比图中右侧放置数据的mbuf和以太网输入的mbuf可以发现,m_len
和m_pkthdr.len
都减小了28字节(即IP首部和UDP首部的部分),m_data
指向的地址也减小了28,只保留了26字节的data数据。
链表的第一个mbuf的m_type类型被设置为了MT_SONAME,且这个mbuf是由插口层建立,将这些信息返回给调用系统调用recvfrom和recvmsg的进程中。即便第二个mbuf有空间可以用来存储插口地址结构(发送方IP地址和端口号),也不能放置在一起。因为两个mbuf的类型并不相同,一个是MT_SONAME,一个是MT_DATA。
当进程调用recvfrom()
时,进程会在内核中保持睡眠状态。现在,内核会唤醒我们的进程,并把mbuf中的数据复制到程序的缓存中。然后释放掉这些mbuf
在调用recvfrom时,我们将第5、6个参数设置为NULL,表示我们并不关心发送方的IP地址和UDP端口号,此时recvform只会返回第二个mbuf的data。
如下图所示是8个硬件及软件中断的优先级。划红线的部分为我们之前谈到的两个部分:splnet(软中断,执行协议层代码),splimp(硬中断执行接口层代码)
由上图所示,根据mbufflags
域,我们可以将mbuf分为4类:
阴影部分为不使用的部分
可以看到,后两类的muf又多了三种域:
m_ext.ext_buf
,指向额外空间的起始地址(m_data指向的是缓存的起始)m_ext.ext_free
,在Net/3中不使用m_ext.ext_size
,额外空间的大小,一般为1024或2048,本书以2048为主mub#define MGET(m, how, type) { \
MALLOC((m), struct mbuf *, MSIZE, mbtypes [type], (how)); \
if(m){\
(m)->m_type = (type); \
MBUFLOCK (mbstat.m_mtypes [type]++;) \
(n)->m_next = (struct mbuf * )NULL; \
(m)->m_nextpkt = (struct mbuf *)NULL; \
(m) ->m_data = (m)->m_dat; \
(m)->m_flags = 0; \
} else \
(m) = m_retry((how),(type)); \
这里就不做源码上详细解释,自己看应该能懂。只稍微说几个点:
MBUFLOCK
宏是用来保证执行mbstat.m_mtypes[type]++
的优先级,防止+1时被打断m->data = m->dat
是让data指向缓存区域的开始地址mbstat 是一个用来管理mbuf的全局变量,前面我并没有提到。是因为我觉得这个东西可能不是那么重要,没详细去看😀
struct mbuf* m_get(nowait, type)
int nowait, type;
{
struct mbuf *m;
MGET(m, nowait, type);
retrun (m);
}
没什么东西,就是MGET宏套了个函数外套。nowait的值为M_WAIT或M_DONTWAIT,取决于在存储器不可用时是否等待。
struct mbuf* m_retry(i, t)
int i, t;
{
struct mbuf *m;
m_relaim();
#define m_retry(i,t) (struct mbuf *)0
MGET(m, nowait, type);
#undef m_retry
retrun (m);
}
这是MGET宏的else分支执行的函数,通过m_relaim()
试图腾出一部分空间,然后再次尝试MGET
当接收到以太网帧时,设备驱动程序调用函数m_devget()
创建mbuf来接受数据,根据数据长度不同,会形成四种类型的mbuf链表,如下面所示:
#define mtod(m, t) ((t) ((m)->m_data) )
#define dtom(x) ((struct mbuf *) ((int) (x) & ~ (MSIZE-1)))
mtod()
宏可以直接理解成mbuf to data,即返回mbuf中的m_data指针,不过额外做了类型转换
dtom()
可以理解为data to mbuf,即通过data指针返回指向mbuf的指针
dtom()实际原理也比较简单,MSIZE为128,那么~(MSIZE-1)其实就是0xffffff80,可以明显看到byte的二进制形式位 1000 0000通过清理低位来找到mbuf的起始地址。由于本书全是32位的系统,所以这里我也用的是32位的地址举例。
知道dtom()
的原理,很容易就发现其只适用于不含额外扩容的mbuf结构,此时需要m_pullup
处理
当数据报长度小于协议首部大小时,m_pullup
会将前N个字节数据重组在第一个mbuf中,试图恢复正常的协议首部。N为传入参数,但是当mbuf链表上数据总长度小于N时,函数将失效,该数据报会被丢弃。
就是刚刚谈到的dtom()
问题,当mbuf中有扩容地址或者说数据在“簇”中存放时,这里m_pullup
,将会新建一个mbuf插入链表头,并把簇的前40个byte拷贝近新的mbuf链表头内。
40个字节是因为最大的协议首部为40(20 IP首部+20 TCP首部),这样可以保证在后续传给高层协议处理时,可以正常处理。
下图2-17为处理前,2-18为处理后。另外需要注意到,处理后的m_data指针指向了簇内去除40byte之后的位置,验证了我们之前谈到的m_data指向的是数据的起始,而不是缓存区域的起始。
另外需要注意,TCP不使用m_pullup()
,而是采取另外不同的技术
个人感觉前面哪些详细分析已经差不多了,这数据结构自己也应该能画出来😝,就放两张图看看就好
这个part是一个比较有趣的部分,但流程相对来说也更复杂一些
这里我们假设一个例子,一个程序想把共4096byte写入到TCP插口当中,且TCP传输的最大报文大小为1460。那么基本流程如下:
簇引用计数用来表示一个簇同时被几个mbuf指向。每当释放一个mbuf,该mbuf所指向的簇的引用计数-1,当且仅当簇的引用计数为0时,簇可以被释放。这是为了防止在共享簇时,上述操作中的释放mbuf可能会导致释放掉共享的簇而导致数据丢失。
结构ifnet中包含所有接口的通用信息,每个网络设备拥有独立的ifnet结构。ifnet结构有一个列表,包含这个设备的一个或多个协议地址。
struct ifnet {
struct ifnet *if_next; /* a11 struct ifnets are chained */
struct ifaddr *if_addrlist; /* linked list of addresses per if */
char *if_name; /* name, e.g. 'le' or 'lo' */
short if_unit; /* sub-unit for lower level driver */
u_short if_index; /* numeric abbreviation for this if */
short if_flags ; /* Figure 3.7 */
short if_timer ; /* time 'ti1 if_watchdog called */
int if_pcount; /* number of promiscuous listeners */
caddr_t if_bpf; /* packet filter structure */
struct if_ _data {
/* generic interface information */
u_char ifi_type; /* Figure 3.9 */
u_char ifi_addr1en; /* media address 1ength */
u_char ifi_harlen; /* media header length */
u_1ong ifi_mtu; /* maximum transmission unit */
u_long ifi_metric ; /* lrouting metric (external on1y) */
u_1ong ifi_baudrate; /* linespeed */
/* other ifner members */
#define if_mtu if_data.ifi_mtu
#define if_type if_data.ifi_type
#define if_addrlen if_data.ifi_addrlen
#define if_hdrlen if_data.ifi_hdrlen
#define if_metric if_data.ifi_metric
#define if_baudrate if_data.ifi_baudrate
这里每个部分都有注释,就不用我解释了吧😭,困死了要,这些b东西能考什么,哥们也背不下来啊
ifaddr{}
结构列表,每个ifaddr{}
与一个协议相对应struct ifaddr {
struct ifaddr *ifa_next; /* next address for interface */
struct ifnet *ifa_ifp; /* back-pointer to interface */
struct sockaddr *ifa_addr ; /* address of interface */
struct sockaddr *ifa_dstaddr ; /* other end of p-to-p link */
#define ifa_broadaddr ifa_dstaddr /* broadcast address interface */
struct sockaddr* ifa_netmask; /* used to determine subnet */
void (*ifa_rtrequest) (); /* check or clean routes */
u_short ifa_f1ags; /*mostly rt_ flags for cloning */
short ifa_refcnt; /* references to this structure */
int ifa_metric; /* cost for this interface */
};
感觉这两个结构只要搞懂图就差不多了吧😭
看看图差不多得了,这b课真该死啊😅
用来完成ifnet结构的初始化和搭建,第三章是真逆天,差不多得了
if_attach
被调用了三次: 以一个le_softc
结构为参数从leattach
调用,以一个sl_softc
结构为数从slattach
调用,以一个通用ifnet
结构为参数从loopattach
调用。每次调用时,它向ifnet列表中添加一个的ifnet
结构,为这个接口创建一个链路层ifaddr
结构(包含两个sockaddr_dl
结构),并且初始化ifnet_addrs
数组中的一项。