深入理解iputils网络工具-第2篇 ping程序
2.1 引言
“ping”这个名字源于声纳定位操作。Ping程序由Mike Muuss编写,目的是为了测试另一台主机是否可达。该程序发送一份ICMP回显请求报文给主机,并等待返回ICMP回显应答。
2.2 ping程序的使用敲入命令:
2.4 IP报文结构IP报文结构如下所示:
IP数据报文的首部中有选项部分,这个部分可以用来存储IP时间戳或者IP记录路由选项。
存储IP时间戳,如下图所示:
IP记录路由选项,如下图所示:
2.5 ICMP报文结构
ICMP的封装方式如下图所示:
ICMP报文的结构如下图所示:
2.6 ICMP回显请求和回显应答报文格式
ICMP回显请求和回显应答报文格式如下所示:
2.7 ICMP报文类型列表
不同种类的ICMP报文的首部有所不同。如下:
类型
代码
描述
0
ICMP_ECHOREPLY
0
回显应答
3
ICMP_DEST_UNREACH
目的不可达
0
ICMP_NET_UNREACH
网络不可达
1
ICMP_HOST_UNREACH
主机不可达
2
ICMP_PROT_UNREACH
端口不可达
3
ICMP_PORT_UNREACH
协议不可达
4
ICMP_FRAG_NEEDED
需要进行分片单设置了不分片比特
5
ICMP_SR_FAILED
源站选路失败
6
ICMP_NET_UNKNOWN
目的网络不认识
7
ICMP_HOST_UNKNOWN
目的主机不认识
8
ICMP_HOST_ISOLATED
源主机被隔离(作废不用)
9
ICMP_NET_ANO
目的网络被强制禁止
10
ICMP_HOST_ANO
目的主机被强制禁止
11
ICMP_NET_UNR_TOS
由于服务类型TOS,网络不可达
12
ICMP_HOST_UNR_TOS
由于服务类型TOS,主机不可达
13
ICMP_PKT_FILTERED
由于过滤,通信被强制禁止
14
ICMP_PREC_VIOLATION
主机越权
15
ICMP_PREC_CUTOFF
优先权终止生效
ICMP_SOURCE_QUENCH
4
0
源端被关闭
ICMP_REDIRECT
5
重定向
0
ICMP_REDIR_NET
对网络重定向
1
ICMP_REDIR_HOST
对主机重定向
2
ICMP_REDIR_NETTOS
对服务类型和网络重定向
3
ICMP_REDIR_HOSTTOS
对服务类型和主机重定向
ICMP_ECHO
8
0
请求回显
9
0
路由器通告
10
0
路由器请求
ICMP_TIME_EXCEEDED
11
超时
0
ICMP_EXC_TTL
传输请见生存时间为0
1
ICMP_EXC_FRAGTIME
在数据包组装期间生存时间为0
ICMP_PARAMETERPROB
12
参数问题
0
坏的IP首部
1
缺少必需的选项
ICMP_TIMESTAMP
13
0
时间戳请求
ICMP_TIMESTAMPREPLY
14
0
时间戳应答
ICMP_INFO_REQUEST
15
0
信息请求
ICMP_INFO_REPLY
16
0
信息应答
ICMP_ADDRESS
17
0
地址掩码请求
ICMP_ADDRESSREPLY
18
0
地址掩码应答
pr_icmph()函数中分析ICMP报文类型,并针对错误报文打印出出错问题。惨照上表就能比较好地分析各种问题出现的大致原因了。
另外在rdisc.c文件中使用了ICMP的路由器通告报文(类型为9)和ICMP路由器请求报文(类型为10)。
各种ICMP类型和代码的常量定义在linux-2.6.27/include/linux/icmp.h文件中。
2.8 socket选项程序中使用setsockopt()函数设定了套接字的选项。用到的选项如下:
level(级别)
optname(选项名)
说明
标志
SOL_SOCKET
SO_BROADCAST
允许或禁止发送广播数据
?
SO_ATTACH_FILTER
安装过滤器。
SO_SNDBUF
设置发送缓冲区的大小。
SO_RCVBUF
设置接收缓冲区的大小。
SO_DEBUG
打开或关闭调试信息
?
SO_DONTROUTE
打开或关闭路由查找功能。
?
SO_TIMESTAMP
打开或关闭数据报中的时间戳接收。
?
SO_SNDTIMEO
设置发送超时时间。
SO_RCVTIMEO
设置接收超时时间。
SO_BINDTODEVICE
将套接字绑定到一个特定的设备上。
SOL_RAW
ICMP_FILTER
设置套接字ICMP过滤选项。
IPPROTO_IP
IP_OPTIONS
设置发出的数据报中的IP选项
IP_MULTICAST_LOOP
多播API,禁止组播数据回送
?
IP_MULTICAST_TTL
多播API,设置输出组播数据的TTL值
IP_TOS
设置发出的数据报中的IP TOS
SOL_IP
IP_MTU_DISCOVER
为套接字设置Path MTU Discovery setting(路径MTU发现设置)
?
IP_RECVERR
允许传递扩展的可靠的错误信息
?
程序首先取得了一个UDP的套接字probe_fd,并根据用户的输入配置套接字的选项。probe_fd用到的选项主要有:SO_BINDTODEVICE、SO_BINDTODEVICE、SO_BROADCAST、IP_TOS等。
ICMP报文的套接字icmp_sock用到的选项除了SO_BINDTODEVICE选项以外,列表中的所有选项都用到了。
2.9 ping.c程序的全局变量的分析static int ts_type;
timestamp的类型
在-T选项中设置,可以设置为IPOPT_TS_TSONLY、IPOPT_TS_TSANDADDR或者IPOPT_TS_PRESPEC。
static int nroute = 0;
主机输入的总数,最多为9个,因为IP首部选项中最多能存储9个地址
static __u32 route[10];
在输入多个主机时,存储地址。
可能输入多个主机的情况是:-Ttsprespec [host1 [host2 [host3 [host4]]]] 选项,或者ping hostName1 hostName2 ... hostNameN;前者是想获得确定几个路由对应的时间戳,而后者为什么这么设置,我还不大明白 。
struct sockaddr_in whereto;
存储了目的主机的信息。
int optlen = 0;
ip选项的长度。
由IP的协议可知,最大为40,在需要在IP首部选项字段中存储数据时(例如-T、-R选项)就设置为最大值。
int settos = 0;
服务质量的设置。
可以用-Q选项用来设置服务质量,例如最小开销、 可靠性、吞吐量、低延迟。
IP协议有一个8bit的DS区分服务(以前叫服务类型)。前三位是优先(precedence)字段(在目前,优先字段并未被大家使用),接着4bit是TOS位,最后1bit好像没有使用。
4比特TOS位的意义分别为D(最小时延)、T(最大吞吐量)、R(最高可靠性)、C(最小代价)。
要设置TOS位为对应意义,可以设置-Q <tos>中的 <tos>分别为0x10,0x08,0x04,0x02 。
int icmp_sock;
ICMP的soket文件描述符。
u_char outpack[0x10000];
用来存储ICMP报文首部和数据的数组,为ICMP报文分配的存储空间。
int maxpacket = sizeof(outpack);
用来存储ICMP报文首部和数据的数组的最大大小。
static int broadcast_pings = 0;
标识用户是不是想ping广播地址。
可以通过-b选项设置。
如果不设置,则默认为0。
struct sockaddr_in source;
存储了源主机的信息。
如果-I选项后面带的是源主机地址而不是设备名的话,就将主机的信息存储在source中。在socket试探的连接成功后,程序还用getsockname重新确定了source的值。
char *device;
如果-I选项后面带的是设备名而不是源主机地址的话,如eth0,就用device指向该设备名。
该device指向一个设备名之后,会设置socket的对应设备为该设备。
int pmtudisc = -1;
2.10 ping_common.c程序的全局变量的分析
int options;
存储各种选项的FLAG设置情况。
在判断输入选项时设置各个bit位。
int sndbuf;
发送缓冲区大小。
可以在-S <sndbuf>中设置,如果没有设置,则估计一个大小。
int ttl;
报文ttl的值。
可以在-t选项中设置。
在设置soket选项时设置IP广播报文TTL和IP报文的TTL都为ttl值。
int rtt;
用指数加权移动平均算法估计出来的RTT值。
初始值是0。
gather_statistics()函数中根据上次的RTT值和原来的rtt值加权得到新rtt的值。
在update_interva()函数中用来计算新的interval的值。
int rtt_addend;
配合rtt使用。
用来计算新的interval的值,似乎是更具上个rtt的值给interval留部分余量。
__u16 acked;
接到ACK的报文的16bit序列号。
在gather_statistics()函数里更新,实际的更新方法似的acked不超过0x7FFF,不然就会发生回绕。
int mx_dup_ck = MAX_DUP_CHK;
?
long npackets;
需要传输的最多报文数。
可以在-c 选项里设置。
如果没有设置则默认是0,故此每次在查询此值时就判断是否为0,0似乎作为无穷大来考虑。
long nreceived;
得到回复的报文数。
初始值是0。
在gather_statistics函数中递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。
long nrepeats;
重复的报文数。
初始值是0。
在gather_statistics函数中递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。
long ntransmitted;
发送的报文的最大序列号。
初始值是0。
在pinger函数中递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。
long nchecksum;
checksum错误的恢复报文。
初始值是0。
在gather_statistics函数中,若csfailed为1的时候,则递加,进行统计。在程序执行finsh时,使用这个变量,打印出来作为参考。
不过似乎checksum是不会被改变的,因为gather_statistics的选项csfailed在唯一的一次调用中(parse_reply()函数中)为0。
long nerrors;
icmp错误数。
初始值是0。
在程序接受到出错的报文之后,就会调用receive_error_msg。在这个函数里如果判断确实是一个错误,错误有可能是本地出错,有可能是网络出错,不管是哪个出错,都将这nerrors递加。parse_reply也会改变这个变量。在程序执行finsh时,使用这个变量,打印出来作为参考。
int interval = 1000;
发送两个相邻报文之间相距的时间,单位为毫秒。
可以在-i选项中设置。
在设置-f的洪泛模式下,会设置interval为0。
如果没有设置,则默认是1000。
int preload;
在接受到第一个回复报文之前所发送的报文数。
可以通过-l <preload>选项设置。
如果没有设置,默认值是1。
int deadline = 0;
在deadline秒之后,程序退出。
可以由-w选项设置。如果设置了,则在setup函数中设置闹钟,当程序执行到deadline秒时产生SIGALRM中断,退出程序。
如果没有设置则默认值是0,程序运行没有时间限制。
int lingertime = MAXWAIT*1000;
等待回复的最长时间,单位为毫秒。
可以通过-W选项设置。这个值在完成一次正确发收过程后就由2*tmax代替,而失去作用了。
默认值是MAXWAIT*1000即10000,MAXWAIT定义在ping_common.h中。
struct timeval start_time;
程序运行开始时的主机时间。
在setup函数中使用gettimeofday初始化,在finish函数中和cur_time一起用来计算程序运行的时间。
struct timeval cur_time;
程序运行时当前的主机时间。
volatile int exiting;
程序是不是应该退出。
初始值是0,就是不应该退出。
在中断处理程序sigexit中会将这个值设为1。这个中断处理程序只在产生SIGALRM和SIGINT中断时(可以用Ctrl+c产生)才会执行。中断处理程序在setup函数中安装。
volatile int status_snapshot;
程序是不是应该调用status()函数打印出程序的运行状态。
初始值是0。
在中断处理程序sigstatus中会将这个值设为1。这个中断处理程序只在产生SIGQUIT中断时(可以用Ctrl+\产生)才会执行。中断处理程序在setup函数中安装。
int confirm = 0;
表明sendmsg函数的选项的MSG_CONFIRM选项是否设置。
如果设置MSG_CONFIRM,则会告诉链路层的传送有了进展:已经接受到对方的一个成功的答复。由于MSG_CONFIRM的这个意义,所以在发送第一个数据是MSG_CONFIRM选项不因该设置,即confirm初始值为0。在成功接受到一个回复之后,confirm则应该设置为MSG_CONFIRM了。只有在确定取得一个回复时才将confirm由0改为MSG_CONFIRM,这就是为什么confirm只有在gather_statistics()才会被改变的原因。然而更麻烦的是MSG_CONFIRM选项只有在Linux 2.3及以上内核中才支持,所以就需要confirm_flag变量了。
int confirm_flag = MSG_CONFIRM;
用来修补老版本linux内核的问题。
confirm_flag的初始值为MSG_CONFIRM。这样在gather_statistics()里confirm就更新为confirm_flag了。但是,如果由于设置MSG_CONFIRM而产生了发送错误(linux版本较老,不支持MSG_CONFIRM选项)。这样就会在下个循环里调用gather_statistics(),更新confirm变量,保证不会发送出错了。
int working_recverr;
?
int timing;
是否能够在ping过程中测算时间
如果ICMP报文的数据长度足以存储timeval结构数据,则timing设置为1。如果timing设置为1,则在ICMP报文中插入发送的时间,这样在接受到ICMP回复时,就可以根据该数据计算RRT。否则就无法计算RRT,也就无法进行时间统计了。
从根本上说timing的值由datalen变量的大小决定。
可以尝试运行ping -s1 www.ustc.edu.cn -c 1,看看运行结果怎样。
可以看到没有时间统计输出,因为-s选项设置的datalen值太小。
long tmin = LONG_MAX; /*minimum round trip time */
最小RRT
初始值为LONG_MAX,每次接受到回复报文之后,就在gather_statistics函数中本次RRT是不是比tin大,如果是,就更新tmin。在程序执行完成之后,将打印出这个信息作为参考。
long tmax;
最大RRT
初始值为0,每次接受到回复报文之后,就在gather_statistics函数中本次RRT是不是比tmax大,如果是,就更新tmax。在程序执行完成之后,将打印出这个信息作为参考。
此外tmax还作为每次发送报文后等待接受报文的时间长度的参考,见__schedule_exit函数。如果超出这个时间长度还没有完成一次发送和接受,则发生超时中断。
long long tsum; /*sum of all times, for doing average */
每次RRT之和。
初始值为0,每次接受到回复报文之后,就在gather_statistics函数中加上本次RRT。
用来计算平均RRT。
long long tsum2;
每次RRT的平方和。
初始值为0,每次接受到回复报文之后,就在gather_statistics函数中加上本次RRT的平方。
用来计算RRT的方差。
int pipesize =-1;
初始值为-1。
int datalen = DEFDATALEN;
数据长度。
初始值为DEFDATALEN,即56。
可以通过-s选项设置 。
char *hostname;
目的主机名字。
在开始的时候,由用户作为程序的选项输入。随后通过gethostbyname()函数由主机名得到主机,然后将主机名改为函数返回的官方主机名。
在最后输出的目的主机名就是这个名字。
int uid;
用户ID。
在main函数中通过getuid()取得。
如果uid不是0,即用户不是超级用户,则在设置选项的时候有限制:
-i<interval>,<interval>不得小于0.2;在ping广播地址时,<interval>不能设置为小于1的数。
-M<hint>,在ping广播地址时,<hint>不能设置为IP_PMTUDISC_DO之外的IP_PMTUDISC_DONT或IP_PMTUDISC_WANT。
-s<packetsize>, <packetsize>不能超过sizeof(outpack)-8。
-v,不会输出比较敏感的冗长信息,例如parse_reply函数中可能输出的额外信息。
-l<preload>,ping广播地址时,<preload>不能大于3。
-f,必须要和-i选项配合使用,且<interval>不小于0.2。
int ident;
本进程的ID。
在setup函数中通过getpid()取得。
在ICMP的数据中添加进程ID,并通过判断接受到的ICMP回复的进程ID是不是正确来判断ICMP回复是不是本进程的回复。
static int screen_width = INT_MAX;
窗口的宽度大小,也就是控制台一行能打印多少字符。
在setup函数中通过ioctl()取得。
2.11 重要函数的分析int main(int argc, char **argv);
主函数。
在这个函数里:取得用户输入的选项,并根据这些选项及其参数设置相应的标识和参数值。根据这些标识和参数值,首先连接(connect)一个探测的UDP报文,以探知目的地址的基本情况。然后设置ICMP报文的套接字选项,然后调用setup()函数来进一步设置与协议无关的套接字选项(与ping6公用)。在套接字设置好后,调用main_loop()函数完成探测。
定义在ping.c文件中。
void main_loop(int icmp_sock, __u8 *packet, intpacklen);
完成报文发送、分析的主要函数。
在这个函数里:一直调用pinger()函数发ICMP报文和调用recvmsg()函数接受报文。如果recvmsg()函数没有正确接受报文,调用receive_error_msg()函数处理接受到的ICMP差错报文。如此反复,直到用户要求终止或者报文发送次数达到要求,或者超出的程序的时间限制,程序才停止发送/接受;程序在停止发送/接受后,调用finish()函数打印出统计数据。
在main()函数中调用到此函数。
定义在ping_common.c文件中。在这个文件中的所有函数都能够被ping和ping6共同使用。
void int pinger(void);
构成并发送报文。
在这个函数里:调用send_probe()尝试发送报文,并处理send_probe()没有成功发送时出现的错误。在处理某些种类的错误时,用到receive_error_msg()函数。
在main_loop()函数中调用到此函数。
定义在ping_common.c文件中。
int send_probe()
构建报文,并发送报文。
在这个函数里:根据用户的参数设置,设置ICMP报文的类型、代码、序号、标识符,并往ICMP报文的选项数据部分添加发送时间,然后计算校验和。构建出这个ICMP报文后,调用sendmsg()函数发送ICMP报文。此函数不处理发送出错。
在pinger()函数中调用到此函数。
定义在ping.c文件中。
int receive_error_msg()
处理ICMP差错报文。
在这个函数里:调用设置了MSG_ERRQUEUE标识的recvmsg()来接收错误队列中的ICMP错误报文。取得错误信息之后,分析出错的原因是由于本地原因还是网络原因,并进行处理(比如设置更严格的ICMP过滤)。
在main_loop()函数和pinger()函数中调用到此函数。
定义在ping.c文件中。
void setup(int icmp_sock)
设置与协议无关的选项。
在这个函数里:根据用户设置,这些设置包括interval的设置,socket的是否打开调试信息(SO_DEBUG)、是否打开路由查找功能(SO_DONTROUTE)、是否打开数据报中的时间戳接收(SO_TIMESTAMP)、发送时间限制(SO_SNDTIMEO)、接受时间限制(SO_RCVTIMEO)等选项,往报文内填内容的设置,中断处理程序的设置,闹钟的设置等。
在main()函数和pinger()函数中调用到此函数。
定义在ping_common.c文件中。
2.12 时间间隔和报文预发机制的实现程序使用一个分配时间片的概念,来控制发送报文的时间间隔,并实现在没有接到回复报文之前就预先发送preload个请求报文。
初始时分配interval*preload的时间片用来发送报文(程序中第一次发送设置时间片为interval*(preload-1),由于设置后没有减去第一次发送用去的interval时间片,所以相当于分配了interval*preload的时间片)。每次发送报文都要用掉interval毫秒的时间片。如果时间片不为负数的话,则一直持续发送报文。如果时间片为负数,则退出循环,开始处理接受到的回复报文。处理接受到的回复报文,会用去比较长的时间。
从上次发送报文,到当前准备发送报文的时间被计时器记录(实际上是通过记录上次发送报文的系统时间到当前系统时间之差来记录的),并作为新的时间片加入原时间片中,作为下次发送报文的时间片。为了确保没有接到回复而发送了的报文数目不会超过preload个,这个新的时间片如果超过interval*preload,则被改为interval*preload。如果新的时间片小于发送一个报文的时间interval,则仍然不发送报文,退出发送报文的循环,接受回复报文和处理可能出现的中断。
通过上述方法,实现了两个功能:
1. 可以在不等待回复的情况下,预先发送preload个报文。由于初始时分配的时间片为interval*preload,所以刚开始,程序就连续发送interval个请求报文;如果程序等了很长时间没有发送报文,则计时器的引入使得这一段时间也作为发送时间片的新的一部分,这样程序又可以连续发送几个报文。
2. 可以控制报文发送的时间间隔为interval。从初始时开始,在连续发送preload个报文后,时间片被耗尽。只有在计时器中累加的时间片超过interval时才能再连续发送一个或几个报文(不超过preload个)。
相关函数:
int pinger(void);
void main_loop(int icmp_sock, __u8 *packet, int packlen);
相关选项:
-l <preload>
-i <interval>
2.13 回复等待计时的实现当用户使用-c <count>设置了需要传送/接受的报文数,且通过-w <deadline>设置了程序运行的时间,那么则程序只需要在发送<count>个报文,并等待接受报文,直到接受到<count>个回复或者程序运行时间超过限制为止。如果用户只使用-c<count>设置了需要传送/接受的报文数,没有设置程序运行的时间,那么鉴于有些请求报文丢失而永远不会接到报文,程序不能在发送了<count>个报文之后一直等待。程序一直等待一个可能再也不会出现的事情是难以接受的,它应该做的是在发送<count>个请求报文后,等待一段时间,如果实在没有等到回复报文,就退出。
上面说的等待时间怎么确定呢?如果程序成功地收到了一个或者几个针对请求报文的回复,那么就将两倍的最大RTT作为等待的时间。如果程序没有接到任何的回复,RTT无从得知,就使用lingertime作为等待的最长时间。这个lingertime可以通过-W <timeout>选项由用户设置;如果用户没有设置则为一个常量(程序中,默认等待10秒)。不过值得主注意的是lingertime这个变量在程序成功地收到了回复之后,就没有任何作用了。
最长等待时间由一个闹钟实现。如上所述,设定这个闹钟的条件有下面几个:
1. 需要传送/接受的报文被设置了。
2. 程序运行的时间没有被设置。
3. 已经发送的报文数等于或大于需要传送/接受的报文数。
闹钟的时间被设置为:
1. 如果程序成功地收到了一个或者几个针对请求报文的回复,那么就将两倍的最大RTT作为等待的时间。
2. 否则,设置为lingertime。
当超出闹钟的时间之后,就会产生SIGALRM中断,使得程序退出。
相关函数:
void main_loop(int icmp_sock, __u8 *packet, int packlen);
staticinline int schedule_exit(int next);
schedule_exit(int next)
相关选项:
-c<count>
-w<deadline>