![](/style/images/good.png)
![](/style/images/bad.png)
netstat源代码调试&原理分析 | Spoock
source link: https://blog.spoock.com/2019/05/26/netstat-learn/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
netstat源代码调试&原理分析
估计平时大部分人都是通过netstat
来查看网络状态,但是事实是netstat
已经逐渐被其他的命令替代,很多新的Linux发行版本中很多都不支持了netstat
。以ubuntu 18.04
为例来进行说明:
1 | ~ netstat |
按照difference between netstat and ss in linux?这篇文章的说法,
NOTE
This program is obsolete. Replacement for netstat is ss. Replacement
for netstat -r is ip route. Replacement for netstat -i is ip -s link.
Replacement for netstat -g is ip maddr.
中文含义就是:netstat
已经过时了,netstat
的部分命令已经被ip
这个命令取代了,当然还有更为强大的ss
。ss
命令用来显示处于活动状态的套接字信息。ss命令可以用来获取socket统计信息,它可以显示和netstat类似的内容。但ss的优势在于它能够显示更多更详细的有关TCP和连接状态的信息,而且比netstat
更快速更高效。netstat
的原理显示网络的原理仅仅只是解析/proc/net/tcp
,所以如果服务器的socket连接数量变得非常大,那么通过netstat
执行速度是非常慢。而ss
采用的是通过tcp_diag
的方式来获取网络信息,tcp_diag
通过netlink的方式从内核拿到网络信息,这也是ss
更高效更全面的原因。
下图就展示了ss
和nestat
在监控上面的区别。
ss
是获取的socket
的信息,而netstat
是通过解析/proc/net/
下面的文件来获取信息包括Sockets
,TCP/UDP
,IP
,Ethernet
信息。
netstat
和ss
的效率的对比,找同一台机器执行:
1 | time ss |
ss
明显比netstat
更加高效.
netstat简介
netstat
是在net-tools
工具包下面的一个工具集,net-tools提供了一份net-tools
的源码,我们通过net-tools
来看看netstat
的实现原理。
netstat源代码调试
下载net-tools
之后,导入到Clion
中,创建CMakeLists.txt
文件,内容如下:
1 | cmake_minimum_required(VERSION 3.13) |
修改根目录下的Makefile
中的59行的编译配置为:
1 | CFLAGS ?= -O0 -g3 |
按照如上图设置自己的编译选项
以上就是搭建netstat
的源代码调试过程。
tcp show
在netstat不需要任何参数的情况,程序首先会运行到2317行的tcp_info()
1 | #if HAVE_AFINET |
跟踪进入到tcp_info()
:
1 | static int tcp_info(void) |
参数的情况如下:
- _PATH_PROCNET_TCP,在
lib/pathnames.h
中定义,是#define _PATH_PROCNET_TCP "/proc/net/tcp"
- _PATH_PROCNET_TCP6, 在
lib/pathnames.h
中定义, 是#define _PATH_PROCNET_TCP6 "/proc/net/tcp6"
tcp_do_one
,函数指针,位于1100行,部分代码如下:1
2
3
4
5
6
7
8
9
10static void tcp_do_one(int lnr, const char *line, const char *prot)
{
unsigned long rxq, txq, time_len, retr, inode;
int num, local_port, rem_port, d, state, uid, timer_run, timeout;
char rem_addr[128], local_addr[128], timers[64];
const struct aftype *ap;
struct sockaddr_storage localsas, remsas;
struct sockaddr_in *localaddr = (struct sockaddr_in *)&localsas;
struct sockaddr_in *remaddr = (struct sockaddr_in *)&remsas;
......tcp_do_one()
就是用来解析/proc/net/tcp
和/proc/net/tcp6
每一行的含义的,关于/proc/net/tcp
的每一行的含义可以参考之前写过的osquery源码解读之分析process_open_socket中的扩展章节。
INFO_GUTS6
1 | #define INFO_GUTS6(file,file6,name,proc,prot4,prot6) \ |
INFO_GUTS6
采用了#define
的方式进行定义,最终根据是flag_inet
(IPv4)或者flag_inet6
(IPv6)的选项分别调用不同的函数,我们以INFO_GUTS1(file,name,proc,prot4)
进一步分析。
INFO_GUTS1
1 | #define INFO_GUTS1(file,name,proc,prot) \ |
rocinfo = proc_fopen((file))
获取/proc/net/tcp
的文件句柄fgets(buffer, sizeof(buffer), procinfo)
解析文件内容并将每一行的内容存储在buffer中(proc)(lnr++, buffer,prot)
,利用(proc)
函数解析buffer。(proc)
就是前面说明的tcp_do_one()
函数
tcp_do_one
以" 14: 020110AC:B498 CF0DE1B9:4362 06 00000000:00000000 03:000001B2 00000000 0 0 0 3 0000000000000000
这一行为例来说明tcp_do_one()
函数的执行过程。
由于分析是Ipv4
,所以会跳过#if HAVE_AFINET6
这段代码。之后执行:
1 | num = sscanf(line, |
解析数据,并将每一列的数据分别填充到对应的字段上面。分析一下其中的每个字段的定义:
1 | char rem_addr[128], local_addr[128], timers[64]; |
在Linux
中sockaddr_in
和sockaddr_storage
的定义如下:
1 | struct sockaddr { |
之后代码继续执行:
1 | sscanf(local_addr, "%X", &localaddr->sin_addr.s_addr); |
将local_addr
使用sscanf(,"%X")
得到对应的十六进制,保存到&localaddr->sin_addr.s_addr
(即in_addr
结构体中的s_addr
)中,同理&remaddr->sin_addr.s_addr
。运行结果如下所示:
addr_do_one
1 | addr_do_one(local_addr, sizeof(local_addr), 22, ap, &localsas, local_port, "tcp"); |
程序继续执行,最终会执行到addr_do_one()
函数,用于解析本地IP地址和端口,以及远程IP地址和端口。
1 | static void addr_do_one(char *buf, size_t buf_len, size_t short_len, const struct aftype *ap, |
saddr = ap->sprint(addr, flag_not & FLAG_NUM_HOST);
这个表示是否需要将addr
转换为域名的形式。由于addr
值是127.0.0.1
,转换之后得到的就是localhost
,其中FLAG_NUM_HOST
的就等价于--numeric-hosts
的选项。sport = get_sname(htons(port), proto, flag_not & FLAG_NUM_PORT);
,port
无法无法转换,其中的FLAG_NUM_PORT
就等价于--numeric-ports
这个选项。!flag_wide && (addr_len + port_len > short_len
这个代码的含义是判断是否需要对IP
和PORT
进行截断。其中flag_wide
的等同于-W, --wide don't truncate IP addresses
。而short_len
长度是22.snprintf(buf, buf_len, "%s:%s", saddr, sport);
,将IP:PORT
赋值给buf
.
output
最终程序执行
1 | printf("%-4s %6ld %6ld %-*s %-*s %-11s", |
按照制定的格式解析,输出结果
finish_this_one
最终程序会执行finish_this_one(uid,inode,timers);
.
1 | static void finish_this_one(int uid, unsigned long inode, const char *timers) |
flag_exp
等同于-e
的参数。-e, --extend display other/more information
.举例如下:1
2
3
4
5
6
7netstat -e
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode
tcp 0 0 localhost:6379 172.16.1.200:46702 ESTABLISHED redis 437788048
netstat
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 localhost:6379 172.16.1.200:46702 ESTABLISHED发现使用
-e
参数会多显示User
和Inode
号码。而在本例中还可以如果用户名不存在,则显示uid
getpwuidflag_prg
等同于-p, --programs display PID/Program name for sockets
.举例如下:1
2
3
4
5
6
7netstat -pe
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode PID/Program name
tcp 0 0 localhost:6379 172.16.1.200:34062 ESTABLISHED redis 437672000 6017/redis-server *
netstat -e
Proto Recv-Q Send-Q Local Address Foreign Address State User Inode
tcp 0 0 localhost:6379 172.16.1.200:46702 ESTABLISHED redis 437788048可以看到是通过
prg_cache_get(inode)
,inode来找到对应的PID和进程信息;flag_selinux
等同于-Z, --context display SELinux security context for sockets
prg_cache_get
对于上面的通过inode
找到对应进程的方法非常的好奇,于是去追踪prg_cache_get()
函数的实现。
1 | #define PRG_HASH_SIZE 211 |
在prg_hash
中存储了所有的inode编号与program
的对应关系,所以当给定一个inode编号时就能够找到对应的程序名称。那么prg_hash
又是如何初始化的呢?
prg_cache_load
我们使用debug模式,加入-p
的运行参数:
程序会运行到2289行的prg_cache_load(); 进入到prg_cache_load()函数中.
由于整个函数的代码较长,拆分来分析.
1 | #define PATH_PROC "/proc" |
dirproc=opendir(PATH_PROC);errno = 0, direproc = readdir(dirproc)
遍历/proc拿到所有的pidprocfdlen = snprintf(line,sizeof(line),PATH_PROC_X_FD,direproc→d_name);
遍历所有的/proc/pid拿到所有进程的fddirfd = opendir(line);
得到/proc/pid/fd的文件句柄
获取inode
1 | while ((direfd = readdir(dirfd))) { |
memcpy(line + procfdlen - PATH_FD_SUFFl, PATH_FD_SUFF "/",PATH_FD_SUFFl + 1);safe_strncpy(line + procfdlen + 1, direfd->d_name, sizeof(line) - procfdlen - 1);
得到遍历之后的fd信息,比如/proc/pid/fdlnamelen = readlink(line, lname, sizeof(lname) - 1);
得到fd所指向的link,因为通常情况下fd一般都是链接,要么是socket链接要么是pipe链接.如下所示:1
2
3
4
5
6
7
8$ ls -al /proc/1289/fd
total 0
dr-x------ 2 username username 0 May 25 15:45 .
dr-xr-xr-x 9 username username 0 May 25 09:11 ..
lr-x------ 1 username username 64 May 25 16:23 0 -> 'pipe:[365366]'
l-wx------ 1 username username 64 May 25 16:23 1 -> 'pipe:[365367]'
l-wx------ 1 username username 64 May 25 16:23 2 -> 'pipe:[365368]'
lr-x------ 1 username username 64 May 25 16:23 3 -> /proc/uptime通过extract_type_1_socket_inode获取到link中对应的inode编号.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25#define PRG_SOCKET_PFX "socket:["
#define PRG_SOCKET_PFXl (strlen(PRG_SOCKET_PFX))
static int extract_type_1_socket_inode(const char lname[], unsigned long * inode_p) {
/* If lname is of the form "socket:[12345]", extract the "12345"
as *inode_p. Otherwise, return -1 as *inode_p.
*/
// 判断长度是否小于 strlen(socket:[)+3
if (strlen(lname) < PRG_SOCKET_PFXl+3) return(-1);
//函数说明:memcmp()用来比较s1 和s2 所指的内存区间前n 个字符。
// 判断lname是否以 socket:[ 开头
if (memcmp(lname, PRG_SOCKET_PFX, PRG_SOCKET_PFXl)) return(-1);
if (lname[strlen(lname)-1] != ']') return(-1); {
char inode_str[strlen(lname + 1)]; /* e.g. "12345" */
const int inode_str_len = strlen(lname) - PRG_SOCKET_PFXl - 1;
char *serr;
// 获取到inode的编号
strncpy(inode_str, lname+PRG_SOCKET_PFXl, inode_str_len);
inode_str[inode_str_len] = '\0';
*inode_p = strtoul(inode_str, &serr, 0);
if (!serr || *serr || *inode_p == ~0)
return(-1);
}获取程序对应的cmdline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19if (!cmdlp) {
if (procfdlen - PATH_FD_SUFFl + PATH_CMDLINEl >=sizeof(line) - 5)
continue;
safe_strncpy(line + procfdlen - PATH_FD_SUFFl, PATH_CMDLINE,sizeof(line) - procfdlen + PATH_FD_SUFFl);
fd = open(line, O_RDONLY);
if (fd < 0)
continue;
cmdllen = read(fd, cmdlbuf, sizeof(cmdlbuf) - 1);
if (close(fd))
continue;
if (cmdllen == -1)
continue;
if (cmdllen < sizeof(cmdlbuf) - 1)
cmdlbuf[cmdllen]='\0';
if (cmdlbuf[0] == '/' && (cmdlp = strrchr(cmdlbuf, '/')))
cmdlp++;
else
cmdlp = cmdlbuf;
}由于cmdline是可以直接读取的,所以并不需要像读取fd那样借助与readlink()函数,直接通过 read(fd, cmdlbuf, sizeof(cmdlbuf) - 1) 即可读取文件内容.
snprintf(finbuf, sizeof(finbuf), "%s/%s", direproc->d_name, cmdlp);
拼接pid和cmdlp,最终得到的就是类似与 6017/redis-server * 这样的效果- 最终程序调用
prg_cache_add(inode, finbuf, "-");
将解析得到的inode和finbuf 加入到缓存中.
prg_cache_add
1 | #define PRG_HASH_SIZE 211 |
unsigned hi = PRG_HASHIT(inode);
使用inode整除211得到作为hash值for (pnp = prg_hash + hi; (pn = *pnp); pnp = &pn->next)
由于prg_hash是一个链表结构,所以通过for循环找到链表的结尾;pn = *pnp;pn->next = NULL;pn->inode = inode;safe_strncpy(pn->name, name, sizeof(pn→name));
为新的inode赋值并将其加入到链表的末尾;
所以prg_node是一个全局变量,是一个链表结果,保存了inode编号与pid/cmdline之间的对应关系;
prg_cache_get
1 | static const char *prg_cache_get(unsigned long inode) |
分析完毕prg_cache_add()之后,看prg_cache_get()就很简单了.
unsigned hi = PRG_HASHIT(inode);
通过inode号拿到hash值for (pn = prg_hash[hi]; pn; pn = pn->next)
遍历prg_hash链表中的每一个节点,如果遍历的inode与目标的inode相符就返回对应的信息.
通过对netstat的一个简单的分析,可以发现其实netstat就是通过遍历/proc目录下的目录或者是文件来获取对应的信息.如果在一个网络进程频繁关闭打开关闭,那么使用netstat显然是相当耗时的.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK