ToBe | Howto

Linux Kernel Module

Preparing
Configuring build host
sudo apt-get install linux-headers-`uname -r`

Note: on Raspberry Pi, kernel headers may not be available

sudo rpi-update
sudo reboot

wget https://raw.githubusercontent.com/notro/rpi-source/master/rpi-source -O /usr/bin/rpi-source && sudo chmod +x /usr/bin/rpi-source && /usr/bin/rpi-source -q --tag-update
sudo apt-get install bc
rpi-source --skip-gcc
Building
Creating a Kernel Module

A module is declared as follow:

my-module.c
#include <linux/init.h>
#include <linux/module.h>

static int __init init(void) {
  printk(KERN_INFO "my-module: starting...");
  return 0;
}

static void __exit exit(void) {
  printk(KERN_INFO "my-module: ...stopping.");
}

module_init(init);
module_exit(exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bertrand Tognoli");
MODULE_DESCRIPTION("Some useless module");
MODULE_VERSION("1.0");
Makefile
KDIR ?= /lib/modules/$(shell uname -r)/build

obj-m := my-module.o

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

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

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

.PHONY: all install clean

To test that module:

# Building the module
make
# Installing the module
sudo make install

# Starting the module
sudo insmod my-module
# Deleting the module
sudo rmmod my-module
Installing
Deploying Kernel module
DKMS

Kernel modules need to be built from a particular Kernel version. When kernel is upgraded, the module needs to be re-built

DKMS helps to automate that process

sudo apt install dkms
dkms.conf
PACKAGE_NAME=tobe-pilot
PACKAGE_VERSION=1.1
MAKE="make -C kmod KDIR=/lib/modules/${kernelver}/build"
CLEAN="make -C kmod clean"
BUILT_MODULE_NAME=tobe-pilot
BUILT_MODULE_LOCATION=kmod
DEST_MODULE_LOCATION=/kernel/drivers/misc/
AUTOINSTALL=yes
# Create DKMS module directory
sudo mkdir /usr/src/my-module-1.0
sudo cp -r dkms.conf kmod /usr/src/tobe-pilot-1.1

sudo dkms add -m my-module -v 1.0
sudo dkms build -m my-module -v 1.0
sudo dkms install -m my-module -v 1.0
sudo mkdir /usr/src/tobe-pilot-1.1-x
sudo cp -r dkms.conf kmod /usr/src/tobe-pilot-1.1-x

sudo dkms add -m tobe-pilot -v 1.1-x

ll /lib/modules/4.19.97+/updates

sudo dkms add -m tobe-pilot -v 1.1-x
sudo dkms build -m tobe-pilot -v 1.1-x
sudo dkms install -m tobe-pilot -v 1.1-x

/var/lib/dkms/tobe-pilot/1.1/4.19.97+/armv6l/log/make.log
/var/lib/dkms/tobe-pilot/1.1/4.19.97+/armv6l/module/tobe-pilot.ko
/lib/modules/4.19.97+/extra/tobe-pilot.ko
Module data
Configuring the module
Module parameter

Parameter can be accessesed via /sys/module pseudo filesystem

my-module.c
static char *param = "unset";
module_param(param, charp, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(param, "Module parameter (default=unset)");

Note: A module parameter can be changed from userspace (if allowed), but module itself is not notified about the change.

cat /sys/module/my-module/parameters/param
echo "set" > /sys/module/my-module/parameters/param
SysFS and KObject

SysFS virtual filesystem expose an interface to kernel modules kobject structure.

my-module.c
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/string.h>

static int data;

static struct device *dev;
static struct kobject *obj;

static ssize_t show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) {
  return sprintf(buf, "%d\n", data);
}

static ssize_t store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) {
  int ret = kstrtoint(buf, 10, &data);
  return (ret < 0 ? ret : count);
}

static struct kobj_attribute attr = __ATTR(data, 0664, show, store);
static struct attribute *attrs[] = { &attr.attr, NULL };
static struct attribute_group attr_group = { .attrs = attrs };

