Introduction

If a user application is not working properly or goes down or gets killed, currently there are only limited ways to detect and monitor such issues. Also, most of the times monitoring and maintaining high availability of an application requires third-party software. This article provides a way to implement a kernel module on Linux, compile it, and explore ways in which a user application can monitor and communicate with this kernel module. The kernel module can continuously monitor the user application, take in commands from the user application, create raw sockets, bind on them, and send data.

Figure 1. Two-way interaction between a user-application and a kernel module

Two-way interaction between a user-application and a kernel module

Figure 1 shows the user-application and kernel module interaction components.

Application: It can be any application running at user-level which can interact with kernel module.

Kernel module: It contains the definition of the system calls that can be used by the application and kernel level APIs to monitor application health, communicate with user level applications, and so on.

Prerequisites to use Linux kernel extension

Linux kernel extension is used to monitor application health, communicate with user-level applications, and manage kernel logs. The following prerequisites are required for users to make use of it.

  • Knowledge of C programming language
  • Knowledge of Linux operating system
  • Understanding about GCC complier

Load/Unload and check status of kernel module

The following commands can be used to load or unload and list various kernel modules. These will be helpful in writing and using kernel extensions through user applications.

Loading kernel module

insmod: This command is used to insert a module into the kernel.

Usage: insmod ‘kernel_ext_binary’

# insmod helloWorld.ko
Welcome to Hello world Module.

Unloading kernel module

Usage: rmmod ‘kernel_ext_binary’

# rmmod helloWorld.ko
Goodbye, from Hello world.

Listing of all running kernel modules

lsmod: This command lists all the kernel modules that are currently loaded. Usage: lsmod | grep ‘kernel_ext_binary’

# lsmod | grep hello
helloWorld 12189  1

Detailed information about kernel module

modinfo: This command displays additional information about the module. Usage: modinfo hello*.ko

# modinfo helloWorld.ko
filename:       /root/helloWorld.ko
description:    Basic Hello World KE
author:         helloWorld
license:        GPL
rhelversion:    7.3
srcversion:     5F60F86F84D8477986C3A50
depends:
vermagic:       3.10.0-514.el7.ppc64le SMP mod_unload modversions

The above commands can be run on console and through a binary application using the system() call.

Communicate with user space

The user space must open a file specified by path name using the open() API. This file will be used by both, the user application and the kernel module to interact with each other. All commands and data from the user application are written to this file (from which the kernel module will read and act upon). The reverse is also possible.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);

Usage:

int fd;
#define DEVICE_FILE_NAME "/dev/char_dev"
fd = open(DEVICE_FILE_NAME, 0);

The return value of open() is a file descriptor, a small non-negative integer that is used in subsequent system calls (in this case, ioctl).

Use of ioctl calls

The ioctl() system call can be invoked from a user space to manipulate the underlying device parameters.

#include <sys/ioctl.h>

int ioctl(int fd, int cmd, ...);

Here, fd is the file descriptor returned from open() and cmd is the same as what is implemented in the kernel module’s ioctl().

Usage:

#define IOCTL_SEND_MSG _IOR(MAJOR_NUM, 0, char *)
int ret_val;
char message[100];
ret_val = ioctl(file_desc, IOCTL_SEND_MSG, message);
if (ret_val < 0) {
printf("ioctl_send_msg failed:%d\n", ret_val);
exit(−1);
}

In the above example IOCTL_SEND_MSG is a command which is sent to the module.

_IOR means that the application is creating an ioctl command number for passing information from a user application to the kernel module. The first argument, MAJOR_NUM, is the major device number we’re using.

The second argument is the number of the command (there could be several with different meanings).

The third argument is the type we want to get from the process to the kernel.

In the same way, user application can get a message from kernel with little modification to the ioctl arguments.

Thread handling at the kernel module

The following sections provide ways to handle multithreading in kernel context. We can create and manage multiple kernel threads inside a kernel module.

Thread creation

We can create multiple threads in a module using the following calls.

#include <linux/kthread.h>
static struct task_struct * sampleThread = NULL;
sampleThread = kthread_run(threadfn, data, namefmt, …)

kthread_run() creates a new thread and tells it to run. threadfn is the function name to run. data *is a pointer to the function arguments namefmt is the name of the thread (in ps command output)

Thread stopping

We can stop running threads by using the following call:

kthread_stop(sampleThread);

This API stops the thread gracefully.

Establish socket communication

It is possible to create a raw socket in the kernel mode using the sock_create() function. Through this socket, a kernel module can communicate with other user-level applications within or outside the host.

struct socket *sock;
struct sockaddr_ll *s1 = kmalloc(sizeof(struct sockaddr_ll),GFP_KERNEL);
result = sock_create(PF_PACKET, SOCK_RAW, htons(ETH_P_IP), &sock);
if(result < 0)
{
printk(KERN_INFO "[vmmKE] unable to create socket");
    return -1;
}

//copy the interface name to ifr.name  and other required information.
strcpy((char *)ifr.ifr_name, InfName);
s1->sll_family = AF_PACKET;
s1->sll_ifindex = ifindex;
s1->sll_halen = ETH_ALEN;
s1->sll_protocol = htons(ETH_P_IP);

result = sock->ops->bind(sock, (struct sockaddr *)s1, sizeof(struct sockaddr_ll));
if(result < 0)
{
printk(KERN_INFO "[vmmKE] unable to bind socket");
    return -1;
}

Using sock_sendmsg(), the kernel module can send data using the computed message structure.

struct msghdr message;
int ret= sock_sendmsg(sock, (struct msghdr *)&message);

Generate signals to user space process

Signals can also be generated from the kernel module to user application. If the process identifier (PID) of the user process is known to kernel, using this pid the module can fill in the required pid structure and pass it on to send_sig_info() to trigger the required signal.

struct pid *pid_struct = find_get_pid(pid);
struct task_struct *task = pid_task(pid_struct,PIDTYPE_PID);
int signum = SIGKILL, sig_ret;
struct siginfo info;
memset(&info, '\0', sizeof(struct siginfo));
info.si_signo = signum;
//send a SIGKILL to the daemon
sig_ret = send_sig_info(signum, &info, task);
if (sig_ret < 0)
{
printk(KERN_INFO "error sending signal\n");
return -1;
}

Log rotation

A log rotation file is used by rsyslog to support log rotation of kernel module logs. If the user wants to redirect all specific kernel module related logs to a specific file, an entry needs to be added to rsyslog (/etc/rsyslog.conf) as follows.

Usage:

:msg,startswith,\"[HelloModule]\" /var/log/helloModule.log

The above entry allows rsyslog to redirect all kernel logs starting with [HelloModule] to the /var/log/helloModule.log file. You can look for more samples in the /etc/rsyslog.conf file.

Sample: Users can write their own rotation script (such as the following one) and place it in /etc/logrotate.d.

"/var/log/helloModule.log" {
daily
rotate 4
maxsize 2M
create 0600 root
postrotate
    service rsyslog restart > /dev/null
endscript
}

This script checks daily if the log file has exceeded 2M (that is, 2 MB) size and supports 4 rotations of this file. If the log size exceeds 2 MB, it will create a new file with same name and 0600 file permissions, and the old file will be appended with the date time stamp.

Post rotation, it will restart the rsyslog service.

Makefile

Refer to the following content of the makefile to generate the binaries for a sample program, helloWorld.c

obj−m += helloWorld.o
all:
make −C /lib/modules/$(shell uname −r)/build M=$(PWD) modules
clean:
make −C /lib/modules/$(shell uname −r)/build M=$(PWD) clean

Note: This sample is based for RHEL flavor, other flavors may vary in implementation of makefile.

Integration of kernel module with user application

This section shows a sample user application and a kernel module. The user application will be able to communicate with the kernel module and exchange data.

The user application uses ioctl calls to send data to the kernel module. In the following example, these ioctl calls can be used to send application-specific details or send any updates at a later point of time. The following sample program incorporates all the concepts explained earlier.

Sample user application

# cat helloWorld.h

#ifndef HELLOWORLD_H
#define HELLOWORLD_H
#include <linux/ioctl.h>

// cmd ‘KE_DATA_VAR’ to send the integer type data
#define KE_DATA_VAR _IOR('q', 1, int *)

#endif

# cat helloWorld.c

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include "helloWorld.h"

/* @brief: function to load the kernel module */
void load_KE()
{
    printf ("loading KE\n");
    if (system ("insmod /root/helloWorld.ko") == 0)
    {
        printf ("KE loaded successfully");
    }
}

/* @brief: function to unload the kernel module */
void unload_KE()
{
    printf ("unloading KE\n");
    if (system ("rmmod /root/helloWorld.ko") == 0)
    {
        printf ("KE unloaded successfully");
    }
}

/* @brief: method to send data to kernel module */
void send_data(int fd)
{
    int v;

    printf("Enter value: ");
    scanf("%d", &v);
    getchar();
    if (ioctl(fd, KE_DATA_VAR, &v) == -1)
    {
        perror("send data error at ioctl");
    }
}

int main(int argc, char *argv[])
{
    const char *file_name = "/dev/char_device"; //used by ioctl
    int fd;
    enum
    {
        e_load, //load the kernel module
        e_unload, //unload the kernel module
        e_send, //send a HB from test binary to kernel module
    } option;

    if (argc == 2)
    {
        if (strcmp(argv[1], "-l") == 0)
        {
            option = e_load;
        }
        else if (strcmp(argv[1], "-u") == 0)
        {
            option = e_unload;
        }
                }
        else if (strcmp(argv[1], "-s") == 0)
        {
            option = e_send;
        }
        else
        {
            fprintf(stderr, "Usage: %s [-l | -u | -s ]\n", argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr, "Usage: %s [-l | -u | -s ]\n", argv[0]);
        return 1;
    }

    if ((option != e_load) && (option != e_unload))
    {
        fd = open(file_name, O_RDWR);
        if (fd == -1)
        {
            perror("KE ioctl file open");
            return 2;
        }
    }
    switch (option)
    {
        case e_load:
            load_KE();
            break;
        case e_unload:
            unload_KE();
            break;
        case e_send:
            send_data(fd);
            break;
        default:
            break;
    }

    if ((option != e_load) && (option != e_unload))
    {
        close (fd);
    }
return 0;
}

Sample kernel module
# cat helloWorld.c
#include <linux/slab.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <asm/uaccess.h>
#include <linux/time.h>
#include <linux/mutex.h>
#include <linux/socket.h>
#include <linux/ioctl.h>
#include <linux/notifier.h>
#include <linux/reboot.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/kmod.h>
#include <linux/if.h>
#include <linux/net.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/time.h>
#include <linux/delay.h>

typedef struct
{
    char ethInfName[8];
    char srcMacAdr[15];
    char destMacAdr[15];
    int ifindex;
}KEConfig_t;

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Owner Name");
MODULE_DESCRIPTION("Sample Hello world");
MODULE_VERSION("0.1");

static char *name = "world";
static struct task_struct *ke_thread;
static struct KEConfig_t KECfg;
module_param(name, charp, S_IRUGO);
MODULE_PARM_DESC(name, "The name to display in /var/log/kern.log");


/* @brief: create socket and send required data to HM
 * creates the socket and binds on to it.
 * This method will also send event notification
 * to HM.
 * */
static int createSocketandSendData(char *data)
{
    int ret_l =0;
    mm_segment_t oldfs;
    struct msghdr message;
    struct iovec ioVector;

    int result;
    struct ifreq ifr;
    struct socket *sock;
    struct sockaddr_ll *s1 = kmalloc(sizeof(struct sockaddr_ll),GFP_KERNEL);
    if (!s1)
    {
       printk(KERN_INFO "failed to allocate memory");
       return -1;
    }
    printk(KERN_INFO "inside configureSocket");
    memset(s1, '\0', sizeof(struct sockaddr_ll));
    memset(픦, '\0', sizeof(ifr));

    result = sock_create(PF_PACKET, SOCK_RAW, htons(ETH_P_IP), &sock);
    if(result < 0)
    {
        printk(KERN_INFO "unable to create socket");
        return -1;
    }
    printk(KERN_INFO "interface: %s", KECfg.ethInfName);
    printk(KERN_INFO "ifr index: %d", KECfg.ifindex);
    strcpy((char *)ifr.ifr_name, KECfg.ethInfName);

    s1->sll_family = AF_PACKET;
    s1->sll_ifindex = KECfg.ifindex;
    s1->sll_halen = ETH_ALEN;
    s1->sll_protocol = htons(ETH_P_IP);
result = sock->ops->bind(sock, (struct sockaddr *)s1, sizeof(struct sockaddr_ll));
    if(result < 0)
    {
        printk(KERN_INFO "Unable to bind socket");
        return -1;
    }

    //create the message header
    memset(&message, 0, sizeof(message));
    message.msg_name = sockData->sock_ll;
    message.msg_namelen = sizeof(*(sock_ll));

    ioVector.iov_base = data;
    ioVector.iov_len  = sizeof(data);
    message.msg_iov = &ioVector;
    message.msg_iovlen = 1;
    message.msg_control = NULL;
    message.msg_controllen = 0;
    oldfs = get_fs();
    set_fs(KERNEL_DS);
    ret_l = sock_sendmsg(sockData->sock, &message, sizeof(data));


    return 0;
}

static long ke_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
    int b;

    switch (cmd)
    {
        case KE_DATA_VAR:
        if (get_user(b, (int *)arg))
        {
        return -EACCES;
        }
        //set the time of HB here
        mutex_lock(&dataLock);
        do_gettimeofday(&hbTv);
        printk(KERN_INFO "time of day is %ld:%lu \n", hbTv.tv_sec, hbTv.tv_usec);
        printk(KERN_INFO "data %d\n", b);
        //send data out
        createSocketandSendData(&b);
        mutex_unlock(&dataLock);
        break;
        default:
            return -EINVAL;
    }

    return 0;
}

/* @brief: method to register the ioctl call */
static struct file_operations ke_fops =
{
    .owner = THIS_MODULE,
#if (LINUX_VERSION_CODE < KERNEL_VERSION(2,6,35))
    .ioctl = ke_ioctl
#else
    .unlocked_ioctl = ke_ioctl
#endif
};

/* @brief The thread function */
int ke_init()
{
    printk(KERN_INFO "Inside function");
    return 0;
}

/* @brief The LKM initialization function */
static int __init module_init(void)
{
   printk(KERN_INFO "module_init initialized\n");
   if ((ret = alloc_chrdev_region(&dev, FIRST_MINOR, MINOR_CNT, "KE_ioctl")) < 0)
   {
       return ret;
   }

   cdev_init(&c_dev, &ke_fops);

   if ((ret = cdev_add(&c_dev, dev, MINOR_CNT)) < 0)
   {
       return ret;
   }

   if (IS_ERR(cl = class_create(THIS_MODULE, "char")))
   {
       cdev_del(&c_dev);
       unregister_chrdev_region(dev, MINOR_CNT);
       return PTR_ERR(cl);
   }
   if (IS_ERR(dev_ret = device_create(cl, NULL, dev, NULL, "KEDevice")))
   {
       class_destroy(cl);
       cdev_del(&c_dev);
       unregister_chrdev_region(dev, MINOR_CNT);
       return PTR_ERR(dev_ret);
   }

   //create related threads
   mutex_init(&dataLock); //initialize the lock
   KEThread = kthread_run(ke_init,"KE thread","KEThread");

  return 0;
}

void thread_cleanup(void)
{
    int ret = 0;

    if (ke_thread)
    ret = kthread_stop(ke_thread);
    if (!ret)
        printk(KERN_INFO "Kernel thread stopped");
}

/* @brief The LKM cleanup function */
static void __exit module_exit(void)
{
   device_destroy(cl, dev);
   class_destroy(cl);
   cdev_del(&c_dev);
   unregister_chrdev_region(dev, MINOR_CNT);

   thread_cleanup();
   printk(KERN_INFO "Exit %s from the Hello world!\n", name);
}

module_init(module_init);
module_exit(module_exit);

A module always begins with either init_module or the function you specify with the module_init() call. This is the entry function for modules. It tells the kernel what functionality the module provides and sets up the kernel to run the module’s functions when they’re needed.

All modules end by calling either cleanup_module() or the function you specify with the module_exit() call. This is the exit function for modules. It undoes whatever the entry function did. It unregisters the functionality that the entry function registered.

Note: Suppose a file is opened at user space using open(), which is used to communicate with the kernel module and

if any execve() call is executed by the user process, then set the FD_CLOEXEC socket option on fd (file descriptor).

fd = open(“/dev/char_device”, O_RDWR);
fcntl(fd, F_SETFD, FD_CLOEXEC);

If the FD_CLOEXEC option is not set on this fd, the file descriptor is set to remain open across an execve() call. The kernel module fails to unload complaining that it is already in use and is dependent on the process initialized through the execve() call.

Summary

This article can help you write, compile, and run Linux kernel extensions in general. In addition, it also provides specific ways to monitor user applications, their health, and restart user applications if it fails or hangs. It also provides ways to perform kernel logging, establish layer two socket communication with user applications, and perform synchronization between kernel extension and user application.