Linux Kernel Development – Creating a Proc file and Interfacing With User Space

On The first post we built a Simple Kernel Module with init and exit functions and covered the basic concepts in kernel programming

Next, We added a Kernel Module Parameters to configure the kernel module data

In this post, We will create the first interface to user space application using procfs (/proc) file

Proc File System

Proc is a pseudo file system for interfacing with the kernel internal data structures. As a user, you can use proc files for system diagnostics  – CPU, memory, Interrupts and many more. You can also configure a lot of parameters like scheduler parameters, kernel objects, memory and more

The common interaction with proc is using cat and echo from the shell. For example:

# cat /proc/cpuinfo
# echo "50"> /proc/sys/kernel/sched_rr_timeslice_ms

Creating a new Proc file

To create a proc file system we need to implement a simple interface – file_operation.

We can implement more than 20 functions but the common operations are read, write. To register the interface use the function proc_create

The basic structure is:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h>   
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#define BUFSIZE  100


MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Liran B.H");


static struct proc_dir_entry *ent;

static ssize_t mywrite(struct file *file, const char __user *ubuf,size_t count, loff_t *ppos) 
{
	printk( KERN_DEBUG "write handler\n");
	return -1;
}

static ssize_t myread(struct file *file, char __user *ubuf,size_t count, loff_t *ppos) 
{
	printk( KERN_DEBUG "read handler\n");
	return 0;
}

static struct file_operations myops = 
{
	.owner = THIS_MODULE,
	.read = myread,
	.write = mywrite,
};

static int simple_init(void)
{
	ent=proc_create("mydev",0660,NULL,&myops);
	return 0;
}

static void simple_cleanup(void)
{
	proc_remove(ent);
}

module_init(simple_init);
module_exit(simple_cleanup);

If you build and insert the module, you will see a new file /proc/mydev , You can test the read and write operations using cat and echo (only see the kernel log messages)

# echo "test" > /proc/mydev
bash: echo: write error: Operation not permitted

# cat /proc/mydev
# dmesg | tail -2
[  694.640306] write handler
[  714.661465] read handler

 

Implementing The Read Handler

The read handler receives 4 parameters:

  • File Object – per process structure with the opened file details (permission , position, etc.)
  • User space buffer
  • Buffer size
  • Requested position (in and out parameter)

To implement the read callback we need to:

  • Check the requested position
  • Fill the user buffer with a data (max size <= Buffer size) from the requested position
  • Return the number of bytes we filled.

For example, the user run the following code:

int fd = open("/proc/mydev", O_RDWR);

len = read(fd,buf,100);
len = read(fd,buf,50);

On the first call to read we get the user buffer, size = 100, position = 0 , we need to fill the buffer with up to 100 bytes from position 0, update the position and return the number of bytes we wrote. If we filled the buffer with 100 bytes and returned 100 the next call to read we get the user buffer, size=50 and position=100

Suppose we have 2 module parameters and we want to return their values on proc read handler we write the following:

static int irq=20;
module_param(irq,int,0660);

static int mode=1;
module_param(mode,int,0660);


static ssize_t myread(struct file *file, char __user *ubuf,size_t count, loff_t *ppos) 
{
	char buf[BUFSIZE];
	int len=0;
	printk( KERN_DEBUG "read handler\n");
	if(*ppos > 0 || count < BUFSIZE)
		return 0;
	len += sprintf(buf,"irq = %d\n",irq);
	len += sprintf(buf + len,"mode = %d\n",mode);
	
	if(copy_to_user(ubuf,buf,len))
		return -EFAULT;
	*ppos = len;
	return len;
}

This is a simple implementation, We check if this is the first time we call read (pos=0) and the user buffer size is bigger than BUFSIZE , otherwise we return 0 (end of file)

Then, we build the returned buffer, copy it to the user , update the position and return the number we wrote

Build and insert the module, you can test it with cat command:

# sudo insmod ./simproc.ko irq=32 mode=4
# cat /proc/mydev 
irq = 32
mode = 4

Exchanging Data With User-Space

In the kernel code, you can’t just use memcpy between an address supplied by user-space and the address of a buffer in kernel-space:

  • Correspond to completely different address spaces (thanks to virtual memory).
  • The user-space address may be swapped out to disk.
  • The user-space address may be invalid (user space process trying to access unauthorized data).

You must use dedicated functions in your read and write file operations code:

include <asm/uaccess.h>
unsigned long copy_to_user(void __user *to,const void *from, unsigned long n);
unsigned long copy_from_user(void *to,const void __user *from,unsigned long n);

 

Implementing the Write Handler

The write handler is similar to the read handler. The only difference is that the user buffer type  is a const char pointer. We need to copy the data from the user buffer to the requested position and return the number of bytes copied

In this example we want to set both values using a simple command:

# echo "32 6" > /proc/mydev

The first value is the irq number and the second is the mode.

The code for the write handler:

static ssize_t mywrite(struct file *file, const char __user *ubuf,size_t count, loff_t *ppos) 
{
	int num,c,i,m;
	char buf[BUFSIZE];
	if(*ppos > 0 || count > BUFSIZE)
		return -EFAULT;
	if(copy_from_user(buf,ubuf,count))
		return -EFAULT;
	num = sscanf(buf,"%d %d",&i,&m);
	if(num != 2)
		return -EFAULT;
	irq = i; 
	mode = m;
	c = strlen(buf);
	*ppos = c;
	return c;
}

Again, we check if this is the first time we call write (position 0) , then we use copy_from_user to memcpy the data from the user address space to the kernel address space. We extract the values, check for errors , update the position and return the number of bytes we received

The complete module code:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h>   
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#define BUFSIZE  100


MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Liran B.H");

static int irq=20;
module_param(irq,int,0660);

static int mode=1;
module_param(mode,int,0660);

static struct proc_dir_entry *ent;

static ssize_t mywrite(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) 
{
	int num,c,i,m;
	char buf[BUFSIZE];
	if(*ppos > 0 || count > BUFSIZE)
		return -EFAULT;
	if(copy_from_user(buf, ubuf, count))
		return -EFAULT;
	num = sscanf(buf,"%d %d",&i,&m);
	if(num != 2)
		return -EFAULT;
	irq = i; 
	mode = m;
	c = strlen(buf);
	*ppos = c;
	return c;
}

static ssize_t myread(struct file *file, char __user *ubuf,size_t count, loff_t *ppos) 
{
	char buf[BUFSIZE];
	int len=0;
	if(*ppos > 0 || count < BUFSIZE)
		return 0;
	len += sprintf(buf,"irq = %d\n",irq);
	len += sprintf(buf + len,"mode = %d\n",mode);
	
	if(copy_to_user(ubuf,buf,len))
		return -EFAULT;
	*ppos = len;
	return len;
}

static struct file_operations myops = 
{
	.owner = THIS_MODULE,
	.read = myread,
	.write = mywrite,
};

static int simple_init(void)
{
	ent=proc_create("mydev",0660,NULL,&myops);
	printk(KERN_ALERT "hello...\n");
	return 0;
}

static void simple_cleanup(void)
{
	proc_remove(ent);
	printk(KERN_WARNING "bye ...\n");
}

module_init(simple_init);
module_exit(simple_cleanup);

Note : to implement more complex proc entries , use the seq_file wrapper

User Space Application

You can open the file and use read/write functions to test the module. Don’t forget to move the position bask to 0 after each operation:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void main(void)
{
	char buf[100];
	int fd = open("/proc/mydev", O_RDWR);
	read(fd, buf, 100);
	puts(buf);

	lseek(fd, 0 , SEEK_SET);
	write(fd, "33 4", 5);
	
	lseek(fd, 0 , SEEK_SET);
	read(fd, buf, 100);
	puts(buf);
}	

 

You can find the full source code here

 

 

Tagged ,

12 thoughts on “Linux Kernel Development – Creating a Proc file and Interfacing With User Space

  1. Malonu skaityti!

  2. please add a tutorial for INTERRUPT handling in linux device drivers also

  3. thanks a lot

  4. Fantastic!

    I had to replace line 6, from:
    #include

    To:
    #include

    1. from:
      asm/uaccess.h

      TO:
      liunx/uaccess.h

  5. I implemented a pretty similar module like this one (as I am practicing on one of the code projects in OS System Concepts book).
    But I got a problem with proc_write() function when I echo into /proc file, like this:

    echo “1234” > /proc/pid
    bash: /proc/pid: cannot overwrite existing file

    I also check the kernel log but only proc_read() was logged
    Please help me out of this!!!!

  6. Thank you for the clear and simple example for demonstrating the usage of the proc file system. The example works if it is tested with the included user mode application.I tried the example with Ubuntu 20.

    I got no output testing the driver with the console command sequence “cat /proc/mydev”. After some debugging, the count parameter value is 131702 when reading with the “cat” command. 131702 is greater than the 100 defined for the BUFSIZE causing the read function to return -EFAULT. I changed the logic to make sure there is enough space to hold the output for reading in ‘buf’ and only copy from ‘buf’ to ‘ubuf’ for no more than the count size. Both reading with the application and “cat” command delivers the same result with the new logic.

  7. Nice post, Thanks!

  8. Looks like this code is obsolete in Ubuntu 20.04 LTS, the create_proc() function has changed

    ./include/linux/proc_fs.h:108:24: note: expected ‘const struct proc_ops *’ but argument is of type ‘struct file_operations *’
    108 | struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops);

    1. Yes, Same error here

    2. yes, the interface has changed, this is the problem of writing kernel code , change the type to proc_ops and the callback name to proc_read

      1. cheers Liran that worked, Ubuntu not helpful in that apt-get source linux downloads the wrong version of the kernel, so all example code does not compile, doh!

Comments are closed.