How a User System Call Is Connected to the File Operation Method in a Device Driver in Linux

In this article, we go over how a user system call is connected to the file operation function in a device driver in linux.
So a device can represent any physical hardware such as a mouse, keyboard, microphone, speaker, memory card, etc.
Devices work in the user space and the kernel space.
The user space is how the user interacts with the device.
Let's say a user plugs in a memory card into his or her laptop.
And then the user opens up the directory of the memory that has files stored on it.
In this case, the user interacted with the device using the open() function to gain access to this device.
This open() function now must connect to the open() function in the device driver in the kernel space. Only the actual device driver communicates with the hardware itself.
Remember that if a user opens up the directory of a memory card and does something such as insert another file and deletes a file, this must actually happen in the memory card, meaning the device actually added to the memory (in the case of a file add) or deleted from the memory card (in the case of a file delete). This doesn't happen in the user space. These actions only occur due to the functions run on the device driver, which is the code which executes on the hardware itself.
How this occurs is shown in the following diagram below.
So let's go over this diagram.
So users obviously interact with devices and they do this in the user space.
The user usually begins by calling the user system call open() function to gain access to the device program or directory.
After this, the system call is then transferred to the kernel space.
The user level system call has now been transferred to the kernel space .
Why?
Because the kernel space is the only space that eventually deals directly with the hardware.
Again, if you're dealing with a memory card, you have to have software that can directly add a file to the hardware or delete a file from the hardware.
User-level space code cannot do this.
Only the device driver has the code to be able to directly communicate with the hardware level components such as registers to add or delete from these registers. The device driver is the only code that knows initimate levels of the hardware of the device.
When the user level system call reaches the kernel, the call first goes to the VFS (Virtual File System). This VFS opens a file by creating a new file object and linking it to the corresponding inode object.
If you look at the code for the open() function in our device driver code, you see that we have to pass in a pointer to an inode object, as well as a pointer to the file object.
Let's go over more about this inode object and this file object data types.
So it's important to know that Unix makes a clear distinction between the contents of a file and the information about a file.
An inode object is a VFS data structure (struct inode) that holds general information about a file.
Whereas a VFS file data structure (struct file) tracks interaction on an opened file by the user process, the inode object contains all the information needed by the file system to handle a file. Each file has its own inode object, which the filesystem uses to identify the file. And each inode object is associated with an inode number, which uniquely identifies the file within the file system. The VFS identifies a file using the inode number. That is the VFS's way of uniquely identifying a file. This way, if the file's name is renamed later, it doesn't affect the identification of the file. The file is identified by the VFS through the inode number and not by the file name.
The inode object is created and stored in memory when a new file (regular or device file) is created. When you create a file, an inode object is created; that means, an object of type struct inode and is placed in memory.
A file object is created in the kernel space whenever a file is opened. In other words, whenever a file is opened, a file object is created in the kernel space. There will be one file object for every file that is opened.
The file object is of type struct file and stores information about the interaction between an open file and a process.
This information about the file object exists only in kernel memory during the period when a process has the file opened. The contents of file object is not written back to disks unlike inode (which is in permanent memory at least until the file is deleted). Whenever the file is closed, the file object will be destroyed.
An example to understand this would be the following analogy.
Let's start you're working with a word processor and you create a file named Budget.doc and save it to the computer.
Then you open up the document again but in a separate window. And you do this 2 more times.
There would be 4 instances of the same document open on your computer .
At any time, you could modify either of these 4 instances of the same document. This would represent 4 open file structure objects.
However, these are all the same files, Budget.doc
Instead of a unique name, though, the virtual file system uniquely categories the file by an unique number, the inode number.
Though there are 4 instances, in this case, open on the computer, they are all the same file, thus, having the same inode number, signifying they are the same document.
This analogy helps to explain an inode object vs a file object.
A unique document can only have one inode object but multiple open file objects.
So let's now go into the exact code to see how this works.
As stated previously, whenever a device file is created (whether through udev or mknod), the function, init_special_inode() gets called.
In this function, the inode->i_fop field is initialized with the default file operations (def_chr_fops).
In this function, also the inode object's i_rdev is initialized (i_rdev is the device number).
This is shown below.
You can see at the beginning of the function, the line, inode->i_fop= &def_chr_fops;
This assigns the inode to a default character file operations. You'll see exactly what this is below. But for now, know that this sets the file operations of this inode to a dummy default settings until our own file operations are input into the inode file operations. In the dummy character file operations, there are only 2 file operations present, including the open() function and the llseek() function. Later these file operations will be initialized to our own.
Below this line, the next line is, inode->i_rdev= rdev;
The VFS assigns the device number to this function. The device number is now assigned to the newly created device file.
If you look at the function, it also takes in as a parameter the mode. Later in the code, you can see if statements, where the code is checking to see whether the object is a character device, a block device, etc. If it is a character device, it gets set to default character file operations. If it is a block device, it gets set to default block file operations.
Now let's take a look at this def_chr_fops function, which is found in the /linux/fs/ directory.
This is shown below.
You can see in the comments above the function that this is is a dummy default file operation.
Later, we will see how we initialize the file operations to our own function instead of a dummy function.
So, as a recap, when a device file gets created, we create the device file using udev. Then the inode object gets created in memory and the inode's i_rdev field is initialized with the device number through the init_special_inode() function. Lastly, the inode object's i_fop field is set to the dummy default file operations through the def_chr_fops() function.
The above deals with the creation of a device file, with the subsequent inode object creation and dummy default operations.
Now we deal with what goes on in the user process, when the user makes a user system level call.
This is specifically for the open() function.
The first thing is the user invokes the open system call on the device file.
Secondly, a file objects gets created.
Thirdly, the inode's i_fop gets copied to the file object's f_op (dummy default file operations of char devie file)
Fourthly, the open function of the dummy default file operations gets called (chrdev_open).
Fifthly, the inode object's i_cdev field is initialized with cdev, which was added during the cdev_add
Sixthly, the real file operations of your device driver (inode->cdev->fops) gets copied to file->f_op
Lastly, the file->f_op->open method gets called, which is the read open method of your driver.
In order to do all of these tasks, there a number of methods used.
These methods are shown below. Realize, though, many more are run because methods are called within methods.
So from the open user system level call, the do_sys_open() method is run, which pretty much does error checking to make sure the user system level call is performed successfully. This method is found in the open.c file in the /linux/fs/ directory.
Next, in the do_filp_open() method, a file object is created. This method is found in the namei.c file in the /linux/fs/ directory.
Next, in the do_dentry_open() method, default fops are set. This method is found in the open.c file in the /linux/fs/ directory.
Next, in the chrdev_open() method, a default dummy file operation is run, which then leads to our driver open method. This method is found in the char_dev.c file in the /linux/fs/ directory.
Next, our driver open method is run.
You don't really need to know what is going on specifically in these files, because they've already been created in the source code when you download the linux kernel.
The only thing you really have to do yourself is create the device file and create the file operation method.
We show how to do these tasks in separate articles.
So this is how a user system call is connected to the file operation method in a device driver in linux.
Related Resources