Linux 内核的许多接口都没有详细记录。或者,即使它们看起来有很好的文档记录,它们也可能非常复杂,这可能会让人很难理解接口的功能性属性,或者通常更难理解接口的非功能性属性是什么。
因此,我对任何想要深入了解内核 API 或需要使用内核 API 创建高性能应用程序的人的建议是,需要能够使用内核代码才能取得成功。
在这种情况下,提问者想要了解通过共享内存接口(数据包 mmap)向内核发送原始帧的性能特征。
Linux 文档是here https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt。它有一个指向“如何做”的陈旧链接,现在可以找到该链接here https://sites.google.com/site/packetmmap/并包括一份副本packet_mmap.c
(我有一个稍微不同的版本here https://github.com/jrdriscoll/packet_mmap.
该文档主要面向阅读,这是使用数据包 mmap 的典型用例:从接口有效读取原始帧,e.g.有效地从高速接口捕获数据包,几乎没有或没有丢失。
然而OP对高性能感兴趣writing,这是一个不太常见的用例,但对于流量生成器/模拟器可能有用,这似乎是 OP 想要用它做的事情。值得庆幸的是,“如何做”都是关于编写框架的。
即便如此,关于其实际工作原理的信息也很少,并且没有任何明显的帮助来回答 OP 的问题,即为什么使用数据包 mmap 似乎并不比不使用它而是一次发送一帧更快。
值得庆幸的是,内核源代码是开源的并且索引良好,因此我们可以求助于源代码来帮助我们获得问题的答案。
为了找到相关的内核代码,您可以搜索几个关键字,但是PACKET_TX_RING
作为此功能独有的套接字选项而脱颖而出。在互联网上搜索“PACKET_TX_RING linux cross reference”会发现少量参考资料,包括af_packet.c
,经过一点检查似乎是所有的实施AF_PACKET
功能,包括数据包 mmap。
翻翻af_packet.c
,看来使用数据包 mmap 进行传输的核心工作发生在tpacket_snd()
。但这是正确的吗?我们如何判断这是否与我们认为的有关?
从内核获取此类信息的一个非常强大的工具是系统点击 https://sourceware.org/systemtap/。 (使用这个需要为你的内核安装调试符号。我碰巧使用的是 Ubuntu,并且this https://wiki.ubuntu.com/Kernel/Systemtap是让 SystemTap 在 Ubuntu 上运行的秘诀。)
一旦您使用 SystemTap,您就可以将 SystemTap 与packet_mmap.c
看看是否tpacket_snd()
甚至可以通过在内核函数上安装探测器来调用tpacket_snd
,然后运行packet_mmap
通过共享 TX 环发送帧:
$ sudo stap -e 'probe kernel.function("tpacket_snd") { printf("W00T!\n"); }' &
[1] 19961
$ sudo ./packet_mmap -c 1 eth0
[...]
STARTING TEST:
data offset = 32 bytes
start fill() thread
send 1 packets (+150 bytes)
end of task fill()
Loop until queue empty (0)
END (number of error:0)
W00T!
W00T!
W00T!我们正在做某件事;tpacket_snd
实际上正在被调用。但我们的胜利将是短暂的。如果我们继续尝试从现有内核构建中获取更多信息,SystemTap 会抱怨它找不到我们想要检查的变量,并且函数参数将打印出以下值:?
or ERROR
。这是因为内核是经过优化编译的,并且包含了所有功能AF_PACKET
在单个翻译单元中定义af_packet.c
;许多函数都是由编译器内联的,实际上会丢失局部变量和参数。
为了从中窥探到更多信息af_packet.c
,我们将不得不构建一个内核版本,其中af_packet.c
是在没有优化的情况下构建的。看here https://github.com/jrdriscoll/packet_mmap寻求一些指导。我会等待。
好的,希望这不是太难,并且您已经成功启动了 SystemTap 可以从中获取大量有用信息的内核。请记住,这个内核版本只是为了帮助我们弄清楚数据包 mmap 是如何工作的。我们无法从该内核获得任何直接的性能信息,因为af_packet.c
已建成without优化。如果事实证明我们需要获取有关优化版本的行为方式的信息,我们可以使用以下命令构建另一个内核af_packet.c
经过优化编译,但添加了一些检测代码,这些代码通过不会优化的变量公开信息,以便 SystemTap 可以看到它们。
所以让我们用它来获取一些信息。看一眼status.stp https://github.com/jrdriscoll/packet_mmap/blob/master/status.stp:
# This is specific to net/packet/af_packet.c 3.13.0-116
function print_ts() {
ts = gettimeofday_us();
printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}
# 325 static void __packet_set_status(struct packet_sock *po, void *frame, int status)
# 326 {
# 327 union tpacket_uhdr h;
# 328
# 329 h.raw = frame;
# 330 switch (po->tp_version) {
# 331 case TPACKET_V1:
# 332 h.h1->tp_status = status;
# 333 flush_dcache_page(pgv_to_page(&h.h1->tp_status));
# 334 break;
# 335 case TPACKET_V2:
# 336 h.h2->tp_status = status;
# 337 flush_dcache_page(pgv_to_page(&h.h2->tp_status));
# 338 break;
# 339 case TPACKET_V3:
# 340 default:
# 341 WARN(1, "TPACKET version not supported.\n");
# 342 BUG();
# 343 }
# 344
# 345 smp_wmb();
# 346 }
probe kernel.statement("__packet_set_status@net/packet/af_packet.c:334") {
print_ts();
printf("SET(V1): %d (0x%.16x)\n", $status, $frame);
}
probe kernel.statement("__packet_set_status@net/packet/af_packet.c:338") {
print_ts();
printf("SET(V2): %d\n", $status);
}
# 348 static int __packet_get_status(struct packet_sock *po, void *frame)
# 349 {
# 350 union tpacket_uhdr h;
# 351
# 352 smp_rmb();
# 353
# 354 h.raw = frame;
# 355 switch (po->tp_version) {
# 356 case TPACKET_V1:
# 357 flush_dcache_page(pgv_to_page(&h.h1->tp_status));
# 358 return h.h1->tp_status;
# 359 case TPACKET_V2:
# 360 flush_dcache_page(pgv_to_page(&h.h2->tp_status));
# 361 return h.h2->tp_status;
# 362 case TPACKET_V3:
# 363 default:
# 364 WARN(1, "TPACKET version not supported.\n");
# 365 BUG();
# 366 return 0;
# 367 }
# 368 }
probe kernel.statement("__packet_get_status@net/packet/af_packet.c:358") {
print_ts();
printf("GET(V1): %d (0x%.16x)\n", $h->h1->tp_status, $frame);
}
probe kernel.statement("__packet_get_status@net/packet/af_packet.c:361") {
print_ts();
printf("GET(V2): %d\n", $h->h2->tp_status);
}
# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2136 do {
# 2137 ph = packet_current_frame(po, &po->tx_ring,
# 2138 TP_STATUS_SEND_REQUEST);
# 2139
# 2140 if (unlikely(ph == NULL)) {
# 2141 schedule();
# 2142 continue;
# 2143 }
# 2144
# 2145 status = TP_STATUS_SEND_REQUEST;
# 2146 hlen = LL_RESERVED_SPACE(dev);
# 2147 tlen = dev->needed_tailroom;
# 2148 skb = sock_alloc_send_skb(&po->sk,
# 2149 hlen + tlen + sizeof(struct sockaddr_ll),
# 2150 0, &err);
# 2151
# 2152 if (unlikely(skb == NULL))
# 2153 goto out_status;
# 2154
# 2155 tp_len = tpacket_fill_skb(po, skb, ph, dev, size_max, proto,
# 2156 addr, hlen);
# [...]
# 2176 skb->destructor = tpacket_destruct_skb;
# 2177 __packet_set_status(po, ph, TP_STATUS_SENDING);
# 2178 atomic_inc(&po->tx_ring.pending);
# 2179
# 2180 status = TP_STATUS_SEND_REQUEST;
# 2181 err = dev_queue_xmit(skb);
# 2182 if (unlikely(err > 0)) {
# [...]
# 2195 }
# 2196 packet_increment_head(&po->tx_ring);
# 2197 len_sum += tp_len;
# 2198 } while (likely((ph != NULL) ||
# 2199 ((!(msg->msg_flags & MSG_DONTWAIT)) &&
# 2200 (atomic_read(&po->tx_ring.pending))))
# 2201 );
# 2202
# [...]
# 2213 return err;
# 2214 }
probe kernel.function("tpacket_snd") {
print_ts();
printf("tpacket_snd: args(%s)\n", $$parms);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2140") {
print_ts();
printf("tpacket_snd:2140: current frame ph = 0x%.16x\n", $ph);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2141") {
print_ts();
printf("tpacket_snd:2141: (ph==NULL) --> schedule()\n");
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2142") {
print_ts();
printf("tpacket_snd:2142: flags 0x%x, pending %d\n",
$msg->msg_flags, $po->tx_ring->pending->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2197") {
print_ts();
printf("tpacket_snd:2197: flags 0x%x, pending %d\n",
$msg->msg_flags, $po->tx_ring->pending->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d)\n", $err);
}
# 1946 static void tpacket_destruct_skb(struct sk_buff *skb)
# 1947 {
# 1948 struct packet_sock *po = pkt_sk(skb->sk);
# 1949 void *ph;
# 1950
# 1951 if (likely(po->tx_ring.pg_vec)) {
# 1952 __u32 ts;
# 1953
# 1954 ph = skb_shinfo(skb)->destructor_arg;
# 1955 BUG_ON(atomic_read(&po->tx_ring.pending) == 0);
# 1956 atomic_dec(&po->tx_ring.pending);
# 1957
# 1958 ts = __packet_set_timestamp(po, ph, skb);
# 1959 __packet_set_status(po, ph, TP_STATUS_AVAILABLE | ts);
# 1960 }
# 1961
# 1962 sock_wfree(skb);
# 1963 }
probe kernel.statement("tpacket_destruct_skb@net/packet/af_packet.c:1959") {
print_ts();
printf("tpacket_destruct_skb:1959: ph = 0x%.16x, ts = 0x%x, pending %d\n",
$ph, $ts, $po->tx_ring->pending->counter);
}
这定义了一个函数(print_ts
以微秒分辨率打印出 UNIX 纪元时间)和一些探测器。
首先,我们定义探针,以便在 tx_ring 中的数据包设置或读取状态时打印出信息。接下来我们定义探针的调用和返回tpacket_snd
并在 内的点do {...} while (...)
循环处理 tx_ring 中的数据包。最后我们向 skb 析构函数添加一个探针。
我们可以使用以下命令启动 SystemTap 脚本sudo stap status.stp
。然后运行sudo packet_mmap -c 2 <interface>
通过接口发送 2 帧。以下是我从 SystemTap 脚本获得的输出:
[1492581245.839850] tpacket_snd: args(po=0xffff88016720ee38 msg=0x14)
[1492581245.839865] GET(V1): 1 (0xffff880241202000)
[1492581245.839873] tpacket_snd:2140: current frame ph = 0xffff880241202000
[1492581245.839887] SET(V1): 2 (0xffff880241202000)
[1492581245.839918] tpacket_snd:2197: flags 0x40, pending 1
[1492581245.839923] GET(V1): 1 (0xffff88013499c000)
[1492581245.839929] tpacket_snd:2140: current frame ph = 0xffff88013499c000
[1492581245.839935] SET(V1): 2 (0xffff88013499c000)
[1492581245.839946] tpacket_snd:2197: flags 0x40, pending 2
[1492581245.839951] GET(V1): 0 (0xffff88013499e000)
[1492581245.839957] tpacket_snd:2140: current frame ph = 0x0000000000000000
[1492581245.839961] tpacket_snd:2141: (ph==NULL) --> schedule()
[1492581245.839977] tpacket_snd:2142: flags 0x40, pending 2
[1492581245.839984] tpacket_snd: return(300)
[1492581245.840077] tpacket_snd: args(po=0x0 msg=0x14)
[1492581245.840089] GET(V1): 0 (0xffff88013499e000)
[1492581245.840098] tpacket_snd:2140: current frame ph = 0x0000000000000000
[1492581245.840093] tpacket_destruct_skb:1959: ph = 0xffff880241202000, ts = 0x0, pending 1
[1492581245.840102] tpacket_snd:2141: (ph==NULL) --> schedule()
[1492581245.840104] SET(V1): 0 (0xffff880241202000)
[1492581245.840112] tpacket_snd:2142: flags 0x40, pending 1
[1492581245.840116] tpacket_destruct_skb:1959: ph = 0xffff88013499c000, ts = 0x0, pending 0
[1492581245.840119] tpacket_snd: return(0)
[1492581245.840123] SET(V1): 0 (0xffff88013499c000)
这是网络捕获:
SystemTap 输出中有很多有用的信息。我们可以看到tpacket_snd
获取环中第一帧的状态(TP_STATUS_SEND_REQUEST http://lxr.free-electrons.com/source/include/uapi/linux/if_packet.h#L114为 1),然后将其设置为TP_STATUS_SENDING
(2)。它与第二个相同。下一帧有状态TP_STATUS_AVAILABLE
(0),不是发送请求,所以调用schedule()
屈服,并继续循环。由于没有更多的帧要发送(ph==NULL
)并且已请求非阻塞(msg->msg_flags ==
MSG_DONTWAIT http://lxr.free-electrons.com/source/include/linux/socket.h#L259) the do {...} while (...)
循环终止,并且tpacket_snd
回报300
,排队等待传输的字节数。
Next, packet_mmap
calls sendto
再次(通过“循环直到队列为空”代码),但是 tx 环中没有更多数据要发送,并且请求非阻塞,因此它立即返回 0,因为没有数据已排队。请注意,它检查状态的帧与它在上一次调用中最后检查的帧相同——它不是从 tx 环中的第一帧开始,它检查了head
(这在用户态中不可用)。
异步地,首先在第一帧上调用析构函数,将帧的状态设置为TP_STATUS_AVAILABLE
并减少挂起计数,然后在第二帧上。请注意,如果未请求非阻塞,则在结束时进行测试do {...} while (...)
循环将等到所有待处理的数据包都已传输到 NIC(假设它支持分散数据)后再返回。你可以通过运行来观看packet_mmap
与-t
使用阻塞 I/O 的“线程”选项(直到“循环直到队列为空”)。
有几点需要注意。首先,SystemTap 输出上的时间戳不会增加:从 SystemTap 输出推断时间顺序是不安全的。其次,请注意网络捕获(本地完成)的时间戳是不同的。 FWIW,该接口是廉价塔式计算机中的廉价 1G。
所以在这一点上,我想我们或多或少知道如何af_packet
正在处理共享的 tx 环。接下来是 tx 环中的帧如何找到到达网络接口的路径。回顾一下可能会有帮助本节 https://wiki.linuxfoundation.org/networking/kernel_flow#layer-2link-layer-eg-ethernet(关于如何处理第 2 层传输)overview https://wiki.linuxfoundation.org/networking/kernel_flowLinux 网络内核中的控制流。
好的,如果您对第 2 层传输的处理方式有基本的了解,那么这个数据包 mmap 接口似乎应该是一个巨大的消防水龙带;用数据包加载共享 tx 环,调用sendto()
with MSG_DONTWAIT
, 进而tpacket_snd
将迭代创建 skb 的 tx 队列并将它们排队到 qdisc 上。异步地,skb 将从 qdisc 中出列并发送到硬件 tx 环。 skb 应该是非线性 http://vger.kernel.org/~davem/skb_data.html因此它们将引用 tx 环中的数据而不是复制,并且一个好的现代 NIC 应该能够处理分散的数据并引用 tx 环中的数据。当然,任何这些假设都可能是错误的,所以让我们尝试用这个消防水带将大量伤害倾倒在 qdisc 上。
但首先,关于 qdisc 的工作原理,有一个不被普遍理解的事实。它们保存有限数量的数据(通常以帧数计算,但在某些情况下可以以字节为单位进行测量),如果您尝试将帧排队到完整的 qdisc,则该帧通常会被丢弃(取决于enqueuer 决定这样做)。因此,我将给出一个提示,即我最初的假设是 OP 使用数据包 mmap 将帧快速发送到 qdisc 中,以至于许多帧被丢弃。但不要太执着于这个想法;它会带你走向一个方向,但始终保持开放的心态。让我们尝试一下,看看会发生什么。
尝试这个的第一个问题是默认的 qdiscpfifo_fast
不保留统计数据。所以让我们用 qdisc 替换它pfifo
确实如此。默认情况下pfifo
将队列限制为TXQUEUELEN
帧数(一般默认为 1000)。但由于我们想要展示压倒性的 qdisc,所以我们将其明确设置为 50:
$ sudo tc qdisc add dev eth0 root pfifo limit 50
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8004: root refcnt 2 limit 50p
Sent 42 bytes 1 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
我们还测量一下处理帧所需的时间tpacket_snd
使用 SystemTap 脚本call-return.stp
:
# This is specific to net/packet/af_packet.c 3.13.0-116
function print_ts() {
ts = gettimeofday_us();
printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}
# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2213 return err;
# 2214 }
probe kernel.function("tpacket_snd") {
print_ts();
printf("tpacket_snd: args(%s)\n", $$parms);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d)\n", $err);
}
启动 SystemTap 脚本sudo stap call-return.stp
然后让我们将 8096 个 1500 字节帧喷射到该 qdisc 中,而容量只有 50 个帧:
$ sudo ./packet_mmap -c 8096 -s 1500 eth0
[...]
STARTING TEST:
data offset = 32 bytes
start fill() thread
send 8096 packets (+12144000 bytes)
end of task fill()
Loop until queue empty (0)
END (number of error:0)
那么让我们检查一下 qdisc 丢弃了多少数据包:
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8004: root refcnt 2 limit 50p
Sent 25755333 bytes 8606 pkt (dropped 1, overlimits 0 requeues 265)
backlog 0b 0p requeues 265
WAT http://i0.kym-cdn.com/photos/images/newsfeed/000/173/576/Wat8.jpg?1315930535?丢弃转储到 50 帧 qdisc 上的 8096 帧之一?让我们检查 SystemTap 输出:
[1492603552.938414] tpacket_snd: args(po=0xffff8801673ba338 msg=0x14)
[1492603553.036601] tpacket_snd: return(12144000)
[1492603553.036706] tpacket_snd: args(po=0x0 msg=0x14)
[1492603553.036716] tpacket_snd: return(0)
WAT https://i.stack.imgur.com/n6X4l.jpg?处理8096帧花费了近100mstpacket_snd
?让我们检查一下实际传输需要多长时间;即 8096 帧,1500 字节/帧,1gigabit/s ~= 97ms。WAT https://i.stack.imgur.com/QwLBa.jpg?闻起来就像有什么东西堵住了。
让我们仔细看看tpacket_snd
。呻吟:
skb = sock_alloc_send_skb(&po->sk,
hlen + tlen + sizeof(struct sockaddr_ll),
0, &err);
That 0
看起来很无害,但这实际上是 noblock 参数。它应该是msg->msg_flags & MSG_DONTWAIT
(事实证明这是4.1 中已修复 https://github.com/torvalds/linux/commit/fbf33a2802f71dba06faa948af805884ff16a2ab)。这里发生的情况是,qdisc 的大小并不是唯一的限制资源。如果为 skb 分配空间超出套接字的 sndbuf 限制的大小,则此调用将阻塞以等待 skb 被释放或返回-EAGAIN
给非阻塞调用者。在 V4.1 的修复中,如果请求非阻塞,如果非零则返回写入的字节数,否则返回-EAGAIN
对于呼叫者来说,这几乎就像有人不希望你弄清楚如何使用它(e.g.你用 80MB 的数据填充一个 tx 环,调用 sendtoMSG_DONTWAIT
,并且您返回的结果是您发送的 150KB 而不是EWOULDBLOCK
).
因此,如果您运行的是 4.1 之前的内核(我相信 OP 运行的是 >4.1 并且不受此错误影响),您将需要修补af_packet.c
并构建新内核或升级到内核 4.1 或更高版本。
我现在已经启动了内核的修补版本,因为我使用的机器运行的是 3.13。虽然如果 sndbuf 已满我们不会阻塞,但我们仍然会返回-EAGAIN
。我做了一些改变packet_mmap.c
增加 sndbuf 的默认大小并使用SO_SNDBUFFORCE
如有必要,可以覆盖每个套接字的系统最大值(似乎需要大约 750 字节 + 每个帧的帧大小)。我还做了一些补充call-return.stp
记录 sndbuf 最大大小(sk_sndbuf
)、使用量(sk_wmem_alloc
),返回的任何错误sock_alloc_send_skb
以及从返回的任何错误dev_queue_xmit
将 skb 排队到 qdisc 中。这是新版本:
# This is specific to net/packet/af_packet.c 3.13.0-116
function print_ts() {
ts = gettimeofday_us();
printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}
# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2133 if (size_max > dev->mtu + reserve + VLAN_HLEN)
# 2134 size_max = dev->mtu + reserve + VLAN_HLEN;
# 2135
# 2136 do {
# [...]
# 2148 skb = sock_alloc_send_skb(&po->sk,
# 2149 hlen + tlen + sizeof(struct sockaddr_ll),
# 2150 msg->msg_flags & MSG_DONTWAIT, &err);
# 2151
# 2152 if (unlikely(skb == NULL))
# 2153 goto out_status;
# [...]
# 2181 err = dev_queue_xmit(skb);
# 2182 if (unlikely(err > 0)) {
# 2183 err = net_xmit_errno(err);
# 2184 if (err && __packet_get_status(po, ph) ==
# 2185 TP_STATUS_AVAILABLE) {
# 2186 /* skb was destructed already */
# 2187 skb = NULL;
# 2188 goto out_status;
# 2189 }
# 2190 /*
# 2191 * skb was dropped but not destructed yet;
# 2192 * let's treat it like congestion or err < 0
# 2193 */
# 2194 err = 0;
# 2195 }
# 2196 packet_increment_head(&po->tx_ring);
# 2197 len_sum += tp_len;
# 2198 } while (likely((ph != NULL) ||
# 2199 ((!(msg->msg_flags & MSG_DONTWAIT)) &&
# 2200 (atomic_read(&po->tx_ring.pending))))
# 2201 );
# [...]
# 2213 return err;
# 2214 }
probe kernel.function("tpacket_snd") {
print_ts();
printf("tpacket_snd: args(%s)\n", $$parms);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2133") {
print_ts();
printf("tpacket_snd:2133: sk_sndbuf = %d sk_wmem_alloc = %d\n",
$po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2153") {
print_ts();
printf("tpacket_snd:2153: sock_alloc_send_skb err = %d, sk_sndbuf = %d sk_wmem_alloc = %d\n",
$err, $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2182") {
if ($err != 0) {
print_ts();
printf("tpacket_snd:2182: dev_queue_xmit err = %d\n", $err);
}
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2187") {
print_ts();
printf("tpacket_snd:2187: destructed: net_xmit_errno = %d\n", $err);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2194") {
print_ts();
printf("tpacket_snd:2194: *NOT* destructed: net_xmit_errno = %d\n", $err);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d) sk_sndbuf = %d sk_wmem_alloc = %d\n",
$err, $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}
让我们再试一次:
$ sudo tc qdisc add dev eth0 root pfifo limit 50
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8001: root refcnt 2 limit 50p
Sent 2154 bytes 21 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
$ sudo ./packet_mmap -c 200 -s 1500 eth0
[...]
c_sndbuf_sz: 1228800
[...]
STARTING TEST:
data offset = 32 bytes
send buff size = 1228800
got buff size = 425984
buff size smaller than desired, trying to force...
got buff size = 2457600
start fill() thread
send: No buffer space available
end of task fill()
send: No buffer space available
Loop until queue empty (-1)
[repeated another 17 times]
send 3 packets (+4500 bytes)
Loop until queue empty (4500)
Loop until queue empty (0)
END (number of error:0)
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8001: root refcnt 2 limit 50p
Sent 452850 bytes 335 pkt (dropped 19, overlimits 0 requeues 3)
backlog 0b 0p requeues 3
这是 SystemTap 输出:
[1492759330.907151] tpacket_snd: args(po=0xffff880393246c38 msg=0x14)
[1492759330.907162] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 1
[1492759330.907491] tpacket_snd:2182: dev_queue_xmit err = 1
[1492759330.907494] tpacket_snd:2187: destructed: net_xmit_errno = -105
[1492759330.907500] tpacket_snd: return(-105) sk_sndbuf = 2457600 sk_wmem_alloc = 218639
[1492759330.907646] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.907653] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 189337
[1492759330.907688] tpacket_snd:2182: dev_queue_xmit err = 1
[1492759330.907691] tpacket_snd:2187: destructed: net_xmit_errno = -105
[1492759330.907694] tpacket_snd: return(-105) sk_sndbuf = 2457600 sk_wmem_alloc = 189337
[repeated 17 times]
[1492759330.908541] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.908543] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 189337
[1492759330.908554] tpacket_snd: return(4500) sk_sndbuf = 2457600 sk_wmem_alloc = 196099
[1492759330.908570] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.908572] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 196099
[1492759330.908576] tpacket_snd: return(0) sk_sndbuf = 2457600 sk_wmem_alloc = 196099
现在一切都按预期进行;我们修复了一个错误,该错误导致我们阻止超过 sndbuf 限制,并且我们调整了 sndbuf 限制,使其不再成为约束,现在我们看到来自 tx 环的帧被排队到 qdisc 上,直到它已满,此时我们返回ENOBUFS
.
现在的下一个问题是如何有效地持续发布到 qdisc 以保持接口繁忙。请注意,执行packet_poll
在我们填满 qdisc 并返回的情况下是没有用的ENOBUFS
,因为它只是查询头部是否在TP_STATUS_AVAILABLE
,在这种情况下将保留TP_STATUS_SEND_REQUEST
直到随后调用sendto
成功将帧排队到 qdisc。一个简单的权宜之计(在 packet_mmap.c 中更新)是在 sendto 上循环,直到成功或出现除以下之外的错误:ENOBUFS
or EAGAIN
.
不管怎样,即使我们没有完整的解决方案来有效地防止 NIC 被饿死,我们现在所知道的也足以回答 OP 的问题。
从我们所了解到的情况来看,当 OP 在阻塞模式下使用 tx 环调用 sendto 时,tpacket_snd
将开始将 skbs 排队到 qdisc 上,直到超过 sndbuf 限制(默认值通常非常小,大约 213K,而且,我发现共享 tx 环中引用的帧数据也计入此),此时它将阻塞(而仍然持有pg_vec_lock
)。随着 skb 的释放,更多的帧将被排队,并且可能再次超出 sndbuf,我们将再次阻塞。最终,所有数据都将排队到 qdisc,但是tpacket_snd
将继续阻塞,直到所有帧都已传输(在 NIC 收到帧之前,您无法将 tx 环中的帧标记为可用,因为驱动程序环中的 skb 引用 tx 环中的帧)同时仍保持pg_vec_lock
。此时 NIC 处于饥饿状态,任何其他套接字写入器都已被锁阻止。
另一方面,当OP一次发布一个数据包时,它将由packet_snd
如果 sndbuf 中没有空间,它将阻塞,然后将帧排队到 qdisc 上,并立即返回。它不等待帧被传输。当 qdisc 被耗尽时,可以将额外的帧排队。如果发布者能够跟上,NIC 将永远不会挨饿。
此外,操作会为每个 sendto 调用复制到 tx 环中,并将其与不使用 tx 环时传递固定帧缓冲区进行比较。如果不以这种方式复制,您不会看到加速(尽管这不是使用 tx 环的唯一好处)。