文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
阻塞I/O会一直阻塞住对应的进程直到操作完成;
非阻塞I/O在内核准备数据的情况下会立刻返回;
通过一种机制一个进程能同时等待多个文件描述符(fd),而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
select(fd_set read[],fd_set [],fd_set [],timeout)
select仅仅知道了有I/O事件发生,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1)单个进程可监视的fd数量被限制,即能监听端口的大小有限。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
2)对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
3)需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
1)从用户空间拷贝fd_set到内核空间,也即从当前程序拷贝fd_set数组进内核;
2)对所有的fd进行一次poll操作,即把当前进程挂载到fd上。
3)poll操作过程中select会唤醒所有的队列中节点,进行遍历,得到它们的掩码(不同的掩码表示不同的就绪状态)。
4)如果所有设备返回的掩码都没有显示任何的事件触发,就去掉回调函数的函数指针,进入有限时的睡眠状态,再恢复和不断做poll,再作有限时的睡眠,直到其中一个设备有事件触发为止。
5)只要有事件触发,系统调用返回,将fd_set从内核空间拷贝到用户空间,回到用户态,用户就可以对相关的fd作进一步的读或者写操作了。
O(n)
1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3)select支持的文件描述符数量太小了,默认是1024
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
poll将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态。它是基于链表存储的,故没有最大连接数的限制。
poll使用一个 pollfd的指针实现。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
O(n)
1)不同点:pollfd并没有最大数量限制(数据量过大后也会造成性能下降),select支持的文件描述符最大为1024;
2)相同点:和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符;
未完待续。。。