Today, I experimented with the linux kernel modules for the first time and I’ve written a simple module that prints a message (helloworld :P) every time that someone reads from the /dev/ktest
(a custom character device) and counts how many times the device was opened for reading.
Here’s what I’ve done step-by-step:
- I made sure that the linux headers are installed. On Debian:
1# apt-get install linux-headers-`uname -r` - I wrote a Makefile:
123456789obj-m += ktest.o.PHONY: allall:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules.PHONY: cleanclean:make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
The Makefile runs make in in /lib/modules/uname -r
/build. This directory contains another Makefile which is used to actually compile my module. - After the Makefile, I’ve written the code of the test module (ktest.c: https://eleni.mutantstargoat.com/hg/index.cgi/kernel-helloworld-test/file/dbbd63da261f/ktestmodule/ktest.c). Let’s examine it line by line:
Every module starts by calling some macros. Apart from those for the license, author, description that obviously set the license, the author and the description, we need to call module_init and module_exit. The module_init macro’s parameter is a function pointer to a module initialization callback that will be called at the begining, whereas the module_exit macro’s parameter is a function pointer to a callback that will be called at the end. The callbacks are the ktest_ini and ktest_fini functions below.
1234567/* macros */MODULE_LICENSE("GPL");MODULE_AUTHOR("hikiko");MODULE_DESCRIPTION("my kernel helloworld");module_init(ktest_init);module_exit(ktest_fini);
Let’s see the implementation:The initialization callback registers a character device (that we previously defined as DEV_NAME “/dev/ktest”) and prints information on success or failure.
123456789101112static int ktest_init(void){hello_counter = 0;/* register character device */if((dev_num = register_chrdev(0, DEV_NAME, &ops)) < 0) {printk(KERN_ALERT "Failed to register character device.\n");return -1;}printk(KERN_INFO "Registered character device. Name: %s number: %d.\n", DEV_NAME, dev_num);return 0;}The exit callback unregisters the device.
123456static void ktest_fini(void){unregister_chrdev(dev_num, DEV_NAME);printk(KERN_INFO "Unregistered character device. Name: %s number: %d.\n", DEV_NAME, dev_num);}In order to perform operations on this device, we need to fill a struct of type file_operations:
1234static struct file_operations ops = {.open = ktest_open,.release = ktest_release,.read = ktest_readWe don’t need to fill all the fields, but only those that interest us. The rest will be set to zero automatically. By populating .open, .release, .read we set the functions that are going to be called when we open, release and read the device, respectively.
123456static int ktest_open(struct inode *inode, struct file *file){/* 1 device file only => we ignore the params ^ */try_module_get(THIS_MODULE);return 0;}This function is used to tell the kernel which device we want to open (inode = the filesystem node). Since we only have one device file we can ignore the parameters for simplicity and call try_module_get(THIS_MODULE). This call is supposed to be unsafe in general but I didn’t care much since this is only a helloworld example and I wanted to keep things simple.
12345static int ktest_release(struct inode *inode, struct file *file){module_put(THIS_MODULE);return 0;}Same here, I ignored the parameters and used module_put for simplicity.
The function that is called when we read from the device is the following:
1234567891011121314151617181920212223242526static ssize_t ktest_read(struct file *file, char *buf, size_t buf_size,loff_t *offset){/* buf = userspace buffer,* kbuf = kernel buffer */char *kbuf;int bytes;if(!(kbuf = kmalloc(buf_size, GFP_KERNEL))) {printk(KERN_ALERT "Failed to allocate memory.\n");return -ENOMEM;}/* fill kbuf and copy to userspace buf */bytes = snprintf(kbuf, buf_size, "Hello world, num: %d.\n", ++hello_counter);if(copy_to_user(buf, kbuf, bytes)){kfree(kbuf);return -EFAULT;}kfree(kbuf);return bytes;}This function takes as a parameter a pointer to the device, a pointer to a userspace buffer and the buffer size and offset. What I’ve done here was to create a kernelbuffer (kbuf) in the size of the userspace buffer, fill it with some text, copy it to the userspace buffer (to pass it to the user program) and free the kernel buffer.
- Next step was to insert the module to the kernel and test it works.
I created a device /dev/ktest first:1# mknod /dev/ktestThere might be a better way to create devices using for example udev when the module is loaded or something else but I didn’t know how to do it in a generic way that works everywhere, so, I just called mknod to create the device quickly. :-p
To insert the ktest module to the kernel:
1# insmod ktest.koafter running make. I used dmesg to see if the ktest device was registered, the message was something like:
[524178.076365] Registered character device. Name: ktest number: 243.
To test if the module works, I initially used cat but then I’ve written a short program myself to read the output once every time (cat reads from the device continuously):
12345678910111213141516171819202122232425262728#include ;#include#includeint main(void){char buf[512];size_t size;int fd;if((fd = open("/dev/ktest", O_RDONLY)) == -1) {fprintf(stderr, "Failed to open device ktest.\n");return 1;}if((size = read(fd, buf, sizeof buf)) == -1) {fprintf(stderr, "Failed to read from device ktest.\n");close(fd);return 1;}printf("num char: %d\n", (int)size);buf[size] = '\0';printf("quoting kernel: %s", buf);close(fd);return 0;}By running it I was able to see the helloworld output. To remove the module I ran:
1# rmmod ktest.ko
Test code:
https://eleni.mutantstargoat.com/hg/index.cgi/kernel-helloworld-test/file