Session Module
Advanced Operating Systems and Virtualization - A.A. 2018/2019

Final Project Report

Author: Federico Alfano

Introduction

It's a subsystem that makes the user able to access a file using sessions. In this way every modification is not visible until the session is close. Many processes can access concurrently a given file. The system provides also some statistical information into the /sys/kernel/session-module directory where the user can:

The idea is to create a new file, copy the old one into it and then redirect all the VFS operations on the session file overwriting the file operations. At the close, the session file is will be copied back into the original one and then is unlinked.

Usage

The module needs to be compiled and then inserted, so with a terminal open into the module directory we need to launch the following commands:

1 make
2 sudo insmod session-module.ko

On the contrary if we want to remove the module this is the right command:

1 sudo rmmod session-module

The user can be allowed to use the module's facilities by installing the userspace library into the libsession folder with those commands

1 make
2 sudo make install

and then copying this code inside the source of your program

1 #ifndef SESSION
2 #define SESSION
3 
4 
5 extern int session_init(void);
6 
7 extern int session_open(int , int );
8 
9 extern int session_close(int , int );
10 
11 extern int session_exit(int );
12 
13 #endif

finally you need to compile with the -lsession flag

User Space Library

The userspace library is a simple interface that provides the user a set of operations to interact with the module. In particular the operations needed are:

All the operations are a call to the facilities provided by the module

1 int session_init()
2 {
3  return open(CHAR_DEVICE, O_RDWR);
4 }
5 
6 int session_open(int session_id, int fd)
7 {
8  return ioctl(session_id, SESSION_OPEN, &fd);
9 }
10 
11 int session_close(int session_id, int fd)
12 {
13  return ioctl(session_id, SESSION_CLOSE, &fd);
14 }
15 
16 int session_exit(int session_id)
17 {
18  return close(session_id);
19 }

Kernel-Level Data Structures

The subsystem has two global rbtrees that are represented respectively by:

They contain the counter for the per-process sessions and the per-file sessions. The system relies also on two global variables:

The former is used to store the current base path where the sessions can live, the latter keeps track of the total number of sessions. All data can be read into the /sys directory and the path can also be modified from the user.

Kernel-Level Subsystem Implementation

Initialization

The initialization allocates dynamically a MAJOR and a single MINOR for the char device and then initializes that and set the global variable my_cdev that is a struct defined into the header, containing the char device and a kobject. The next step is the build of the sysfs tree that contains all the files needed for keeping track of sessions and for managing the path

Sysfs structure

The last step is to set the PWD as default base path

1 get_fs_pwd(current->fs ,&base_path);

Module operations

The device initialization wants file_operations structure in order to control the behaviour of the module; the following implementations only need three of them:

The third one is the essence of the module, so it deserves a dedicated paragraph.

module_open

It's called when the user wants to start to work with sessions, so the first thing to do is to check if a session is already open and return an EEXIST error if it's so

1 if((node = rb_search(&root, pid, &tree_rwlock)))
2  return -EEXIST;

after that, it will create a node into the rbtree containing the processes:

1 node = kmalloc(sizeof(struct session_proc_node), GFP_KERNEL);
2  scnprintf(node->key, PROC_NAME_LENGTH, "%d", current->pid);
3  atomic_set(&node->session_counter, 0);
4  rb_insert(&root, node, &tree_rwlock);

The insertion is sensitive to race conditions, so it keeps a rwlock that is managed into the helper function in tree_utils.c. And finally it creates a file into /sys/kernel/session_module/proc directory with the number of open sessions.

1 proc_counter_attr = get_attribute(node->key, 0444, proc_counter_show, NULL);
2  node->attr = (const struct attribute*) &proc_counter_attr->attr;
3  sysfs_create_file(proc_kobj, &proc_counter_attr->attr);

module_release

Basically, the release method is a check to verify if the user has closed the module in the right way.

1 if(current_node!=NULL)
2 {
3  clear_proc_node();
4  send_sig(SIGPIPE, current, 0);
5 }

module_ioctl

The function is in charge to manage all the operations on the file, let's see them in detail:

OPEN_SESSION

This is the operation called at the opening of the session. After some checks on the validity of the file and of permissions it performs the following operations:

