Linux device driver learning notes - character device driver - Note.1

Keywords: Linux Operation & Maintenance kernel

Linux device driver learning notes - character device driver - Note.1


1, Character drive

[1] Primary and secondary equipment No

Character devices are accessed by names in the file system. Device files with names called file systems

Specify that they are located in the / dev directory. You can view their details through the ls -l command. c represents a char character device, b represents a block device, 1, 10 and 251 in the picture represent their primary device number, while 7 and 229 in the back represent the secondary device number of the device. The Linux kernel allows multiple drivers to share the primary number.

[2] Internal representation of equipment number

In the kernel, dev_t type (defined in < Linux / types. H >) is used to hold the device number - both primary and secondary parts are included. For the 2.6.0 kernel, dev_t is a 32-bit quantity, 12 bits as the primary number and 20 bits as the secondary number

Using < Linux / kdev_ t. A set of macro definitions in H >_ The primary or secondary number of T, using:

MAJOR(dev_t dev);

MINOR(dev_t dev);

If you have primary and secondary numbers, you need to convert them to a dev_t. You can use:

MKDEV(int major, int minor);

[3] Application and cancellation of equipment number

Uncontrollable application method

  • The first method is to apply for equipment, but the kernel will help you apply for 256 secondary equipment numbers at one time, which is more than a lot. The number is not particularly controllable, so we have the following dynamic allocation of equipment numbers.
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
	return __register_chrdev(major, 0, 256, name, fops);

static inline void unregister_chrdev(unsigned int major, const char *name)
	__unregister_chrdev(major, 0, 256, name);

Controllable application equipment number

  • To manually apply for a device number, you need to know your free device number in advance and fill in the function. This is obviously not very friendly. When you fill in 0, it will help you find a temporarily useless device number for you by default.
int register_chrdev_region(dev_t first, unsigned int count, char *name);

 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 * Return value is zero on success, a negative error code on failure.
int register_chrdev_region(dev_t from, unsigned count, const char *name)
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	return 0;
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	return PTR_ERR(cd);
  • Dynamically assign you a master number
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);  

 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
  • Using this function, dev is an output only parameter, which holds the first number of your allocation range when the function is successfully completed. fisetminor should be the first sub number to be used in the request; It is often the 0. count and name parameters, as given to request_ chrdev_ Release and use of equipment number:
void unregister_chrdev_region(dev_t first, unsigned int count); 

 * unregister_chrdev_region() - return a range of device numbers
 * @from: the first in the range of numbers to unregister
 * @count: the number of device numbers to unregister
 * This function will unregister a range of @count device numbers,
 * starting with @from.  The caller should normally be the one who
 * allocated those numbers in the first place...
void unregister_chrdev_region(dev_t from, unsigned count)
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
  • In fact, we find that the application structure eventually jumps to__ register_ chrdev_ In region
     * Register a single major with a specified minor range.
     * If major == 0 this functions will dynamically allocate a major and return
     * its number.
     * If major > 0 this function will attempt to reserve the passed range of
     * minors and will return zero on success.
     * Returns a -ve errno on failure.
    static struct char_device_struct *
    __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name);
[4] General form of applying for equipment number
if (module_major) { 
    dev = MKDEV(module_major, module_minor); 
    result = register_chrdev_region(dev, module_nr_devs, "module"); 
else { 
    result = alloc_chrdev_region(&dev, module_minor, module_nr_devs, modulel"); 
    module_major = MAJOR(dev); 
if (result < 0) { 
    printk(KERN_WARNING "module: can't get major %d\n", module_major); 
    return result; 

2, Three key data structures

Most basic driver operations include three important kernel data structures called file_operations, file, and inode

[1]file_operations file operations
  • After entering the kernel code, you will find that the corresponding explanations of the interfaces used in the user space can be found in the kernel space, which is also a very interesting thing

  • Some common operation functions in this structure
struct module *owner  
first file_operations Member is not an operation at all; It is a pointer to the module that owns the structure.
It is initialized to THIS_MODULE, One in <linux/module.h> Macros defined in.  

loff_t (*llseek) (struct file *, loff_t, int);  
llseek Method is used to change the current read in the file/Write position, And the new location as(Positive)Return value. loff_t Parameter is a"long offset", And at least 64 bits wide even on 32-bit platforms. The error is indicated by a negative return value. If this function pointer is NULL, seek Calls can be modified in potentially unpredictable ways file Position counter in structure( stay"file structure" Described in section).  

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
Used to get data from the device. A null pointer at this position causes read System call to -EINVAL("Invalid argument") fail. 
A non negative return value represents the number of bytes successfully read( The return value is a "signed size" type, It is often an integer type local to the target platform).  
ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);  
Initialize an asynchronous read -- A read operation that may not end before the function returns. If this method is NULL, All operations will be performed by read Substitute for(Synchronously).  

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);  
Send data to device. If NULL, -EINVAL Return to call write System called program. If nonnegative, The return value represents the number of bytes successfully written.  

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *); 
Initializes an asynchronous write on the device.  

unsigned int (*poll) (struct file *, struct poll_table_struct *);  
poll Method is the back end of three system calls: poll, epoll, and select, Are used to query whether reading or writing to one or more file descriptors will block. poll Method should return a bitmask indicating whether non blocking read or write is possible, also, Possibly, The information provided to the kernel is used to make the calling process sleep until I/O Become possible. If a driven poll Method is NULL, The device is assumed to be readable and writable without blocking.  

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);  
ioctl System calls provide methods for issuing device specific commands(For example, format a track of a floppy disk, This is neither reading nor writing). in addition, How many? ioctl Commands are recognized by the kernel without reference fops surface. If the equipment is not provided ioctl method, For any undefined request(-ENOTTY, "There is no such device ioctl"), The system call returned an error.  

