DAY69 Practical Guide to Linux Character Device Drivers

Practical Guide to Linux Character Device Drivers

1. Core Concepts Review (Understand the Basics First)

1.1 What is a Character Device Driver?

A character device driver is a kernel program that operates character devices, where data is accessed sequentially as a "byte stream." Its core functions are:

  • Providing unified file operation interfaces (open/read/write/close) to applications;
  • Interfacing with hardware to implement specific hardware control logic (for demonstration purposes, printk is used here instead of actual hardware operations).

1.2 Three Essential Elements of Driver Implementation (Your Focus)

  1. Hardware Operation Methods: Implement functions like open/read/write/close to encapsulate hardware control logic;
  2. Device Number Allocation: The device number is the driver's "ID card" (32-bit, with 12 bits for the major number and 20 bits for the minor number). The major number represents the device type, while the minor number distinguishes devices of the same type;
  3. Driver Registration: Register the driver's operation methods and device number with the kernel so the kernel can recognize and associate the device.

1.3 Key Tools and Commands

Tool/Command Purpose
arm-linux-gnueabihf-gcc Cross-compiler for compiling ARM architecture applications
make zImage Compile the Linux kernel, bundling the driver module into the kernel image
cat /proc/devices View registered driver device numbers (major number + device name)
mknod Manually create a device node (applications access the driver via this node)
dmesg View kernel print messages (output from printk in the driver)

2. In-Depth Analysis of Driver Code (Your demo_driver.c)

Your demo_driver.c perfectly implements the three essential elements of a character device driver. Let's break down the core logic step by step:

2.1 Step 1: Implement Hardware Operation Methods (file_operations Structure)

The core of the driver is the struct file_operations structure, which defines the operation interfaces callable by applications. The corresponding code is:

c 复制代码
// Define hardware operation methods (for demonstration, only print debug messages)
static int open(struct inode *inode, struct file *file) {
  printk("demo1_init open\n");  // Triggered when the application calls open
  return 0;
}

static ssize_t read(struct file *file, char __user *buf, size_t size, loff_t *loff) {
  printk("demo1_init read\n");   // Triggered when the application calls read
  return 0;
}

static ssize_t write(struct file *file, const char __user *buf, size_t size, loff_t *loff) {
  printk("demo1_init write\n");  // Triggered when the application calls write
  return 0;
}

static int close(struct inode *inode, struct file *file) {
  printk("demo1_init close\n");  // Triggered when the application calls close
  return 0;
}

// Bind operation methods to the file_operations structure
static struct file_operations fops = {
  .owner = THIS_MODULE,  // Declare the module owning the driver (prevents accidental unloading)
  .open = open,          // Associate the open function
  .read = read,          // Associate the read function
  .write = write,        // Associate the write function
  .release = close       // Associate the close function (release is called when the application closes)
};

Principle : When an application calls open("/dev/demo1", O_RDWR), the kernel finds the corresponding driver via the device number and then calls the bound open function in the driver.

2.2 Step 2: Apply for a Device Number (Static Allocation Method)

The device number is the "agreement credential" between the driver and the kernel. You used static allocation (specifying a fixed major number 255 and minor number 0). Code analysis:

c 复制代码
#define DEV_MAJOR 255  // Major number (custom, must ensure it's not already in use)
#define DEV_MINOR 0    // Minor number
#define DEV_NAME "demo1"  // Device name

static dev_t dev;  // Stores the combined 32-bit device number

// Combine major and minor numbers: MKDEV(major, minor)
dev = MKDEV(DEV_MAJOR, DEV_MINOR);

// Statically allocate the device number: parameters (device number, number of devices, device name)
register_chrdev_region(dev, 1, DEV_NAME);

Note : Static allocation requires ensuring the major number is not already used by another driver. Check occupied major numbers via cat /proc/devices. If you don't want to specify manually, you can use alloc_chrdev_region for dynamic allocation (the kernel automatically assigns an unused major number).

2.3 Step 3: Register the Driver with the System (cdev Structure)

The kernel manages character device drivers via the cdev structure. You need to associate file_operations with the device number and register it with the kernel. Code analysis:

c 复制代码
static struct cdev cdev;  // Core structure for character device drivers

// Initialize cdev: bind cdev with file_operations
cdev_init(&cdev, &fops);

// Register cdev with the kernel: parameters (cdev pointer, device number, number of devices)
cdev_add(&cdev, dev, 1);

Driver Loading/Unloading : Define the driver's loading and unloading entry points via the module_init and module_exit macros:

c 复制代码
// Executed when the driver loads (during insmod or kernel startup)
static int __init demo1_init(void) {
  // Device number allocation + driver registration logic (explained above)
  printk("-------------------------------------- demo1_init\n");
  return 0;
}

// Executed when the driver unloads (rmmod)
static void __exit demo1_exit(void) {
  cdev_del(&cdev);  // Remove cdev from the kernel
  unregister_chrdev_region(dev, 1);  // Release the device number
  printk("-------------------------------------- demo1_exit\n");
}

module_init(demo1_init);  // Register the loading entry
module_exit(demo1_exit);  // Register the unloading entry

3. Application Code Analysis (Your main.c)

