Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。
I/O多路复用模型允许我们同时等待多个套接字描述符是否就绪。Linux系统为实现I/O多路复用提供的最常见的一个函数是select
函数,该函数允许进程指示内核等待多个事件中的任何一个发生,并只有在一个或多个事件发生或经历一段指定的时间后才唤醒它。
select模型如下图所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
在服务器端,服务器通过调用socket()
函数创建一个socket对象,通过bind()
方法绑定一个端口号与IP,在调用listen()
方法完成初始化后,调用accept()
阻塞等待,处于监听端口的状态,客户端调用socket()
初始化后,调用connect()
发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()
返回,同时应答一个ACK段,服务器收到后从accept()
返回。
socket
连接成功之后,用户首先将需要进行IO操作的socket
添加到select
中,然后阻塞等待select
系统调用返回。当数据到达时,socket
被激活,select
函数返回。用户线程正式发起read
请求,读取数据并继续执行。
socket()
函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()
用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。当我们调用socket()
创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()
函数,否则就当调用connect()
、listen()
时系统会自动随机分配一个端口。
select
是一个复杂的函数,有许多不同的应用场景,我们将只讨论第一种场景:等待一组描述符准备好读。
select
函数的参数有多个。其中,参数n指定需要测试的描述符的数目,测试的描述符范围从0到n-1。第二个参数fdset指定需要测试的可读描述符集合。当fdset集合中有描述符可读,或者经历了timeout时间时,select将返回。当select返回时,作为一个副作用,select修改了参数fdset指向的描述符集合,这时fdset变成由读集合中准备好可以读了的描述符组成。select函数的返回值则指明了就绪集合的基数。值得注意的是,由于这个副作用,我们必须每次在调用select时都更新读集合。
#include <unistd.h> #include <sys/types.h> int select(int n, fd_set *fdset, NULL, NULL, struct timeval *timeout); FD_ZERO(fd_set *fdset); // 将fdset初始为为空集合 FD_CLR(int fd, fd_set *fdset); // 从fdset清除fd FD_SET(int fd, fd_set *fdset); // 将fd添加到fdset FD_ISSET(int fd, fd_set *fdset); // fd是否存在于fdset
通过调用select()
函数,在用户空间中保存了已连接的 Socket
文件描述符数组的fds
,会被映射到一个set结构的bitmap
(bitmap
的每一位对应fds的下标,表示需要监听的socket
),select()
函数将bitmap
结构拷贝到内核空间中,让内核去监听网络事件是否发生,我们调用select
可以告知内核我们对哪些描述符感兴趣以及等待多久时间。
在内核层面中,内核就是让CPU就是不停遍历检测bitmap
中每一位对应的文件描述符,检查这个文件描述符是否有IO网络事件的发生。如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET
错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上),关系到套接字列表的操作都需要使用循环,在轮询的时候,需要遍历一次,再新的一轮开始时,将列表加入队列又需要遍历一次.也就是说,Select在工作一次时,需要至少遍历2次列表,这是它效率较低的原因之一。
select调用的性能影响因素主要在于:
1.每次调用时要重复地从用户态读入参数。
2.每次调用时要重复地扫描文件描述符。由上面对select()
函数的分析可知,select()
函数主要的工作在于内核中不停遍历检测bitmap
中每一位对应的文件描述符,检查这个文件描述符是否有IO网络事件的发生。
3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。
4.select调用中使用到的bitmap结构为1024位,最多只能监听1024个文件描述符(监听1024个socket)