int (*open) (struct inode *, struct file *);  
Although this is often the first operation on the device file, The driver is not required to declare a corresponding method. as 
If this item is NULL, The device has been successfully opened, But your driver won't be notified.  

//The corresponding close() operation is relative to the open() operation
int (*release) (struct inode *, struct file *);  
This operation is referenced when the file structure is released. like open, release Can be NULL.  

int (*lock) (struct file *, int, struct file_lock *);  
lock Method is used to implement file locking; Locking is an essential feature for regular files, But how many times does the device drive 
Almost never achieve it.  

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);  
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);  
These methods achieve divergence/Aggregate read and write operations. Occasionally, an application needs to do a single read or write operation that contains multiple memory areas; These system calls allow them to do so without having to make additional copies of the data. If these function pointers are NULL, read and write Method called( May be more than once ).  
  • Generally, the simplest module device driver only implements the most important device methods. Its file_ The operations structure is initialized as follows:
struct file_operations my_module_fops = {  
    .owner = THIS_MODULE, 
    .read = module_read,  
    .write = modulel_write, 
    .ioctl = module_ioctl,  
    .open = modulel_open,  
    .release modulell_release,  
[2] file structure
  • Partial interception of structure

struct file, defined in < Linux / Fs. H >, is the second most important data structure in device driver

Note that file has nothing to do with the file pointer of the user space program

  • A FILE is defined in the C library and never appears in the kernel code

  • A struct file, on the other hand, is a kernel structure that never appears in user programs

  • The file structure represents an open file (it is not specific to the device driver; each open file in the system has an associated struct file in kernel space). It is created by the kernel when it is open and passed to any function operating on the file until it is finally closed. After all instances of the file are closed, the kernel releases this data structure. In the kernel source code, The pointer to struct file is often called file or filp("file pointer"). This pointer is called filp to avoid confusion with the structure itself. Therefore, file refers to the structure and filp is the structure pointer

  • Note that release is not called every time a process calls close. Whenever a file structure is shared (for example, after a fork or dup), release will not be called until all copies are closed. You can also see the member variable that records the number of file operations in this structure.

  • Some common operation functions in this structure

mode_t f_mode;  

The file schema determines whether the file is readable or writable(Or both), Pass bit FMODE_READ and FMODE_WRITE. You might want to be in your open perhaps ioctl Function to check the read and write permissions of this member, But you don't need to check your read-write permission, Because the kernel checks before calling your method. Read or write attempts are denied when the file has not been opened for that access, The driver doesn't even know this.  

loff_t f_pos;  
Current read / write location. loff_t 64 bit on all platforms( stay gcc In terminology long long ). The driver can read this value, If it needs to know the current location in the file, But normally it should not be changed;Read and write should update a location with the pointers they received as the last arguments, Instead of acting directly on filp->f_pos. An exception to this rule is in llseek Method, Its purpose is to change the file location.  

unsigned int f_flags;  
These are file flags, for example O_RDONLY, O_NONBLOCK, and O_SYNC. The drive should be checked O_NONBLOCK Flag to see if a non blocking operation is requested( We are in the first chapter"Blocking and non blocking operations"Nonblocking is discussed in the section I/O ); Other signs are rarely used. Especially, Read should be checked/Write permission, use f_mode instead of f_flags. All flags are in the header file <linux/fcntl.h> Defined in.  

struct file_operations *f_op;  
Actions associated with files. The kernel arranges pointers as its open Part of the implementation, Then read it when it needs to dispatch any operation. filp->f_op Values in are never saved by the kernel as subsequent references; This means that you can change the file operation associated with your file, The new method will work after you return the caller. for example, Associated to primary number 1 (/dev/null, /dev/zero, wait)of open The code is replaced by the open sub number filp->f_op Operations in. This approach allows several behaviors to be implemented, Under the same major number without introducing overhead in each system call. The ability to replace file operations is object-oriented programming"Method overloading"Kernel peer for.  

void *private_data;  
open The system call sets this pointer to NULL, Calling for driver open Before method. You are free to use this member or ignore it; You can use this member to point to the assigned data, But then you have to remember before the kernel destroys the file structure, stay release Method to free that memory. private_data Is a useful resource, Preserve state information between system calls, Most of our example modules use it.  

struct dentry *f_dentry;  
Directory entry associated with the file( dentry )structure. Device driver writers normally don't need to care dentry  
structure, Except as filp->f_dentry->d_inode access inode structure.  
[3] inode structure
  • Partial interception of structure

  • The idea that everything in the Linux system is a file. Each file corresponds to its own unique file number. The inode structure is internally used by the kernel to represent the file. Therefore, it is different from the file structure representing the open file descriptor. There may be many file structures that represent multiple open descriptors for a single file, but they all point to a single inode structure. By using the ls -i instruction, we can see that this is the inode number of the file.

inode Structure contains a lot of information about files. As a general rule, This structure has only two members, which is useful for writing driver code:  
dev_t i_rdev; For nodes representing device files, This member contains the actual device number.  

struct cdev *i_cdev;  
|-->struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;

struct cdev Is the internal structure of the kernel, Representative character device; This member contains a pointer, Point to this structure, When the node refers to a character device file.  
Kernel developers have added two macros, Can be used from one inode Get primary and secondary numbers from:  
unsigned int iminor(struct inode *inode);  
unsigned int imajor(struct inode *inode);
|-->static inline unsigned iminor(const struct inode *inode)
        return MINOR(inode->i_rdev);

    static inline unsigned imajor(const struct inode *inode)
        return MAJOR(inode->i_rdev);

3, A simple driver framework

Posted by storyboo on Sun, 07 Nov 2021 18:38:23 -0800