The application accesses the driver via the "device node" (/dev/demo1), essentially calling the driver's file_operations interfaces. Code logic:

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  // Open the device node: corresponds to the driver's open function
  int fd = open("/dev/demo1", O_RDWR);
  if (fd < 0) {
    perror("open demo1 failed");  // Open failed (e.g., device node not created)
    return 1;
  }

  char buf[10] = {0};
  read(fd, buf, sizeof buf);  // Read from the driver: corresponds to the driver's read function
  write(fd, buf, sizeof buf); // Write to the driver: corresponds to the driver's write function
  close(fd);                  // Close the driver: corresponds to the driver's close function

  return 0;
}

Interaction Flow Between Application and Driver :
app: open() → kernel: sys_open() → driver: open() → kernel returns file descriptor fd → app: read/write/close() → driver: corresponding function executes

4. Complete Practical Steps (From Compilation to Verification)

You've completed the code writing and application compilation. Now follow these steps to compile, flash, and verify the driver:

4.1 Step 1: Add the Driver to the Kernel (Critical!)

To load the driver during kernel startup, add demo_driver.c to the kernel source and modify the corresponding Makefile and Kconfig (refer to previous kernel compilation notes):

  1. Copy demo_driver.c to the kernel source's drivers/char/ directory (default directory for character device drivers);

  2. Modify drivers/char/Makefile, adding the line:

    makefile 复制代码
    obj-$(CONFIG_DEMO_DRIVER) += demo_driver.o
  3. Modify drivers/char/Kconfig, adding the driver configuration option:

    kconfig 复制代码
    config DEMO_DRIVER
        tristate "Demo Character Device Driver"
        help
          This is a demo character device driver for IMX6ULL.
  4. Enable the driver via menuconfig:

    bash 复制代码
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

    Navigate to "Character devices" → select "Demo Character Device Driver" (choose Y to compile into the kernel), then save and exit.

4.2 Step 2: Compile the Kernel to Generate zImage

bash 复制代码
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

After successful compilation, the zImage image containing the demo driver will be generated in the arch/arm/boot/ directory.

4.3 Step 3: Flash the Kernel and Boot the Development Board

Download zImage and the corresponding device tree (imx6ull.dtb) to the development board via TFTP or flash them to an SD card, then boot the development board.

4.4 Step 4: Verify Driver Registration Success

  1. Check Device Number : Execute the following command on the development board terminal. If you see 255 demo1, it indicates successful driver registration:

    bash 复制代码
    cat /proc/devices  
  2. Create Device Node : Applications access the driver through device nodes. Run the following command to create one (ensure the major/minor numbers match the driver):

    bash 复制代码
    mknod /dev/demo1 c 255 0  
    • c: Denotes a character device.
    • 255: Major device number.
    • 0: Minor device number.

4.5 Step 5: Run Application for Verification

  1. Copy the compiled demo1app to the development board via NFS (refer to earlier NFS mounting notes).

  2. Run the application:

    bash 复制代码
    ./demo1app  
  3. Check Driver Output : Execute dmesg. If the following output appears, it confirms successful interaction between the application and driver:

    复制代码
    -------------------------------------- demo1_init  
    demo1_init open  
    demo1_init read  
    demo1_init write  
    demo1_init close  

5. Troubleshooting Common Issues

5.1 demo1 Not Listed in cat /proc/devices

  • Driver Not Compiled into Kernel : Verify DEMO_DRIVER is enabled in menuconfig and recompile the kernel.
  • Major Number Conflict : Modify DEV_MAJOR to an unused value (e.g., 240) and recompile.

5.2 Application Fails to open("/dev/demo1")

  • Missing Device Node : Execute mknod /dev/demo1 c 255 0.
  • Insufficient Permissions : Run chmod 777 /dev/demo1 to grant full access.

5.3 printk Output Not Visible in dmesg

  • Kernel Log Level Restriction : Execute echo 8 > /proc/sys/kernel/printk to enable all log levels.
相关推荐
程序猿编码2 小时前
实战Linux内核模块:终止ptrace跟踪程序与被跟踪进程
linux·网络·内核·内核模块·ptrace
bai5459362 小时前
STM32 CubeIDE 超声波测距
stm32·单片机·嵌入式硬件
GHL2842710902 小时前
TeamTalk-msg_server学习
运维·服务器·c++·学习
咩咩不吃草2 小时前
Linux环境下MySQL的安装与使用与Navicat
linux·运维·数据库·mysql·navicat
Aloudata2 小时前
NoETL 指标平台如何保障亿级明细查询的秒级响应?——Aloudata CAN 性能压测深度解析
数据库·数据分析·自动化·指标平台
maoku662 小时前
从关键词到语义:向量数据库如何让AI真正理解你的需求
数据库·人工智能
寻道码路2 小时前
【MCP探索实践】Google GenAI Toolbox:Google开源的企业级AI数据库中间件、5分钟搞定LLM-SQL安全互联
数据库·人工智能·sql·开源·aigc
数据知道2 小时前
PostgreSQL 核心原理:一文掌握 WAL 缓冲区与刷盘策略(性能与数据安全的权衡)
数据库·postgresql
三个人工作室2 小时前
mysql允许所有ip地址访问,mysql允许该用户访问自己的数据库【伸手党福利】
数据库·tcp/ip·mysql