1.0版本的select已经看过了,2.6中已经完全重构了,代码每次看都好像懂了,但每次回忆核心流程又感觉有点勉强,我希望通过一种关键流程的形式去分析,而不是贴一大堆的代码
- 关键结构对象
poll_wqueues
,在每次do_select
中会被new
一个出来并初始化,初始化的关键是把__pollwait
方法指针设置到pt中,后面会分析到poll_wait方法的时候,实际上去执行的就是这儿设置的__pollwait
。
/*
* Structures and helpers for select/poll syscall
*/
struct poll_wqueues {
poll_table pt;
struct poll_table_page *table; // 当inline_entries数组使用完了之后,会动态申请的页
struct task_struct *polling_task; // 当前等待的进程
int triggered; // 当被唤醒后该值被设置,以防重复唤醒
int error;
int inline_index; // 当前inline_entries使用到的位置
struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};
// do_select中对poll_wqueues进行初始化
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;
}
-
*f_op->poll
,linux使用了类似接口的模块开发,file对象只是一个接口层,每个file对象都有一个f_op
对象,是实际上真正驱动程序的代码。所以,*f_op->poll
就是调用不同的硬件驱动的poll方法。
一般对应的驱动中都会有一个自己的等待队列而且是统一的,linux给等待队列定义了两个对象wait_queue_head_t
和wait_queue_t
,wait_queue_head_t
是一个包含了自旋锁的双向链表,链表当然也是使用了linux
中的list_head
了,注意了,该链表除了表头也就是wait_queue_head_t
,其他项都是wait_queue_t
结构了,而wait_queue_t
则是存放等待进程的真正地方。
// 真正存放等待队列中等待进程的地方
typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func; // 唤醒方法
struct list_head task_list;
};
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
- 那回到刚才说的
__pollwait
方法,因为最终设备的驱动都会调用poll_wait
方法来触发,该方法比较重要,值得多贴一些代码来解释,该方法第一个参数是监听fd对应的file,第二个参数是驱动设备的等待队列头,第三个参数是咱们第一条说的注入进去的poll_table,所有fd都共用这一个poll_table。
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && wait_address)
p->qproc(filp, wait_address, p);
}
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
// 根据poll_table的指针取得外层对象poll_wqueues的指针
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
// 先从数组中inline_entries中取空闲的,如果用完了则申请新页存放
struct poll_table_entry *entry = poll_get_entry(pwq);
if (!entry)
return;
get_file(filp);
entry->filp = filp;
entry->wait_address = wait_address;
// 设置监听的类型=poll和读写
entry->key = p->key;
// 对wait_queue链表初始化,flags=0,唤醒func=pollwake
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
// 加入到该设备的等待队列中
add_wait_queue(wait_address, &entry->wait);
}
这里还有个细节,一旦*f_op->poll
返回的mask不为0并且是监听的类型,则会把wait
也就是那个wait_table
对象设置为null,这样对下一个file调用poll
方法的时候就不会把自己加入到等待队列中了。
- 退出,当有任何一个事件可用的时候直接返回,当poll_table发生错误的时候也退出。剩下就根据超时去睡眠,方法第一次执行的时候会根据
end_time
设置超时expire
。然后会调用poll_schedule_timeout
去睡眠,如果该方法返回0代表是超时事件唤醒的则设置timeout
,注意不论是超时唤醒还是被某个驱动程序的事件唤醒后,都会再走一次循环,再检查一遍每一个事件的状态。
// 第一次完毕后如果任何一个事件都没有发生,也会置wait为NULL,防止下一次循环再一次设置等待队列
wait = NULL;
// 如果有事件发生,或者timeout都会退出循环
if (retval || timed_out || signal_pending(current))
break;
// 如果发生了错误则也退出
if (table.error) {
retval = table.error;
break;
}
/*
* 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 = timespec_to_ktime(*end_time);
to = &expire;
}
// 开始睡眠
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
to, slack))
timed_out = 1;
- 扫尾,从每个等待队列中删除该进程的等待项,方法是
poll_freewait
,就是遍历inline_entries
数组,取出poll_table_entry
然后挨个调用free_poll_entry
方法从队列中删除。然后检查是否使用了申请的内存,有的话也要一一退出
void poll_freewait(struct poll_wqueues *pwq)
{
struct poll_table_page * p = pwq->table;
int i;
// 从数组中取出poll_table_entry然后挨个调用free_poll_entry从队列释放
for (i = 0; i < pwq->inline_index; i++)
free_poll_entry(pwq->inline_entries + i);
// 如果使用了申请的内存,则也要一一释放
while (p) {
struct poll_table_entry * entry;
struct poll_table_page *old;
entry = p->entry;
do {
entry--;
free_poll_entry(entry);
} while (entry > p->entries);
old = p;
p = p->next;
free_page((unsigned long) old);
}
}
- 唤醒,驱动程序可以写的时候会唤醒等待队列上的进程,调用的是
wake_up_interruptible