U-Boot 中 PING 命令处理流程---本文转载,地址忘了哈
这里打算从 U-Boot 的 ping 命令说起。ping 命令是用于测试网络是否和目标网络畅通简单工
具,在 U-Boot 中 ping 命令的使用方法是:
ping
比如 ping 192.168.1.100,如果调试的板子和目标 IP 之间的通信畅通的话,将输出如下信
息:
host 192.168.1.100 is alive
否则显示:
ping failed; host 192.168.1.100 is not alive
好,现在知道了现象我们来看背后的本质 (使用的 U-Boot 的版本号为 u-boot-2010.06-rc2)。
U-Boot 中加入 ping 命令的代码如下:
U_BOOT_CMD(
ping, 2, 1, do_ping,
"send ICMP ECHO_REQUEST to network host",
"pingAddress"
);
凭直觉我们可以知道这个命令的具体执行函数应该是 do_ping 函数,后面双引号里面的内容
应该是 ping 命令的帮助说明。好了,跟着感觉走,去看看 do_ping 函数。
do_ping 函数短小精悍,全部代码不过 20 来行:
int do_ping (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
if (argc < 2)
return -1;
NetPingIP = string_to_ip(argv[1]);
if (NetPingIP == 0) {
cmd_usage(cmdtp);
return -1;
}
if (NetLoop(PING) < 0) {
printf("ping failed; host %s is not alive\n", argv[1]);
return 1;
}
printf("host %s is alive\n", argv[1]);
return 0;
}
这个函数的第三、第四个函数我们应该很熟悉,没错,就是和 main 函数一样的参数。可以知
道这个命令至少要两个参数,否则直接返回。然后看看和我们输出相关的,即最终判定网络
是否通畅的地方:两个 printf 函数。从代码里面可以知道,若 NetLoop 函数返回值小于 0,
则网络不通;否则网络通畅。
先看看在调用 NetLoop 函数之前,这里做了一点点工作:
NetPingIP = string_to_ip(argv[1]);
我们在使用 ping 命令的时候的格式如下:
ping 192.168.1.100
因此这里的 argv[1]就是我们的 IP 地址的字符串格式了。再看看 NetPingIP 是神马?它定义
在 net/net.c 文件中:
IPaddr_t NetPingIP;
说它是一个 IPaddr_t 类型的变量。我靠,这个 IPaddr_t 又是神马玩意?继续跟踪,到了
include/net.h 文件中,它是这样定义的:
typedef ulong IPaddr_t;
现在终于知道了,TNND 这个 NetPingIP 其实就是一个 unsigned long 类型的全局变量。
根据经验,在目标平台上这个 NetPingIP 占用 32 位,实际上就是一个 IP 地址的大小,在 TCP/IP
协议的 IP 数据报格式当中有定义。
那么这里转换出来肯定是在后面要使用它了,具体怎么转换的就不要看了,字符串处理说复
杂不复杂,但要是你敢说简单的话,小心砖头砸死你。我们这里就把转换出来的结果说一下。
我们在 pc 机上使用 ping 命令时一般会有如下输出:
[yqliu29@debian u-boot-2010.06-dian]$ ping www.baidu.com -c 5
PING www.a.shifen.com (119.75.218.45) 56(84) bytes of data.
...........................................
后面的结果就不看了,我们看到一个 IP 地址:119.75.218.45。同样是凭直觉,我们可以知
道这个 IP 地址应该是百度的一个地址。我们知道我们的 IP 地址是点分十进制格式的。这是
什么意思呢?比如百度的这个 IP 地址:119.75.218.45,他的意思就是把一个 32 位的数分成
4 个字节,每个字节八位,用三个点分开的四个数分别占据这四个字节当中的一个。因此百
度的这个 IP 地址实际上可以转换成下面的格式:
119---0x77
75----0x4b
218---0xda
45----0x2d
那么这个整数就是 0x774bda2d,转换为十进制就是 2001459757。我们在浏览器的地址栏里面
输入:
http://2001459757
也能打开百度搜索网页。
说到这里,其实就说明了一个问题:
NetPingIP = string_to_ip(argv[1]);
这行代码把我们输入的 IP 地址解析成了后面代码需要使用的 IP 地址,现在记住这一条就行
了。
解析了 IP 地址,后面就是调用具体的工作函数 NetLoop 了。调用的时候给了一个参数:PING。
我们来看看这个 PING 是神马玩意,在 include/net.h 文件中:
typedef enum { BOOTP, RARP, ARP, TFTP, DHCP, PING, DNS, NFS, CDP, NETCONS, SNTP }
proto_t;
原来他是一个枚举型的变量,我们可以凭此猜测这个 NetLoop 函数还可以接受这个枚举定义
的其他参数。
准备工作就做到这里,下面接着看 NetLoop 函数。这个函数比较庞大,在 net/net.c 文件中,
我们分成若干部分来看:
int
NetLoop(proto_t protocol)
{
bd_t *bd = gd->bd;
#ifdef CONFIG_NET_MULTI
NetRestarted = 0;
NetDevExists = 0;
#endif
/* XXX problem with bss workaround */
NetArpWaitPacketMAC = NULL;
NetArpWaitTxPacket = NULL;
NetArpWaitPacketIP = 0;
NetArpWaitReplyIP = 0;
NetArpWaitTxPacket = NULL;
NetTxPacket = NULL;
NetTryCount = 1;
这里先不看这些定义变量的具体定义,等到用到的时候在回头过来看,这里我们知道这些变
量被初始化就 OK 了。接着看代码:
if (!NetTxPacket) {
int i;
/*
* Setup packet buffers, aligned correctly.
*/
NetTxPacket = &PktBuf[0] + (PKTALIGN - 1);
NetTxPacket -= (ulong)NetTxPacket % PKTALIGN;
for (i = 0; i < PKTBUFSRX; i++) {
NetRxPackets[i] = NetTxPacket + (i+1)*PKTSIZE_ALIGN;
}
}
这段代码看起来有点矛盾,因为前面已经把 NetTxPacket 设置为了 NULL,这里还进行一个判
断,好像有点多余?先不管,我们就知道这个条件有的代码会执行就行了。看看这个里面干
了什么:
NetTxPacket = &PktBuf[0] + (PKTALIGN - 1);
PktBuf 是一个字符数组,定义如下:
volatile uchar PktBuf[(PKTBUFSRX+1) * PKTSIZE_ALIGN + PKTALIGN];
这上面的三个宏定义分别如下:
# define PKTBUFSRX 4
#define PKTSIZE_ALIGN 1536
#define PKTALIGN 32
所以这里定义的是一个大概 5 个 1536 字节大的数组。先不管这个,看看后面做了什么。
NetTxPacket -= (ulong)NetTxPacket % PKTALIGN;
NetTxPacket 先是获得了一个地址,然后把这个地址减去一个值,减去的这个值是地址本身
和 32 的余数,那么实际上就是把 NetTxPacket 进行 32 字节对齐了。
接着:
for (i = 0; i < PKTBUFSRX; i++) {
NetRxPackets[i] = NetTxPacket + (i+1)*PKTSIZE_ALIGN;
}
实际上就是把 NetRxPackets 数组进行赋值,这个数组定义如下:
volatile uchar *NetRxPackets[PKTBUFSRX];
说明它也是指向字符串的数组,可以知道这个数组都被赋予了 PktBuf 当中的某个地址,并且
这些地址都是 32 位对其的。为神马要对齐?现在我也不知道。
if (!NetArpWaitTxPacket) {
NetArpWaitTxPacket = &NetArpWaitPacketBuf[0] + (PKTALIGN - 1);
NetArpWaitTxPacket -= (ulong)NetArpWaitTxPacket % PKTALIGN;
NetArpWaitTxPacketSize = 0;
}
同样,这里这个判断貌似也是多余,因为前面已经直接把 NetArpWaitTxPacket 设置为了 NULL,
这个后面的代码肯定会执行的。还是直接看后面的代码:
NetArpWaitTxPacket = &NetArpWaitPacketBuf[0] + (PKTALIGN - 1);
先给它一个值,这个值就是数组当中的一个地址,数组定义如下:
uchar NetArpWaitPacketBuf[PKTSIZE_ALIGN + PKTALIGN];
然后和上面的一样,进行一个地址对齐,32 位:
NetArpWaitTxPacket -= (ulong)NetArpWaitTxPacket % PKTALIGN;
到这里,一些 house keeping 的代码就完成了,然后是一些和硬件相关的:
eth_halt();
#ifdef CONFIG_NET_MULTI
eth_set_current();
#endif
if (eth_init(bd) < 0) {
eth_halt();
return(-1);
}
首先是 eth_halt()函数,定义在 net/eth.c 文件中:
void eth_halt(void)
{
if (!eth_current)
return;
eth_current->halt(eth_current);
eth_current->state = ETH_STATE_PASSIVE;
}
实际上它只是调用了具体网卡驱动的 halt 函数,然后把网卡状态设置为 passive。由于网卡
驱动的 halt 函数只是进行了网卡的硬件操作,不是我们这里关心的重点,因此这里就不分析
了。
同样,后面的 eth_init 函数也调用的是网卡驱动的 init 函数,对网卡硬件进行一个初始化,
这里也不看具体实现代码了。
接着看下面的代码:
#ifdef CONFIG_NET_MULTI
memcpy (NetOurEther, eth_get_dev()->enetaddr, 6);
#else
eth_getenv_enetaddr("ethaddr", NetOurEther);
#endif
实际上执行的是第一个函数,可以看出我们实际上是把本机的 mac 地址拷贝到了 NetOurEther
中。本机的 mac 地址是在调用 eth_init 函数的时候从网络控制器中读取到当前网络设备的
enetaddr 中的,和具体硬件相关,因此这里也不详细看了。
NetState = NETLOOP_CONTINUE;
/*
* Start the ball rolling with the given start function. From
* here on, this code is a state machine driven by received
* packets and timer events.
*/
NetInitLoop(protocol);
然后设置网络状态,初始化循环。来看看 NetInitLoop 函数:
static void
NetInitLoop(proto_t protocol)
{
static int env_changed_id = 0;
bd_t *bd = gd->bd;
int env_id = get_env_id ();
/* update only when the environment has changed */
if (env_changed_id != env_id) {
NetCopyIP(&NetOurIP, &bd->bi_ip_addr);
NetOurGatewayIP = getenv_IPaddr ("gatewayip");
NetOurSubnetMask= getenv_IPaddr ("netmask");
NetServerIP = getenv_IPaddr ("serverip");
NetOurNativeVLAN = getenv_VLAN("nvlan");
NetOurVLAN = getenv_VLAN("vlan");
#if defined(CONFIG_CMD_DNS)
NetOurDNSIP = getenv_IPaddr("dnsip");
#endif
env_changed_id = env_id;
}
return;
}
实际上就是检查环境变量有没有改变,如果改变了的话,就是用最新的环境变量。
到这里,ping 的准备工作就做完了,在一个 switch 选项当中,执行 PingStart()函数,下面
分析这个函数的具体实现:
static void PingStart(void)
{
#if defined(CONFIG_NET_MULTI)
printf ("Using %s device\n", eth_get_name());
#endif /* CONFIG_NET_MULTI */
NetSetTimeout (10000UL, PingTimeout);
NetSetHandler (PingHandler);
PingSend();
}
先调用 NetSetTimeout 设置超时时间和超时处理函数,该函数如下:
void
NetSetTimeout(ulong iv, thand_f * f)
{
if (iv == 0) {
timeHandler = (thand_f *)0;
} else {
timeHandler = f;
timeStart = get_timer(0);
timeDelta = iv;
}
}
这个函数实际上就是给三个全局变量赋值:
timeHandler = f; 这个东东应该是在后面检测到超时会调用的超时处理函数。
timeStart = get_timer(0); 获取现在的时间,以便后面检测是否超时。
timeDelta = iv; 超时限制时间
所以,这个函数就是为后面的超时处理预先进行一些准备工作。
然后是调用了 NetSetHandler 函数,来看看这个函数:
void
NetSetHandler(rxhand_f * f)
{
packetHandler = f;
}
这个函数就是把一个函数指针进行赋值,这个函数指针定义如下:
static rxhand_f *packetHandler; /* Current RX packet handler */
从定义来看,这个函数指针是用来处理接收包的函数。这里先进行设置,在发出 ping 信息以
后,应该会使用这个指针来进行接收处理。
最后,PingStart()函数调用了 PingSend()函数来发送 ping 数据包。这个函数代码不是很长,
但是也不是很短,我们分开来看:
int PingSend(void)
{
static uchar mac[6];
volatile IP_t *ip;
volatile ushort *s;
uchar *pkt;
这里是一些局部变量定义,在这个函数当中会用到,这里先不管,后面使用到再说。
memcpy(mac, NetEtherNullAddr, 6);
debug("sending ARP for %08lx\n", NetPingIP);
NetArpWaitPacketIP = NetPingIP;
NetArpWaitPacketMAC = mac;
pkt = NetArpWaitTxPacket;
pkt += NetSetEther(pkt, mac, PROT_IP);
ip = (volatile IP_t *)pkt;
首先,对 mac 数组赋值,由于 NetEtherNullAddr 定义如下:
uchar NetEtherNullAddr[6] =
{ 0, 0, 0, 0, 0, 0 };
所以 mac 数组实际上全部被设置为 0.(很奇怪这里为神马不用 memset 来直接设置为 0?)
然后,若有 debug 功能,则打印一条 debug 信息。
NetArpWaitPacketIP = NetPingIP;
这里对 NetArpWaitPacketIP 赋值,这个值应该在后面 ARP 信息中会用到,被赋予的值是待
PING 的目的 IP 地址,NetPingIP 这个值在 do_ping()函数中初始化过,前面已经说过。
NetArpWaitPacketMAC = mac;
设置 ARP 协议的目的 mac 地址,这里是一个指向字符串的指针。
pkt = NetArpWaitTxPacket;
pkt += NetSetEther(pkt, mac, PROT_IP);
先对 pkt 指针赋值,目标地址为 NetArpWaitTxPacket,这个值在 NetLoop()函数的开头的地
方已经进行了处理,可以回头去看看前面的说明。然后调用了 NetSetEther()函数,这个函
数的返回值加上现在的 pkt 地址形成了新的 pkt 地址。如果熟悉 tcp/ip 协议,我们可以猜测
到这里应该会设置 ether 数据头,然后把指针移到 IP 报头处。我们来看看具体的代码:
int
NetSetEther(volatile uchar * xet, uchar * addr, uint prot)
{
Ethernet_t *et = (Ethernet_t *)xet;
ushort myvlanid;
myvlanid = ntohs(NetOurVLAN);
if (myvlanid == (ushort)-1)
myvlanid = VLAN_NONE;
memcpy (et->et_dest, addr, 6);
memcpy (et->et_src, NetOurEther, 6);
if ((myvlanid & VLAN_IDMASK) == VLAN_NONE) {
et->et_protlen = htons(prot);
return ETHER_HDR_SIZE;
} else {
VLAN_Ethernet_t *vet = (VLAN_Ethernet_t *)xet;
vet->vet_vlan_type = htons(PROT_VLAN);
vet->vet_tag = htons((0 << 5) | (myvlanid & VLAN_IDMASK));
vet->vet_type = htons(prot);
return VLAN_ETHER_HDR_SIZE;
}
}
这个函数实际上就是把传入的 addr 赋值给 ether 报头的目的地址的 mac 地址,把自己的 mac
地址也加入 mac 报头,然后是一个两字节长的协议长度。这里实际上就是在组装 ethernet
的数据头,不熟悉 TCP/IP 协议的需要看看 TCP/IP 协议。然后返回 Ethernet 的报头长度,
Ethernet 报头长度为固定长度 14,所以这里就是定义的一个宏进行返回。
ip = (volatile IP_t *)pkt;
/*
* Construct an IP and ICMP header. (need to set no fragment bit - XXX)
*/
ip->ip_hl_v = 0x45; /* IP_HDR_SIZE / 4 (not including UDP) */
ip->ip_tos = 0;
ip->ip_len = htons(IP_HDR_SIZE_NO_UDP + 8);
ip->ip_id = htons(NetIPID++);
ip->ip_off = htons(IP_FLAGS_DFRAG); /* Don't fragment */
ip->ip_ttl = 255;
ip->ip_p = 0x01; /* ICMP */
ip->ip_sum = 0;
NetCopyIP((void*)&ip->ip_src, &NetOurIP); /* already in network byte order */
NetCopyIP((void*)&ip->ip_dst, &NetPingIP); /* - "" - */
ip->ip_sum = ~NetCksum((uchar *)ip, IP_HDR_SIZE_NO_UDP / 2);
刚才已经把 pkt 加上了 Ethernet 报头的偏移量,然后把这个地址给 ip 指针,这里就是要设
置 Ethernet 的上层协议 IP 层的数据了,先看看 IP 报头的格式定义:
/*
* Internet Protocol (IP) header.
*/
typedef struct {
uchar ip_hl_v; /* header length and version */
uchar ip_tos; /* type of service */
ushort ip_len; /* total length |
*/ |
ushort ip_id; /* identification |
*/ |
ushort ip_off; /* fragment offset field */
uchar ip_ttl; /* time to live */
uchar ip_p; /* protocol */
ushort ip_sum; /* checksum */
IPaddr_t ip_src; /* Source IP address */
IPaddr_t ip_dst; /* Destination IP address */
ushort udp_src; /* UDP source port */
ushort udp_dst; /* UDP destination port */
ushort udp_len; /* Length of UDP packet */
ushort udp_xsum; /* Checksum */
} IP_t;
这个数据结构的最后 8 个字节实际上不是 IP 层的协议,U-Boot 中这样做肯能是为了处理数
据方便,因为上层的 UDP 协议很简单,就定义到 IP 数据结构当中一起来了。这个数据结构和
前面的 Ethernet 结构一样,都是 TCP/IP 协议的一部分,不熟悉的需要去看看协议的定义。
然后看代码的实现:
ip->ip_hl_v = 0x45;
根据 TCP/IP 协议的定义,前 4 个字节是协议的版本,IPv4 的话为 4,IPv6 的话为 6;后面 4
字节的是表示协议头的字长,IPv4 这里我们用 5 个字。
ip->ip_tos = 0;
这里设置的是 tpye of service,表示数据报的优先级,一般用 0,具体什么意思去看看协议。
ip->ip_len = htons(IP_HDR_SIZE_NO_UDP + 8);
这里设置的是整个数据报的长度,包括 IP 协议头自身长度和数据区的长度。
ip->ip_id = htons(NetIPID++);
数据报的编号。
ip->ip_off = htons(IP_FLAGS_DFRAG); /* Don't fragment */
分片设置,由于数据报总长度小于一个 Ethernet 数据报的长度,所以不用分片。
ip->ip_ttl = 255;
数据报的寿命,每经过一个路由器该值减一,若为 0 的时候还没到目的地址,则丢弃这个数
据报。
ip->ip_p = 0x01; /* ICMP */
指定上层协议是 ICMP,因为这里是 PING 命令。
ip->ip_sum = 0;
首部校验和先暂时设置为 0.
NetCopyIP((void*)&ip->ip_src, &NetOurIP); /* already in network byte order */
NetCopyIP((void*)&ip->ip_dst, &NetPingIP); /* - "" - */
设置自身的 IP 地址和目的 IP 地址,NetOurIP 在 NetInitLoop()函数中已经设置,NetPingIP
在 do_ping()函数中已经设置。
ip->ip_sum = ~NetCksum((uchar *)ip, IP_HDR_SIZE_NO_UDP / 2);
刚才已经把首部校验和设置为 0,这里才是真正的校验和的设置;校验和的算法这里先不管
了。
s = &ip->udp_src; /* XXX ICMP starts here */
s[0] = htons(0x0800); /* echo-request, code */
s[1] = 0; /* checksum */
s[2] = 0; /* identifier */
s[3] = htons(PingSeqNo++); /* sequence number */
s[1] = ~NetCksum((uchar *)s, 8/2);
这一段代码实际上是设置的 ICMP 协议的数据了。ICMP 协议在 IP 协议层之上。具体为什么这
样设置建议看看 ICMP 协议的定义。
NetArpWaitTxPacketSize = (pkt - NetArpWaitTxPacket) + IP_HDR_SIZE_NO_UDP + 8;
这里设置的是 ARP 等待的数据报的长度,这个长度实际上就是和发出去的数据长度一样,包
括 16 字节的 Ethernet 头,20 字节的 IP 协议头和 8 字节的 ICMP 协议内容。
PingSend()代码的最后一部分代码如下:
NetArpWaitTry = 1;
NetArpWaitTimerStart = get_timer(0);
ArpRequest();
return 1; /* waiting */
}
设置重试次数,获取当前时间,然后调用 ArpRequest()函数。
到这里,我们发现 PingSend()函数实际上就是准备了 Ethernet 报头、IP 数据报头和 ICMP
协议数据,即我们需要发出去的数据都已经组合完成,最后调用 ArpRequest()函数进行发送
操作。来看看这个函数:
void ArpRequest (void)
{
int i;
volatile uchar *pkt;
ARP_t *arp;
debug("ARP broadcast %d\n", NetArpWaitTry);
pkt = NetTxPacket;
pkt += NetSetEther (pkt, NetBcastAddr, PROT_ARP);
首先还是给 pkt 分配一个地址,这个地址就是 NetTxPacket 指向的地址,在 NetLoop 函数的
开始部分已经给 NetTxPacket 分配了合适的地址,可以回过去看看。然后把 pkt 指向地址作
为 Ethernet 数据报的开始部分,设置 Ethernet 数据报头;设置完成后把 pkt 加上 Ethernet
报头的便宜量 16。
arp = (ARP_t *) pkt;
arp->ar_hrd = htons (ARP_ETHER);
arp->ar_pro = htons (PROT_IP);
arp->ar_hln = 6;
arp->ar_pln = 4;
arp->ar_op = htons (ARPOP_REQUEST);
memcpy (&arp->ar_data[0], NetOurEther, 6); /* source ET addr */
NetWriteIP ((uchar *) & arp->ar_data[6], NetOurIP); /* source IP addr */
for (i = 10; i < 16; ++i) {
arp->ar_data[i] = 0; /* dest ET addr = 0 */
}
上面的代码设置 ARP 协议要求的数据,具体如下:
arp->ar_hrd = htons (ARP_ETHER); //设置目标地址硬件类型
arp->ar_pro = htons (PROT_IP); //设置目标地址上层协议为 IP 协议
arp->ar_hln = 6; //目标地址硬件地址长度
arp->ar_pln = 4; //目标地址上层协议地址长度(IP 地址长度)
arp->ar_op = htons (ARPOP_REQUEST); //操作类型
memcpy (&arp->ar_data[0], NetOurEther, 6); //设置自身 mac 地址
NetWriteIP ((uchar *) & arp->ar_data[6], NetOurIP); //设置自身 IP 地址
for (i = 10; i < 16; ++i) { //目标 mac 地址,全为 0 表示广播
arp->ar_data[i] = 0; /* dest ET addr = 0 */
}
接下来的几行代码:
if ((NetArpWaitPacketIP & NetOurSubnetMask) !=
(NetOurIP & NetOurSubnetMask)) {
if (NetOurGatewayIP == 0) {
puts ("## Warning: gatewayip needed but not set\n");
NetArpWaitReplyIP = NetArpWaitPacketIP;
} else {
NetArpWaitReplyIP = NetOurGatewayIP;
}
} else {
NetArpWaitReplyIP = NetArpWaitPacketIP;
}
NetArpWaitPacketIP 在 PingSend()函数的开始被设置为 ping 的目标地址的 IP,而
NetOurSubnetMask 在 NetInitLoop()函数中从环境变量中取得,我们一把在环境变量中把他
设置为 255.255.255.0,那么转换成 32 为的数据则为 0xFFFFFF00。因此判断这里是否执行的
依 据就 是判 断目标 IP 地 址和 自身 IP 地址 的高 24 位是 否相 等, 若相 等则 直接把
NetArpWaitReplyIP 设 置 为 NetArpWaitPacketIP , 而 这 个 NetArpWaitPacketIP 是 在
PingSend()函数的开始处设置为被 ping 的目的地址,前面已经讲过;若不相等的话,就是我
们说的“不在同一个网段”当中,那么就需要有一个 gatewayip,即网关地址,这个值也是
从环境变量中读出来,若环境变量中没有则会把这个值设置为 0,那么就会打印这句##
Warning: gatewayip needed but not set。
NetWriteIP ((uchar *) & arp->ar_data[16], NetArpWaitReplyIP);
(void) eth_send (NetTxPacket, (pkt - NetTxPacket) + ARP_HDR_SIZE);
然后这句是把等待 ARP 回应的目标 IP 地址写入 ARP 数据包当中,写完过后调用 eth_send 函
数用硬件把刚才组装的 ARP 出具发送出去,发送的长度为 ARP 数据的长度和 Ethernet 数据头
长度。
到这里,ARP 数据通过硬件发送出去了,然后要做的工作就是等待数据的返回,在 NetLoop()
中完成了 PingSend()函数,就是等待数据的返回了,接着看这一部分代码。
/*
* Main packet reception loop. Loop receiving packets until
* someone sets `NetState' to a state that terminates.
*/
for (;;) {
WATCHDOG_RESET();
#ifdef CONFIG_SHOW_ACTIVITY
{
extern void show_activity(int arg);
show_activity(1);
}
#endif
/*
* Check the ethernet for a new packet. The ethernet
* receive routine will process it.
*/
eth_rx();
可以看到这里是一个死循环,首先在这个循环里面复位了看门狗,这个先不管。然后就是调
用 eth_rx()函数,从函数名称就可以知道这个是进行网络数据的接收。这个函数和底层的网
卡驱动相关,我们这里先不管网卡驱动,就知道这个 eth_rx()最后将会调用 NetReceive()
函数来对接收到的数据进行处理就好了。因此我们直接看 NetReceive()函数。
void
NetReceive(volatile uchar * inpkt, int len)
{
Ethernet_t *et;
IP_t *ip;
ARP_t *arp;
IPaddr_t tmp;
int x;
uchar *pkt;
#if defined(CONFIG_CMD_CDP)
int iscdp;
#endif
ushort cti = 0, vlanid = VLAN_NONE, myvlanid, mynvlanid;
debug("packet received\n");
NetRxPacket = inpkt;
NetRxPacketLen = len;
et = (Ethernet_t *)inpkt;
这里定义了一些变量,然后对一些局部变量进行赋值,不详细看了,后面用到再说。
/* too small packet? */
if (len < ETHER_HDR_SIZE)
return;
这里检查接收数据的长度,如果比 Ethernet 数据报头的长度还小的话,显然数据是没有意义
的,就不用进一步处理了。
x = ntohs(et->et_protlen);
debug("packet received\n");
if (x < 1514) {
............................
} else if (x != PROT_VLAN) { /* normal packet */
ip = (IP_t *)(inpkt + ETHER_HDR_SIZE);
len -= ETHER_HDR_SIZE;
} else { /* VLAN packet */
................................
}
一般情况下,正常的数据包会进入第二个选项,因此把其他两个代码都省略了。进入过后把
总长度减去 Ethernet 报头,然后把输入数据缓冲区向前移动 Ethernet 的报头长度的字节数
(14 个字节), 赋给指针变量 ip,方便后面的处理。
然后的代码大致如下:
switch (x) {
case PROT_ARP:
......................
break;
case PROT_RARP:
......................
break;
case PROT_IP:
......................
break;
}
实际的代码被省略了,这里看一看他的大概结构。他实际上是根据 Ethernet 报头的上层协议
字段还决定这里到底返回的是何种数据,然后根据相应的协议类型还选用不同的处理方式。
(这里需要了解 Ethernet 报头的详细定义,建议看看)。
由于我们这里分析的是在发出了一个 ARP 请求后等待的数据包,因此这里就应该执行的是
PROT_ARP 后面的代码,而其他选项的代码不会被执行,所以我们看这个后面的代码:
arp = (ARP_t *)ip;
if (len < ARP_HDR_SIZE) { //长度至少要大于等于 ARP 数据报头
printf("bad length %d < %d\n", len, ARP_HDR_SIZE);
return;
}
if (ntohs(arp->ar_hrd) != ARP_ETHER) { //硬件地址类型必须为 Ethernet
return;
}
if (ntohs(arp->ar_pro) != PROT_IP) { //上层协议为 IP 协议
return;
}
if (arp->ar_hln != 6) { //硬件地址必须为 6 个字节的长度
return;
}
if (arp->ar_pln != 4) { //上层协议(IP 协议)地址长度必须为 4 个字节
return;
}
if (NetOurIP == 0) { //本地 IP 要有效
return;
}
if (NetReadIP(&arp->ar_data[16]) != NetOurIP) { //如果得到的包的目的 IP
不是本机,不处理
return;
}
这里是首先做一些基本的检查,具体检查的项目见前面代码处的注释。由于 ARP 请求分两种:
一是请求本机送回自身的 mac 地址;二是本机已经发送了 ARP 请求,这里收到的是其他主机
的返回信息。显然我们的情况属于第二种,因此我们来看第二种情况下的处理代码:
case ARPOP_REPLY: /* arp reply */
/* are we waiting for a reply */
if (!NetArpWaitPacketIP || !NetArpWaitPacketMAC)
break;
这里首先检查 NetArpWaitPacketIP 和 NetArpWaitPacketMAC 两个是否有效,这两个变量的值
都是在 PingSend()函数当中预先设置好的,因此这里应该是有效。他们分别代表等待的主机
的 IP 地址和 mac 地址。
后面的处理代码如下:
tmp = NetReadIP(&arp->ar_data[6]);
/* matched waiting packet's address */
if (tmp == NetArpWaitReplyIP) {
debug("Got it\n");
/* save address for later use */
memcpy(NetArpWaitPacketMAC, &arp->ar_data[0], 6);
#ifdef CONFIG_NETCONSOLE
(*packetHandler)(0,0,0,0);
#endif
/* modify header, and transmit it */
memcpy(((Ethernet_t *)NetArpWaitTxPacket)->et_dest,
NetArpWaitPacketMAC, 6);
(void) eth_send(NetArpWaitTxPacket, NetArpWaitTxPacketSize);
/* no arp request pending now */
NetArpWaitPacketIP = 0;
NetArpWaitTxPacketSize = 0;
NetArpWaitPacketMAC = NULL;
}
return;
可以看出,首先是从收到的 ARP 数据包当中读取出发出这个包的主机的 IP 地址,若它不是我
们等待的主机的 IP,那么直接返回,不用进一步处理了;若是,那就是我们所期望的情况,
那就继续发送 ICMP 请求。因为 ARP 请求的目的就是为了得到目标 IP 对应的 mac 地址,所以
这个 if 语句里面首先把目标地址的 mac 地址拷贝到 NetArpWaitPacketMAC 地址,以便后面使
用。然后就是把这个 mac 地址填充到已经准备好的 ICMP 信息的相应字段。( 在前面的
PingSend()函数里面已经准备好所有的 ICMP 信息,但是不能直接发送,因为不知道目标地址
的 mac 地址,可以说那个时候是万事俱备,只欠东风。“东风”就是这里的目标地址的 mac
地址,因此现在可以发送 ICMP 请求鸟~~~)。
完成这个发送后,eth_rx()函数的这一次就算已经完成了,那么就又该回到 NetLoop()函数
的 for 循环当中来了。这次要等待的就不是 ARP 返回信息,而是等待 ICMP 的返回信息鸟,处
理的地方还是在刚才提到的 NetReceive()函数中。不同的地方是上次是等待的 ARP 消息,在
那个关键的 switch 语句中选用了 PROT_ARP 后面的代码,而这次会选用 PROT_IP 后面的代码,
因此我们来看看详细的代码:
case PROT_IP:
debug("Got IP\n");
/* Before we start poking the header, make sure it is there */
if (len < IP_HDR_SIZE) {
debug("len bad %d < %lu\n", len, (ulong)IP_HDR_SIZE);
return;
}
/* Check the packet length */
if (len < ntohs(ip->ip_len)) {
printf("len bad %d < %d\n", len, ntohs(ip->ip_len));
return;
}
len = ntohs(ip->ip_len);
debug("len=%d, v=%02x\n", len, ip->ip_hl_v & 0xff);
/* Can't deal with anything except IPv4 */
if ((ip->ip_hl_v & 0xf0) != 0x40) {
return;
}
/* Can't deal with IP options (headers != 20 bytes) */
if ((ip->ip_hl_v & 0x0f) > 0x05) {
return;
}
/* Check the Checksum of the header */
if (!NetCksumOk((uchar *)ip, IP_HDR_SIZE_NO_UDP / 2)) {
puts ("checksum bad\n");
return;
}
/* If it is not for us, ignore it */
tmp = NetReadIP(&ip->ip_dst);
if (NetOurIP && tmp != NetOurIP && tmp != 0xFFFFFFFF) {
#ifdef CONFIG_MCAST_TFTP
if (Mcast_addr != tmp)
#endif
return;
}
/*
* The function returns the unchanged packet if it's not
* a fragment, and either the complete packet or NULL if
* it is a fragment (if !CONFIG_IP_DEFRAG, it returns NULL)
*/
if (!(ip = NetDefragment(ip, &len)))
return;
同样是先做一些基本的检查,检查的内容在代码的注释中已经有了,就不在叙述,接着看后
面:
if (ip->ip_p == IPPROTO_ICMP) {
ICMP_t *icmph = (ICMP_t *)&(ip->udp_src);
正好我们等待的就是 ICMP 消息,所以这里会执行,接着:
switch (icmph->type)
又是一个 switch,它是用来判断 ICMP 这次具体要求我们干什么,很现在这里应该是告诉我
们它收到了我们刚才的请求,那么就应该执行:
case ICMP_ECHO_REPLY:
/*
* IP header OK. Pass the packet to the current handler.
*/
/* XXX point to ip packet */
(*packetHandler)((uchar *)ip, 0, 0, 0);
return;
可 以 看 到 这 里 就 调 用 了 一 个 函 数 , 这 个 指 针 函 数 在 PingStart() 函 数 中 通 过 调 用
NetSetHandler()函数设置的,真正调用的函数实际上是 PingHandler()函数,我们来看看这
个函数:
static void
PingHandler (uchar * pkt, unsigned dest, unsigned src, unsigned len)
{
IPaddr_t tmp;
volatile IP_t *ip = (volatile IP_t *)pkt;
tmp = NetReadIP((void *)&ip->ip_src);
if (tmp != NetPingIP)
return;
NetState = NETLOOP_SUCCESS;
}
可以看到这个函数比较了 NetPingIP 和我们收到的数据包当中提取出来的 IP 地址,若发送这
个包的主机的 IP 地址和 NetPingIP 一样则把 NetState 设置为 NETLOOP_SUCCESS,否则直接
返回。
这里又要回头去看看 NetLoop()函数里面的 for(;;)循环了,看看这个 for 循环在什么时候结
束。
switch (NetState) {
case NETLOOP_RESTART:
#ifdef CONFIG_NET_MULTI
NetRestarted = 1;
#endif
goto restart;
case NETLOOP_SUCCESS:
if (NetBootFileXferSize > 0) {
char buf[20];
printf("Bytes transferred = %ld (%lx hex)\n",
NetBootFileXferSize,
NetBootFileXferSize);
sprintf(buf, "%lX", NetBootFileXferSize);
setenv("filesize", buf);
sprintf(buf, "%lX", (unsigned long)load_addr);
setenv("fileaddr", buf);
}
eth_halt();
return NetBootFileXferSize;
case NETLOOP_FAIL:
return (-1);
}
可以看到他是根据 NetState 的状态来进行处理,若到了 NETLOOP_SUCCESS,则函数就会返回
传输的字节数,当然这个值是大于零的。结合 do_ping()函数,我们就能看到返回大于 0 的
时候为 ping 成功的消息。
这里没有分析在出错的时候 (ping 不通)怎样一个处理流程,这一部分可以尝试自己去分析,
因为到这里代码的轮廓已经比较清晰了。
下面把 ping 的流程梳理一下,理解起来清晰一点:
1、准备 ICMP 数据包,但是这个时候还不知道目标 IP 的地址,因此不能直接发送 ICMP 数据。
2、准备 ARP 数据包,发送 ARP 数据包,然后等到目标机的返回。
3、处理目标机的返回信息,提取目标主机的 mac 地址,填充到第一步当中的 ICMP 数据包当
中去。
4、发送 ICMP 数据包到目标机。
5、等待 ICMP 数据包的返回。
6、完成流程。
这里分析的虽然只有 ping 命令,但是它已经把 u-boot 中整个网络的框架都使用到了,因此分
析其他协议(如基于 UDP 的 tftp 协议)应该也不是什么困难,希望有读者把 U-Boot 中的网络进
行一个更一般性的分析,分享给大家!