如何将投票功能添加到内核模块代码中?

15
据我所知,为了从内核空间通知用户空间,一种方法是使用poll。这意味着内核驱动程序应该首先提供poll方法。 下面的代码来自于互联网,它确实可行!
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <linux/vmalloc.h>
#include <asm/uaccess.h>
 
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Fortune Cookie Kernel Module");
MODULE_AUTHOR("M. Tim Jones");
 
#define MAX_COOKIE_LENGTH       PAGE_SIZE
 
static struct proc_dir_entry *proc_entry;
static char *cookie_buf;  // Space for fortune strings
static int write_index;   // Index to write next fortune
static int read_index;    // Index to read next fortune
 
ssize_t fortune_write( struct file *filp, const char __user *buff,
                        unsigned long len, void *data )
// Refer to: ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
{
  int space_available = (MAX_COOKIE_LENGTH-write_index);
 
  if (len > space_available) {
    printk(KERN_INFO "fortune: cookie buffer is full!\n");
    return -ENOSPC;
  }
 
  if (copy_from_user( &cookie_buf[write_index], buff, len )) {
    return -EFAULT;
  }
 
  write_index += len;
  cookie_buf[write_index-1] = 0;
 
  return len;
}
 
ssize_t fortune_read(struct file *file, char *buf, size_t count, loff_t *f_pos){
// Refer to: ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    int len;
 
    //there's no fortune or a fortune has already been read
    //the *f_pos > 0 hack is needed because `cat /proc/fortune` would otherwise
    //display every thing in the cookie_buf
    if(write_index == 0 || *f_pos > 0){
        return 0;
    }
 
    // cicle through fortunes
    if(read_index >= write_index){
        read_index = 0;
    }
 
    len = sprintf(buf, "%s\n", &cookie_buf[read_index]);
 
    read_index += len;
    *f_pos += len;
 
    return len;
}
 
static const struct file_operations proc_test_fops = {
   .owner        = THIS_MODULE,
//    .open        = led_proc_open,
   .read        = fortune_read,
//    .llseek        = seq_lseek,
//    .release    = single_release,
   .write        = fortune_write,
//    unsigned int (*poll) (struct file *, struct poll_table_struct *);
//    int (*fasync) (int, struct file *, int);
};
 
int __init init_fortune_module( void )
{
    int ret = 0;
    cookie_buf = (char *)vmalloc( MAX_COOKIE_LENGTH );
    if (!cookie_buf) {
        ret = -ENOMEM;
    } else {
        memset( cookie_buf, 0, MAX_COOKIE_LENGTH );
//        proc_entry = create_proc_entry( "fortune", 0644, NULL );
        proc_entry = proc_create( "fortune", 0644, NULL, &proc_test_fops );
 
        if (proc_entry == NULL) {
            ret = -ENOMEM;
            vfree(cookie_buf);
            printk(KERN_INFO "fortune: Couldn't create proc entry\n");
        } else {
            write_index = 0;
            read_index = 0;
            printk(KERN_INFO "fortune: Module loaded.\n");
        }
    }
 
    return ret;
}
 
void __exit exit_fortune_module( void )
{
//    remove_proc_entry("fortune", &proc_entry);
    proc_remove(proc_entry);
    vfree(cookie_buf);
    printk(KERN_INFO "fortune: Module unloaded.\n");
}
 
module_init( init_fortune_module );
module_exit( exit_fortune_module );

我可以这样做使它工作:
echo "hello" > /proc/fortune

然后

cat /proc/fortune

想要查看结果。但如何添加投票方法呢?我尝试了几次,但仍然失败。

2个回答

17

在内核本身可以找到一些好的例子。请看下列文件:

按照以下步骤添加poll()函数到你的代码中。

  1. 包含需要的头文件:

     #include <linux/wait.h>
     #include <linux/poll.h>
    
  2. 声明等待队列变量:

     static DECLARE_WAIT_QUEUE_HEAD(fortune_wait);
    
  3. 添加 fortune_poll() 函数,并将其作为 .poll 回调函数添加到文件操作结构中:

  4.  static unsigned int fortune_poll(struct file *file, poll_table *wait)
     {
         poll_wait(file, &fortune_wait, wait);
         if (new-data-is-ready)
             return POLLIN | POLLRDNORM;
         return 0;
     }
    
     static const struct file_operations proc_test_fops = {
         ....
         .poll = fortune_poll,
     };
    
    注意,如果有新数据可读,则应返回POLLIN | POLLRDNORM,如果没有新数据可读(poll()调用已超时),则应返回0。详见man 2 poll

  5. 在有新数据时,请通知您的等待队列:

     wake_up_interruptible(&fortune_wait);
    

