This page was written before the new TTY layer had been committed. The new layer has since been committed in August 2008 (SVN r181905), and is the default in FreeBSD 8.0 and later.


Like almost any other UNIX-like operating system, FreeBSD has a TTY layer. A TTY layer allows you to do a lot of things, but in most situations it's used by users to run commands in a shell. FreeBSD's TTY layer works like it should, but it has a lot of room for improvements:

I (EdSchouten) am going to work on the TTY layer for my internship the first half year of 2008. I am willing to work on it even further after I've graduated. My work will be stored in the //depot/user/ed/mpsafetty branch in Perforce.

Design notes

This chapter will describe the differences between the old and the new TTY layer. It's not a formal guide (yet), but it should make it easier to port existing drivers to the new code.

First off, I'll summarize the major differences between the old and the new TTY layer, so we get a better idea why the API is like it is:

The basics: allocating TTY's

The first thing most device drivers can do, is allocate the actual TTY. The new TTY code uses a two-step procedure to allocate TTY's:

struct tty *tty_alloc(struct ttydevsw *, void *, struct mtx *);
void    tty_makedev(struct tty *, struct ucred *, const char *, ...) __printflike(3, 4);

They can be used like this:

struct tty *tp;

tp = tty_alloc(&my_device_class, pointer_i_want_to_store_inside_the_tty, NULL);
tty_makedev(tp, credentials, "z%d", 0);

This will create a device node called ttyz0 and maybe some nodes called cuaz0, ttyz0.lock, etc. There are a couple of things I haven't really described yet, so I will do this here:

The &my_device_class is a reference to a ttydevsw structure, which describes the class of devices we're dealing with. my_device_class could have looked like this:

static struct ttydevsw my_device_class = {
        .tsw_flags          = // binary flags such as TF_NOPREFIX (no "tty" prefix for device node names), TF_INITLOCK (create ".init" and ".lock" nodes) and TF_CALLOUT (create "cua" nodes).
        .tsw_open           = // function to notify the driver that the device is being opened
        .tsw_close          = // function to notify the driver that the device is being closed
        .tsw_outwakeup      = // function to notify the driver that data is available in the output buffers of the TTY
        .tsw_inwakeup       = // function to notify the driver that after the driver was unable to deliver (rint) data to the TTY, the TTY has now successfully made space so that the driver can rint() again (yes, i know, hard to describe, but it's somewhat the converse of outwakeup).
        .tsw_ioctl          = // may be used by the driver to add additional ioctl()'s to the device nodes (return ENOIOCTL on unimplemented ones)
        .tsw_param          = // device parameters changes (baud rate, flow control, etc)
        .tsw_modem          = // modem flags changed
        .tsw_mmap           = // XXX: only used by syscons: syscons allows you to mmap() a TTY to get a mapping to a screen buffer
        .tsw_free           = // we'll discuss this later: a callback to notify the driver that the TTY has been successfully remove from the system

(Please take a look at sys/ttydevsw.h for details)

A nice thing is that none of the functions need to be implemented. The first time you allocate a TTY using this device class, it patches all the NULL pointers in the structure to a default implementation, which does mostly nothing. open(), close(), inwakeup() and ioctl() do nothing, param() just enforces some default settings (hardcoding the baud rate at 38400, to enforce a sane buffer size), but some may cause a panic when called. It makes no sense not to implement a free() routine when your driver implements hotplugging, because the driver will need to know when the device unit number may be reclaimed.

Now the second argument of tty_alloc(): just like si_drv1 and si_drv2, the TTY structure has its own pointer space that can be used by the device driver. Because we don't allow access to the devfs device nodes, we will not toy around with si_drvX anymore. You can use tty_softc() to obtain the value from the TTY pointer.

The third argument: the mutex. In most cases you just want to pass NULL here. There are some reasons why you don't want to pass NULL:

But keep in mind: we could decide to remove the mutex-argument from tty_alloc() somewhere in the very far future. It is just a tool to make things a little more simple right now.

Now that we've discussed tty_alloc(), let's take a look at the tty_makedev(). This routine should be called once the driver is ready and it is safe to expose our TTY to the world. It may create one, but can create up to six device nodes in /dev, depending on the tsw_flags. The cred argument you're seeing: by default tty_makedev() creates TTY's with system-like privileges (root:wheel), but when credentials are passed, it does the following:

This should mainly be used by device drivers where we can say: they are owned by the user who wants them. It should be used by pts(4) and nmdm(4).

Interacting with TTY's

Now that you've got yourself a TTY and know about the hooks it offers - how do I extract data from it? This is the basic list of functions you can use:

// indicate a change in modem events (carrier drop)
void    ttydisc_modem(struct tty *, int);
// is the TTY in an optimized state, allowing me to pass the entire buffer at once? similar to TF_CAN_BYPASS_L_RINT
#define ttydisc_can_bypass(tp) ((tp)->t_flags & TF_BYPASS)
// deliver a single byte of input to the TTY layer. the 3rd argument is a bitmask of the TRE_ definitions in sys/ttydisc.h
int     ttydisc_rint(struct tty *, char, int);
// deliver an entire buffer. will panic if ttydisc_can_bypass(tp) == 0. then you should deliver it to ttydisc_rint() with error data
size_t  ttydisc_rint_bypass(struct tty *, char *, size_t);
// this should be called after rint()'ing data! unlike the old implementation, we don't generate wakeups after each byte has been delivered. you have to do it manually!
void    ttydisc_rint_done(struct tty *);
// obtain output data from the TTY
size_t  ttydisc_getc(struct tty *, void *buf, size_t);
// do not use: `fast path' way to copy data directly to userspace. i guess only pts(4) will use this.
int     ttydisc_getc_uio(struct tty *, struct uio *);

(Please take a look at sys/ttydisc.h for details)


Locking is quite easy if you remember the following rules:

Abandoning a TTY

The TTY layer uses two steps to remove a TTY:

Flow control

There is a big difference in the way the new TTY code handles flow control. Unlike the old layer where the driver just checks TS_* flags before poking around in the inq, the new TTY code uses a very simple mechanism:

This may make older code a little harder to port, but it should make sense. An advantage of this model is that it is both suited for physical and virtual drivers. The pts(4) driver may need to actually implement blocking when writing too much data to a PTY master device. The old TTY layer didn't really offer a mechanism for this without patching the cdev routines.


Be sure to take a look at the really primitive drivers. I'll discuss the IA64 /sys/ia64/ia64/ssc.c driver here, which is only about 180 lines. Please refer to the original file. The following pieces of code are just the interesting snippets (which are (C) Doug Rabson).

The TTY device class

As you can see, the interesting part of the driver starts here:

static tsw_open_t       ssc_open;
static tsw_outwakeup_t  ssc_outwakeup;
static tsw_close_t      ssc_close;

static struct ttydevsw ssc_class = {
        .tsw_flags      = TF_NOPREFIX,
        .tsw_open       = ssc_open,
        .tsw_outwakeup  = ssc_outwakeup,
        .tsw_close      = ssc_close,

The SSC driver only implements three functions, namely the open() and close() routines to start and stop a timer, which polls the interface, because it does not seem to be interrupt driven. It also implements outwakeup(), which is used to trigger the driver to start transmitting data.

The actual TTY is allocated inside ssc_cnattach(), which is called by the low-level SYSINIT macro:

static void
ssc_cnattach(void *arg)
        struct tty *tp;

