Code Analysis

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.

nmdm_modevent Function

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.

Note

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.

Note

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.

nmdm_clone Function

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>.

nmdm_alloc Function

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.

Note

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.

Note

struct 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).

nmdm_outwakeup Function

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).

nmdm_task_tty Function

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);
}

Note

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.

nmdm_inwakeup Function

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);
}

Note

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.

nmdm_modem Function

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);
        }

}

Note

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.

nmdm_param Function

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.

nmdm_timeout Function

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.

bits_per_char Function

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.

Don’t Panic

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.190.232