一. Select是Linux下提供的一种监控文件状态的机制。Seletc对多个描述符的状态进行监控,一旦所监控的描述符符合可读、可写、异常中的一种,线程结束阻塞,否则阻塞到描述符集发生变化,或者超出等待时间
原型为:
sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp)
参数1:表示监控的描述符的个数,为描述符的个数加1.
参数2:表示监控状态为可读取的描述符的集合
参数3:表示监控状态为可写的描述符的集合
参数4:表示监控状态为异常(exception)的描述符的集合
参数5:时间段,超时时间的长短,超过这个时间以后,即使文件不可读、不可写、异常,sys_select也会退出,不在阻塞。
二.Linux的特点:一切皆文件。通常情况下可多种以将任意对象转化为描述符的显示,如socket、消息队列,可以转化描述的资源都可以使用select等待。
三.select的优点
显示中,许多任务是由消息驱动的,有任务来临的时候线程执行,如dhcp server、snmp server,当有报文过得时候,开始处理报文,如果报文处理完以后,任务结束,等待新的消息。
1.1 最容易象想到的线程等待的方法是sleep,伪代码如:
while(1)
{
接收消息;
处理消息;
sleep(1s);
},
select的伪代码:
while(1)
{
ret =select();
接收消息;
处理消息
}
这种使用sleep的方法可以避免线程循环允许空任务,造成cpu的浪费。但是如果长时间没有消息到来,执行接收消息也是要占用cpu的,即使读出的为空消息。而select很好的解决了这个问题:如果我的消息队列是不可读,也就是没有消息过来,用户空间线程这个时候休眠,不占用任何资源,一旦有消息到来,内核唤醒该线程,select不再阻塞,线程被内核唤醒。
在内核中执行do_select函数也是使用的定期轮询的方式监控描述符。
四、select的机制
select通过调用内核的_sys_select,在内核中循环监控描述符的poll状态,一般驱动中,设备都有poll函数的实现,该函数在内核执行,一个unix socket和tcp socket的描述符结构如:
static struct proto_ops unix_stream_ops = {
.family = PF_UNIX,
.owner = THIS_MODULE,
.release = unix_release,
.bind = unix_bind,
.connect = unix_stream_connect,
.socketpair = unix_socketpair,
.accept = unix_accept,
.getname = unix_getname,
.poll = unix_poll,
.ioctl = unix_ioctl,
.listen = unix_listen,
.shutdown = unix_shutdown,
.setsockopt = sock_no_setsockopt,
.getsockopt = sock_no_getsockopt,
.sendmsg = unix_stream_sendmsg,
.recvmsg = unix_stream_recvmsg,
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
.mmap = sock_no_mmap,
.sendpage = inet_sendpage,
.splice_read = tcp_splice_read,
#ifdef CONFIG_COMPAT
.compat_setsockopt = compat_sock_common_setsockopt,
.compat_getsockopt = compat_sock_common_getsockopt,
.compat_ioctl = inet_compat_ioctl,
#endif
};
内核的设备的Poll函数在状态变为可读是,会主动唤醒被阻塞的线程。
五、关键函数
/*源码来源于Linux-3.4.69*/
/*
end_time是一个时间点,而不是时间段,由系统调用sys_select入口,通过poll_select_set_timeout计算
*/
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;
rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0)
return retval;
n = retval;
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait->_qproc = NULL;
timed_out = 1;
}
/*设置轮询的周期:select_estimate_accuracy函数根据当前时间和截止时间,选择一个合适的周期,去执行外层循环,执行完以后会休眠slack时间,slack以纳秒为单位,这种方式极大地提高了CPU的使用率,避免使用死循环造成的资源浪费*/
if (end_time && !timed_out)
slack = select_estimate_accuracy(end_time);
retval = 0;
/*外层循环为任务死循环,用于连续执行循环内部代码,满足条件以后通过break跳出。
中层循环表示轮询每个描述符集。
内层循环表示轮询每个描述符 */
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += BITS_PER_LONG;
continue;
}
for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
/*调用描述符的poll函数,poll函数会在文件实现的时候添加函数,该函数的本质是查看文件的状态*/
if (f_op && f_op->poll) {
wait_key_set(wait, in, out, bit);
mask = (*f_op->poll)(file, wait);
}
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
wait->_qproc = NULL;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
wait->_qproc = NULL;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
wait->_qproc = NULL;
}
}
}
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
cond_resched();
}
wait->_qproc = NULL;
/*
条件1:retval不为0时,表示检测的描述符的状态发生了变化时,跳出死循环。
条件2:time_out为0,表示超时等待的时间结束,此时跳出循环。
条件3:有信号唤醒当前进程
*/
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;
}
/*置当前进程为可中断等待状态,等待被唤醒,执行一次poll_schedule_timeout的时间为slack,当到达超时时间to以后,poll_schedule_timeout返回0,跳出循环。
哪个位置会唤醒进程呢?*/
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
to, slack))
timed_out = 1;
}
poll_freewait(&table);
return retval;
}