文章目录
1、啥是IO复用
网络交互比喻
服务员 和 客户的关系,每天都会有客户到餐厅用餐,而每次用餐都需要服务员的招待,如果没有服务员的招待则会出现餐厅异常的情况,所以当客户的出现则必须要有服务员的接待,
1.1、但是接待会出现两种情况:
(1)一种是每出现一个客户,则专门指派一个服务员进行服务,此服务员只处理这个客户的请求
(2)另一种则是,安排一个服务员同时对接多个食客,也能保证食客的客户请求
(食客 与 服务员的两种图)
1.2、这种接待形式对应的也是我们cs架构中两种形式:
(1)在早期的cs架构中内核没有提供IO复用的接口时,
就是一个client 连接请求过来,server端直接使用一个进程或者线程去对接,这样一个检测fd的接收缓存中是否有数据,如若有数据则进行读取接收,众所周知基于Linux的线程开辟的进程资源都是受限于内核空间,则注定了不能多开,所以一个进程/线程对应一个fd在之后的历史发展中就慢慢淘汰了
fork(callback());
void callback(){
while(1){
int ret = recv();
if(ret > 0){
send();
}
}
}
(2)而第二种也就是今天的主角,一个服务员接待多个食客乃至N多个这种行为叫做IO 复用。
while(1){
int ret = ( io 复用接口(select / poll / epoll));
for(int i = 0 ; i < ret ; i ++){
// 处理每一个IO的事物
}
}
2、server端如何接受数据
2.1、相关知识
2.1.1、阻塞、非阻塞、同步、异步
以出门去商店打酱油的方式讲一下这四种形式
同步:(占用客户资源,己方线程)
(1)阻塞:客户出门之后去打酱油,商店没有酱油了,客户此时一直到商店有酱油了再回来
(2)非阻塞:出门去打酱油,商店没有酱油了,客户就回家,不就之后又来商店看是否有酱油,如果没有酱油,客户就一直执行回家、来店咨询
异步:(占用商家资源,就是对方线程)
客户出门之后去打酱油,商家没有了,客户说有了给我送过来
2.1.2、socket 的组成
socket的是由接收缓存、发送缓存、等待队列和异步通知队列一些成员组成(这些存在都是由内核进行管理)
所以如果应用层要收到数据最先收到的是socket的接收缓存
2.1.3、进程调度:
Linux经常是以多进程存在,但是又时常都是单核的CPU,
所以在同一个时间是没法运行多个进程,就会存在有的进程在运行,而有的进程则在等待,就会有以下两种队列
(1)工作队列
多个业务需要执行时就将数据排列在工作队列,等待时间轮片算法的调度
(2)等待队列
在我们所说的socket阻塞模式下,这些负责管理的阻塞socket的进程就在等待队列休眠,直到socket的接收缓存中有了数据,等待操作系统的唤醒
2.2、网口接收数据
接收流程
(1)我们的数据最开始是先到达至网口,
(2)网口将数据映射至Linux内存中,
(3)通过硬件中断通知到cpu(众所周知硬件中断是级别最高的通知),CPU将网络数据写至socket的接收缓存
(4)数据在接收缓存可读写之后,应用层根据之前所说
-
要么自身的进程一直读取此IO,直到读取到数据(同步操作)
-
要么通过各种IO复用的形式管理这些IO,保证数据已经在缓存中的时候应用层能知道,并将句柄发送给专门的线程池进行读取数据(同步、异步都有)
3、IO复用之select
3.1、Select 使用方式
fd_set set;
FD_ZERO(&set); /将set清零使集合中不含任何fd/
FD_SET(fd, &set); /将fd加入set集合/
FD_CLR(fd, &set); /将fd从set集合中清除/
FD_ISSET(fd, &set); /在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,
当检测到fd在set中则返回真,否则,返回假(0)/
FD_ZERO(&readfds); // 初始化集合
FD_SET(server_sockfd, &readfds);// 将服务器端socket加入到集合中
testfds = readfds;
while(1)
{
select(&testfds); // 内核中针对readfds 集合句柄进行检测 ,将有数据的句柄返回回来
/*扫描所有的文件描述符*/
for(fd = 0; fd < FD_SETSIZE; fd++)
{
/*找到相关文件描述符*/
if(FD_ISSET(fd,&testfds))
{
if(fd == can_connect) // 如果与服务端FD 一致,则接受客户请求建立链接,并将建立链接对应的客户FD加入集合
{
aeecpt(socket);
FD_SET(client_sockfd, &readfds);
}
else if(fd == can_read) // 如果是集合中的客户FD,则读取
{
read(socket, buffer);
process(buffer);
}
}
}
3.2、Select 底层实现原理
回顾知识点:
(1)socket 拥有 读写缓存区、等待队列、异步通知队列
- 等待队列:
当有一个socket事件时,会将其相关的进程放至其等待队列上等待唤醒
(1)select 将其管理所有socket的等待队列都填写的当前进程A
(2)当有一个或者多个socket收到数据的时候,就会将进程A从所有的等待队列中A进程移除,加入到工作队列中
(3)进程将他管理的socket缓存便利一遍即可知道哪些socket有数据了
(4)将对应的集合以比特位的形式返回
其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除(此时已经成为工作队列中节点,不再适合等待队列)。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,应用程序并不知道哪些socket收到数据,还需要遍历一次。