接着昨天的继续学习: 里面会用到昨天学过的东西
博客连接如下: https://cloud.tencent.com/developer/article/1997421
今天的目标, 学epoll
为什么学epoll, 用redis举例. epoll是所有模型中, 占用内核空间最小的, 执行速度最快的.
redis用了epoll, nginx也是用了epoll
什么是socket?
计算有内核, 内核上面有app, app有server服务端和client客户端.
进行socket通讯,server端首先会有一个端口号, 然后绑定端口号, 然后监听端口号
看看ooxx中打印的启动redis的时候的进程消息, 搜索socket, 可以看到有绑定端口号6379, 监听端口号6379
我们可以用man命令来看一下socket的文档
man 2 socket
这个命令的含义时,
man: 查询linxu的文档,
2: 查询2类文档
socket: 查询的内容
1. 操作系统内核提供了一个socket
这个socket会做一件什么事呢?
他有一个返回值, 返回一个file descriptor文件描述符, 简称fd, 其实就是一个数值
拿着这个数值可以做什么事?
比如: accept, bind, listen等
接下来我们来看bind的example.
我们是怎么创建socket连接的呢?
第一步: 创建一个socket (sfd=socket(AF_UNIX, SOCK_STREAM,0)) ,返回一个文件描述符
第二步: 拿到文件描述符fd, 去绑定, 然后监听.
第三步: 执行accept等待, 等待看看有没有进入到这个fd的东西, 然后得到一个客户端的文件描述符cfd.
开看看redis是如何建立socket连接的
首先, 程序开始没多久, 就建了了一个socket连接, 返回文件描述符6
第二: 给文件描述符6绑定端口号
第三: 监听文件描述符6
比如,现在有一个客户端, 要建立socket连接, 按照上面的步骤
连接建立成功了. fd6代表的是服务端, fd8代表的是客户端.
建立连接, 目的是要获取数据,
这时候会有一个read(cfd8), 参数是cfd8
我们可以查看是否有这个系统调用: man 2 read
read是一个阻塞的状态. client建立连接了, 可能一直也不发数据. 他阻塞了, 其他的连接就进不来了. 这是一个主线程.
那么怎么解决这个问题呢?
建立多线程. 让一个连接开启一个线程, 什么意思呢? read不是阻塞的么? 那么阻塞不要发送在主线程, 我给你重新开一个线程, 你去那里面阻塞去.
这样新建一个客户端连接, 就要多开一个线程
这就是早期的模型-----一客户端一线程的多线程模型. 有客户端发数据, 就发数据, 然后在相应的线程处理就可以了
我们使用了一连接一线程的多线程模型. 为什么使用多线程模型呢?
因为阻塞. 建立socket连接后, 等待客户端连接进来, 连接以后, 调用内核的系统方法read, 这是一个阻塞的方法. 一阻塞就执行不动了, 卡死了
我们知道是因为阻塞使用的多线程, 如果客户端一年不发消息, 这个线程就阻塞1年, 10年不发消息, 这个线程就阻塞10年, 系统浪费太严重了.
那么我们要解决的问题, 就是阻塞.
但是,我们知道这个方法是系统内核的方法. 程序调用系统内核的socket, 程序调用系统内核的bind, listen, accept, 也是程序调用系统内核的read. 程序改版不了阻塞这件事, 只有内核改变, 这事才能解决.
于是内核升级了, 从此进入了NIO的时代
先来看man手册
man 2 socket
这里有一个SOCK_NONBLOCK, 非阻塞的socket
NIO有两层含义, 一个是new io, 一个是non io, java中NIO的含义是new io的意思
我们来看看NIO是如何实现的
和上一个模型对比,
解决了BIO一个客户端一个连接一个线程的多线程问题. 现在NIO一个线程就可以和多个客户端建立连接了.
发生这个问题的根本原因是while循环, 那就要减少循环的次数. 如果有一个客户端有连接, 那就直接返回这个连接就可以了
所以就有了多路复用
我们先来看看一个手册
man 2 select
系统调用里面多了一个系统命令, select
select什么意思? 我们来看看他的是参数
调select可以做意见什么样的事情呢? 看文档下面的描述
select允许程序监控多个文件描述符, 等待直到有一个或多个文件描述符状态达到ready.
这个时候是怎么做的呢?
1. 在while循环的地方, 有一个, 首先调用系统方法select(10w), 把10w个连接传给内核. 这个过程的复杂度是O(1)
程序不用循环遍历了, 循环遍历转移到了内核. 但是内核依然会循环遍历10w次, 其实内核的遍历里面可能也是只有1个链接是有效的, 剩下的99999个链接都浪费了.
分析问题:
下面说什么是事件
我们知道, 操作系统有一个cpu, 还有一块内存. 内存里放的是程序. 程序有内核程序和用户自定义的程序. 除了cpu和内存, 还有网卡, 键盘.
那么问题来了? 如果只有1核cpu的时候, 如何保证网卡能上网, 键盘能敲字, 用户的应用程序也能正常运行. cpu是怎么保证他们都能工作的?
1核cpu怎么保证他们之间交替运行的?
中断, 有一个中断器, 在cpu中有一个晶振器 -- 时间中断. 晶振器一秒钟震动1w或者10w次
假如, 每秒振10下, 每下就是1/10秒
每次震荡, 会产生一个时间中断, 时间中断器会告诉cpu , 放下手里的事.
放下手里的事干嘛呢? cpu就是一个硬件, 他知道有哪些程序呢? 他不知道
中断有一个中断号, 中断号会在内核里维护一个中断号的callback
硬件, 网卡等驱动程序, 会根据自己的硬件获得一个中断号, 埋一个callback函数到内核
硬件, 网卡等驱动程序, 会根据自己的硬件获得一个终端号, 埋一个callback函数
切换程序可以靠晶振来切换
用户程序想调用内核, 会埋一个软中断, 表示从程序切换到内核去
硬件调用中断器, 会埋一个硬中断
这样就保证了, 1核cpu在在中断产生的时候, 接管所有事情的发生
这时候, 客户端通过电缆, 一堆的010101访问到网卡了. 网卡有缓存, 但缓存是有限的, 随着数据量的增大, 缓存存不下了, 也不能立刻让cpu取走, 这时候怎么办呢?
这时候在内存里, 会开辟一块空间, 用来存网卡数据的, 这块空间叫DMA. Direct Memory Access,直接内存存取
网卡接收到数据了, 就放到DMA里面
这时候, 假如发生了时间中断. 网卡的中断号是88, 这时候在内存中, 有一个88对应的callback函数, 这个函数就指向了DMA的地址, 看看DMA中都有什么数据.
callback从DMA读取的东西, 内核处理完之后, 再有相应的事件, 通知到应用程序的进程, 应用程序再把数据拷贝过来,进行计算. 这是整体的流程
计算机在处理网卡, 键盘等的工作的时候, 是通过事件通知到应用程序的, 而不是一直循环遍历等待.
首先, 客户端到达的时候, 会有一个中断和callback事件, 其实callback事件调用后, 内核间接知道了有网络数据包到达. 根据这个知识点, 回到我们的socket通信上来.
多路复用, 在内核里会有10w次的循环遍历, 其实我们没必要一直循环遍历, 我们可以定义一个事件, 客户端有消息到达了, 触发事件, 返回应用程序就可以了.
先来看manual手册
man epoll
epoll -- I/O event. 表示io里面有事件了
epoll的体系中, 有epoll_create, epoll_ctl, epoll_wait, 并且都是2类的系统调用
我们来看看redis的日志
ooxx/目录下
这里有两个系统调用, epoll_create和epoll_ctl. 然后还会有很多epoll_wait. 不停的等待. 循环等待
下面来了解一下epoll
不管是bio , nio, epoll, socket通信是不变的. socket连接, bind, listen都是一样
接下来我们看看epoll是怎么通信的?
第一步: 调用epoll_create. 调用epoll_create会得到一个什么东西呢?我们来看一下2类的系统文档
man 2 epoll_create
如果调用成功, 系统会返回一个二类的文件描述符
通过看redis的调用也能看的出来.
调用epoll_create, 返回了一个文件描述符5
我们知道socket连接返回的文件描述符是监听用的, 那epoll返回的文件描述符干什么用的呢?
在计算机内核里开辟了一块空间, 用文件描述符fd5 来表示. 接下来做什么了呢? 我们来看看redis调用
第一步: 调用了 epoll_create, 返回文件描述符5, 在内核开辟了一块空间, 命名为fd5
第二步: 调用socket建立连接, 返回文件描述符fd6
调用bind为文件描述符fd6绑定端口6379
然后调用了listen(fd6), 监听,文件文件描述符fd6
这里建立了两遍socket连接, 一个是ipv4的, 一个是ipv6的
第三步: 调用了epoll_ctl, 把文件描述符fd6放到了内核空间fd5里面. 放在里面干嘛呢? fd6要监听accept事件. accept是客户端和服务端建立连接的事件
第四步: epoll_ctl调用了以后, 开始调用epoll_wait, 疯狂的等待, 他在等什么?
备注: ctl是control控制, 表示对这个文件描述符执行什么样的操作
比如这时候, 有一个客户端连接来了, fd6的accept就会知道有客户端想要建立连接, 这时候, 会吧fd6放到到内核里的另外一块区域里
(fd5那块空间是一个红黑树, fd6是一个list)
然后epoll_wait就会有返回值, 返回的是一个链表. 只不过现在只有一个. epoll_wait拿到了返回值会做什么事呢?
程序拿到fd6了, 然后调用fd6的accept和客户端连接连接的事件. 返回cdf9
第五步: 再次调用epoll_ctl, 将cfd9放入到fd5这块内核空间里, 为什么这么做呢?因为,我不知道客户端什么时候会发消息, 是一年, 还是2年? 放到fd5中,监听cfd9的read时间
第六步: 再次调用epoll_wait. 这次在等什么呢?
假如这时又有一个客户端连接来了. 又会触发fd6的accept事件. 这时会吧fd6和cfd9 都放入链表中. 程序调用fd6的accept时间, cfd9的read事件.
第七步: 继续循环, 不停的调用
解决在多路复用里面的两个问题
内核开辟的这块空间, 保存所有的文件描述符. 剩下的事情, 就是等待,不停的等待