Operating Systems 2019F: Tutorial 8

From Soma-notes
Jump to navigation Jump to search

Again in this tutorial you will be building and installing kernel modules. You will need root access to install kernel modules.

It is highly recommended that you use a comp3000 openstack instance for the exercises below for two reasons. First, you may have difficulties compiling kernel modules on other systems. Second, these operations are potentially dangerous and mistakes could destroy all data on the Linux system. Consider yourself warned!

In this tutorial you'll be playing with the remember kernel module. This module creates a device /dev/remember. You can write data to this device and when you read from it, it will return the data that you most recently wrote.

Getting started

First, make sure your system is ready to build things. On ubuntu:

 sudo apt install build-essential libelf-dev
 sudo apt clean

Note that libelf-dev is a new addition. (To update your system, run "sudo apt update; sudo apt dist-upgrade")

Download and unzip remember.zip. From the command line:

 wget https://homeostasis.scs.carleton.ca/~soma/os-2018f/code/tut7/remember.zip
 unzip remember.zip

Next, build the module and install it:

 cd remember
 make
 sudo insmod remember.ko

And then test the module:

 echo "Hello world" > /dev/remember
 cat /dev/remember

Be sure to check the kernel logs before and after. You may even want to follow the logs as you play with the module. To do this, run:

 tail -f /var/log/kern.log

You'll want to do this in a separate window.

Tasks/Questions

A good resource is this page on Linux kernel memory management.

Part A (mandatory)

  1. What can you do with the Makefile other than type "make"?
  2. How much data can you write to /dev/remember? What determines the limit? (Hint: try writing the contents of remember.c to /dev/remember)
  3. When is data being allocated? When is it deallocated?
  4. How can you increase the amount of data that /dev/remember stores? (Hint: what is saved_data_order for?)
  5. Does the kernel use virtual or physical addresses for its own data structures?
  6. How are we copying data to and from kernel space? Is it the same way we did before?
  7. What was changed from newgetpid.c that allows the /dev/remember device to be written to? What parts of the code had to be changed? What had to be added?

Part B (optional)

  1. If you remove the remember module while a process is accessing it, what happens to the process? What happens if you try to reload the remember module?
  2. What happens if you call class_create() in a module using a class name that already exists in the kernel?
  3. Fix the remember module so that it returns EFAULT to userspace when given an invalid pointer.
  4. In the remember module, modify remember_read() so it uses a non-zero offset properly rather than simply logging an error.
  5. Modify the remember module so llseek system calls work as they do on regular files, for whence values of SEEK_CUR and SEEK_SET. You should not allow the offset to be set to a value past the current end of file.
  6. Modify the remember module so the behavior of writes change as follows.
    1. Increase the allocation size to 16K of storage.
    2. Allocate memory when data is first written to /dev/remember and free memory when zero bytes are written to /dev/remember or when the remember module is unloaded.
    3. Preserve data across writes such that a shorter write preserves data from a previous longer write.
    4. Allow writes to non-zero offsets (subject to the maximum size of /dev/remember).

Code

remember.c

/* 
  remember.c

  remember module COMP 3000, Carleton University
  remembers what is written to it, returns in when read along with
  info about how it is stored

  License: GPLv2 or later
  Author: Anil Somayaji
  November 3, 2018

  device driver and module code derived from:
  https://appusajeev.wordpress.com/2011/06/18/writing-a-linux-character-device-driver/
  and
  http://pete.akeo.ie/2011/08/writing-linux-device-driver-for-kernels.html
*/

#include <linux/module.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/mm.h>

#define DEVICE_NAME "remember"
#define CLASS_NAME "comp3000"

static struct class* remember_class = NULL;
static struct device* remember_device = NULL;
static int remember_major;

static struct page *saved_data_page = NULL;
static char *saved_data = NULL;
static unsigned long saved_data_len = 0;
static int saved_data_max = PAGE_SIZE;
static int saved_data_order = 0;

static int remember_open(struct inode *the_inode, struct file *f)
{
        pr_info("Remember: device opened\n");
        return 0;
}

static ssize_t remember_read(struct file *f, char *buf, size_t len, loff_t *offset)
{
        unsigned long n;
        char *error_msg = "Buffer too small.";
        
        pr_info("Remember: read started\n");
        
        if (*offset > 0) {
                pr_info("Remember: read non-zero offset, aborting\n");
                return 0;
        }

        if (len < saved_data_len) {
                pr_info("Remember: read short buffer\n");
                n = strlen(error_msg) + 1;  // Include terminating null byte
                if (n > len) {
                        n = len;
                }
                copy_to_user(buf, error_msg, n);
                
                return n;
        } else {
                pr_info("Remember: read returning data, %ld bytes\n",
                        saved_data_len);
                copy_to_user(buf, saved_data, saved_data_len);
                *offset = saved_data_len;

                return saved_data_len;
        }
}

