Heng30的博客
搜索 分类 关于 订阅

如何给Linux字符设备驱动实现poll?

2025-03-02

一个驱动实现了poll函数,就能够让客户端程序使用selectepoll等异步IO系统调用进行读写驱动,大大丰富了驱动的使用。下面的例子就带大家看看如何去实现。

实例代码

// simple_cdev.dev

#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/device/class.h>
#include <linux/err.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <linux/uaccess.h>

#define BUFFER_SIZE 1024
#define DEV_MINOR_COUNTS 1
#define DEV_NAME "simple_cdev"

typedef struct {
    struct cdev *cdev;   // 字符设备
    struct class *class; // 设备类
    struct device *dev;  // /dev目录下的设备

    struct wait_queue_head wait_queue; // 等待队列头
    bool condition;                    // 条件变量

    char buffer[BUFFER_SIZE]; // 缓冲区
} simple_cdev_t;

simple_cdev_t my_cdev = {0};

static int _open(struct inode *inode, struct file *file) {
    pr_info("simple_cdev_open\n");
    return 0;
}

static ssize_t _read(struct file *file, char __user *userbuf, size_t size,
                     loff_t *offset) {
    // 非阻塞
    if (file->f_flags & O_NONBLOCK) {
        if (!my_cdev.condition) {
            return -EAGAIN;
        }
    } else {
        // 阻塞
        wait_event_interruptible(my_cdev.wait_queue, my_cdev.condition);
    }

    my_cdev.condition = false;
    pr_info("simple_cdev_read\n");

    size = min(size, BUFFER_SIZE);
    unsigned long remain_len = copy_to_user(userbuf, my_cdev.buffer, size);

    if (remain_len > 0) {
        pr_err("copy_to_user failed\n");
        return -EIO;
    }

    return size;
}

static ssize_t _write(struct file *file, const char __user *userbuf,
                      size_t size, loff_t *offset) {
    pr_info("simple_cdev_write\n");

    size = min(size, BUFFER_SIZE);
    unsigned long remain_len = copy_from_user(my_cdev.buffer, userbuf, size);

    if (remain_len > 0) {
        pr_info("copy_from_user failed\n");
        return -EIO;
    }

    // 唤醒等待队列
    my_cdev.condition = true;
    wake_up_interruptible(&my_cdev.wait_queue);

    return size;
}

static int _release(struct inode *inode, struct file *file) {
    pr_info("simple_cdev_release\n");
    return 0;
}

// IO多路复用中的回掉函数
static __poll_t _poll(struct file *file, struct poll_table_struct *table) {
    pr_info("simple_cdev_poll\n");

    int mask = 0;

    // 将fd指定的设备的等待队列挂载到wait列表中
    poll_wait(file, &my_cdev.wait_queue, table);

    // 如果条件满足,置位对应的掩码
    // POLL_IN 只读事件,POLL_OUT 只写事件,POLL_ERR 错误事件
    if (my_cdev.condition) {
        return mask | POLL_IN;
    }

    return mask;
}

static struct file_operations fops = {
    .open = _open,
    .read = _read,
    .write = _write,
    .release = _release,
    .poll = _poll,
};

static int __init simple_cdev_init(void) {
    pr_info("simple_cdev_init\n");

    my_cdev.cdev = cdev_alloc();
    if (!my_cdev.cdev) {
        pr_err("simple_cdev cdev_alloc failed!\n");
        return -ENOMEM;
    }

    // 初始化字符设备
    cdev_init(my_cdev.cdev, &fops);

    // 申请设备号, 由系统分配主设备号和一个从设备号
    int ret =
        alloc_chrdev_region(&my_cdev.cdev->dev, 0, DEV_MINOR_COUNTS, DEV_NAME);
    if (ret) {
        pr_err("simple_cdev alloc_chrdev_region failed!\n");
        return ret;
    }

    pr_info("simple_cdev major = %d\n", MAJOR(my_cdev.cdev->dev));

    // 添加到内核
    ret = cdev_add(my_cdev.cdev, my_cdev.cdev->dev, DEV_MINOR_COUNTS);
    if (ret) {
        pr_err("simple_cdev cdev_add failed!\n");
        return ret;
    }

    // 申请设备类, 会在/sys/class目录下常见一个simple_cdev的目录
    my_cdev.class = class_create("simple_cdev");
    if (IS_ERR(my_cdev.class)) {
        pr_err("simple_cdev class_create failed\n");
        return PTR_ERR(my_cdev.class);
    }

    // 申请设备对象,会在/dev目录下创建设备文件simple_cdev,
    // 并且在/sys/class/simple_cdev目录下常见一个simple_cdev的设备节点
    my_cdev.dev =
        device_create(my_cdev.class, NULL, my_cdev.cdev->dev, NULL, DEV_NAME);
    if (IS_ERR(my_cdev.dev)) {
        pr_err("simple_cdev device_create failed\n");
        return PTR_ERR(my_cdev.dev);
    }

    // 初始化等待队列和条件变量
    init_waitqueue_head(&my_cdev.wait_queue);
    my_cdev.condition = false;

    return 0;
}

