This is an old revision of the document!
Introduction
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:
- The TTY layer still uses the Giant Lock (see SMPTODO)
- The buffering mechanisms used inside the TTY objects could be a lot smarter (less memory overhead, less copying of data)
- The layer lacks any form of abstraction, which makes it really hard to add any new features to this layer
- Hotplugging isn't handled properly
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:
- TTY drivers no longer interact with devfs and character device routines directly. The TTY layer now resides between devfs and the device driver. This means you must no longer create device nodes with routes patches to the TTY layer directly.
- By default, each TTY has its own mutex to protect its internal datastructures. This TTY can also be used by the device driver, though it may be smarted to split it up and use a mutex to protect the driver's data. To ease migration and implementation of various drivers, a driver can force the TTY layer to use a specific mutex, instead of a per-TTY one.
- Instead of patching up the routines inside the tty structure, the new TTY layer has a per-class structure, similar to cdevsw. This means you cannot patch routines inside the TTY layer after allocating it.
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
.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
};
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:
- If you pass &Giant, the TTY will be locked down using the Giant lock. This means the following:
- All the ttydevsw routines will be called by the TTY layer holding Giant, just like the old TTY layer which used D_NEEDGIANT.
- You can just call into the TTY layer without calling TTY layer locking routines first. If you hold Giant, you can just rint(), etc.
- Drivers like nmdm are a little complex to lock down: two TTY's talking to each other. To keep it simple right now, we just allocate one mutex to lock both of them.
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:
- Devices are created with ownership of the real user ID of the process.
- The group is set to tty.
- Permissions are set to 0640.
- Device nodes are protected from being opened from another jail.
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 *);
Locking
Locking is quite easy if you remember the following rules:
- Always hold the TTY lock when entering the ttydisc_*() routines.
- The lock is always held inside the ttydevsw routines.
Abandoning a TTY
The TTY layer uses two steps to remove a TTY:
- The driver calls tty_rel_gone() while holding the TTY lock. The lock is unlocked by the TTY layer. Do not touch the TTY after you've called this. (ACHTUNG!)
- When the TTY layer is finished evacuating all the threads and removing the device nodes from /dev, it calls tsw_free, passing only the pointer to the softc, which means you can now clean up your data and also reclaim the device unit number. You MUST NOT reuse the number before tsw_free is called, because this means we'll have two devices in /dev sharing the same name.
Examples
Be sure to take a look at the really primitive drivers. Take a look at ia64's ssc.c, for example. It contains an example of a very simple driver that interacts with the TTY layer.
Questions?
Send me an email or poke me on IRC!