6.U-Boot 中 PING 命令处理流程

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 的话为 4IPv6 的话为 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 中的网络进
行一个更一般性的分析,分享给大家!
 

你可能感兴趣的:(6.U-Boot 中 PING 命令处理流程)