Character driver file operation methods
In the previous article, we explored about the cdev_init() and cdev_add().We actually registered our character device using cdev, and the cdev structure is initialized with a file operation variable.
The file operation variable a what we created is not yet initialized, so will initialize that later when we define our drivers file operation methods. And that what we are going to explore now, character driver file operation methods.
Character Driver System Calls
In this character driver, we will provide support for user-level system calls, including open, close, read, write, and llseek.
In the previous article, we created a variable of this struct type, struct file operations. This structure you can find in include/linux/fs.h.
This structure is a collection of various functions pointers. And all these are possible file operation methods for a regular file or for a device file.
In this article, we’ll focus on implementing the lseek, read, write, open, and release methods in your driver and initializing these member elements using the struct file_operations
variable defined in your driver file.
Let’s start with the open method. Suppose you’ve loaded your “pcd” driver (short for “pseudo character device driver”) and created a device file, say /dev/pcd
.
When a user process uses the open system call to open this device file, the control is transferred to the kernel’s Virtual File System (VFS).
When an open system call is used on this device file, what happens?
You know the system is transferred to the kernel. The kernel will process that system call. Now, that happens in the VFS. That is virtual file system.
From user level, the control is first passed to the kernels VFS subsystem. And from the VFS subsystem, the control is passed to the appropriate drivers open method. let’s call it as pcd_open. This is a open method for the open system call, which is invoked on this device file.
When the VFS receives open request on a device file or on a regular file, what VFS does is it opens a file by creating a new file object and linking it to the corresponding inode object. I’ll talk more on inode object and file object little later, but for a time being, you just have to remember this.
The open system call execution is control is first passed to the VFS, and the VFS finds the appropriate driver, and it calls the open method of the driver. When it calls the open method, it passes two arguments. The first argument is pointer of Inode associated with file name, the device file name. And the second argument what it passes is a pointer of file object.
Let’s explore more about a struct inode and struct file.
During file operation of a regular file or a device file, there are various VFS data structures involved. The important data structures involved are struct inode, struct cdev, struct file operations, and struct file. You may be a familiar with this, struct cdev? which represents a character device, and we registered this struct cdev using cdev_add. While registering the cdev, we initialize the cdev’s file operation with our drivers file operation methods.
Basically, cdev points to Struct file_ops. Cdev has a field, which holds the pointer to this a file operation variable what we created in the driver file. Apart from these two, these two are another two important data structures a which are involved during file access, whether it could be a normal file or a device file.
Let’s explore about this inode object and this file object.
Unix makes a clear distinction between the contents of a file and the information about the file. So, an inode is a VFS data structure; the data structure is struct inode. That holds general information about a file.
If you want to know exactly what are the general information about the file, you should explore the member elements of the inode. So, whereas VFS ‘file’ data structure (that is struct file) tracks interaction on an opened file by the user processes.
The struct file data structure is used to track interaction on an opened file. Where as struct inode holds some general information about a file like file name, the inode number, and other details.
Inode contains all the information needed by the file system to handle file. Each file has its own inode object, which the file system uses to identify the file. Each inode object is associated with an inode number, which uniquely identifies the file within the file system.
Basically, the VFS identifies a file using an inode number, so not using its’s filename. You could doesn’t matter whether you use the same file today or you may rename the file tomorrow, doesn’t matter what happens to the file, but unless it is deleted, the VFS always identifies a file using its inode number.
The inode object is created and stored in memory as when a new file gets created, a file could be regular file or a device file. That means whenever you create a file, an inode object is created. That means the object of type struct inode and it will be placed in the memory.
What is a file object?
Whenever a file is opened, a file object is created in the kernel space. There will be one file object for every open of a regular or device file.
A file object is of type struct file; this is a kernel space a data structure, just like inode. But for every open of a device file or a regular file, a file object will be created. That means, if you open the same file, let’s say ten times, then there will be ten file objects will be created in the memory. There will be one file object for every open of a regular or device file. So, the file object stores information about the interaction between an open file and a user process.
This information exist only in kernel memory during the period when a process has the file open. The contents of file object is not written back to disk unlike inode. File objects contents will not be written back to the disk. It will get created whenever you open a file, and VFS may modify various member elements of this file object, but none of those changes will be written back to the disk.
Whenever you close a file, the file object will be destroyed by the VFS.
But that is not the case with inode. So, each file has its own inode object, which will be there in memory. And if there is any change in the nodes member elements, then that will be written back to the Disk.
You know what happens when you create a device file. This will give some clear picture how our system call execution gets connected to our driver method. Let’s say a device file is created, either manually or by using udev. I’ll explain the udev device file creation later. Whenever a device file is created, this function gets called init_special_inode. This is called by the VFS. And this function you can find in fs/inode.c.
Here, you see the virtual file system passes the device number, So the device number of the newly created device file.
Look at this code here. And I said, whenever you create a device file inode object will be created. That inode object’s address will be passed here. And the VFS also sends what kind of device file it is. Whether it is a character device file, or a block device file, or a FIFO, or something like that. It mentions the mode in this argument.
This code decodes the mode.
if (S_ISCHR(mode)) {
inode ->i_fop = &def_chr_fops;
Inode ->i_rdev = rdev;
Here, it is checking for ISCHR. So, is it a character device file. If true, look at this code. The inodes i_rdev, that’s a device number. Where ‘i’ stands for inode, is initialized to the rdev. That is the device number of the newly created device file, which is passed here (dev_t rdev). That means, now inode is formed, and inodes device number field is initialized with the device number of the newly created device file.
And after that, inode has one more field called i_fop. i_fop is a pointer variable of type struct file operations. And this is initialized to default character file operations. All these things happen when you create a device file.
What is this def_chr_fops?
This is defined in fs/char_dev.c, as shown in Figure 6. This is actually a dummy default file operations, as shown in Figure 6.
So, the only thing this does is contain the open that then fills in the correct operations depending on the special file. Basically, this is a dummy file operations, which has only open and lseek method. This is a dummy open method chrdev_open.
So, we’ll see later how this is being used in the VFS. These two main activities happen when you create a device file. VFS calls this function, and it initializes newly created inodes object with the default character file operations, and it initializes the device number field of the newly created inode object with the device number of the newly created device file.
And the inode object will be there in the memory. And later, when you call the open system call, so all these activities happen. So, in the previous image, you understood that whenever a device file is created, an inode object is created in the memory, and inode objects, a device number is initialized, and inode objects file operation field is also initialized with dummy default file operation methods.
Inode object is there in the memory now. Whenever the user-level program executes open system call on a device file, these are the kernel level functions a get involved in processing the open system call. It first executes a do_sys_open kernel function in the kernel space, and that calls do_filp_open function.
do_filp_open is a place where a file object will be created. For every open, a file object will be created in the kernel space. That file object creation happens in this kernel function do_filp_open. Because the ‘file’ object controls and open file.
And after that, do_filp_open calls, do_dentry_open. In this function, what happens is the default dummy file operations will be called.
What is the default dummy file operation?
chrdev_open. So, chrdev_open will be called. And from chrdev_open, your driver’s open method will be called.
These are the activities involved in a open system call processing in the kernel space. If you want more details a regarding this, you can refer to understanding linux kernel third edition, page number 524. This is a very nice book, so you should refer to understand internals of Linux kernel.
Basically, what happens is whenever a file object is created in do_filp_open, the default fops will be called in the do_dentry_open.
What’s the default fops?
That is chrdev_open. This one will get called. And from here, your driver’s open method will get called.
Let’s check the source code of do_dentry_open shown in Figure 8.
So, this function you can find in fs/open.c . struct file *f is a newly created file object pointer. And take a look here what happens.
The struct file also has file operation variable, a pointer variable. That gets initialized here. That means, inode’s file operation field is copied into ‘file’ objects file operation field.
And after that, you can see that, at this point, open method of dummy default file operation is called. That means chrdev_open gets called. Here, let’s explore this function. I would like to directly take you to the complete source code of this function. You can go to fs/char_dev.c. Here just search for chrdev_open.
Here, you see what happens.
First, the cdev field is checked. Initially, it will be null, actually. Because this function initializes i_rdev and i_fop. It doesn’t initialize I_cdev. Initially, this field will be null. That is checked here(if (!p)). If it is null, then below things will get executed.
Look at this line (kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx).
So, there is Lookup for cdev. That is the cdev what you added using the cdev_add().
Take a look here, how does that lookup works?
That lookup works by using the device number. So, while you used cdev_add function, you added the cdev to the VFS by using the device Number. And now, here, by using that device number, the cdev will be retrieved. That is the cdev object what you added is retrieved.
Basically, this line doesn’t extract the sudo object directly, so instead, it actually extracts or lookup for another kernel object called kobj. By using this object here, it extracts the cdev object.
Basically, it uses a container_of function. I will talk more on container_of little later. Because we use a container_of in our driver source code as well. The container_of macro gives you a pointer to parent object. Cdev is the parent of kobj, actually. By using the child object, the container of extracts pointer of the parent object. So, the parent object here is cdev, it’s like that. So, we will understand more on that later, and it initializes this(new) local variable. This is a cdev pointer(struct cdev *new = NULL).
And look at this line inode->i_cdev = p = new ; That newly extracted cdev object is initialized to inodes i_cdev field. That’s how inodes cdev field get’s initialized. That means, this newly extracted cdev object is nothing but what you added using cdev_add().
After that, you can see that here; take a look into this code now. So, there is a fops variable, which is of type struct file operations. fop pointer gets initialized by using cdev’s, this is cdev. ‘p’ is cdev. Here it is. Cdev’s file operations. This is nothing but your drivers file operation pointer.
Who initialized this?
This is initialized by cdev_init() remember that. That is now copied into this local variable, and there is a replace of file operations. So, the file pointer had the dummy default file operation. That is now replaced by the actual file operations of your driver.
And after that, here you go ret=filp->f_op->open(inode,filp); File object, file operation, open. Now this calls your driver’s open method. That’s how the open system get’s connected to your driver open method. Just spend some time with chrdev_open function. So, we will understand.
If you are confused now, let me summarize. So, now there are two parts. One is when the device file is gets created, and another part is when the user process executes the open system call. We have to understand this in two different scenarios.
The first scenario is when a device file gets created.
How device file gets created?
Either from udev or from manually by running some device file creation commands.
First, create device file using udev, let’s say. So, when the device file gets created, inode object gets created in memory, and the inode’s i_rdev field is initialized with device number, and inode objects file operation field is set to dummy default file operations. That is def_chr _fops. That’s a dummy one. Inode object is in the memory.
And when the user process executes open system call. So, the user invokes an open system call on the device file.
Remember that, whenever open is executed, the kernel first creates a file object for that. File object gets created. And inode’s file operation gets copy to file object file operation. That is dummy default file operation of char device file.
After that, open function of dummy default file operations gets called, that is chrdev_open. In chrdev_open, inode object’s i_cdev field is initialized with cdev which you added during cdev_add (lookup happens using inode-> i_rdev field) then, inode->cdev_fops (this is a real file operations of your driver) gets a copied to file object file operation Field.
And after that, finally, from file object-> file operation filed-> the open method gets called. That is a real open method of your driver.
So, this is a pictorial representation (as shown in Figure 10) of a how the flow goes actually. There will be an in memory Inode object, and in memory Inode object has a pointer field, that is i_cdev, and this will be pointing to your cdev object what you added using cdev_add. And this cdev object has f_ops field. This file operation field is pointing to your drivers file operation. That file operation field is copied into file objects file operation(f_op) field. And this is a file object which controls an opened file.
From here, the open, read, write, close all system calls will get connected to your driver’s methods. When you use the open system call on the device file, it returns.
What it returns?
It returns the file descriptor. That file descriptor is actually a reference to the opened file object. So, reference to this file object is returned as file descriptor. And the subsequent read, write, and close system calls must use this a fd as a reference to deal with the assigned file object in the kernel space.
Get the Full Course on Linux Device Driver Here
FastBit Embedded Brain Academy Courses
Click here: https://fastbitlab.com/course1