...making Linux just a little more fun!
For the purpose of this article, let's consider a device to be a virtual represention, within Linux, of hardware that one would like to drive by using a piece of software. In the Linux world, devices are implemented in the form of modules. By using modules, we can provide device functionality that can be accessed from userspace.
A userspace entry point to a device is provided by a file node in the /dev directory. As we know, most of the things in Linux world are represented in the form of files. We can do [ls -l] on any device file, which will report the device type - character or block device, as well as its major number and minor number.
The type of device indicates the way data is written to a device. For a character device, it's done serially, byte by byte, and for a block device (e.g., hard disk) in the form of chunks of bytes - just as the name suggests.
The major number is assigned at the time of registering the device (using some module) and the kernel uses it to differentiate between various devices. The minor number is used by the device driver programmer to access different functions in the same device.
Looking at the number of files in the /dev directory, one might think that a very large number of devices are up and running in the system, but only few might be actually present and running. This can be seen by executing [cat /proc/devices]. (One can then see the major numbers and names of devices that are passed at the time of registering.)
First of all, we should know the basics of generating a module object file. The module uses kernel space functions and since the whole kernel code is written inside the __KERNEL__ directive we need to define it at time of compiling, or in our source code. We need to define the MODULE directive before anything else because Module functions are defined inside it. In order to link our module with the kernel, the version of the running kernel should match the version which the module is compiled with, or [insmod] will reject the request. This means that we must include the [include] directory present in the Linux source code of the appropriate version. Again, if my module file is called my_dev.c, a sample compiler instruction could be [gcc -D__KERNEL__ -I/usr/src/linux.2.6.7/linux/include -c my_dev.c]. A -D is used to define any directive symbol. Here we need to define __KERNEL__, since without this kernel-specific content won't be available to us.
The two basic functions for module operations are module_init and module_exit. The insmod utility loads the module and calls the function passed to module_init, and rmmod removes the module and calls function passed to module_exit. So inside module_init, we can do whatever we wish using our kernel API. For registering the char device, the kernel provides register_chrdev which takes three arguments, namely: the major number, the char string (which gives a tag name to the device), and the file operations struct address which defines all the stuff we would like to do with our char device. struct file_operations is defined in $(KERNELDIR)/linux/include/fs.h which declares the function pointers for basic operations like open, read, write, release, etc. One needs to implement whatever functions are necessary for the device. Finally, inside the function passed to module_exit, we should free the resources using unregister_chrdev which will be called when we do rmmod.
Below is the code listing where the device is nothing but an 80 byte chunk of memory.
Load the device using [insmod my_dev.o]. Look for the entry through /proc/modules and /proc/devices. Create a file node in /dev directory using [mknod /dev/my_device c 222 0]. Look inside the code, we have given the major number as 222. You might think that this number may clash with some other device - well, that's correct, but I have checked whether this number is already occupied by some other device. One could use dynamic allocation of the major number; for that we have to pass 0 as the argument.
Now we can read the data in the device using [cat /dev/my_device] and can write to our device using [echo "something" > /dev/my_device]. We can also write full-fledged userspace code to access our device using standard system calls of open, read, write, close, etc. Sample code is presented below.
------------------------------------------- /* Sample code to access our char device */ #include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> int main() { int fd=0,ret=0; char buff[80]=""; fd=open("/dev/my_device",O_RDONLY); printf("fd :%d\n",fd); ret=read(fd,buff,10); buff[ret]='\0'; printf("buff: %s ;length: %d bytes\n",buff,ret); close(fd); } ------------------------------------------- Output fd: 3 buff: hi from kernel ;length: 14 bytes -------------------------------------------
In this article I have tried to show how to use the kernel functions to register a character device, and how to invoke it from userspace. There are many issues that have not been touched upon here, such as the concurrency problem where we need to provide a semaphore for the device to do mutual exclusion as more than one process may try to access it. I will try to cover these issues in my future articles.
Talkback: Discuss this article with The Answer Gang
I am from New Delhi, India and am a great Linux fan and love the way
Linux gives freedom to control the hardware gizmos. I am using Linux
since the start of the new millennium but started digging into kernel
sources recently after completing the B-Tech from IIT-Guwahati.
It all began with a desire to create modules to control the peripheral
devices and since then there is no turning back.
I would like to share my experiences and any interesting thing that
comes across me during this Linux journey through Linux Gazette
Articles.