之前想找一个可以监视系统进程网络流量的工具,网上都推荐使用nethogs,我在自己的linux系统上安装了nethogs,感觉还挺好用的,但是当我在公司的服务器上运行的时候就会出现各种问题,有时候会卡死,有时候虽然不卡死,显示的信息也不太对,奇奇怪怪,但是又很想用这个功能,于是阅读了下nethogs的源码,发现了一些问题,应该是该工具存在一些局限性。
我所谓的局限性主要体现在 当服务器上有大量的网络连接,也就是短连接比较多的情况,nethogs就无法正常工作。而我使用的服务器恰恰有比较多的短连接,导致该工具无法使用,下面说下产生这种情况的原因。
该工具大体的思路是通过libpcap库,抓取网络包,并处理抓取的包信息。
首先判断该包是否为已存在连接的包:
while (current != NULL) {
/* the reference packet is always *outgoing* */
if (packet->match(current->getVal()->refpacket)) {
return current->getVal();
}
current = current->getNext();
}
bool Packet::match(Packet *other) {
return sa_family == other->sa_family && (sport == other->sport) &&
(dport == other->dport) &&
(sa_family == AF_INET
? (sameinaddr(sip, other->sip)) && (sameinaddr(dip, other->dip))
: (samein6addr(sip6, other->sip6)) &&
(samein6addr(dip6, other->dip6)));
}
上面的while循环就是查找所有已经存在的连接,方法是比较协议族和四元组。如果找不到,还有一种情况,查找源地址是否存在,通过比较端口号和IP地址。
while (current != NULL) {
/* the reference packet is always outgoing */
if (packet->matchSource(current->getVal()->refpacket)) {
return current->getVal();
}
bool Packet::matchSource(Packet *other) {
return (sport == other->sport) && (sameinaddr(sip, other->sip));
}
如果通过上面的查找找到了,万事大吉,直接把这个新的包添加到该连接即可。如果connection不等于NULL,表示找到了,将新的包添加到该连接。如果找不到,就比较麻烦,说明是一个新的连接,由于nethogs是用来监视进程流量的,所以它必须知道该连接是属于哪个进程。所以要通过getProcess获取进程信息,但是该信息的获取可能会比较花费时间。
Connection *connection = findConnection(packet, IPPROTO_TCP);
if (connection != NULL) {
/* add packet to the connection */
connection->add(packet);
} else {
/* else: unknown connection, create new */
connection = new Connection(packet);
getProcess(connection, args->device);
}
因为getProcess比较重要,分开说明。第一步先判断该连接是否已经存在,方法是查找该连接的inode值,gethashstring即获取四元组组成的一个字符串,每个对应一个inode,获取方法一会再说。
unsigned long inode = conninode[connection->refpacket->gethashstring()];
gethashstring:
hashstring = (char *)malloc(HASHKEYSIZE * sizeof(char));
char *local_string = (char *)malloc(50);
char *remote_string = (char *)malloc(50);
if (sa_family == AF_INET) {
inet_ntop(sa_family, &sip, local_string, 49);
inet_ntop(sa_family, &dip, remote_string, 49);
} else {
inet_ntop(sa_family, &sip6, local_string, 49);
inet_ntop(sa_family, &dip6, remote_string, 49);
}
if (Outgoing()) {
snprintf(hashstring, HASHKEYSIZE * sizeof(char), "%s:%d-%s:%d",
local_string, sport, remote_string, dport);
} else {
snprintf(hashstring, HASHKEYSIZE * sizeof(char), "%s:%d-%s:%d",
remote_string, dport, local_string, sport);
}
因为刚才判断新抓到的包是属于新的连接的,所以这里的inode应该不存在,也就是inode等于0。如果inode为0,要先获取inode值,获取方法为:
void reread_mapping() {
DIR *proc = opendir("/proc");
if (proc == 0) {
std::cerr << "Error reading /proc, needed to get inode-to-pid-maping\n";
exit(1);
}
dirent *entry;
while ((entry = readdir(proc))) {
if (entry->d_type != DT_DIR)
continue;
if (!is_number(entry->d_name))
continue;
get_info_for_pid(entry->d_name);
}
closedir(proc);
}
即遍历整个/proc目录,查找所有进程的inode值,即查找每个进程的fd目录,套接字类型的描述符,该值就是下图中socket:[]中的那串数字。这样就获取了进程和inode的对照关系。
linux0@ubuntu:/proc/5544/fd$ ls -ltr
total 0
lrwx------ 1 linux0 linux0 64 Dec 5 09:56 3 -> 'socket:[89478]'
lrwx------ 1 linux0 linux0 64 Dec 5 09:56 2 -> /dev/pts/1
lrwx------ 1 linux0 linux0 64 Dec 5 09:56 1 -> /dev/pts/1
lrwx------ 1 linux0 linux0 64 Dec 5 09:56 0 -> /dev/pts/1
知道了进程和inode的对照关系,还要知道inode和连接的 关系,所以refreshconninode获取当前所有的连接,方法是读取/proc/net/tcp和/proc/net/tcp6,获取四元组信息和inode。
if (!addprocinfo("/proc/net/tcp")) {
std::cout << "Error: couldn't open /proc/net/tcp\n";
exit(0);
}
addprocinfo("/proc/net/tcp6");
int matches = sscanf(buffer,
"%*d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %*X "
"%*X:%*X %*X:%*X %*X %*d %*d %ld %*512s\n",
local_addr, &local_port, rem_addr, &rem_port, &inode);
char *hashkey = (char *)malloc(HASHKEYSIZE * sizeof(char));
char *local_string = (char *)malloc(50);
char *remote_string = (char *)malloc(50);
inet_ntop(sa_family, &result_addr_local, local_string, 49);
inet_ntop(sa_family, &result_addr_remote, remote_string, 49);
snprintf(hashkey, HASHKEYSIZE * sizeof(char), "%s:%d-%s:%d", local_string,
local_port, remote_string, rem_port);
free(local_string);
conninode[hashkey] = inode;
buffer就是/proc/net/tcp和/proc/net/tcp6中的一行数据。将conninode更新为最新的四元组和inode对照表,包含当前的所有inode信息。这样就可以通过inode,间接找到进程和连接四元组的关系。知道新的包是属于哪个进程的。
通过上面的一系列操作,可以发现,假设服务器以长连接为主,那么整个服务器的进程与包的关系是比较容易判断的,因为连接比较固定,nethogs也就可以正常发挥作用。但是如果服务器存在比较多的短连接,那么每次这些新加入的短连接与进程的对应关系都是不明确的,都得经过上面分析的查找过程,特别是当/proc/net/tcp文件比较大的时候,该查找是非常缓慢的,导致的结果就是nethogs把绝大多数的时间都花在了查找连接与进程的关系上,而不是处理流量大小上,这种情况下, nethogs无法正常工作。
后面因为感觉这种进程流量监视的方式太过麻烦,最关键的是nethogs无法对udp做出有效处理,最终我选择自己根据nethogs写一个只监视一个进程流量的工具,可以处理tcp和udp,虽然可能太过简单存在很多问题,但是在我一般的测试中,感觉基本可用,主要也基本符合使用需求。可以同时检测tcp和udp。