linux select和poll的实现机制其实是类似的,而且最终都会使用到内核驱动提供的具体poll方法,poll的使用详解参考这篇文章:
https://blog.csdn.net/oqqYuJi12345678/article/details/106313768
1 应用层使用举例
1.1函数原型
int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
1.2函数说明
select()用来等待文件描述词状态的改变。参数n代表最大的文件描述词加1,参数readfds、writefds和exceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况。底下的宏提供了处理这三种描述词组的方式:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd的位
FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd的位是否为真
FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set);用来清除描述词组set的全部位
1.3结构体说明
先说明两个结构体:
1) struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。
fd_set集合可以通过一些宏由人为来操作,比如
清空集合FD_ZERO(fd_set *);
将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set*);
将一个给定的文件描述符从集合中删除FD_CLR(int,fd_set*);
检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。
2) struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。如下所示:
struct timeval
{
time_t tv_sec;
time_t tv_usec;
};
1.4.具体参数说明:
1) int n:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
2) fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
3) fd_set*writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
4) fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
5)struct timeval *timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
1.5返回值
负值:select错误
正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件
如果参数timeout设为NULL则表示select()没有timeout。
1.6错误代码
执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n为负值。
ENOMEM 核心内存不足
1.7 范例
main()
{
int sock;
FILE *fp;
struct fd_set fds;
struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
char buffer[256]={0}; //256字节的接收缓冲区
/* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
sock=socket(...);
bind(...);
fp=fopen(...); */
while(1)
{
FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,&fds); //添加描述符
FD_SET(fp,&fds); //同上
maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1
switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
{
case -1: exit(-1);break; //select错误,退出程序
case 0:break; //再次轮询
default:
if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
{
recvfrom(sock,buffer,256,.....);//接受网络数据
if(FD_ISSET(fp,&fds)) //测试文件是否可写
fwrite(fp,buffer...);//写入文件
buffer清空;
}// end if break;
}// end switch
}//end while
}//end main
2 内核实现详解
typedef struct {
unsigned long *in, *out, *ex;
unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;
// long sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp)
SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
struct timespec end_time, *to = NULL;
struct timeval tv;
int ret;
if (tvp) {
if (copy_from_user(&tv, tvp, sizeof(tv))) {
return -EFAULT;
}
// 计算超时时间
to = &end_time;
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC)) {
return -EINVAL;
}
}
ret = core_sys_select(n, inp, outp, exp, to);
// 复制剩余时间到用户空间
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
return ret;
}
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timespec *end_time)
{
fd_set_bits fds;
void *bits;
int ret, max_fds;
unsigned int size;
struct fdtable *fdt;
//小对象使用栈上的空间,节约内存, 加快访问速度
long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
ret = -EINVAL;
if (n < 0) {
goto out_nofds;
}
rcu_read_lock();
// 取得进程对应的 fdtable
fdt = files_fdtable(current->files);
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds) {
n = max_fds;
}
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
// 栈上的空间不够, 申请内存, 全部使用堆上的空间
ret = -ENOMEM;
bits = kmalloc(6 * size, GFP_KERNEL);
if (!bits) {
goto out_nofds;
}
}
fds.in = bits;
fds.out = bits + size;
fds.ex = bits + 2*size;
fds.res_in = bits + 3*size;
fds.res_out = bits + 4*size;
fds.res_ex = bits + 5*size;
// 复制用户空间到内核
if ((ret = get_fd_set(n, inp, fds.in)) ||
(ret = get_fd_set(n, outp, fds.out)) ||
(ret = get_fd_set(n, exp, fds.ex))) {
goto out;
}
// 初始化fd set
zero_fd_set(n, fds.res_in);
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
ret = do_select(n, &fds, end_time);
if (ret < 0) {
goto out;
}
if (!ret) {
// 该返回值会被系统捕获, 并以同样的参数重新调用sys_select()
ret = -ERESTARTNOHAND;
if (signal_pending(current)) {
goto out;
}
ret = 0;
}
// 复制到用户空间
if (set_fd_set(n, inp, fds.res_in) ||
set_fd_set(n, outp, fds.res_out) ||
set_fd_set(n, exp, fds.res_ex)) {
ret = -EFAULT;
}
out:
if (bits != stack_fds) {
kfree(bits);
}
out_nofds:
return ret;
}
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();
// 检查fds中fd的有效性, 并获取当前最大的fd
retval = max_select_fd(n, fds);
rcu_read_unlock();
if (retval < 0) {
return retval;
}
n = retval;
// 初始化 poll_wqueues 结构, 设置函数指针_qproc 为__pollwait
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1;
}
if (end_time && !timed_out) {
// 估计需要等待的时间.
slack = select_estimate_accuracy(end_time);
}
retval = 0;
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;
// 遍历所有的描述符, i 文件描述符 ,n为所有描述符中值最大的那个,这样会有一个问题
//没有设置进select 但是值小于n的描述符也会被遍历,有一定的效率问题,但这样实现比较简单
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;
// 检查当前的 slot 中的描述符
in = *inp++;
out = *outp++;
ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) { // 没有需要监听的描述符, 下一个slot
i += __NFDBITS;
continue;
}
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n) {
break;
}
// 不需要监听描述符 i
if (!(bit & all_bits)) {
continue;
}
// 取得文件结构
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
// 没有 f_op 的话就认为一直处于就绪状态
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll) {
// 设置等待事件的掩码
-----------------------------------------------------------(1)
wait_key_set(wait, in, out, bit);
/*
static inline void wait_key_set(poll_table *wait, unsigned long in,
unsigned long out, unsigned long bit)
{
wait->_key = POLLEX_SET;// (POLLPRI)
if (in & bit)
wait->_key |= POLLIN_SET;//(POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
if (out & bit)
wait->_key |= POLLOUT_SET;//POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
}
*/
// 获取当前的就绪状态, 并添加到文件的对应等待队列中
----------------------------------------------------------------------------(2)
mask = (*f_op->poll)(file, wait);
// 和poll完全一样
}
fput_light(file, fput_needed);
// 释放文件
// 检查文件 i 是否已有事件就绪,
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
// 如果已有就绪事件就不再向其他文件的
// 等待队列中添加回调函数
wait = NULL;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
wait = NULL;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
wait = NULL;
}
}
}
-----------------------------------------------------------(3)
if (res_in) {
*rinp = res_in;
}
if (res_out) {
*routp = res_out;
}
if (res_ex) {
*rexp = res_ex;
}
cond_resched();
}
wait = NULL; // 该添加回调函数的都已经添加了
---------------------------------------------------------------------(4)
if (retval || timed_out || signal_pending(current)) {
break; // 信号发生,监听事件就绪或超时
}
if (table.error) {
retval = table.error; // 产生错误了
break;
}
// 转换到内核时间
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(&table);
return retval;
}
从core_sys_select可以看出,select的实现和poll机制类似,最终会调用驱动程序的poll实现,不过相比于poll来说,select监视的事件会不那么灵活,仅设置了in,out,和ex集中状态
(1)根据传入的参数,设置相应的监听事件,可以看到in,out和ex分别对应一些事件的集合:
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)
(2)调用 驱动的poll方法,如果驱动有数据,返回的mask不为0
(3)当监听的事件就绪时,把值传回给文件描述符,用于向用户层返回就绪事件
(4)遍历完所有文件以后,如果有就绪文件,则跳出for 死循环,不会睡眠,返回用户层,否则睡眠,等待唤醒,执行下一次扫描过程
该文整理自: