程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

linux驱动移植-IO多路复用模型(poll机制)

一、Linux IO模型

1.1 按键测试程序存在的问题

上一小节写到的中断方式获取按键值时,应用程序不停的查询是否有按键发生改变,大部分时间程序都处在read休眠的那个位置。

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc,char **argv)
{
    int fd,ret;
    unsigned int key_val = 0;
    
    fd = open("/dev/buttons", O_RDWR);
    if (fd < 0)
    {
        printf("can't open!\n");
        return -1;
    }
 
    while (1)
    {
        ret = read(fd, &key_val, 1);    // 读取一个字节值,(当在等待队列时,本进程就会进入休眠状态)   只有按键按下或者松开,才会返回
        if(ret < 0){
            printf("read error\n");
            continue;
        }
        printf("key_val = 0x%x\n", key_val);
    }
    
    return 0;
}

实际上这是一个同步IO操作,因为一个read操作就阻塞了当前线程,导致其他代码无法执行。解决这个问题有若干种办法:

  • 异步IO操作:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知调用者。
  • 采用多线程解决并发的问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降;

1.2 IO模型

Linux下有五种IO模型:

  • 阻塞IO;
  • 非阻塞IO;
  • 多路复用IO;
  • 信号驱动IO;
  • 异步IO;

前四种都是同步IO,只有最后一种是异步IO。

