Please follow the following submission criteria:

  1. Submit a ZIP file which contains all your code.
  2. The ZIP file should contain one directory for each part, named part1 and part2.
  3. For each part, include a text file for your written answers, and any additional files that are required for each question.

Please see the Assignment 2 submission guidelines for further information on how to set up a VM for these kernel programming exercises.

In Assignments 2 and 4, we wrote some basic kernel modules that can be dynamically loaded into the kernel at runtime. While kernel modules are powerful, it is sometimes necessary to make direct modifications to the kernel source code. For example, Google's Android operating system, likely one of the most successful Linux distributions to date, uses its own fork of the Linux kernel to implement a number of features related to, e.g., power management, memory management, logging, and interprocess communication.

In this assignment, we look at how to modify system calls in the Linux kernel. It is in fact possible to alter the behaviour of system calls from a kernel module by intercepting them using a technique called hooking, but we will look instead at how to directly modify the system call source.

For the previous assignments, we only needed the kernel headers, which allowed us to compile our modules against the kernel version that we were using. Linux distributions generally provide tools to build the kernel source of the existing version that comes pre-installed with the distribution. For example, with Ubuntu you can run apt-get source linux-image-$(uname -r) to download the source for the kernel that you are currently running. However, we're going to do something more fun - we're going to download the bleeding-edge source code from Linus Torvalds' git repository. If you're not familiar with git, I highly recommend this free book.

Before you continue, you may want to make sure that you have all the prerequisites installed on your VM. You should run:

  • sudo apt-get install build-essential to install the gcc compiler and some other libraries (this is probably already installed)
  • sudo apt-get install git to install git
  • sudo apt-get install libncurses5-dev, which is needed for the text-based menu system for configuring the kernel
  • sudo apt-get build-dep linux to install any remaining prerequisite packages for building the kernel

Now that you are ready, download the kernel source to your working directory. You can find the URL for Linus Torvalds' repository on this page. Next, we need to configure the kernel to specify which modules we want to include in it. There are various ways in which this can be done, the most tedious of which is to run make menuconfig and manually go through all the options. Note that including more options will make the kernel larger and it will also take much longer to compile. When you are finished modifying your configuration, it will be saved to the .config file in your kernel source directory. There are a couple of other commands that can make your life easier when configuring your kernel. Running make oldconfig will copy the configuration file for the kernel currently installed on your system, which is usually stored in /boot/config-$(uname -r), and will prompt you for any new options that might be available in the more recent kernel that you are building (just accept the default by pressing Enter for any new options that you are prompted with). This will save you the manual labour of having to go through all the options yourself, and will ensure that you will be able to boot successfully from your new kernel.

Hint:

The default Ubuntu configuration will leave many modules enabled which you probably don't need. If you want to cut down on your compilation time, you can run make localmodconfig, which will look at all the modules that are currently running and will disable everything but those modules in your configuration file. This will drastically cut down your compilation time, but does have certain pitfalls (so only use this option if you really want to cut down compilation time, and are willing to take the risk that you may face some problems): If you have any hardware that isn't currently plugged in, any modules that may be required to drive that hardware will probably be disabled in your configuration file. For example, the CD-ROM/DVD file system module will probably be disabled, so you should mount a CD image before running make localmodconfig. You can do this by clicking on the "Device" menu in Virtualbox and selecting "Insert Guest Additions CD Image...". You will likely need to re-install the Guest Additions (by running autorun.sh from the CD image) after installing your new kernel, if you want to restore some VirtualBox features such as mouse/clipboard integration, drag and drop support, etc. If you run into problems with missing modules, you can always run make menuconfig again, select the missing modules that you need, and re-run make modules and make modules_install. If your kernel fails to boot, you can reboot your VM and hit the "Escape" key before Ubuntu starts booting to bring up the GRUB menu, where you can choose an older kernel to boot from.

After configuring your kernel, you will need to run make bzImage to compile the kernel, followed by make modules to compile the modules. You may then install your kernel by running sudo make modules_install and sudo make install (in that order).

To get full marks for this part, please complete the following steps:

  1. Download the kernel source from Linus' repository.
  2. Configure the kernel using whichever method that you prefer.
  3. Modify the configuration so that your name shows up in the kernel version string (the kernel version string is what appears when you run uname -r in a terminal). Hint: You will find what you need under "General setup".
  4. Submit your .config file (2 marks).
  5. Submit a dmesg log showing that you have successfully booted into your newly-compiled kernel (3 marks).

Our goal for this exercise is to modify the execve() system call so that any command that starts with "comp3000" will automatically be given a higher priority. To figure out how to do this, let us first look at a couple of useful tools.