static void __exit simple_cdev_exit(void) {
    pr_info("simple_cdev_exit\n");

    device_destroy(my_cdev.class, my_cdev.cdev->dev);
    class_destroy(my_cdev.class);
    cdev_del(my_cdev.cdev);
    unregister_chrdev_region(my_cdev.cdev->dev, DEV_MINOR_COUNTS);
    kfree(my_cdev.cdev);
}

module_init(simple_cdev_init);
module_exit(simple_cdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("heng30");
MODULE_VERSION("v0.0.1");
MODULE_DESCRIPTION("A simple_cdev kernel module");

Makefile编译脚本

#!/bin/sh

top-dir = $(shell pwd)
kernel-version = $(shell uname -r)
kernel-dir ?= /lib/modules/$(kernel-version)/build

obj-m += simple_cdev.o

all:
        make -C $(kernel-dir) modules M=$(top-dir)

clean:
        rm -f *.o *.ko *.mod *.mod.c *.order *.symvers
        make -C $(kernel-dir) clean m=$(top-dir)

select测试代码

// select.c
// 编译命令:gcc -o select select.c

#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

char buffer[BUFFER_SIZE];

int main(int argc, char *argv[]) {
    if (argc != 2) {
        perror("please input a device to read and write");
        return -1;
    }

    fd_set read_set;

    int fd = open(argv[1], O_RDONLY);
    assert(fd >= 0);

    while (true) {
        FD_ZERO(&read_set);
        FD_SET(fd, &read_set);

        int fds = select(fd + 1, &read_set, NULL, NULL, NULL);

        if (fds < 0) {
            perror("select error");
            return -1;
        } else if (fds == 0) {
            continue;
        } else {
            for (int efd = 0; efd < fd + 1; efd++) {
                if (!FD_ISSET(efd, &read_set)) {
                    continue;
                }

                ssize_t rlen = read(efd, buffer, BUFFER_SIZE - 1);
                if (rlen == 0) {
                    continue;
                } else if (rlen < 0) {
                    buffer[0] = '\0';
                    perror("read error");
                } else {
                    buffer[rlen] = '\0';
                    printf("len = %ld, %s\n", rlen, buffer);
                }
            }
        }

        sleep(1);
    }

    close(fd);
    return 0;
}

测试

  • 安装驱动:insmod simple_cdev.ko

  • 移除驱动:rmmod simple_cdev.ko

  • 打开两个终端窗口:

    • 第1个窗口运行:./select /dev/simple_cdevselect命令会一直等待。
    • 第2个窗口运行:echo "hello" > /dev/simple_cdev。第1个窗口会马上输出hello字符串。在第2个窗口不断地输入内容,第1个窗口会输出相同的内容。
    [27230.570089] simple_cdev_open
    [27230.570122] simple_cdev_write
    [27230.570138] simple_cdev_release
    [27230.570339] simple_cdev_poll
    [27230.570363] simple_cdev_read
    [27231.586524] simple_cdev_poll
    

    函数调用过程:驱动调用write函数->唤醒等待队列->驱动执行poll函数->客户端程序从select函数返回->客户端程序执行read函数->驱动执行read函数->客户端再次调用select函数->驱动执行poll函数