Linux为了OS的安全性等的考虑,进程是无法直接操作IO设备的,其必须通过系统调用请求内核来协助完成IO动作,而内核会为每个IO设备维护一个buffer。
对于一个设备IO ,这里我们以read举例,它会涉及到两个系统对象,一个是调用这个IO的进程或线程(process or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:

  • 等待设备数据准备就绪阶段:用户进程发起请求,内核接收到请求,从IO设备中获取数据到buffer,等待数据准备 (Waiting for the data to be ready);
  • 将设备数据从内核空间拷贝到用户空间阶段:将buffer中的数据copy到用户进程的地址空间,即将数据从内核拷贝到用户进程中 (Copying the data from the kernel to the process);

在异步IO模型中,当用户进程发起系统调用后,立刻就可以开始去做其它的事情,然后直到IO执行的两个阶段都完成之后,内核会给用户进程发送通知,告诉用户进程操作已经完成了。

异步IO的读操作是通过aio_read实现的,具体可以参考linux下aio异步读写详解与实例

关于这五种IO模型的具体区别可以查看博客:Linux IO模型介绍以及同步异步阻塞非阻塞的区别。这里我们就简单的概述一下:

  • 异步IO和同步IO的主要区别在于IO操作的第二阶段,同步IO用户进程会发生堵塞,而异步IO用户进程不会发生堵塞;
  • 阻塞IO和非阻塞IO主要就在于当设备没有数据时,我们调用read函数是立即返回还是处于睡眠状态;

1.3 同步IO

实际上同步IO操作包含了多种IO模型,我们依然以按键测试应用程序中调用read函数作为例子进行讲解。

(1) 阻塞IO模型

也就是我们上面这个例子,调用read函数线程一直处于阻塞状态,一直等到有按键变化,才会将数据从内核拷贝到用户空间。

(2) 非阻塞IO模型

如果我们在open函数打开/dev/buttons设备时,指定了O_NONBLOCK标志,read函数就不会阻塞。如果没有按键发生改变,就会立即返回-1。

我们采用轮询的方式去调用read函数,类似下面的伪代码:

while(1) 
{ 

    ret1 = read(设备1); 

    if(ret1 > 0) 

       处理数据; 

    ret2 = read(设备2); 

    if(ret2 > 0) 

       处理数据; 

    ..............................

}
    

采用这种方式,调用者只是查询一下,并不会阻塞在这里,这样我们可以同时监控多个设备。上面的代码也会存在另一个问题,线程会在不停的轮询,会导致CPU使用率急剧升高。

因此我们可以在循环的最后加入一定时长的睡眠,但是这么做又会有另一个问题,如果设备有数据到达由于睡眠可能导致数据处理不及时。因此又衍生了IO多路复用模型解决这个问题。

(3) IO多路复用模型;

IO多路复用就是通过一种机制,一个进程/线程可以监视多个设备,一旦某个设备就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

在linux操作系统中,目前支持IO多路复用的系统调用有select、pselect、poll、epoll。

在调用read之前先调用select/poll/epoll 等函数,它们可以阻塞地同时监视多个设备,还可以设定阻塞等待的超时时间,并且当内核准备好数据的时候会通知调用者,这时候再去调用read读取数据。

 

(4) 信号驱动IO模型

这个下一篇博客单独介绍。

1.4 改造目标

这一节我们将利用IO多路复用中的poll函数,对按键驱动程序进行改造,达到如下目标:

  • 当有按键改变时,我们再去调用read函数,否则进程就阻塞(通过poll函数设置等待超时时间);

二、linux poll机制分析

当应用程序调用poll函数的时候,会通过swi软件中断进入到内核层,然后调用sys_poll系统调用。

2.1 poll

poll函数原型如下:

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

参数如下:

  •  *fds:是一个poll文件描述符结构体数组(可以处理多个poll),结构体pollfd如下,其中events和revents值参数如下;
 struct pollfd {
     int   fd;         /* file descriptor 文件描述符*/
     short events;     /* requested events 请求的事件*/
     short revents;    /* returned events 返回的事件(函数返回值)*/
};

常量

说明

POLLIN

普通或优先级带数据可读

POLLRDNORM

normal普通数据可读

POLLRDBAND

优先级带数据可读

POLLPRI

Priority高优先级数据可读

POLLOUT

普通数据可写

POLLWRNORM

normal普通数据可写

POLLWRBAND

band优先级带数据可写

POLLERR

发生错误

POLLHUP

发生挂起

POLLNVAL

描述字不是一个打开的文件

  • nfds:表示多少个fd,如果1个,就填入1;
  • timeout:超时时间,单位ms;

返回值:

  • 0:表示超时或者fd文件描述符无法打开;
  • -1:表示错误;
  • >0时 :就是上面表格中几个常量;

2.2 sys_poll

我们在fs/select.c文件中,找到sys_poll函数原型:

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
                int, timeout_msecs)
{
        struct timespec64 end_time, *to = NULL;
        int ret;

        if (timeout_msecs >= 0) {
                to = &end_time;
                poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
                        NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
        }

        ret = do_sys_poll(ufds, nfds, to);

        if (ret == -EINTR) {
                struct restart_block *restart_block;

                restart_block = &current->restart_block;
                restart_block->fn = do_restart_poll;
                restart_block->poll.ufds = ufds;
                restart_block->poll.nfds = nfds;

                if (timeout_msecs >= 0) {
                        restart_block->poll.tv_sec = end_time.tv_sec;
                        restart_block->poll.tv_nsec = end_time.tv_nsec;
                        restart_block->poll.has_timeout = 1;
                } else
                        restart_block->poll.has_timeout = 0;

                ret = -ERESTART_RESTARTBLOCK;
        }
        return ret;
}

这里sys_poll函数声明都是使用了宏SYSCALL_DEFINE3,如何具体展开的可以参考Linux系统调用之SYSCALL_DEFINE。这个函数有三个参数:

  • struct pollfd __user *  ufds:poll函数传进来的;
  • unsigned int nfds:poll函数传进来的;
  • int timeout_msecs:poll函数传进来的;

接下来,我们分析该函数的执行流程:

  • 首先,如果设定了超时时间不为0,会调用 poll_select_set_timeout 函数将超时时间转换为 timespec64 结构变量,注意超时时间将会以当前时间(monotonic clock)为基础,转换为未来的一个超时时间点(绝对时间);
  • 然后调用了do_sys_poll,这个函数很重要;
  • 最后对返回结果进行校验;

2.3 do_sys_poll

do_sys_poll它也位于fs\Select.c:

static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
                struct timespec64 *end_time)
{
        struct poll_wqueues table;
        int err = -EFAULT, fdcount, len, size;
        /* Allocate small arguments on the stack to save memory and be
           faster - use long to make sure the buffer is aligned properly
           on 64 bit archs to avoid unaligned access */
        long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
        struct poll_list *const head = (struct poll_list *)stack_pps;
        struct poll_list *walk = head;
        unsigned long todo = nfds;

        if (nfds > rlimit(RLIMIT_NOFILE))
                return -EINVAL;

        len = min_t(unsigned int, nfds, N_STACK_PPS);
        for (;;) {
                walk->next = NULL;
                walk->len = len;
                if (!len)
                        break;

                if (copy_from_user(walk->entries, ufds + nfds-todo,
                                        sizeof(struct pollfd) * walk->len))
                        goto out_fds;

                todo -= walk->len;
                if (!todo)
                        break;

                len = min(todo, POLLFD_PER_PAGE);
                size = sizeof(struct poll_list) + sizeof(struct pollfd) * len;
                walk = walk->next = kmalloc(size, GFP_KERNEL);
                if (!walk) {
                        err = -ENOMEM;
                        goto out_fds;
                }
        }

        poll_initwait(&table);
        fdcount = do_poll(head, &table, end_time);
        poll_freewait(&table);

        for (walk = head; walk; walk = walk->next) {
                struct pollfd *fds = walk->entries;
                int j;

                for (j = 0; j < walk->len; j++, ufds++)
                        if (__put_user(fds[j].revents, &ufds->revents))
                                goto out_fds;
        }

        err = fdcount;
out_fds:
        walk = head->next;
        while (walk) {
                struct poll_list *pos = walk;
                walk = walk->next;
                kfree(pos);
        }

        return err;
}
View Code

该函数主要做了以下事情:

  • 在内核栈分配空间,通过poll_list链表保存ufds(struct pollfd类型数组);
  • 进入for(;;):
    • 将pollfd从用户空间拷贝到内核空间;
  • 调用poll_initwait;
  • 调用do_poll完成poll的实际调用处理;
  • 将每个fd上产生的事件revents再从内核空间拷贝到用户空间;

从图中可以看到这里将ufds数组中的poll文件描述符拆分存放在poll_list连表中。链表每一个元素存放len成员指定个数个poll文件描述符。

2.4 poll_initwait

poll_initwait(&table) 对poll_wqueues 结构体变量table进行初始化:table->pt->qproc = __pollwait:

void poll_initwait(struct poll_wqueues *pwq)
{
        init_poll_funcptr(&pwq->pt, __pollwait);
        pwq->polling_task = current;
        pwq->triggered = 0;
        pwq->error = 0;
        pwq->table = NULL;
        pwq->inline_index = 0;
}

其中struct poll_wqueues结构如下:

/*
 * Structures and helpers for select/poll syscall
 */
struct poll_wqueues {
        poll_table pt;
        struct poll_table_page *table;
        struct task_struct *polling_task;
        int triggered;
        int error;
        int inline_index;
        struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};

函数指针 table->pt->_qproc 被初始化指向 __pollwait 函数,这个和 poll 调用过程中阻塞与唤醒机制相关,后面将介绍。

2.5 do_poll

do_sys_poll函数在调用完poll_initwait(&table) 之后,随后即调用 do_poll 函数完成 poll 操作,最后将每个文件描述符fd产生的事件再拷贝到内核空间。

static int do_poll(struct poll_list *list, struct poll_wqueues *wait,
                   struct timespec64 *end_time)
{
        poll_table* pt = &wait->pt;
        ktime_t expire, *to = NULL;
        int timed_out = 0, count = 0;
        u64 slack = 0;
        __poll_t busy_flag = net_busy_loop_on() ? POLL_BUSY_LOOP : 0;
        unsigned long busy_start = 0;

