Linux Device Driver Tutorial Part 6 – Cdev structure and File Operations

This article is a continuation of the  Series on Linux Device Driver and carries on the discussion on character drivers and their implementation. This is Part 6 of the Linux device driver tutorial. In this tutorial, we will discuss Cdev structure and File Operations of Character drivers.

Based on our previous tutorial, we know about the major, minor number, and device file. So as I said in the previous tutorial, we need to open, read, write, and close the device file. So let’s start…

Cdev structure and File Operations of Character drivers

If we want to open, read, write, and close we need to register some structures to the driver.

cdev structure

In Linux kernel struct inode structure is used to represent files. Therefore, it is different from the file structure that represents an open file descriptor. There can be numerous file structures representing multiple open descriptors on a single file, but they all point to a single inode structure.

The inode structure contains a great deal of information about the file. As a general rule, cdev structure is useful for writing driver code:

 struct cdev is one of the elements of the inode structure. As you probably may know already, an inode structure is used by the kernel internally to represent files. The struct cdev is the kernel’s internal structure that represents char devices.  This field contains a pointer to that structure when the inode refers to a char device file.

This is cdev structure. Here we need to fill the two fields,

  1. file_operation (This we will see after this cdev structure)
  2. owner (This should be THIS_MODULE)

There are two ways of allocating and initializing one of these structures.

  1. Runtime Allocation
  2. Own allocation

If you wish to obtain a standalone cdev structure at runtime, you may do so with code such as:

struct cdev *my_cdev = cdev_alloc( );
my_cdev->ops = &my_fops;

Or else you can embed the cdev structure within a device-specific structure of your own by using below function.

void cdev_init(struct cdev *cdev, struct file_operations *fops);

Once the cdev structure is set up with file_operations and owner, the final step is to tell the kernel about it with a call to:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

Where,

dev is the cdev structure,

num is the first device number to which this device responds, and

count is the number of device numbers that should be associated with the device. Often count is one, but there are situations where it makes sense to have more than one device number correspond to a specific device.

If this function returns negative error code, your device has not been added to the system. So check the return value of this function.

After a call to cdev_add(), your device is immediately alive. All functions you defined (through the file_operations structure) can be called.

To remove a char device from the system, call:

void cdev_del(struct cdev *dev);

Clearly, you should not access the cdev structure after passing it to cdev_del.

File_Operations

The file_operations structure is how a char driver sets up this connection. The structure, defined in <linux/fs.h>, is a collection of function pointers. Each open file is associated with its own set of functions. The operations are mostly in charge of implementing the system calls and are, therefore, named openread, and so on.

file_operations structure or a pointer to one is called fops. Each field in the structure must point to the function in the driver that implements a specific operation or be left NULL for unsupported operations. The whole structure is mentioned below snippet.

This file_operations structure contains many fields. But we will concentrate on very basic functions. Below we will see some fields explanation.

struct module *owner:

The first file_operations field is not an operation at all; it is a pointer to the module that “owns” the structure. This field is used to prevent the module from being unloaded while its operations are in use. Almost all the time, it is simply initialized to THIS_MODULE, a macro defined in <linux/module.h>.

read

ssize_t (*read) (struct file *, char _ _user *, size_t, loff_t *);

This is used to retrieve data from the device. A null pointer in this position causes the read system call to fail with -EINVAL (“Invalid argument”). A non-negative return value represents the number of bytes successfully read (the return value is a “signed size” type, usually the native integer type for the target platform).

write

ssize_t (*write) (struct file *, const char _ _user *, size_t, loff_t *);

It is used to sends the data to the device. If NULL -EINVAL is returned to the program calling the write system call. The return value, if non-negative, represents the number of bytes successfully written.

ioctl

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

The ioctl system call offers a way to issue device-specific commands (such as formatting a track of a floppy disk, which is neither reading nor writing). Additionally, a few ioctl commands are recognized by the kernel without referring to the fops table. If the device doesn’t provide an ioctl method, the system call returns an error for any request that isn’t predefined (-ENOTTY, “No such ioctl for device”). You can find the IOCTL tutorial here.

