从思维角度去探讨epoll机制

服务器中的epool

当前网络服务器开发中,只要涉及到并发IO,那么epool是逃不掉的话题。本文从网络通信的基本原理出发,慢慢引申出如何将一对一的通信,转化为IO复用,最后引出select/epool机制。

epool允许在多个非阻塞的socket描述符上等待可读、可写事件,本质是一个事件驱动模型。简单来说,假设我们当前的
server 有
10 万个
TCP 连接,在这些
TCP 连接这种,能够读/写数据的连接并不是
10 万,可 能只有
5000,或者更少,这是因为用户不可能实时活跃。

如果说我们能够直接找出这
5000 个活跃的连接进行处理的话,那么系统效率将得到巨大的提升,epoll 的本质作用就是在 这
10 万个连接中找到这
5000 个活跃连接

基于socket编程流程

socket这个单词,在英文里面是插座的意思。为何跟插座联系在一起呢?发挥你的想象力吧!当我们将一个插头插入到一个插座上的时候,这个场景,跟网络通信有啥广义的相似性呢?

socket可以看做一个文件描述符(具备IO的属性),也可以认为是网络通信的端点。

服务端的TCP socket编程套路如下:

  1. socket();创建一个socket
  2. bind();绑定socket到IP/port
  3. listen(); 监听连接
  4. accept();从客户端接收连接
  5. recv()/send();数据传输
  6. close();关闭socket

注意几点:

  1. 客户端在执行connect的时候,如果已经返回,则三次握手已经完成,跟服务端的accept没有任何关系。即时在accept()之前建立的三次握手。
  2. 内核为每一个listen状态的套接字设置两个队列,未完成连接队列和已完成连接队列,这两个队列共用listen设置的连接长度。
  3. 当客户端发送syn报文后,服务器收到这个syn报文并检测连接队列是否已满,如果满了就丢弃这个syn,如果没满就把这个链接放入未完成队列,并发送ack和syn报文,待客户端收到ack和syn之后,会发送ack报文到服务端,且从connect函数返回,服务器收到这个ack后把连接从未完成队列中取出放入已完成队列,等待accept把这个链接取走,此时三次握手已经全部完成。
  4. 没有特殊情况下,很快就会从未完成队列中拿走。如果服务端在发送ack和syn后,超过一定时间(75秒)内没有收到ack,那么服务器会将这个链接丢掉。
  5. accept()仅仅是将已完成队列中的链接取出来,如果没有链接,则会阻塞

通过epool将一大串知识点联系在一起

需要从线程阻塞、中断优化、网卡处理数据过程等角度,去剖析epool是怎么被发明出来的,epool内在的本质究竟是什么。而不是仅仅背诵一些生硬的概念,有些情况下,知道了系统是怎么设计的以及为何这么设计,就从更深层次理解了系统。

阻塞和非阻塞

linux下所有的IO模型都是阻塞的,这是收发数据的基本原理导致的。阻塞不是问题,因为阻塞不会消耗cpu,IO多路复用也不是减少了阻塞,而是减少了运行。其实IO多路复用,通过引入新的处理方式,有效地减少了上下文切换。

当进程被阻塞/挂起时候,其实是不占用cpu资源的。“可运行状态”会占用cpu资源,创建和销毁进程会占用cpu资源。

为了支持多任务,Linux实现了进程调度的功能,即cpu时间片的调度。

为了方便时间片的调度,所有可运行状态的进程,会组成一个队列,叫工作队列。内核可以根据数据包中的端口号,很容易地找到对应的socket,将对应的进程状态修改为可运行的状态,即加入到工作队列,此时用户可以处理数据。

同步和异步

Linux下所有的IO模型都是同步的,BIO是同步的,select是同步的,pool是同步的,epool仍旧是同步的。
java提供的AIO,或许被称作异步的,但是jvm是运行在用户态的,Linux没有提供任何的异步支持。所以jvm提供的异步支持,是自己封装的。
所谓的同步和异步,只是两种事件分发器,英文是event dispatcher。

select和epool的形象区别

select需要O(n)查找的时间复杂度,而epool的时间查找复杂度是O(1),即epool返回的全部都是就绪的待执行socket,而select还需要遍历一遍才知道是不是就绪的socket。

select需要傻傻地遍历一遍所有的关心的socket,而epool只需要处理关心且活跃的socket即可,回调函数已经帮自己完成的筛选的操作。越是socket数量越大,而活跃的socket相对越少,越能体现epool的优势。

比如有一万人站在你面前,你要跟其中活跃的10个人进行通话/通信/讲话,但是select需要莎莎地问一下这一万人中的每一人,你是不是需要通话的那个人。而对于epool而言,直接相当于将这10个人领出来了,你只需要分别跟着10人一个一个地聊天即可,省去了从10万人中将这10个人筛选出来的成本。

select对应的示范代码

int fds[] = ... //socket数组
fd_set read_fds, temp_read_fds;
FD_ZERO(&read_fds);
for(int i=0; i<fds.count; i++){
	FD_SET(fds[i], &read_fds);
}
while(1){
	temp_read_fds = read_fds;

	//这个就是监视器,会阻塞进程
	//temp_read_fds既作为入参,又作为出参
	//将全部符合条件的socket又填充到temp_read_fds作为出参
	int n = select(..., &temp_read_fds, ...);
	for(int i=0; i<fds.count; i++){
		if(FD_ISSET(fds[i], &temp_read_fds)){
			//梳理业务逻辑
			FD_CLR(fds[i], &read_fds);
		}
	}
}

epoll对应的示范代码

int fds[] = ...//socket数组
int efd = epoll_create(..)//在内核空间,创建epoll实例,红黑树+就绪链表
for(int i=0; i<fds.count; i++){
	epoll_ctl(efd, ..., fds[i], ...);
}

struct epoll_event[MAX_EVENTS];//用户空间分配内存,用来出参
while true{

	//这个是监视器,会阻塞进程
	//内核会利用红黑树,快速查找select需要的socket,放入就绪列表
	//将就绪列表中,取一定数量到event,这里events作为出参
	int n = epoll_wait(efd, &events, ...);
	for(int i=0; i<n; i++){
		//取出待处理的socket
		events[i].data.fd;
	}

}

大脑要能够想象出,用户空间跟内核空间交互的时候,这些辅助数据结构是如何发挥作用的。

可以看到,系统设计到最后,可以归为是用什么样的数据结构+处理逻辑进行排列组合,以降低整体的时间复杂度和空间复杂度。所以通过各种场景对自己进行数据结构算法的训练,是非常非常重要的。

数据结构算法通过各种载体来训练

将基础的程序组件,都当做数据结构算法的练习题目即可。比如数据库,其实本质就是数据结构算法的一种运用而已,而所谓的架构设计,也不过是配列组合资源,从时间空间复杂度角度去思考解决方案。再往上抽象其实锤炼的是思维能力,而设计程序仅仅是训练思维能力的一种载体。

通过诸多练习题目来锤炼思维能力

那么思维能力又包括很多种,逻辑思维,发散思维,演绎思维,抽象思维,形象思维,灵感思维等等,最后转化为将培育思维能力,作为重中之重。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值