In Unix-based systems, a process' priority is determined by its nice value, which has a minimum value of -20 and maximum value of 19. New processes inherit the nice value of their parent, and typically any command run by a user will have a default nice value of 0. Note that the higher the nice value, the lower the priority of the process (hence the term "nice", since the process is being "polite" to the rest of the processes running on the system). This value can be changed either:

  • For a new process, by running nice -n [nice value] [command] to execute your command with the given nice value
  • For existing processes, by running renice [nice value] -p [pid] to change the nice value of the process with the given pid

Another useful tool is strace, which is used to execute any command and output all the system calls that have been made. For example, if you run strace ls in a terminal, it will display all the system calls that the ls command invokes to list the contents of your current directory.

You may want to type man nice or man strace in a terminal to see more detailed instructions on how to use these tools.

The first step in this exercise is to find which system call is used to change a process' priority. To do this, you may use strace to execute any command through nice and observe the resulting output. Note that the nice command simply changes its own nice value and then invokes execve() to replace its own process image with whatever command you give it. Please submit the following:

  1. Your strace output (note that you can use the -o flag to save the output to a text file) (2 marks).
  2. The name of the system call that is used to set the nice value of the process (2 marks).
  3. The name of the kernel source file where the above system call is defined (2 marks).
  4. The name of the kernel source file where execve() is defined (2 marks).

Hint:

Make use of the Linux Cross-Reference, which we used in Assignments 2 and 4, and note that all system calls are defined using the macro SYSCALL_DEFINEX(), where X is the number of arguments that the system call requires.

After completing the above steps, you should be ready to make the necessary modifications to your kernel source. Modify the execve() system call so that any file name that contains "comp3000" is given a nice value of -10 (8 marks). Please follow the instructions below for submitting your code:

  1. Before making any modifications, you may want to create your own branch (this is not strictly necessary, but will make your life easier in case you want to pull in any updates from the master branch at a later point in time) by running git checkout -b mybranchname.
  2. After you have made your modifications, you should commit them by running git commit -a -m "Your commit message".
  3. Finally, run git format-patch -1 HEAD to generate a patch file containing the changes that you have made in your most recent commit.
  4. Submit the patch file from the above step, along with the actual files that you have modified.

Hint:

You cannot invoke a system call from within another system call. You will need to read through the source of the system call that changes the nice value of a process, understand how it is done, and make the necessary modifications to execve().

Help! I ran out of space on my VM!

Unfortunately, it seems that the VM image is configured to use a very small disk image. The root partition is only 6.8GB, and there is only 2.1GB of space available when the VM is freshly imported. My linux source directory is about 2GB, so it's unlikely that you will have enough space to compile the kernel without adding more disk space. There are two ways to work around this issue:

  • The easy way (adding a new disk to the VM):
    1. Shut down your VM.
    2. In VirtualBox, right-click on the VM and click "Settings".
    3. Click on the "Storage" tab on the left-hand side.
    4. Click on "Controller: SATA" in the storage tree. Then, click on the button that appears beside it with a + sign, which should be labeled "Adds hard disk" when you hover over it with your mouse.
    5. Click the "Create new disk" button in the dialog box that pops up.
    6. In the wizard that pops up, pick the default options "VDI" and "Dynamically allocated", and choose whatever size you would like for the disk image. I would choose at least 6GB to be safe - if you choose the "dynamically allocated" option, it won't actually use up the maximum disk space on your computer unless you actually fill the disk up with data. So in other words, if you have a 4GB disk image with no files in it, it won't take up any space on your computer. The only potential downside is that once the image grows (as you fill it up with files), it can't be shrunk again by deleting the files.
    7. Boot up your VM.
    8. Click on the application menu (bottom left corner) and navigate to Preferences > Disks.
    9. You should now have a second hard disk (probably /dev/sdb). Select the new disk on the left-hand side and format it by clicking the gear icon and choosing the default menu options "Don't overwrite existing data (Quick)" and "Compatible with Linux systems (Ext4)".
    10. After formatting, click the gear icon again and choose "Edit mount options". Turn off the automatic mount options, and specify a mount point (i.e., the directory into which you would like to mount the new disk). For example, you may choose "/scratch". Also, replace the flags "nosuid,nodev,nofail,x-gvfs-show" with "defaults".
    11. Open a terminal, and create the directory that you chose for your mount point (e.g., "sudo mkdir /scratch").
    12. Type in "sudo mount -a" to mount the new disk.
    13. If you run "df -h", you will see that your new disk /dev/sdb should be mounted to the mount point that you specified. You may now continue your work on this new disk.
  • The hard (?) way: You can try to resize your VM's existing disk image, if you can figure out how to do it properly. See here for some useful information.

How do I check the nice values of the processes that are running on my system?

You can check this using the ps tool. Typing ps lax in a terminal will list all the processes, and the nice values will be listed under the "NI" column. Type man ps if you would like to learn more about the different options that you can use.