Monday, October 24, 2011

The Unix serial port output buffer

Unix serial ports have associated input and output buffers in the kernel. When writing data to the port, if sufficient space is available in the output buffer, the write call returns after copying the data into the buffer. The hardware slowly outputs data from the buffer, freeing space for more data. A program can easily fill up the buffer, because the CPU works much faster than the serial port. When insufficient space is available in the buffer, a normal blocking write will wait. It will not simply wait for sufficient space; instead, it will wait for the buffer to empty to a specific low limit.

When simply sending large amounts of data, this behaviour is appropriate and efficient. Programs don't need to constantly wake to feed more data to the hardware. Instead, they can put a lot of data into the buffer and then sleep until the buffer gets to that low limit. However, for some applications the buffer is a problem.

Bytes that are already in the buffer form a delay between when a new byte is written and when it is actually output from the port. (All those bytes need to be output first.) As a result, serial responses and actions from serially controlled hardware may be delayed. If a program performs checks to see whether to continue sending data, that will also be delayed when a write call waits for the output buffer to empty.

In Linux the output buffer is one page (typically 4KB), and the process is woken when only 256 bytes (set via WAKEUP_CHARS) remain. That may seem like a small amount of memory, but at low baud rates, it corresponds to a long time. There is no standard way to change the buffer, and in Linux, it could only be changed by editing kernel source code and recompiling the kernel.

It is simplest to wait for data to be output after writes. This can be done by waiting for responses after writs or via tcdrain. (Other methods such as fsync and O_SYNC cannot be relied on for serial ports.) Unfortunately, this defeats the efficiency benefits of buffering.

When the issue is a need to stop output quickly, instead of a need for constant low latency, data in the buffer can be discarded via tcflush. However, this gets tricky. Since writes can wait for a long time, some form of asynchronous I/O is needed. Once the buffer is flushed, serial controlled hardware may be in an unknown state, which complicates things.

As an added benefit, tcflush and tcdrain also deal with the hardware buffer. In most built in serial ports, these buffers are tiny. However, USB to serial adapters can have large buffers.

No comments: