驱动程序的poll休眠唤醒机制学习记录及示例程序分析(没有涉及具体的硬件操作-poll机制与最初的想象有很大的差别)

对poll休眠唤醒机制的理解

理解poll机制前,请先看下我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/145225508

在上面这篇博文中,利用函数wait_event_interruptible()使线程进入休眠状态,直到某个事件发生,线程才被唤醒并继续执行后面的代码。

但有时候我们希望线程进入休眠状态的时间是有限的,即线程等待某个事件的发生是有时间限制的,如果超过这个时间就不等了,然后线程被这个超时事件唤醒,进而去执行后面的代码,当然在后面的代码中,按等待的事件超时未发生的分支走。
举个具体的例子,你和别人约好某个时间在某个地点见面做某件事,你按时到达那里,结果你在那里左等不见人,右等不见人,这个时候你就是处于一种休眠状态中(因为对方未到你无法做事),但你总不能一直等下去吧,这个时候你就给自己设置个计时器,如果超过20分钟对方还未到,就不等了。如果20分钟内,对方到了,那按正常的安排走,如果超过20分钟对方还未到,就打车回家。

驱动程序的poll机制就能满足我们上面的这个需求,它会为等待事件发生的线程设置一个时间,在这个时间范围内,如果事件发生,那么它就返回一个值表示这个事件发生了,如果超过这个时间,相关的事件还没有发生,那么它也回返回一个值表示这个事件没有发生。
当然这个时间值可以设置为-1,表示等待的时间为无穷大,这个时候poll机制就退化为上篇博文(https://blog.csdn.net/wenhao_ir/article/details/145225508)中的休眠唤醒机制。

Linux系统内核提供了函数poll()来实现poll机制在用户空间的部分,其地位与系统内核提供的函数open()、read()、write()、close()的地位一样。
当用户空间调用函数poll()后,在驱动程序中,文件操作结构体(file_operations)中成员poll对应的函数便会被调用,如下图所示:
在这里插入图片描述
本文通过一个不涉及具体的硬件操作的驱动程序来说明poll机制是怎么回事儿,也就是重心放在对其原理和结构的理解上。

poll机制的流程其实和自己最初的想象有很大区别

poll机制的实现我刚开始认为很简单,后来仔细研究后发现不是自己想象的那回事儿,比如下面这些:
①你以为程序一开始在驱动程序的操作函数poll_demo_poll()中运行到语句poll_wait(file, &wait_queue, wait);时,它就会进入休眠状态,其实不是,第一次运行到到语句poll_wait(file, &wait_queue, wait);时它是不会进入休眠状态的,第一次运行语句poll_wait(file, &wait_queue, wait);时,这条语句不会有什么具体的行动。只有第二次运行语句poll_wait(file, &wait_queue, wait);时,它才会将线程放入等待队列,从而让线程进入休眠状态。
②你以为被语句poll_wait(file, &wait_queue, wait);挂入等待队列中的线程在被唤醒后函数poll_wait()本身会提供判断要不要彻底被唤醒的判断机制,就像博文https://blog.csdn.net/wenhao_ir/article/details/145225508中的函数wait_event_interruptible那样,本身提供了唤醒后判断要不要彻底被唤醒的条件【通过它的第3个参数condition实现】,但很遗憾,它没有提供,它被唤醒后就会继续执行后面的代码,poll机制根据它后面代码提供的返回值判断要不要再次调用驱动程序的操作函数poll_demo_poll(),如果再次调用操作函数poll_demo_poll(),则线程才会再次进入休眠状。
③…更多自己想不到的内容请看我写的“详细分析与poll机制有关的代码流程”。

完整源码

驱动程序的源码“poll_demo_drv.c”

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/poll.h>
#include <linux/wait.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/device.h>


static int major;
static struct class *poll_demo_class;

static char *device_buffer;
static size_t buffer_size = 1024;
static size_t data_len = 0;

static wait_queue_head_t wait_queue;
static int data_ready = 0;

static int poll_demo_open(struct inode *inode, struct file *file)
{
    // pr_info("poll_demo: Device opened\n");
    return 0;
}

static int poll_demo_release(struct inode *inode, struct file *file)
{
    // pr_info("poll_demo: Device closed\n");
    return 0;
}

static ssize_t poll_demo_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
    if (data_len == 0)
    {
        pr_info("poll_demo: No data to read\n");
        return 0; // No data available
    }

    if (count > data_len)
        count = data_len;

    if (copy_to_user(buf, device_buffer, count))
        return -EFAULT;

    data_len = 0;  // Data is consumed
    data_ready = 0;

    // pr_info("poll_demo: Read %zu bytes\n", count);
    return count;
}

static ssize_t poll_demo_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
    if (count > buffer_size)
        count = buffer_size;

    if (copy_from_user(device_buffer, buf, count))
        return -EFAULT;

    data_len = count;
    data_ready = 1;

    pr_info("poll_demo: Written %zu bytes\n", count);

    wake_up_interruptible(&wait_queue);

    return count;
}

static unsigned int poll_demo_poll(struct file *file, poll_table *wait)
{
    unsigned int mask = 0;

    poll_wait(file, &wait_queue, wait);

    if (data_ready)
        mask |= POLLIN | POLLRDNORM;

    // pr_info("I am poll_demo_poll.\n");

    return mask;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = poll_demo_open,
    .release = poll_demo_release,
    .read = poll_demo_read,
    .write = poll_demo_write,
    .poll = poll_demo_poll,
};

static int __init poll_demo_init(void)
{
    major = register_chrdev(0, "poll_demo_major", &fops);
    if (major < 0)
    {
        pr_err("poll_demo: Failed to register device\n");
        return major;
    }

    device_buffer = kmalloc(buffer_size, GFP_KERNEL);
    if (!device_buffer)
    {
        unregister_chrdev(major, "poll_demo_major");
        return -ENOMEM;
    }

    poll_demo_class = class_create(THIS_MODULE, "swh_poll_demo_class");
	if (IS_ERR(poll_demo_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "poll_demo_major");
		return PTR_ERR(poll_demo_class);
	}

	device_create(poll_demo_class, NULL, MKDEV(major, 0), NULL, "poll_demo_0"); /* /dev/read_keys0 */

    init_waitqueue_head(&wait_queue);

    pr_info("poll_demo: Module loaded, major = %d\n", major);
    return 0;
}

static void __exit poll_demo_exit(void)
{

    device_destroy(poll_demo_class, MKDEV(major, 0));
    class_destroy(poll_demo_class);
    unregister_chrdev(major, "poll_demo_major");
    kfree(device_buffer);
    pr_info("poll_demo: Module unloaded\n");
}

module_init(poll_demo_init);
module_exit(poll_demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Poll Mechanism Demo");

读数据的测试程序(poll_demo_test_read.c)的源码

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>

#define DEVICE_PATH "/dev/poll_demo_0"
#define BUFFER_SIZE 128

// 打印线程的执行函数
void* print_while_waiting(void* arg) {
    while (1) {
        printf("I am another thread, and while the main thread is waiting for poll events, I can still run normally.\n");
        sleep(20); // 每隔20秒打印一次
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    int fd;
    struct pollfd fds[1];
    char read_buffer[BUFFER_SIZE];
    int ret;
    pthread_t print_thread;
    int timeout = -1; // 默认无限等待

    // 解析命令行参数
    if (argc == 2) {
        timeout = atoi(argv[1]); // 转换超时时间为整数(单位:毫秒)
        if (timeout < 0) {
            fprintf(stderr, "Invalid timeout value: %d. Using default (infinite wait).\n", timeout);
            timeout = -1;
        }
    } else {
        printf("Usage: %s <timeout_ms>\n", argv[0]);
        return EXIT_FAILURE;
    }

    // 打开设备文件
    fd = open(DEVICE_PATH, O_RDONLY);
    if (fd < 0) {
        perror("Failed to open device");
        return EXIT_FAILURE;
    }

    // 配置 poll 的监听对象信息
    fds[0].fd = fd;
    fds[0].events = POLLIN; // 监听可读事件

    printf("Waiting for data with timeout: %d milliseconds...\n", timeout);

    // 创建一个线程,每隔一段时间打印输出一条信息表示在等待期间,另外的线程继续正常执行。
    if (pthread_create(&print_thread, NULL, print_while_waiting, NULL) != 0) {
        printf("Failed to create print thread\n");
        close(fd);
        return -1;
    }

    // 主循环,连续等待和读取数据
    while (1) {
        // 调用 poll,等待设备的数据就绪
        ret = poll(fds, 1, timeout); // 使用从命令行传入的超时时间
        if (ret < 0) {
            perror("poll failed");
            break; // 发生错误,退出循环
        } else if (ret == 0) {
            printf("Timeout occurred! No data available within %d milliseconds.\n", timeout);
        } else {
            // 检查返回的事件,这个检查是有必要的,因为假如poll监听的是fds中的几个成员,你不能光凭poll的返回值就判断出是哪一个监听对象产生了感兴趣事件
            if (fds[0].revents & POLLIN) {
                // printf("Data is available! Reading...\n");
                ret = read(fd, read_buffer, BUFFER_SIZE - 1);
                if (ret < 0) {
                    perror("Failed to read from device");
                    break; // 读取失败,退出循环
                } else {
                    read_buffer[ret] = '\0'; // 确保字符串以 NULL 结束
                    printf("Read %d bytes: %s\n", ret, read_buffer);
                }
            } else {
                printf("Unexpected event: 0x%x\n", fds[0].revents);
            }
        }
    }

    // pthread_join 的作用是使主线程等待线程 print_thread 结束后再继续执行剩下的代码。
    pthread_join(print_thread, NULL);

    // 关闭设备文件
    close(fd);
    return EXIT_SUCCESS;
}

写数据的测试程序(poll_demo_test_write.c)的源码

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define DEVICE_PATH "/dev/poll_demo_0"

int main(int argc, char *argv[]) {
    int fd;
    int ret;
    
    // 检查是否传入了字符串参数
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <string_to_write>\n", argv[0]);
        return EXIT_FAILURE;
    }

    // 获取待写入的字符串
    const char *input_string = argv[1];

    // 打开设备文件
    fd = open(DEVICE_PATH, O_WRONLY);
    if (fd < 0) {
        perror("Failed to open device");
        return EXIT_FAILURE;
    }

    // 向设备写入数据
    ret = write(fd, input_string, strlen(input_string));
    if (ret < 0) {
        perror("Failed to write to device");
        close(fd);
        return EXIT_FAILURE;
    }

    // printf("Successfully wrote '%s' to device.\n", input_string);

    // 关闭设备文件
    close(fd);
    return EXIT_SUCCESS;
}

认识关键函数poll()

用户态中的 poll() 函数是一个用于监视文件描述符事件的系统调用,主要用于实现事件驱动的 I/O 模型。它允许程序同时监视多个文件描述符,等待其状态变化(如可读、可写或异常)。与传统的阻塞式 readwrite 操作相比,poll 提供了更高的效率和灵活性。


函数原型

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明

  1. struct pollfd *fds

    • 是一个 pollfd 结构体数组【注意是结构体数组,里面存储着多个pollfd结构体】,用于描述需要监视的文件描述符及其感兴趣的事件。
    • 每一个pollfd 结构体就是一个监听对象,所以fds可看成存储监听对象的结构体数组。
    • 每个pollfd 结构体定义如下:
      struct pollfd {
          int fd;        // 文件描述符
          short events;  // 需要监视的事件
          short revents; // 实际发生的事件
      };
      
    • events 是输入字段,指定要监视的事件;revents 是输出字段,用于返回实际发生的事件。
  2. nfds_t nfds

    • fds数组中需要监视的文件描述符数量,举个例子,假如数组fds中有3个pollfd 结构,即三个监听对象,而我这里设置nfds的值为2,那么只监听前两个监听对象,即只打开前两个监听对象对应的驱动程序中的poll处理函数。
  3. int timeout

    • 超时时间(以毫秒为单位)。以下是常见值及含义:
      • 正值:等待的最大时间(毫秒)。
      • 0:立即返回,不阻塞。
      • -1:无限等待,直到事件发生。

返回值

  • 正值:表示发生了感兴趣事件的监听对象的数量。
  • 0:超时,且在指定的时间内没有任何事件发生。
  • -1:发生错误(通过 errno 查看具体原因)。

常见事件类型
eventsrevents 字段的值是以下事件类型的位掩码:

事件类型描述
POLLIN有数据可读。
POLLRDNORM普通数据可读。
POLLOUT写操作不会阻塞(可以发送更多数据)。
POLLWRNORM普通数据可写。
POLLERR发生错误。
POLLHUP挂起事件(通常表示管道或套接字的另一端已关闭)。
POLLNVAL无效的文件描述符。

工作原理

  1. 调用流程

    • 用户态中poll被调用后,会调用驱动程序中的poll操作函数,并将监听对象的相关信息以参数poll_table *wait传递给驱动程序中的poll操作函数。
    • 驱动程序的poll操作函数中的语句poll_wait(file, &wait_queue, wait);在运行时会首先检查
      内核检查对应的监听对象是否有匹配的事件发生:
      • 如果事件已就绪,poll 立即返回。
      • 如果未就绪,当前线程利用函数poll_wait()使自己进入睡眠状态,直到事件发生或超时。
    • 超时或事件发生后,内核返回已发生感兴趣事件的监听对象的数量,并填充 revents 字段。
  2. 与内核中驱动程序的交互

    • 驱动程序中实现 poll 操作函数(如 poll_demo_poll),调用 poll_wait 将线程挂入等待队列。
    • 通过 wake_up_interruptible 唤醒所有处于等待队列中的线程,与poll机制相关的线程被唤醒后继续执行驱动程序的poll操作函数中的语句poll_wait(file, &wait_queue, wait);后面的代码,poll操作函数运行完后会返回一个记录事件类型的返回值,此时poll机制会根据这个返回值与poll_wait函数的第3个参数wait中记录的感兴趣事件相对比,如果返回值是感兴趣事件,则彻底被唤醒,进而执行后面的代码,如果返回值不是感兴趣事件,则再次调用驱动程序中的poll操作函数,当运行到语句poll_wait(file, &wait_queue, wait);时再次进入等待队列,线程又进入睡眠状态。

认识关键函数poll_wait()【其第3个参数是理解的难点】

poll_wait 函数
在 Linux 内核中,poll_wait 是设备驱动中实现 poll 操作的方法之一。它的主要功能是将当前线程添加到一个等待队列中,使得线程进入休眠状态,并且通过第3个参数让线程与具体的监听对象关联起来,这样线程在被函数wake_up_interruptible()唤醒后才知道自己是否需要彻底被唤醒。
具体来说,线程在被函数wake_up_interruptible()唤醒后会继续运行驱动程序中 poll 操作函数中的语句poll_wait(file, &wait_queue, wait);后面的代码,并获得 poll 操作函数的返回值。此时poll机制会把这个返回值与poll_wait函数的第3个参数wait中记录的与该线程关联的监听对象的感兴趣事件相对比,看监听对象中的感兴趣事件是否发生,如果发生,则线程彻底醒来,如果没有发生,poll机制会让线程又一次调用驱动程序中的 poll 操作函数,从而因为 poll 操作函数中的语句poll_wait()而再次进入休眠状态。

然后线程中的poll机制用这个返回值去检查与这个线程关联的poll监听对象中的感兴趣事件是否发生,如果发生,则线程彻底醒来,如果没有发生,poll机制会让线程又一次调用驱动程序中的 poll 操作函数,从而因为 poll 操作函数中的语句poll_wait()而再次进入休眠状态。


函数原型

void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

参数说明
struct file *filp

  • 指向当前打开文件的文件结构体(来自用户态传递的文件描述符)。
  • 通常是驱动在操作文件时的上下文信息,提供给 poll 用于记录该文件的相关事件。

wait_queue_head_t *wait_address

  • 指向等待队列头的指针。
  • 驱动程序在此队列上挂载线程,挂载后,相关线程进入休眠状态。
  • 等到相关状态改变(比如数据可用),驱动通过 wake_up_interruptible 等方法唤醒等待队列中的线程。

poll_table *p
通过poll_table,使得休眠的线程能够与监听对象关联起来。

具体来说每次用户态调用 poll函数 ,内核会:
①为用户态调用创建一个 poll_table 数据结构。
②在驱动的 poll 回调操作函数中,当 poll_wait 被调用时, poll_wait 一方面将线程加入等待队列,使得线程进入休眠状态。另一方面通过第3个参数poll_table *p让线程与具体的监听对象关联起来,这样线程在被函数wake_up_interruptible()唤醒后才知道自己是否需要彻底被唤醒。
具体来说,线程在被函数wake_up_interruptible()唤醒后会继续运行驱动程序中 poll 操作函数中的语句poll_wait(file, &wait_queue, wait);后面的代码,并获得 poll 操作函数的返回值。此时poll机制会把这个返回值与poll_wait函数的第3个参数wait中记录的与该线程关联的监听对象的感兴趣事件相对比,看监听对象中的感兴趣事件是否发生,如果发生,则线程彻底醒来,如果没有发生,poll机制会让线程又一次调用驱动程序中的 poll 操作函数,从而因为 poll 操作函数中的语句poll_wait()而再次进入休眠状态。

另外还需要知道: p == NULL是被允许的,当 p == NULL时,说明驱动中的 poll 函数并非由用户态的 poll 调用触发,此时poll_wait函数不会执行挂起操作,而是直接往后执行代码,直接返回当前状态。

★详细分析与poll机制有关的代码流程★

阅读下面的解读前建议先阅读下我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/145225508 中对于等待队列的解读【搜索“等待队列的头部详解”】

首先在用户空间的main函数中打开设备文件:

fd = open(DEVICE_PATH, O_RDONLY);

然后设置用户空间的内核函数poll()运行时需要的第1个参数struct pollfd *fds,内核函数poll()的原型如下:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其第1个参数fds是一个结构体数组,其每一个成员都是一个结构体struct pollfd的实例。结构体struct pollfd的每一个实例可以看成是一个监听对象,里面存储着这个监听对象的文件描述符、这个监听对象感兴趣的事件以及这个监听对象实际监测到的事件,其定义如下:

     struct pollfd {
         int fd;        // 文件描述符
         short events;  // 需要监视的事件
         short revents; // 实际发生的事件
     };

在我的程序中,对第1个参数fds的设置如下:

    // 配置 poll 的监听对象信息
    fds[0].fd = fd;
    fds[0].events = POLLIN; // 监听可读事件

在这里,只向数组fds中写入了一个成员,也就是说我们在程序中只利用poll机制监听一个对象,如果这里向数组fds写入的是多个成员,即写入了多个监听对象,那可以用内核函数poll()的第2个参数nfds_t nfds设置监听几个对象,举个例子,假如数组fds中有3个pollfd 结构,即三个监听对象,而我这里设置nfds的值为2,那么只监听前两个监听对象。

设置好内核函数poll()需要的第1个参数struct pollfd *fds后,就开始调用内核函数poll()了,相关代码如下:

ret = poll(fds, 1, timeout);

其中的timeout表示超时时间,这里暂不作展开解释,你把我写的整个详细流程看完后就知道其具体意义了。

内核函数poll()被调用后,就会去调用驱动程序中在文件操作结构体中指定的poll操作函数,在这里是函数poll_demo_poll
在这里插入图片描述
函数poll_demo_poll的源码如下:

static unsigned int poll_demo_poll(struct file *file, poll_table *wait)
{
    unsigned int mask = 0;

    poll_wait(file, &wait_queue, wait);

    if (data_ready)
        mask |= POLLIN | POLLRDNORM;

    // pr_info("I am poll_demo_poll.\n");

    return mask;
}

其第1句代码unsigned int mask = 0;是把mask的值置为0,mask用于记录发生了什么事件,最终mask会作为函数poll_demo_poll的返回值被返回。
Linux的poll机制中定义了如下这些事件:

事件类型描述
POLLIN有数据可读。
POLLRDNORM普通数据可读。
POLLOUT写操作不会阻塞(可以发送更多数据)。
POLLWRNORM普通数据可写。
POLLERR发生错误。
POLLHUP挂起事件(通常表示管道或套接字的另一端已关闭)。
POLLNVAL无效的文件描述符。

注意:这些事件也是监听对象结构体struct pollfdeventsrevents 字段的值。

把mask的值置为0后,会运行实现poll机制的关键代码:

 poll_wait(file, &wait_queue, wait);

poll_wait()函数的原型如下:

void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

这个poll_wait()函数用于实现把线程加入等待队列中,线程加入等待队列后,实际上就是处于休眠状态了。

参数struct file *filp是打开的设备文件的文件结构体,它来自用户空间的poll()函数,它的具体介绍见 https://blog.csdn.net/wenhao_ir/article/details/144927750,在它里面存储着设备文件的相关信息。通过后面的分析我们发现这个参数并不一定会被使用。

poll_wait()函数的源码并不复杂,甚至很简单,我从内核源码中摘录如下:

// 位置:include\linux\poll.h
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && p->_qproc && wait_address)
		p->_qproc(filp, wait_address, p);
}

