Author: Mugabi Siro

Category: Device Drivers

Summary:

This entry describes device file access from kernel space code. Basic file operations on a TTY character device are considered here as a case study. Linux 3.x used.

Tags: linux

Background

There are good reasons why the open(2), read(2), write(2), close(2), etc system calls (or more precisely, POSIX API libc wrapper functions) exist. System calls provide a standard interface that prevent incorrect access of h/w and other system resources. Having known/legal points of entry or access to these resources allows the kernel to securely check the accuracy/feasibility of a request (based on permissions, parameters, resource availability, etc) before granting it. This approach allows complex systems with numerous processes contending for devices and other system resources such as memory to be managable and stable. Abstracted system interfaces also facilitate program portability as programs can be compiled and run on different kernel versions transperently. In brief, accessing device files should be done from userspace.

Nevertheless, there are some obscure scenarios where accessing device files may have to be (or is preferably) done from kernel space. This may happen during, say the development of some embedded prototype. In fact, with some driver frameworks, this is the way to go. Linux, therefore, provides interfaces for file access from kernel space, but their use is still discouraged. Citing fs/open.c:

/**
 * filp_open - open file and return file pointer
 *
 * @filename:   path to open
 * @flags:  open flags as per the open(2) second argument
 * @mode:   mode for the new file if O_CREAT is set, else ignored
 *
 * This is the helper to open a file from kernelspace if you really
 * have to.  But in generally you should not do this, so please move
 * along, nothing to see here..
 */
struct file *filp_open(const char *filename, int flags, umode_t mode)
{
    struct filename name = {.name = filename};
    return file_open_name(&name, flags, mode);
}
EXPORT_SYMBOL(filp_open);

Check out drivers/staging/comedi/drivers/serial2002.c for a real world example. This entry presents a skeletal implementation that demonstrates device file access from kernel space code.

File Operations

The file structure, struct file, defined in include/linux/fs.h, represents an open file. Every open file in the system (including device files) has an associated struct file. Conventionally, a struct file is created by the kernel when an open(2) is done on a file. In the case of device files, assuming a successful open of the file, all file operations defined by the device driver (i.e. the callbacks registered via struct file_operations) will receive a pointer to this structure via their struct file * function argument. Upon the last close(2) on the file descriptor, the kernel releases this data structure.

Since the device file is now to be opened from kernel space, code employs the filp_open (see fs/open.c) interface exported by the kernel in order to obtain the file's struct file. If this operation succeeds, then the code can now perform file operations defined by the corresponding driver directly via respective pointer references to members of driver's struct file_operations which is embedded in this struct file object.

Opening The Device File

The filp_open interface is declared in include/linux/fs.h:

extern struct file *filp_open(const char *, int, umode_t);

and, defined and exported in fs/open.c. The filesystem pathname for the device file (e.g. /dev/ttyS0) will be specified as the first argument. The second argument will accept the device file's open flags as per open(2) while the third will specify the mode for the new file. Note that if the O_CREAT file creation flag was not specified in flags, then mode is ignored.

Unlike userspace open(2) which returns a valid file descriptor upon success or -1 on error (errno set accordingly), filp_open returns valid a pointer to the file's struct file or a cast value set to an error code. The validity of the return value could be checked via PTR_ERR, IS_ERR, etc (see include/linux/err.h).

Read/Write Operations

Once a valid struct file pointer has been acquired via filp_open, accessing the driver's file operations is a simple matter of pointer referencing e.g:

f->f_pos = 0;
f->f_op->write(f, buf, count, &f->f_pos);

where f is of type struct file * and f_op is of type struct file_operations * (also defined in include/linux/fs.h).

However, since the driver method is being accessed from kernel space, there are a few things that need to be taken into consideration. Conventionally, driver (read/write) file operations copy data to/from userspace supplied buffers via functions such as copy_{to,from}_user. These functions in turn perform checks (via the access_ok macro) against the userspace buffer for validity - mainly range checks (i.e. its virtual address is indeed userspace if it lies within a certain range). Now, when accessing a device file from kernel space, it is likely that a kernel space allocated buffer will now be passed to the driver file operation (instead of a user land buffer). Depending on the execution context of the kernel code i.e. on behalf of a user space process or as some separately scheduled kernel thread, explicitly adjusting address range limits of the current thread may be necessary in order to trick access_ok, for example:

mm_segment_t oldfs;

oldfs = get_fs( ); /* in case of "USER_DS" */
set_fs(KERNEL_DS);
f->f_pos = 0;
result = f->f_op->write(f, buf, count, &f->f_pos);
set_fs(oldfs);

where buf is a kernel space allocated buffer. The architecture specific get_fs and set_fs macros adjust the address range limit of the current thread. Check out arch/ARCH/include/asm/uaccess.h for their definitions.

Closing The Device File

Closing the opened file is a simple matter of invoking filp_close. (see fs/open.c):

int filp_close(struct file *filp, fl_owner_t id);

This function destroys the struct file * pointer.

Code and Test

The following source code is presented for illustration and test purposes. Writing to the module's device file will send a string of characters to /dev/ttyS0. Performing:

$ echo 'Dunia, vipi?' > /dev/xktty

has the same effect as

$ echo 'Dunia, vipi?' > /dev/ttyS0

but, of course, xktty is a rudimentary implementation (one process at a time) that actually accesses ttyS0 in kernel space. This test may be run from the serial terminal of a (QEMU) VM instance1. You could as well experiment with serial port system consoles for other machine architectures e.g. /dev/ttyAMA0 (ARM), or with different TTY devices e.g. Virtual Terminals (/dev/tty{1,2,...}).

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>

/* ============== krn space '/dev/ttyN' access ============= */

static struct file *ktty_open(const char *filename, int flags, umode_t mode)
{
    return filp_open(filename, 0, O_RDWR);
}

static ssize_t ktty_write(struct file *f, const char *buf, int count)
{

    int result;
    mm_segment_t oldfs;

    oldfs = get_fs();
    set_fs(KERNEL_DS);
    f->f_pos = 0;
    result = f->f_op->write(f, buf, count, &f->f_pos);
    set_fs(oldfs);

    return result;
}

static void ktty_close(struct file *xktty, fl_owner_t id)
{
        filp_close(xktty, id);
}

/* =============== module file operations ===================== */
DEFINE_MUTEX(xmutex);
static struct file *xktty = NULL;
static int xktty_open(struct inode *inode, struct file *filp)
{
    #define XKTTY_MAX_PATH 20
    #define XKTTY_NUM 0
    char filename[XKTTY_MAX_PATH];

    /* only one process at a time */
    if(!(mutex_trylock(&xmutex)))
        return -EBUSY;

    snprintf(filename, XKTTY_MAX_PATH, "/dev/ttyS%d", XKTTY_NUM);
    xktty = ktty_open(filename, 0, O_RDWR);
    if (PTR_RET(xktty)) {
        mutex_unlock(&xmutex);
        return PTR_RET(xktty);
    }

    return 0;
}

static int xktty_release(struct inode *inode, struct file *file)
{
    if(!IS_ERR_OR_NULL(xktty))
        ktty_close(xktty, 0);
    mutex_unlock(&xmutex);
    return 0;
}

static ssize_t xktty_write(struct file *filp,
                 const char __user * buf, size_t count,
                 loff_t * f_pos)
{

    #define XKTTY_MAX_BUF_LEN 200
    const char kbuf[XKTTY_MAX_BUF_LEN];

    count = count < XKTTY_MAX_BUF_LEN ? count : XKTTY_MAX_BUF_LEN;
    if (copy_from_user((char *)kbuf, (const char __user *)buf, count))
        return -EFAULT;

    if (!IS_ERR_OR_NULL(xktty))
        return ktty_write(xktty, kbuf, count);
    else
        return -EFAULT;
}

static struct file_operations xktty_ops = {
    .owner = THIS_MODULE,
    .open = xktty_open,
    .release = xktty_release,
    .write = xktty_write,
};

/* =================== init/exit ======================== */

static struct cdev cdev;
static struct class *class;
static int xktty_mjr;

static int xktty_init(void)
{
    #define XKTTY_NAME "xktty"

    dev_t devt = MKDEV(0, 0);
    if (alloc_chrdev_region(&devt, 0, 1, XKTTY_NAME) < 0)
        return -1;
    xktty_mjr = MAJOR(devt);

    cdev_init(&cdev, &xktty_ops);
    cdev.owner = THIS_MODULE;
    devt = MKDEV(xktty_mjr, 0);
    if (cdev_add(&cdev, devt, 1))
        goto exit0;

    class = class_create(THIS_MODULE, XKTTY_NAME);
    if (!class)
        goto exit1;

    devt = MKDEV(xktty_mjr, 0);
    if (!(device_create(class, NULL, devt, NULL, XKTTY_NAME)))
        goto exit2;

    return 0;

exit2:
    class_destroy(class);
exit1:
    cdev_del(&cdev);
exit0:
    unregister_chrdev_region(MKDEV(xktty_mjr, 0), 1);

    return -1;
}

static void xktty_fini(void)
{
    device_destroy(class, MKDEV(xktty_mjr, 0));
    class_destroy(class);
    cdev_del(&cdev);
    unregister_chrdev_region(MKDEV(xktty_mjr, 0), 1);
}

module_init(xktty_init);
module_exit(xktty_fini);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("siro mugabi, nairobi-embedded.org");

A sample makefile for the job:

kmod = xktty
ifneq ($(KERNELRELEASE),)
obj-m := $(kmod).o
else

KDIR ?= /lib/modules/$$(uname -r)/build

all:
    $(MAKE) -C $(KDIR) M=$$PWD modules

clean:
    $(MAKE) -C $(KDIR) M=$$PWD clean

.PHONY  : clean
endif

Resources And Further Reading

  • Linux 3.x

  • Books

    • Understanding The Linux Kernel, 3rd Edition, Daniel P. Bovet, Marco Caseti, 2005, O'Reilly. Somewhat dated but still very useful.

Footnotes

1. See QEMU Commandline: Serial Port System Console. [go back]