Linux is a variant of the UNIX operating system. The principle and idea of writing a driver in Linux is similar to that of other
Unix systems, but the drivers in the DoS or window environment are very different. In a Linux environment, the driver is designed with simple ideas, convenient operations, and powerful functions. However, it only supports a small number of functions and can only rely on functions in the kernel. Some common operations should be compiled by yourself, debugging is not convenient. I have developed a driver for a multi-media card developed by the lab over the past few weeks and gained some experience. I hope to share it with Linux fans. please correct me if there are any improper problems.
The following text mainly comes from KHg, johnsonm write Linux Device Driver, Brennan's Guide to inline assembly, the Linux A-Z, and some information about device driver on Tsinghua BBS. some of these materials are outdated and some are incorrect. I have corrected them based on my own test results.
I. Concepts of Linux Device Driver
A system call is an interface between the operating system kernel and applications, and a device driver is an interface between the operating system kernel and machine hardware. the device driver shields the application from hardware details. In this way, the hardware device is only a device file, and the application can operate the hardware device like a common file. the device driver is part of the kernel and implements the following functions:
1. initialize and release the device.
2. Transmit data from the kernel to the hardware and read data from the hardware.
3. Read the data that the application sends to the device file and send back the data requested by the application.
4. Detect and handle device errors.
In Linux, there are two main types of device files: character devices and Block devices. the main difference between a character device and a block device is that when a read/write request is sent to a character device, the actual hardware I/O usually happens immediately after it is sent, it uses a piece of system memory as a buffer. When a user's process can meet the user's requirements for device requests, it will return the requested data. If not, you can call the request function to perform actual I/O operations. block devices are designed for disks and other slow devices to avoid excessive CPU time consumption.
As mentioned, the user process deals with the actual hardware through the device file. Each device file has its File Attribute (C/B), which indicates that the character device is still very weak? In addition, each file has two device numbers. The first is the master device number, which identifies the driver, and the second is the slave device number, which identifies different hardware devices that use the same device driver, for example, if there are two floppy disks, you can use the device number to distinguish them. the master device ID of the device file must be the same as the master device ID applied by the device driver during registration. Otherwise, the user process will not be able to access the driver.
It must be mentioned that when a user's process calls the driver, the system enters the core State and is no longer preemptive scheduling. that is to say, the system must perform other work only after the sub-functions of your driver are returned. if your driver is in an endless loop, unfortunately you only have to restart the machine, and then there is a long fsck. // hehe
When reading/writing, it first looks at the buffer content, if the buffer data
How to compile a device driver in a Linux operating system
Ii. instance analysis
Let's write a simple character device driver. Although it does not do anything, it can be used to understand the operating principle of the Linux Device Driver. input the following C code into the machine and you will get a real device driver. however, my kernel is 2.0.34, which may cause problems in earlier versions. I have not tested it yet. // Xixi
# DEFINE _ no_version __
# Include <Linux/modules. h>
# Include <Linux/version. h>
Char kernel_version [] = uts_release;
This section defines some version information. Although it is not very useful, it is also necessary. johnsonm said that all drivers must start with <Linux/config. h>, but I don't think so.
Because a user process deals with hardware through a device file, the operation on the device file is similar to some system calls, such as open, read, write, close ...., note: It's not fopen or fread. But how can we associate system calls with drivers? This requires an understanding of a very critical data structure:
Struct file_operations {
INT (* seek) (struct inode *, struct file *, off_t, INT );
INT (* read) (struct inode *, struct file *, Char, INT );
INT (* write) (struct inode *, struct file *, off_t, INT );
INT (* readdir) (struct inode *, struct file *, struct dirent *, INT );
INT (* select) (struct inode *, struct file *, Int, select_table *);
INT (* IOCTL) (struct inode *, struct file *, unsined int, unsigned long );
INT (* MMAP) (struct inode *, struct file *, struct vm_area_struct *);
INT (* open) (struct inode *, struct file *);
INT (* release) (struct inode *, struct file *);
INT (* fsync) (struct inode *, struct file *);
INT (* fasync) (struct inode *, struct file *, INT );
INT (* check_media_change) (struct inode *, struct file *);
INT (* revalidate) (dev_t Dev );
}
The name of each member in this structure corresponds to a system call. when a user process uses a system call to perform read/write operations on device files, the system calls the system to find the corresponding device driver through the master device number of the device file, then read the corresponding function pointer of the data structure, and then give the control to the function. this is the basic principle of Linux device drivers. in this case, the main task of writing a device driver is to write a sub-function and fill in the fields of file_operations.
It's quite simple, isn't it?
The following describes the subprogram.
# Include <Linux/types. h>
# Include <Linux/fs. h>
# Include <Linux/mm. h>
# Include <Linux/errno. h>
# Include <ASM/segment. h>
Unsigned int test_major = 0;
Static int read_test (struct inode * node, struct file * file,
Char * Buf, int count)
{
Int left;
If (verify_area (verify_write, Buf, count) =-efault)
Return-efault;
For (Left = count; left> 0; left --)
{
_ Put_user (1, Buf, 1 );
Buf ++;
}
Return count;
}
This function is prepared for the read call. when read is called, read_test () is called, which writes all the user's buffers. 1.buf is a parameter called by read. it is an address of the user's process space. however, when read_test is called, The system enters the core state. therefore, you cannot use the Buf address. You must use _ put_user (). This is a function provided by kernel to transmit data to users. there are also many functions with similar functions. see. before copying data to a user space, you must verify whether the Buf is available.
The verify_area function is used.
Static int write_tibet (struct inode * inode, struct file * file,
Const char * Buf, int count)
{
Return count;
}
Static int open_tibet (struct inode * inode, struct file * file)
{
Mod_inc_use_count;
Return 0;
}
Static void release_tibet (struct inode * inode, struct file * file)
{
Mod_dec_use_count;
}
These functions are all empty operations. When the actual call happens, nothing is done. They only provide function pointers for the following structure.
Struct file_operations test_fops = {
Null,
Read_test,
Write_test,
Null,/* test_readdir */
Null,
Null,/* test_ioctl */
Null,/* test_mmap */
Open_test,
Release_test, null,/* test_fsync */
Null,/* test_fasync */
/* Nothing more, fill with nulls */
};
The main body of the device driver can be said to be written. Now we need to embed the driver into the kernel. The driver can be compiled in two ways. One is to compile into the kernel, and the other is to compile into the module. If compiled into the kernel, it will increase the size of the kernel and change the source file of the kernel, it cannot be dynamically uninstalled, which is not conducive to debugging. Therefore, we recommend that you use the module method.
Int init_module (void)
{
Int result;
Result = register_chrdev (0, "test", & test_fops );
If (result <0 ){
Printk (kern_info "test: Can't Get Major number/N ");
Return result;
}
If (test_major = 0) test_major = result;/* dynamic */
Return 0;
}
The init_module function is called when compiled modules are transferred to the memory using the insmod command. Here, init_module only registers a character device to the system's character device table. Register_chrdev requires three parameters. The first parameter is the device number to be obtained. If it is zero, the system selects an unused device number and returns it. The second parameter is the device file name. The third parameter is used to register the pointer of the function in which the driver actually performs the operation.
If the registration is successful, the system returns the device's master device number. If the registration fails, a negative value is returned.
Void cleanup_module (void)
{
Unregister_chrdev (test_major, "test ");
}
When you use rmmod to uninstall a module, the cleanup_module function is called, which releases the table items that the character device test occupies in the system character device table.
A simple character device can be written. The file name is test. C.
Compile the following code
$ Gcc-O2-dmodule-d1_kernel _-C test. c
The file test. O is a device driver.
If the device driver has multiple files, compile each file according to the command line above, and then
LD-r file1.o file2.o-O modulename.
The driver has been compiled. Now install it in the system.
$ Insmod-F test. o
If the installation is successful, you can see the device test in the/proc/devices file and its master device number.
Run
$ Rmmod Test
Next, create a device file.
Mknod/dev/test C major minor
C Indicates a character device. Major indicates the master device number, which is displayed in/proc/devices.
Use shell commands
$ CAT/proc/devices | awk "}"
You can get the master device number and add the above command line to your shell script.
Minor is set from the device number to 0.
We can now access our driver through the device file. Write a small test program.
# Include <stdio. h>
# Include <sys/types. h>
# Include <sys/STAT. h>
# Include <fcntl. h>
Main ()
{
Int testdev;
Int I;
Char Buf [10];
Testdev = open ("/dev/test", o_rdwr );
If (testdev =-1)
{
Printf ("cann' t open file/N ");
Exit (0 );
}
Read (testdev, Buf, 10 );
For (I = 0; I <10; I ++)
Printf ("% d/N", Buf [I]);
Close (testdev );
}
Compile and run the program to see if all 1 is printed?
The above is just a simple demonstration. Real and practical drivers need to be complicated, such as interrupt, DMA, and I/O port. These are the real difficulties. Please refer to the following section for more information.
How to compile a device driver in a Linux operating system
Iii. Specific problems in Device Drivers
1. I/O port. (For more information: Linux Device Driver Security Port allocation)
I/O port is indispensable for dealing with hardware. The old ISA Device often occupies the actual I/O port. in Linux, the operating system does not block the I/O port. That is to say, any driver can operate on any I/O port, which can easily cause confusion. Each driver should avoid misuse of ports by itself.
There are two important kernel functions to ensure that the driver can do this.
1) check_region (INT io_port, int off_set)
This function checks the system's I/O table to see if any other driver occupies an I/O port.
Parameter 1: The base address of the IO port,
Parameter 2: The range occupied by the I/O port.
Returned value: 0 is not in use. If it is not 0, it is already in use.
2) request_region (INT io_port, int off_set, char * devname)
If this I/O port is not occupied, you can use it in our driver. Before use, you must register with the system to prevent being occupied by other programs. After registration, you can see the registered IO port in the/proc/ioports file.
Parameter 1: The base address of the IO port.
Parameter 2: The range occupied by the I/O port.
Parameter 3: The device name that uses this Io address.
After registering the I/O port, you can access it with letters such as INB () and outb () with confidence.
In some PCI devices, I/O ports are mapped to a memory segment. to access these ports is equivalent to accessing a memory segment. Frequently, we need to obtain the physical address of a piece of memory. In a DOS environment (the reason is not to mention the DOS Operating System is because I think DOS is not an operating system at all. It is too simple and too insecure) as long as the segment is used: offset. In window95, 95ddk provides a vmm call _ maplineartophys to convert a linear address to a physical address. But in Linux, how does one do it?
2. Memory operations
Dynamically open the memory in the device driver, instead of using malloc, but kmalloc, or directly apply for a page using get_free_pages. Kfree or free_pages are used to release the memory. Note that the physical address is returned by functions such as kmalloc! Malloc and other returned linear addresses! I am not quite clear about the physical address returned by kmalloc: Since the conversion from a linear address to a physical address is completed by the compaction CPU hardware, the operand of the Assembly command should be a linear address, the driver cannot directly use a physical address but a linear address. But in fact, kmalloc does return a physical address and can directly access the actual Ram through it. I think this can be explained in two ways: Disable paging in the core state, but this seems unrealistic. The other is that the Linux page Directory and page table items are designed to make the physical address equivalent to the linear address. I don't know what I think, right? Please advise me.
To put it bluntly, note that kmalloc can only be opened up to-16, and 16 bytes are occupied by the page descriptor structure. For more information about kmalloc usage, see KHg.
Memory-mapped I/O Ports, registers, or RAM (such as memory) of hardware devices generally occupy address space above f0000000. Cannot be accessed directly in the driver. You need to use the kernel function vremap to obtain the address after re ing.
In addition, many hardware require a large continuous memory for DMA transmission. This memory must remain in the memory and cannot be exchanged to files. However, kmalloc can only open up to KB of memory.
This can be solved by sacrificing some system memory methods.
The specific method is as follows: for example, your machine uses 32 MB of memory in Lilo. in the conf startup parameter, the mem = 30 m is added. In this way, Linux considers that your machine has only 30 m of memory, and the remaining 2 M has vremap, which can be used for DMA.
Remember that the memory mapped with vremap is released when unremap is not used. Otherwise, page tables are wasted.
3. interrupt handling
Like handling I/O ports, to use an interrupt, you must first register with the system.
Int request_irq (unsigned int IRQ,
Void (* handle) (INT, void *, struct pt_regs *),
Unsigned int long flags,
Const char * Device );
IRQ: the request is interrupted.
Handle: the pointer to the interrupt processing function.
Flags: a fast interrupt to the sa_interrupt request, with 0 normal interruptions.
Device: device name.
If the registration succeeds, 0 is returned. In this case, you can view the request interruption in the/proc/interrupts file.
4. Some common problems.
Sometimes timing is important for hardware operations. However, if you use the C language to write some low-level hardware operations, GCC will usually optimize your program, so that the timing is wrong. If you write it in a sink, GCC will also optimize the Assembly Code unless you modify it with the volatile keyword. The safest way is to disable optimization. Of course, this can only be a part of your own code. If you do not optimize all the code, you will find that the driver cannot be loaded at all. This is because some GCC extension features are used to compile the driver, which must be reflected only after the optimization options are added.
Previous Article: Linux kernel-level backdoor principles and simple practices documentation: Linux Device Driver Programming
Next article: How to Implement C language for obtaining system time in Linux