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.
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_SIZEbefore copying. - User pointer validity: Use
copy_from_userandcopy_to_user– never directly dereference user pointers. - Mutex locking: Protect shared buffer in both read and write to prevent race conditions.
- Position management: Ensure
*pposis 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!