一.背景
最初,在学习并发编程时,无论是多线程还是多进程,我们总是会在我们应用程序中调用各种各样的系统调用(如accept()、read()、recv()等)来监听客户端的各种事件,这样显然会使得我们的应用程序显得庞大,效率自然而然的会下降,所以,此时多路IO转接服务程序就产生了。
二.多路IO转接服务器
多路IO转接服务器是只应用程序中调用了系统内核的一种函数,这种函数实现了帮应用程序监控客户端事件是否发生,如果对应事件发生,告诉我们应用程序哪个文件描述符上有事件发生。这样我们的应用程序直接去处理事件即可,缓解了时刻监听所有文件描述符的压力。
有点像代理的意思,或者说是中间层,内核帮我们找到事件发生的文件描述符
我们自己的应用程序直接去处理就好了,就不需要我们等待什么的,客户和服务器交互立即可以完成
举个例子:
服务器和客户端建立链接,执行accept()一般会阻塞等待,但是我们可以将socketfd
交给内核,让内核帮我们监控起来,如果有事件(链接的事件)发生,
服务器就可以马上和这个sockfd建立链接,如果没有事件发生,这个时间我们可以做其他事
就不用一直等他
当我们建立链接以后又得到一个fd,我们又可以把这个fd交给内核帮我们监控起来
三.内核暴漏的接口 :select()
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
四.select()函数参数以及对应操作函数的解释:
1.
返回值:
返回所有监听集合中,有对应事件发生的文件描述符的总数
2.
参数解释:
int nfds:
监控的文件描述符里数值最大的文件描述符+1,
例如,所有需要select监控文件描述符中数值最大的是100,则该值即为101
select是从0开始一直遍历到数值最大的标识符。
fd_set *readfds:
监控有读事件产生的文件描述符的集合,实际上是个位图,传入传出参数
加入到这些集合中,只是觉得有可能发生对应的事件,也有可能不会发生
fd_set *writefds:
监控有写事件产生的文件描述符的集合,实际上是个位图,传入传出参数
fd_set *exceptfds:
监控有异常事件产生的文件描述符的集合,实际上是个位图,传入传出参数
struct timeval *timeout:
原型:
struct timeval {
long tv_sec; /* seconds 秒 */ 秒
long tv_usec; /* microseconds 微秒 */
};
定时器,告诉内核监控这些事件多长时间,分三种情况:
1.NULL,永远的等下去
2.设置timeval,等待固定的时间
3.设置timeval里事件为均为0,检查描述字后立即返回,轮询的方式
3.
void FD_CLR(int fd, fd_set *set);
//将fd从set中清除出去,也就是将对应位图的对应位置0
int FD_ISSET(int fd, fd_set *set);
//判断对应的fd是否在set中
void FD_SET(int fd, fd_set *set);
//将对应的fd添加到对应的set中
void FD_ZERO(fd_set *set);
//将对应的set进行清空,也就是置为0
五.优缺点总结
1.可以监听的事件类型:读、写、异常;
2.返回值:
假设我三种类型的文件描述符集合分别为:
r: 7 8 9 (7 8 有读事件产生)
w:8 10 27 (8 有写事件产生)
e: 7 8 9 10 11 (7 8 11有异常事件产生)
那么返回值将返回5,因为有五个文件描述符有事件产生,即使可能有重复的
是三个集合中所有有事件产生的文件描述符之和
(当然此时函数的第一个参数可能是11 + 1 =12)
缺点:返回值没有告诉我们的应用程序到底是哪个描述符发生了什么事件,需要我们自己去判断
3.对于三个传入传出参数,传入时为了让内核使用,让内核根据监听,修改对应位上哪个fd有事件产生
就置位1
等到定时器事件到了,然后再将内核修改的fd个数返回回去,
可惜的是,具体哪个fd,哪个事件,还需要我们应用程序自己去判断
4.文件描述符上限 1024 ,也就是最多可以监听的fd数据
因为在unix那个时期,由于硬件水平低,1024已经足够了,不然内存大小跟不上
相当于这个1024是写死了,但是实在想改,是可以的,需要重新编译linux内核
5.加入select()一共监听2和1023这两个fd,返回时返回值为2
但是我不知道到底是哪个,所以需要我们从2到1023循环遍历,挺影响效率的
6.传入和传出的集合都是同一个集合,所以我在修改前需要将原来的集合做一份拷贝
当监听的比较大的适合,拷贝也费时间
传入select()前的读事件对应的位图:
置1的是我们希望select()给我们监听的文件描述符,这里是三个,如图
当select()返回,就说明监听的三个集合中的某一个或者某几个集合中一些fd有事件发生
传出后的读事件对应的位图: 满足保持置1,不满足的修改为0,然后传出给用户
这个例子中,我们传入三个fd,最终有两个fd有事件发生
其他两个事件类型对应的原理是一样的。
顺序:
初始化
fd_set readfds;
FD_ZERO(&readfds);
给readfds添加我们期望监控的fd
FD_SET(fd,&readfds);
......
然后调用select(),等到函数返回后
FD_ISSET(fd,&readfds); //判断fd是否发生了读事件
###########################################################################
补充几点,以前一些东西可能没有解释清楚:
1)select的实现,在内核里它是被定义成一个数组的。数组肯定就有大小,数组的大小被默认定义了1024,默认的情况下只能支持1024个socket的管理。
2)select返回后,应用层需要不断轮询这个fd_set,去判断socket期望的是否发生。
3)select返回后,会把内核管理的这个fd_set清空,依然需要把用户态的socket拷贝到内核的select管理的fd_set,发生频繁从用户态的数据copy到内核里面。
poll的实现,对select进行了改进,就是用链表去保存了socket。也就是说克服了select对socket数量的限制。
高并发的情况下使用select或者poll能支持的并发是有限的。 C10K(一万个左右的并发)
因此内核为了解决这个问题,内核重新设计的多路并发,也就是使用epoll来管理了socket。