程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

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);
}
复制代码

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
2023-09-06*源19
2023-09-11*朝科88
2023-09-21*号5
2023-09-16*真60
2023-10-26*通9.9
2023-11-04*慎0.66
2023-11-24*恩0.01
2023-12-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
2024-09-06*强8.8
2024-09-09*波1
2024-09-10*口1
2024-09-10*波1
2024-09-12*波10
2024-09-18*明1.68
2024-09-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(624)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示