对于一次IO访问,例如read操作,数据会先被拷贝到操作系统内核缓存区,然后才从操作系统内核缓存区拷贝到应用程序的地址空间。它会经历两个阶段:
1) 等待数据准备
2) 将数据拷贝到用户进程中
正是因为如此,Linux下面有5种IO模式
◆阻塞型IO
◆非阻塞型IO
◆IO多路复用
◆信号驱动
◆异步IO
使用场景
IO复用是为了解决大量客户端访问问题而提出来的,它与多进程/多线程技术相比,系统开销小,系统不需要创建和维护这么多的进程/线程。
IO复用就是适用的场合:
1) 客户处理多个fd
2) 一个客户处理多个socket
3) 一个tcp服务器既要处理监听socket,又要处理已连接socket
4) 一个服务器要处理多个服务或协议
select、poll和epoll都是使用IO复用的机制,下面分析select函数。
说明:
参数 | 说明 |
nfds | 待测试的fd个数,它是最大fd加1,这样从0~nfds-1均被测试 |
readfds | 指定要让内核测试的读、写和异常fd。 Fd_set是文件描述符fd的集合。 FD_CLR宏:将指定的fd从集合中删除 FD_ISSET宏:检查集合中指定的fd是否可以读写 FD_SET宏:将指定的描述符加入集合中 FD_ZERO宏:清空集合 |
writefds | |
exceptfds | |
timeout | 等待所指定fd中任何一个就绪等待的时间。 永远等待:仅在至少有一个fd准备好的情况下返回,参数设置为NULL 等待一段时间:在至少一个fd准备好后返回,但是不能超过该参数指定的秒数和微秒数。 不等待:检查描述符立即返回,也即是轮询。该参数中的秒数和微秒数都指定为0。 |
内核代码分析
下面看下Linux内核中select实现的相关代码,核心处理函数为core_sys_select
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timespec *end_time) { /* Allocate small arguments on the stack to save memory and be faster */ long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
/* max_fds can increase, so grab it once to avoid race */ rcu_read_lock(); fdt = files_fdtable(current->files ); /*获取当前进程的文件描述符表*/ max_fds = fdt->max_fds ; rcu_read_unlock(); if (n > max_fds) n = max_fds; |
数组stack_fds空间大小为256字节。如果应用层传入的参数n大于当前进程文件描述符表的max_fds,则修正n值等于max_fds。
/* * We need 6 bitmaps (in/out/ex for both incoming and outgoing), * since we used fdset we need to allocate memory in units of * long-words. */ size = FDS_BYTES(n); bits = stack_fds; if (size > sizeof(stack_fds) / 6) { /* Not enough space in on-stack array; must use kmalloc */ bits = kmalloc(6 * size, GFP_KERNEL); } 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; |
size值为n个fd需要的字节空间,每个fd占1bit。每个fd总共需要6bit空间,分别存储读in、写out、异常ex以及结果res_in、res_out和res_ex。
当size*6空间大于stack_fds数组空间大小时,调用kmalloc重新分配空间。
fds的内存布局如下图所示:
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))) /*从用户空间拷贝fd set*/ goto out; zero_fd_set(n, fds.res_in); zero_fd_set(n, fds.res_out); zero_fd_set(n, fds.res_ex); /*初始化fd set的结果集*/ ret = do_select(n, &fds, end_time);
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; |
get_fd_set函数就是调用copy_from_user从用户态空间拷贝fds的读fd in、写fd out和异常fd ex。接着初始化fds的结果集的读fd res_in、写fd res_out和异常fd res_ex。
do_select是select实现的核心函数,将在下面进行分析。
set_fd_set调用__copy_to_user将结果集的读fd res_in、写fd res_out和异常fd res_ex拷贝到用户态空间。
下面接着分析do_select函数,代码片段如下:
rcu_read_lock(); retval = max_select_fd(n, fds); /*得到要监测的最大的描述符值*/ rcu_read_unlock(); n = retval; poll_initwait(&table); wait = &table.pt; if (end_time && ! end_time->tv_sec && !end_time->tv_nsec ) { /*定时器值为0表示不等待*/ wait->_qproc = NULL; timed_out = 1; } |
max_select_fd函数根据已打开fd位图检查用户打开的fd,要求对应的fd必须打开,并返回最大的fd值。
poll_initwait函数将当前进程加入等待队列table,并将table添加到poll_table中。
for (;;) { 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) { in = *inp++; out = *outp++; ex = *exp++; all_bits = in | out | ex; if (all_bits == 0) { /*此位图区间没有我们关心的fd*/ i += BITS_PER_LONG; /*以BITS_PER_LONG个bit步长移动*/ continue; } for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) { if (! (bit & all_bits)) /*遍历BITS_PER_LONG个bit中的每一bit*/ continue; f = fdget(i); if (f.file) { f_op = f.file->f_op ; if (f_op->poll ) { wait_key_set(wait, in, out, bit, busy_flag); mask = (*f_op->poll )(f.file, wait); /*对于socket描述符,poll函数应该是sock_poll*/ } fdput(f); if ((mask & POLLIN_SET) && (in & bit)) { /*关心的fd的读就绪*/ res_in |= bit; /*更新读结果fd*/ retval++; /*增加就绪个数*/ wait->_qproc = NULL; } if ((mask & POLLOUT_SET) && (out & bit)) { /*关心的fd的写就绪*/ res_out |= bit; /*更新写结果fd*/ retval++; /*增加就绪个数*/ wait->_qproc = NULL; } if ((mask & POLLEX_SET) && (ex & bit)) { /*关心的fd的异常就绪*/ res_ex |= bit; /*更新异常结果fd*/ retval++; wait->_qproc = NULL; } } } if (res_in) *rinp = res_in; /*遍历完此fd区间后更新读结果集*/ if (res_out) *routp = res_out; /*遍历完此fd区间后更新写结果集*/ if (res_ex) *rexp = res_ex; /*遍历完此fd区间后更新异常结果集*/ cond_resched(); /*进行一次调度让其他进程运行,后面由等待队列唤醒*/ } |
上述do_select代码片段就是遍历位图中我们所关心的fd,并更新读、写、异常结果集。