前言
本篇缘起是我要实现类似ss -i 的功能,通过netlink获取系统中的所有socket信息 代码如下
sendto(netlinkdf, msg,...)
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);
skb = skb_recv_datagram(sk, flags, noblock, &err);
err = skb_copy_datagram_iovec(data_skb, 0, msg->msg_iov, copied);
if (flags & MSG_TRUNC)
copied = data_skb->len;
skb_free_datagram(sk, skb);
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 = 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;
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)) {
if (h->nlmsg_type == NLMSG_DONE) {
err = rtnl_dump_done(h, a);
found_done = 1;
break;
}
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);
}
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);
}
}
}
|