        /* Optimise the no-wait case */
        if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
                pt->_qproc = NULL;
                timed_out = 1;
        }

        if (end_time && !timed_out)
                slack = select_estimate_accuracy(end_time);

        for (;;) {
                struct poll_list *walk;
                bool can_busy_loop = false;

                for (walk = list; walk != NULL; walk = walk->next) {
                        struct pollfd * pfd, * pfd_end;

                        pfd = walk->entries;
                        pfd_end = pfd + walk->len;
                        for (; pfd != pfd_end; pfd++) {
                                /*
                                 * Fish for events. If we found one, record it
                                 * and kill poll_table->_qproc, so we don't
                                 * needlessly register any other waiters after
                                 * this. They'll get immediately deregistered
                                 * when we break out and return.
                                 */
                                if (do_pollfd(pfd, pt, &can_busy_loop,
                                              busy_flag)) {
                                        count++;
                                        pt->_qproc = NULL;
                                        /* found something, stop busy polling */
                                        busy_flag = 0;
                                        can_busy_loop = false;
                                }
                        }
                }
                /*
                 * All waiters have already been registered, so don't provide
                 * a poll_table->_qproc to them on the next loop iteration.
                 */
                pt->_qproc = NULL;
                if (!count) {
                        count = wait->error;
                        if (signal_pending(current))
                                count = -EINTR;
                }
                if (count || timed_out)
                        break;

                /* only if found POLL_BUSY_LOOP sockets && not out of time */
                if (can_busy_loop && !need_resched()) {
                        if (!busy_start) {
                                busy_start = busy_loop_current_time();
                                continue;
                        }
                        if (!busy_loop_timeout(busy_start))
                                continue;
                }
                busy_flag = 0;

                /*
                 * If this is the first loop and we have a timeout
                 * given, then we convert to ktime_t and set the to
                 * pointer to the expiry value.
                 */
 if (end_time && !to) {
                        expire = timespec64_to_ktime(*end_time);
                        to = &expire;
                }

                if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
                        timed_out = 1;
        }
        return count;
}
View Code

do_poll函数主要做了以下事情:

  • timeout设置为0时,会将 pt->_qproc 设置为NULL,同时不阻塞,相当于退化为轮询操作;
  • 设置了有效的超时时间后,会设置slack;
  • for(;;):
    • 遍历每一个poll文件描述符:
      • 调用do_pollfd,如果do_pollfd返回非负值,表示发现事件触发,此时无需再将当前进程加入到相应的等待队列;
    •   pt->_qproc = NULL,当前进程已经在上述的遍历中被加入到各个fd对应驱动的等待队列,所以这里直接设置为NULL;
    • 如果发现事件触发,或者time_out=1,提前退出循环;
    • 调用poll_schedule_timeout,使当前poll调用进程进行休眠,让出CPU,超时时间到达时返回,设置timed_out=1,在下一个轮询后返回上层调用;

do_poll 函数首先从头部到尾部遍历链表 poll_list ,对每一项 pollfd 调用 do_pollfd 函数。 do_pollfd 函数主要将当前 poll 调用进程加入到每个 pollfd 对应fd所关联的底层驱动等待队列中。 do_pollfd 调用后,如果某个fd已经产生事件,count将会自增,那么后续遍历其他fd时,无需再将当前进程加入到对应的等待队列中, poll 调用也将返回而不是睡眠(schedule)。

2.6 do_pollfd

do_poll函数在遍历poll文件描述符时,会执行do_pollfd函数:

/*
 * Fish for pollable events on the pollfd->fd file descriptor. We're only
 * interested in events matching the pollfd->events mask, and the result
 * matching that mask is both recorded in pollfd->revents and returned. The
 * pwait poll_table will be used by the fd-provided poll handler for waiting,
 * if pwait->_qproc is non-NULL.
 */
static inline __poll_t do_pollfd(struct pollfd *pollfd, poll_table *pwait,
                                     bool *can_busy_poll,
                                     __poll_t busy_flag)
{
        int fd = pollfd->fd;
        __poll_t mask = 0, filter;
        struct fd f;

        if (fd < 0)
                goto out;
        mask = EPOLLNVAL;
        f = fdget(fd);
        if (!f.file)
                goto out;

        /* userland u16 ->events contains POLL... bitmap */
        filter = demangle_poll(pollfd->events) | EPOLLERR | EPOLLHUP;
        pwait->_key = filter | busy_flag;
        mask = vfs_poll(f.file, pwait);
        if (mask & busy_flag)
                *can_busy_poll = true;
        mask &= filter;         /* Mask out unneeded events. */
        fdput(f);

out:
        /* ... and so does ->revents */
        pollfd->revents = mangle_poll(mask);
        return mask;
}

do_pollfd 主要完成与底层VFS中的驱动程序 file->f_op->poll(file,pwait),这就跟驱动扯上关系了, __pollwait在这里就被用到了。

static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
{
        if (unlikely(!file->f_op->poll))
                return DEFAULT_POLLMASK;
        return file->f_op->poll(file, pt);
}

仍然以我们的按键驱动为例。我们会编写button_poll函数(后面会介绍):

  • 调用 poll_wait(file, &button_waitq, pt)将poll调用进程加入到设备自定义的等待队列button_waitq中;
  • 当有按键发生变化时,就触发POLLIN事件,否者就返回0;

然后调用mangle_poll过滤出每个文件描述符感兴趣的事件,最后会把过滤出的事件放入pollfd->revents 中,作为结果返回,如果没有文件描述符fd感兴趣的事件则返回的值为0。

2.7 _pollwait

在button_poll驱动程序中,我们调用poll_wait:

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);
}

poll_wait 进而调用到 poll_table p->_qproc ,而后者通过 poll_initwait(&table) 被初始化为 __pollwait ,参数wait_address为我们按键驱动程序中声明的等待队列button_waitq。

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                                poll_table *p)
{
        struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt); 
        struct poll_table_entry *entry = poll_get_entry(pwq);
        if (!entry)
                return;
        entry->filp = get_file(filp);
        entry->wait_address = wait_address;
        entry->key = p->_key;
        init_waitqueue_func_entry(&entry->wait, pollwake);
        entry->wait.private = pwq;
        add_wait_queue(wait_address, &entry->wait);
}

将当前poll调用进程添加到button_waitq等待队列中,一旦有按键发生变化,就会唤醒等待队列中的所有进程,从而唤醒poll机制。

2.8 poll_schedule_timeout

在该函数中首先会设置当前进程状态为TASK_INTERRUPTIBLE,在该状态下,进程如果休眠的话可以被信号和wake_up唤醒。

static int poll_schedule_timeout(struct poll_wqueues *pwq, int state,
                          ktime_t *expires, unsigned long slack)
{
        int rc = -EINTR;

        set_current_state(state);
        if (!pwq->triggered)
                rc = schedule_hrtimeout_range(expires, slack, HRTIMER_MODE_ABS);
        __set_current_state(TASK_RUNNING);

        /*
         * Prepare for the next iteration.
         *
         * The following smp_store_mb() serves two purposes.  First, it's
         * the counterpart rmb of the wmb in pollwake() such that data
         * written before wake up is always visible after wake up.
         * Second, the full barrier guarantees that triggered clearing
         * doesn't pass event check of the next iteration.  Note that
         * this problem doesn't exist for the first iteration as
         * add_wait_queue() has full barrier semantics.
         */
        smp_store_mb(pwq->triggered, 0);

        return rc;
}

do_poll最后调用poll_schedule_timeout,让本进程休眠一段时间,注意应用程序执行poll调用后,如果timeout没超时或者count为0则进程会进入休眠。那么谁会唤醒进程呢?

  • 休眠指定的超时时间到了;
  • 驱动程序条件就绪时,就会把button_waits队列上挂着的进程唤醒;

2.9 总结

poll 系统调用的整体过程可以概括为下图:

 三、按键驱动-poll改造

3.1 button_poll

在上一级驱动程序里添加如下代码:

#include <linux/poll.h>                //添加头文件


static unsigned int button_poll(struct file *file, poll_table *wait)
{
    unsigned int ret = 0;

    // 将当前进程放到button_waitq列表
    poll_wait(file, &button_waitq, wait);

    /* 中断发生了,即按键发生改变 */
    if(ev_press)
        ret |= POLLIN;

    return ret;
}

static struct file_operations button_fops = {
    .owner   =   THIS_MODULE,
    .open    =   button_open,
    .read    =   button_read,
    .release =   button_close,
    .poll    =   button_poll,
};

我们将当前进程加入了button_waitq等待队列中了。这样当按键中断发生时,wake_up_interruptible会唤醒等待队列中的所有进程,从而唤醒当前进程。

当没有按键发生改变时返回0,当有按键发生改变,返回POLLIN,其中参数意义之前已经介绍过。

3.2 修改button_read

static ssize_t button_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    int count;
    if (size != 1){
        printk("read error\n");
        return -EINVAL;
    }

    /* 如果没有按键动作, 休眠 */
    // wait_event_interruptible(button_waitq, ev_press);

    /* 如果有按键动作, 上传key_val给用户层 */
    count = copy_to_user(buf, &key_val, 1);

    /* 数据发完后,立马设为休眠状态,避免误操作 */
    ev_press = 0;   
    
    return count;
}

这里屏蔽了wait_event_interruptible函数的调用,这个函数本质也是条件参数没有满足时,会进行休眠状态,并把当前进程加入到button_waitq等待队列中。我们已经通过poll机制实现了这个功能,所以这里就不需要了。

3.3 修改测试应用程序

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

int main(int argc,char **argv)
{
    int fd,ret;
    unsigned int key_val = 0;
    struct pollfd key_fds;
    
    fd = open("/dev/buttons", O_RDWR);
    if (fd < 0)
    {
        printf("can't open!\n");
        return -1;
    }

    key_fds.fd = fd;
    key_fds.events = POLLIN;   // poll直接返回需要的条件
 
    while (1)
    {
        /* 调用sys_poll系统调用,如果5S内没有产生POLLIN事件,那么返回,如果有POLLIN事件,直接返回 */
        ret = poll(&key_fds, 1, 5000);
        if(!ret)          // 超时
        {
            printf("time out\n");
         }
        else // poll机制被唤醒,表示有数据可读
        {        
            ret = read(fd, &key_val, 1);    //读取按键值
            if(ret < 0){
                printf("read error\n");
                continue;
            }
            printf("key_val = 0x%x\n", key_val);

        }
    }
    
    return 0;
}

3.4 下载到开发板测试

按照上一节的方式安装驱动,测试应用程序。效果如下:

[root@zy:/]# ./main
time out
time out
time out
key_val = 0x1
key_val = 0x1
key_val = 0x1
key_val = 0x1
key_val = 0x1
key_val = 0x1
key_val = 0x81
key_val = 0x1
key_val = 0x1
key_val = 0x1
key_val = 0x81
time out
key_val = 0x3
key_val = 0x3
key_val = 0x83

若5S没有数据,则打印time out。

3.4  程序整体执行流程

  • 当执行应用程序时,首先打开/dev/buttons设备;
  • 接着进入死循环调用poll(fds, 1, 5000),系统调用sys_poll最后调用到do_poll函数(死循环函数)里陷入休眠(休眠前先执行了一次驱动里的button_poll函数);
  • 当有按键按下时,调用wake_up_interruptible唤醒当前进程;
  • 重新循环执行do_poll函数第一个if函数的判断语句,此时button_poll函数返回非0值,执行count++;再往下执行第二个if语句break退出循环;
  • 将pollfd从内核空间拷贝到用户空间,sys_poll系统调用返回count;
  • 此时再调用read将键值读出来;
  • 当5秒内没有操作按键时,也会退出poll(fds, 1, 5000),打印time out;

四、代码下载

Young / s3c2440_project[drivers]

参考文章:

[1]8.中断按键驱动程序之poll机制(详解)

[2]五、Linux驱动之poll机制

[3]Linux驱动之poll机制的理解与简单使用

[4]select、poll、epoll之间的区别总结[整理]

[5]网络通信 --> IO多路复用之select、poll、epoll详解

[6]Linux内核poll/select机制简析

posted @ 2022-02-15 23:47  大奥特曼打小怪兽  阅读(545)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步