其第3个参数涉及到的结构体poll_table的定义如下:

// 位置:include\linux\poll.h
typedef struct poll_table_struct {
	poll_queue_proc _qproc; //_qproc用于将线程加入队列,内核默认设置为函数 __pollwait
	unsigned long _key;  // 一般用于存储poll的监听事件类型
} poll_table;

poll_wait()函数的代码逻辑很简单【注意,符号&&代表逻辑与运算,而不是与运算】,即当pp->_qprocwait_address都为真的情况下,去调用参数poll_table *p中的成员函数_qproc【这里的参数p由内核在调用驱动程序中的poll操作函数poll_demo_poll前自动生成】,这个成员函数_qproc的作用是实现将线程加入等待队列wait_address中,wait_address代表等待队列的头部,线程一旦加入等待队列中,实际上就是休眠了。
成员_qproc默认被设置为__pollwait函数,在内核为驱动程序中的poll操作函数poll_demo_poll()提供第3个参数poll_table *p时,_qproc被设置为指向__pollwait函数。
从源码中我们可以看到_qproc需要的3个参数为(filp, wait_address, p),所以我们的poll_wait()函数需要的输入参数为(struct file * filp, wait_queue_head_t * wait_address, poll_table *p),即对应于_qproc需要的3个参数,_qproc需要的参数wait_address很好理解为什么需要,因为你_qproc的主要作用就是要把线程加入等待队列,所以肯定需要等待队列的头部嘛。而至于_qproc需要的参数filpp,则可能是当系统默认的__pollwait函数被替换为自定义函数时为了让用户编写比较复杂的_qproc函数而提供的【注意参数p还存储着poll的监听事件类型哦】,实际上并不一定被使用,就像我们传给驱动程序中的read操作函数和write操作函数中的参数struct file *file也不一定用到是一个道理。
所以根据分析,poll_wait()函数需要的参数struct file *filp不一定被用到哦,但为了程序的统一和可扩展,也需要提供哟。

通过上面的分析咱们就对下面这句代码有了个详细的理解。

 poll_wait(file, &wait_queue, wait);

接下来,我们继续说咱们的代码的流程,根据我的实际试验表明,具体的流程如下:
用户态中的poll函数调用驱动程序中的poll操作函数poll_demo_poll,然后第一次执行到poll_wait(file, &wait_queue, wait);时,线程不管怎么样都不会进入等待队列,而是会完整的执行完一遍函数poll_demo_poll,关于这一点的证据是我在下面这个位置放了一条打印信息的命令:
在这里插入图片描述
然后每次我运行测试程序,都还没有超时,就马上打印了信息I am poll_demo_poll.,如果执行poll_wait(file, &wait_queue, wait);时,线程进入了休眠状态,那肯定不会在超时前就打印出信息I am poll_demo_poll.了。我猜测之所以能这样应该是由于在_qproc的默认函数__pollwait中有个标志表示是不是第一次调用函数__pollwait,如果是第一次调用,它就不把线程加入等待队列中。

函数poll_demo_poll第一次执行完后会返回事件状态值(即变量mask的值):
如果此时poll机制发现mask中代表的事件类型是自己的监听对象期望的事件,则用户空间中的poll函数退出,并把1作为返回值,这个返回值1表示发生了感兴趣事件的监听对象的数量,同时会把相应的监听对象的字段的字段short revents设置为mask的值,供后续的代码判断使用。
如果此时poll机制发现mask中代表的事件类型不是自己的监听对象期望的事件,则用户空间中的poll函数不会退出,并且此时会再次调用函数poll_wait(file, &wait_queue, wait);,此时由于是第二次调用函数poll_wait(file, &wait_queue, wait);,它就会使线程加入等待队列,即让线程处于休眠状态,并启动计时。

当有数据写入时,驱动程序中的write操作函数poll_demo_write在写完数据后,会调用函数wake_up_interruptible唤醒等待队列中的所有线程:
在这里插入图片描述
与poll机制有关的线程在被唤醒后会去执行poll_demo_poll函数中语句poll_wait(file, &wait_queue, wait);后面的语句,即下面红框中的语句:
在这里插入图片描述
此时由于写入了数据,变量data_ready的逻辑值为真,所以mask的值为POLLIN | POLLRDNORM,函数poll_demo_poll执行完毕后,会把mask的值作为返回值提供给poll机制,poll机制发现此时mask的值就是监听对象的期望事件的值,则用户空间中的poll函数退出,并把1作为返回值,这个返回值1表示发生了感兴趣事件的监听对象的数量,同时会把相应的监听对象的字段short revents设置为mask的值,供后续的代码判断使用,后续的代码就走下面红框中的分支:
在这里插入图片描述

如果并没有超时,但这个与poll机制相关的线程被某个与自己无关的函数中的wake_up_interruptible()语句给唤醒了,则被唤醒后这个与poll机制相关的线程还还是会去执行语句poll_wait(file, &wait_queue, wait);后面的语句,即下面红框中的语句:
在这里插入图片描述
此时,由于变量data_ready的逻辑值为假,那么返回的mask的值仍然是0,此时poll机制发现不是监听对象的期望事件的值,但由于并未超时,所以并不会退出用户态中的poll函数,并且此poll机制会再一次调用函数poll_demo_poll,当运行到函数poll_demo_poll时,又使线程进入等待队列,即线程又进入睡眠状态。

如果超时了还没数据写入,则线程还是会在poll机制的超时处理函数中被唤醒,超时处理函数中应该也是调用了函数wake_up_interruptible()唤醒队列中的所有线程,与poll机制有关的线程在被唤醒后还是会去执行语句poll_wait(file, &wait_queue, wait);后面的语句【这是我实际测试过的,当超时后,会有信息“I am poll_demo_poll.”打印输出,这说明它后面的语句是被执行了的】,即下面红框中的语句:
在这里插入图片描述
此时,由于变量data_ready的逻辑值为假,那么返回的mask的值仍然是0,此时poll机制发现不是监听对象的期望事件的值,并且又由于已经超时了,所以用户空间中的poll函数退出并返回的值是0,至于此时有没有把监听对象的字段short revents设置为mask的值,这个不清楚,如果后面有需要,可以写个测试程序试验一下,其实就是咱们这个工程中的测试程序稍微改一下就可以测试出来,但这里我实在是不想去测了,何况这并不是个重要的问题。此时后续的代码就走下面红框中的分支:
在这里插入图片描述
至此,整个代码的poll机制部分就分析完了。还需要说明一下的是驱动程序中的读函数:

static ssize_t poll_demo_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
    if (data_len == 0)
        return 0;

    if (count > data_len)
        count = data_len;

    if (copy_to_user(buf, device_buffer, count))
        return -EFAULT;

    data_len = 0;  // 数据被消费
    data_ready = 0; // 重置数据准备标志

    return count;
}
  • 功能:在数据被用户读取后,清空 data_ready,确保事件的状态与实际同步。
  • 关键点
    • data_lendata_ready 被清零,表示数据已被消费,避免错误触发 poll 事件。

至少整个程序的关键流程至此就终于说清楚了。

交叉编译出驱动模块和两个测试程序

代码复制到Ubuntu中…
在这里插入图片描述

make

在这里插入图片描述
复制到NFS网络文件目录中,备用。
在这里插入图片描述

加载驱动模块

打开串口终端→打开开发板→挂载网络文件系统

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod  /mnt/poll_demo/poll_demo_drv.ko

在这里插入图片描述

查看有没有相应的设备文件生成

ls /dev/

有了,如下:
在这里插入图片描述

运行测试程序

poll等待时间为无限的情况

首先我们让读程序的poll()函数的第三个时间参数为-1,即进入休眠状态的时间为无限,运行程序时不加时间参数那么poll()函数的第三个时间参数便为-1
运行下面的命令:

cd /mnt/poll_demo/
./poll_read_01 -1

在这里插入图片描述

确认程序能正常运行后,由于我们还需要运行程序poll_write_01,所以我们需要用“CTRL+C”终止掉它,然后在后台运行:

./poll_read_01 -1 &

在这里插入图片描述
然后我们运行程序poll_write_01,向运行于内核空间的驱动程序中的存储区device_buffer写入数据。

./poll_write_01 Suwenhao_Says_HelloWorld!

在这里插入图片描述
在这里插入图片描述
可见,实现了我们的需求,即主线程利用poll进入无限时间的休眠状态,等待数据写入,当有数据写入后,被唤醒,然后读出了写入的数据。并且在主线程进入休眠状时,线程print_thread仍然在运行中,并且每隔20秒打印输出字符串I am another thread, and while the main thread is waiting for poll events, I can still run normally.

按照我写的程序,在主程序中,读完一次数据后,又会循环去运行读数据部分的代码,所以我们再写一次数据应该还能读出,所以我们再利用写程序写条数据:

./poll_write_01 Suwenhao_loves_WangHong

在这里插入图片描述在这里插入图片描述
可见,又一次被唤醒并且读出了写入的数据。

poll等待时间为有限时间的情况

设置等待时间为8秒,如果超时,则会打印输出信息Timeout occurred! No data available within %d milliseconds.\n
在这里插入图片描述

./poll_read_01 8000

运行如果如下图所示:

在这里插入图片描述

接下来,我们先“CTRL+C”结束它,然后后台运行,并在等待时间内向运行于内核空间的驱动程序中的存储区device_buffer写入数据。

后台运行读程序:

./poll_read_01 8000 &

超时前向运行于内核空间的驱动程序中的存储区device_buffer写入数据:

./poll_write_01 Suwenhao_Says_HelloWorld!

在这里插入图片描述
可见,达到了我们想要的结果!

再写一次数据:

./poll_write_01 Suwenhao_loves_WangHong

在这里插入图片描述

完美~

结束运行于后台中的读程序

ps

在这里插入图片描述

kill 7047

在这里插入图片描述

卸载驱动模块

rmmod poll_demo_drv

在这里插入图片描述

查看下设备文件还有没有

ls /dev/

在这里插入图片描述
可见 文件/dev/poll_demo_0已经不在了。

查看下设备类还在不在

ls /sys/class/

在这里插入图片描述
可见,名字为swh_poll_demo_class的设备类目录已经不在了。

看下驱动程序poll_demo_major还在不在

cat /proc/devices

在这里插入图片描述
可见major号为245,名字为“poll_demo_major”的驱动程序已经不在了。

附完整工程文件

https://pan.baidu.com/s/1CeYq4YErjhwJS3diIAQzBA?pwd=3kwh

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值