Writing kernel mode drivers for ELKS

Like most alternative operating systems, ELKS is missing drivers for many peripherals. Therefore this text describes to write additional drivers.

As a proof of concept, the 'hellodev' driver will be implemented now:

/*
 * hello driver for ELKS kernel
 */

#include <linuxmt/kernel.h> /* printk() */
#include <linuxmt/fs.h>     /* file_operations struct */
#include <string.h>

#define HELLO_DEVICE_NAME       "hellodev"
#define HELLO_MAJOR     9

static char message[256];
char *msgptr = message;

static int hello_write(struct inode *inode, struct file *file, char* buf, int len)
{
    memcpy_fromfs(message,buf,len);
    printk("hellodev: hello_write() called\n");
    return 0;
}

static int hello_read(struct inode *inode, struct file *file, char* buf, int len)
{
    memcpy_tofs(buf, message, len);
    printk("hellodev: hello_read() called\n");
    return len;
}

static int hello_open(struct inode *inode, struct file *file)
{
    printk("hellodev: hello_open() called\n");
    return 0;
}

static void hello_release(struct inode *inode, struct file *file)
{
    printk("hellodev: hello_release() called\n");
    return;
}

static struct file_operations hello_fops = {
    NULL,                       /* lseek */
    hello_read,                 /* read */
    hello_write,                /* write */
    NULL,                       /* readdir */
    NULL,                       /* select */
    NULL,                       /* ioctl */
    hello_open,                 /* open */
    hello_release               /* release */
    
#ifdef BLOAT_FS
        ,
    NULL,                       /* fsync */
    NULL,                       /* check_media_type */
    NULL                        /* revalidate */
#endif
};

void hello_init(void)
{
        printk("hellodev: hello_init() called\n");

    /* register device */
    if (register_chrdev(HELLO_MAJOR, HELLO_DEVICE_NAME, &hello_fops))
        printk("hellodev: unable to register\n");
}

This driver just implements the functions hello_init(), hello_open(), hello_read(), hello_write() and hello_release(). When these functions are called it outputs a message using the printk() statement. This is the equivalent for printf() in kernel code. Also the functions memcpy_fromfs() and memcpy_tofs() are used. The kernel will for security reasons not accept pointers from userspace which may be faulty. Therefore you have to use these functions. For single chars there are equivalent functions available called get_user_char() and put_user_char(). You will also not be able to use inportb() and outportb() functions, use the inb() and inb_p() or out() and out_p() functions instead. The „_p“ versions insert a small pause or delay before returning to support slower devices.

When the driver is loaded the kernel will call hello_init() and the hellodev driver will execute the register_chrdev() function. This function contains the device name which will appear in the /dev directory and a pointer to the file_operations structure called hello_ops. In there the kernel will find the addresses of the functions defined in the driver.

The kernel identifies the driver by its unique the major number. In our demo driver we have included these macros in the code:

#define HELLO_DEVICE_NAME       "hellodev"
#define HELLO_MAJOR     9

Usually in ELKS these are defined in the elks/include/linuxmt/major.h file which the drivers include. However, that file still needs to be changed. Increase the maximum number of char devices to 10:

#define MAX_CHRDEV 10

Put the code above as the hellodev.c file into the elks/arch/i86/drivers/char/ directory and add it to the Makefile in this directory:

else
OBJS  = bioscon.o common.o serial.o lp.o xt_key.o init.o dircon.o mem.o \
        hellodev.o ntty.o meta.o tcpdev.o pty.o bell.o # clist.o tty.o

Also edit the init.c file:

void chr_dev_init(void)
{
#if 1
hello_init();
#endif
#ifdef CONFIG_CHAR_DEV_RS
rs_init();

Normally, you would define a macro in the elks/arch/i86/defconfig file which can be modified with „menuconfig“ and modify the files elks/arch/i86/drivers/char/config.in plus Documentation/Configure.help.

Further, the hello_init() function has to be declared in the elks/include/linuxmt/init.h file:

extern void xtk_init(void);
extern void hello_init(void);

extern void init_console(void);

To load the driver simply add it after the tcpdev device in the elkscmd/rootfs_template/dev/MAKEDEV script. In this script there is a macro for the mknod() function called MAKEDEV:

# TCPDEV, used by ktcp

        mknod tcpdev c 8 0
        $MKDEV hellodev c 9 0

The letter „c“ stands for character driver while the number 9 is the major number and zero the minor number.

To sum it up, you added hellodev.c into the elks/arch/i86/drivers/char directory and modified init.c and the Makefile in there. Also you modified major.h and init.h in the elks/include/linuxmt directory and the file MAKEDEV in the elkscmd/rootfs_template/dev directory.

Now you can compile ELKS again and it will include the hellodev driver and load it. If you enter „ls /dev“ you will see a device node called hellodev.

To test this driver use this program which you may drvtest.c:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
 
#define BUFFER_LENGTH 256               
static char receive[BUFFER_LENGTH];     
 
int main(){
   int ret, fd;
   char stringToSend[BUFFER_LENGTH];

   printf("Starting device test code example...\n");

   fd = open("/dev/hellodev", O_RDWR);  

   if (fd < 0){
      perror("Failed to open the device...");
      return errno;
   }

   printf("Type in a short string to send to the kernel module:\n");
   scanf("%s", stringToSend);                // Read in a string
   printf("Writing message to the device [%s].\n", stringToSend);

   ret = write(fd, stringToSend, strlen(stringToSend)); 
   if (ret < 0){
      perror("Failed to write the message to the device.");
      return errno;
   }
 
   printf("Now reading the string back from the device...\n");
   ret = read(fd, &receive[0], BUFFER_LENGTH);        
   if (ret < 0){
      perror("Failed to read the message from the device.");
      return errno;
   }

   printf("The received message is: [%s]\n", receive);

   return 0;
}

To compile this program enter „bcc -ansi -o drvtest drvtest.c“ and e.g. copy the executable drvtest to the full3 image file using a loop device.

Here are some additional notes how ELKS drivers work describing the concepts used above taken from the fs.txt file:

The inode->i_op pointer is initialised in the function which reads in an inode, e.g. V1_minix_read_inode, and its contents depend on the type of file. If the node is a character or block device, i_op points to chrdev_inode_operations or blkdev_inode_operations respectively. These tables are declared in devices.c and contain pointers just to the respective "open" functions, chrdev_open and blkdev_open.

When the user opens one of these inodes, the kernel calls the open function and passes in a pointer to the relevant "struct file" record. The fops pointer in this record is copied from chrdevs[major].fops or blkdevs[major].fops, which was set up when the device was initialised and registered itself.

So, in the case of the console for example, the file record defined as dircon_ops (see drivers/char/dircon.c) has pointers to the functions to perform I/O to the console.

In the case of block devices, the read() and write() functions *don't* point to device-specific read and write functions; rather, they point to the generic functions block_read() and block_write() in block_dev.c. These take care of caching blocks, part-block reads and writes etc.



Since the ELKS kernel is almost 64k in size now, currently there is only very limited space available for additional device drivers. User mode device drivers should be considered as an alternative.

4th of March 2017 Georg Potthast