< A; main device number and secondary device number
Access to the character device is performed through the device name in the file system. Those device names are simply nodes that are called file system trees, and they are typically located in the/dev directory. Device files for character device drivers can be identified by ' C ' in the first column of the ls-l command output. Block devices are also located in/dev, identified by the character ' B '
CRW-RW----1 root root 253, 0 2013-09-11 20:33 Usbmon0
CRW-RW----1 root root 253, 1 2013-09-11 20:33 Usbmon1
CRW-RW----1 root root 253, 2 2013-09-11 20:33 Usbmon2
BRW-RW----1 root disk 8, 0 2013-09-11 20:33 SDA
BRW-RW----1 root disk 8, 1 2013-09-11 20:34 sda1
BRW-RW----1 root disk 8, 2 2013-09-11 20:33 sda2
The main device number identifies the driver for the device, and the modern Linux kernel allows multiple drivers to share the main device number, but most devices are still organized according to the principle "one master device corresponds to one driver".
The secondary device number is used by the kernel to correctly determine the device that the device file refers to. A direct pointer to the kernel device can be obtained through the secondary device number, or the second device number as an index to the device's local array. In either case, the kernel itself does not care about any additional information about the secondary device number, except that the device is known to point to devices implemented by the driver.
1. Internal expression of equipment designator
In the kernel, the dev_t type (defined in <linux/types.h>) is used to hold the device number----including the main device number and the secondary device number. To obtain the main device number and the secondary device number of the dev_t, use the macro major/minor:major (dev_t dev) defined in <linux/kdev_t.h>. MINOR (dev_t dev); instead, use Mkdev (int major, int MINOR) If you need to convert the master device number and the secondary device number to the dev_t type;
2. Assigning and releasing device numbers
Before creating a character device, the first thing to do is to get one or more device numbers. The necessary functions to complete this work are defined in <linux/fs.h>:
int Register_chrdev_region (dev_t first, unsigned int count, char *name);
A.first is the starting value for the range of device numbers to be assigned, and this device number for first is often set to 0, but is not required for this function.
The B.count is the number of consecutive device numbers that are requested.
C.name is the name of the device associated with the number range, which appears in/proc/devices and Sysfs.
The return value of the d.register_chrdev_region is 0 when the assignment succeeds, and in the case of an error, a negative error code is returned, and the requested number range cannot be used.
If you do not know which main device number the device will use, you will use Alloc_chrdev_region to dynamically assign the device number.
int alloc_chrdev_region (dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
A.dev is an output-only parameter that saves the first number of an allocated range after a successful call.
The B.firstminor should be the requested first secondary device number to be used, which is usually 0.
The C.count and name parameters are the same as the Register_chrdev_region function.
Regardless of the method in which the device number is assigned, the device numbers should be released when they are no longer used, and the release of the device number needs to use the following function void Unregister_chrdev_region (dev_t first, unsigned int count); Usually we call the Nregister_chrdev_region function in the purge function.
Before the user-space program can access the device number, the driver needs to connect the device number and the internal function, which are used to implement the operation of the device.
3. Dynamic Allocation Master device number a part of the main device number has been statically assigned to most common devices. A list of these devices can be found in the Documentation/devicex.txt file of the kernel source tree. For a driver, the handout should not arbitrarily select a device number that is not currently used as the main device number, but should use the dynamic allocation mechanism to obtain the main device number.
The disadvantage of dynamic allocation is that the device node cannot be pre-created because the assigned master device number is not guaranteed to always be consistent. To load a device driver that uses a dynamic master number, the call to Insmod can be replaced by a simple script that reads/proc/devices after calling Insmod to obtain the newly assigned master device number and then creates the corresponding device file. The best way to assign a master device number is to use dynamic allocation by default, while preserving the scope to specify the main device number when loading or even compiling.
< two > Some important data structures
Most of the basic driver operations involve three important kernel data structures, namely file_operations, file, and Inode.
1. File operation
The file_operations structure is used to establish the connection device number and driver action, which is defined in <linux/fs.h>, which contains a set of function pointers. Each open file (referred to as file below) is associated with a set of functions. The operation of the driver is mainly used to implement the system call, named Open, read, etc., you can think of the file as an "object", and manipulate its function "method". File_operations structures or pointers to such structures are called fops. Each field in the structure must point to a function that implements a specific action in the driver, and the corresponding field can be set to a null value for an unsupported operation. For each function, the specific processing behavior of the kernel is different if the corresponding field is assigned a null pointer.
In File_operations, there are many parameters that contain a __user string, which is actually a form of documentation, indicating that the pointer is a user-space address and therefore cannot be directly referenced.
struct module *owner: A pointer to the module that owns the structure. The kernel uses this field to avoid uninstalling the module when its operation is in use. The member is initialized to This_module, which is defined in <linux/module.h>
Int (*open) (struct inode *, struct file *): This is the first action on a device file, but does not require that the driver must declare a phase
The method to be used. If this entry is NULL, the device's open operation is always successful, but the system does not notify the driver.
Int (*release) (struct inode *, struct file *): This operation is invoked when the file structure is disposed. Similar to open, the release can also be set
Null,release is not called when the process calls close every time. As long as the file structure is shared, release will wait until all replicas are closed before they are called. If you need to refresh those pending data when you close any of the replicas, you should flush the method beforehand.
Int (*flush) (struct file *): A call to the flush operation occurs when the process shuts down the device file descriptor copy, and it should perform an operation that has not yet been completed on the device. If flush is set to NULL, the kernel simply ignores the user's program request.
The unsigned int (*poll) (struct file *, struct poll_table_struct *):p Oll method is the back-end implementation of the three system calls of Poll/epoll and select. The poll method should return a bitmask that indicates whether a non-blocking read or write is possible, and also provides the kernel with information to put the calling process into hibernation until I/O becomes possible. If the driver defines the poll method as NULL, the device is considered to be both readable and writable, and will not be blocked.
ssize_t (*read) (struct file *, char __user *, size_t, lofft_t *): Used to read data from the device. When the function pointer is given NULL, it causes the read system call to fail and returns-EINVAL. The function returns a non-negative value indicating the number of bytes successfully read.
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t): Sends data to the device, if there is no such function, write
The system call returns a-einval to the program, which indicates the number of bytes successfully written if the return value is non-negative.
2.file structure
The struct file defined in <linux/fs.h> is the second most important data structure used by a device driver. Note that the file structure is not associated with file in the user-space program. File is defined in the C library and does not appear in the kernel code, and the struct file is a kernel structure that does not appear in the user program.
The file structure represents an open document that is created by the kernel at open and passed to all functions that operate on that file, knowing the last Close function. After all instances of the file have been closed, the kernel releases the data structure. The most important members of the struct file are listed below:
mode_t F_mode: File mode, which identifies whether a file is readable or writable through the fmode_read and fmode_write bits, because the kernel has checked access rights before invoking the driver's read and write, you do not have to check permissions for both methods. In the case where the file is opened without the corresponding access permission, the read and write operation to the file is rejected by the kernel, and the driver does not need to make any additional judgments.
unsigned int f_flags: file flags, such as O_rdonly/o_nonblock/o_sync. In order to check whether a user request is a non-blocking operation, the driver needs to check the O_nonblock flag, while other flags are seldom used. Note that checking read/write permissions should look at F_mode instead of F_flags. All of these flags are defined in <linux/fcntl.h> loff_t F_pos: The current read/write position. Loff_t is a 64-bit number that can be read if the driver needs to know the current location in the file, but do not modify it. Instead of working directly with File->f_pos, Read/write uses the last pointer parameter that they receive to update the position. An exception to this rule is the Llseek method, which is designed to modify the file location itself.
struct file_operations *f_op: file-related operations. The kernel assigns a value to the pointer when it executes the open operation, and then reads the pointer later when it needs to be processed. The value in File->f_op is never saved as a convenient reference, that is, we can modify the associated operation of the file at any time we need it, and after returning to the caller, the new action method takes effect immediately.
void *private_data:open system calls set this pointer to null before invoking the driver's open method. The driver can use this field for any purpose or ignore the field.
3.inode structure
The kernel uses the INODE structure to represent the file internally, so it differs from the file structure, which represents the open descriptor. For a single file, there may be many file structures that represent the open files descriptor, but they all point to a single inode structure. Only two of the fields in the structure are useful for writing a driver:
dev_t I_rdev: The inode structure that represents the device file that contains the true device number
The struct Cdev *i_cdev:struct Cdev is the internal structure of the kernel that represents the character device. When the inode points to a character device file, the field is wrapped to a pointer to the struct CDEV structure. To prevent incompatibility issues with kernel version upgrades, you typically do not use I_rdev directly, but instead use the following macro to obtain the device number:
unsigned int iminor (struct inode *inode); unsigned int imajor (struct inode *inode);
< three > registration of character devices
Before the kernel invokes the operation of the device, one or more struct CDEV structures must be assigned and registered. To do this, you must include <LINUX/CDEV.H>, which defines the structure and the associated auxiliary functions:
There are two ways to assign and initialize a struct CDEV structure:
struct Cdev *my_cdev = Cdev_allo ();
void Cdev_init (struct cdev *cdev, struct file_operations *fops);
My_cdev->ops = &my_fops;
There is also a struct cdev field that needs to be initialized, similar to the file_operations structure, and the struct Cdev has an owner field that should be set to This_module.
After the CDEV structure is set up, the final step is to tell the kernel about the structure through the following call:
int Cdev_add (struct cdev *dev, dev_t num, unsigned int count);
Dev is the CDEV structure, NUM is the first device number that corresponds to the device, and count is the number of device numbers that should be associated with the device, and count often takes 1. When using Cdev_add, it is important to note:
A. This call may fail. If it returns a negative error full, the device is not added to the system.
B. As long as Cdev_add returned, the operation of the device is called by the kernel. Therefore, you cannot call Cdev_add when the driver is not fully prepared to handle the operation on the device.
To remove a character device from the system, make the following call: void Cdev_del (struct cdev *dev); After you pass dev to the Cdev_del function, you should no longer have access to the CDEV structure.
The way to get up early:
The classic way to register character device drivers: int register_chrdev (unsigned int major, const char *name, struct file_operations *fops), or if using Register_ The Chrdev function, the correct function to remove its own device from the system is: int Unregister_chrdev (unsigned int major, const char *name);
< four >open and release
1.open method: The Open method provides the ability for the driver to initialize, thus preparing for subsequent operations to complete the initialization process. In most drivers, open should do the following:
A. Checking for device-specific errors
B. If the device is first opened, initialize it
C. Update the F_OP pointer if necessary
D. Assigning and filling in data structures placed in Filp->private_data
The first thing to do is determine the specific device to open, the Open method prototype is: Int (*open) (struct inode *inode,struct file *filp), where the Inode parameter contains the information we need in its I_cdev field, The CDEV structure that we have previously set. The only problem is that we usually do not need to cdev the structure itself, but want to get the SCULL_DEV structure that contains the CDEV structure. By defining the CONTAINER_OF macro implementation in <linux/kernel.h>: container_of (pointer, Container_type, Container_field); This macro requires a pointer to a Container_field field that contains the structure of the Container_type type, and then returns the structure pointer that contains the field.
struct Scull_dev *dev; /*device information*/
dev = container_of (inode->i_cdev, struct scull_dev, Cdev);
Filp->private_data = Dev; /*for other methods*/
Another way to determine which device to open is to check the secondary device number that is saved in the inode structure.
2.release method: The release method works just as opposed to open, and this device method should complete the following tasks:
A. Releases all content that is assigned by open and saved in Filp->private_data.
B. Turn off the device on the last side of the shutdown operation
< five >read and write
The Read and write methods accomplish a similar task, which is to copy data into the application space or, in turn, copy the data from the application space.
ssize_t Read (struct file *filp, char __user *buff, size_t conut, loff_t *OFFP);
ssize_t Write (struct file *filp, char __user *buff, size_t conut, loff_t *OFFP);
The parameter FILP is a file pointer, and the parameter count is the length of the data requested for transmission, and the parameter buff is a buffer that points to the user space, which either protects the data to be written or is an empty buffer that holds the read-in data. The last OFFP is a pointer to the "Long offset type" object that indicates where the user accesses the file.
It should be noted that the buff parameter of the read and write methods is a pointer to the user space, so the kernel code cannot directly reference the contents. There are several reasons for this limitation:
A. The pointer to user space may be invalid when running in kernel mode, depending on the schema that the driver is running or the kernel configuration. The address may not be mapped to kernel space at all, or may point to some random data.
B. Even if the pointer represents the same thing in kernel space, the memory of the user space is paged, and the memory involved may not be in RAM at all when the system call is invoked. A direct reference to user space memory will result in a page fault, which is not allowed for kernel code and may result in a "oops" that will cause the death of the process calling the system call.
C. The pointers we are discussing may be provided by the user program, and the program may be defective or a malicious program. If the driver blindly references a user-supplied pointer, it causes the system to open the backdoor, allowing the user space program to access or overwrite the memory in the system. If the reader does not intend to compromise the security of the user's system because of his or her own driver, never directly reference the user space pointer.
What the read and write codes do is to make a copy of the whole piece of data between the user's address space and the kernel address space. The implementation core of the read and write methods is:
unsigned long copy_to_user (void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user (void __user *to, const void *from, unsigned long count);
The functions are not limited to copying data between kernel space and user space, they also check whether the pointer to user space is valid. If the pointer is not valid, the data will not be copied and, on the other hand, if an invalid address is encountered during the copy process, only part of the data will be copied. In both cases, the return value also requires a copy of the memory amount value. As for the actual device method, the Read method's task is to copy data from the device to the user space, while the Write method copies the data from the user space to the device. Each read or write system call requests a certain number of bytes to be transferred, but the driver does not limit the transfer of small amounts of data. Regardless of how much data is transferred, the file location represented by *OFFP should be updated to reflect the current file location after a successful completion of the new system call. When an error occurs, both the read and write methods return a negative value that is greater than or equal to 0 to tell the caller how many bytes were successfully transferred. If an error occurs after the partial data is transferred correctly, the return value must be the number of bytes successfully transferred. Although the kernel functions represent errors by returning negative values, and the return value indicates the type of error, the clock that the program running in user space sees is the 1 of the return value.
1.read Method:
The calling program interprets the return value of read as follows:
A. If the return value equals the Count parameter passed to the read system call, the transfer of the requested number of bytes has completed successfully.
B. If the return value is positive, but smaller than count, then only some of the data is successfully delivered. This situation may have many causes due to different devices. In most cases, the program will reread the data.
C. If the return value is 0, it means that the end of the file has been reached.
D. A negative value means an error has occurred that indicates what error occurred and the error code is defined in <linux/errno.h>.
2.write Method:
like read, write can transmit less than the requested amount of data according to the following return value rules:
A. If the return value equals count, the requested number of bytes is completed
B. If the return value is positive, but less than count, only part of the data is transferred. The program may try to write the rest of the data again.
C. If the value is 0, meaning nothing is written, the result is not an error, and there is no reason to return an error code.
D. Assignment means that an error has occurred and that the valid error code defined in <linux/errno.h> is the same as read.