
I want to admit at once that I kind of deceived you, because the driver, according to
Wikipedia, is
a computer program with which another program (usually the operating system) gets access to the hardware of a device . And today we will create some kind of a blank for the driver, since in fact, we will not work with any hardware. You can add this useful functionality yourself if you wish.
What we are creating today will be more correct to call
LKM (Linux Kernel Module or the kernel load module). It is worth saying that the driver is one of the varieties of LKM.
')
We will write the module under the kernel 2.6. LKM for 2.6 is different from 2.4. I will not dwell on the differences, because it does not fall within the framework of the post.
We will create a character device / dev / test, which will be processed by our module. I want to make a reservation right away that placing a character device is not necessarily in the / dev directory, it’s just part of an “ancient magic ritual.”
A bit of theory
In short, LKM is an object that contains code to extend the capabilities of an already running Linux kernel. Those. it works in kernel space, not user space. So you should not experiment on a working server. In case of an error that has crept into the module, get a kernel panic. We assume that I warned you.
A kernel module must have at least 2 functions: an initialization function and an exit function. The first is called when the module is loaded into the kernel space, and the second, respectively, when it is unloaded. These functions are set using macros:
module_init and
module_exit .
It is worth saying a few words about the
printk () function. The main purpose of this function is to implement the mechanism for registering events and warnings. In other words, this function is for recording some information in the kernel log.
Because the driver works in the kernel space, it is delimited from the user's address space. And we would like to be able to return a certain result. To do this, use the
put_user () function. It is exactly what is involved in transferring data from kernel space to user space.
I also want to say a few words about character devices.
Run the
ls -l /dev/sda*
. You will see something like:
brw-rw---- 1 root disk 8, 0 2010-10-11 10:23 /dev/sda
brw-rw---- 1 root disk 8, 1 2010-10-11 10:23 /dev/sda1
brw-rw---- 1 root disk 8, 2 2010-10-11 10:23 /dev/sda2
brw-rw---- 1 root disk 8, 5 2010-10-11 10:23 /dev/sda5
Between the word "disk" and the date there are two numbers separated by a comma. The first number is called the highest number of the device. The leading number indicates which driver is used to service this device. Each driver has its own unique major number.
Device files are created using
mknod commands , for example:
mknod /dev/test c 12
. With this command we will create the device / dev / test and specify the major number for it (12).
I will not go deep into theory because Anyone interested - he can read about it in more detail. I will give a link at the end.
Before you start
You need to know a few "magic" commands:
- insmod - add a module to the kernel
- rmmod - remove, respectively
- lsmod - list current modules
- modinfo - display information about the module
To compile the module, we need the headers of the current kernel.
In debian / ubutnu, you can easily put them like this (for example, for 2.6.26-2-686):
apt-get install linux-headers-2.6.26-2-686
Or build the package for your current kernel yourself:
fakeroot make-kpkg kernel_headers
Source code
#include <linux/kernel.h> /* printk() .. */
#include <linux/module.h> /* , */
#include <linux/init.h> /* */
#include <linux/fs.h>
#include <asm/uaccess.h> /* put_user */
// , Modinfo
MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "Alex Petrov <petroff.alex@gmail.com>" );
MODULE_DESCRIPTION( "My nice module" );
MODULE_SUPPORTED_DEVICE( "test" ); /* /dev/testdevice */
#define SUCCESS 0
#define DEVICE_NAME "test" /* */
//
static int device_open( struct inode *, struct file * );
static int device_release( struct inode *, struct file * );
static ssize_t device_read( struct file *, char *, size_t, loff_t * );
static ssize_t device_write( struct file *, const char *, size_t, loff_t * );
// , static, .
static int major_number; /* */
static int is_device_open = 0; /* ? */
static char text[ 5 ] = "test\n" ; /* , */
static char * text_ptr = text; /* */
//
static struct file_operations fops =
{
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
// . . main()
static int __init test_init( void )
{
printk( KERN_ALERT "TEST driver loaded!\n" );
//
major_number = register_chrdev( 0, DEVICE_NAME, &fops );
if ( major_number < 0 )
{
printk( "Registering the character device failed with %d\n" , major_number );
return major_number;
}
//
printk( "Test module is loaded!\n" );
printk( "Please, create a dev file with 'mknod /dev/test c %d 0'.\n" , major_number );
return SUCCESS;
}
//
static void __exit test_exit( void )
{
//
unregister_chrdev( major_number, DEVICE_NAME );
printk( KERN_ALERT "Test module is unloaded!\n" );
}
//
module_init( test_init );
module_exit( test_exit );
static int device_open( struct inode *inode, struct file *file )
{
text_ptr = text;
if ( is_device_open )
return -EBUSY;
is_device_open++;
return SUCCESS;
}
static int device_release( struct inode *inode, struct file *file )
{
is_device_open--;
return SUCCESS;
}
static ssize_t
device_write( struct file *filp, const char *buff, size_t len, loff_t * off )
{
printk( "Sorry, this operation isn't supported.\n" );
return -EINVAL;
}
static ssize_t device_read( struct file *filp, /* include/linux/fs.h */
char *buffer, /* buffer */
size_t length, /* buffer length */
loff_t * offset )
{
int byte_read = 0;
if ( *text_ptr == 0 )
return 0;
while ( length && *text_ptr )
{
put_user( *( text_ptr++ ), buffer++ );
length--;
byte_read++;
}
return byte_read;
}
* This source code was highlighted with Source Code Highlighter .
Module assembly
Well, now we can write a small
Makefile :
obj-m += test.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
And check its performance:
root @ joker: / tmp / test # makemake -C /lib/modules/2.6.26-2-openvz-amd64/build M=/tmp/test modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.26-2-openvz-amd64'
CC [M] /tmp/1/test.o
Building modules, stage 2.
MODPOST 1 modules
CC /tmp/test/test.mod.o
LD [M] /tmp/test/test.ko
make[1]: Leaving directory `/usr/src/linux-headers-2.6.26-2-openvz-amd64'
Let's see what we did:
root @ joker: / tmp / test # ls -ladrwxr-xr-x 3 root root 4096 21 12:32 .
drwxrwxrwt 12 root root 4096 21 12:33 ..
-rw-r--r-- 1 root root 219 21 12:30 demo.sh
-rw-r--r-- 1 root root 161 21 12:30 Makefile
-rw-r--r-- 1 root root 22 21 12:32 modules.order
-rw-r--r-- 1 root root 0 21 12:32 Module.symvers
-rw-r--r-- 1 root root 2940 21 12:30 test.c
-rw-r--r-- 1 root root 10364 21 12:32 test.ko
-rw-r--r-- 1 root root 104 21 12:32 .test.ko.cmd
-rw-r--r-- 1 root root 717 21 12:32 test.mod.c
-rw-r--r-- 1 root root 6832 21 12:32 test.mod.o
-rw-r--r-- 1 root root 12867 21 12:32 .test.mod.o.cmd
-rw-r--r-- 1 root root 4424 21 12:32 test.o
-rw-r--r-- 1 root root 14361 21 12:32 .test.o.cmd
drwxr-xr-x 2 root root 4096 21 12:32 .tmp_versions
Now let's take a look at the information about the newly compiled module:
root @ joker: / tmp / test # modinfo test.kofilename: test.ko
description: My nice module
author: Alex Petrov <druid@joker.botik.ru>
license: GPL
depends:
vermagic: 2.6.26-2-openvz-amd64 SMP mod_unload modversions
Finally, install the module in the kernel:
root @ joker: / tmp / test # insmod test.koLet's see if our module is listed:
root @ joker: / tmp / test # lsmod | grep testtest 6920 0
And what fell into the logs:
root @ joker: / tmp / test # dmesg | tail[829528.598922] Test module is loaded!
[829528.598926] Please, create a dev file with 'mknod /dev/test c 249 0'.
Our module tells us what to do.
Follow his advice:
root @ joker: / tmp / test # mknod / dev / test c 249 0Finally, let us check if our module is working:
root @ joker: / tmp / test # cat / dev / testtest
Our module does not support receiving data from the user:
root @ joker: / tmp / test # echo 1> / dev / testbash: echo: :
Let's see what the module says to our actions:
root @ joker: / tmp / test # dmesg | tail[829528.598922] Test module is loaded!
[829528.598926] Please, create a dev file with 'mknod /dev/test c 249 0'.
[829747.462715] Sorry, this operation isn't supported.
Delete it:
root @ joker: / tmp / test # rmmod testAnd let's see what he says to us at parting:
root @ joker: / tmp / test # dmesg | tail[829528.598922] Test module is loaded!
[829528.598926] Please, create a dev file with 'mknod /dev/test c 249 0'.
[829747.462715] Sorry, this operation isn't supported.
[829893.681197] Test module is unloaded!
Delete the device file so that it does not confuse us:
root @ joker: / tmp / test # rm / dev / testConclusion
Further development of this “blank” depends only on you. You can turn it into a real driver that will provide the interface to your device, or use it to further explore the Linux kernel.
I just had a completely crazy idea to make sudo through a device file. Those. send the command to / dev / test and it is executed as root.
Literature
And in the end I will give a link to the spell book
LKMPG (Linux Kernel Module Programming Guide)UPD:Some may not have a module compiled via the Makefile described above.
Decision:
Create a Makefile with only one line:
obj-m + = test.oAnd run the assembly like this:
make -C / usr / src / linux-headers-`uname -r` SUBDIRS = $ PWD modulesUPD2:Corrected the error in the source.
The parser is buggy and keeps 'MODULE_DEscriptION ("My nice module");'. Naturally in the module_description all letters are capitalized.
UPD3:segoon sent several amendments to the post:
1) In the device_open () function is the race condition:
static int device_open (struct inode * inode, struct file * file)
{
text_ptr = text;
if (is_device_open) <<<<
return -EBUSY;
is_device_open ++; <<<<
return SUCCESS;
}
If one process increases is_device_open during execution by another
command process between if (is_device_open) and is_device_open ++, then
As a result, the file will open 2 times. For
atomic actions you need to use
function from the atomic_XXX () series.
Atomic operations need to be used in all places of work with data. In this case, and in close ().
2) device_write () could not write at all, because handler for
By default, write () itself returns an error.
3) put_user () MUST verify the result. If not zero, then
need either
a) return the -EFAULT result and pretend that there was nothing (i.e.
do not completely remove the read data from internal buffers in this
case, the data is constant and nothing needs to be changed)
b) return the number of ALREADY written byte (this is called partial read,
allowed by POSIX). At the same time, care must be taken not to return 0:
read () = 0 means that the file has come to an end, but it is not.
4) In the kernel, 0 is used as the successful exit code, not
Defined constant SUCCESS. There are exceptions, for example, in
a network packet handler, but where either -EXXX is returned (the code
errors), or 0 (everything is fine), it is the constant 0 that is used.
Many more functions can be replaced by more suitable counterparts, but this
would complicate the understanding of the article by beginners :)