昨晚搞到快两点,今天9.30才起床,希望11点半之前能搞完去吃饭😭

第八章 IP:网际协议

IP分组

image-20230425092925635

参照上图,有几个基本概念(本例中的传输层协议为UDP):

  • 报文:由传输层送至IP的数据报,本身包含传输层首部和数据
  • 数据报:IP在报文的前面加上自己的IP首部,且根据长度考虑是否分片(上图1-2行)
  • :IP将分好片的数据报交给数据链路层,数据链路填上自己的首部后变为帧(上图3行)
  • 分组:一个分片的数据报或者一个不需分片的数据报为分组

需注意的是IP数据报结构存储不受机器大小端的影响,均按照下面地址结构存储

image-20230425093907332

IP数据报结构如上图所示,上面的个部分含义已在图中标出

可能需注意的地方:ip_hl常用4字节度量,即当ip_hl == 5时,实际首部长度为5*4=20。其中选项的部分为0~40不等,所以ip_hl的最大值为:(20+40)/4 = 15,最小值为:20/4=5。另外,由于ip_hl为4的倍数,所以选项的长度一般也为4的倍数。

ipintr函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ipintr ()
{
struct ip *ip;
struct mbuf *m;
struct ipq *fp;
struct in_ifaddr *ia;
int hlen, s;
next:
/*
Get next datagram off input queue and get IP header
in first mbuf.
*/
s = splimp() ;
IF_DEQUEUE (&ipintrq, m) ;
sp1x(s);
if(m==0) return;
/*
很多很多代码😭
*/
goto next;
bad:
m_freem(m);
goto next;
}

该函数的基本框架如上,大致就是依靠两个goto实现对分组处理的循环,直到队列为空结束。中间省略的代码主要做下面几件事(实在太长了):

  1. 验证:把分组从ipintrq中取出,验证它们的内容,损坏和有差错的分组自动被丢弃。
  2. 转发或不转发:调用ip_dooptions来处理IP选项,如果没到达最终目的地就尝试转发,反之交给对应传输层协议
  3. 重装(重装在第10章讲,那我就不管了,开摆!)
  4. 分用:重装完全的数据报,ip指针设置为到达目的地的完整数据报,反之为空

ip_forward函数

到达非目的地的分组需要被转发。只有当ipforwarding非零或当分组中包含源路由时,ipintr才调用该函数ip_forward实现转发。ip_forward依靠route结构体与路由表接口

1
2
3
4
struct route{
struct rtentry *ro_rt;
struct sockaddr ro_dst;
}
  • ro_rt 指向与路由表相关的rtentry结构体
  • ro_dst 指向存放目的地址信息的sockaddr结构体

ip_forward()的基本流程分为两部分:

  • 确定系统是否需要转发此分组,修改IP首部,位分组选择路由
  • 处理ICMP重定向报文,并把分组交给ip_output进行转发

流程中可能需注意的点:

  • IP转发算法把最近的路由缓存在全局route结构的ipforward_rt中
  • 当转发错误时,由且仅由路由器向源主机返回ICMP重定向报文,以此更新主机路由表

ip_output

ip_output为IP输出代码,主要为三部分:

  • 首部初始化
  • 路由选择
  • 源地址选择和分片

第23章 UDP

整体数据传输框架

image-20230425104820413

整体框架如上图所示,从上到下分别是:

用户进程(应用层)–>UDP(传输层)–>IP(网络层)–>数据链路层–>硬件网卡(物理层)

UDP 首部

image-20230425105506669

UDP首部如上图所示,对应域的含义也有标记。需要注意的是IP首部和UDP首部一般是紧挨在一起,我们在CH1-CH3中分析mbuf时已经可以注意到这种现象。而实际上在udp内相关函数(如udp_input,udp_output)处理时也会将两者放在一起使用,他们共同构成了结构体udpiphdr,如下所示:

1
2
3
4
struct udpiphdr{
struct ippvly ui_i; /* overlaid ip structure*/
struct udphdr ui_u; /* udp header*/
}

20字节的IP首部被定义为 ippvly结构体,如下所示,可自行对照IP首部结构图:

1
2
3
4
5
6
7
8
struct ipov1y {
caddr_t ih_next, ih_prev; /* for protocol sequence q's */
u_char ih_x1; /* (unused) */
u_char ih_pr; /* protocol */
short ih_len; /* protocol length */
struct in_addr ih_src; /* source internet address */
struct in_addr ih_dst; /* destination internet address */
};

udp_init函数

初始化函数,用来构建PCB双向链表的起始,让头部PCB的next指针和prev指针都指向自己。需注意UDP临时端口号由1024开始分配

前0~1023端口为常用端口,一般都有固定对应服务,想要监听这些端口时也需要sudo权限

PCB是22章的概念:PCB协议控制块存放各UDP和TCP插口所要求的多个信息片,UDP所需要的所有信息都可以在Internet PCB中找到,不存在UDP控制块

udp_out函数

当程序调用send,sendto,sendmsg,write或writev时会发生UDP输出。基本流程如下图:image-20230425111210300

udp_output主要干这几件事:

  • 添加IP/UDP首部,具体过程可以看前三章分析mbuf的部分
  • 检验和计算和伪首部

我们在前三章分析mbuf时说到过,udp会填写一部分首部,ip会填写一部分首部,这里就是分别调用udp_output,ip_output实现的image-20230425111755234

下图中阴影部分由ip_output()填写,其余浅色部分交由udp_output()填写

image-20230425111716276

UDP在检验和时会包含三部分内容:12字节的伪首部,8字节的UDP首部,UDP数据

image-20230425112329338

伪首部和UDP首部如上图所示,下图是填充首部和检验和的整个示意图

image-20230425112939950

CRC校验码

这部分机组的时候学过,自行适当复习一下,课件例题如下

image-20230425113213347

image-20230425113221906

udp_input函数

当IP在它的协议字段指定为UDP的输入队列上收到一个IP数据报时,才发生UDP的输入。IP通过协议交换表中的pr_input函数调用函数udp_input。因为IP的输入是在软件中断级,所以udp_input也在这一级上执行。udp_input的目标是把UDP数据报放置到合适的插口的缓存内,唤醒该插口上因输入阻塞的所有进程。udp_input 函数的主要分三个部分:

  • UDP对收到的数据报完成一般性的确认;

  • 处理目的地是单播地址的UDP数据报:找到合适的PCB,把数据报放到插口的缓存内,

  • 处理目的地是广播或多播地址的UDP数据报:必须把数据报提交给多个插口。

确认部分可以继续复习前三章mbuf分析部分,个人感觉这本章和前三章关系还是比较紧密的,可以多对照看看

课后练习题

image-20230425114209049