见上文高性能服务器编程(一)"多进程多线程和进程池线程池“
在多进程多线程和进程池和线程池进行多并发操作的时候,进程或线程给一个客户端服务时,大部分是在阻塞等待客户端处理;
目标:当客户端发送来数据后,再分配线程或进程为其服务,这一次服务完成则进程或线程进入阻塞态等待下一次数据到达;所分配的进程和线程不仅仅只为一个客户端服务,而是为每一次数据服务。
这种方式就称为我们的I/O复用。
一、I/O复用
I/O复用:统一管理所有的i/o。一个进程或者一个线程能够同时对多个文件描述符(socket)提供服务;服务器上的进程或线程,如何将多个文件描述符同一监听,当任意一个文件描述符上有事件发生,其都能同时处理。
I/O复用使用最多的场合是TCP服务器要同时处理监听socket和连接socket。另外,在客户端程序要同时处理多个socket、客户端程序要同时处理用户输入和网络连接、服务器要同时处理TCP请求和UDP请求以及服务器要同时监听多个端口或者处理多种服务等情况下也会使用I/O复用技术。
这里主要有三种I/O复用的方式:select、poll、epoll(Linux上独有的);
这里主要实现:单进程单线程同时处理多个文件描述符。
二、select:启动监听,本身是会阻塞的
1、函数原型以及参数解释
Int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *excefds,struct timeval *timeout );
Nfds:最大文件描述符的值+1;提高服务器的效率;不用在整个df_set中进行检测,只需要检测在最大文件描述符之前的序列;
fd_set:用来记录文件描述符; 监听的文件描述符
不但需要它往内核传递关注的文件描述符,也需要返回就绪的文件描述符,将就绪的文件描述符和未就绪的都返回。
所以每次调用select都必须重新设置read write except
Readfds:用户关注的或者说是感兴趣的可读事件的文件描述符集合;
Writefds:用户关注的或者说是感兴趣的可写事件的文件描述符集合;
Excefds:用户关注的或者说是感兴趣的异常事件的文件描述符集合;
Fd_set的结构体设置:
这是书上所给出的fd_set的结构体,观察这样的结构体我们可以将其简写为:
Struct fd_set
{
Unsigned long fds_bits[32];
}fd_set;
Fd_set àlong fdset[32];--à1024位,1024个文件描述符(0~1023)
Timeout:设置超时时间;如果timeout为NULL,则select一直阻塞;null代表永久阻塞,直到有文件描述符上有事件发生;
返回值:
>0:返回就绪文件描述符的个数;
==0:超时;
==-1:出错;
2、select的用途
在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
3、select所要解决的问题以及解决办法
如何将文件描述符分别设置到readfds,weitefds,execfds?
答案:进行按位设置将文件描述符分别设置到readfds,weitefds,execfds;
系统为我们提供了按位操作的一组宏函数:
Select返回后,如何知道哪些文件描述符就绪?
利用上述第四个宏函数,就能知道哪些文件描述符就绪。利用FD_ISSET进行循环探测。
4、每次调用select之前重新设置fd_Set
每次select调用之前都要重新设置readfds, writefds, excefds。
因为在用户到内核态传递的时候,将所关注的事件对应的文件描述符设置为1,传递进去,表示当前有事件需要处理;然后内核态获取事件后,在处理结束之后,返回给用户态的时候,内核态会将它修改为0。
Select每次都会将所有的文件描述符(包括就绪的和未就绪的)返回,select返回后还必须循环探测具体是哪些就绪的文件描述符。应用程序在探测就绪文件描述符时的时间复杂度为0(n)。
5、select编程流程
流程如下:
1.TCP服务器设置 sockfd bind listen
2.将sockfd添加到fds中
3.启动while循环
3.1将fds中的文件描述符设置到readfds中
3.2启动select
3.3循环探测哪些文件描述符就绪
3.3.1sockfd---》有客户端完成三次握手 accept Insert_fd
3.3.2链接fd---》客户端有数据到达,recv>0 业务逻辑处理;<=0:close(fd),delete(fd);
6、文件描述符的就绪条件
在哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。下列情况下socket可读:
(1)满足下列条件时,套接字准备好读:
Sockfd内核接收缓存区中的字节数大于或等于其低水平为标记SO_RCVLOWAT。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0;
Socket通信的对方关闭连接。此时对该socket的读操作将返回0;
监听socket上有新的连接请求;
Socket上有未处理的错误。我们可以使用getsockopt来读取和清除该错误;
(2)满足下列条件时,套接字准备好写:
Socket内核发送缓存中的可用字节数大于或等于其低水平位标记SO_SNDLOWAT;此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0;
Socket的写操作别关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号;
Socket使用非阻塞connect连接成功或者失败超时之后;
Socket上有未处理的错误。此时我们可以使用getsockopt来读取和清楚该错误;
7、select的缺点
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
文件描述符就绪时,内核会修改readfds、writefds和execfds结构,所以每次调用select之前,必须重新将文件描述符注册一遍;
每次调用select都需要在内核遍历传递进出来的所有fd,这个开销在fd很多时也很大,时间复杂度为O(n);
但个进程能够监视的文件描述符存在最大的限制;
8、在I/O复用中添加进程池和线程池
这里的进程池和线程主要来做的是业务处理,这就和我们后期要提到的Reactor模式。
小结:
select是实现我们当客户端发送来数据后,再分配线程或进程为其服务,这一次服务完成则进程或线程进入阻塞态等待下一次数据到达;所分配的进程和线程不仅仅只为一个客户端服务,而是为每一次数据服务这个目标的方法之一。
它在用户态维护一个32位的数组来保存就绪事件的文件描述符,当要对其进行处理的时候,设置事件对应的文件描述符为1并将数组拷贝到内核中进行处理,内核处理完成之后,并会修改对应位的文件描述符为0,然后再将所有的文件描述符(就绪的和未就绪的都需要返回)拷贝回到用户态。并且在每一次进行select的时候都需要重新设置readfds、writefds以及execfds。切记,select记录文件描述符是按位设置的。每次想要知道多少文件描述符就绪,必须进行循环探测。利用的是系统为我们提供的FD_ISSET这个宏函数。