#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/select.h>
#include <termios.h>
#include <inttypes.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>
/* Maximum number of devices to be benchmarked in parallel. */
#ifndef MAX_DEVS
#define MAX_DEVS 32
#endif
/* Microseconds to wait for I/O per round */
#ifndef WAIT_US
#define WAIT_US 1000
#endif
/* Signal handler */
static volatile sig_atomic_t done = 0;
static inline void handle_done(int signum)
{
done = 1;
}
static int install_done(int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
return sigaction(signum, &act, NULL);
}
/* Device stages. */
enum {
STAGE_READY = 0,
STAGE_IO = 1,
STAGE_DONE = 3,
};
/* Device data. */
static int dev_count = 0;
static const char *dev_path[MAX_DEVS];
static int dev_fd[MAX_DEVS];
static struct termios dev_termios[MAX_DEVS];
/* Device ping-pong state */
static int dev_stage[MAX_DEVS];
static int dev_errs[MAX_DEVS]; /* Errors */
/* Input buffers */
static uint64_t *dev_inbuf[MAX_DEVS];
static size_t dev_inlen[MAX_DEVS]; /* Bytes */
/* Output buffers */
static uint64_t *dev_outbuf[MAX_DEVS];
static size_t dev_outlen[MAX_DEVS]; /* Bytes */
/* Message roundtrip times per device */
static size_t dev_rtts[MAX_DEVS];
static uint64_t *dev_rtt[MAX_DEVS];
static uint64_t dev_ns[MAX_DEVS]; /* Total duration */
/* Helper: Open device. */
static int dev_open(const char *path, struct termios *term)
{
struct termios oldterm;
int fd, saved_errno;
/* Unnecessary sanity check... */
if (!path || !*path) {
errno = ENOENT;
return -1;
}
/* Open device. */
fd = open(path, O_RDWR | O_NONBLOCK);
if (fd == -1)
return -1;
do {
if (tcgetattr(fd, &oldterm) == -1)
break;
/* Save original terminal settings */
if (term)
memcpy(term, &oldterm, sizeof oldterm);
oldterm.c_iflag = IGNBRK | IGNPAR;
oldterm.c_oflag = 0;
oldterm.c_cflag = CS8 | CREAD;
oldterm.c_lflag = 0;
oldterm.c_cc[VMIN] = 1;
oldterm.c_cc[VTIME] = 0;
cfsetispeed(&oldterm, B230400);
cfsetospeed(&oldterm, B230400);
/* Technically, this is not guaranteed to have
set all the flags we asked; close enough for us. */
if (tcsetattr(fd, TCSAFLUSH, &oldterm) == -1)
break;
/* Success. */
return fd;
} while (0);
saved_errno = errno;
close(fd);
errno = saved_errno;
return -1;
}
/* Helper: Wall clock "nanosecond" timer. */
static inline uint64_t nanoseconds(void)
{
struct timespec now;
if (clock_gettime(CLOCK_REALTIME, &now) == 0)
return ((uint64_t)now.tv_sec * UINT64_C(1000000000)) + (uint64_t)now.tv_nsec;
else
return 0;
}
/* Helper: Compare elapsed nanoseconds. */
static int compare_nanoseconds(const void *p1, const void *p2)
{
const uint64_t ns1 = *(const uint64_t *)p1;
const uint64_t ns2 = *(const uint64_t *)p2;
return (ns1 < ns2) ? -1 :
(ns1 > ns2) ? +1 :
0 ;
}
/* Xorshift64* PRNG for filling in message payloads with pseudo-random data. */
static uint64_t prng_state = 1;
static uint64_t prng_u64(void)
{
uint64_t state = prng_state;
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
prng_state = state;
return state * UINT64_C(2685821657736338717);
}
/* Helper: Fill buffer with pseudorandom data.
Assumes at least an uint64_t of padding at end. */
static void prng_fill(uint64_t *msg, size_t bytes)
{
uint64_t *const end = (uint64_t *)((unsigned char *)msg + bytes);
uint64_t *u64 = (uint64_t *)msg;
/* end may not be aligned, but that's okay; we never dereference it. */
while (u64 < end)
*(u64++) = prng_u64();
}
int main(int argc, char *argv[])
{
size_t msglen, msgcount, nsent, nreceived, nidle;
time_t updated;
int arg, i;
char dummy;
if (argc < 4) {
/* In case executed from another program without argv[0]. */
if (argc < 1 || !argv[0])
return EXIT_FAILURE;
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s MSGLEN COUNT DEVICE [ DEVICE ... DEVICE ]\n", argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "Where:\n");
fprintf(stderr, " MSGLEN is the length of each message in bytes,\n");
fprintf(stderr, " COUNT is the number of messages per device, and\n");
fprintf(stderr, " DEVICE is one or more serial devices (ttys) to use.\n");
fprintf(stderr, "\n");
fprintf(stderr, "This program sends a message to each DEVICE, measuring the\n");
fprintf(stderr, "wall-clock roundtrip time, COUNT times, also verifying\n");
fprintf(stderr, "the integrity of the message payload.\n");
fprintf(stderr, "\n");
fprintf(stderr, "Up to %d DEVICEs can be benchmarked in parallel.\n", MAX_DEVS);
fprintf(stderr, "\n");
fprintf(stderr, "The program will run until interrupted with INT (Ctrl+C), TERM, or HUP signal.\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
/* Parse message length. */
if (sscanf(argv[1], " %zu %c", &msglen, &dummy) != 1 || msglen < 1) {
fprintf(stderr, "%s: Invalid message length.\n", argv[1]);
return EXIT_FAILURE;
}
/* Parse message count. */
if (sscanf(argv[2], " %zu %c", &msgcount, &dummy) != 1 || msgcount < 1) {
fprintf(stderr, "%s: Invalid number of messages.\n", argv[2]);
return EXIT_FAILURE;
}
/* Install signal handlers, for aborting the benchmark. */
if (install_done(SIGINT) == -1 ||
install_done(SIGHUP) == -1 ||
install_done(SIGTERM) == -1) {
fprintf(stderr, "Cannot use signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
/* Open each device. */
for (arg = 3; arg < argc; arg++) {
/* Skip empty device names. */
if (!argv[arg] || !*argv[arg])
continue;
/* Too many devices? */
if (dev_count >= MAX_DEVS) {
fprintf(stderr, "Too many devices!\n");
return EXIT_FAILURE;
}
/* Open device. */
dev_path[dev_count] = argv[arg];
dev_errs[dev_count] = 0;
dev_fd[dev_count] = dev_open(argv[arg], &dev_termios[dev_count]);
if (dev_fd[dev_count] == -1) {
fprintf(stderr, "%s: %s.\n", argv[arg], strerror(errno));
return EXIT_FAILURE;
}
if (dev_fd[dev_count] >= FD_SETSIZE) {
fprintf(stderr, "%s: Too many devices for select() to support.\n", argv[arg]);
return EXIT_FAILURE;
}
/* Pad the buffers so we can fill them with full uint64_t's. */
dev_inbuf[dev_count] = malloc(msglen + sizeof (uint64_t));
dev_outbuf[dev_count] = malloc(msglen + sizeof (uint64_t));
if (!dev_inbuf[dev_count] || !dev_outbuf[dev_count]) {
fprintf(stderr, "%s: Not enough memory for message buffers.\n", argv[arg]);
return EXIT_FAILURE;
}
dev_inlen[dev_count] = 0;
dev_outlen[dev_count] = 0;
/* Allocate roundtrip time measurements. */
dev_rtt[dev_count] = malloc((msgcount + 1) * sizeof dev_rtt[dev_count][0]);
if (!dev_rtt[dev_count]) {
fprintf(stderr, "%s: Not enough memory for roundtrip timing measurements.\n", argv[arg]);
return EXIT_FAILURE;
}
dev_rtts[dev_count] = 0;
dev_count++;
}
nidle = 0;
nsent = 0;
nreceived = 0;
updated = (time_t)0;
/* Benchmarking loop. */
while (!done) {
ssize_t bytes;
time_t now;
int dones = 0, reads = 0, writes = 0;
now = time(NULL);
if (now != updated) {
fprintf(stdout, "\r%zu sent, %zu received; %zu idle ", nsent, nreceived, nidle);
fflush(stdout);
updated = now;
}
for (i = 0; i < dev_count; i++) {
if (dev_stage[i] == STAGE_READY) {
/* Prepare a new message. */
prng_fill(dev_outbuf[i], msglen);
/* Record timestamp. */
dev_rtt[i][dev_rtts[i]] = nanoseconds();
/* If the very first message, record start time also. */
if (dev_rtts[i] == 0)
dev_ns[i] = dev_rtt[i][0];
/* Be ready to send this message immediately. */
dev_stage[i] = STAGE_IO;
dev_outlen[i] = 0;
/* No data received yet from this device. */
dev_inlen[i] = 0;
}
if (dev_stage[i] == STAGE_IO && dev_outlen[i] < msglen) {
/* Try sending more data. */
bytes = write(dev_fd[i], (char *)dev_outbuf[i] + dev_outlen[i], msglen - dev_outlen[i]);
if (bytes > 0) {
dev_outlen[i] += bytes;
nsent += bytes;
writes++;
} else
if (bytes == -1 && (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)) {
fprintf(stderr, "Warning: %s: %s.\n", dev_path[i], strerror(errno));
}
}
if (dev_stage[i] == STAGE_IO) {
/* Try receiving more data. */
if (dev_inlen[i] < msglen) {
bytes = read(dev_fd[i], (char *)dev_inbuf[i] + dev_inlen[i], msglen - dev_inlen[i]);
if (bytes > 0) {
dev_inlen[i] += bytes;
nreceived += bytes;
reads++;
}
}
/* All data received? */
if (dev_inlen[i] >= msglen) {
/* Record roundtrip duration, */
dev_rtt[i][dev_rtts[i]] = nanoseconds() - dev_rtt[i][dev_rtts[i]];
/* Compare input and output buffers. */
if (memcmp(dev_inbuf[i], dev_outbuf[i], msglen))
dev_errs[i]++;
dev_rtts[i]++;
if (dev_rtts[i] < msgcount) {
/* Rinse and repeat. */
dev_stage[i] = STAGE_READY;
} else {
/* Record full exchange duration. */
dev_ns[i] = nanoseconds() - dev_ns[i];
/* This device is done; close it. */
dev_stage[i] = STAGE_DONE;
close(dev_fd[i]);
dev_fd[i] = -1;
}
}
}
/* Count number of devices that have completed the benchmark. */
if (dev_stage[i] == STAGE_DONE)
dones++;
}
/* All done? */
if (dones >= dev_count)
break;
/* If we did any I/O, there is no need to sleep. */
if (reads > 0 || writes > 0)
continue;
nidle++;
#if defined(WAIT_US) && (WAIT_US-0 > 0)
/* Sleep until I/O ought to be possible. */
{
struct timeval waittime;
fd_set readfds, writefds, exceptfds;
int maxfd = -1;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
waittime.tv_sec = (WAIT_US / 1000000);
waittime.tv_usec = (WAIT_US % 1000000);
for (i = 0; i < dev_count; i++) {
if (dev_stage[i] == STAGE_IO) {
if (dev_inlen[i] < msglen) {
const int fd = dev_fd[i];
FD_SET(fd, &readfds);
if (maxfd < fd)
maxfd = fd;
}
if (dev_outlen[i] < msglen) {
const int fd = dev_fd[i];
FD_SET(fd, &writefds);
if (maxfd < fd)
maxfd = fd;
}
}
}
if (maxfd > 0) {
/* We don't actually care which devices are readable/writable,
since we do tentative reads and writes on each device. */
(void)select(maxfd + 1, &readfds, &writefds, &exceptfds, &waittime);
}
}
#endif
}
/* Make sure all devices are closed. */
for (i = 0; i < dev_count; i++) {
if (dev_fd[i] != -1) {
close(dev_fd[i]);
dev_fd[i] = -1;
}
}
/* Compile statistics. */
for (i = 0; i < dev_count; i++) {
/* Copy first roundtrip time to end of buffer. */
dev_rtt[i][msgcount] = dev_rtt[i][0];
/* Sort existing roundtrip times in ascending order. */
if (dev_rtts[i] > 1) {
qsort(dev_rtt[i], dev_rtts[i], sizeof dev_rtt[i][0], compare_nanoseconds);
}
}
fprintf(stdout, "\r%zu sent, %zu received; %zu idle.\n", nsent, nreceived, nidle);
/* Print statistics. */
printf("#\tErrors\tBandwidth [bytes/sec]\tFirst [ms]\tRTTmin [ms]\tRTTmedian [ms]\tRTTmax [ms]\n");
for (i = 0; i < dev_count; i++) {
printf("%s\t%d\t", dev_path[i], dev_errs[i]);
/* Total bandwidth known? */
if (dev_stage[i] == STAGE_DONE && dev_ns[i] > 0) {
printf("%.3f", (double)msgcount * (double)msglen * 1000000000.0 / (double)dev_ns[i]);
} else {
printf("N/A");
}
/* Roundtrips measured? */
if (dev_rtts[i] > 0) {
printf("\t%.3f\t%.3f\t%.3f\t%.3f\n",
(double)dev_rtt[i][msgcount] / 1000000.0, /* First roundtrip in ms */
(double)dev_rtt[i][0] / 1000000.0, /* Fastest roundtrip in ms */
(double)dev_rtt[i][dev_rtts[i]/2] / 1000000.0, /* Median */
(double)dev_rtt[i][dev_rtts[i]-1] / 1000000.0); /* Maximum */
} else {
printf("\n");
}
}
return EXIT_SUCCESS;
}