系统调用poll机制分析(非常透彻)

本文详细解析了RT-Thread操作系统中poll系统调用的实现原理,包括应用程序如何调用poll函数,相关函数的分析,核心思想总结及深入扩展。重点介绍了poll在轮询多个文件时的工作机制,以及线程如何在无数据时休眠并在数据到达时被唤醒。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前序

		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多个文件理解相同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值