select源码剖析

asmlinkage long
sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timeval __user *tvp)

select是个系统调用,他进入内核态之后就调用sys_select(),这个函数的功能有以下几点:
1、参数检查,对n进行判断,n的最大值就是当前进程所能打开的最大文件数量,一般情况下为1024
2、用一个结构体保存用户传进来的参数,fd_set_bits结构,定义如下:
typedef struct {
	unsigned long *in, *out, *ex;
	unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;

in,out,ex分别保存用户注册的感兴趣事件,而res_in,res_out,res_ex,分别保存这个文件描述符上的用户感兴趣的事件的已发生事件,当返回时会把res_in ,res_out,res_ex中的值赋给in,out,ex,所以这里就有从用户空间到内核空间,内核空间到用户空间的拷贝问题,而且每次调用select时都会发生大量的重复来回拷贝问题,造成效率上的问题

fd_set+bits组装好之后(保存了用户感兴趣的事件)就要做具体的事情了,那就是监听多个文件描述符,它把fd_set_bits当做参数调用do_select()函数做具体的文件描述符上的监听工作

int do_select(int n, fd_set_bits *fds, long *timeout)

这个函数做了以下几件事情:
1、每次取一个long字节长度的fd过来,然后分别把他们的读写异常事件做逻辑或运算,如果不为零就说明有此long 字节的fd里面是用户感兴趣的事件对应的文件描述符,于是就调用该文件的poll方法(每个文件的类型都对应有相应的文件操作,这个操作在file->f_op中的poll方法被赋予不同的值,例如管道文件的poll方法就被赋值为pipe_poll()),如果通过poll方法获取到的掩码为零就说明这个文件描述上没有事件发生,直接执行下次的循环,如果发生了,但是还不知道是读或者写或者异常中的哪一些,于是就拿1每次左移动以为和这个long长度的fd分别做逻辑与运算,如果不为零就说明这个位对应的fd有感兴趣的事件发生,然后就拿这个l分别去和用户传进来的in,out,ex做逻辑与运算就知道了,并且做相应的计数++,
2、如果第一次对这个文件描述符调用poll方法的话,调用他时传递的参数wait就不为空,就需要将此进程挂入该文件的等待队列中,在file中有该文件的等待队列的头,此时就会调用刚刚在第一步的过程中在poll之前他会先初始化一个poll_table 的机构体,这个结构体里面就一个函数指针,在select,poll中被初始化为_pollwait(),当需要将该进程加入该文件的等待队列时就要调用这个函数。
poll_table的定义:
typedef struct poll_table_struct {
	poll_queue_proc qproc;
} poll_table;
3、如果扫描一遍之后没有时间到达就调用schedule_timeout进入睡眠,当时间到达,有信号到达,有时间到达时才会被唤醒,有事件达到时他会重新扫描一遍所有的fd这个时候
一定可以返回,扫描得到的事件记录在res_in,res_out,res_ex中,出了大循环之后就将这里的值分别拷贝到用户空间的in,out,ex中,释放相应的数据结构,返回。


void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *_p)

这个函数首先通过p根据其在poll_wqueue中的偏移量找到poll_wqueue结构体,然后通过这个结构体中的poll_table_page分配一个poll_table_entry,而加入该文件的等待队列就是通过这个结构体实现的(由于监听个的fd可能会很多,不可能全部在堆栈上分配,因此这里就采用动态分配的方式,每次分配一页的大小,用单链表串起来,而一页当中有很多的poll_table_entry项)
poll_table_entry定义如下:
struct poll_table_entry {
	struct file * filp;
	wait_queue_t wait;
	wait_queue_head_t * wait_address;
};
这个里面它wait_address中保存该文件的等待队列的头,wait就是一个等待队列的节点,包含进程的task_struct,唤醒的回调函数(这里使用默认的回调函数),如果该文件的有事件到达就会调用该函数,唤醒正在等待的进程,重新扫描一遍fd,如果计数不为零就从select中返回了,并且还会释放这里的poll_wqueue等数据结构,

这里会用到一个数据结构poll_wqueue,定义如下
struct poll_wqueues {
	poll_table pt;
	struct poll_table_page * table;
	int error;
};

缺点

1、它支持的最大文件描述符的数量为本进程支持的最大文件数量,当有几万个文件描述符时,他就不支持,即使通过内核微调调整这个值,效率也不高
2、即使有一个文件描述符上有事件到达,他也得返回,但是这个肯定要循环调用它监听,所以每次调用都需要大量重复的用户空间到内核空间,内核空间到用户空间的拷贝,而且每次都是全部拷贝,效率低下
3、每次调用都要重复的开辟entry将其加入相应文件的等待队列,这里开销太大


你可能感兴趣的:(Linux)