中心环节
从理论上来说,IP是整个协议软件的中心环节。它接收来自网络接口软件传入数据报,同时也接收由上层协议生成的外发数据报。IP在为数据报选择路由后,要么将其发送给一个网络接口,要么将其交给本机的上层协议
单从主机的角度来看,IP软件可以分为两个不同的部分考虑:一部分处理输入,另一部分处理输出。输入部分使用IP首部中的PROTO字段来决定应该由上层协议中的哪个模块来接收传入数据报。输出部分使用一张本地选路表来选择外发数据包要送往的下一跳。
将IP分隔成输入和输出两部分虽然由直觉上的吸引力,但它使得IP和上层协议软件之间的沟通变得十分笨拙。另外,IP软件必须能在网关中工作,而网关中的选路过程要比主机中的更复杂。实际上,由于一个网关必须将发送来的数据报继续转送它的下一跳,所以网关中的软件都不能轻易的被划分成输入和输出部分。这样,在IP处理传入数据报时可能会会产生输出数据。而当发送来的数据报引起错误时,网关还必须生成ICMP差错信息。者更进一步的使输入和输出的界限模糊。在下面的讨论中,讲把重点放在网关上,而把主机作为特殊情况对待。
IP软件设计思想
为保持IP软件的简单性和统一性,Xini中采用的组织结构上的技巧有三种:
- 统一的输入队列以及统一的选路过程:
- IP过程对必须由它做处理的所有数据报采用相同的输入队列形式,与这些数据报是来自于网络的过程还是由本机产生的无关。
- IP不考虑数据报的来源,只是从队列中取出数据报并为它们选择路由。
- 一个统一的输入结构使事情变得简单:IP不需要在程序代码中对本地生成的数据报个别对待。另外,由于IP使用单一的路由算法来为所有的数据报选择路由,这样更便于人们理解一个数据报所采纳的路由
- 独立的IP进程:
- IP软件作为一个单一的、自包含的进程执行。
- 为IP单独创建一个进程使软件容易理解和更改。
- 它使编写出的IP软件能够不依赖硬件中断或者是应用程序调用的那些过程
- 本地主机接口:
- 为避免将发往本机的情况作为特例对待,在我们的实现策略中,为本地传送创建了一个伪网络接口;本地接口具有与其他网络接口相同的结构,但它所对应的是本地协议软件,而不是一个物理结构
- IP算法为每个数据报选择路由,并将其传递给某个网络接口,包括以本机为目标的数据报。
- 当一个常规网络接口收到一份数据报时,它将该数据报发往某个物理网络。
- 当本机接口收到一份数据报时,它利用PROTO字段来决定应该当本机上的哪个协议软件模块来接收此数据报。
- 这样,IP认为所有选择路由的过程是统一而且对称的:它从任一接口接收一份数据报,并为其选择路由,然后发送到另一个接口,而不必为本机产生的(或者发往本机的)数据报做特殊处理
虽然建立网关的必要性引发了很多设计思想,但针对网关的设计在主机上同样有效,并允许在主机和网关上使用同样的程序。显然,同一的路由算法与本机接口的集合,将消除程序代码中对一些特殊情况的特殊处理。更重要的是,由于本机是选路表中可用表项来控制的有效目的站,因而就有可能为其增加访问保护措施,以允许管理员实施某些交付原则。比如,管理员能够轻易地做到允许或者不允许指定设备上的两个应用程序之间相互交换信息,如同允许或不允许不同设备上的应用程序之间的通信一样
IP软件结构和数据报流程
这里描述了理论上的组织结构,并给出了输入输出的数据流。(概况的说就是:IP由一个单一的进程和一组网络接口队列组成,数据报必须经过这些队列发送给进程。IP不断的从其中的一个队列中提取数据报,利用选路表为该数据报选择下一跳,并将数据报送往适当的网络输出进程进行发送)
本节我们将展开描述并补充细节
选择传入数据报的策略
这里提到,每个网络接口,包括伪网络接口,都有属于自己的、送往IP进程的数据报队列。如下:
如果有多个数据报正在输入队列中等待,IP进程必须选取其中之一,并为其选择路由。IP选取哪一个数据报将决定系统的行为:
挑选数据报并为其选择路由的IP程序段实现了一个重要策略:它决定了数据报来源的相对优先报
比如说,如果IP总是最先选择伪网络接口队列,那么它就给予由本机生成的外发数据报最高优先级。如果IP仅当其他所有队列都空时,才选择伪网络队列,那么通过网络发送过来的数据报就拥有高优先级,而本地生成的数据报的优先级最低。
显然,这两种极端都是不可取的。
- 一方面,给外来的数据报赋予高优先级将意味着本地软件可能在等待IP为数据报选择路由时,被阻塞任意长的时间。对某个与工作繁忙的网络相连接的网关来说,这种延迟会使本地应用程序,包括网络管理应用无法进行通信。
- 另一方面,给本地生成的数据报以高优先级将导致在本机上运行的任何应用程序都要优先于来自网络的IP数据流。如果因错误而导致一个本地应用不断的向外发送数据报,这些输出分组将组织外来数据报到达网络管理软件。因此,管理员也就无法使用网络管理工作来排除工作
正确的做法是公平分配优先权,使得传入和外发数据在选择路由时享有平等的优先权。我们的实现策略是以循环法来处理数据报,以期达到公平合理。也就是说,它从某个队列中选择处一个数据报,并为其做路由处理之后,继续向前移动并检查下一个队列。如果有K个含有数据报的队列正在等待着路由处理,那么IP在处理完所有K个队列众的第一个数据报之后,才有可能处理到任意队列中的第二个数据报。如下
/* ipgetp.c - ipgetp */
#include <conf.h>
#include <kernel.h>
#include <network.h>
#include <proc.h>
static int ifnext = NI_LOCAL;
/*------------------------------------------------------------------------
* ipgetp -- choose next IP input queue and extract a packet
*------------------------------------------------------------------------
*/
struct ep * ipgetp(int *pifnum)
{
struct ep *pep;
int i;
recvclr(); /* make sure no old messages are waiting */
while (TRUE) {
for (i=0; i < Net.nif; ++i, ++ifnext) { //Net.nif是网络接口注册
if (ifnext >= Net.nif)
ifnext = 0;
if (nif[ifnext].ni_state == NIS_DOWN)
continue;
if (pep = NIGET(ifnext)) {
*pifnum = ifnext;
return pep;
}
}
ifnext = receive();
}
/* can't reach here */
}
如上所示,静态变量ifnext 用作接口数组的索引。它遍历整个网络接口结构。在每个接口中,它检测状态变量ni_state,以确定接口是否已经被打开。当过程ipgetp发现一个打开的接口中有正在等待的数据报时,它就使用宏NIGET来提取并返回第一个数据报。再次调用ipgetp时,将跨过前一次处理的接口,继续查找
允许IP进程被阻塞
过程ipgetp包含了一种微妙的优化思想:
如果所有输入队列都空,IP进程在调用ipgetp时被阻塞。一旦由一个数据报到达,IP进程就恢复执行并理解检测已有数据报到达的那个接口
要理解这个优化思想,有必要先了解两个事实。首先,当与某个特定接口相关联的设备驱动程序在它的输入队列中放入一个数据报时,就像IP进程发送一个报文。其次,ipgetp中的循环语句可以调用receive结束。在ipgetp遍历了所有网络接口而没有发现任何数据报之后,它调用receive,receive在信息到达之前为阻塞状态。当receive返回时,它以函数值的形式向主调过程传递一个报文。报文中包含了一个指针,执行已有数据报到达的接口。ipgetp将该接口指针复制给ifnext,并重新开始遍历。
既然已经知道了IP所使用的数据报的策略,那么来查看一下IP进程的结构。其基本算法非常简单。IP不断地调用ipgetp来选择一个数据报,接着调用一个过程来计算下一跳的地址,并将数据报置入与将要发送该数据报的网络接口相关联的队列中。
尽管从概念上来很简单,但是很多细节使得代码复杂化。比如:
- 如果数据报来自于某个网络,IP必须验证此数据报的校验和是否正确。
- 如果选路表中没有指定的目的站,IP必须生成一个ICMP"目的站不可达"报文。
- 如果选路表中指出该数据报应当被送往产生这个数据报的网络中的某个目的站,IP必须生成一个ICMP"重定向"报文
- 最后,IP必须处理定向广播这一特殊情况,此时IP向指定的网络发送数据报的副本,并向网关自身的上层协议软件也发送一份副本。
IP进程从过程ipproc的执行开始
/* ipproc.c - ipproc */
#include <conf.h>
#include <kernel.h>
#include <network.h>
struct ep *ipgetp(int *);
struct route *rtget(IPaddr, Bool);
/*------------------------------------------------------------------------
* ipproc - handle an IP datagram coming in from the network
*------------------------------------------------------------------------
*/
PROCESS ipproc(void)
{
struct ep *pep;
struct ip *pip;
struct route *prt;
Bool nonlocal;
int ifnum;
ippid = getpid(); /* so others can find us */
signal(Net.sema); /* signal initialization done */
while (TRUE) { // 处理一个数据报
pep = ipgetp(&ifnum); // 调用ipgetp来选取一个数据报,并将ifnum设置为于该数据报对应的接口索引
pip = (struct ip *)pep->ep_data;
if ((pip->ip_verlen>>4) != IP_VERSION) { // 检测数据报的版本
IpInHdrErrors++;
freebuf(pep);
continue;
}
if (IP_CLASSE(pip->ip_dst)) { // 是不是E类地址
IpInAddrErrors++;
freebuf(pep);
continue;
}
if (ifnum != NI_LOCAL) { // 此数据报不是本机产生的,就需要检测数据报是否正确
if (cksum((WORD *)pip, IP_HLEN(pip))) { // ipproc调用cksum来验证校验和
IpInHdrErrors++;
freebuf(pep);
continue;
}
ipnet2h(pip); //在接收一个数组包后,主机将这些整形值从网络字节序转换成本机字节序
pep->ep_order |= EPO_IP;
}
prt = rtget(pip->ip_dst, (ifnum == NI_LOCAL)); // 调用过程rtget为该数据报选择路由。
if (prt == NULL) { // 如果路由不存在,ipproc将调用过程icmp,生成并发送一个ICMP"目的站不可达"报文
if (gateway) {
iph2net(pip); // 在发送一个数据报之前,主机必须把所有整数值从本机字节序转为网络字节序
pep->ep_order &= ~EPO_IP;
icmp(ICT_DESTUR, ICC_NETUR,
pip->ip_src, pep, 0);
} else {
IpOutNoRoutes++;
freebuf(pep);
}
continue;
}
nonlocal = ifnum != NI_LOCAL && prt->rt_ifnum != NI_LOCAL;
if (!gateway && nonlocal) {
IpInAddrErrors++;
freebuf(pep);
rtfree(prt);
continue;
}
if (nonlocal)
IpForwDatagrams++;
/* fill in src IP, if we're the sender */
if (ifnum == NI_LOCAL) {
if (pip->ip_src == ip_anyaddr)
if (prt->rt_ifnum == NI_LOCAL)
pip->ip_src = pip->ip_dst;
else
pip->ip_src =
nif[prt->rt_ifnum].ni_ip;
} else if (--(pip->ip_ttl) == 0 &&
prt->rt_ifnum != NI_LOCAL) { // 一旦选路完成,ipproc递减寿命计数器(ip_ttl)、如果寿命到达0,ipproc生成一个ICMP超时报文
IpInHdrErrors++;
iph2net(pip); // 在发送一个数据报之前,主机必须把所有整数值从本机字节序转为网络字节序
pep->ep_order &= ~EPO_IP;
icmp(ICT_TIMEX, ICC_TIMEX, pip->ip_src, pep, 0);
rtfree(prt);
continue;
}
ipdbc(ifnum, pep, prt); /* 处理定向广播 */
ipredirect(pep, ifnum, prt); /* do redirect, if needed */
if (prt->rt_metric != 0) // 检测路由度量值,决定该数据报应当被直接发往自己的目的站,还是发送到下一跳。
ipputp(prt->rt_ifnum, prt->rt_gw, pep); // 发送到下一跳: 调用ipputp将数据报插入到某个输出队列中
else
ipputp(prt->rt_ifnum, pip->ip_dst, pep); // 直接发往自己的目的站: 调用ipputp将数据报插入到某个输出队列中
rtfree(prt);
}
}
int ippid, gateway, bsdbrc;
- ipproc先将其进程IP保存在全局变量ippid中,并发出网络初始化信号,然后进入一个无限循环
- 在每一次循环执行时,ipproc处理一个数据报。它调用ipgetp来选取一个数据报,并将ifnum设置为于该数据报对应的接口索引
- 在检测了数据报的版本并验证其中没有E类地址之后,ipproc调用cksum来验证校验和(除非此数据报是本机产生的)
- 一旦ipproc得到的是一个有效的数据报,它调用过程rtget为该数据报选择路由。rtget计算一个路由并返回执行模式该路由结构的指针。如果路由不存在,ipproc将调用过程icmp,生成并发送一个ICMP"目的站不可达"报文
- ipproc必须为本地生成的数据报填写正确的源地址。为做到这一点,它首先检测数据报,确认上层协议是否已为它指定了一个固定的源地址。如果没有,ipproc填写源地址。根据标准规定,ipproc将数据报的源地址设置为发送该数据报的网络接口的IP地址。如果路由执行本机接口(比如,该数据报来自本机,选择路由后又回到本机),ipproc将数据报的目的地址复制到源地址字段中
- 一旦选路完成,ipproc递减寿命计数器(ip_ttl)、如果寿命到达0,ipproc生成一个ICMP超时报文
- ipproc调用过程ipdbc来处理定向广播:ipdbc复制到达本机的定向广播数据报,并向本地软件发送一个副本。ipproc将副本原件发往指定网络
- 必要时ipproc还要调用过程ipredirect生成IMCP“重定向”报文:
- 为了决定是否需要重新发送这个报文,ipproc比较两个网络接口,一个是将数据报发送来的网络接口,另一个是数据报在选择路由后将要发往的接口,如果他们是相同的,就需要重定向。
- ipredirect检测网络的子网掩码,决定它应该发送一个网络重定向报文还是一个主机重定向报文
- 最后,ipproc检测路由度量值,决定该数据报应当被直接发往自己的目的站,还是发送到下一跳。
- 度量值为0的路由表示网关可以直接将数据报发送到它的目的站,其他任何大于0的值表示网关应当将数据报发往下一跳。
- 不管是选择了下一跳地址,还是选择了数据报的目的地址,ipproc调用ipputp将数据报插入到某个输出队列中
IP使用的常量定义
在文件ip.h中定义了IP软件使用的符号常量。另外,它还用结构IP定义了IP数据报的格式:
/* ip.h - IP_HLEN, IP_CLASS{A,B,C,D,E} */
/* Internet Protocol (IP) Constants and Datagram Format */
#define IP_ALEN 4 /* IP address length in bytes (octets) */
typedef unsigned long IPaddr; /* internet address */
#if BYTE_ORDER == BIG_ENDIAN
#define IP_CLASSA(x) (((x) & 0x80000000) == 0) /* IP Class A */
#define IP_CLASSB(x) (((x) & 0xc0000000) == 0x80000000) /* IP Class B */
#define IP_CLASSC(x) (((x) & 0xe0000000) == 0xc0000000) /* IP Class C */
#define IP_CLASSD(x) (((x) & 0xf0000000) == 0xe0000000) /* IP Class D */
#define IP_CLASSE(x) (((x) & 0xf8000000) == 0xf0000000) /* IP Class E */
#else /* BYTE_ORDER */
#define IP_CLASSA(x) (((x) & 0x80) == 0x00) /* IP Class A address */
#define IP_CLASSB(x) (((x) & 0xc0) == 0x80) /* IP Class B address */
#define IP_CLASSC(x) (((x) & 0xe0) == 0xc0) /* IP Class C address */
#define IP_CLASSD(x) (((x) & 0xf0) == 0xe0) /* IP Class D address */
#define IP_CLASSE(x) (((x) & 0xf8) == 0xf0) /* IP Class E address */
#endif /* BYTE_ORDER */
/* Some Assigned Protocol Numbers */
#define IPT_ICMP 1 /* protocol type for ICMP packets */
#define IPT_IGMP 2 /* protocol type for IGMP packets */
#define IPT_TCP 6 /* protocol type for TCP packets */
#define IPT_EGP 8 /* protocol type for EGP packets */
#define IPT_UDP 17 /* protocol type for UDP packets */
#define IPT_OSPF 89 /* protocol type for OSPF packets */
struct ip {
u_char ip_verlen; /* IP version & header length (in longs)*/
u_char ip_tos; /* type of service */
u_short ip_len; /* total packet length (in octets) */
short ip_id; /* datagram id */
short ip_fragoff; /* fragment offset (in 8-octet's) */
u_char ip_ttl; /* time to live, in gateway hops */
u_char ip_proto; /* IP protocol (see IPT_* above) */
short ip_cksum; /* header checksum */
IPaddr ip_src; /* IP address of source */
IPaddr ip_dst; /* IP address of destination */
u_char ip_data[1]; /* variable length data */
};
#define IP_VERSION 4 /* current version value */
#define IP_MINHLEN 5 /* minimum IP header length (in longs) */
#define IP_TTL 255 /* Initial time-to-live value */
#define IP_MF 0x2000 /* more fragments bit */
#define IP_DF 0x4000 /* don't fragment bit */
#define IP_FRAGOFF 0x1fff /* fragment offset mask */
#define IP_PREC 0xe0 /* precedence portion of TOS */
/* IP Precedence values */
#define IPP_NETCTL 0xe0 /* network control */
#define IPP_INCTL 0xc0 /* internet control */
#define IPP_CRIT 0xa0 /* critical */
#define IPP_FLASHO 0x80 /* flash over-ride */
#define IPP_FLASH 0x60 /* flash */
#define IPP_IMMED 0x40 /* immediate */
#define IPP_PRIO 0x20 /* priority */
#define IPP_NORMAL 0x00 /* normal */
/* macro to compute a datagram's header length (in bytes) */
#define IP_HLEN(pip) ((pip->ip_verlen & 0xf)<<2)
#define IPMHLEN 20 /* minimum IP header length (in bytes) */
/* IP options */
#define IPO_COPY 0x80 /* copy on fragment mask */
#define IPO_CLASS 0x60 /* option class */
#define IPO_NUM 0x17 /* option number */
#define IPO_EOOP 0x00 /* end of options */
#define IPO_NOP 0x01 /* no operation */
#define IPO_SEC 0x82 /* DoD security/compartmentalization */
#define IPO_LSRCRT 0x83 /* loose source routing */
#define IPO_SSRCRT 0x89 /* strict source routing */
#define IPO_RECRT 0x07 /* record route */
#define IPO_STRID 0x88 /* stream ID */
#define IPO_TIME 0x44 /* internet timestamp */
#define IP_MAXLEN BPMAXB-EP_HLEN /* Maximum IP datagram length */
/* IP process info */
extern PROCESS ipproc();
#define IPSTK 512 /* stack size for IP process */
#define IPPRI 100 /* IP runs at high priority */
#define IPNAM "ip" /* name of IP process */
#define IPARGC 0 /* count of args to IP */
extern IPaddr ip_maskall; /* = 255.255.255.255 */
extern IPaddr ip_anyaddr; /* = 0.0.0.0 */
extern IPaddr ip_loopback; /* = 127.0.0.1 */
extern int ippid, gateway;
struct ip *iph2net(struct ip *), *ipnet2h(struct ip *);
unsigned short cksum(WORD *, unsigned);
int ipsend(IPaddr, struct ep *, unsigned, u_char, u_char, u_char);
int ipputp(unsigned, IPaddr, struct ep *);
Bool isbrc(IPaddr);
校验和的计算
ipproc利用过程cksum来计算或验证数据报的首部校验和。首部校验和将首部视为16位整数的序列,并把校验和定义为对首部中所有16为整数各求反码,并将结果相加,再对求得的和计算一次二进制反码。得到的和数以及反码定义为二进制反码算法
大多数设备使用二进制补码算法,因此仅累积得出一个16位的校验和并不能得到正确的结果。为了便于移植并避免以汇编语言编写程序,过程cksum使用了C语言编码。如下:
/*------------------------------------------------------------------------
* cksum - Return 16-bit ones complement of 16-bit ones complement sum
*------------------------------------------------------------------------
*/
short cksum(unsigned short *buf,int nwords)
{
unsigned long sum;
for (sum=0; nwords>0; nwords--)
sum += *buf++;
sum = (sum >> 16) + (sum & 0xffff); /* add in carry */
sum += (sum >> 16); /* maybe one more */
return ~sum;
}
处理定向广播
只要一个数据报发送到定向广播地址,则在指定目的网络中的所有机器(包括网关和主机)都必须接收到一份副本。
然而,大多数网络硬件不会将广播分组的副本再次传送会发出这个广播的机器。如果一个网关需要一份广播数据报的副本,那么软件必须采用明确的措施,以保留一份副本。因此,如果一个网关接收到的数据报的目的地址是直接与这个网关相连的某个网络的定向广播地址,那么这个网关必须做到两件事:
- 为本机的协议软件复制一份数据报
- 在指定的网络上广播该数据报
如下:
/* ipdbc.c - ipdbc */
#include <conf.h>
#include <kernel.h>
#include <network.h>
struct route *rtget(IPaddr, Bool);
/*------------------------------------------------------------------------
* ipdbc - handle IP directed broadcast copying
*------------------------------------------------------------------------
*/
void ipdbc(unsigned ifnum, struct ep *pep, struct route *prt)
{
struct ip *pip = (struct ip *)pep->ep_data;
struct ep *pep2;
struct route *prt2;
int len;
if (prt->rt_ifnum != NI_LOCAL)
return; /* not ours */
if (!isbrc(pip->ip_dst))
return; /* not broadcast */
prt2 = rtget(pip->ip_dst, RTF_LOCAL);
if (prt2 == NULL)
return;
if (prt2->rt_ifnum == ifnum) { /* not directed */
rtfree(prt2);
return;
}
/* directed broadcast; make a copy */
/* len = ether header + IP packet */
len = EP_HLEN + pip->ip_len;
if (len > EP_MAXLEN)
pep2 = (struct ep *)getbuf(Net.lrgpool);
else
pep2 = (struct ep *)getbuf(Net.netpool);
if (pep2 == (struct ep *)SYSERR) {
rtfree(prt2);
return;
}
memcpy(pep2, pep, len);
/* send a copy to the net */
ipputp(prt2->rt_ifnum, pip->ip_dst, pep2);
rtfree(prt2);
return; /* continue; "pep" goes locally in IP */
}
ipproc为所有数据报调用ipdbc,其中绝大多数并没有指定要定向广播。
- 首先,ipdbc检查数据报来源,因为如果该数据报是由本机机器产生的,则不需要复制。
- 然后,ipdbc调用isbrc,将数据报的目的地址与这个网关直接相关联的所有网络的定向广播地址相比较,如果均不相同,则说明这是一个非广播数据报,而非广播数据报也不需要复制
在不需要复制的情况下,ipdbc不采取任何行动,直接返回。ipproc将像平常一样的选择一个路由并继续向其发送该数据报
如果数据报的目的地址是某个直接与网关相连的网络上的定向广播地址,它必须被复制。其中一份副本送往本地机器软件,另一份以正常方式继续转发。为了复制数据报,ipdbc根据数据报的大小从标准网络缓冲池或大缓冲池中分配一个缓冲区。如果分配成功就将数据报复制到新缓冲区中,并把新缓冲区放入输出端口。此端口与发送数据报时经过的网络接口相关联。当ipdbc返回后,ipproc将副本原件通过伪网络接口传递给本地机器
识别一个广播地址
IP协议标准规定了三种类型的广播地址:本地网络广播地址(“全1”)、定向网络广播地址(主机地址段为全"1"的A、B、C三级IP地址)以及一个子网广播地址(主机地址段为段"1"的子网IP地址)。注意Berkeley将TCP/IP合并入BSD Unix中使用的是非标准广播地址,也叫做Berkeley广播,这些广播的格式在应该是全1的地方使用了全0来表示
因此,下面范例中接收使用权“0”或者全“1”两种格式的广播:
/* isbrc.c - isbrc */
#include <conf.h>
#include <kernel.h>
#include <sleep.h>
#include <network.h>
/*------------------------------------------------------------------------
* isbrc - Is "dest" a broadcast address?
*------------------------------------------------------------------------
*/
Bool isbrc(IPaddr dest)
{
int inum;
/* all 0's and all 1's are broadcast */
if (dest == ip_anyaddr || dest == ip_maskall)
return TRUE;
/* check real broadcast address and BSD-style for net & subnet */
for (inum=0; inum < Net.nif; ++inum)
if (dest == nif[inum].ni_brc ||
dest == nif[inum].ni_nbrc ||
dest == nif[inum].ni_subnet ||
dest == nif[inum].ni_net)
return TRUE;
return FALSE;
}
IP首部中的字节顺序
为了使网际协议独立于运行它的机器,协议标准为首部中所有整数型规定了网络字节序
在发送一个数据报之前,主机必须把所有整数值从本机字节序转为网络字节序。在接收一个数组包后,主机将这些整形值从网络字节序转换成本机字节序
过程iph2net和ipnet2h执行这一任务
/* iph2net.c - iph2net */
#include <conf.h>
#include <kernel.h>
#include <network.h>
/*------------------------------------------------------------------------
* iph2net - convert an IP packet header from host to net byte order
*------------------------------------------------------------------------
*/
struct ip * iph2net(struct ip *pip)
{
/* NOTE: does not include IP options */
pip->ip_len = hs2net(pip->ip_len); // 从主机短整型(16位整数)到网络字节序
pip->ip_id = hs2net(pip->ip_id); // 从主机短整型(16位整数)到网络字节序
pip->ip_fragoff = hs2net(pip->ip_fragoff); // 从主机短整型(16位整数)到网络字节序
return pip;
}
/* ipnet2h.c - ipnet2h */
#include <conf.h>
#include <kernel.h>
#include <network.h>
/*------------------------------------------------------------------------
* ipnet2h - convert an IP packet header from net to host byte order
*------------------------------------------------------------------------
*/
struct ip * ipnet2h(struct ip *pip)
{
/* NOTE: does not include IP options */
pip->ip_len = net2hs(pip->ip_len);
pip->ip_id = net2hs(pip->ip_id);
pip->ip_fragoff = net2hs(pip->ip_fragoff);
return pip;
}
向IP发送数据报
发送本地生成的数据报
给出一个本地生成的数据报和一个目的IP地址,过程ipsend填写IP首部并将数据报放入本机接口的队列中,IP进程将从这个队列中取出并发送数据报
/* ipsend.c - ipsend */
#include <conf.h>
#include <kernel.h>
#include <network.h>
#include <proc.h>
static ipackid = 1;
/*------------------------------------------------------------------------
* ipsend - send an IP datagram to the specified address
*------------------------------------------------------------------------
*/
int ipsend(IPaddr faddr, struct ep *pep, unsigned datalen,
u_char proto, u_char ptos, u_char ttl)
{
struct ip *pip = (struct ip *) pep->ep_data;
pep->ep_type = EPT_IP;
pep->ep_order |= EPO_IP|EPO_NET;
pip->ip_verlen = (IP_VERSION<<4) | IP_MINHLEN;
pip->ip_tos = ptos;
pip->ip_len = datalen+IP_HLEN(pip);
pip->ip_id = ipackid++;
pip->ip_fragoff = 0;
pip->ip_ttl = ttl;
pip->ip_proto = proto;
pip->ip_dst = faddr;
/*
* special case for ICMP, so source matches destination
* on multi-homed hosts.
*/
if (pip->ip_proto != IPT_ICMP)
pip->ip_src = ip_anyaddr;
if (enq(nif[NI_LOCAL].ni_ipinq, pep, 0) < 0) {
freebuf(pep);
IpOutDiscards++;
}
send(ippid, NI_LOCAL);
IpOutRequests++;
return OK;
}
/* special IP addresses */
IPaddr ip_anyaddr = 0;
#if BYTE_ORDER == LITTLE_ENDIAN
IPaddr ip_loopback = 0x0100007F; /* 127.0.0.1 */
#else /* BYTE_ORDER */
IPaddr ip_loopback = 0x7F000001; /* 127.0.0.1 */
#endif /* BYTE_ORDER */
主调进程利用参数指定IP首部中的某些值。参数proto中的值用于指定协议类型,ptos中的值用于指定优先级和服务类型,而参数ttl用于寿命字段。
ipsend填写首部中的每个字段,包括指定的目的地址。为了保证每个外发数据报中标识字段都有唯一的值,ipsend将全局变量ipackid赋予该标识字段,然后递增变量ipackid 。ipsend完成填写首部的任务后,调用enq把数据报放置到本地主机(伪网络)接口的队列中
注意,虽然网络接口ni_ipinq队列在正常情况下包含的是传入数据报(也就是来自其他网点的数据报),但从应用软件的角度来看,伪网络接口的队列中包含的是“外发”的数据报。最后,在IP进程因等待数据报的到达而处于阻塞状态的情况下,ipsend调用send向IP进程发送一个报文
发送传入数据报
当一个经由网络发送来的数据报到达时,网络接口层的设备驱动程序必须将其放置在适当的IP队列中。它调用ip_in完成这项工作。
/* ip_in.c - ip_in */
#include <conf.h>
#include <kernel.h>
#include <network.h>
#include <proc.h>
/*------------------------------------------------------------------------
* ip_in - IP input function
*------------------------------------------------------------------------
*/
int ip_in(struct netif *pni, struct ep *pep)
{
struct ip *pip = (struct ip *)pep->ep_data;
IpInReceives++;
if (enq(pni->ni_ipinq, pep, pip->ip_tos & IP_PREC) < 0) {
IpInDiscards++;
freebuf(pep);
}
send(ippid, (pni-&nif[0]));
return OK;
}
给出一个指向分组缓冲区的指针,ip_in调用enq将分组放置到接口的队列中。如果队列已满,ip_in递增变量IpInDiscards的值,以记录下这个队列的溢出差错,并丢弃分组。最后,在IP进程被阻塞并等待接收数据报的情况下,ip_in向IP进程发送一个报文