嵌入式Linux驱动开发(五)——poll机制原理以及驱动实现

0.198字数 1823阅读 776

之前的文章里面说到了关于使用两种方式来获取不同的按键值,具体的文章可以参看以下文章:
嵌入式Linux驱动开发(三)——字符设备驱动之查询的方式获取按键值
嵌入式Linux驱动开发(四)——字符设备驱动之中断方式以及中断方式获取按键值

前情回顾:

再开始今天的内容之前,先简单review一下,我们都用了什么方案来获取按键值,他们的特点都是什么。只有不断地理清了思路,我们才能够更好的理解,为何会出现如此多的解决方案,当遇到问题的时候,才可以对症下药。

第一种方案,也是最粗暴的一种,在应用层通过一个死循环的read函数,来不断地查看底层read函数,在底层我们并未做阻塞,每次应用层read,底层都会给上层应用返回值。那么,这个简单粗暴的方式,显然是最不合理的,因为read函数不断地在读取,使得CPU的使用率被死循环占用了99%,这基本上是灾难式的解决方案。虽然简单,但是代价也是显而易见的。

为了降低CPU的使用率,那么就要避免使用死循环不断地来从底层获取信息,那么,也就产生了两种解决思路,也分别就是第二篇文章中的两种解决方案了。

第二种方案,直接舍弃了上层应用read的操作,全部交给底层,通过中断来实现。(request_irqfree_irq函数来注册和卸载中断)只要按键按下,进入中断模式,调用底层的中断处理函数,来将按键信息进行打印。虽然上层应用没有什么影响,但是,这样的方案也并不是我们想要的,因为,底层相对稳定的部分,参与了他不该参与的部分——具体中断如何解决。这样其实也是灾难性的,难道因为上层应用程序的需求发生了改变,还需要修改底层的驱动程序吗?!这显然要把从用户到应用层开发再到底层应用开发人员给烦死。但是这个方案虽然不能解决需求,但它也并不是一无是处的,正是因为他的存在,才出现了第三种解决的方案。

第三种方案,实际从底层解决思路的角度来看,是通过睡眠的方式解决的,从上层来看是通过阻塞来解决的。也就,上层应用通过调用了阻塞式的read函数(这也是大部分 read 函数的使用场景),如果没有数据返回来,就让 read 阻塞在这里,上层应用可以放心的死循环来调用 read, 不必担心它会不停地调用底层而耗费大量资源。对于底层如何来实现阻塞式的 read 呢?其实也很简单是通过睡眠的方式来实现的。
static DECLARE_WAIT_QUEUE_HEAD(wait_queue);
wake_up_interruptible(&wait_queue);
wait_event_interruptible(wait_queue, condition);
通过以上的函数,来实现底层应用的睡眠功能,结合第二种方案,在上层 read 的时候直接让驱动睡眠,而不是返回。通过注册中断处理函数,在其中,来唤醒并通过copytouser函数来把数据返回到上层应用的 read 函数中,从而实现了上层的阻塞功能。不但可以检测到中断的具体发生情况,同时也避免了 CPU 的高使用率的发生。

通过以上一系列的方法,初步实现了获取按键值的需求,但是,还不能够满足我们的所有需求,比如,最后的解决方案是将 read 阻塞在了那里,这不一定能满足我们所有的需求,因此,也诞生了以下的方案——poll机制。

延续之前文章中提到的最后一种方案,一直阻塞在那里可能会限制我们的需求,那么,是否可以通过一种方案,让他不要一直阻塞,而是阻塞一段时间,如果没有变化就先让它返回,不要一直阻塞在那里。

对于Linux应用开发来说,poll 机制的使用和 open、read函数的使用大同小异,只需要调用 poll 函数即可。关于这个函数的具体使用方法,可以通过man poll来查看具体的函数使用方法。
poll 函数的原型如下:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

根据man手册的信息,可以在应用层实现 poll 函数和 read 函数的调用。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <unistd.h>


int main()
{
        //打开设备文件
    int fd = open("/dev/poll_eint", O_RDWR);    
    if(fd < 0)
    {
        printf("open error\n");
        return -1;
    }

#if 0
/*
    poll 函数所接收的第一个参数,该结构体的内容
*/
    struct pollfd {
        int   fd;         /* file descriptor 文件描述符*/
        short events;     /* requested events 输入参数*/
        short revents;    /* returned events 输出参数,事件发生后由内核来修改*/
    };
    
#endif

        //创建 struct polld 结构体,并为其中的成员赋值
    struct pollfd fds[1];
    fds[0].fd = fd;                //指定文件描述
    fds[0].events = POLLIN;//有数据可以读的时候返回


    int ret = 0;            //poll 函数的返回值,依据返回值判断是否有数据可读
    unsigned char value = 0; //读取的按键值
    
        //循环查询
        while(1)  
    {
    //  int poll(struct pollfd *fds, nfds_t nfds, int timeout); 
    /*  
            fds:struct pollfd 数组,其中管理了文件描述以及输入参数
            ndfs: 需要通过 poll 机制管理的文件描述符数量
            timeout:超时时间,单位为毫秒
        */
                //调用 poll 函数
        ret = poll(fds, 1, 5000);

                //如果返回值为0,打印超时
        if(!ret)
                        printf("timeout.....%d\n\n", ret);
        else{
                        //返回值不为0,说明有数据可以读取,通过 read 来读取数据并打印
            read(fd, &value, 1);
            printf("value: %x\t ret: %d\n", value, ret);
        }
    }
    
    return 0;
}

以上就是一个简单的通过 poll 查询的应用程序示例代码。但是,Linux 系统是如何来实现 poll 函数的,驱动程序如何来写是接下来要讨论的话题。

和 open 一样,上层调用 open 函数,进入内核空间,调用 sys_open 系统调用,在逐层向下调用,一直到调用到驱动程序中 file_operations 中注册的 open函数为止。poll 函数也同样,根据内核源代码,具体 poll 函数的调用原理如下。

  • poll机制的调用原理
应用层通过调用: poll函数
    进入到内核空间的系统调用: sys_poll(位于/linux/sys_poll.h 文件中)
      do_sys_poll(...., timeout_jiffies)
        poll_initwait(&table)
          init_poll_funcptr(&pwq->pt, __pollwait)
              pt->qproc = qproc  //相当于table->qproc = __pollwait
        do_poll(nfds, head, &table, timeout)
          for(;;)  //死循环
            if(do_pollfd(pfd, pt))
              //do_pollfd中,调用mask = file->f_op->poll(file, pwait);  return mask; 
              //实际此时就会调用到驱动程序中的 poll 函数
                  p->qproc(filp, wait_address, p);//驱动程序中的 poll 函数,需要调用 poll_wait 函数,poll_wait 函数中执行了这个条语句
                    __pollwait()//把当前进程挂到队列中去管理,并不休眠
              count++;  //如果驱动的poll返回非0值,那么count++
              pt = NULL;  
            if(count || !*timeout || signal_pending(current))
              break; //break的三个条件:count非0;超时;有信号等待处理
             //如果条件不成立,会进入到休眠状态
            //驱动的poll:p->qproc(filp, wait_address, p) 把当钱进程挂到wait_address队列中去
            __timeout = schedule_timeout(__timeout)  //休眠__timeout的时间

根据以上的调用路线可以看到其中有一句f_op->poll(....),可以看出这个句话实际是从之前我们很熟悉的struct file_operations中取出了poll的函数指针,并且添加到一个超时队列中来进行调用管理的。
那么,我们只需要看看具体的 poll 函数原型是怎么样的也就可以来写poll 机制的驱动了。

struct file_operations {
//  .........
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
//.........
};

驱动程序使用 poll 机制以及中断机制来获取按键值

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/uaccess.h>
#include <linux/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <asm/irq.h>
#include <linux/poll.h>

#define EINT_PIN_COUNT 4

static const char* dev_name = "poll_eint";
static volatile unsigned int major = 0;

static struct class* poll_class;
static struct class_device* poll_class_device;

struct pin_desc
{
    unsigned int pin;
    unsigned int value;
};

//%kernel%\include\asm-arm\arch\irqs.h
//#define IRQ_EINT0      S3C2410_IRQ(0)     /* 16 */
//中断号数组
static const int eints[EINT_PIN_COUNT] =
{
    IRQ_EINT0,
    IRQ_EINT2,
    IRQ_EINT11,
    IRQ_EINT19
};

static struct pin_desc pins[4] =
{
    {S3C2410_GPF0,  0x1},
    {S3C2410_GPF2,  0x2},
    {S3C2410_GPG3,  0x3},
    {S3C2410_GPG11, 0x4},
};

unsigned int status = 0;
unsigned int condition = 0;
unsigned char value = 0;  
static DECLARE_WAIT_QUEUE_HEAD(wait_queue); //初始化中断等待序列
//中断处理函数
static irqreturn_t irq_handler(int irq, void *dev_id)
{
        //获取按键值,如果按下则与0x80
    struct pin_desc* desc = (struct pin_desc*) dev_id;
    status = s3c2410_gpio_getpin(desc->pin);
    if(status)
        value = desc->value | 0x80;
    else
        value = desc->value;
    
        //从睡眠队列中唤醒
    wake_up_interruptible(&wait_queue);
    condition = 1;
    
    return 0;
}


static ssize_t poll_read (struct file *file, char __user *buff, size_t size, loff_t *ppos)
{
    //read 的时候直接进入睡眠状态,有中断发生的时候在中断处理函数中唤醒
    printk(".........read\n\n");
    wait_event_interruptible(wait_queue, condition);
    condition = 0;
    
     //唤醒后,讲数据发送到用户空间
    printk("read.........\n\n");
    copy_to_user(buff, &value, 1);
    
    return 0;
}

static unsigned int poll_poll (struct file *file, struct poll_table_struct *pts)
{
    unsigned int mask = 0;

    printk("poll<<<<<<<<<<<<<<\n\n");
//  static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
    poll_wait(file, &wait_queue, pts);  //将需要管理的文件描述符、休眠的队列告诉内核,内核会将该进程放到管理队列中,但并不会在这里进行休眠
    
    printk("poll>>>>>>>>>>>>>>\n\n");
    
    
        //如果按键被按下,返回非零值,以便应用层判断poll 返回值
    if(condition)
        mask |= POLLIN | POLLRDNORM;    


    return mask;
}

static int poll_open (struct inode *inode, struct file *file)
{
    //  在 open 的时候注册按键中断
    int i;
    for(i = 0; i < EINT_PIN_COUNT; ++i){
        request_irq(eints[i], irq_handler, IRQT_BOTHEDGE, dev_name, &pins[i]);
    }
    printk("interrupt register\n");

    return 0;
}

static int poll_release (struct inode *inode, struct file *file)
{
    //释放之前注册的中断
    //void free_irq(unsigned int irq, void *dev_id)
    int i = 0;
    for(;i < EINT_PIN_COUNT; ++i){
        free_irq(eints[i], &pins[i]);
    }
    
    printk("button released\n");
    return 0;
}

struct file_operations poll_fops = 
{
    .owner = THIS_MODULE,
    .open = poll_open,
    .read = poll_read,
    .poll = poll_poll,
    .release = poll_release,
};

static int __init poll_init(void)
{
    major = register_chrdev(major, dev_name, &poll_fops);
    poll_class = class_create(THIS_MODULE, dev_name);
    poll_class_device = class_device_create(poll_class, NULL, MKDEV(major, 0), NULL, dev_name);
    printk("init\n");
    return 0;
}

static void __exit poll_exit(void)
{
    unregister_chrdev(major, dev_name);
    class_device_unregister(poll_class_device);
    class_destroy(poll_class);
    printk("exit\n");
}

module_init(poll_init);
module_exit(poll_exit);

MODULE_AUTHOR("Ethan Lee <4128127@qq.com>");
MODULE_LICENSE("GPL");

通过以上的代码,就可以实现通过 poll 机制和中断的方式来实现不完全阻塞的状态来查询按键值了。
那么以上的方法实际都是通过应用层查询的方式来实现了具体的按键值获取任务,是不是还有其他的方式来获取按键值呢?
假设把应用程序主动查看的方式比作一个人通过不断地打电话的方式来询问快递是否送达,那么,是不是可以让驱动程序来给应用程序打电话,通知他状态发生了变化呢?
这个当然是可以的,肯定需要用到进程间的通讯方式。
具体的实现过程,在下一篇文章里面我们来探讨吧。

推荐阅读更多精彩内容