簡體   English   中英

在 Linux 下,如何以編程方式檢查給定 NIC 是否支持傳輸時間戳?

[英]How do I programmatically check if transmit timestamps are supported by a given NIC, under Linux?

我試圖找到一種在 Linux 下使用 C 以編程方式檢查軟件傳輸時間戳( SOF_TIMESTAMPING_TX_SOFTWARE )是否受給定 NIC 支持的方法,以便恢復到其他類型的時間戳(或完全禁用它們),如果它們不支持支持的。

特別是,我的目標是在調用ioctl(SIOCSHWTSTAMP)並檢查其返回值(可以在此處找到更新的文檔ioctl(SIOCSHWTSTAMP) ,檢查它們是否受支持,就像對硬件時間戳所做的那樣。

ethtool -T <interface name>已經提供了這些信息,但我認為調用system()popen()不是一個好主意,因為ethtool可能沒有安裝在系統上,我絕對不想把它作為運行我的程序的先決條件。

在進行一些試驗時,我使用了來自這個問題的代碼的改編:

#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;
}

此代碼將發出 10 個數據包請求軟件傳輸時間戳,然后它將嘗試發送其他 10 個數據包,但請求硬件傳輸時間戳,依此類推。

它將應該發送數據包的接口名稱作為參數。 我注意到,當支持傳輸硬件/軟件時間戳時,一切都按預期工作,根據內核時間戳文檔,如enp0s31f6 (以太網)接口案例:

$ 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

相反,如果我嘗試通過無線接口啟動示例程序,但不支持任何類型的傳輸時間戳,如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

對於涉及軟件傳輸時間戳的內容,我從未收到任何消息循環回錯誤隊列,如果將-1指定為poll超時,則會導致無限期等待,或者如果指定超時(並且在設置時過期),則會導致EAGAIN錯誤, 一直以來):

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>.....

使用 UDP 套接字和使用原始套接字(通過將#define RAW_SOCKET設置為10 )時,結果是相同的。

為了避免等待永遠不會出現的回送消息(或等待超時到期),有沒有一種方法可以以編程方式檢查給定接口是否支持SOF_TIMESTAMPING_TX_SOFTWARE並最終禁用我的整個機制程序,在嘗試檢索無法檢索的傳輸時間戳之前?

非常感謝您提前。

您應該使用ethtool使用的相同接口。 有一個特定的 ioctl 稱為SIOCETHTOOL ,它從驅動程序級別檢索有關時間戳功能的信息。 這是一個簡短的示例(為簡潔起見,缺少錯誤處理等):

// 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);

RX 時間戳也一樣。 通過這種方式,您應該能夠確定是否支持時間戳。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM