timestamp/timestamp_linux.go (150 lines of code) (raw):
/*
Copyright (c) Facebook, Inc. and its affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package timestamp
import (
"encoding/binary"
"fmt"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
// unix.Cmsghdr size differs depending on platform
var socketControlMessageHeaderOffset = binary.Size(unix.Cmsghdr{})
var timestamping = unix.SO_TIMESTAMPING_NEW
func init() {
// if kernel is older than 5, it doesn't support unix.SO_TIMESTAMPING_NEW
var uname unix.Utsname
if err := unix.Uname(&uname); err == nil {
if uname.Release[0] < '5' {
// reading such timestamps on 32bit machines will not work, but we can't support everything
timestamping = unix.SO_TIMESTAMPING
}
}
}
/*
scmDataToTime parses SocketControlMessage Data field into time.Time.
The structure can return up to three timestamps. This is a legacy
feature. Only one field is non-zero at any time. Most timestamps
are passed in ts[0]. Hardware timestamps are passed in ts[2].
*/
func scmDataToTime(data []byte) (ts time.Time, err error) {
// 2 x 64bit ints
size := 16
// first, try to use hardware timestamps
ts, err = byteToTime(data[size*2 : size*3])
if err != nil {
return ts, err
}
// if hw timestamps aren't present, use software timestamps
// we can't use ts.IsZero because for some crazy reason timestamp parsed using time.Unix()
// reports IsZero() == false, even if seconds and nanoseconds are zero.
if ts.UnixNano() == 0 {
ts, err = byteToTime(data[0:size])
if err != nil {
return ts, err
}
if ts.UnixNano() == 0 {
return ts, fmt.Errorf("got zero timestamp")
}
}
return ts, nil
}
// byteToTime converts LittleEndian bytes into a timestamp
func byteToTime(data []byte) (time.Time, error) {
// __kernel_timespec from linux/time_types.h
// can't use unix.Timespec which is old timespec that uses 32bit ints on 386 platform.
sec := int64(binary.LittleEndian.Uint64(data[0:8]))
nsec := int64(binary.LittleEndian.Uint64(data[8:]))
return time.Unix(sec, nsec), nil
}
func ioctlTimestamp(fd int, ifname string, filter int32) error {
hw := &hwtstampСonfig{
flags: 0,
txType: hwtstampTXON,
rxFilter: filter,
}
i := &ifreq{data: uintptr(unsafe.Pointer(hw))}
copy(i.name[:unix.IFNAMSIZ-1], ifname)
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), unix.SIOCSHWTSTAMP, uintptr(unsafe.Pointer(i))); errno != 0 {
return fmt.Errorf("failed to run ioctl SIOCSHWTSTAMP: %s (%d)", unix.ErrnoName(errno), errno)
}
return nil
}
// EnableSWTimestampsRx enables SW RX timestamps on the socket
func EnableSWTimestampsRx(connFd int) error {
flags := unix.SOF_TIMESTAMPING_RX_SOFTWARE |
unix.SOF_TIMESTAMPING_SOFTWARE
// Allow reading of SW timestamps via socket
if err := unix.SetsockoptInt(connFd, unix.SOL_SOCKET, timestamping, flags); err != nil {
return err
}
return nil
}
// EnableSWTimestamps enables SW timestamps (TX and RX) on the socket
func EnableSWTimestamps(connFd int) error {
flags := unix.SOF_TIMESTAMPING_TX_SOFTWARE |
unix.SOF_TIMESTAMPING_RX_SOFTWARE |
unix.SOF_TIMESTAMPING_SOFTWARE |
unix.SOF_TIMESTAMPING_OPT_TSONLY // Makes the kernel return the timestamp as a cmsg alongside an empty packet, as opposed to alongside the original packet.
// Allow reading of SW timestamps via socket
if err := unix.SetsockoptInt(connFd, unix.SOL_SOCKET, timestamping, flags); err != nil {
return err
}
if err := unix.SetsockoptInt(connFd, unix.SOL_SOCKET, unix.SO_SELECT_ERR_QUEUE, 1); err != nil {
return err
}
return nil
}
// EnableHWTimestamps enables HW timestamps (TX and RX) on the socket
func EnableHWTimestamps(connFd int, iface string) error {
if err := ioctlTimestamp(connFd, iface, hwtstampFilterAll); err != nil {
if err := ioctlTimestamp(connFd, iface, hwtstampFilterPTPv2Event); err != nil {
return err
}
}
// Enable hardware timestamp capabilities on socket
flags := unix.SOF_TIMESTAMPING_TX_HARDWARE |
unix.SOF_TIMESTAMPING_RX_HARDWARE |
unix.SOF_TIMESTAMPING_RAW_HARDWARE |
unix.SOF_TIMESTAMPING_OPT_TSONLY // Makes the kernel return the timestamp as a cmsg alongside an empty packet, as opposed to alongside the original packet.
// Allow reading of HW timestamps via socket
if err := unix.SetsockoptInt(connFd, unix.SOL_SOCKET, timestamping, flags); err != nil {
return err
}
if err := unix.SetsockoptInt(connFd, unix.SOL_SOCKET, unix.SO_SELECT_ERR_QUEUE, 1); err != nil {
return err
}
return nil
}
func waitForHWTS(connFd int) error {
// Wait until TX timestamp is ready
fds := []unix.PollFd{{Fd: int32(connFd), Events: unix.POLLPRI, Revents: 0}}
if _, err := unix.Poll(fds, 1); err != nil { // Timeout is 1 ms
return err
}
return nil
}
// recvoob receives only OOB message from the socket
// This is used for TX timestamp read of MSG_ERRQUEUE where we couldn't care less about other data.
// This is partially based on Recvmsg
// https://github.com/golang/go/blob/2ebe77a2fda1ee9ff6fd9a3e08933ad1ebaea039/src/syscall/syscall_linux.go#L647
func recvoob(connFd int, oob []byte) (oobn int, err error) {
var msg unix.Msghdr
msg.Control = &oob[0]
msg.SetControllen(len(oob))
_, _, e1 := unix.Syscall(unix.SYS_RECVMSG, uintptr(connFd), uintptr(unsafe.Pointer(&msg)), uintptr(unix.MSG_ERRQUEUE))
if e1 != 0 {
return 0, e1
}
return int(msg.Controllen), nil
}
// ReadTXtimestampBuf returns HW TX timestamp, needs to be provided 2 buffers which all can be re-used after ReadTXtimestampBuf finishes.
func ReadTXtimestampBuf(connFd int, oob, toob []byte) (time.Time, int, error) {
// Accessing hw timestamp
var boob int
txfound := false
// Sometimes we end up with more than 1 TX TS in the buffer.
// We need to empty it and completely otherwise we end up with a shifted queue read:
// Sync is out -> read TS from the previous Sync
// Because we always perform at least 2 tries we start with 0 so on success we are at 1.
attempts := 0
for ; attempts < maxTXTS; attempts++ {
if !txfound {
// Wait for the poll event, ignore the error
_ = waitForHWTS(connFd)
}
tboob, err := recvoob(connFd, toob)
if err != nil {
// We've already seen the valid TX TS and now we have an empty queue.
// All good
if txfound {
break
}
// Keep looking for a valid TX TS
continue
}
// We found a valid TX TS. Still check more if there is a newer one
txfound = true
boob = tboob
copy(oob, toob)
}
if !txfound {
return time.Time{}, attempts, fmt.Errorf("no TX timestamp found after %d tries", maxTXTS)
}
timestamp, err := socketControlMessageTimestamp(oob[:boob])
return timestamp, attempts, err
}
// ReadTXtimestamp returns HW TX timestamp
func ReadTXtimestamp(connFd int) (time.Time, int, error) {
// Accessing hw timestamp
oob := make([]byte, ControlSizeBytes)
// TMP buffers
toob := make([]byte, ControlSizeBytes)
return ReadTXtimestampBuf(connFd, oob, toob)
}
// socketControlMessageTimestamp is a very optimised version of ParseSocketControlMessage
// https://github.com/golang/go/blob/2ebe77a2fda1ee9ff6fd9a3e08933ad1ebaea039/src/syscall/sockcmsg_unix.go#L40
// which only parses the timestamp message type.
func socketControlMessageTimestamp(b []byte) (time.Time, error) {
mlen := 0
for i := 0; i < len(b); i += mlen {
h := (*unix.Cmsghdr)(unsafe.Pointer(&b[i]))
mlen = int(h.Len)
// depending on the kernel version, when we ask for SO_TIMESTAMPING_NEW we still might get messages with type SO_TIMESTAMPING
if h.Level == unix.SOL_SOCKET && int(h.Type) == unix.SO_TIMESTAMPING_NEW || int(h.Type) == unix.SO_TIMESTAMPING {
return scmDataToTime(b[i+socketControlMessageHeaderOffset : i+mlen])
}
}
return time.Time{}, fmt.Errorf("failed to find timestamp in socket control message")
}