我正在尝试确定机器接收数据包,处理数据包并给出答案所需的时间。
我称之为“服务器”的这台机器运行一个非常简单的程序,它在缓冲区中接收一个数据包(recv(2)
),将收到的内容(memcpy(3)
)复制到另一个缓冲区然后发回数据包(send(2)
)。服务器运行NetBSD 5.1.2。
我的客户多次测量往返时间(pkt_count
):
struct timespec start, end;
for(i = 0; i < pkt_count; ++i)
{
printf("%d ", i+1);
clock_gettime(CLOCK_MONOTONIC, &start);
send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);
//struct timespec nsleep = {.tv_sec = 0, .tv_nsec = 100000};
//nanosleep(&nsleep, NULL);
printf("%.3f ", timespec_diff_usec(&end, &start));
}
为清楚起见,我删除了错误检查和其他小问题。客户端运行在64位的Ubuntu 12.04上。两个程序都以实时优先级运行,尽管只有Ubuntu内核是实时的(-rt)。程序之间的连接是TCP。这很好,平均给我750微秒。
然而,如果我启用已注释的nanosleep调用(睡眠时间为100μs),我的测量值会下降100μs,平均值为650μs。如果我睡眠200μs,则测量值降至550μs,依此类推。这一直持续到600μs的睡眠,平均为150μs。然后,如果我将睡眠时间提高到700μs,我的测量值平均会达到800μs。我用Wireshark证实了我的程序的措施。
我无法弄清楚发生了什么。我已经在客户端和服务器中设置了TCP_NODELAY套接字选项,没有区别。我使用UDP,没有区别(相同的行为)。所以我猜这种行为不是由于Nagle算法。它可能是什么?
[UPDATE]
这是客户端与Wireshark一起输出的屏幕截图。现在,我在另一台机器上运行我的服务器。我使用相同配置的相同操作系统(因为它是笔式驱动器中的Live System),但硬件不同。此行为未显示,一切按预期工作。但问题仍然存在:为什么会在之前的硬件中发生?
[更新2:更多信息]
正如我之前所说,我在两台不同的服务器计算机上测试了我的一对程序(客户端/服务器)。我绘制了两个获得的结果。
第一台服务器(奇怪的)是RTD Single Board Computer,带有1Gbps以太网接口。第二台服务器(普通服务器)是Diamond Single Board Computer,带有100Mbps以太网接口。他们都从SAME pendrive运行SAME OS(NetBSD 5.1.2)。
从这些结果来看,我确实认为这种行为是由驱动程序或NIC本身引起的,尽管我仍然无法想象它为什么会发生......
答案 0 :(得分:2)
好的,我得出了结论。
我在服务器中使用Linux而不是NetBSD尝试了我的程序。它按预期运行,即,无论我在代码的那一点睡眠多少,结果都是一样的。
这个事实告诉我,问题可能在于NetBSD的界面驱动程序。为了识别驱动程序,我阅读了dmesg
输出。这是相关部分:
wm0 at pci0 dev 25 function 0: 82801I mobile (AMT) LAN Controller, rev. 3
wm0: interrupting at ioapic0 pin 20
wm0: PCI-Express bus
wm0: FLASH
wm0: Ethernet address [OMMITED]
ukphy0 at wm0 phy 2: Generic IEEE 802.3u media interface
ukphy0: OUI 0x000ac2, model 0x000b, rev. 1
ukphy0: 10baseT, 10baseT-FDX, 100baseTX, 100baseTX-FDX, 1000baseT, 1000baseT-FDX, auto
因此,正如您所看到的,我的界面名为wm0
。根据{{3}}(第9页),我应该通过查阅文件sys/dev/pci/files.pci
,第625行(this)来检查加载了哪个驱动程序。它显示:
# Intel i8254x Gigabit Ethernet
device wm: ether, ifnet, arp, mii, mii_bitbang
attach wm at pci
file dev/pci/if_wm.c wm
然后,搜索驱动程序源代码(dev/pci/if_wm.c
,here),我发现了一段可能会改变驱动程序行为的代码:
/*
* For N interrupts/sec, set this value to:
* 1000000000 / (N * 256). Note that we set the
* absolute and packet timer values to this value
* divided by 4 to get "simple timer" behavior.
*/
sc->sc_itr = 1500; /* 2604 ints/sec */
CSR_WRITE(sc, WMREG_ITR, sc->sc_itr);
然后我将此1500值更改为1(尝试增加允许的每秒中断次数)和0(尝试完全消除中断限制),但这两个值都产生相同的结果:
这至少比以前的情况更好。
因此,我得出结论,该行为是由服务器的接口驱动程序引起的。我不想进一步调查以找到其他罪魁祸首,因为我正在从NetBSD迁移到Linux以进行涉及这种单板计算机的项目。
答案 1 :(得分:0)
这是一个(希望受过教育的)猜测,但我认为它可以解释你所看到的。
我不确定linux内核的实时性。它可能不是完全先发制人......所以,有了免责声明,继续:))......
根据调度程序的不同,任务可能会有一个所谓的“quanta”,这只是在安排了相同优先级的另一个任务之前可以运行的大部分时间。如果内核不完全预先-emptive,这也可能是更高优先级任务可以运行的点。这取决于我不太了解的调度程序的细节。
在您的第一个gettime和第二个gettime之间的任何地方,您的任务都可能被抢先一步。这只是意味着它“暂停”,另一个任务就是在一段时间内使用CPU。
没有睡眠的循环可能会像这样
clock_gettime(CLOCK_MONOTONIC, &start);
send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);
printf("%.3f ", timespec_diff_usec(&end, &start));
clock_gettime(CLOCK_MONOTONIC, &start);
<----- PREMPTION .. your tasks quanta has run out and the scheduler kicks in
... another task runs for a little while
<----- PREMPTION again and your back on the CPU
send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);
// Because you got pre-empted, your time measurement is artifically long
printf("%.3f ", timespec_diff_usec(&end, &start));
clock_gettime(CLOCK_MONOTONIC, &start);
<----- PREMPTION .. your tasks quanta has run out and the scheduler kicks in
... another task runs for a little while
<----- PREMPTION again and your back on the CPU
and so on....
当你将纳秒睡眠放入时,这很可能是调度程序能够在当前任务的量子到期之前运行的点(同样适用于recv(),这会阻塞)。所以你得到的可能是这样的
clock_gettime(CLOCK_MONOTONIC, &start);
send(sock, send_buf, pkt_size, 0);
recv(sock, recv_buf, pkt_size, 0);
clock_gettime(CLOCK_MONOTONIC, &end);
struct timespec nsleep = {.tv_sec = 0, .tv_nsec = 100000};
nanosleep(&nsleep, NULL);
<----- PREMPTION .. nanosleep allows the scheduler to kick in because this is a pre-emption point
... another task runs for a little while
<----- PREMPTION again and your back on the CPU
// Now it so happens that because your task got prempted where it did, the time
// measurement has not been artifically increased. Your task then can fiish the rest of
// it's quanta
printf("%.3f ", timespec_diff_usec(&end, &start));
clock_gettime(CLOCK_MONOTONIC, &start);
... and so on
然后会发生某种交错,有时候你会在两个gettime()之间加以预防,有时因为纳米睡眠而在它们之外。根据x,您可能会遇到一个最佳位置(偶然),以使您的抢占点平均超出您的时间测量区域。
无论如何,这是我的两便士,希望它有助于解释事情:)
关于完成“纳秒”的一点注释...
我认为需要对“纳秒”睡眠持谨慎态度。我这说的原因是我认为除非使用特殊硬件,否则普通计算机不太可能实际执行此操作。
通常情况下,操作系统会有一个常规系统“tick”,大约5ms生成。这是一个由RTC(实时时钟 - 只是一点硬件)产生的中断。使用此“滴答”系统然后生成它的内部时间表示。因此,平均OS仅具有几毫秒的时间分辨率。这个滴答不是更快的原因是在保持非常准确的时间和不用定时器中断淹没系统之间要达到平衡。
我不确定我的平均现代电脑是否有点过时...我认为它们中的一些确实有更高的res时间,但仍然没有达到纳秒范围,它们甚至可能在100uS下挣扎。
因此,总而言之,请记住,您可能获得的最佳时间分辨率通常在毫秒范围内。
编辑:重新审视这个并认为我会添加以下内容......并不能解释您所看到的内容,但可能会为调查提供另一种途径...
如上所述,纳米睡眠的定时精度不可能优于毫秒。此外,您的任务可能会被抢占,这也会导致计时问题。还有一个问题是,数据包上升到协议栈所需的时间可能会有所不同,以及网络延迟。
您可以尝试的一件事是,如果您的NIC支持,IEEE1588(又名PTP)。如果您的NIC支持它,它可以在PTP事件数据包离开时进行时间戳并进入PHY。这将为您提供网络延迟的概率估计。这可以消除您在软件抢占等方面可能遇到的任何问题。我知道我担心Linux PTP会蹲下,但您可以尝试http://linuxptp.sourceforge.net/
答案 2 :(得分:0)
我认为'量子'是解释的最佳理论。 在linux上它是上下文切换频率。 内核为处理量子时间提供了条件。但过程在两种情况下被抢占:
使用优先级/ rt等将未使用的量子时间分配给另一个准备运行的进程。
实际上,上下文切换频率配置为每秒10000次,它为量子提供大约100us。但内容切换需要一段时间,它是cpu依赖的,请看: http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html 我不明白,为什么内容开关频率很高但是讨论的是linux内核论坛。
你可以在这里找到部分类似的问题: https://serverfault.com/questions/14199/how-many-context-switches-is-normal-as-a-function-of-cpu-cores-or-other答案 3 :(得分:0)
如果应用程序发送的数据量很大且足够快,则可能会填充内核缓冲区,从而导致每个send()的延迟。由于睡眠在测量部分之外,因此它会吃掉在send()调用上花费的时间。
帮助检查这种情况的一种方法是使用相对较少的迭代次数运行,然后进行适度的迭代次数。如果问题发生在具有小数据包大小(例如<1k)的少量迭代(比如20),那么这可能是不正确的诊断。
请记住,如果以这样的紧密循环方式发送数据,您的进程和内核很容易淹没网络适配器和以太网(或其他媒体类型)的线速。
我无法阅读屏幕截图。如果wireshark在线上显示恒定的传输速率,则表明这是正确的诊断。当然,进行数学计算 - 将数据速度除以数据包大小(+标题) - 应该可以了解数据包的最大发送速率。
对于导致延迟增加的700微秒,这更难确定。我对那个没有任何想法。
答案 4 :(得分:0)
我对如何创建更准确的性能测量有一个建议。 使用RDTSC指令(甚至更好的内在__rdtsc()函数)。这涉及在不离开ring3的情况下读取CPU计数器(无系统调用)。 gettime函数几乎总是涉及系统调用,这会减慢速度。
您的代码有点棘手,因为它涉及2个系统调用(发送/接收),但通常最好在第一次测量之前调用sleep(0)以确保非常短的测量不会接收上下文切换。当然,应该通过性能敏感函数中的宏来禁用/启用时间测量(和Sleep())代码。
通过让您的进程释放执行时间窗口(例如sleep(0)),可以欺骗某些操作系统提高进程优先级。在下一个计划时,操作系统(并非所有)将提高您的流程的优先级,因为它没有完成执行时间配额。