前言
本篇缘起是我要实现类似ss -i
的功能,通过netlink获取系统中的所有socket信息
代码如下
//发送tcpdiag的数据
sendto(netlinkdf, msg,...)
//sleep足够时间,使得内核处理
//因为每次都读取少部分数据,以为内核没来得及处理
sleep(10)
char buffer[10000]
len = recvfrom(buffer, sizeof(buffer), 0);
上面流程中,recvfrom
返回的字节大小,小于buffer大小,理应我们认为是收全了的,但是实际情况就是,buffer中的数据,实际上只有非常小的一部分socket信息
recvfrom/recvmsg
首先还是要看下recvmsg
做了哪些事情,注意recvfrom
最后还是调用了recvmsg
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags, struct sockaddr __user *, addr,
int __user *, addr_len)
{
...
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_iovlen = 1;
msg.msg_iov = &iov;
iov.iov_len = size;
iov.iov_base = ubuf;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = sizeof(address);
err = sock_recvmsg(sock, &msg, size, flags);
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
if (err2 < 0)
err = err2;
}
fput_light(sock->file, fput_needed);
out:
return err;
}
上篇 讲过,netlink注册的一些函数调用栈是这样的
sock_recvmsg->netlink_recvmsg
核心就是netlink_recvmsg
,核心流程如下
static int netlink_recvmsg(struct kiocb *kiocb, struct socket *sock,
{
struct sock *sk = sock->sk;
struct netlink_sock *nlk = nlk_sk(sk);
//从netlink的socket中,获取接收队列中的数据,这个队列其实就是socket信息
//socket信息,首先是当你调用sendto(msg)发送tcp_diag请求的时候
//就已经同步的放到了netlink的socket中了
skb = skb_recv_datagram(sk, flags, noblock, &err);
//copy到用户态
err = skb_copy_datagram_iovec(data_skb, 0, msg->msg_iov, copied);
//MSG_TRUNC告诉内核recvfrom的返回值大小,是实际skb的大小,而不是因为入参buffer太小从而返回buffer大小
//
if (flags & MSG_TRUNC)
copied = data_skb->len;
//释放skb
skb_free_datagram(sk, skb);
//核心在这里,接着获取剩余的socket信息
if (nlk->cb && atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf / 2) {
ret = netlink_dump(sk);
if (ret) {
sk->sk_err = ret;
sk->sk_error_report(sk);
}
}
}
上面的流程有几个核心点,首先,对于tcp_diag流程而言,上篇 讲过当你sendto(msg)发送到内核时,实际调用了netlink_dump->tcp_diag_dump
函数,来循环获取本机所有socket信息,这个过程是同步的。但是tcp_diag_dump
之所以高性能很关键的一点就是因为它本身不会循环获取完所有的socket,这会导致加锁时间太长。所以,下一次获取剩余数据的时机
其实就是当用户调用recvfrom/recvmsg
的时候。那么,怎么避免不重复获取socket,靠的是nlk->cb
中保存了之前的信息。
所以,当我们执行一次len = recvfrom(buffer, sizeof(buffer), 0);
,如果tcp_diag
模块还没循环完成,即nlk->cb
还有值,那么虽然len返回的大小小于buffer的大小,但是实际上,本次recvfrom
操作之时,内核因为又跑了一遍netlink_dump->tcp_diag_dump
,导致netlink的socket的接受队列里面,实际还是有数据的,而这些数据,并没有通过当前这次recvfrom
反馈出来,这导致用户态的代码就很奇葩。
还有一点很关键,如果我们的用户态代码的buffer太小,例如buffer大小是1k,但是skb是3k,由于调用完这次recvfrom
,skb就被释放了(没加MSG_PEEK的话),本次buffer是不全的,下次recvfrom
的数据实际上是新一次的dump出来的。从而连netlink格式都是错误的。
for() {
//循环多次,len返回一直有数据,但是大小可能一直小于buffer大小
len = recvfrom(buffer, sizeof(buffer), 0);
}
这简直是尿频尿不尽。
所以netlink接受数据的流程,其实是很奇葩的。那应用层该怎么办呢?实际上就是循环读,然后判断netlink的done数据包,然后停止读。以 iproute2 为例,rtnl_recvmsg->__rtnl_recvmsg
封装了recvmsg
函数
static int rtnl_recvmsg(int fd, struct msghdr *msg, char **answer)
{
struct iovec *iov = msg->msg_iov;
char *buf;
int len;
iov->iov_base = NULL;
iov->iov_len = 0;
//获取实际大小
len = __rtnl_recvmsg(fd, msg, MSG_PEEK | MSG_TRUNC);
if (len < 0)
return len;
if (len < 32768)
len = 32768;
//开辟实际大小
buf = malloc(len);
if (!buf) {
fprintf(stderr, "malloc error: not enough buffer\n");
return -ENOMEM;
}
iov->iov_base = buf;
iov->iov_len = len;
//消费一次skb,skb大小就是len
len = __rtnl_recvmsg(fd, msg, 0);
if (len < 0) {
free(buf);
return len;
}
if (answer)
*answer = buf;
else
free(buf);
return len;
}
而应用层是这么读的
static int rtnl_dump_filter_l(struct rtnl_handle *rth,
const struct rtnl_dump_filter_arg *arg)
{
struct sockaddr_nl nladdr;
struct iovec iov;
struct msghdr msg = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
char *buf;
int dump_intr = 0;
while (1) {
int status;
const struct rtnl_dump_filter_arg *a;
int found_done = 0;
int msglen = 0;
status = rtnl_recvmsg(rth->fd, &msg, &buf);
while (NLMSG_OK(h, msglen)) {
//不断判断netlink messagede type,为done就break
if (h->nlmsg_type == NLMSG_DONE) {
err = rtnl_dump_done(h, a);
found_done = 1;
break; /* process next filter */
}
if (h->nlmsg_type == NLMSG_ERROR) {
err = rtnl_dump_error(rth, h, a);
if (err < 0) {
free(buf);
return -1;
}
goto skip_it;
}
if (!rth->dump_fp) {
err = a->filter(h, a->arg1);
if (err < 0) {
free(buf);
return err;
}
}
skip_it:
h = NLMSG_NEXT(h, msglen);
}
//读到了 NLMSG_DONE 就退出
if (found_done) {
if (dump_intr)
fprintf(stderr,
"Dump was interrupted and may be inconsistent.\n");
return 0;
}
if (msg.msg_flags & MSG_TRUNC) {
fprintf(stderr, "Message truncated\n");
continue;
}
if (msglen) {
fprintf(stderr, "!!!Remnant of size %d\n", msglen);
exit(1);
}
}
}