[英]Linux, UDP datagrams, and kernel timestamps: Lots of examples and stackoversflow entries later, and still cannot get timestamps at all
我一直在嘗試讓 Linux(內核 4.1.4)給我發送和接收 UDP 數據報的時間戳,但未能成功。 我已閱讀原始內核文檔 ( https://www.kernel.org/doc/Documentation/networking/timestamping.txt ),以及許多示例和許多 stackoverflow 條目。 我可以毫無問題地在發送方和接收方之間發送數據報。 但是我無法獲得發送或接收數據報的時間戳,而且我無法弄清楚我做錯了什么。
一件奇怪的事情是,當我使用 MSG_ERRQUEUE 通道獲取已發送數據報的時間戳信息時,我確實獲得了原始傳出數據包,並且確實獲得了第一條輔助消息(SOL_IP、IP_RECVERR),但我沒有獲得第二條消息(應該是 SOL_SOCKET 級別,輸入 SCM_TIMESTAMPING)。
在另一個關於為發送的數據包獲取時間戳的 stackoverflow 條目( 時間戳傳出數據包)中,有人提到某些驅動程序可能沒有實現對skb_tx_timestamp
的調用,但我檢查了我的(Realtek),並且該調用肯定在那里。
這是我設置 UDP 接收器的方法(未顯示錯誤處理代碼):
inf->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
timestampOn = SOF_TIMESTAMPING_RX_SOFTWARE | SOF_TIMESTAMPING_RX_HARDWARE;
r = setsockopt(inf->fd, SOL_SOCKET, SO_TIMESTAMPING, ×tampOn, sizeof(timestampOn));
r = setsockopt(inf->fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
memset(&(inf->local), 0, sizeof(struct sockaddr_in));
inf->local.sin_family = AF_INET;
inf->local.sin_port = htons(port);
inf->local.sin_addr.s_addr = htonl(INADDR_ANY);
r = bind(inf->fd, (struct sockaddr *)&(inf->local), sizeof(struct sockaddr_in));
是否使用 SO_REUSEPORT 似乎並不重要。
對於接收,我的理解是我們不使用 MSG_ERRQUEUE。 只有當我們想要發送消息的時間戳時。 此外,當我將 MSG_ERRQUEUE 與 recvmsg 一起使用時,我得到“資源暫時不可用”。 這是我接收數據報的方式:
int recv_len;
struct msghdr msg;
struct iovec iov;
memset(&msg, 0, sizeof(msg));
memset(&iov, 0, sizeof(iov));
// Space for control message info plus timestamp
char ctrl[2048];
memset(ctrl, 0, sizeof(ctrl));
//struct cmsghdr *cmsg = (struct cmsghdr *) &ctrl;
// Ancillary data buffer and length
msg.msg_control = (char *) ctrl;
msg.msg_controllen = sizeof(ctrl);
// Dest address info
msg.msg_name = (struct sockaddr *) &(inf->remote);
msg.msg_namelen = sizeof(struct sockaddr_in);
// Array of data buffers (scatter/gather)
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// Data buffer pointer and length
iov.iov_base = buf;
iov.iov_len = len;
recv_len = recvmsg(inf->fd, &msg, 0);
然后我將一個指向 msg 的指針傳遞給另一個執行此操作的函數( handle_time
):
struct timespec* ts = NULL;
struct cmsghdr* cmsg;
struct sock_extended_err *ext;
for( cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg,cmsg) ) {
printf("level=%d, type=%d, len=%zu\n", cmsg->cmsg_level, cmsg->cmsg_type, cmsg->cmsg_len);
}
接收到零消息。 所以這是第一個問題。 我上面的設置代碼與我在網上找到的六個其他示例相匹配,但我沒有從中獲得任何輔助數據。
接下來,讓我們轉向發送數據報。 這是設置:
inf->port = port;
inf->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
memset(&(inf->remote), 0, sizeof(struct sockaddr_in));
inf->remote.sin_family = AF_INET;
inf->remote.sin_port = htons(port);
timestampOn = SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_TX_HARDWARE;
r = setsockopt(inf->fd, SOL_SOCKET, SO_TIMESTAMPING, ×tampOn, sizeof(timestampOn));
on = 1;
r = setsockopt(inf->fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
r = inet_aton(address, &(inf->remote.sin_addr));
這就是我發送數據報的方式:
int send_len, r, i;
struct msghdr msg;
struct iovec iov;
memset(&msg, 0, sizeof(msg));
memset(&iov, 0, sizeof(iov));
// Space for control message info plus timestamp
char ctrl[2048];
memset(ctrl, 0, sizeof(ctrl));
//struct cmsghdr *cmsg = (struct cmsghdr *) &ctrl;
// Ancillary data buffer and length
//msg.msg_control = (char *) ctrl;
//msg.msg_controllen = sizeof(ctrl);
// Dest address info
msg.msg_name = (struct sockaddr *) &(inf->remote);
msg.msg_namelen = sizeof(struct sockaddr_in);
// Array of data buffers (scatter/gather)
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// Data buffer pointer and length
iov.iov_base = buf;
iov.iov_len = len;
send_len = sendmsg(inf->fd, &msg, 0);
我見過的例子重用了 msg 和 iov 數據結構,但在我的實驗中,我添加了代碼以確保清除所有內容,以防萬一發送留下任何東西,盡管它沒有任何區別。 這是獲取時間戳的代碼:
memset(&msg, 0, sizeof(msg));
memset(&iov, 0, sizeof(iov));
memset(ctrl, 0, sizeof(ctrl));
msg.msg_control = (char *) ctrl;
msg.msg_controllen = sizeof(ctrl);
msg.msg_name = (struct sockaddr *) &(inf->remote);
msg.msg_namelen = sizeof(struct sockaddr_in);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
iov.iov_base = junk_buf;
iov.iov_len = sizeof(junk_buf);
for (;;) {
r = recvmsg(inf->fd, &msg, MSG_ERRQUEUE);
if (r<0) {
fprintf(stderr, "Didn't get kernel time\n");
return send_len;
}
printf("recvmsg returned %d\n", r);
handle_time(&msg);
}
數據緩沖區包含預期的原始數據報。 我返回的輔助數據包括一條消息,handle_time 打印為:
level=0, type=11, len=48
這是級別 SOL_IP 和類型 IP_RECVERR,這是根據文檔預期的。 查看有效負載(一個結構 sock_extended_err),errno 是 42(ENOMSG,沒有所需類型的消息),origin 是 4(SO_EE_ORIGIN_TXSTATUS)。 從文檔來看,這應該會發生,並表明實際上我確實設法通知內核我想要 TX 狀態消息。 但是沒有第二個輔助消息!
我試圖查看是否有任何內核編譯選項可能會禁用此功能,但我沒有找到任何選項。 所以我在這里完全感到困惑。 誰能幫我弄清楚我做錯了什么?
謝謝!
更新:我嘗試在另一台 Linux 機器上運行相同的代碼,這次是 CentOS 7(內核 3.10.0-693.2.2.el7.x86_64)。 我不知道那台機器有什么樣的 NIC,但是當我嘗試發送數據報時,我得到了一些其他奇怪的行為。 對於第一個數據報,當我啟動這個程序時,我會收到消息和一條輔助消息,就像上面一樣。 對於隨后的每次sendmsg
調用,errno 都會告訴我收到“無效參數”錯誤。 如果我不在套接字上啟用時間戳,此錯誤就會消失。
更新 2:我發現我沒有在驅動程序中啟用時間戳所必需的 ioctl。 不幸的是,當我進行此調用時,我從 errno 獲得了 ENODEV(沒有這樣的設備)。 這是我嘗試這樣做的方法(我從https://github.com/majek/openonload/blob/master/src/tests/onload/hwtimestamping/tx_timestamping.c模仿):
struct ifreq ifr;
struct hwtstamp_config hwc;
inf->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
memset(&ifr, 0, sizeof(ifr));
hwc.flags = 0;
hwc.tx_type = HWTSTAMP_TX_ON;
hwc.rx_filter = HWTSTAMP_FILTER_ALL;
ifr.ifr_data = (char*)&hwc;
r = ioctl(inf->fd, SIOCSHWTSTAMP, &ifr);
話雖如此,我對軟件時間戳比較滿意,它不需要這個調用。 所以我不確定這是否有幫助。
更新 3:請求了一個可編譯的示例。 整個程序非常小,所以我把它放到了 pastebin 中: https : //pastebin.com/qd0gspRc
此外,這是 ethtool 的輸出:
Time stamping parameters for eth0:
Capabilities:
software-transmit (SOF_TIMESTAMPING_TX_SOFTWARE)
software-receive (SOF_TIMESTAMPING_RX_SOFTWARE)
software-system-clock (SOF_TIMESTAMPING_SOFTWARE)
PTP Hardware Clock: none
Hardware Transmit Timestamp Modes: none
Hardware Receive Filter Modes: none
由於這顯然不支持硬件時間戳,因此 ioctl 沒有實際意義。 我嘗試將發送方和接收方的 SO_TIMESTAMPING 設置更改為 SOF_TIMESTAMPING_TX_SOFTWARE 和 SOF_TIMESTAMPING_RX_SOFTWARE。 那沒有幫助。
然后我嘗試將 SOF_TIMESTAMPING_SOFTWARE 添加到兩者中。 我終於開始得到一些東西:
level=1, type=37, len=64
級別 1 是 SOL_SOCKET,類型 37 是 SCM_TIMESTAMPING。 我會回到文檔並弄清楚如何解釋這一點。 它說明了關於傳遞三個時間結構的數組。 驅動程序對skb_tx_timestamp
的調用應該已經足夠了,這樣就不需要我啟用“假”軟件時間戳來獲取某些信息。
就像我在評論中所說的,使用SOF_TIMESTAMPING_SOFTWARE
和SOF_TIMESTAMPING_RAW_HARDWARE
是必要的,因為如果我正確理解文檔,有些位將生成時間戳,有些位在這里用於在控制消息中報告它們:
1.3.1 時間戳生成
一些位是對堆棧的請求,以嘗試生成時間戳。 它們的任何組合都是有效的。 對這些位的更改適用於新創建的數據包,而不適用於堆棧中已有的數據包。 因此,可以通過在兩個
setsockopt
調用中嵌入一個send()
調用來選擇性地請求數據包子集的時間戳(例如,用於采樣),一個是啟用時間戳生成,另一個是禁用它。 時間戳也可能是由於特定套接字請求之外的原因而生成的,例如在系統范圍內啟用接收時間戳時,如前所述。1.3.2 時間戳報告
其他三位控制將在生成的控制消息中報告哪些時間戳。 對位的更改在堆棧中的時間戳報告位置立即生效。 僅針對也具有相關時間戳生成請求集的數據包報告時間戳。
之后,使用數據文檔說:
2.1
SCM_TIMESTAMPING
記錄這些時間戳在帶有 cmsg_level
SOL_SOCKET
、 cmsg_typeSCM_TIMESTAMPING
和有效載荷類型的控制消息中返回
struct scm_timestamping { struct timespec ts[3]; };
...
該結構最多可以返回三個時間戳。 這是一個遺留功能。 任何時候至少有一個字段是非零的。 大多數時間戳在
ts[0]
中傳遞。 硬件時間戳在ts[2]
中傳遞。
要獲得傳輸時間戳,這需要一些配置,首先您需要知道軟件時間戳並不總是可用的,我只實現了獲取硬件傳輸時間戳。 但我不是這些領域的專家,我只是嘗試使用我發現的信息來實現時間戳。
其次,我需要使用linuxptp 工具激活硬件功能,我使用hwstamp_cli :
hwstamp_ctl -i eth0 -r 1 -t 1
通過這個和對你的代碼的一些修改,我實現了硬件傳輸時間戳,但只能使用 ethX 接口,因為 lo 接口沒有這些功能 AFAIK 所以最終代碼是:
#include <arpa/inet.h>
#include <errno.h>
#include <inttypes.h>
#include <linux/errqueue.h>
#include <linux/net_tstamp.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define UDP_MAX_LENGTH 1500
typedef struct {
int fd;
int port;
int err_no;
struct sockaddr_in local;
struct sockaddr_in remote;
struct timeval time_kernel;
struct timeval time_user;
int64_t prev_serialnum;
} socket_info;
static int setup_udp_receiver(socket_info *inf, int port) {
inf->port = port;
inf->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (inf->fd < 0) {
inf->err_no = errno;
fprintf(stderr, "setup_udp_server: socket failed: %s\n",
strerror(inf->err_no));
return inf->fd;
}
int timestampOn =
SOF_TIMESTAMPING_RX_SOFTWARE | SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE |
// SOF_TIMESTAMPING_OPT_TSONLY |
0;
int r = setsockopt(inf->fd, SOL_SOCKET, SO_TIMESTAMPING, ×tampOn,
sizeof timestampOn);
if (r < 0) {
inf->err_no = errno;
fprintf(stderr, "setup_udp_server: setsockopt failed: %s\n",
strerror(inf->err_no));
return r;
}
int on = 1;
r = setsockopt(inf->fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof on);
if (r < 0) {
inf->err_no = errno;
fprintf(stderr, "setup_udp_server: setsockopt2 failed: %s\n",
strerror(inf->err_no));
return r;
}
inf->local = (struct sockaddr_in){.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
.sin_addr.s_addr = htonl(INADDR_ANY)};
r = bind(inf->fd, (struct sockaddr *)&inf->local, sizeof inf->local);
if (r < 0) {
inf->err_no = errno;
fprintf(stderr, "setup_udp_server: bind failed: %s\n",
strerror(inf->err_no));
return r;
}
inf->prev_serialnum = -1;
return 0;
}
static int setup_udp_sender(socket_info *inf, int port, char *address) {
inf->port = port;
inf->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (inf->fd < 0) {
inf->err_no = errno;
fprintf(stderr, "setup_udp_client: socket failed: %s\n",
strerror(inf->err_no));
return inf->fd;
}
int timestampOn =
SOF_TIMESTAMPING_RX_SOFTWARE | SOF_TIMESTAMPING_TX_SOFTWARE |
SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE |
// SOF_TIMESTAMPING_OPT_TSONLY |
0;
int r = setsockopt(inf->fd, SOL_SOCKET, SO_TIMESTAMPING, ×tampOn,
sizeof timestampOn);
if (r < 0) {
inf->err_no = errno;
fprintf(stderr, "setup_udp_server: setsockopt failed: %s\n",
strerror(inf->err_no));
return r;
}
inf->remote = (struct sockaddr_in){.sin_family = AF_INET,
.sin_port = htons((uint16_t)port)};
r = inet_aton(address, &inf->remote.sin_addr);
if (r == 0) {
fprintf(stderr, "setup_udp_client: inet_aton failed\n");
inf->err_no = 0;
return -1;
}
inf->local = (struct sockaddr_in){.sin_family = AF_INET,
.sin_port = htons(0),
.sin_addr.s_addr = htonl(INADDR_ANY)};
inf->prev_serialnum = -1;
return 0;
}
static void handle_scm_timestamping(struct scm_timestamping *ts) {
for (size_t i = 0; i < sizeof ts->ts / sizeof *ts->ts; i++) {
printf("timestamp: %lld.%.9lds\n", (long long)ts->ts[i].tv_sec,
ts->ts[i].tv_nsec);
}
}
static void handle_time(struct msghdr *msg) {
for (struct cmsghdr *cmsg = CMSG_FIRSTHDR(msg); cmsg;
cmsg = CMSG_NXTHDR(msg, cmsg)) {
printf("level=%d, type=%d, len=%zu\n", cmsg->cmsg_level, cmsg->cmsg_type,
cmsg->cmsg_len);
if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) {
struct sock_extended_err *ext =
(struct sock_extended_err *)CMSG_DATA(cmsg);
printf("errno=%d, origin=%d\n", ext->ee_errno, ext->ee_origin);
continue;
}
if (cmsg->cmsg_level != SOL_SOCKET)
continue;
switch (cmsg->cmsg_type) {
case SO_TIMESTAMPNS: {
struct scm_timestamping *ts = (struct scm_timestamping *)CMSG_DATA(cmsg);
handle_scm_timestamping(ts);
} break;
case SO_TIMESTAMPING: {
struct scm_timestamping *ts = (struct scm_timestamping *)CMSG_DATA(cmsg);
handle_scm_timestamping(ts);
} break;
default:
/* Ignore other cmsg options */
break;
}
}
printf("End messages\n");
}
static ssize_t udp_receive(socket_info *inf, char *buf, size_t len) {
char ctrl[2048];
struct iovec iov = (struct iovec){.iov_base = buf, .iov_len = len};
struct msghdr msg = (struct msghdr){.msg_control = ctrl,
.msg_controllen = sizeof ctrl,
.msg_name = &inf->remote,
.msg_namelen = sizeof inf->remote,
.msg_iov = &iov,
.msg_iovlen = 1};
ssize_t recv_len = recvmsg(inf->fd, &msg, 0);
gettimeofday(&inf->time_user, NULL);
if (recv_len < 0) {
inf->err_no = errno;
fprintf(stderr, "udp_receive: recvfrom failed: %s\n",
strerror(inf->err_no));
}
handle_time(&msg);
return recv_len;
}
static ssize_t udp_send(socket_info *inf, char *buf, size_t len) {
struct iovec iov = (struct iovec){.iov_base = buf, .iov_len = len};
struct msghdr msg = (struct msghdr){.msg_name = &inf->remote,
.msg_namelen = sizeof inf->remote,
.msg_iov = &iov,
.msg_iovlen = 1};
gettimeofday(&inf->time_user, NULL);
ssize_t send_len = sendmsg(inf->fd, &msg, 0);
if (send_len < 0) {
inf->err_no = errno;
fprintf(stderr, "udp_send: sendmsg failed: %s\n", strerror(inf->err_no));
}
return send_len;
}
static ssize_t meq_receive(socket_info *inf, char *buf, size_t len) {
struct iovec iov = (struct iovec){.iov_base = buf, .iov_len = len};
char ctrl[2048];
struct msghdr msg = (struct msghdr){.msg_control = ctrl,
.msg_controllen = sizeof ctrl,
.msg_name = &inf->remote,
.msg_namelen = sizeof inf->remote,
.msg_iov = &iov,
.msg_iovlen = 1};
ssize_t recv_len = recvmsg(inf->fd, &msg, MSG_ERRQUEUE);
if (recv_len < 0) {
inf->err_no = errno;
if (errno != EAGAIN) {
fprintf(stderr, "meq_receive: recvmsg failed: %s\n",
strerror(inf->err_no));
}
return recv_len;
}
handle_time(&msg);
return recv_len;
}
typedef struct {
int64_t serialnum;
int64_t user_time_serialnum;
int64_t user_time;
int64_t kernel_time_serialnum;
int64_t kernel_time;
size_t message_bytes;
} message_header;
static const size_t payload_max = UDP_MAX_LENGTH - sizeof(message_header);
static ssize_t generate_random_message(socket_info *inf, char *buf,
size_t len) {
if (len < sizeof(message_header)) {
return -1;
}
message_header *header = (message_header *)buf;
char *payload = (char *)(header + 1);
size_t payload_len = (size_t)random() % (payload_max + 1);
if (payload_len > len - sizeof(message_header)) {
payload_len = len - sizeof(message_header);
}
for (size_t i = 0; i < payload_len; i++) {
payload[i] = (char)random();
}
static int64_t serial_num = 0;
*header = (message_header){
.user_time_serialnum = inf->prev_serialnum,
.user_time = inf->time_user.tv_sec * 1000000000L + inf->time_user.tv_usec,
.kernel_time_serialnum = inf->prev_serialnum,
.kernel_time =
inf->time_kernel.tv_sec * 1000000000L + inf->time_kernel.tv_usec,
.serialnum = serial_num,
.message_bytes = payload_len};
size_t total = payload_len + sizeof *header;
printf("uts%5" PRId64 ": kt=%" PRId64 ", ut=%" PRId64 ", sn=%" PRId64
": s=%zu\n",
header->user_time_serialnum, header->kernel_time, header->user_time,
header->serialnum, total);
inf->prev_serialnum = serial_num++;
return (ssize_t)total;
}
static void sender_loop(char *host) {
socket_info inf;
int ret = setup_udp_sender(&inf, 8000, host);
if (ret < 0) {
return;
}
for (int i = 0; i < 2000; i++) {
useconds_t t = random() % 2000000;
usleep(t);
char packet_buffer[4096];
ssize_t len =
generate_random_message(&inf, packet_buffer, sizeof packet_buffer);
if (len < 0) {
return;
}
udp_send(&inf, packet_buffer, (size_t)len);
while (meq_receive(&inf, packet_buffer, sizeof packet_buffer) != -1) {
}
}
}
static void receiver_loop(void) {
socket_info inf;
int ret = setup_udp_receiver(&inf, 8000);
if (ret < 0) {
return;
}
for (int i = 0; i < 1000; i++) {
char packet_buffer[4096];
udp_receive(&inf, packet_buffer, sizeof packet_buffer);
}
}
#define USAGE "Usage: %s [-r | -s host]\n"
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, USAGE, argv[0]);
return 0;
}
if (0 == strcmp(argv[1], "-s")) {
if (argc < 3) {
fprintf(stderr, USAGE, argv[0]);
return 0;
}
sender_loop(argv[2]);
} else if (0 == strcmp(argv[1], "-r")) {
receiver_loop();
} else {
fprintf(stderr, USAGE, argv[0]);
}
}
示例輸出:
$ ./a.out -r
level=1, type=37, len=64
timestamp: 1511196758.087209387s
timestamp: 0.000000000s
timestamp: 0.000000000s
End messages
level=1, type=37, len=64
timestamp: 1511196759.333507671s
timestamp: 0.000000000s
timestamp: 0.000000000s
End messages
$ ./a.out -s "8.8.8.8"
uts -1: kt=238059712, ut=140918979990070, sn=0: s=482
uts 0: kt=238059712, ut=1511197522000237457, sn=1: s=132
level=1, type=37, len=64
timestamp: 0.000000000s
timestamp: 0.000000000s
timestamp: 1511197359.637050597s
level=0, type=11, len=48
errno=42, origin=4
End messages
uts 1: kt=238059712, ut=1511197523000483805, sn=2: s=1454
level=1, type=37, len=64
timestamp: 0.000000000s
timestamp: 0.000000000s
timestamp: 1511197360.883295397s
level=0, type=11, len=48
errno=42, origin=4
End messages
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.