1 session_filp = filp_open(cur_addr, O_TMPFILE | O_RDWR , 0644);
2 down_read(priv_data->rw_sem);
3 err = vfs_copy_file_range(original_filp, 0, session_filp, 0, i_size_read(original_filp->f_inode), 0);
4 up_read(priv_data->rw_sem);
5 session_filp->f_flags = original_filp->f_flags;

this file will be pointed out by the private_data field which is located into the original file. This one is a special structure defined in the header which contains the session file pointer, the absolute path and a semaphore that is used to protect sessions during the close.

1 tmp_key = d_path(&original_filp->f_path, tmp_addr, MAX_PATH_SIZE);
2  f_node = rb_search_file(&root_files, tmp_key, &tree_rwlock_files);
3  if(f_node== NULL)
4  {
5 
6  f_node = kmalloc(sizeof(struct session_file_node), GFP_KERNEL);
7 
8  atomic_set(&f_node->counter,1);
9  strcpy(f_node->key, tmp_key);
10  rb_insert_file(&root_files, f_node, &tree_rwlock_files);
11  }
12  else
13  {
14  atomic_inc(&f_node->counter);
15 
16  }
1 fops_replacement = kmalloc(sizeof(struct file_operations), GFP_KERNEL);
2 
3  *fops_replacement = (struct file_operations) *original_filp->f_op;
4  fops_replacement->write = session_write;
5  fops_replacement->read = session_read;
6  fops_replacement->llseek = session_llseek;
7  fops_replacement->release = session_release;
8  replace_fops(original_filp, fops_replacement);

And finally it increments the global counter and the counter of the sessions opened by the process

CLOSE_SESSION

The close_session is responsible of closing the file and of copying the content of the session into the original file. All is done thanks to the function flush that after some operations makes its job:

1 down_write(priv_data->rw_sem);
2 vfs_truncate(&filp->f_path, 0);
3 err = vfs_copy_file_range(session_file, 0, filp, 0, file_size, 0);
4 up_write(priv_data->rw_sem);

after that it restores the default file operations

1 replace_fops(filp, inodep->i_fop);

The flush returns an EPIPE error if the given file is removed from the filesystem, checking at the variable i_nlink

1 if(inodep->i_nlink==0)
2 {
3  send_sig(SIGPIPE, current, 1);
4  up_write(priv_data->rw_sem);
5  printk(KERN_ERR "Sigpipe is sent");
6  return -EPIPE;
7 }

Testcase and Benchmark

Correctness

The whole developement had a test driven approach, all tests are contained into the tests/unit_tests folder, they were performed throught the check, a unit test framework. In particular the file test.c contains the main functions tested during the development, looking the code it's easy to understand that the tests were about:

1 Suite *s;
2  TCase *tc_core;
3 
4  s = suite_create("Core functionalities Tests");
5  tc_core = tcase_create("Core");
6 
7  tcase_add_test(tc_core, test_session_init);
8  tcase_add_test(tc_core, test_file_op);
9  tcase_add_test(tc_core, test_errors);
10  tcase_add_test(tc_core, test_sysfs);
11  tcase_add_test(tc_core, test_fork_processes);
12  tcase_add_test_raise_signal(tc_core, test_signal, SIGPIPE);
13  suite_add_tcase(s, tc_core);
14  return s;

Performance

The file perf_test compiled into the make with the -pg option tries many times a write operation with and without sessions:

1 if(argc<3)
2  printf("Usage: perf_test.c file1 file2");
3 int i;
4 
5 for(i=0; i<TRIES;i++)
6  write_with_session(argv[1]);
7 for(i=0; i<TRIES;i++)
8  write_without_session(argv[2]);
9 return 0;

The tests are performed initially with empty file and they shows no significant drop of performances with a relative low number of try. When tries are increased with the O_APPEND mode (more than 10k) the write with session takes more than the 99% of the time. The analysis is made with gproof with the following command into the terminal after the execution:

1 gprof perf_test gmon.out > little_file_analysis

so the file little_file_analysis contains the report of the previous test.

the second test is performed with a medium file (2MB instead of 100byte) and 1k tries and with 10k tries. We can see that the drop of performace always belongs to the vfs_copy_file_range call into the kernel as we expected. All analysis are provided into the test folder.

Note:

All the tests were performed on a LUbuntu 16 distro with kernel version: 4.15.0.