内核poll回调和等待队列

poll的本意是调查,在程序里就是检查事件是否发生。

f_op→poll() 是文件系统的统一IO检测函数。

poll_wait() 函数,把当前进程加入到驱动里自定义的等待队列上,当驱动事件就绪后,就可以在驱动里自定义的等待队列上唤醒调用poll的进程。

设置回调函数,一般回调函数func是内核默认函数 default_wake_function,可以自己设置,功能就是唤醒了进程。epoll就利用了队列钩子函数把产生的事件内容copy到rdlist ,这样就不需要我们自己遍历监听句柄们查有谁产生了事件。

在内核里面,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。可以使用等待队列在实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等。

DECLARE_WAIT_QUEUE(name, tsk),该宏用于定义并初始化一个名为name的等待队列。name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current,在 Linux内核中current相当于一个全局变量 , 表当前进程。

struct poll_wqueues table; 

void poll_initwait(struct poll_wqueues *pwq){
    init_poll_funcptr(&pwq->pt, __pollwait); //初始化poll函数指针
    pwq->polling_task = current;  //将当前进程记录在pwq结构体
    pwq->triggered = 0;
    pwq->error = 0;
    pwq->table = NULL;
    pwq->inline_index = 0;
}
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc){
    pt->_qproc = qproc;
    pt->_key   = ~0UL;  //所有的事件使能
}
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);
等待队列和队列列表都是在创建这个fd的时候同时创建的。

初始化的时候注册回调函数__pollwait,等有数据的时候驱动层通过tcp层调用__pollwait。

调用__pollwait 是因为没有数据需要排队等待,排队等待就是要把等待队列挂到等待队列头。为什么底层不自己挂非得返回来让上层调用挂?因为这个队列带有唤醒函数,回调到上层就是为了让上层设置自己的唤醒函数。

回调__pollwait 的时候需要把IO检查项填写到检查等待列表 poll_wqueues 中(poll_wqueues 中的数组inline_entries),后面通过poll_wqueues 就可以拿到所有等待信息。

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
    //根据poll_wqueues的成员pt指针p找到所在的poll_wqueues结构指针
    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;
    //设置entry->wait.func = pollwake
    init_waitqueue_func_entry(&entry->wait, pollwake); 
    entry->wait.private = pwq;       // 设置private内容为pwq
    add_wait_queue(wait_address, &entry->wait); // 将该pollwake加入到等待链表头
}

static inline wait_queue_head_t *sk_sleep(struct sock *sk) {//睡眠
          BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
          return &rcu_dereference_raw(sk->sk_wq)->wait;
}

static inline void sock_poll_wait(struct file *filp, struct socket *sock,  poll_table *p){
          if (!poll_does_not_wait(p)) {
                    poll_wait(filp, &sock->wq.wait, p);//内核底层poll_wait()
          }
}
static inline 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);//调用上层设置的回调函数__pollwait

}

从上面代码可见,内核文件poll()调用最终调的是上层设置的回调__pollwait,传回一个上层没有的参数——等待队列头wait_address,上层拥有wait_address之后就可以把自己挂在这个队列头。另一个参数poll_table 是上层传下去的然后再传回来,主要为了在底层判断是否有设置回调。

select 内核等待机制

调用select,初始化一个poll等待队列,局部变量 struct poll_wqueues table,通过这个队列初始化设置当前进程指针 current 和 poll 等待的回调函数(也就是内核底层检查没有定制的事件发生时返回上层调用,让上层调用决定做么做)。

通过fd拿到文件指针,通过文件指针调用poll(),遇到没有对应事件,回调__pollwait, 并传回等待队列头。

在__pollwait 通过 poll_table 找到局部变量 struct poll_wqueues,然后挂载:给struct poll_wqueues设置文件指针、唤醒函数pollwake 、 队列头,然后把这个队列项 &entry→wait 添加到队列头。

添加队列之后当前函数睡眠阻塞,这样这个函数栈就暂时保存起来,等待有事件唤醒的时候恢复继续执行。

select执行完,函数栈销毁,即之前设置的队列、回调、进程都解散,等下次再进入select 时重新再做同样的事情(即所有的挂载都是一次性的)。

do_select(){

        struct poll_wqueues table; //问卷队列,里面有问卷poll_table (pw->pt ))

        poll_initwait(&table) {init_poll_funcptr(&pwq->pt, __pollwait);} 首先初始化注册回调函数(只是把回调函数__pollwait挂载在调查问卷上,这个调查问卷是个局部变量,是为了给后面vfs_poll用的)

        poll_table* wait = &table.pt;//调查问卷, 这里面有个回调 pt->_qproc

        for (;;){

                //遍历fd的long数组
                for (i = 0; i < n; ++rinp, ++routp, ++rexp){ //相对于epoll,select性能多消耗在了这几个循环中

                        all_bits = in | out | ex; // 把select时的那3个fd_set合并到一个long上

                        for (j = 0; j < BITS_PER_LONG;++j, ++i, bit <<= 1){//遍历数组中每个long的位,fd_set 设置之后每个fd都会落到每一位上

                        if (!(bit & all_bits)) continue; //1位1位的查看,没有监听的fd的话继续往下查,有的话通过 fdget(i) 拿到这个fd的文件指针
                        f = fdget(i);

        mask = (*f_op->poll)(f.file, wait);

// 检查是否有数据,有这返回mask,没有则设置唤醒回调函数。通过tcp协议层的 tcp_poll 检查驱动层的数据,没有数据的话通过wait中的_qproc调用__pollwait() 。__pollwait()之所以在这一步调是因为在vfs_poll里面能给__pollwait传递它所要的参数从tcp层返回来的这个socket的等待队列地址,然后把唤醒函数 pollwake()挂在这个队列上,一旦有数据来的时候pollwake调用唤醒poll_schedule_timeout,poll_schedule_timeout返回之后进入循环取回定制的事件返回给应用层。

                        以返回值 mask 同POLLIN_SET,POLLOUT_SET,POLLEX_SET相与来判断是否有对应的事件,然后再把位结果拷贝出去

                        fdput(f);

                }

                //上面vfs_poll() 检查如果没有定制的事件(POLLIN_SET,POLLOUT_SET,POLLEX_SET)则让当前线程睡觉。

                poll_schedule_timeout(&table,TASK_INTERRUPTIBLE,to, slack);//睡眠。在睡眠状态如果有定制事件过来则会被唤醒,即从这个函数退出来继续上面的循环调用vfs_poll() 取出事件设置fd_set传出去。 通过回调函数__pollwait()注册的

        }

通过 poll_table 查找包含当前 poll_table 的poll队列列表,从poll队列列表中拿排队对象 poll_table_entry, 。。。

//select.c
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;
        //socket ready后的回调函数设置为pollwake,用来叫醒app
	init_waitqueue_func_entry(&entry->wait, pollwake);//pollwake调try_to_wake_up(curr->private, mode, wake_flags); curr->private就是在poll_initwait被初始化为current的pwq->polling_task
	entry->wait.private = pwq;
        //把entry挂入wait_address
	add_wait_queue(wait_address, &entry->wait);
}

 

epoll和select的不同在于执行curr→func()的时候epoll唤醒的时候调的是ep_poll_callback()而不是pollwake()。

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key){
    wait_queue_t *curr, *next;
    list_for_each_entry_safe(curr, next, &q->task_list, task_list) {//循环遍历
        unsigned flags = curr->flags;
        if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)break;//跳出循环
    }
}

selectpollwake()唤醒当前task继续执行但需要进入下一循环去获取定时的事件然后拷贝出来给上层应用。

epoll创建有自己的就绪队列,有数据进来时回调函数ep_poll_callback()把事件放到就绪队列中,然后唤醒当前task继续执行,直接从就绪队列中拿事件进行数据操作。

等待队列相关结果和函数

typedef struct poll_table_struct {
    poll_queue_proc _qproc;
    unsigned long _key;
} poll_table;
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);//类似于子类和基类的相互转换
struct poll_wqueues {//队列列表
    poll_table pt;//struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);//可以通过 poll_table pt 找到 poll_wqueues
    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];
};
struct poll_table_entry {
    struct file *filp;
    unsigned long key;
    wait_queue_t wait;  //wait等待队列项
    wait_queue_head_t *wait_address; //wait的等待队列头,就是一个等待队列的头部。
};

struct __wait_queue {

          unsigned int flags; #define WQ_FLAG_EXCLUSIVE 0x01

          void *private;

          wait_queue_func_t func;

          struct list_head task_list;

};

#define __WAITQUEUE_INITIALIZER(name, tsk) {.private    = tsk, .func = default_wake_function, .task_list = { NULL, NULL } } 
#define DECLARE_WAITQUEUE(name, tsk) wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
struct __wait_queue_head { //等待队列头
          spinlock_t lock; 
          struct list_head task_list; //双向循环链表,存放等待的进程
}; typedef struct __wait_queue_head wait_queue_head_t;

struct list_head {
          struct list_head *next, *prev;
};//双向列表

在初始化的时候,链表头的prev和next都是指向自身

static inline void INIT_LIST_HEAD(struct list_head *list) {
          list->next = list;
          list->prev = list;
}

static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) {
          next->prev = new;
          new->next = next;
          new->prev = prev;
          prev->next = new;
}

static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new){
          list_add(&new->task_list, &head->task_list);
}

唤醒机制

休眠与唤醒流程:

  1. 进程A调用wait_event(wq, condition)就是向等待队列头中添加等待队列项wait_queue_t,该该等待队列项中的成员变量private记录当前进程,其成员变量func记录唤醒回调函数,然后调用schedule()使当前进程进入休眠状态。
  2. 进程B调用wake_up(wq)会遍历整个等待列表wq中的每一项wait_queue_t,依次调用每一项的唤醒函数try_to_wake_up()。这个过程会将private记录的进程加入rq运行队列,并设置进程状态为TASK_RUNNING。
  3. 进程A被唤醒后只执行如下检测:
    • 检查condition是否为true,满足条件则跳出循环,再把wait_queue_t从wq队列中移除;
    • 检测该进程task的成员thread_info->flags是否被设置TIF_SIGPENDING,被设置则说明有待处理的信号,则跳出循环,再把wait_queue_t从wq队列中移除;
    • 否则,继续调用schedule()再次进入休眠等待状态,如果wait_queue_t不在wq队列,则再次加入wq队列。

ttwu :try to wake up的缩写

int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags, void *key) { //获取wait所对应的进程

          return try_to_wake_up(curr->private, mode, wake_flags);

}

int try_to_wake_up(struct task_struct* p, unsigned int state, int wake_flags)

{
unsigned long flags;
int cpu, src_cpu, success = 0;

bool freq_notif_allowed = !(wake_flags & WF_NO_NOTIFIER);
bool check_group = false;
wake_flags &= ~WF_NO_NOTIFIER;

smp_mb__before_spinlock();
raw_spin_lock_irqsave(&p->pi_lock, flags); //关闭本地中断
src_cpu = cpu = task_cpu(p);

//如果当前进程状态不属于可唤醒状态集,则无法唤醒该进程
//wake_up()传递过来的TASK_NORMAL等于(TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE)
if (!(p->state & state))
goto out;

success = 1;
smp_rmb();
if (p->on_rq && ttwu_remote(p, wake_flags)) //当前进程已处于rq运行队列,则无需唤醒
goto stat;
...

ttwu_queue(p, cpu); //【小节3.2.3】
stat:
ttwu_stat(p, cpu, wake_flags);
out:
raw_spin_unlock_irqrestore(&p->pi_lock, flags); //恢复本地中断
...
return success;
}

static void ttwu_queue(struct task_struct* p, int cpu)
{
struct rq* rq = cpu_rq(cpu); // 获取当前进程的运行队列
raw_spin_lock(&rq->lock);
lockdep_pin_lock(&rq->lock);
ttwu_do_activate(rq, p, 0); // 【小节3.2.4】
lockdep_unpin_lock(&rq->lock);
raw_spin_unlock(&rq->lock);
}

static void ttwu_do_activate(struct rq* rq, struct task_struct* p, int wake_flags)
{
ttwu_activate(rq, p, ENQUEUE_WAKEUP | ENQUEUE_WAKING);
ttwu_do_wakeup(rq, p, wake_flags);
}

static inline void ttwu_activate(struct rq* rq, struct task_struct* p, int en_flags)
{
activate_task(rq, p, en_flags); //将进程task加入rq队列
p->on_rq = TASK_ON_RQ_QUEUED;

if (p->flags & PF_WQ_WORKER)
wq_worker_waking_up(p, cpu_of(rq)); //worker正在唤醒中,则通知工作队列
}

static void ttwu_do_wakeup(struct rq* rq, struct task_struct* p, int wake_flags)
{
check_preempt_curr(rq, p, wake_flags);
p->state = TASK_RUNNING; //标记该进程为TASK_RUNNING状态
...
}

container_of()

#define list_entry(ptr, type, member) \
          container_of(ptr, type, member) //已知结构体type的成员member的地址ptr,求解结构体type的起始地址

#define container_of(ptr, type, member) ({ \ const typeof(((type *)0)->member) * __mptr = (ptr); //ptr与member类型不匹配,编译时便会有warnning \

 (type *)((char *)__mptr - offsetof(type, member)); })

list_entry主要用于从list节点查找其内嵌在的结构。比如定义一个结构struct A{ struct list_head list; }; 如果知道结构中链表的地址ptrList,就可以从ptrList进而获取整个结构的地址(即整个结构的指针) struct A *ptrA = list_entry(ptrList, struct A, list);

这种地址翻译的技巧是linux的拿手好戏,container_of随处可见,只是链表节点多被封装在更复杂的结构中。

struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tangcpp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值