Open

int (*open) (struct inode *, struct file *);

Though this is always the first operation performed on the device file, the driver is not required to declare a corresponding method. If this entry is NULL, opening the device always succeeds, but your driver isn’t notified.

release (close)

int (*release) (struct inode *, struct file *);

This operation is invoked when the file structure is being released. Like openrelease can be NULL.

Example

If you want to understand the complete flow just have a look at our dummy driver.

Dummy Driver

Here I have added a dummy driver snippet. In this driver code, we can do all open, read, write, close operations. Just go through the code.

[Get the source code of this example from the GitHub]

Testing the Device Driver

  1. Build the driver by using Makefile (sudo make)
  2. Load the driver using sudo insmod
  3. Do echo 1 > /dev/etx_device

Echo will open the driver and write 1 into the driver and finally close the driver. So if I do echo to our driver, it should call the open, write and release functions. Just check.

4. Now Check using dmesg

5. Do cat > /dev/etx_device

Cat command will open the driver, read the driver, and close the driver. So if I do cat to our driver, it should call the open, read, and release functions. Just check.

6. Now Check using dmesg

7. Unload the driver using sudo rmmod

Instead of doing echo and cat command in the terminal you can also use open(), read(), write(), close() system calls from user-space applications.

I hope you understood this tutorial. This is just a dummy driver tutorial. In our next tutorial, we will see some real-time applications using file operations device drivers. 🙂

5 4 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Anonymous
Anonymous
August 21, 2017 1:37 AM

Articles are simple and precise to refresh the concepts

Anonymous
Anonymous
April 6, 2018 3:37 AM

This tutorial is perfect for people who want to start learning kernel programming

srishti verma
srishti verma
October 18, 2018 4:48 AM

After giving the command dmesg. I could see the message “module inserted” but I am not getting the message when driver got open, read and released. I had given the command echo 1 /dev/etx_device before dmesg.

EmbeTronicx India
EmbeTronicx India
Reply to  srishti verma
October 22, 2018 2:43 AM

Hi Srishti,

Have you changed the source code?

Thanks.

ganesh sastry
ganesh sastry
April 1, 2019 10:19 PM

how to create a vitual device drive to copy the files from one vm to other vm

viyyapu Durga Prasad
July 16, 2019 1:04 PM

Hi EmbetroniX,
Your tutorials are very precise and easy to understand. I tried the dummy driver. When I write to the device,it is writing to driver but when check logs they are not printed. please help me on this

Thanks

Martin Pålsson
Martin Pålsson
August 26, 2019 1:24 PM

For everyone that has trouble with permission to write, please read the following post on stackoverflow:

https://stackoverflow.com/questions/34522426/unable-to-write-on-dev-files

TLDR:
sudo echo 1 > /dev/etx_device does not do what you think it would.
rather, get the root prompty by
sudo su
and echo to file
echo 1 > /dev/etx_device

Allt the best
Martin

AtlaskD
AtlaskD
November 24, 2019 12:00 PM

Hi EmbetronicX,
why did you use static keyword for most of the functions and variables except for etx_driver_exit() and dev?
Thanks

EmbeTronicX
EmbeTronicX
Reply to  AtlaskD
November 24, 2019 12:51 PM

Hi AtlaskD,

Its your wish to use static keyword before any variable and functions. But u should know how static keyword works.

Static keyword limit the visibility to that file. So you cannot access that function or variable from outside of the file (i.e) you cannot access it from other files.

Here my intention to write a driver with multiple files. So all the files will have same init and exit function. Other function’s scope is limited to only this file. I have to remove that static keyword from init function.

Hope this would answered your question.

Divin
Divin
June 3, 2020 11:52 AM

Hi,
Perfect attempt!
 
Could you please explain what are the use cases and advantages of using runtime allocation and own allocation?

10
0
Would love your thoughts, please comment.x
()
x
%d bloggers like this: