This article is a continuation of the Series on Linux Device Drivers and carries the discussion on character drivers and their implementation. The aim of this series is to provide easy and practical examples that anyone can understand. This is the simple Linux Device Driver Programming Tutorial – Linux Device Driver Tutorial Part 7.
From our previous tutorials, we know about major, and minor numbers, device files, and file operations of device drivers using the dummy drivers. But today we are going to write a real driver without hardware.
Table of Contents
Prerequisites
To continue with this tutorial, you must have set up the Ubuntu or Raspberry Pi or Beaglebone. If you aren’t set up anything yet, we suggest you to set up the boards that you have using the below-given tutorials.
You can find a video explanation of this tutorial here. You can also find all the Linux device driver’s video playlists here.
Linux Device Driver Programming Tutorial
Introduction
We already know that in Linux everything is a File. So in this tutorial, we are going to develop two applications.
- User Space application (User program)
- Kernel Space program (Driver)
The user Program will communicate with the kernel space program using the device file. Lets Start.
Kernel Space Program (Device Driver)
We already know about major, and minor numbers, device files, and file operations of the device drivers. If you don’t know please visit our previous tutorials. Now we are going to discuss more file operations in the device driver. Basically, there are four functions in the device driver.
- Open driver
- Write Driver
- Read Driver
- Close Driver
Now we will see one by one of these functions. Before that, I will explain the concept of this driver.
Concept
Using this driver we can send strings or data to the kernel device driver using the write function. It will store that string in the kernel space. Then when I read the device file, it will send the data which is written by write by function to the userspace application.
Functions used in this driver
- kmalloc()
- kfree()
- copy_from_user()
- copy_to_user()
kmalloc()
We will see the memory allocation methods available in the kernel, in our next tutorials. But now we will use only the kmalloc in this tutorial.
kmalloc function is used to allocate the memory in kernel space. This is like a malloc() function in userspace. The function is fast (unless it blocks) and doesn’t clear the memory it obtains. The allocated region still holds its previous content. The allocated region is also contiguous in physical memory.
#include <linux/slab.h> void *kmalloc(size_t size, gfp_t flags);
Arguments
size– how many bytes of memory are required.
flags– the type of memory to allocate.
The flags argument may be one of:
GFP_USER – Allocate memory on behalf of the user. May sleep.
GFP_KERNEL – Allocate normal kernel ram. May sleep.
GFP_ATOMIC – Allocation will not sleep. May use emergency pools. For example, use this inside interrupt handler.
GFP_HIGHUSER – Allocate pages from high memory.
GFP_NOIO – Do not do any I/O at all while trying to get memory.
GFP_NOFS – Do not make any fs calls while trying to get memory.
GFP_NOWAIT – Allocation will not sleep.
__GFP_THISNODE – Allocate node-local memory only.
GFP_DMA – Allocation is suitable for DMA. Should only be used for kmalloc caches. Otherwise, use a slab created with SLAB_DMA.
Also, it is possible to set different flags by OR’ing in one or more of the following additional flags:
__GFP_COLD – Request cache-cold pages instead of trying to return cache-warm pages.
__GFP_HIGH – This allocation has high priority and may use emergency pools.
__GFP_NOFAIL – Indicate that this allocation is in no way allowed to fail (think twice before using).
__GFP_NORETRY – If memory is not immediately available, then give up at once.
__GFP_NOWARN – If allocation fails, don’t issue any warnings.
__GFP_REPEAT – If allocation fails initially, try once more before failing.
There are other flags available as well, but these are not intended for general use, and so are not documented here. For a full list of potential flags, always refer to linux/gfp.h.
kfree()
This is like a free() function in the userspace. This is used to free the previously allocated memory.
void kfree(const void *objp)
Arguments
*objp – pointer returned by kmalloc
copy_from_user()
This function is used to Copy a block of data from user space (Copy data from user space to kernel space).
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
Arguments
to – Destination address, in the kernel space
from – The source address in the user space
n – Number of bytes to copy
Returns number of bytes that could not be copied. On success, this will be zero.
copy_to_user()
This function is used to Copy a block of data into userspace (Copy data from kernel space to user space).
unsigned long copy_to_user(const void __user *to, const void *from, unsigned long n);
Arguments
to – Destination address, in the user space
from – The source address in the kernel space
n – Number of bytes to copy
Returns number of bytes that could not be copied. On success, this will be zero.
Open()
This function is called first, whenever we are opening the device file. In this function, I am going to allocate the memory using kmalloc. In the userspace application, you can use open() system call to open the device file.
static int etx_open(struct inode *inode, struct file *file)
{
/*Creating Physical memory*/
if((kernel_buffer = kmalloc(mem_size , GFP_KERNEL)) == 0){
printk(KERN_INFO "Cannot allocate memory in kernel\n");
return -1;
}
printk(KERN_INFO "Device File Opened...!!!\n");
return 0;
}
write()
When writing the data to the device file it will call this write function. Here I will copy the data from user space to kernel space using copy_from_user() function. In the userspace application, you can use write() system call to write any data the device file.
static ssize_t etx_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
copy_from_user(kernel_buffer, buf, len);
printk(KERN_INFO "Data Write : Done!\n");
return len;
}
read()
When we read the device file it will call this function. In this function, I used copy_to_user(). This function is used to copy the data to the userspace application. In the userspace application, you can use read() system call to read the data from the device file.
static ssize_t etx_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
copy_to_user(buf, kernel_buffer, mem_size);
printk(KERN_INFO "Data Read : Done!\n");
return mem_size;
}
close()
When we close the device file that will call this function. Here I will free the memory that is allocated by kmalloc using kfree(). In the userspace application, you can use close() system call to close the device file.
static int etx_release(struct inode *inode, struct file *file)
{
kfree(kernel_buffer);
printk(KERN_INFO "Device File Closed...!!!\n");
return 0;
}
Linux Device Driver Tutorial Programming – Full Driver Code
[Get the source code from GitHub]
/***************************************************************************//**
* \file driver.c
*
* \details Simple Linux device driver (Real Linux Device Driver)
*
* \author EmbeTronicX
*
* \Tested with Linux raspberrypi 5.10.27-v7l-embetronicx-custom+
*
*******************************************************************************/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include<linux/slab.h> //kmalloc()
#include<linux/uaccess.h> //copy_to/from_user()
#include <linux/err.h>
#define mem_size 1024 //Memory Size
dev_t dev = 0;
static struct class *dev_class;
static struct cdev etx_cdev;
uint8_t *kernel_buffer;
/*
** Function Prototypes
*/
static int __init etx_driver_init(void);
static void __exit etx_driver_exit(void);
static int etx_open(struct inode *inode, struct file *file);
static int etx_release(struct inode *inode, struct file *file);
static ssize_t etx_read(struct file *filp, char __user *buf, size_t len,loff_t * off);
static ssize_t etx_write(struct file *filp, const char *buf, size_t len, loff_t * off);
/*
** File Operations structure
*/
static struct file_operations fops =
{
.owner = THIS_MODULE,
.read = etx_read,
.write = etx_write,
.open = etx_open,
.release = etx_release,
};
/*
** This function will be called when we open the Device file
*/
static int etx_open(struct inode *inode, struct file *file)
{
pr_info("Device File Opened...!!!\n");
return 0;
}
/*
** This function will be called when we close the Device file
*/
static int etx_release(struct inode *inode, struct file *file)
{
pr_info("Device File Closed...!!!\n");
return 0;
}
/*
** This function will be called when we read the Device file
*/
static ssize_t etx_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
//Copy the data from the kernel space to the user-space
if( copy_to_user(buf, kernel_buffer, mem_size) )
{
pr_err("Data Read : Err!\n");
}
pr_info("Data Read : Done!\n");
return mem_size;
}
/*
** This function will be called when we write the Device file
*/
static ssize_t etx_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
//Copy the data to kernel space from the user-space
if( copy_from_user(kernel_buffer, buf, len) )
{
pr_err("Data Write : Err!\n");
}
pr_info("Data Write : Done!\n");
return len;
}
/*
** Module Init function
*/
static int __init etx_driver_init(void)
{
/*Allocating Major number*/
if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){
pr_info("Cannot allocate major number\n");
return -1;
}
pr_info("Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev));
/*Creating cdev structure*/
cdev_init(&etx_cdev,&fops);
/*Adding character device to the system*/
if((cdev_add(&etx_cdev,dev,1)) < 0){
pr_info("Cannot add the device to the system\n");
goto r_class;
}
/*Creating struct class*/
if(IS_ERR(dev_class = class_create(THIS_MODULE,"etx_class"))){
pr_info("Cannot create the struct class\n");
goto r_class;
}
/*Creating device*/
if(IS_ERR(device_create(dev_class,NULL,dev,NULL,"etx_device"))){
pr_info("Cannot create the Device 1\n");
goto r_device;
}
/*Creating Physical memory*/
if((kernel_buffer = kmalloc(mem_size , GFP_KERNEL)) == 0){
pr_info("Cannot allocate memory in kernel\n");
goto r_device;
}
strcpy(kernel_buffer, "Hello_World");
pr_info("Device Driver Insert...Done!!!\n");
return 0;
r_device:
class_destroy(dev_class);
r_class:
unregister_chrdev_region(dev,1);
return -1;
}
/*
** Module exit function
*/
static void __exit etx_driver_exit(void)
{
kfree(kernel_buffer);
device_destroy(dev_class,dev);
class_destroy(dev_class);
cdev_del(&etx_cdev);
unregister_chrdev_region(dev, 1);
pr_info("Device Driver Remove...Done!!!\n");
}
module_init(etx_driver_init);
module_exit(etx_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <embetronicx@gmail.com>");
MODULE_DESCRIPTION("Simple Linux device driver (Real Linux Device Driver)");
MODULE_VERSION("1.4");
Makefile
obj-m += driver.o ifdef ARCH #You can update your Beaglebone path here. KDIR = /home/embetronicx/BBG/tmp/lib/modules/5.10.65/build else KDIR = /lib/modules/$(shell uname -r)/build endif all: make -C $(KDIR) M=$(shell pwd) modules clean: make -C $(KDIR) M=$(shell pwd) clean
Building the Device Driver
- Build the driver by using Makefile (
sudo make) or if you are using the Beaglebone board, then you can usesudo make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-. You can download the Makefile here.
User Space Application
This application will communicate with the device driver. You can download the all codes (driver, Makefile, and application) Here.
/***************************************************************************//**
* \file test_app.c
*
* \details Userspace application to test the Device driver
*
* \author EmbeTronicX
*
* \Tested with Linux raspberrypi 5.10.27-v7l-embetronicx-custom+
*
*******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int8_t write_buf[1024];
int8_t read_buf[1024];
int main()
{
int fd;
char option;
printf("*********************************\n");
printf("*******WWW.EmbeTronicX.com*******\n");
fd = open("/dev/etx_device", O_RDWR);
if(fd < 0) {
printf("Cannot open device file...\n");
return 0;
}
while(1) {
printf("****Please Enter the Option******\n");
printf(" 1. Write \n");
printf(" 2. Read \n");
printf(" 3. Exit \n");
printf("*********************************\n");
scanf(" %c", &option);
printf("Your Option = %c\n", option);
switch(option) {
case '1':
printf("Enter the string to write into driver :");
scanf(" %[^\t\n]s", write_buf);
printf("Data Writing ...");
write(fd, write_buf, strlen(write_buf)+1);
printf("Done!\n");
break;
case '2':
printf("Data Reading ...");
read(fd, read_buf, 1024);
printf("Done!\n\n");
printf("Data = %s\n\n", read_buf);
break;
case '3':
close(fd);
exit(1);
break;
default:
printf("Enter Valid option = %c\n",option);
break;
}
}
close(fd);
}
Compile the User Space Application
Use the below line in the terminal to compile the user space application.
gcc -o test_app test_app.c
If you are using the Beaglebone board, then you can use arm-linux-gnueabihf-gcc -o test_app test_app.c.
Execution (Output)
As of now, we have driver.ko and test_app. Now we will see the output.
- Load the driver using
sudo insmod driver.ko - Run the application (
sudo ./test_app)
********************************* *******WWW.EmbeTronicX.com******* ****Please Enter the Option****** 1. Write 2. Read 3. Exit *********************************
- Select option 1 to write data to the driver and write the string ( In this case I’m going to write “embetronicx” to the driver.
1 Your Option = 1 Enter the string to write into driver :embetronicx Data Writing ...Done! ****Please Enter the Option****** 1. Write 2. Read 3. Exit *********************************
- That “embetronicx” string got passed to the driver. And driver stored that string in the kernel space. That kernel space was allocated by
kmalloc. - Now select option 2 to read the data from the device driver.
2 Your Option = 2 Data Reading ...Done! Data = embetronicx
- See now, we got that string “embetronicx“.
Just see the below image for your clarification.
Note: Instead of using user space application, you can use
echoandcatcommand.
Video Explanation
In our next tutorial, we will discuss IOCTL in the Linux device driver.
Please find the other Linux device driver tutorials here.
You can also read the below tutorials.

Embedded Software | Firmware | Linux Devic Driver | RTOS
Hi, I am a tech blogger and an Embedded Engineer. I am always eager to learn and explore tech-related concepts. And also, I wanted to share my knowledge with everyone in a more straightforward way with easy practical examples. I strongly believe that learning by doing is more powerful than just learning by reading. I love to do experiments. If you want to help or support me on my journey, consider sharing my articles, or Buy me a Coffee! Thank you for reading my blog! Happy learning!
Discover more from EmbeTronicX
Subscribe to get the latest posts sent to your email.


Hello,
Tried read function with cat, it puts me in an infinity loop printing hello message (or other stuff which the buffer contains).
Tried another implementation of read function found on the internet and it works as expected.
static ssize_t etx_read(struct file *f, char __user *buf, size_t len, loff_t *off) { ssize_t bytes_to_read = min(len, (size_t)(mem_size - *off)); if (bytes_to_read <= 0) { pr_info("Data Read: End of file\n"); return 0; // End of file } if (copy_to_user(buf, kernel_buffer + *off, bytes_to_read)) { pr_err("Data Read: Err!\n"); return -EFAULT; } *off += bytes_to_read; pr_info("Data Read: Done!\n"); return bytes_to_read; }Your function work fine with the user space app tho.
Hi, These are good tutorials. I was replicating the code at my side for testing and found that the full driver code is incomplete and is not on lines with the description given in the tutorial itself.
Hi,
Please take the code from github and try.
Hi, I just modified the same at my side and it was working fine. Only problem I observed was in application code as well with ‘scanf’
hi , thanks for tutorial ! ,
i have a question for part Part 7 when i write data from user space to kernel space there is no limit on how much i can send and when i read you send 1024 ? how can we secure this ? would this be vulnerable to buffer overflows?
Hi,
Thanks a lot for the tutorial. I have a doubt in the application program.
scanf(” %[^\t\n]s”, write_buf);
I noticed that when you remove the blankspaces before the %[^\t\n]s, the scanf does not block or wait for the input.
Why is this happening and how to solve this issue without having to add blank spaces?
Hope to hear back from you 🙂
I am trying to run this tutorial on a rpi 3 with raspbian kernel 5.10.60-v7 Kernel. I am able to compile and load the driver module properly. But when I am trying to run the user space test app I am getting the output “Cannot open device file” as I am getting file descriptor as a negative value. I then ran ls /dev/etx* to check if the device file etx_device exists and I am getting
/dev/etx_device as the output of the ls -l /dev/etx* command which implies that the device file exists. Then what could be the issue ? could someone kindly help me with this?
Are you running the application with sudo?
Ex: sudo ./test_app
Very nice tutorial. Simple to understand and gives hands-on with coding too!
Hi Sir , why we using class device create and class_create functions what is the significance of it please let me know
Hello, Have you gone through this tutorial?
Good Explanation sir.Easy understanding of Drivers.
Can you please explain why “If you are going to use echo and cat command, please allocate the kernel space memory in init function instead of open() function”
Hi Mahesh Babu,
Have you see the dmesg when you are doing echo and cat? If you saw that, you will get some clue.
In both the case open call in driver invoked then whats the difference if something else please let me know
Yes you are correct. See our application. We are opening the driver. We are doing our operations. Once our job done, then only we are closing the driver. Until that we don’t close the driver. That’s why we are allocating memory in open call. In that “echo” and “cat” case, each command will open and close immediately. So each command will create a new buffer. To avoid that if we allocate memory at init time, then each command will access the same memory.
First of all, great content… it’s easy to understand and to the point.
Still I have little doubt. What is the problem if each command creates a new buffer ? Anyway, we are calling close in each echo and cat commandthat releases the buffer. Please let me know if my understanding is corrrect
Hi Abhinav,
Yes you are correct. When we use
echoandcatcommand, it will open and close. But our aim is to check that read and write is working or not. How will you check? That’s why we thought of creating one variable and then keep on reading and writing the same variable.When you doecho, it will open, write and finally close. When you docat, it will open, read and finally close. So in this case, if you do echo / cat command, it will create buffer in open, then write/read and finally release that buffer in close. So it will be difficult to understand.The way of explaining the things are awesome.
Hi,
Very good tutorial, that’s clear and easy to learn.
But ok, now I can read/write a kernel space memory (that’s a software side). What’s up with a real hardware (homemade PCIExpress card, custom USB profil, hardware IO,…), how to do the link between kernel space memory and the hardware (hardware side code).
I don’t know if this is more complicated or not.
What do you think?
Hi Monnoliv,
Thanks for your appreciation. Hardware side, That is different story. We need to register the device driver to the subsystem. In our future tutorials we will cover those topics.