Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

Build a Kernel Module for CSE330: Writing and Reading from proc Filesystem

Learn how to implement a Linux kernel module that interacts with the proc filesystem for Project 3. Step-by-step guide with code examples, testing tips, and bonus challenges.

CSE330 project 3 proc filesystem kernel module Linux kernel module tutorial proc write read example kernel module lseek CSE330 assignment help operating systems project proc filesystem write beyond limit read from tail kernel module Linux kernel programming CSE330 bonus challenge proc_filesys.c TODO kernel module testing student project guide systems programming tutorial Linux /proc example

Introduction to CSE330 Project 3: proc Filesystem Kernel Module

In CSE330 Project 3, you are tasked with creating a kernel module that interacts with the proc filesystem (/proc). This assignment is a foundational exercise in operating systems, teaching you how to implement file operations like write, read from head, tail, and middle, and handle error checking. By the end, you'll have a functional kernel module that can store and retrieve data, similar to how many system utilities expose information via /proc.

This tutorial walks you through the TODO parts of proc_filesys.c, explains the logic behind each function, and provides testing strategies. Whether you're a CSE330 student looking for project help or a Linux kernel enthusiast, these step-by-step instructions will help you succeed.

Understanding the Assignment Structure

The provided code skeleton contains a kernel module that creates a /proc entry (e.g., /proc/your_module). Your job is to implement the file operations: proc_write, proc_read, proc_lseek, and cleanup. The test script test.sh will verify your implementation against three test cases:

  • Test 1 (100 pts): write to kernel, read entire content, read from head, read from middle.
  • Test 2 (Bonus 0.5 pt): write beyond size limit should return -EINVAL.
  • Test 3 (Bonus 0.5 pt): read from tail.

The module must handle multiple writes, manage a fixed-size buffer (e.g., 4096 bytes), and support seeking. Think of this as a mini in-memory file – akin to how a gaming leaderboard stores scores, or a chat app logs messages. You control what gets written and read.

Step 1: Setting Up the Buffer and Module Parameters

First, define a buffer and its size. Use a static char array or kmalloc for dynamic allocation. A mutex is essential to protect concurrent access (though single-threaded testing may not enforce it, it's good practice).

#define BUFFER_SIZE 4096
static char *proc_buffer;
static int buffer_len; // current data length
static struct mutex buffer_mutex;

In the __init function, allocate memory and initialize the mutex:

static int __init proc_init(void) {
    proc_buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
    if (!proc_buffer)
        return -ENOMEM;
    mutex_init(&buffer_mutex);
    // create proc entry...
    return 0;
}

Don't forget to free in __exit.

Step 2: Implementing the Write Operation

The proc_write function receives user data and copies it to the kernel buffer. It must check if the new data would exceed BUFFER_SIZE; if so, return -EINVAL (for bonus). Otherwise, append or overwrite? The assignment likely expects overwrite (i.e., each write replaces previous content). Let's assume that.

static ssize_t proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) {
    if (count > BUFFER_SIZE)
        return -EINVAL;
    if (copy_from_user(proc_buffer, ubuf, count))
        return -EFAULT;
    buffer_len = count;
    *ppos = 0; // reset position after write
    return count;
}

This simple version resets the position. For more advanced, you could allow appending, but the test expects a fresh buffer. The size limit check is crucial for the bonus.

Step 3: Implementing the Read Operation

The proc_read function must support reading from the current file position (*ppos). It copies data from buffer to user space, up to count bytes, and advances *ppos. Return the number of bytes read, or 0 if at end.

static ssize_t proc_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos) {
    if (*ppos >= buffer_len)
        return 0;
    size_t available = buffer_len - *ppos;
    size_t to_read = min(count, available);
    if (copy_to_user(ubuf, proc_buffer + *ppos, to_read))
        return -EFAULT;
    *ppos += to_read;
    return to_read;
}

This handles reading from any position, including head (position 0) and middle (any offset). For the bonus read from tail, you need to implement proc_lseek to allow seeking to the end. Alternatively, the test may call lseek before read. We'll cover that next.

Step 4: Implementing lseek for Tail Read

The proc_lseek function is called when user space uses lseek. To support read from tail, you must handle SEEK_END:

static loff_t proc_lseek(struct file *file, loff_t offset, int whence) {
    loff_t new_pos;
    switch (whence) {
        case SEEK_SET:
            new_pos = offset;
            break;
        case SEEK_CUR:
            new_pos = file->f_pos + offset;
            break;
        case SEEK_END:
            new_pos = buffer_len + offset; // offset is negative for tail
            break;
        default:
            return -EINVAL;
    }
    if (new_pos < 0 || new_pos > buffer_len)
        return -EINVAL;
    file->f_pos = new_pos;
    return new_pos;
}

With this, a user can lseek(fd, -10, SEEK_END) to read last 10 bytes. The test likely does exactly that.

Step 5: Registering File Operations

Define a struct file_operations and assign your functions. Then create the proc entry using proc_create.

static const struct proc_ops proc_fops = {
    .proc_write = proc_write,
    .proc_read = proc_read,
    .proc_lseek = proc_lseek,
};

static int __init proc_init(void) {
    // ... allocate buffer, init mutex
    proc_create("my_module", 0666, NULL, &proc_fops);
    return 0;
}

Note: In newer kernels, use proc_ops instead of file_operations. Adjust according to your kernel version.

Testing Your Module

After compiling (make), load the module with sudo insmod proc_filesys.ko. Then run the test script: sudo ./test.sh 1. It will perform writes and reads, checking output. For example, it might write "Hello World" and expect to read it back. If you get segmentation faults or invalid argument errors, check your buffer size handling and copy_from_user.

Debug with dmesg to see kernel prints. Add printk statements to trace execution. For instance:

printk(KERN_INFO "proc_write: count=%zu, buffer_len=%d
", count, buffer_len);

This is especially helpful when you're stuck on why write returns -EINVAL.

Common Pitfalls and Tips

  • Buffer overflow: Always check count <= BUFFER_SIZE before copying.
  • User pointer validity: Use copy_from_user and copy_to_user – never directly dereference user pointers.
  • Mutex locking: Protect shared buffer in both read and write to prevent race conditions.
  • Position management: Ensure *ppos is updated correctly. The test may call read multiple times; each call should advance position.
  • Return values: Return exact number of bytes written/read, or negative error code.

Bonus Challenges: Write Beyond Limit and Tail Read

For the write beyond limit bonus, your proc_write must return -EINVAL when count > BUFFER_SIZE. Simple.

For the read from tail bonus, implement proc_lseek correctly. The test may use lseek with SEEK_END and a negative offset. For example, if buffer contains "abcdefghij", tail read of 3 bytes should return "hij".

Conclusion

By completing this project, you've built a real kernel module that interacts with the proc filesystem. This skill is valuable for systems programming, Linux kernel development, and understanding how operating systems manage data. As trends like AI-driven system monitoring and cloud-native infrastructure grow, kernel-level knowledge becomes a superpower. Good luck with your CSE330 project!