select这个系统调用的原型如下
第一个参数nfds用来告诉内核 要扫描的socket fd的数量+1 ,select系统调用最大接收的数量是1024,但是如果每次都去扫描1024,实际上的数量并不多,则效率太低,这里可以指定需要扫描的数量。 最大数量为1024,如果需要修改这个数量,则需要重新编译Linux内核源码。
第2、3、4个参数分别是readfds、writefds、exceptfds,传递的参数应该是fd_set 类型的引用,内核会检测每个socket的fd, 如果没有读事件,就将对应的fd从第二个参数传入的fd_set中移除,如果没有写事件,就将对应的fd从第二个参数的fd_set中移除,如果没有异常事件,就将对应的fd从第三个参数的fd_set中移除 。这里我们应该 要将实际的readfds、writefds、exceptfds拷贝一份副本传进去,而不是传入原引用,因为如果传递的是原引用,某些socket可能就已经丢失 。
最后一个参数是等待时间, 传入0表示非阻塞,传入>0表示等待一定时间,传入NULL表示阻塞,直到等到某个socket就绪 。
FD_ZERO()这个函数将fd_set中的所有bit清0,一般用来进行初始化等。
FD_CLR()这个函数用来将bitmap(fd_set )中的某个bit清0,在客户端异常退出时就会用到这个函数,将fd从fd_set中删除。
FD_ISSET()用来判断某个bit是否被置1了,也就是判断某个fd是否在fd_set中。
FD_SET()这个函数用来将某个fd加入fd_set中,当客户端新加入连接时就会使用到这个函数。
epoll_create系统调用用来创建epfd,会在开辟一块内存空间(epoll的结构空间)。size为epoll上能关注的最大描述符数,不够会进行扩展,size只要>0就行,早期的设计size是固定大小,但是现在size参数没什么用,会自动扩展。
返回值是epfd,如果为-1则说明创建epoll对象失败 。
第一个参数epfd传入的就是epoll_create返回的epfd。
第二个参数传入对应 *** 作的宏,包括 增删改(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD) 。
第三个参数传入的是 需要增删改的socket的fd 。
第四个参数传入的是 需要 *** 作的fd的哪些事件 ,具体的事件可以看后续。
返回值是一个int类型,如果为-1则说明 *** 作失败 。
第一个参数是epfd,也就是epoll_create的返回值。
第二个参数是一个epoll_event类型的指针,也就是传入的是一个数组指针。 内核会将就绪的socket的事件拷贝到这个数组中,用户可以根据这个数组拿到事件和消息等 。
第三个参数是maxevents,传入的是 第二个参数的数组的容量 。
第四个参数是timeout, 如果设为-1一直阻塞直到有就绪数据为止,如果设为0立即返回,如果>0那么阻塞一段时间 。
返回值是一个int类型,也就是就绪的socket的事件的数量(内核拷贝给用户的events的元素的数量),通过这个数量可以进行遍历处理每个事件 。
一般需要传入 ev.data.fd 和 ev.events ,也就是fd和需要监控的fd的事件。事件如果需要传入多个,可以通过按位与来连接,比如需要监控读写事件,只需要像如下这样 *** 作即可: ev.events=EPOLLIN | EPOLLOUT 。
LT(水平触发), 默认 的工作模式, 事件就绪后用户可以选择处理和不处理,如果用户不处理,内核会对这部分数据进行维护,那么下次调用epoll_wait()时仍旧会打包出来 。
ET(边缘触发),事件就绪之后, 用户必须进行处理 ,因为内核把事件打包出来之后就把对应的就绪事件给清掉了, 如果不处理那么就绪事件就没了 。ET可以减少epoll事件被重复触发的次数,效率比LT高。
如果需要设置为边缘触发只需要设置事件为类似 ev.events=EPOLLIN | EPOLLET 即可 。
select/poll/epoll是nio多路复用技术, 传统的bio无法实现C10K/C100K ,也就是无法满足1w/10w的并发量,在这么高的并发量下,在进行上下文切换就很容易将服务器的负载拉飞。
1.将fd_set从用户态拷贝到内核态
2.根据fd_set扫描内存中的socket的fd的状态,时间复杂度为O(n)
3.检查fd_set,如果有已经就绪的socket,就给对应的socket的fd打标记,那么就return 就绪socket的数量并唤醒当前线程,如果没有就绪的socket就继续阻塞当前线程直到有socket就绪才将当前线程唤醒。
4.如果想要获取当前已经就绪的socket列表,则还需要进行一次系统调用,使用O(n)的时间去扫描socket的fd列表,将已经打上标记的socket的fd返回。
CPU在同一个时刻只能执行一个程序,通过RR时间片轮转去切换执行各个程序。没有被挂起的进程(线程)则在工作队列中排队等待CPU的执行,将进程(线程)从工作队列中移除就是挂起,反映到Java层面的就是线程的阻塞。
什么是中断?当我们使用键盘、鼠标等IO设备的时候,会给主板一个电流信号,这个电流信号就给CPU一个中断信号,CPU执行完当前的指令便会保存现场,然后执行键盘/鼠标等设备的中断程序,让中断程序获取CPU的使用权,在中断程序后又将现场恢复,继续执行之前的进程。
如果第一次没检测到就绪的socket,就要将其进程(线程)从工作队列中移除,并加入到socket的等待队列中。
socket包含读缓冲区+写缓冲区+等待队列(放线程或eventpoll对象)
当从客户端往服务器端发送数据时,使用TCP/IP协议将通过物理链路、网线发给服务器的网卡设备,网卡的DMA设备将接收到的的数据写入到内存中的一块区域(网卡缓冲区),然后会给CPU发出一个中断信号,CPU执行完当前指令则会保存现场,然后网卡的中断程序就获得了CPU的使用权,然后CPU便开始执行网卡的中断程序,将内存中的缓存区中的数据包拿出,判断端口号便可以判断它是哪个socket的数据,将数据包写入对应的socket的读(输入)缓冲区,去检查对应的socket的等待队列有没有等待着的进程(线程),如果有就将该线程(进程)从socket的等待队列中移除,将其加入工作队列,这时候该进程(线程)就再次拥有了CPU的使用权限,到这里中断程序就结束了。
之后这个进程(线程)就执行select函数再次去检查fd_set就能发现有socket缓冲区中有数据了,就将该socket的fd打标记,这个时候select函数就执行完了,这时候就会给上层返回一个int类型的数值,表示已经就绪的socket的数量或者是发生了错误。这个时候就再进行内核态到用户态的切换,对已经打标记的socket的fd进行处理。
将原本1024bit长度的bitmap(fd_set)换成了数组的方式传入 ,可以 解决原本1024个不够用的情况 ,因为传入的是数组,长度可以不止是1024了,因此socket数量可以更多,在Kernel底层会将数组转换成链表。
在十多年前,linux2.6之前,不支持epoll,当时可能会选择用Windows/Unix用作服务器,而不会去选择Linux,因为select/poll会随着并发量的上升,性能变得越来越低,每次都得检查所有的Socket列表。
1.select/poll每次调用都必须根据提供所有的socket集合,然后就 会涉及到将这个集合从用户空间拷贝到内核空间,在这个过程中很耗费性能 。但是 其实每次的socket集合的变化也许并不大,也许就1-2个socket ,但是它会全部进行拷贝,全部进行遍历一一判断是否就绪。
2.select/poll的返回类型是int,只能代表当前的就绪的socket的数量/发生了错误, 如果还需要知道是哪些socket就绪了,则还需要再次使用系统调用去检查哪些socket是就绪的,又是一次O(n)的 *** 作,很耗费性能 。
1.epoll在Kernel内核中存储了对应的数据结构(eventpoll)。我们可以 使用epoll_create()这个系统调用去创建一个eventpoll对象 ,并返回eventpoll的对象id(epfd),eventpoll对象主要包括三个部分:需要处理的正在监听的socket_fd列表(红黑树结构)、socket就绪列表以及等待队列(线程)。
2.我们可以使用epoll_ctl()这个系统调用对socket_fd列表进行CRUD *** 作,因为可能频繁地进行CRUD,因此 socket_fd使用的是红黑树的结构 ,让其效率能更高。epoll_ctl()传递的参数主要是epfd(eventpoll对象id)。
3.epoll_wait()这个系统调用默认会 将当前进程(线程)阻塞,加入到eventpoll对象的等待队列中,直到socket就绪列表中有socket,才会将该进程(线程)重新加入工作队列 ,并返回就绪队列中的socket的数量。
socket包含读缓冲区、写缓冲区和等待队列。当使用epoll_ctl()系统调用将socket新加入socket_fd列表时,就会将eventpoll对象引用加到socket的等待队列中, 当网卡的中断程序发现socket的等待队列中不是一个进程(线程),而是一个eventpoll对象的引用,就将socket引用追加到eventpoll对象的就绪列表的尾部 。而eventpoll对象中的等待队列存放的就是调用了epoll_wait()的进程(线程),网卡的中断程序执行会将等待队列中的进程(线程)重新加入工作队列,让其拥有占用CPU执行的资格。epoll_wait()的返回值是int类型,返回的是就绪的socket的数量/发生错误,-1表示发生错误。
epoll的参数有传入一个epoll_event的数组指针(作为输出参数),在调用epoll_wait()返回的同时,Kernel内核还会将就绪的socket列表添加到epoll_event类型的数组当中。
Linux下select调用的过程:1.用户层应用程序调用select(),底层调用poll())
2.核心层调用sys_select() ------>do_select()
最终调用文件描述符fd对应的struct file类型变量的struct file_operations *f_op的poll函数。
poll指向的函数返回当前可否读写的信息。
1)如果当前可读写,返回读写信息。
2)如果当前不可读写,则阻塞进程,并等待驱动程序唤醒,重新调用poll函数,或超时返回。
3.驱动需要实现poll函数。
当驱动发现有数据可以读写时,通知核心层,核心层重新调用poll指向的函数查询信息。
poll_wait(filp,&wait_q,wait) // 此处将当前进程加入到等待队列中,但并不阻塞
在中断中使用wake_up_interruptible(&wait_q)唤醒等待队列
对select与非阻塞I/O实现的分析
咔咔,space改版后的第一篇文章,嘿嘿
--------------------------
使用非阻塞I/O的应用程序经常使用select,poll等系统调用,他们本质上都允许决定是否可以对一个或多个打开的文件做非阻塞的读取或写入,这些调用也会阻塞进程。
ldd3详细阐述了非阻塞I/O的实现方法,以及poll的数据结构,但是select是如何利用驱动程序中 f_op->poll相对应的方法来实现这种阻塞的呢?向老哥请教并查看了源码后,基本弄清了在对设备文件调用select之后,到底发生了什么。下面先简要介绍select系统调用及驱动中的相关实现,再说明两者之间的联系。
select的函数原型如下:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout)
用于等待fd集合中的对应的文件改变状态。注意timeout参数是timeval结构体而不是timespec,三个fd集合分别用于等待文件可读、可写、例外。对这些集合的 *** 作非常方便,内核提供了以FD_开头的宏。第一个参数n应该等于三个集合中最大的fd值加1。timeout为0时,select立即返回不会阻塞,这在轮询(polling)时很有用;若要一直阻塞直到有fd可用,timeout应设为NULL。
select系统调用需要来自设备驱动程序的相应支持,也就是poll方法,它做两件事:
1. 在一个或多个可指示poll状态变化的等待队列上调用poll_wait;
2. 返回一个用来描述 *** 作是否可以立即无阻塞执行的位掩码。
一个典型的poll方法的实现如下(摘自ldd3):
struct scull_pipe *dev = filp->private_data
unsigned int mask = 0
/*
* The buffer is circularit is considered full
* if "wp" is right behind "rp" and empty if the
* two are equal.
*/
down(&dev->sem)
poll_wait(filp, &dev->inq, wait)
poll_wait(filp, &dev->outq, wait)
if (dev->rp != dev->wp)
mask |= POLLIN | POLLRDNORM /* readable */
if (spacefree(dev))
mask |= POLLOUT | POLLWRNORM /* writable */
up(&dev->sem)
return mask
这段代码增加某设备的两个等待队列到poll_table中,然后根据数据的可读或可写状态设置相应的位掩码。当用户应用程序调用了select函数时,内核会调用由该系统调用引用的全部文件的poll方法,并向他们传递同一个poll_table。
select系统调用过程如下:
select sys_select do_select *fop->poll
sys_select作一些准备工作和检查,比如检查传递进来的timeout参数,用rcu更新max_fdset的值,为in/out/ex分配bitmap并为fds赋值,然后调用do_select:
ret = do_select(n, &fds, &timeout)
进入do_select之后,首先初始化poll_wqueues类型的table,再将poll_table赋给wait,poll_table是构成实际数据结构的一个简单封装(可以查看linux/poll.h,这里不详述):
struct poll_wqueues table
poll_table *wait
poll_initwait(&table)
wait = &table.pt
下面就是真正干活的代码了,这是一个for的无限循环,下面是核心代码:
for (j = 0j <__NFDBITS++j, ++i, bit <<= 1) {
......
if (file) {
f_op = file->f_op
mask = DEFAULT_POLLMASK
if (f_op &&f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait)
......
}
cond_resched()
}
现在终于知道f_op->poll中干的两件事是用来做什么的了,给mask赋值,是为了传递给do_select里的mask,而调用poll_wait就是为了加入设备的等待队列然后给cond_resched()用,如果没有一个被唤醒,那么cond_resched()就是立刻切换其他进程,用户空间select休眠(timeout非0时)。注意到poll方法的第二个参数有两种可能,这是为什么呢?先别急,等会看看retval是干嘛的就知道了。
代码中有两个地方可以跳出这个无限循环:
if (retval || !__timeout || signal_pending(current))
break
if(table.error) {
retval = table.error
break
}
retval非零,timeout为0(即立刻跳出),休眠过程中收到信号以及出错。后面三个很好理解,第一个是怎么回事呢?我们来看看有关retval赋值的代码:
retval = max_select_fd(n, fds)
n = retval
retval = 0
n保存最大的文件描述符,retval在进入for无限循环前被置为0。通过比较相关的位,一旦发现可以进行I/O,retval的值就会加1,变成非零,这个时候(*f_op->poll)(file, retval ? NULL : wait)的第二个参数变成NULL,原因显而易见,因为内核知道此时不会发生任何等待,因此也不需要构造等待队列。另外,当timeout为0时,wait会被设为NULL,因此这种情况下即使retval为0,即没有可用I/O,poll的第二个参数还是NULL,系统不需要处理等待队列。跳出循环之后将进程状态设置为TASK_RUNNING,并对poll_table进行清空,最后将retval返回给sys_select做一些“善后”工作
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)