前序
rt-thread其实很多代码都是参考Linux的架构,或者运用的是Linux的设计思想,poll系统调用是经
常使用到的系统调用,本次博客来分析poll在rtt(以下没有特殊说明,就把rt-thread简称为rtt)中的实
现原理,如果了解了poll在rtt中具体的原理和实现过程,那么在分析Linux中的poll将有莫大的帮助。
一、应用程序如何调用poll函数?
poll函数的原型
struct pollfd
{
int fd; /* 1.1 文件描述符 */
short events; /* 1.2 文件关心的events */
short revents; /* 1.3 poll调用的时候产生的events*/
};
/* 返回值:返回已经满足条件的fd的个数
** 参数: nfds 要监控文件的个数
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
通过判断参数中revents成员变量就知道,poll的时候是否有满足要求的events产生。
二、相关函数分析
(1)分析poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
{
int num;
struct rt_poll_table table; /* 1.1 这里定义来一个 rt_poll_table类型的局部变量 */
poll_table_init(&table); /* 1.2 这里初始化定义的table这个局部变量 */
num = poll_do(fds, nfds, &table, timeout); /* 1.3 调用poll_do函数 */
poll_teardown(&table); /* 1.4 释放table */
return num;
}
从poll的调用函数中,我们可以看到,这个函数总共分了四个步骤,注释1.1的地方是定义一个局部变
量table,注释1.2的地方是初始化这个table变量,注释1.3的地方是很关键的地方,注释1.4的地方是
把table变量做一些“释放”的动作,具体的都在下面分析。
(2)poll_table_init函数在初始化变量的时候,到底对table这个结构体做了什么?
(2.1)struct rt_poll_table结构体定义。
/* rt_pollreq_t结构体定义 */
typedef struct rt_pollreq
{
poll_queue_proc _proc; /* 1.1 处理队列的函数 */
short _key; /* 1.2 */
} rt_pollreq_t;
struct rt_poll_table
{
rt_pollreq_t req; /* 2.1 请求队列 */
rt_uint32_t triggered; /* 2.2 the waited thread whether triggered */
rt_thread_t polling_thread;/*2.3 指向当前线程*/
struct rt_poll_node *nodes;/* 2.4 poll的节点 */
};
(2.2)poll_table_init函数分析。
static void poll_table_init(struct rt_poll_table *pt)
{
pt->req._proc = _poll_add;
pt->triggered = 0;
pt->nodes = RT_NULL;
pt->polling_thread = rt_thread_self();
}
poll_table_init函数把table变量中的请求队列req的处理函数指向到_poll_add函数,
table的polling_thread变量指向当前的线程,也就是指向当前调用poll函数的这个线程。
(3)poll_do函数才是poll实现的关键函数!
(3.1)poll_do的具体函数代码。
static int poll_do(struct pollfd *fds, nfds_t nfds, struct rt_poll_table *pt, int msec)
{
int num;
int istimeout = 0;
int n;
struct pollfd *pf;
if (msec == 0)
{
pt->req._proc = RT_NULL;
istimeout = 1; /* 1.1 从这里可以看出,如果超时时间设置为0,则在下面做一次查询后立即返回 */
}
while (1) /* 1.4 while(1)死循环 */
{
pf = fds;
num = 0;
for (n = 0; n < nfds; n ++)
{
if (do_pollfd(pf, &pt->req)) /* 1.2 如果这里返回非0值,num就会++,下面break返回 */
{
num ++;
pt->req._proc = RT_NULL;
}
pf ++;
}
pt->req._proc = RT_NULL;
if (num || istimeout) /* break返回 */
break;
if (poll_wait_timeout(pt, msec)) /* 1.3 如果超时也将会返回 */
istimeout = 1;
}
return num;
}
(3.2)do_pollfd函数才是核心!
static int do_pollfd(struct pollfd *pollfd, rt_pollreq_t *req)
{
int mask = 0;
int fd;
fd = pollfd->fd;
if (fd >= 0)
{
struct dfs_fd *f = fd_get(fd); /* 2.1 获取文件句柄 */
mask = POLLNVAL; /* POLLNVAL定义一个非0的查询标志位 */
if (f)
{
mask = POLLMASK_DEFAULT;
/* 2.2 如果文件的文件操作函数集中有poll函数就会调用操作集里面的poll函数 */
if (f->fops->poll)
{
req->_key = pollfd->events | POLLERR | POLLHUP;
mask = f->fops->poll(f, req); /* 2.3 调用文件操作集中的poll函数 */
}
/* Mask out unneeded events. */
mask &= pollfd->events | POLLERR | POLLHUP;
fd_put(f);
}
}
pollfd->revents = mask; /* 2.5 把产生的、满足用户需求的events返回 */
return mask; /* 2.4 返回mask值 */
}
do_pollfd函数返回的是mask的值,想要mask返回非0只必须满足下面条件:
(i) 文件的操作集中存在poll函数。
(ii)文件操作集的poll返回非0值。
(iii)文件操作集的poll返回的非0值,必须要有用户设置pollfd->events值中感兴趣的(包括系统异常
的event),也就是用户想要的。
(3.3)poll_wait_timeout函数分析。
static int poll_wait_timeout(struct rt_poll_table *pt, int msec)
{
rt_int32_t timeout;
int ret = 0;
struct rt_thread *thread;
rt_base_t level;
thread = pt->polling_thread;
timeout = rt_tick_from_millisecond(msec);
level = rt_hw_interrupt_disable();
if (timeout != 0 && !pt->triggered)
{
rt_thread_suspend(thread); /* 3.1 挂起当前线程 */
if (timeout > 0)
{
/* 3.2 设置线程中软定时器的超时时间 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer)); /* 3.3启动定时器 */
}
rt_hw_interrupt_enable(level);
rt_schedule(); /* 3.4 系统调度,可以让出CPU,等待软定时器超时 */
level = rt_hw_interrupt_disable();
}
ret = !pt->triggered;
rt_hw_interrupt_enable(level);
return ret;
}
poll_wait_timeout函数启动线程自己的软定时器,那么这个软定时器在哪里初始化?超时函数是哪个?
答:在创建线程的时候,就会初始化这个软定时器,并且把超时函数统一设置为rt_thread_timeout()函数。
此时,注释3.1的地方把线程挂起,注释3.4的地方启动软定时器,等到软定时器超时的时候,就会把线程唤醒,
此时产生超时,从而poll函数又退出了。
等等,问题来了,难道poll系统调用就轮询一次,然后等待超时?这样还不如直接费阻塞read好了,肯定哪里出
差错了/斜眼笑/,起始前面的文件操作集中poll一直没有分析使用到,我们可以大胆猜想一下,文件操作集中
的poll的功能:
(i) 让线程挂到某个队列上面,进行休眠。
(ii)等到数据到来的时候,从某个队列中唤醒此线程。
(iii)poll_do()函数中注释1.4是一个while(1)死循环,poll_wait_timeout()函数调用之后,线程进入了休眠状态,如果此时线程被唤醒,就会从poll_do()的while(1)处继续开始运行,此时再调用文件操作集中的poll函数,此时如果返回的mask值符合用户需求,那么到下面/* break返回 */处,就会跳出返回。
猜想很美好,能否show me some code?下面我们分析一下rtt中网络文件系统中的文件操作集的poll函数。
(3.4)网络文件系统的文件操作集的poll实现。
static void _poll_add(rt_wqueue_t *wq, rt_pollreq_t *req)
{
/* 此处省略若干行代码... */
node->wqn.polling_thread = pt->polling_thread;
node->wqn.wakeup = __wqueue_pollwake; /* 4.1 默认等待队列的唤醒函数,其实没有什么用 */
rt_wqueue_add(wq, &node->wqn); /* 4.2 加入到wq队列中 */
/* 此处省略若干行代码... */
}
rt_inline void rt_poll_add(rt_wqueue_t *wq, rt_pollreq_t *req)
{
if (req && req->_proc && wq)
{
/* 4.3 _poroc()在调用poll_table_init()初始化table的时候指向_poll_add()函数 */
req->_proc(wq, req);
}
}
/* 下面是文件操作集的poll函数 */
static int wiz_poll(struct dfs_fd *file, struct rt_pollreq *req)
{
/* 此处省略若干行代码... */
rt_poll_add(&sock->wait_head, req); /* 4.4 这里就是挂入请求队列 */
/* 此处继续省略若干行代码... */
}
文件操作集中的wiz_poll()函数最终调用到_poll_add()函数,把当前线程加入到一个等待队列中,此时并
不会休眠,只是在的poll_wait_timeout()函数中,让当前线程休眠。
怎么把挂入队列中的线程唤醒?刚刚poll_wait_timeout()函数启动的软定时器怎么办?下面还是以网络文
件系统的文件操作集来说明:
(i)网卡收到数据函数调用过程:
wiz_recv_notice_cb()
wiz_do_event_changes()
rt_wqueue_wakeup() ---->此函数就把刚才挂入到队列中的线程进行唤醒
(ii)在rt_wqueue_wakeup() 中会把线程加入到线程就绪队列中,同时停止定时器,下面说部分代码:
rt_err_t rt_thread_resume(rt_thread_t thread)
{
/* 此处省略若干行代码... */
/* 把休眠的线程从等待链表中移除 */
rt_list_remove(&(thread->tlist));
/* 关闭线程的软定时器 */
rt_timer_stop(&thread->thread_timer);
/* 把休眠的线程加入到就绪链表 */
rt_schedule_insert_thread(thread);
/* 此处省略若干行代码... */
return RT_EOK;
}
void rt_wqueue_wakeup(rt_wqueue_t *queue, void *key)
{
/* 此处省略若干行代码... */
rt_thread_resume(entry->polling_thread);
/* 此处省略若干行代码... */
}
(4)分析poll_teardown函数。
static void poll_teardown(struct rt_poll_table *pt)
{
struct rt_poll_node *node, *next;
next = pt->nodes;
while (next)
{
node = next;
rt_wqueue_remove(&node->wqn); /* 1.1 把table中node占用的资源释放掉 */
next = node->next;
rt_free(node);
}
}
在之前文件操作集中的poll函数最终会调到_poll_add()这个函数来分配队列中节点的资源,
poll_teardown()函数就是把之前分配的资源释放掉。
三、核心思想总结。
(1)为什么要poll?轮询并不是真的在一直消耗CPU去查询,而是如果没有数据,则会休眠,有数据到来的时候,就会唤醒线程。
(2)文件操作集的poll函数并不会让线程休眠,真正的休眠是在poll_wait_timeout()开启软定时器的函数里面。
(3)poll可以同时轮询多个文件,但是只要有一个文件有数据返回,系统调用poll就会返回,应用怎么知道是哪个文件有数据可以使用呢?答案很简单,就是跟进系统调用poll的fds参数的revents成员值来确定有数据可以使用的文件句柄。
四、深入扩展。
明白了poll的作用和原理之后,我们不妨发散思维,继续深入思考以下问题:
(1)一个线程多个地方调用poll,这个怎么处理?
答:只要有一个地方的poll导致线程休眠,其他地方的poll自然不会被运行。
(2)多个线程同时poll相同的一个文件,怎么处理?
答:此时需要分两种情况处理:
(i) 文件只能被一个线程读。这种情况,当一个文件被打开后,其他线程就无法再打开,所以不存在多个线程poll相同一个文件。
(ii)文件可以被多个线程同时读。我们再分析一下队列唤醒函数rt_wqueue_wakeup(),此时,只要是用户
调用poll的时候,其参数与唤醒时候的event上报的相同,就能全部被唤醒。
void rt_wqueue_wakeup(rt_wqueue_t *queue, void *key)
{
/* 此处省略若干代码... */
if (!(rt_list_isempty(queue_list)))
{
for (node = queue_list->next; node != queue_list; node = node->next)
{
entry = rt_list_entry(node, struct rt_wqueue_node, list);/* 1.1 遍历队列 */
/* 1.2 此处指向__wqueue_pollwake()函数,这里的key就是线程感兴趣的event */
if (entry->wakeup(entry, key) == 0)
{
rt_thread_resume(entry->polling_thread);
need_schedule = 1;
rt_wqueue_remove(entry);
break;
}
}
}
/* 此处省略若干代码... */
}
(3)多个线程同时poll多个文件,怎么理解或者处理?
答:与单个线程poll多个文件理解相同。