void init_saved_data(void)
{
        pr_info("Remember: allocating data page");

        if (saved_data) {
                pr_err("Remember: saved_data already initialized!");
        } else {       
                saved_data_page = alloc_pages(GFP_KERNEL,
                                              saved_data_order);
                saved_data = (char *) page_address(saved_data_page);
                saved_data_len = 0;

                pr_info("saved_data at kernel virtual address %lx",
                        (unsigned long) saved_data);
                pr_info("saved_data_page page struct at address %lx",
                        (unsigned long) saved_data_page);
        }
}

void free_saved_data(void)
{
        if (saved_data_page) {
                pr_info("Remember: freeing old data page");
                __free_pages(saved_data_page, saved_data_order);
                saved_data_page = NULL;
                saved_data = NULL;
                saved_data_len = 0;
        }
}

static ssize_t remember_write(struct file *f, const char *buf, size_t len,
                           loff_t *offset)
{
        unsigned long result;

        if (*offset > 0) {
                pr_info("Remember: write nonzero offset, aborting");

                return 0;
        }
        
        free_saved_data();
        init_saved_data();
        
        if (len > saved_data_max) {
                len = saved_data_max;
        }
        
        pr_info("Remember: write saving data, %ld bytes", len);

        result = copy_from_user(saved_data, buf, len);
        saved_data_len = len;        

        *offset = len;
        
        return len;
}

static int remember_release(struct inode *the_inode, struct file *f)
{        
        pr_info("Remember: device closed\n");
        return 0;
}


static struct file_operations remember_fops = {
        .open = remember_open,
        .read = remember_read,
        .write = remember_write,
        .release = remember_release,
};


static char *remember_devnode(struct device *dev, umode_t *mode)
{
        if (mode)
	        *mode = 0666;
        return NULL;
}

static int __init remember_init(void)
{
        int retval;
  
        remember_major = register_chrdev(0, DEVICE_NAME, &remember_fops);
        if (remember_major < 0) {
                pr_err("failed to register device: error %d\n", remember_major);
                retval = remember_major;
                goto failed_chrdevreg;
        }
 
        remember_class = class_create(THIS_MODULE, CLASS_NAME);
        if (IS_ERR(remember_class)) {
                pr_err("Remember: failed to register device class '%s'\n",
                       CLASS_NAME);
                retval = PTR_ERR(remember_class);
                goto failed_classreg;
        }
 
        remember_class->devnode = remember_devnode;

        remember_device = device_create(remember_class, NULL,
                                        MKDEV(remember_major, 0),
                                        NULL, DEVICE_NAME);

        if (IS_ERR(remember_device)) {
                pr_err("Remember: failed to create device '%s'\n", DEVICE_NAME);
                retval = PTR_ERR(remember_device);
                goto failed_devreg;
        }
        
        pr_info("Remember: device registered using major %d.\n",
                remember_major);
        
        return 0;
        
 failed_devreg:
        class_unregister(remember_class);
        class_destroy(remember_class);
 failed_classreg:
        unregister_chrdev(remember_major, DEVICE_NAME);
 failed_chrdevreg:
        return -1;
}

static void __exit remember_exit(void)
{
        free_saved_data();

        device_destroy(remember_class, MKDEV(remember_major, 0));
        class_unregister(remember_class);
        class_destroy(remember_class);
        unregister_chrdev(remember_major, "remember");
        pr_info("Unloading Remember module.\n");
        return;
}

module_init(remember_init);
module_exit(remember_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Anil Somayaji <soma@scs.carleton.ca>");
MODULE_DESCRIPTION("A remember character device module");

Makefile

Note that the large spaces below should be tabs.

MODNAME := remember
obj-m := $(MODNAME).o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

clean:
        /bin/rm -f *.o *.ko *~
        /bin/rm -f Module.symvers modules.order Modules.symvers *.mod.c
        /bin/rm -f .*o.cmd .cache.mk
        /bin/rm -rf .tmp_versions

zip:
        rm -f $(MODNAME).zip
        mkdir $(MODNAME)
        cp -a Makefile $(MODNAME).c $(MODNAME)
        zip -r $(MODNAME).zip $(MODNAME)
        rm -rf $(MODNAME)