# 计算机组成部分
以下几个问题需要弄明白
- 什么是内核
- 什么是系统调用
- 为什么内存区域需要划分用户空间和内核空间
内核是操作系统的重要组成部分(废话),进程通过内核程序调用计算机硬件。系统调用就是用户空间程序调用内核程序的过程。内核
封装底层硬件操作,提供接口调用,从安全角度看,用户空间随意访问内核程序占用空间的数据是不安全的。
上下文切换和中断,中断一般伴随着上下文切换,中断分为软中断和硬中断,什么是中断?cpu接收到来自硬件和软件的发生某事件的信号,中断会打断正在正常执行的程序,开始处理事件。
<<Unix网络编程>>一书中介绍了五种IO模型,每一种IO都是对前一种的升级优化,阻塞IO,非阻塞IO,多路复用IO,信号驱动IO以及异步IO。
程序指令如何运行
- ALU:执行运算
- Register:存放从缓存或者主存中
- PC:记录当前线程执行到哪一步指令
- Cache:弥补内存与cpu之间速度差异,L1-3.其中L3同一物理cpu的多核之间共享,L1和L2私有。
- 超线程:就是单ALu多组(寄存器+程序计数器),因为ALU速度最快
# Socket
# 模拟
Linux中一切皆文件,操作系统上socket的表现形式就是文件描述符,文件系统中有存放有表示进程打开的socket的文件描述符,具体在/proc/{pid}/fd下。 socket建立一定有唯一的四元组组成。
# 1.创建连接
exec 8<> /dev/tcp/www.chenjiwei.cn/80
# 2.请求
echo -e 'GET / HTTP/1.0\n' 1>&8
# 3.输出返回
cat 0<&8
## 4.查看socket
cd /proc/$$/fd
2
3
4
5
6
7
8
- url可以随意。建立与目标url的socket通信,8:数字可任意指定,<>:输入输出流
- 1表示输出,&8类似指针引用,写get请求到文件描述符为8的socket中
- 0:表示输入,从socket获取返回输出到控制台上
- $$:当前进程ID
# 过程
- socket 文件描述符
- bind 文件描述符到指定端口
- listen 监听这个文件描述符
- accept 返回新连接的客户端=fd
# tcp
定义:一种可靠的、面向连接的传输层协议。三次握手和四次挥手是不可分隔的最小单元。
# 线程
从linux内核来看,线程是通过系统调用clone函数,通过内核clone一个轻量级的线程(附属父进程)
# 阻塞IO(BIO)
# 阻塞读
# 阻塞写
# 缺点
- 连接和线程一一对应,并发量大的情况,系统可能无法创建线程。
- 建立连接后,如果没有发送数据,服务端新建的线程就会处于阻塞状态,浪费系统资源。
# 场景
- 连接数少,并发低的业务场景
# 非阻塞IO(NIO)
NIO出现的目的是为了使用更少线程数来处理更多的连接。
当用户线程处理连接的socket(编程意义上的),当缓冲区没有数据时,系统调用会直接返回错误标志,这样用户线程就能处理下一个socket,达到使用较少
线程处理更多连接的目的,实现的基础是内核支持failfast。
不同于阻塞IO的阻塞写(一定要一次性梭哈,写完数据),非阻塞写的特点在于能写多少是多少,当缓冲区没有多余空间,用户线程就会返回缓冲区空间字节数给应用程序,
方便用户线程轮询写完剩余数据到缓冲区中。
# 场景优缺点
- 避免了每个连接每个线程
- 用户线程轮询去发起系统调用,用户线程就会不停在用户态和内核态之间不停切换,这种上下文切换在高并发下开销也是巨大的
- 无意义轮询读写没有发生变化的socket
# IO多路复用
主要理解两个概念,多路和复用,多路就是处理多个连接,而复用就是使用更少的线程,其实这就是非阻塞IO的实现,但是NIO存在上文提到的缺点,为了
避免频繁的system call
,可以把轮询socket的工作移交到内核,这就需要内核支持这样的操作,用户线程在用户态只用一次系统调用就可以了。
# select
用户线程把各个连接上的FD放在一个BitMap数组结构中,下标为表示连接的文件描述符,下标对应的值,1表示该fd上有读写事件,0表示该fd上没有读写事件,
内核会轮询这个数组,设置socket上有读写事件的下标为1(这里修改的是原数组)
,然后完成系统调用解除阻塞状态,用户线程轮询数组做后续处理。所以select模型适合1k左右的并发
性能瓶颈
- 由于内核在原数组进行打标,用户线程每次系统调用前都要重置数组
- BitMap数组长度限定为1024,所以SELECT模型只能处理1024个连接
- 由于大多数网络连接都是不活跃的,内核每次遍历整个fd集合都会产生大量不必要的开销
- 内核不会单独返回就绪IO的集合,用户线程就需要自行遍历找出被内核打标的fd
- select系统调用并不是线程安全的
# poll
poll是select的改进版本,仅仅改进了select只能监听1024个文件描述符数量的限制。使用没有固定长度的数组替代固定长度为1024的bitmap,受限于操作系统文件描述符的限制, 在性能上没有没有作出改进,和select本质上没有多大差别。
# epoll
阻塞io中用户进程阻塞和被唤醒的原理
在阻塞io中提到,当socket缓冲区中没有数据,用户线程会让出cpu,并进入阻塞状态,那么用户线程如何阻塞在socket,又是如何被内核唤醒的呢? 简单来讲就是socket会开辟一个等待队列,把用户进程存放在此,并注册回调函数(作用在于当数据到达socket之后唤醒用户线程)...具体函数和对应的数据结构就省略了...
socket创建过程
服务端调用accept之后,进入阻塞状态,客户端经过tcp三次握手之后,内核会创建一个socket作为服务端和客户端通信的内核接口,然后将这个socket放在当前进程打开的 文件列表中管理起来(/proc/{pid}/fd下,一般存在0:标准输入,1:标准输出,2:错误输出)
- epoll_create
- epoll_ctl
- epoll_wait
epoll_create创建epoll对象,select用数组管理连接,poll用链表管理,epoll使用红黑树并且只返回活跃的socket连接,避免用户线程遍历全部连接查找活跃socket。红黑树在查找
、删除、插入综合性能是这几种数据结构中最优的。
epoll_ctl可以向epoll对象中添加需要管理的socket连接。用户程序调用epoll_wait之后,内核会返回IO就绪的socket到回调事件,epoll回调函数是区别于select和poll采用轮询方式性能根本差异所在。
# 信号驱动IO
信号驱动io类比美食城取餐,老板会给个信号器,此时可以去找座位刷手机(做别的事),当信号器滴滴滴响的时候,我们就需要去取餐,由于取餐(拷贝数据的过程)还是自己来,所以这还是个同步IO模型。
但是在实际,使用TCP协议通信时
,AIO几乎不会别采用,原因如下:
- 信号IO在大量IO操作时可能会因为IO信号队列溢出导致没办法通知
- 由于在unix系统中,
SIGIO信号
没有附加信息,产生TCP信号的事件不一定是数据到达缓冲区的时间,所以无法区分是否该通知用户程序读取数据。
# 异步IO(AIO)
以上四种模型都是同步IO模型,他们都会阻塞在数据拷贝阶段。异步IO在数据准备阶段和数据拷贝均有内核完成,应用程序在指定数组引用数据
即可,不会造成任何阻塞。信号IO和异步IO在于内核通知可以拷贝数据和数据已经拷贝
完成的区别。Linux异步IO实现并不成熟,而信号IO机制不适合TCP协议,所以目前大部分还是采用多路复用模型
。
# 用户空间IO模型
用户空间的IO模型与内核空间区别仅仅在于分工不同,连接、读取、写入、计算...
Reactor模型有两个重要角色,Reactor负责IO建立连接监听,IO读写以及事件分发,handler负责处理分发的事件。Reactor依赖事件驱动。
reactor模型主要有三种
- 单线程
一个线程处理连接,IO读写以及事件分发和处理,缺点不言而喻
- 多线程
一个线程处理连接、IO读写事件分发,区别在于使用单线程或线程池另外处理分发的业务事件
- 主从模型
在多线程模型下进一步查分,主Reactor负责建立连接,并发连接时间注册到从Reactor,从Reactor负责监听读写和分发。监听和读写可以适当使用线程池提高性能。
# 网络模型
- 应用层
http协议 请求头请求体
- 传输层
tcp传输协议 三次握手四次挥手 内核完成
- 网络层
ip协议 通过路由表 route -n找到destination地址的跳转路由地址,一般第一跳都是网关路由器
- 链路层
四元组之外还要包含下一跳 那就arp -na 找到ip对应的网卡地址
- 物理层