这是有关实现poll()操作的基本内容。根据您的任务,您可能需要在.read函数中使用一些waitqueue API(例如wait_event_interruptible())。


另请参阅相关问题:在Linux内核模块中实现poll


这个问题对我非常有帮助,但我仍然感到困惑。我在这里有一个相关的问题:https://dev59.com/wlsX5IYBdhLWcg3wRdp_ - zmb
这是一个很好的概述!在“Linux设备驱动程序”章节中,还有很多更多的信息:https://www.oreilly.com/library/view/linux-device-drivers/0596005903/ch06.html - Nick Crews
@KenLin 检查新数据的方法可能因每个特定情况而异。通常,变量用于此目的,可以在中断处理程序中将其设置为“true”(表示新数据已到达),并且可以在“read()”函数中将其设置为“false”(当所有数据都已使用时)。但是,理论上可能会出现其他检查新数据的情况(例如,通过检查循环缓冲区)。我的回答中的代码当然不完整,因为“poll()”机制没有单一应用案例。 - Sam Protsenko
顺便说一句,我知道这个答案很老了,但是现在顶部的链接都是404未找到! - Ken Lin
1
@KenLin 更新了链接,感谢您让我知道。回答您的问题:是的,我忘记提到了。请看一下我之前为我的学生写的这个示例。在hw3_poll()中,poll_wait()会睡眠并等待被唤醒,然后检查data_ready以确保我们被唤醒是因为实际上有新数据可供读取。而在hw3_btn_isr()(中断处理程序)中,data_ready被设置为true,然后调用wake_up_interruptible()来唤醒等待队列。 - Sam Protsenko
显示剩余2条评论

10

最小可运行示例

带有QEMU + Buildroot样板的GitHub上游:

在这个简化的例子中,我们从一个独立的线程生成轮询事件。在实际情况下,轮询事件通常会在硬件完成某些工作并有新数据可供用户空间读取时通过中断触发。

记住的主要观点是,如果 poll 返回零,内核会再次调用它:为什么需要在 poll 中调用 poll_wait?

poll.ko

#include <linux/debugfs.h>
#include <linux/delay.h> /* usleep_range */
#include <linux/errno.h> /* EFAULT */
#include <linux/fs.h>
#include <linux/jiffies.h>
#include <linux/kernel.h> /* min */
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/printk.h> /* printk */
#include <linux/uaccess.h> /* copy_from_user, copy_to_user */
#include <linux/wait.h> /* wait_queue_head_t, wait_event_interruptible, wake_up_interruptible  */
#include <uapi/linux/stat.h> /* S_IRUSR */

static int ret0 = 0;
module_param(ret0, int, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(i, "if 1, always return 0 from poll");

static char readbuf[1024];
static size_t readbuflen;
static struct dentry *debugfs_file;
static struct task_struct *kthread;
static wait_queue_head_t waitqueue;

static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    ssize_t ret;
    if (copy_to_user(buf, readbuf, readbuflen)) {
        ret = -EFAULT;
    } else {
        ret = readbuflen;
    }
    /* This is normal pipe behaviour: data gets drained once a reader reads from it. */
    /* https://dev59.com/D3I-5IYBdhLWcg3w3crS */
    readbuflen = 0;
    return ret;
}

/* If you return 0 here, then the kernel will sleep until an event
 * happens in the queue. and then call this again, because of the call to poll_wait. */
unsigned int poll(struct file *filp, struct poll_table_struct *wait)
{
    pr_info("poll\n");
    /* This doesn't sleep. It just makes the kernel call poll again if we return 0. */
    poll_wait(filp, &waitqueue, wait);
    if (readbuflen && !ret0) {
        pr_info("return POLLIN\n");
        return POLLIN;
    } else {
        pr_info("return 0\n");
        return 0;
    }
}

static int kthread_func(void *data)
{
    while (!kthread_should_stop()) {
        readbuflen = snprintf(
            readbuf,
            sizeof(readbuf),
            "%llu",
            (unsigned long long)jiffies
        );
        usleep_range(1000000, 1000001);
        pr_info("wake_up\n");
        wake_up(&waitqueue);
    }
    return 0;
}

static const struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = read,
    .poll = poll
};

static int myinit(void)
{
    debugfs_file = debugfs_create_file(
        "lkmc_poll", S_IRUSR | S_IWUSR, NULL, NULL, &fops);
    init_waitqueue_head(&waitqueue);
    kthread = kthread_create(kthread_func, NULL, "mykthread");
    wake_up_process(kthread);
    return 0;
}

static void myexit(void)
{
    kthread_stop(kthread);
    debugfs_remove(debugfs_file);
}

module_init(myinit)
module_exit(myexit)
MODULE_LICENSE("GPL");

poll.out用户空间:

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <fcntl.h> /* creat, O_CREAT */
#include <poll.h> /* poll */
#include <stdio.h> /* printf, puts, snprintf */
#include <stdlib.h> /* EXIT_FAILURE, EXIT_SUCCESS */
#include <unistd.h> /* read */

int main(int argc, char **argv) {
    char buf[1024];
    int fd, i, n;
    short revents;
    struct pollfd pfd;

    if (argc < 2) {
        fprintf(stderr, "usage: %s <poll-device>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    fd = open(argv[1], O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }
    pfd.fd = fd;
    pfd.events = POLLIN;
    while (1) {
        puts("poll");
        i = poll(&pfd, 1, -1);
        if (i == -1) {
            perror("poll");
            assert(0);
        }
        revents = pfd.revents;
        printf("revents = %d\n", revents);
        if (revents & POLLIN) {
            n = read(pfd.fd, buf, sizeof(buf));
            printf("POLLIN n=%d buf=%.*s\n", n, n, buf);
        }
    }
}

使用方法:

insmod poll.ko
mount -t debugfs none /sys/kernel/debug
./kernel_modules/poll.out /sys/kernel/debug/lkmc_poll

结果:每秒从用户空间打印jiffies到标准输出,例如:
poll
<6>[    4.275305] poll
<6>[    4.275580] return POLLIN
revents = 1
POLLIN n=10 buf=4294893337
poll
<6>[    4.276627] poll
<6>[    4.276911] return 0
<6>[    5.271193] wake_up
<6>[    5.272326] poll
<6>[    5.273207] return POLLIN
revents = 1
POLLIN n=10 buf=4294893588
poll
<6>[    5.276367] poll
<6>[    5.276618] return 0
<6>[    6.275178] wake_up
<6>[    6.276370] poll
<6>[    6.277269] return POLLIN
revents = 1
POLLIN n=10 buf=4294893839

强制让 file_operation 返回 0,以更清楚地了解会发生什么:

insmod poll.ko ret0=1

样例输出:

poll
<6>[   85.674801] poll
<6>[   85.675788] return 0
<6>[   86.675182] wake_up
<6>[   86.676431] poll
<6>[   86.677373] return 0
<6>[   87.679198] wake_up
<6>[   87.680515] poll
<6>[   87.681564] return 0
<6>[   88.683198] wake_up

从这里我们可以看出,控制权并没有返回到用户空间:内核只是一遍又一遍地调用 file_operation 中的 poll 函数。

在 Linux 5.4.3 上进行了测试。


每次等待队列中发生事件时,都会再次调用此函数。这是否意味着 poll_wait(filp, &waitqueue, wait); 会被多次调用?这不好吧,因为相同的 filpwaitqueue 将被添加到轮询表中。 - Ken Lin
或许这个想法是,当poll在等待队列中检测到事件发生时,它应该返回一个非零值来通知内核设备已经准备好进行I/O操作。因此,内核将会释放轮询表并将控制权返回给用户空间? - Ken Lin
@KenLin 我不确定它的内部工作原理,大多数情况下我只是从其他来源复制示例 :-) 但我认为快速查看轮询系统调用定义将很快澄清这一点,但我现在有点懒。仅从这个角度来看,似乎内核会自动分配/释放某些东西。如果您以后能更好地理解它或发现当前设置存在问题,请告诉我。我还添加了无数的打印示例,以使事情更清晰。 - Ciro Santilli OurBigBook.com
1
非常感谢!我实际上也尝试了你在更新的答案中提到的类似方法,并得出了相同的结论。我想没有什么比自己尝试更好的了! - Ken Lin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接