本文旨在讲解 Ifconfig 流量信息的获得。将通过 Ifconfig 代码阅读来跟踪流量统计的来 源,这将贯通网络模块,proc文件系统,到硬件设备驱动来探寻这些信息的来源和流向。
Ifconfig是 net-tools的一个组件。 net-tools 为 GNU/Linux 提供控制网络子系统的很多 重要工具(arp, ifconfig, netstat...),他几乎成为所有发行版必备的软件。
来看他的输出情况。包含了基本的网卡信息,内核网络配置。除此之外,还包括了流量统计 信息。笔者开始就抱有非常疑惑的信息,Ifconfig是如何做到这一点的?这样一个高层的应 用程序难道和网卡紧密相连么?正是由于这样的疑问,我才开始了阅读代码的过程。
wick@ ~: sudo ifconfig eth0
eth0 Link encap:以太网 硬件地址 00:e0:4c:43:d4:8d
inet 地址:192.168.16.91 广播:192.168.16.255 掩码:255.255.255.0
inet6 地址: fe80::2e0:4cff:fe43:d48d/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 跃点数:1
接收数据包:20825 错误:0 丢弃:0 过载:0 帧数:0
发送数据包:35057 错误:0 丢弃:0 过载:0 载波:0
碰撞:0 发送队列长度:1000
接收字节:2225064 (2.2 MB) 发送字节:50827282 (50.8 MB)
中断:23 基本地址:0xb800
本篇实例讲解使用到了 —- net-tools-1.60 源码,可以在官方网站找到
linux-2.6.29 代码
NET_LIB = $(NET_LIB_PATH)/lib$(NET_LIB_NAME).a来自 net-tools 的Makefile告诉我们,ifconfig工具的链接很简单。除了一个库文件以外, 只需要ifconfig.o。默认情况下,没有指明 obj 文件生成方式的,都只是需要单个同名的C 源码文件 ifconfig.c。
ifconfig: $(NET_LIB) ifconfig.o
$(CC) $(LDFLAGS) -o ifconfig ifconfig.o $(NLIB) $(RESLIB)
尽快找寻main文件中的参数选项,我们只需要找到列举 eth0 信息的选项分支,其他赶快跳 过。在ifconfig.c:288后以下段落,是我们关注的参数解析过程。
/* Create a channel to the NET kernel. */首先,调用NET_LIB中的 sockets_open 函数创建 socket 链接;然后在无参数的情况下, if_print 打印所有网络接口信息;如果有参数,则将参数作为接口名称,将会打印对应接 口信息。后者将是我们 `ifconfig eth0' 的参数情况,而 if_print 是唯一调用函数。
if ((skfd = sockets_open(0)) < 0) {
perror("socket");
exit(1);
}
/* Do we have to show the current setup? */
if (argc == 0) {
int err = if_print((char *) NULL);
(void) close(skfd);
exit(err < 0);
}
/* No. Fetch the interface name. */
spp = argv;
safe_strncpy(ifr.ifr_name, *spp++, IFNAMSIZ);
if (*spp == (char *) NULL) {
int err = if_print(ifr.ifr_name);
(void) close(skfd);
exit(err < 0);
}
同一个文件(ifconfig.c)就可以看到if_print函数,看起来很简洁。
static int if_print(char *ifname)首先,如果ife_short有效,则打印表头。这是网络接口信息的简洁模式,使用`ifconfig -s'的效果正式如此,`-s'将设置 ife_short = 1,这在main函数解析参数时候已经完成。 然后,分析参数,我们这里的ifname就是"eth0",因此进入else分支。
{
int res;
if (ife_short)
printf(_("Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg/n"));
if (!ifname) {
res = for_all_interfaces(do_if_print, &opt_a);
} else {
struct interface *ife;
ife = lookup_interface(ifname);
res = do_if_fetch(ife);
if (res >= 0)
ife_print(ife);
}
return res;
}
下面深入到lookup_interfaces (lib/interfaces.c)
struct interface *lookup_interface(char *name)
{
struct interface *ife = NULL;
if (if_readlist_proc(name) < 0)
return NULL;
ife = add_interface(name);
return ife;
}
紧跟着是 if_readlist_proc 和 add_interface,一个个来。
static int if_readlist_proc(char *target)一大长串,这该是我们现在遇到的最长的函数了,但是这个方法远远比想象的要简单的多, 文件I/O就占据了老长。这个方法就是读取网络接口的列表,从那儿读取呢?我们一眼就看 到了 ————
{
static int proc_read;
FILE *fh;
char buf[512];
struct interface *ife;
int err;
if (proc_read)
return 0;
if (!target)
proc_read = 1;
fh = fopen(_PATH_PROCNET_DEV, "r");
if (!fh) {
fprintf(stderr, _("Warning: cannot open %s (%s). Limited output./n"),
_PATH_PROCNET_DEV, strerror(errno));
return if_readconf();
}
fgets(buf, sizeof buf, fh); /* eat line */
fgets(buf, sizeof buf, fh);
#if 0 /* pretty, but can't cope with missing fields */
fmt = proc_gen_fmt(_PATH_PROCNET_DEV, 1, fh,
"face", "", /* parsed separately */
"bytes", "%lu",
"packets", "%lu",
"errs", "%lu",
"drop", "%lu",
"fifo", "%lu",
"frame", "%lu",
"compressed", "%lu",
"multicast", "%lu",
"bytes", "%lu",
"packets", "%lu",
"errs", "%lu",
"drop", "%lu",
"fifo", "%lu",
"colls", "%lu",
"carrier", "%lu",
"compressed", "%lu",
NULL);
if (!fmt)
return -1;
#else
procnetdev_vsn = procnetdev_version(buf);
#endif
err = 0;
while (fgets(buf, sizeof buf, fh)) {
char *s, name[IFNAMSIZ];
s = get_name(name, buf);
ife = add_interface(name);
get_dev_fields(s, ife);
ife->statistics_valid = 1;
if (target && !strcmp(target,name))
break;
}
if (ferror(fh)) {
perror(_PATH_PROCNET_DEV);
err = -1;
proc_read = 0;
}
#if 0
free(fmt);
#endif
fclose(fh);
return err;
}
fh = fopen(_PATH_PROCNET_DEV, "r");
在 lib/pathnames.h 中看到其芳踪
#define _PATH_PROCNET_DEV "/proc/net/dev"
首先,探测是否已经检测过 网络接口列表,咱们不作浪费时间的无用功。 然后读取 /proc/net/dev,跳过文件的前两行。为啥?自己瞅瞅就知道了。
wick@ ~: cat /proc/net/dev
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
eth0: 3342410 31393 0 0 0 0 0 0 76194593 52347 0 0 0 0 0 0
eth1: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
接着解析参数,将得到的所有接口加入到列表中去(add_interface),然后依据格式填充 ife结构体(get_dev_fields)。这个结构体是自定义的,可以看到这个结构体是一个链表结 构。get_dev_fields方法填充的是其中一个成员结构体,他在人群中是那么的耀眼:
struct user_net_device_stats stats;
他包含的就是统计信息数据,这些数据的来源就是/proc/net/dev。我们作的仅仅是读取这 个文件信息而已……
你要问,还有其他的信息呢?诸如IP地址,MAC地址,ifconfig如何知道我的网卡是无线有 线的呢?你就需要去看 if_fetch 函数了。从中你可以看到大量的ioctl调用,一切豁然开 朗。
立刻来到内核的领地。
以上总结到统计数据信息的位置坐落于 /proc/net/dev,他是被谁写入的呢?要理解这个问 题,你得先理解 PROC 文件系统。介于时间和能力原因,我只能大概解释, PROC 是内核和 用户空间通讯的又一个接口,PROC fs是一个由软件创建的文件系统,我们需要追根溯源到 创建 /proc/net/dev 的代码文件。
如果要问我是如何找到创建软件的代码?我不知道。我只知道最蠢的方法,google 和 grep。
好吧我是直接grep的……于是找到了 /net/core/dev.c。
/*我们直接看到这段,注释就包含了 /proc/net/dev 信息,由此可见,友好详细的注释是多 么多么的重要啊…… 直接看方法,dev_seq_show 是文件中定义的struct seq_operations dev_seq_ops的成员, 这就提到了 proc提供的seq接口,他使用简单的迭代器方法,使用 seq_open -> read_proc 就可以直接输出 proc文件的接口信息。如果你不熟悉 PROCfs 和 seq_file 接口,这一部 分会有点凹口。
* Called from the PROCfs module. This now uses the new arbitrary sized
* /proc/net interface to create /proc/net/dev
*/
static int dev_seq_show(struct seq_file *seq, void *v)
{
if (v == SEQ_START_TOKEN)
seq_puts(seq, "Inter-| Receive "
" | Transmit/n"
" face |bytes packets errs drop fifo frame "
"compressed multicast|bytes packets errs "
"drop fifo colls carrier compressed/n");
else
dev_seq_printf_stats(seq, v);
return 0;
}
简单来说,当你使用open等系统调用打开 /proc/net/dev 的时候,系统将调用 seq_open 函数;相似的,当你使用read时,内核会使用 seq_show 来将实际数据传递到用户空间。我 们的 `cat /proc/net/dev' 输出,就是这里的 dev_seq_show 打印的。
接着,是 dev_seq_show 中调用的 dev_seq_printf_stats 方法,他仅仅使用到了一个函数。 不用多说,继续深入到dev_get_stats,他将是core模块此行的终点。
/**多么有爱的注释啊……如果定义了设备驱动的 get_stats 方法,就调用 get_stats 调用获 得信息,如果没有,直接返回设备写好的数据结构。
* dev_get_stats - get network device statistics
* @dev: device to get statistics from
*
* Get network statistics from device. The device driver may provide
* its own method by setting dev->netdev_ops->get_stats; otherwise
* the internal statistics structure is used.
*/
const struct net_device_stats *dev_get_stats(struct net_device *dev)
{
const struct net_device_ops *ops = dev->netdev_ops;
if (ops->ndo_get_stats)
return ops->ndo_get_stats(dev);
else
return &dev->stats;
}
EXPORT_SYMBOL(dev_get_stats);
在这里真的需要强调注释的好处,简短的两句话,可以让代码的可读性激增啊!
感动之后,我们进入驱动,我现在使用的网卡是8139,相关的驱动是 8139too.c。
8139too 驱动直接包含在内核里了,我们立刻去关怀他的 get_stats 函数。
static const struct net_device_ops rtl8139_netdev_ops = {
.ndo_open = rtl8139_open,
.ndo_stop = rtl8139_close,
.ndo_get_stats = rtl8139_get_stats,
.ndo_validate_addr = eth_validate_addr,
.ndo_set_mac_address = eth_mac_addr,
.ndo_start_xmit = rtl8139_start_xmit,
.ndo_set_multicast_list = rtl8139_set_rx_mode,
.ndo_do_ioctl = netdev_ioctl,
.ndo_tx_timeout = rtl8139_tx_timeout,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = rtl8139_poll_controller,
#endif
};
哦,简单直接,rtl8139_get_stats
static struct net_device_stats *rtl8139_get_stats (struct net_device *dev)这个函数实体只是重写了 dev->stats.rx_missed_errors,其他的信息我们直接 return。 网卡状态信息 struct net_device_stats 已经在网卡驱动的其他操作方法中即时更新了。
{
struct rtl8139_private *tp = netdev_priv(dev);
void __iomem *ioaddr = tp->mmio_addr;
unsigned long flags;
if (netif_running(dev)) {
spin_lock_irqsave (&tp->lock, flags);
dev->stats.rx_missed_errors += RTL_R32 (RxMissed);
RTL_W32 (RxMissed, 0);
spin_unlock_irqrestore (&tp->lock, flags);
}
return &dev->stats;
}
Ifconfig 使用了简单的文件I/O控制,读取 /proc/net/dev 文件来打印流量统计信息。此 文件是 PROCfs 的一个组成,他的操作方法在代码 net/core/dev.c 中,其中 seq 接口的 show 方法可能调用了网卡设备驱动的 get_status 驱动方法来获得网卡信息。这些信息在 网卡操作中会即时更新。