static int __init example_init(void) {
  dev = root_device_register("tobe-pilot");
  obj = kobject_create_and_add("obj", dev->kobj);
  if (!obj)
    return -ENOMEM;

  if (sysfs_create_group(obj, &attr_group)) {
    printk(KERN_WARN "KOBJ: creating sysfs failed.");
    kobject_put(obj);
    return -ENOMEM;
  }
  return 0;
}

static void __exit exit(void) {
  kobject_put(obj);
  root_device_unregister(dev);
}
Kernel API
Kernel SW development
Timer and delay

Delay

Delay function will block caller for given time. They are only recommended for short delays (e.g. <10us)

  ndelay(nsecs); // May not be available on all platforms
  udelay(usecs);
  mdelay(msecs); // Not recommended to lock kernel that long

Sleep

Sleep function are backed by timers (or hrtimers), so they do not block the kernel. hrtimers (usleep_range) is used for timers < 5~10ms

  usleep_range(min, max); // Uses HR timer
  msleep(msecs);
  msleep_interruptible(msecs); // If timer could be interrupted by another signal

Timers

my-module.c
#include <linux/timer.h>

static struct timer_list tim;

static void callback(unsigned long data) {
  mod_timer(&tim, jiffies + HZ);
}

static int __init init(void) {
  init_timer(&tim);
  tim.function = callback;
  tim.data = 0;
  tim.expires = jiffies + HZ;
  add_timer(&tim);
}

static void __exit exit(void) {
  del_timer(&tim);
}
Kernel Threads
my-module.c
#include <linux/kthread.h>
#include <linux/delay.h>

struct task_struct *task;

int thread(void *data) {
  while(1) {
    msleep(500);
    if (kthread_should_stop()) break;
  }
  return 0;
}

void init(void) {
  task = kthread_run(task, NULL, THREAD_NAME);
}

void exit(void) {
  kthread_stop(task);
}

Synchronisation
static DEFINE_SPINLOCK(lock);
static enum { IDLE, ACTIVE } tskStatus;

void tskSetStatus(void) {
  unsigned long flags;
  spin_lock_irqsave(&lock, flags);
  tskStatus = ACTIVE;
  spin_unlock_irqrestore(&lock, flags);
}
Interrupts
my-module.c
#include <linux/interrupt.h>

static irqreturn_t intr(int irq, void *dev) {
  // handle irq
  return IRQ_HANDLED;
}

static int __init init(void) {
  if ((err = request_irq(gpio_to_irq(0), intr, IRQF_SHARED | IRQF_TRIGGER_RISING, "intr", dev)) != 0) {
    return err;
  }

  return 0;
}

static void __exit exit(void) {
  free_irq(gpio_to_irq(0), dev);
}
GPIO

Userspace

echo 0 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio0/direction
cat /sys/class/gpio/gpio0/value

Kernelspace

my-module.c
#include <linux/gpio.h>

static void gpioClear(void) {
  gpio_set_value(0, 0);
}

static void gpioRead(void) {
  if (gpio_get_value(0) == 0)
    printk(KERN_INFO "Gpio: gpio0 is low.");
  else
    printk(KERN_INFO "Gpio: gpio0 is high.");
}

static int __init init(void) {
  int err;

  if ((err = gpio_request(0, "gpio0")) != 0)
    return err;
  // if ((err = gpio_direction_output(0, 1)) != 0) {
  if ((err = gpio_direction_input(0)) != 0) {
    gpio_free(0);
    return err;
  }

  return 0;
}

static void __exit exit(void) {
  gpio_free(0);
}
Invoking userspace

Userspace application call be invoked from Kernel context using UMH API (Unser Mode Helper)

my-module.c
#include <linux/kmod.h>

char *envp[] = {"HOME=/", "PATH=/sbin:/bin:/usr/bin", NULL};
char *argv[] = {"/bin/my-prog", "some args", NULL};

call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
By Bertrand Tognoli
2021-12-07