Reputation: 361
I am trying to find a way to programmatically check, under Linux and using C, if software transmit timestamps (SOF_TIMESTAMPING_TX_SOFTWARE
) are supported by a given NIC, in order to revert to other kinds of timestamps (or disable them completely) if they are not supported.
In particular, my goal would be to check if they are supported like it can be done for hardware timestamps, when calling ioctl(SIOCSHWTSTAMP)
and checking its return value (the updated documentation can be found here).
ethtool -T <interface name>
is already providing this information, but I do not think it could be a good idea to call system()
or popen()
, as ethtool
may not be installed on the system and I definitely do not want to put it as a prerequisite to run my program.
When experimenting a bit, I used an adaptation of the code coming from this question:
#include <arpa/inet.h>
#include <linux/net_tstamp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/errqueue.h>
#include <sys/ioctl.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <unistd.h>
#include <time.h>
#include <poll.h>
#include <linux/if.h>
#define RAW_SOCKET 0 // Set to 0 to use an UDP socket, set to 1 to use raw socket
#define NUM_TESTS 2
#if RAW_SOCKET
#include <linux/if_packet.h>
#include <net/ethernet.h>
#endif
void die(char* s)
{
perror(s);
exit(1);
}
// Wait for data to be available on the socket error queue, as detailed in https://www.kernel.org/doc/Documentation/networking/timestamping.txt
int pollErrqueueWait(int sock,uint64_t timeout_ms) {
struct pollfd errqueueMon;
int poll_retval;
errqueueMon.fd=sock;
errqueueMon.revents=0;
errqueueMon.events=0;
while((poll_retval=poll(&errqueueMon,1,timeout_ms))>0 && errqueueMon.revents!=POLLERR);
return poll_retval;
}
int run_test(int argc, char* argv[], int hw_stamps, int sock, void *si_server_ptr)
{
#if RAW_SOCKET
struct sockaddr_ll si_server=*(struct sockaddr_ll *) si_server_ptr;
#else
struct sockaddr_in si_server=*(struct sockaddr_in *) si_server_ptr;
#endif
fprintf(stdout,"Test started.\n");
int flags;
if(hw_stamps) {
struct ifreq hwtstamp;
struct hwtstamp_config hwconfig;
// Set hardware timestamping
memset(&hwtstamp,0,sizeof(hwtstamp));
memset(&hwconfig,0,sizeof(hwconfig));
// Set ifr_name and ifr_data (see: man7.org/linux/man-pages/man7/netdevice.7.html)
strncpy(hwtstamp.ifr_name,argv[1],sizeof(hwtstamp.ifr_name));
hwtstamp.ifr_data=(void *)&hwconfig;
hwconfig.tx_type=HWTSTAMP_TX_ON;
hwconfig.rx_filter=HWTSTAMP_FILTER_ALL;
// Issue request to the driver
if (ioctl(sock,SIOCSHWTSTAMP,&hwtstamp)<0) {
die("ioctl()");
}
flags=SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_TX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE;
} else {
flags=SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_TX_SOFTWARE;
}
if(setsockopt(sock,SOL_SOCKET,SO_TIMESTAMPING,&flags,sizeof(flags))<0) {
die("setsockopt()");
}
const int buffer_len = 256;
char buffer[buffer_len];
// Send 10 packets
const int n_packets = 10;
for (int i = 0; i < n_packets; ++i) {
sprintf(buffer, "Packet %d", i);
if (sendto(sock, buffer, buffer_len, 0, (struct sockaddr*) &si_server, sizeof(si_server)) < 0) {
die("sendto()");
}
fprintf(stdout,"Sent packet number %d/%d\n",i,n_packets);
fflush(stdout);
// Obtain the sent packet timestamp.
char data[256];
struct msghdr msg;
struct iovec entry;
char ctrlBuf[CMSG_SPACE(sizeof(struct scm_timestamping))];
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &entry;
msg.msg_iovlen = 1;
entry.iov_base = data;
entry.iov_len = sizeof(data);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_control = &ctrlBuf;
msg.msg_controllen = sizeof(ctrlBuf);
// Wait for data to be available on the error queue
pollErrqueueWait(sock,-1); // -1 = no timeout is set
if (recvmsg(sock, &msg, MSG_ERRQUEUE) < 0) {
die("recvmsg()");
}
// Extract and print ancillary data (SW or HW tx timestamps)
struct cmsghdr *cmsg = NULL;
struct scm_timestamping hw_ts;
for(cmsg=CMSG_FIRSTHDR(&msg);cmsg!=NULL;cmsg=CMSG_NXTHDR(&msg, cmsg)) {
if(cmsg->cmsg_level==SOL_SOCKET && cmsg->cmsg_type==SO_TIMESTAMPING) {
hw_ts=*((struct scm_timestamping *)CMSG_DATA(cmsg));
fprintf(stdout,"HW: %lu s, %lu ns\n",hw_ts.ts[2].tv_sec,hw_ts.ts[2].tv_nsec);
fprintf(stdout,"ts[1] - ???: %lu s, %lu ns\n",hw_ts.ts[1].tv_sec,hw_ts.ts[1].tv_nsec);
fprintf(stdout,"SW: %lu s, %lu ns\n",hw_ts.ts[0].tv_sec,hw_ts.ts[0].tv_nsec);
}
}
// Wait 1s before sending next packet
sleep(1);
}
return 0;
}
int main(int argc, char* argv[]) {
int sock;
char* destination_ip = "192.168.1.211";
int destination_port = 1234;
struct in_addr sourceIP;
fprintf(stdout,"Program started.\n");
if(argc!=2) {
fprintf(stderr,"Error. You should specify the interface name.\n");
exit(1);
}
// Create socket
#if RAW_SOCKET
if ((sock = socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL))) < 0) {
die("RAW socket()");
}
#else
if ((sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
die("UDP socket()");
}
#endif
struct ifreq ifindexreq;
#if RAW_SOCKET
struct sockaddr_ll si_server;
int ifindex=-1;
// Get interface index
strncpy(ifindexreq.ifr_name,argv[1],IFNAMSIZ);
if(ioctl(sock,SIOCGIFINDEX,&ifindexreq)!=-1) {
ifindex=ifindexreq.ifr_ifindex;
} else {
die("SIOCGIFINDEX ioctl()");
}
memset(&si_server, 0, sizeof(si_server));
si_server.sll_ifindex=ifindex;
si_server.sll_family=AF_PACKET;
si_server.sll_protocol=htons(ETH_P_ALL);
#else
struct sockaddr_in si_server;
// Get source IP address
strncpy(ifindexreq.ifr_name,argv[1],IFNAMSIZ);
ifindexreq.ifr_addr.sa_family = AF_INET;
if(ioctl(sock,SIOCGIFADDR,&ifindexreq)!=-1) {
sourceIP=((struct sockaddr_in*)&ifindexreq.ifr_addr)->sin_addr;
} else {
die("SIOCGIFADDR ioctl()");
}
bzero(&si_server,sizeof(si_server));
si_server.sin_family = AF_INET;
si_server.sin_port = htons(destination_port);
si_server.sin_addr.s_addr = sourceIP.s_addr;
fprintf(stdout,"source IP: %s\n",inet_ntoa(sourceIP));
#endif
// bind() to interface
if(bind(sock,(struct sockaddr *) &si_server,sizeof(si_server))<0) {
die("bind()");
}
#if !RAW_SOCKET
// Set destination IP (re-using si_server)
if (inet_aton(destination_ip, &si_server.sin_addr) == 0) {
die("inet_aton()");
}
#endif
for(int i=0;i<NUM_TESTS;i++) {
fprintf(stdout,"Iteration: %d - HW_STAMPS? %d\n",i,i%2);
run_test(argc,argv,i%2,sock,(void *)&si_server);
}
close(sock);
return 0;
}
This code will send out 10 packets requesting software transmit timestamps, then it will try to send other 10 packets, but requesting hardware transmit timestamps, and so on.
It takes as argument the interface name over which the packets should be sent.
I noticed that, when transmit hardware/software timestamps are supported, everything is working as expected, according to the kernel timestamping documentation, as in the enp0s31f6
(ethernet) interface case:
$ sudo ./test enp0s31f6
Program started.
source IP: 192.168.1.210
Iteration: 0 - HW_STAMPS? 0
Test started.
Sent packet number 0/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878575 s, 690256891 ns
Sent packet number 1/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878576 s, 690468816 ns
Sent packet number 2/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878577 s, 691003245 ns
Sent packet number 3/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878578 s, 691365791 ns
Sent packet number 4/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878579 s, 691940147 ns
Sent packet number 5/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878580 s, 692198712 ns
Sent packet number 6/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878581 s, 692543005 ns
Sent packet number 7/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878582 s, 692856348 ns
Sent packet number 8/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878583 s, 693098097 ns
Sent packet number 9/10
HW: 0 s, 0 ns
ts[1] - ???: 0 s, 0 ns
SW: 1563878584 s, 693612477 ns
Iteration: 1 - HW_STAMPS? 1
Test started.
Sent packet number 0/10
HW: 1563878585 s, 717541747 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 1/10
HW: 1563878586 s, 718023872 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 2/10
HW: 1563878587 s, 718505122 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 3/10
HW: 1563878588 s, 719091997 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 4/10
HW: 1563878589 s, 719689747 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 5/10
HW: 1563878590 s, 720231247 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 6/10
HW: 1563878591 s, 720462747 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 7/10
HW: 1563878592 s, 721012872 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 8/10
HW: 1563878593 s, 721272372 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Sent packet number 9/10
HW: 1563878594 s, 721588497 ns
ts[1] - ???: 0 s, 0 ns
SW: 0 s, 0 ns
Instead, if I try to launch the sample program over a wireless interface, not supporting any kind of transmit timestamp, as reported by ethtool
:
$ ethtool -T wlp1s0
Time stamping parameters for wlp1s0:
Capabilities:
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
For what concerns software transmit timestamps, I never get any message looped back to the error queue, causing an indefinite wait if -1
is specified as poll
timeout, or an EAGAIN
error if a timeout is specified (and it expires, when it is set, all the times):
sudo ./test wlp1s0
Program started.
source IP: 172.22.116.105
Iteration: 0 - HW_STAMPS? 0
Test started.
Sent packet number 0/10
.....<stops here>.....
The result is the same both when using UDP sockets and when using raw sockets (by setting #define RAW_SOCKET
to 1
or to 0
).
In order to avoid waiting for a looped back message which will never come (or to wait for a timeout expiration), is there a way in which I can programmatically check if SOF_TIMESTAMPING_TX_SOFTWARE
is supported over a given interface and eventually disable the whole mechanism in my program, before trying to retrieve transmit timestamps which cannot be retrieved?
Thank you very much in advance.
Upvotes: 2
Views: 3228
Reputation: 18420
You should use the same interface that ethtool
uses. There is a specific ioctl called SIOCETHTOOL
, that retrieves the information about timestamping capabilities from driver level. This is a short example (error handling etc. is missing for the sake of brevity):
// Specify the ethtool parameter family (timestamping)
struct ethtool_ts_info tsi = {.cmd = ETHTOOL_GET_TS_INFO};
// Specify interface to use (eth1 in this example) and pass data buffer
struct ifreq ifr = {.ifr_name = "eth1", .ifr_data = (void*)&tsi};
// Create a socket for the ioctl command
int fd = socket(AF_INET, SOCK_DGRAM, 0);
// Perform the ioctl
ioctl(fd, SIOCETHTOOL, &ifr);
// and analyze the results
if (tsi.so_timestamping & SOF_TIMESTAMPING_TX_HARDWARE)
printf("%s supports hardware tx timestamps\n", ifr.ifr_name);
if (tsi.so_timestamping & SOF_TIMESTAMPING_TX_SOFTWARE)
printf("%s supports software tx timestamps\n", ifr.ifr_name);
Same for RX timestamps. This way you should be able find out if timestamps are supported or not.
Upvotes: 4