        tp = tty_alloc(&ssc_class, NULL, NULL);
        tty_makedev(tp, NULL, "ssccons");

SYSINIT(ssc_cnattach, SI_SUB_DRIVERS, SI_ORDER_ANY, ssc_cnattach, 0);

The three routines referred by the ttydevsw look like this:

static int
ssc_open(struct tty *tp)

        polltime = hz / SSC_POLL_HZ;
        if (polltime < 1)
                polltime = 1;
        ssc_timeouthandle = timeout(ssc_timeout, tp, polltime);

        return (0);

static void
ssc_close(struct tty *tp)

        untimeout(ssc_timeout, tp, ssc_timeouthandle);

static void
ssc_outwakeup(struct tty *tp)
        char buf[128];
        size_t len, c;

        for (;;) {
                len = ttydisc_getc(tp, buf, sizeof buf);
                if (len == 0)

                c = 0;
                while (len-- > 0)
                        ssc_cnputc(NULL, buf[c++]);

The open() and close() routines are simple to understand: depending on hz, we calculate the interval for a timeout. The outwakeup() routine is where the interesting bits are: the driver just obtains the data from the TTY and writes it byte by byte to to the physical hardware.

The ssc_timeout() routine is called to poll the hardware and store the input inside the TTY:

static void
ssc_timeout(void *v)
        struct tty *tp = v;
        int c;

        while ((c = ssc_cngetc(NULL)) != -1)
                ttydisc_rint(tp, c, 0);

        ssc_timeouthandle = timeout(ssc_timeout, tp, polltime);

Because ssc_cngetc() just returns the characters one by one, we need to rint() them. When we're done, we must not forget to call rint_done() to wake up any blocked threads. Please note that it's not bad to call ttydisc_rint_done() even if you weren't able to rint() any data, because the TTY layer automatically filters spurious wakeups.


Send me an email or poke me on IRC!

Designing the new TTY Hooks layer

We need some kind of mechanism to make in-kernel TTY consumers work again. Right now we've got 5 broken drivers (snp, ppp, sl, ng_h4, ht_tty), which we should somehow fix. Right now the snp(4) driver works like this. The function names used inside the TTY layer refer to the new TTY layer.

                                             | old snp(4) input
<-------|             INPUT PROCESSING        |<------------------
 read() | ttydisc_read()       ttydisc_rint() | driver_intr()
        |                                     |
        |               TTY LAYER             |
        |                                     |
------->|            OUTPUT PROCESSING        |------------------>
write() | ttydisc_write()      ttydisc_getc() | driver_outwakeup()
         v old snp(4) output

I don't entirely agree with this design, because things probably go bad when you turn on the various output processing options. You'd be better off switching your terminal to some kind of raw mode and let snp(4) copy all the data that gets getc()'d by the driver. When you want to inject data, you want to do it at the same spot. That's why I propose the following design:

                                            |^ Capture/inject hooks on the input path
<-------|             INPUT PROCESSING        |<------------------
 read() | ttydisc_read()       ttydisc_rint() | driver_intr()
        |                                     |
        |               TTY LAYER             |
        |                                     |
------->|            OUTPUT PROCESSING        |------------------>
write() | ttydisc_write()      ttydisc_getc() | driver_outwakeup()
                                           |v Capture/inject hooks on the input path

So basically we've got two different categories of hooks:

XXX: propose API?


TTYRedesign (last edited 2021-03-30T07:33:38+0000 by KubilayKocak)