Example 6-1 provides a terse, source-level overview of nmdm(4)
.
Example 6-1. nmdm.c
#include <sys/param.h> #include <sys/module.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/tty.h> #include <sys/conf.h> #include <sys/eventhandler.h> #include <sys/limits.h> #include <sys/serial.h> #include <sys/malloc.h> #include <sys/queue.h> #include <sys/taskqueue.h> #include <sys/lock.h> #include <sys/mutex.h> MALLOC_DEFINE(M_NMDM, "nullmodem", "nullmodem data structures"); struct nmdm_part { struct tty *np_tty; struct nmdm_part *np_other; struct task np_task; struct callout np_callout; int np_dcd; int np_rate; u_long np_quota; int np_credits; u_long np_accumulator; #define QS 8 /* Quota shift. */ }; struct nmdm_softc { struct nmdm_part ns_partA; struct nmdm_part ns_partB; struct mtx ns_mtx; }; static tsw_outwakeup_t nmdm_outwakeup; static tsw_inwakeup_t nmdm_inwakeup; static tsw_param_t nmdm_param; static tsw_modem_t nmdm_modem; static struct ttydevsw nmdm_class = { .tsw_flags = TF_NOPREFIX, .tsw_outwakeup = nmdm_outwakeup, .tsw_inwakeup = nmdm_inwakeup, .tsw_param = nmdm_param, .tsw_modem = nmdm_modem }; static int nmdm_count = 0; static void nmdm_timeout(void *arg) { ... } static void nmdm_task_tty(void *arg, int pending __unused) { ... } static struct nmdm_softc * nmdm_alloc(unsigned long unit) { ... } static void nmdm_clone(void *arg, struct ucred *cred, char *name, int len, struct cdev **dev) { ... } static void nmdm_outwakeup(struct tty *tp) { ... } static void nmdm_inwakeup(struct tty *tp) { ... } static int bits_per_char(struct termios *t) { ... } static int nmdm_param(struct tty *tp, struct termios *t) { ... } static int nmdm_modem(struct tty *tp, int sigon, int sigoff) { ... } static int nmdm_modevent(module_t mod __unused, int event, void *arg __unused) { ... } DEV_MODULE(nmdm, nmdm_modevent, NULL);
Example 6-1 is provided as a convenience; as I go through the code for nmdm(4)
you can refer to it to see how nmdm(4)
’s functions and structures are laid out.
To make things easier to understand, I’ll detail the functions and structures in nmdm(4)
in the order I would’ve written them (instead of in the order they appear). To that end, we’ll begin with the module event handler.
The nmdm_modevent
function is the module event handler for nmdm(4)
. Here is its function definition:
static int nmdm_modevent(module_t mod __unused, int event, void *arg __unused) { static eventhandler_tag tag; switch (event) { case MOD_LOAD: tag = EVENTHANDLER_REGISTER(dev_clone, nmdm_clone, 0, 1000); if (tag == NULL) return (ENOMEM); break; case MOD_SHUTDOWN: break; case MOD_UNLOAD: if (nmdm_count != 0) return (EBUSY); EVENTHANDLER_DEREGISTER(dev_clone, tag); break; default: return (EOPNOTSUPP); } return (0); }
On module load, this function registers the function nmdm_clone
with the event handler dev_clone
.
The dev_clone
event handler was described in Table 5-1 in Don’t Panic.
Recall that functions registered with dev_clone
are called when a solicited item under /dev
does not exist. So when a nmdm(4)
device node is accessed for the first time, nmdm_clone
will be called to create the device node on the fly. Interestingly, this on-the-fly device creation lets one create an unlimited number of nmdm(4)
device nodes.
On module unload, this function begins by checking the value of nmdm_count
.
The variable nmdm_count
is declared near the beginning of Example 6-1 as an integer initialized to 0
.
nmdm_count
counts the number of active nmdm(4)
device nodes. If it equals 0
, nmdm_clone
is removed from the event handler dev_clone
; otherwise, EBUSY
(which stands for error: device busy) is returned.
As mentioned in the previous section, nmdm_clone
creates nmdm(4)
device nodes on the fly. Note that all nmdm(4)
device nodes are created in pairs named nmdm%lu%c
, where %lu
is the unit number and %c
is either A
or B
. Here is the function definition for nmdm_clone
:
static void nmdm_clone(void *arg, struct ucred *cred, char *name, int len, struct cdev **dev) { unsigned long unit; char *end; struct nmdm_softc *ns; if (*dev != NULL) return; if (strncmp(name, "nmdm", 4) != 0) return; /* Device name must be "nmdm%lu%c", where %c is "A" or "B". */ name += 4; unit = strtoul(name, &end, 10); if (unit == ULONG_MAX || name == end) return; if ((end[0] != 'A' && end[0] != 'B') || end[1] != ' ') return; ns = nmdm_alloc(unit); if (end[0] == 'A') *dev = ns->ns_partA.np_tty->t_dev; else *dev = ns->ns_partB.np_tty->t_dev; }
This function first checks the value of *dev
(which is a character device pointer). If *dev
does not equal NULL
, which implies that a device node already exists, nmdm_clone
exits (because no nodes need to be created). Next, nmdm_clone
ensures that the first four characters in name
are equal to nmdm
; otherwise it exits (because the solicited device node is for another driver). Then the fifth character in name
, which should be a unit number, is converted to an unsigned long and stored in unit
. The following if
statement checks that the conversion was a success. Afterward, nmdm_clone
ensures that following the unit number (in name
) is the letter A
or B
; otherwise it exits. Now, having confirmed that the solicited device node is indeed for this driver, nmdm_alloc
is called to actually create the device nodes. Finally, *dev
is set to the solicited device node (either nmdm%luA
or nmdm%luB
).
Note that since nmdm_clone
is registered with dev_clone
, its function prototype must conform to the type expected by dev_clone
, which is defined in <sys/conf.h>
.
As mentioned in the previous section, nmdm_alloc
actually creates nmdm(4)
’s device nodes. Before I describe this function, an explanation of nmdm_class
is needed.
The data structure nmdm_class
is declared near the beginning of Example 6-1 as a TTY device switch table.
static struct ttydevsw nmdm_class = { .tsw_flags = TF_NOPREFIX, .tsw_outwakeup = nmdm_outwakeup, .tsw_inwakeup = nmdm_inwakeup, .tsw_param = nmdm_param, .tsw_modem = nmdm_modem };
The flag TF_NOPREFIX
means don’t prefix tty to the device name
. The other definitions are the operations that nmdm_class
supports. These operations will be described as we encounter them.
Now that you’re familiar with nmdm_class
, let’s walk through nmdm_alloc
.
static struct nmdm_softc * nmdm_alloc(unsigned long unit) { struct nmdm_softc *ns; atomic_add_int(&nmdm_count, 1); ns = malloc(sizeof(*ns), M_NMDM, M_WAITOK | M_ZERO); mtx_init(&ns->ns_mtx, "nmdm", NULL, MTX_DEF); /* Connect the pairs together. */ ns->ns_partA.np_other = &ns->ns_partB; TASK_INIT(&ns->ns_partA.np_task, 0, nmdm_task_tty, &ns->ns_partA); callout_init_mtx(&ns->ns_partA.np_callout, &ns->ns_mtx, 0); ns->ns_partB.np_other = &ns->ns_partA; TASK_INIT(&ns->ns_partB.np_task, 0, nmdm_task_tty, &ns->ns_partB); callout_init_mtx(&ns->ns_partB.np_callout, &ns->ns_mtx, 0); /* Create device nodes. */ ns->ns_partA.np_tty = tty_alloc_mutex(&nmdm_class, &ns->ns_partA, &ns->ns_mtx); tty_makedev(ns->ns_partA.np_tty, NULL, "nmdm%luA", unit); ns->ns_partB.np_tty = tty_alloc_mutex(&nmdm_class, &ns->ns_partB, &ns->ns_mtx); tty_makedev(ns->ns_partB.np_tty, NULL, "nmdm%luB", unit); return (ns); }
This function can be split into four parts. The first increments nmdm_count
by one via the atomic_add_int
function. As its name implies, atomic_add_int
is atomic. Consequently, we don’t need a lock to protect nmdm_count
when we increment it.
The second part allocates memory for a new nmdm_softc
structure. After that, its mutex is initialized. Besides a mutex, nmdm_softc
contains two additional member variables: ns_partA
and ns_partB
. These variables are nmdm_part
structures and will maintain data relating to nmdm%luA
or nmdm%luB
.
st
ruct nmdm_softc
is defined near the beginning of Example 6-1.
The third part connects the member variables ns_partA
and ns_partB
, so that given ns_partA
we can find ns_partB
, and vice versa. The third part also initializes ns_partA
’s and ns_partB
’s task
and callout
structures.
Finally, the fourth part creates nmdm(4)
’s device nodes (that is, nmdm%luA
and nmdm%luB
).
The nmdm_outwakeup
function is defined in nmdm_class
as the tsw_outwakeup
operation. It is executed when output from nmdm%luA
or nmdm%luB
is available. Here is its function definition:
static void nmdm_outwakeup(struct tty *tp) { struct nmdm_part *np = tty_softc(tp); /* We can transmit again, so wake up our side. */ taskqueue_enqueue(taskqueue_swi, &np->np_task); }
This function queues ns_partA
’s or ns_partB
’s task
structure on taskqueue_swi
(that is to say, it defers processing the output from nmdm%luA
and nmdm%luB
).
The nmdm_task_tty
function transfers data from nmdm%luA
to nmdm%luB
, and vice versa. This function is queued on taskqueue_swi
by nmdm_outwakeup
(for verification, see the third argument to TASK_INIT
in nmdm_alloc
). Here is its function definition:
static void nmdm_task_tty(void *arg, int pending __unused) { struct tty *tp, *otp; struct nmdm_part *np = arg; char c; tp = np->np_tty; tty_lock(tp); otp = np->np_other->np_tty; KASSERT(otp != NULL, ("nmdm_task_tty: null otp")); KASSERT(otp != tp, ("nmdm_task_tty: otp == tp")); if (np->np_other->np_dcd) { if (!tty_opened(tp)) { np->np_other->np_dcd = 0; ttydisc_modem(otp, 0); } } else { if (tty_opened(tp)) { np->np_other->np_dcd = 1; ttydisc_modem(otp, 1); } } while (ttydisc_rint_poll(otp) > 0) { if (np->np_rate && !np->np_quota) break; if (ttydisc_getc(tp, &c, 1) != 1) break; np->np_quota--; ttydisc_rint(otp, c, 0); } ttydisc_rint_done(otp); tty_unlock(tp); }
In this function’s explanation, “our TTY” refers to the TTY device (that is, nmdm%luA
or nmdm%luB
) that queued this function on taskqueue_swi
.
This function is composed of two parts. The first changes the connection state between the two TTYs to match the status of our TTY. If our TTY is closed and the other TTY’s Data Carrier Detect (DCD) flag is on, we turn off that flag and switch off their carrier signal. On the other hand, if our TTY has been opened and the other TTY’s DCD flag is off, we turn on that flag and switch on their carrier signal. In short, this part ensures that if our TTY is closed (that is, there is no data to transfer), the other TTY will not have a carrier signal, and if our TTY has been opened (that is, there is data to transfer), the other TTY will have a carrier signal. A carrier signal indicates a connection. In other words, loss of the carrier equates to termination of the connection.
The second part transfers data from our TTY’s output queue to the other TTY’s input queue. This part first polls the other TTY to determine whether it can accept data. Then one character is removed from our TTY’s output queue and placed in the other TTY’s input queue. These steps are repeated until the transfer is complete.
The nmdm_inwakeup
function is defined in nmdm_class
as the tsw_inwakeup
operation. It is called when input for nmdm%luA
or nmdm%luB
can be received again. That is, when nmdm%luA
’s or nmdm%luB
’s input queue is full and then space becomes available, this function is executed. Here is its function definition:
static void nmdm_inwakeup(struct tty *tp) { struct nmdm_part *np = tty_softc(tp); /* We can receive again, so wake up the other side. */ taskqueue_enqueue(taskqueue_swi, &np->np_other->np_task); }
In this function’s explanation, “our TTY” refers to the TTY device (that is, nmdm%luA
or nmdm%luB
) that executed this function.
This function queues the other TTY’s task structure on taskqueue_swi
. In other words, when input for our TTY can be received again, our TTY tells the other TTY to transfer data to it.
The nmdm_modem
function is defined in nmdm_class
as the tsw_modem
operation. This function sets or gets the modem control line state. Here is its function definition:
static int nmdm_modem(struct tty *tp, int sigon, int sigoff) { struct nmdm_part *np = tty_softc(tp); int i = 0; /* Set modem control lines. */ if (sigon || sigoff) { if (sigon & SER_DTR) np->np_other->np_dcd = 1; if (sigoff & SER_DTR) np->np_other->np_dcd = 0; ttydisc_modem(np->np_other->np_tty, np->np_other->np_dcd); return (0); /* Get state of modem control lines. */ } else { if (np->np_dcd) i |= SER_DCD; if (np->np_other->np_dcd) i |= SER_DTR; return (i); } }
In this function’s explanation, “our TTY” refers to the TTY device (that is, nmdm%luA
or nmdm%luB
) that executed this function.
This function sets the modem control lines when the sigon
(signal on) or the sigoff
(signal off) argument is nonzero. If sigon
contains the Data Terminal Ready (DTR) flag, the other TTY’s DCD flag is turned on. If sigoff
contains the DTR flag, the other TTY’s DCD flag is turned off. The other TTY’s carrier signal is turned on or off alongside its DCD flag.
If the preceding discussion didn’t make any sense to you, this should help: A null modem connects the DTR output of each serial port to the DCD input of the other. The DTR output is kept off until a program accesses the serial port and turns it on; the other serial port will sense this as its DCD input turning on. Thus, the DCD input is used to detect the readiness of the other side. This is why when our TTY’s DTR is sigon’d or sigoff
’d, the other TTY’s DCD flag and carrier signal are also turned on or off.
This function gets the modem control line state when sigon and sigoff are 0. If our TTY’s DCD flag is on, SER_DCD
is returned. If the other TTY’s DCD flag is on, indicating that our TTY’s DTR flag is on, SER_DTR
is returned.
The nmdm_param
function is defined in nmdm_class
as the tsw_param
operation. This function sets up nmdm_task_tty
to be executed at regular intervals. That is, it sets nmdm%luA
to periodically transfer data to nmdm%luB
, and vice versa. This periodic data transfer requires flow control to prevent one side from overrunning the other with data. Flow control works by halting the sender when the receiver can’t keep up.
Here is the function definition for nmdm_param
:
static int nmdm_param(struct tty *tp, struct termios *t) { struct nmdm_part *np = tty_softc(tp); struct tty *otp; int bpc, rate, speed, i; otp = np->np_other->np_tty; if (!((t->c_cflag | otp->t_termios.c_cflag) & CDSR_OFLOW)) { np->np_rate = 0; np->np_other->np_rate = 0; return (0); } bpc = imax(bits_per_char(t), bits_per_char(&otp->t_termios)); for (i = 0; i < 2; i++) { /* Use the slower of their transmit or our receive rate. */ speed = imin(otp->t_termios.c_ospeed, t->c_ispeed); if (speed == 0) { np->np_rate = 0; np->np_other->np_rate = 0; return (0); } speed <<= QS; /* bits per second, scaled. */ speed /= bpc; /* char per second, scaled. */ rate = (hz << QS) / speed; /* hz per callout. */ if (rate == 0) rate = 1; speed *= rate; speed /= hz; /* (char/sec)/tick, scaled. */ np->np_credits = speed; np->np_rate = rate; callout_reset(&np->np_callout, rate, nmdm_timeout, np); /* Swap pointers for second pass--to update the other end. */ np = np->np_other; t = &otp->t_termios; otp = tp; } return (0); }
This function can be split into three parts. The first determines whether flow control is disabled. If it is, ns_partA
’s and ns_partB
’s np_rate
variable is zeroed and nmdm_param
exits. The np_rate
variable is the rate at which nmdm_task_tty
will be executed. This rate can differ for nmdm%luA
and nmdm%luB
.
The second part calculates the value for np_rate
. This calculation takes into consideration the speed of nmdm%luA
and nmdm%luB
and the number of bits per character. The second part also determines the maximum number of characters to transfer per execution of nmdm_task_tty
.
Lastly, the third part causes nmdm_timeout
to execute one time after rate
/ hz
seconds. The nmdm_timeout
function queues nmdm_task_tty
on taskqueue_swi
.
The second and third parts are executed twice, once for nmdm%luA
and once for nmdm%luB
.
As indicated in the previous section, the nmdm_timeout
function queues nmdm_task_tty
on taskqueue_swi
at regular intervals. Here is its function definition:
static void nmdm_timeout(void *arg) { struct nmdm_part *np = arg; if (np->np_rate == 0) return; /* * Do a simple Floyd-Steinberg dither to avoid FP math. * Wipe out unused quota from last tick. */ np->np_accumulator += np->np_credits; np->np_quota = np->np_accumulator >> QS; np->np_accumulator &= ((1 << QS) - 1); taskqueue_enqueue(taskqueue_swi, &np->np_task); callout_reset(&np->np_callout, np->np_rate, nmdm_timeout, np); }
This function first checks the value of np_rate
. If it equals 0, nmdm_timeout
exits. Next, ns_partA
’s or ns_partB
’s np_quota
variable is assigned the maximum number of characters to transfer (if you return to nmdm_task_tty Function in nmdm_outwakeup Function, it should be obvious how np_quota
is used). Once this is done, nmdm_task_tty
is queued on taskqueue_swi
and nmdm_timeout
is rescheduled to execute after np_rate
/ hz
seconds.
The nmdm_param
and nmdm_timeout
functions are used to emulate the TTYs’ baud rate. Without these two functions, data transfers would be slower.
The bits_per_char
function returns the number of bits used to represent a single character for a given TTY. This function is used only in nmdm_param
. Here is its function definition:
static int bits_per_char(struct termios *t) { int bits; bits = 1; /* start bit. */ switch (t->c_cflag & CSIZE) { case CS5: bits += 5; break; case CS6: bits += 6; break; case CS7: bits += 7; break; case CS8: bits += 8; break; } bits++; /* stop bit. */ if (t->c_cflag & PARENB) bits++; if (t->c_cflag & CSTOPB) bits++; return (bits); }
Notice that the return value takes into account the variable character size, start bit, stop bit, parity enabled bit, and second stop bit.
Now that we’ve walked through nmdm(4)
, let’s give it a try:
$sudo kldload ./nmdm.ko
$sudo /usr/libexec/getty std.9600 nmdm0A &
[1] 936 $sudo cu -l /dev/nmdm0B
Connected FreeBSD/i386 (wintermute.phub.net.cable.rogers.com) (nmdm0A) login:
Excellent. We’re able to connect to nmdm0A
, which is running getty(8)
, from nmdm0B
.
3.135.190.232