[PATCH 001/001] CHAR DRIVERS: a simple device to give daemons a /sys-likeinterface

From: Bob Smith
Date: Fri Aug 02 2013 - 21:44:08 EST


This character device can give daemons an interface similar to
the kernel's /sys and /proc interfaces. It is a nice way to
give user space drivers real device nodes in /dev.

thanks
Bob Smith



From 7ee4391af95b828179cf5627f8b431c3301c5057 Mon Sep 17 00:00:00 2001
From: Bob Smith <bsmith@xxxxxxxxxxxxx>
Date: Fri, 2 Aug 2013 16:44:48 -0700
Subject: [PATCH] PROXY, a driver that gives daemons a /sys like interface

---
Documentation/proxy.txt | 36 ++++
drivers/char/Kconfig | 8 +
drivers/char/Makefile | 2 +-
drivers/char/proxy.c | 539 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 584 insertions(+), 1 deletion(-)
create mode 100644 Documentation/proxy.txt
create mode 100644 drivers/char/proxy.c

diff --git a/Documentation/proxy.txt b/Documentation/proxy.txt
new file mode 100644
index 0000000..6b9206a
--- /dev/null
+++ b/Documentation/proxy.txt
@@ -0,0 +1,36 @@
+Proxy Character Devices
+
+
+Proxy is a small character device that connects two user space
+processes. It is intended to give user space daemons a /sys like
+interface for configuration and status.
+
+As an example consider a daemon that controls a stepper motor. The
+daemon would create and open one proxy device to read and write
+configuration (/dev/stepper/config) and another proxy device to
+accept a motor step count (/dev/stepper/count).
+Shell commands to illustrate this example:
+ $ stepper_daemon # start the stepper control daemon
+ $ # Set config to full steps, clockwise and 400 step/sec
+ $ echo "full, cw, 400" > /dev/stepper/config
+ $ # Now tell the motor to step 4000 steps
+ $ echo 4000 > /dev/stepper/count
+ $ sleep 2
+ $ # How many steps remain?
+ $ cat /dev/stepper/count
+
+
+Proxy has some unique features that make ideal for providing a
+/sys like interface. It has no internal buffering. The means
+the daemon can not write until a client program is listening.
+Both named pipes and pseudo-ttys have internal buffers.
+
+Proxy will succeed on a write of zero bytes. A zero byte write
+gives the client an EOF. The daemon in the example above would
+use a zero byte write in the last command after it had written the
+number of steps remaining. No other IPC mechanism can close one
+side of a device and leave the other side open.
+
+Proxy works well with select(), an important feature for daemons.
+In contrast, the FUSE filesystem has some issues with select() on
+the client side.
diff --git a/drivers/char/Kconfig b/drivers/char/Kconfig
index 1421997..d21ea1d 100644
--- a/drivers/char/Kconfig
+++ b/drivers/char/Kconfig
@@ -566,6 +566,14 @@ config TELCLOCK
/sys/devices/platform/telco_clock, with a number of files for
controlling the behavior of this hardware.

+config PROXY
+ tristate "Proxy char device that gives daemons a /sys-like interface"
+ default n
+ help
+ Proxy is a character device that minimally connects two user space
+ processes. It is intended to give user space daemons a /sys like
+ interface for configuration and status.
+
config DEVPORT
bool
depends on !M68K
diff --git a/drivers/char/Makefile b/drivers/char/Makefile
index 7ff1d0d..7009038 100644
--- a/drivers/char/Makefile
+++ b/drivers/char/Makefile
@@ -48,7 +48,7 @@ obj-$(CONFIG_PC8736x_GPIO) += pc8736x_gpio.o
obj-$(CONFIG_NSC_GPIO) += nsc_gpio.o
obj-$(CONFIG_GPIO_TB0219) += tb0219.o
obj-$(CONFIG_TELCLOCK) += tlclk.o
-
+obj-$(CONFIG_PROXY) += proxy.o
obj-$(CONFIG_MWAVE) += mwave/
obj-$(CONFIG_AGP) += agp/
obj-$(CONFIG_PCMCIA) += pcmcia/
diff --git a/drivers/char/proxy.c b/drivers/char/proxy.c
new file mode 100644
index 0000000..e56fa65
--- /dev/null
+++ b/drivers/char/proxy.c
@@ -0,0 +1,539 @@
+/*
+ * proxy.c: A bidirectional pipe device
+ *
+ * This device is meant as a simple proxy to connect two user-space
+ * programs through a device, allowing each of the user space programs
+ * to select() on the device. The first program to open the device gets
+ * immediately blocked on either reads or writes until the other side is
+ * opened. The idea of "two sides" is enforced by limiting the number
+ * of opens on the device to two.
+ * This device is different from named pipes and pseudo terminals in
+ * that it is bidirectional and it doesn't block writes when the buffer
+ * is full, it blocks when the buffer is full _OR_ if other end is closed.
+ *
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of Version 2 of the GNU General Public License as
+ * published by the Free Software Foundatio
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc. ; 51 Franklin Street, Fifth Floor ; Boston, MA 02110-1301 ; USA
+ *
+ *
+ * Copyright (C) 2013 Demand Peripherals, Inc.
+ *
+ * Initial release: Bob Smith
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/fs.h>
+#include <linux/poll.h>
+#include <linux/cdev.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+
+
+/* Limits and other defines */
+/* The # proxy devices. Max minor # is one less than this */
+#define NUM_PX_DEVS (255)
+#define DEVNAME "proxy"
+#define DEBUGLEVEL (2)
+
+
+/* Data structure definitions */
+/* This structure describes the buffer and queues in one direction */
+struct cirbuf {
+ char *buf; /* points to sf circular buffer */
+ int widx; /* where to write next sf character */
+ int ridx; /* where to read next sf character */
+ int cidx; /* file closed at this index. ==-1 while open */
+ wait_queue_head_t que; /* sf readers wait on this queue */
+};
+
+/* This data structure describes one proxy device. There
+ * is one of these for each instance (minor #) of proxy.
+ * Since data flow is completely symmetric, we differentiate
+ * the two endpoints as East (e) and West (w), with the
+ * two corresponding directions ew and we.
+ */
+struct px {
+ int minor; /* minor number of this proxy instance */
+ struct cirbuf ewbuf;
+ struct cirbuf webuf;
+ struct semaphore sem; /* lock to protect nopen */
+ int nopen; /* number of opens on this device */
+ struct file *east; /* used to tell which cirbuf to use */
+ struct file *west; /* used to tell which cirbuf to use */
+ int eastaccmode; /* Access mode (O_RDONLY, O_WRONLY) */
+ int westaccmode; /* needed even after one side closes */
+};
+
+
+/* Function prototypes. */
+static int proxy_init_module(void);
+static void proxy_exit_module(void);
+static int proxy_open(struct inode *, struct file *);
+static int proxy_release(struct inode *, struct file *);
+static ssize_t proxy_read(struct file *, char *, size_t, loff_t *);
+static ssize_t proxy_write(struct file *, const char *, size_t, loff_t *);
+static unsigned int proxy_poll(struct file *, poll_table *);
+
+
+/* Global variables */
+static int buffersize = 0x1000; /* circular buffer is 0x1000 4K */
+static unsigned char numberofdevs = NUM_PX_DEVS;
+static int px_major; /* major device number */
+/* Debuglvl controls whether a printk is executed
+ * 0 = no printk at all
+ * 1 = printk on error only
+ * 2 = printk on errors and on init/remove
+ * 3 = debug prink to trace calls into proxy
+ * 4 = debug trace inside of proxy calls
+ */
+static unsigned char debuglevel = DEBUGLEVEL; /* printk verbosity */
+
+struct cdev px_cdev; /* a char device global */
+dev_t px_devicenumber; /* first device number */
+
+module_param(buffersize, int, S_IRUSR);
+module_param(debuglevel, byte, S_IRUSR);
+module_param(numberofdevs, byte, S_IRUSR);
+
+static struct px *px_devices; /* point to devices (minors) */
+
+/* map the callbacks into this driver */
+const struct file_operations proxy_fops = {
+ .owner = THIS_MODULE,
+ .open = proxy_open,
+ .read = proxy_read,
+ .write = proxy_write,
+ .poll = proxy_poll,
+ .release = proxy_release
+};
+
+
+/* Module description and macros */
+
+MODULE_DESCRIPTION
+("Transparently connects two user-space programs through a device");
+MODULE_AUTHOR("Bob Smith");
+MODULE_LICENSE("GPL");
+MODULE_PARM_DESC(buffersize, "Size of each buffer. default=4096 (4K) ");
+MODULE_PARM_DESC(debuglevel, "Debug level. Higher=verbose. default=2");
+MODULE_PARM_DESC(numberofdevs,
+ "Create this many minor devices. default=16");
+
+
+
+int proxy_init_module(void)
+{
+ int i, err;
+ px_devices = kmalloc(numberofdevs * sizeof(struct px), GFP_KERNEL);
+ if (px_devices == NULL) {
+ /* no memory available */
+ if (debuglevel >= 1)
+ pr_err("%s: init fails: no memory.\n",
+ DEVNAME);
+ return 0;
+ }
+ memset(px_devices, 0, numberofdevs * sizeof(struct px));
+
+ /* init devices in this block */
+ for (i = 0; i < numberofdevs; i++) { /* for every minor device */
+ px_devices[i].minor = i; /* set minor number */
+ px_devices[i].ewbuf.buf = (char *) 0;
+ px_devices[i].webuf.buf = (char *) 0;
+ px_devices[i].ewbuf.widx = 0;
+ px_devices[i].webuf.widx = 0;
+ px_devices[i].ewbuf.ridx = 0;
+ px_devices[i].webuf.ridx = 0;
+ px_devices[i].ewbuf.cidx = -1;
+ px_devices[i].webuf.cidx = -1;
+ px_devices[i].east = (struct file *) 0; /* !=0 if open */
+ px_devices[i].west = (struct file *) 0;
+ init_waitqueue_head(&px_devices[i].ewbuf.que);
+ init_waitqueue_head(&px_devices[i].webuf.que);
+ px_devices[i].nopen = 0;
+#ifdef init_MUTEX
+ init_MUTEX(&px_devices[i].sem);
+#else
+ sema_init(&px_devices[i].sem, 1);
+#endif
+ }
+
+ /* alloc number of char devs in kernel */
+ err = alloc_chrdev_region(&px_devicenumber, 0, numberofdevs, DEVNAME);
+ if (err < 0) {
+ if (debuglevel >= 1)
+ pr_err("%s: init fails. err=%d.\n",
+ DEVNAME, err);
+ return err;
+ }
+ px_major = MAJOR(px_devicenumber); /* save assign major */
+ cdev_init(&px_cdev, &proxy_fops); /* init dev structures */
+ kobject_set_name(&(px_cdev.kobj), "proxy%d", px_devicenumber);
+
+ err = cdev_add(&px_cdev, px_devicenumber, numberofdevs);
+ if (err < 0) {
+ if (debuglevel >= 1)
+ pr_err("%s: init fails. err=%d.\n",
+ DEVNAME, err);
+ return err;
+ }
+
+ if (debuglevel >= 2)
+ pr_info("%s: Installed %d minor devices on major number %d.\n",
+ DEVNAME, numberofdevs, px_major);
+ return 0; /* success */
+}
+
+
+void proxy_exit_module(void)
+{
+ int i;
+ if (!px_devices)
+ return;
+
+ for (i = 0; i < numberofdevs; i++) {
+ kfree(px_devices[i].ewbuf.buf);
+ kfree(px_devices[i].webuf.buf);
+ }
+
+ cdev_del(&px_cdev); /* delete major device */
+ kfree(px_devices); /* free */
+ px_devices = NULL; /* reset pointer */
+ unregister_chrdev_region(px_devicenumber, numberofdevs);
+
+ if (debuglevel >= 2)
+ pr_info("%s: Uninstalled.\n", DEVNAME);
+}
+
+
+static int proxy_open(struct inode *inode, struct file *filp)
+{
+ int mnr = iminor(inode);
+ struct px *dev = &px_devices[mnr];
+
+ if (debuglevel >= 3)
+ pr_info("%s open. Minor#=%d.\n", DEVNAME, mnr);
+
+ if (down_interruptible(&dev->sem)) /* prevent races on open */
+ return -ERESTARTSYS;
+
+ if (dev->nopen >= 2) { /* Only two opens please! */
+ up(&dev->sem);
+ return -EBUSY;
+ }
+ dev->nopen = dev->nopen + 1;
+
+ if (!dev->ewbuf.buf) { /* get east-to-west buffer */
+ dev->ewbuf.buf = kmalloc(buffersize, GFP_KERNEL);
+ if (!dev->ewbuf.buf) {
+ if (debuglevel >= 1)
+ pr_err("%s: No memory dev=%d.\n",
+ DEVNAME, mnr);
+ up(&dev->sem);
+ return -ENOMEM;
+ }
+ }
+ if (!dev->webuf.buf) { /* get west-to-east buffer */
+ dev->webuf.buf = kmalloc(buffersize, GFP_KERNEL);
+ if (!dev->webuf.buf) {
+ if (debuglevel >= 1)
+ pr_err("%s: No memory dev=%d.\n",
+ DEVNAME, mnr);
+ up(&dev->sem);
+ return -ENOMEM;
+ }
+ }
+
+ /* store the proxy device in the file's private data */
+ filp->private_data = (void *) dev;
+ if (dev->east == (struct file *) 0) {
+ dev->east = filp; /* tells west from east */
+ dev->webuf.ridx = dev->webuf.widx; /* reader starts caught up */
+ dev->ewbuf.cidx = -1; /* xmit is open */
+ dev->webuf.cidx = -1; /* xmit is open */
+ if (dev->nopen == 2) { /* wake up other end */
+ wake_up_interruptible(&dev->webuf.que);
+ }
+ dev->eastaccmode = filp->f_flags;
+ } else if (dev->west == (struct file *) 0) {
+ dev->west = filp; /* tells east from west */
+ dev->ewbuf.ridx = dev->ewbuf.widx; /* reader starts caught up */
+ dev->webuf.cidx = -1;
+ dev->ewbuf.cidx = -1;
+ if (dev->nopen == 2) { /* wake up other end */
+ wake_up_interruptible(&dev->ewbuf.que);
+ }
+ dev->westaccmode = filp->f_flags;
+ } else if (debuglevel >= 1)
+ pr_err("%s: inconsistent open count.\n", DEVNAME);
+
+ up(&dev->sem); /* unlock sema we are done */
+
+ return nonseekable_open(inode, filp); /* success */
+}
+
+
+static int proxy_release(struct inode *inode, struct file *filp)
+{
+ struct px *dev = (struct px *) filp->private_data;
+
+ if (debuglevel >= 3)
+ pr_info("%s release. Minor#=%d.\n", DEVNAME,
+ ((struct px *) filp->private_data)->minor);
+
+ if (down_interruptible(&dev->sem)) /* prevent races on close */
+ return -ERESTARTSYS;
+
+ dev->nopen = dev->nopen - 1;
+
+ if (dev->east == filp) {
+ dev->east = (struct file *) 0; /* mark as not in use */
+ dev->ewbuf.cidx = dev->ewbuf.widx; /* set close index */
+ } else if (dev->west == filp) {
+ dev->west = (struct file *) 0; /* mark as not in use */
+ dev->webuf.cidx = dev->webuf.widx; /* set close index */
+ } else if (debuglevel >= 1)
+ pr_err("%s: inconsistent open count.\n", DEVNAME);
+
+ up(&dev->sem); /* unlock sema we are done */
+
+ return 0; /* success */
+}
+
+
+/* Utility to look for a full circular buffer */
+int is_full(struct cirbuf *pcbuffer)
+{
+ if ((pcbuffer->ridx - pcbuffer->widx == 1) ||
+ ((pcbuffer->ridx == 0) && (pcbuffer->widx == buffersize - 1)))
+ return 1;
+ else
+ return 0;
+}
+
+
+static ssize_t proxy_read(
+ struct file *filp, char __user *buff,
+ size_t count,
+ loff_t *offset)
+{
+ int ret;
+ int xfer; /* num bytes read from proxy buf */
+ int cpcnt; /* cp count and start location */
+ struct cirbuf *pcbuffer;
+
+ struct px *dev = (struct px *) filp->private_data;
+
+ if (debuglevel >= 3)
+ pr_info("%s: read %d char from dev%d, off=%lld.\n",
+ DEVNAME, count, dev->minor, *offset);
+
+ if (filp == dev->east)
+ pcbuffer = &dev->webuf;
+ else if (filp == dev->west)
+ pcbuffer = &dev->ewbuf;
+ else
+ return 0; /* should not get here */
+
+ /* cidx is set if writer is trying to close the file */
+ if (pcbuffer->ridx == pcbuffer->cidx)
+ return 0;
+
+ /* Wait here until new data is available */
+ while (pcbuffer->ridx == pcbuffer->widx) {
+ if (filp->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+ /* wait on event queue, predicate is .. */
+ if (wait_event_interruptible(pcbuffer->que,
+ (pcbuffer->ridx != pcbuffer->widx))) {
+ if (debuglevel >= 1)
+ pr_err("%s: read failed in wait_event_interruptible\n",
+ DEVNAME);
+ return -ERESTARTSYS;
+ }
+ }
+
+ /* Copy the new data out to the user */
+ xfer = pcbuffer->widx - pcbuffer->ridx;
+ xfer = (xfer < 0) ? (xfer + buffersize) : xfer;
+ xfer = min_t(int, (int) count, xfer);
+ ret = xfer; /* we will handle these bytes */
+
+ cpcnt = buffersize - pcbuffer->ridx;
+ cpcnt = (cpcnt < xfer) ? cpcnt : xfer;
+ if (cpcnt) {
+ if (copy_to_user(buff, pcbuffer->buf + pcbuffer->ridx, cpcnt)) {
+ if (debuglevel >= 1)
+ pr_err("%s: read failed in copy_to_user.\n",
+ DEVNAME);
+ return -EFAULT;
+ }
+ }
+
+ if (xfer - cpcnt > 0) {
+ if (copy_to_user(buff + cpcnt, pcbuffer->buf, xfer - cpcnt)) {
+ if (debuglevel >= 1)
+ pr_err("%s: read failed in copy_to_user.\n",
+ DEVNAME);
+ return -EFAULT;
+ }
+ }
+ pcbuffer->ridx += xfer;
+ pcbuffer->ridx -= (pcbuffer->ridx > buffersize - 1) ? buffersize : 0;
+
+ /* This is what the writers have been waiting for */
+ wake_up_interruptible(&pcbuffer->que);
+
+ if (debuglevel >= 3)
+ pr_info("%s: read %d bytes.\n", DEVNAME, xfer);
+ return ret;
+}
+
+
+static ssize_t proxy_write(
+ struct file *filp,
+ const char __user *buff,
+ size_t count, loff_t *off)
+{
+ int ret;
+ int xfer; /* num bytes to read from user */
+ int cpcnt; /* num bytes in a single copy */
+ struct cirbuf *pcbuffer;
+
+ struct px *dev = (struct px *) filp->private_data;
+
+ if (debuglevel >= 3)
+ pr_info("%s: write %d char from dev%d\n",
+ DEVNAME, count, dev->minor);
+
+ if (filp == dev->east)
+ pcbuffer = &dev->ewbuf;
+ else if (filp == dev->west)
+ pcbuffer = &dev->webuf;
+ else {
+ if (debuglevel >= 3)
+ pr_err("%s: can't tell east from west.\n",
+ DEVNAME);
+ return 0; /* should not get here */
+ }
+
+ /* Wait here until new data is available to write */
+ while ((dev->nopen != 2) || is_full(pcbuffer)) {
+ if (filp->f_flags & O_NONBLOCK)
+ return -EWOULDBLOCK;
+ /* wait on event queue, predicate is .. */
+ if (wait_event_interruptible(pcbuffer->que,
+ ((dev->nopen == 2) && (!is_full(pcbuffer))))) {
+ if (debuglevel >= 1)
+ pr_err("%s: write failed in wait_event_interruptible.\n",
+ DEVNAME);
+ return -ERESTARTSYS;
+ }
+ }
+
+ xfer = pcbuffer->ridx - 1 - pcbuffer->widx;
+ xfer = (xfer < 0) ? xfer + buffersize : xfer;
+ xfer = min_t(int, (int) count, xfer);
+ ret = xfer;
+
+ cpcnt = min(xfer, buffersize - pcbuffer->widx);
+ if (cpcnt) {
+ if (copy_from_user(pcbuffer->buf + pcbuffer->widx,
+ buff, cpcnt)) {
+ if (debuglevel >= 1)
+ printk(
+ "%s: read failed in copy_from_user.\n",
+ DEVNAME);
+ return -EFAULT;
+ }
+ }
+
+ if (xfer - cpcnt > 0) {
+ if (copy_from_user(pcbuffer->buf, buff + cpcnt, xfer - cpcnt)) {
+ if (debuglevel >= 1)
+ printk(
+ "%s: read failed in copy_from_user.\n",
+ DEVNAME);
+ return -EFAULT;
+ }
+ }
+ pcbuffer->widx += xfer;
+ pcbuffer->widx -= (pcbuffer->widx > buffersize - 1) ? buffersize : 0;
+
+ /* Count=0 if writer is trying to close the file */
+ if (count == 0)
+ pcbuffer->cidx = pcbuffer->widx;
+
+ /* This is what the readers have been waiting for */
+ wake_up_interruptible(&pcbuffer->que);
+
+ if (debuglevel >= 3)
+ printk("%s: wrote %d bytes.\n", DEVNAME, ret);
+ return ret;
+}
+
+
+static unsigned int proxy_poll(struct file *filp, poll_table *ppt)
+{
+ int ready_mask = 0;
+ struct px *dev = filp->private_data;
+
+ poll_wait(filp, &dev->ewbuf.que, ppt);
+ poll_wait(filp, &dev->webuf.que, ppt);
+
+
+ if (filp == dev->west) {
+ /* Writable if there's space, the other end is connected,
+ * we haven't already written an end-of-file marker,
+ * the other side is not WRONLY, and our side is not O_RDONLY
+ */
+ if (!is_full(&dev->webuf) && (dev->nopen == 2)
+ && (dev->webuf.cidx != dev->webuf.widx)
+ && ((dev->eastaccmode & O_ACCMODE) != O_WRONLY)
+ && ((filp->f_flags & O_ACCMODE) != O_RDONLY)) {
+ ready_mask = POLLOUT | POLLWRNORM;
+ }
+ /* Readable if the buffer has data or we're at end of file,
+ * and the other sice is not RDONLY,
+ * and our side is not O_WRONLY
+ */
+ if (((dev->ewbuf.widx != dev->ewbuf.ridx)
+ || (dev->ewbuf.ridx == dev->ewbuf.cidx))
+ && ((dev->eastaccmode & O_ACCMODE) != O_RDONLY)
+ && ((filp->f_flags & O_ACCMODE) != O_WRONLY)) {
+ ready_mask |= (POLLIN | POLLRDNORM);
+ }
+ } else if (filp == dev->east) {
+ if (!is_full(&dev->ewbuf) && (dev->nopen == 2)
+ && (dev->ewbuf.cidx != dev->ewbuf.widx)
+ && ((dev->westaccmode & O_ACCMODE) != O_WRONLY)
+ && ((filp->f_flags & O_ACCMODE) != O_RDONLY)) {
+ ready_mask = POLLOUT | POLLWRNORM;
+ }
+ if (((dev->webuf.widx != dev->webuf.ridx)
+ || (dev->webuf.ridx == dev->webuf.cidx))
+ && ((dev->westaccmode & O_ACCMODE) != O_RDONLY)
+ && ((filp->f_flags & O_ACCMODE) != O_WRONLY)) {
+ ready_mask |= (POLLIN | POLLRDNORM);
+ }
+ }
+
+ if (debuglevel >= 3)
+ pr_info("%s: poll returns 0x%x.\n",
+ DEVNAME, ready_mask);
+ return ready_mask;
+}
+
+module_init(proxy_init_module);
+module_exit(proxy_exit_module);
--
1.7.10.4

--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/