Linux查看网络状态
netstat 用于显示各种网络相关信息(网络连接,路由表,接口状态,无效连接,组播成员)
// 查看网络链接状况
netstat -tlunp
Recv-Q(网络接收队列):表示收到的数据已经在本地接收缓冲,但是还有多少没有被进程取走,recv()
Send-Q(网络发送队列):对方没有收到的数据或者说没有Ack的,还是本地缓冲区.
通过netstat的这两个值就可以简单判断程序收不到包到底是包没到还是包没有被进程recv。
这两个值通常应该为0,如果不为0可能是有问题的。packets在两个队列里都不应该有堆积状态。可接受短暂的非0情况。如文中的示例,短暂的Send-Q队列发送pakets非0是正常状态。
如果接收队列 Recv-Q 一直处于阻塞状态,可能是遭受了拒绝 服务 denial-of-service 攻击。
如果发送队列 Send-Q 不能很快的清零,可能是有应用向外发送数据包过快,或者是对方接收数据包不够快。
-a: 列出系统中所有网络连接,包括已经连接的网络服务、监听的网络服务和Socket套接字
-t: 列出TCP数据
-u: 列出UDP数据
-l: 列出正在监听的网络服务(不包含已经连接的网路服务)
-n: 用端口显示服务,而不用服务名
-p: 列出该服务的进程ID(PID)
tcpdump
tcpdump只能抓取流经本机的数据包。
lsof (列出被进程所打开的文件的信息)
- 列出谁在使用某个特定的udp端口 lsof -i udp:55
- 通过某个进程号显示该进行打开的文件 lsof -p 1
ss (获取socket统计信息)
可以显示和netstat类似的内容。但是ss的优势在于它能够显示更详细的有关网络连接的状态信息,而比netstat更快速、更高效。
查看当前服务器的网络连接数
ss -s
查看所有打开的网络端口
ss -l
ss -ta #查看TCP socket
Linux为什么要分用户空间/内核空间
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。
- 现代CPU实现了不同的工作模式,不同模式下CPU可以执行的指令和访问的寄存器不同。
- 从CPU角度出发,保护内核的安全。
用户空间,内核空间怎么相互转移
- 系统调用
- 硬件中断
Linux开机启动过程
- 主机加电自检,加载BIOS硬件信息。
- 读取MBR引导文件。
- 引导Linux内核。
- 运行第一个进行init 【进程号为1】
- 进入相应的运行级别
- 运行终端,输入用户名+密码
Linux系统日志文件在:/var/log/message
Linux重写Unix而来,所以一切皆是文件。
- 普通文件
- 目录文件
- 链接文件
- 设备文件
- 命名管道(FIFO)
什么是inode(索引节点)?
文件存储在硬盘上,最小存储单位是扇区。程序读取文件时,会连续读取多个扇区也就是“块”。存储文件元信息(创建者、大小、创建日期)的区域叫做inode
CPU如何执行程序
图灵机:用机器模拟人们用纸笔进行数学运算的过程。纸带+读写头+读写头上的部件(存储单元、控制单元、运算单元)
冯诺伊曼模型
约定二进制进行计算和存储,定义CPU、内存、输入输出设备、总线。
内存:数据存储单位是bit。最小存储单位是byte,1byte=8bit。内存地址从0开始编号,自增排列,类似于数组。所以内存读写任何一个数据速度都是一样的。
CPU 组成
CPU:32位CPU一次可以计算4个byte。64位一次计算8个byte。
控制器:控制单元+指令译码器+指令寄存器
运算器:ALU+算数运算器+逻辑运算器 【任何运算/指令,最后一定会以0和1的组合流形式完成计算保存到寄存器】
寄存器:数据寄存器+L1+L2+标志寄存器+程序寄存器+段寄存器+通用寄存器【L1、L2就是CPU的高速缓存】(内存离寄存器太远了)
总线:用于CPU和内存以及其它设备之间的通信。由地址总线指定内存地址,通过数据总线传输数据。
线路位宽与CPU位宽
数据通过操作电压传输,低电压0,高电压1。
为了避免低效率串行传输,线路的位宽最好一次能访问到所有内存地址。
CPU需要用地址总线操作内存,一条地址总线每次只能表示0/1两种情况,所以CPU一次只能操作2个内存地址。
- CPU操作4G内存,需要32条地址总线。
CPU位宽最好不要小于线路位宽,32位CPU去加64位大小的数字,需要计算2次才能算出结果。【32位CPU最大操作4GB内存,64位CPU寻址范围很大,2^64】程序执行的基本过程(CPU执行指令)
- CPU读取【程序计数器】的值(指令的内存地址),cpu的【控制单元】操作【地址总线】指定需要访问的内存地址,CPU收到内存的数据后将指令存入到【指令寄存器】
- cpu分析【指令寄存器】中的指令,根据指令类型分配给不同单元。计算类型-》【逻辑运算单元】,存储类型-》【控制单元】
cpu执行完指令,【程序计数器】自增,自增的大小由cpu位宽决定。顺序读取下一条指令。
A = 1 + 2 执行过程
编译器发现1,2是数据放入【数据段】(数据和指令分开区域存放,存指令的地方叫【正文段】)
A=1+2 被翻译成4条指令放入【正文段】。2条load指令,一条add指令,一条store指令。指令
指令内容是一串二进制数字的机器码。不同的CPU有不同的指令集。
数据传输类型。Store/load(寄存器与内存间数据传输)、mov(移动内存地址数据到另一个地址)
运算类型。加减乘除、位运算、比较大小。最多处理两个寄存器中的数据。
跳转类型。修改程序计数器的值,if-else/switch-case/函数调用
信号类型。比如发生中断trap
闲置类型。比如nop,执行后cpu空转一个周期。指令的执行速度
一个1GHz的CPU,1s会产生1G次数的脉冲信号。每一次脉冲信号高低电平的转换就是一个周期,称时钟周期。
一个时钟周期内,CPU仅能完成一个动作,时钟频率高了,工作速度就快了。(一个时钟周期不一定能完成一条指令)
程序的CPU执行时间 = CPU时钟周期 【指令数 每条指令的平均时钟周期数(CPI)】 时钟周期时间(CPU主频)要想程序跑的快
指令数。靠编译器来优化。
- 每条指令的平均时钟周期数(CPI)。现代CPU通过pipline技术,让一条指令需要的CPU周期数变少。
-
64位和32位CPU比较?
64位CPU一次计算超32位的数字,只有运算大数字的时候才有优势。
- 64位CPU有更大的寻址空间。
- 硬件的64/32位指的是CPU位宽,软件的64/32指的是指令的位宽。
储存器金字塔
- CPU Cache
- 内存 (DRAM动态随机存储器,数据存储在电容里不断漏电,需要定时刷新电容)
-
如何写出让CPU跑得更快的代码
L1 Cache通常会分为【数据缓存】和【指令缓存】
- L2 L1/L2 每个核心独有
- L3 是多个CPU核心共享的
CPU Cache数据结构和读取过程
【CPU读写数据时,都是在Cache读写数据,Cache不存在时访问内存】CPU访问内存数据时,都是一小块一小块数据读取的。在内存中,一块的数据我们称为内存块。
直接映射Cache
将内存块的地址始终【映射】在一个CPU line(CPU line 是CPU从内存读取数据到Cache的单位)的地址。映射关系将基于【取模运算】。
例如:内存划分为32个内存块,CPU cache共有8个CPU Line,要访问15号内存块,15%8=7,在第7号CPU line中。【多个内存块对应同一个CPUline】
为了区别不同的内存块,对应的CPU line还要存储一个【组标记Tag】(记录当前CPU line存储数据对应的内存块)
CPU从CPU Cache读取数据时,并不是读取CPU line中的整个数据块,而是读取CPU所需要的一个片段-字Word。如何找到Word呢?用【偏移量offset】
一个内存的访问地址:组标记+CPU Line索引+偏移量
对于CPU Cache数据:索引+有效位+组标记+数据块
如何写出CPU缓存命中率高的代码?
- 数据缓存
遍历数组时,按照内存布局顺序访问,将可以利用到CPU cache带来的好处。
例如:arr[0][0]->arr[0][1]->arr[0][2]
- 指令缓存
问题:先遍历if操作排序速度快,还是先排序再遍历if操作速度快(对)?
- CPU分支预测器。
如果分支预测器可以预测到接下来要执行if里的指令,还是else指令的话,可以提前将指令放入指令缓存中,直接从Cache读取到指令。
如何提升多核CPU的缓存命中率?
现代CPU都是多核心的,进程可能会在不同CPU核心切换执行,因为L1 L2 Cache是每个核心独有的,L3是多核心共享的。各个核心缓存命中率会受到影响。
当有多个同时执行【计算密集型】的线程,可以把线程绑定在某一个CPU核心上。
CPU缓存一致性
数据写入Cache后,内存与Cache对应的数据不同。什么时候才把Cache中的数据写回内存?
数据先写入Cache,合适时机再写到内存
写直达
把数据同时写入到内存和Cache中。
缺点:无论数据在不在Cache里面,每次写操作都会写回到内存。
写回
当CPU发起写操作时,新数据仅仅写入Cache Block中,只有当修改过的Cache Block【被替换】时才写入到内存中。
- 当CPU发起写操作时,数据已经在CPU cache里,就把CPU cache里Cahe block标记为脏,不写入内存。
- 当CPU发起写操作时,Cache Block存放的是别的内存地址的数据,就去检查是否为脏。为脏,就把Cache Block数据写回内存,当前写入数据写入Cache Block,编辑为脏。
多核心缓存一致性问题
L1/L2 Cache是每个核心独有的。
A核心执行i++,由于写回策略,只有A核心这个Cahe block要被替换时,数据才写入到内存中。B核心此时去读取i,由于内存中值未跟新,读取的是旧值。
需要一种机制,同步不同核心里面的缓存数据。
- 写传播:CPU核心的Cache数据跟新时,传播到其它核心的Cache。、
- 事务串行化:某个CPU核心里对数据的操作顺序,其它核心看到也是相同的。(A先写I,B后写I,C接受到顺序相同的指令)
cpu核心对于Cache的操作,同步给其它核心。 - 引入锁概念,两个CPU核心里有相同的数据Cache,对于Cache的跟新,只有拿到锁才能进行。
- 总线嗅探-CPU时时监听总线上的活动
只保证某个CPU核心Cache更新数据这个操作能被其它CPU核心感知。基于总线嗅探的MESI协议-构成流动的状态机
Modified已修改。脏标记,代表Cache Block数据已经被跟新,未同步到内存。【修改无需广播】
Exclusive独占。【修改无需广播】
【需广播】Shared共享。相同的数据在多个CPU核心的Cache里都有。
【需广播】Invalidated已失效。代表Cache Block数据已失效。(修改共享状态的Cache,需要广播将其它核心Cache改成已失效)
CPU如何执行任务CPU如何读写数据
CPU从内存读取数据到Cache时,不是一个字节一个字节的读取,而是读取一个CPU line。
1. 对于数组的加载,CPU会将连续的多个数据加载到Cache中。
2. 对于单独变量来说,则会有Cache伪共享问题,多核CPU持续交替修改同一个line中的不同变量(MESI)。CPU Cache会失效。避免伪共享(多个线程同时读写同一个Cache line中的不同变量)
对于多个线程共享的热点数据,避免这些数据刚好在同一个Cache Line中。
利用空间换时间的思想,浪费一部分Cache空间。
Java 并发框架Disruptor使用【字节填充+继承】方式,避免伪共享问题
64位CPU的CPU line大小是64个字节,一个long类型是8个字节。JVM对象继承关系中父类和子类成员,内存地址是连续排列布局的(RingBuffer继承RingBufferPad)。RingBufferPad中7个long类型数据作为CacheLine的前置填充,RingBuffer中7个long类型数据作为CacheLine的后置填充。
前后各填充了7个不会被读写(final修饰)long变量,无论如何加载Cache Line都不会发生更新操作的数据。
CPU如何选择线程
linux内核中,进程和线程都是用tark_struct结构体表示。
线程的tark_struct结构体部分资源是共享了进程已创建的资源(内存地址空间、代码段、文件描述符)。线程的tark_struct结构体相比进程承载的资源要少。所以轻量级。没有创建线程的进程,只有单个执行流,被称为主线程。但在内核都是tark_struct。
linux内核的调度器,调度的对象都是tark_struct。会根据它的优先级数值划分:
- 实时任务0-99。对系统的响应时间要求比较高。
- 普通任务100-139。响应时间没啥要求。
调度类
Linux为 了保证高优先级的任务能尽早被执行,分为3种调度类。
1. 完全公平调度-CFS
我们平日遇到的基本都是普通任务。Linux实现基于CFS的调度算法,完全公平调度。(优先选择vruntime少的tark_struct任务)让分配给每个任务的CPU时间一样,并为其安排一个虚拟运行时间vruntime,运行时间越长,vruntime越大。没被运行的任务vruntime不变。对于高优先级的普通任务,还考虑了【权重值】,高权重的vruntime小。
CPU运行队列
每个CPU都有自己的运行队列,用于描述CPU上所运行的所有进程。(优先级:Deadline>Realtime>Fail,实时任务总是会比普通任务优先被执行)
- Deadline运行队列dl_rq
- 实时任务运行队列rt_rq
- CFS运行队列csf_rq(用红黑树来描述。按vruntime大小排序,最左侧的叶子节点就是下次会被调度的任务)
调整优先级-调整nice值
没有特定去指定优先级的话,默认情况都是普通任务(CFS调度器管理)。
如果要让普通任务有更多的执行时间,可以调整nice值。要求实时性较高的话,需要改变任务的优先级和调度策略。
软中断
中断的定义
操纵系统收到硬件的中断请求,会打断正在执行的进程。调用内核种的中断处理程序来响应请求。(中断有可能丢失,所以中断程序要短而快)
什么是软中断
为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分为两个阶段。
上半部直接处理硬件请求(打断CPU正在执行的任务)。负责耗时短的工作,快速执行。【hi】
下半部由内核触发(内核线程方式执行)。负责上半部未完成的工作,延迟执行耗时比较长的事情。【si】
如何定位软中断CPU使用率过高的情况
- 使用Top命令,后按1查看所有核心。si就是CPU软中断使用率。
- 使用watch -d cat /proc/softirqs 查看每个软中断类型的中断次数的变化速率。
- 对于网络IO较高的Web服务器,NET_RX网络接受中断的变化速率比其它中断要快很多。
使用 sar -n DEV 查看网卡网络包接受速率,分析是哪个网卡有大量的网络包进来。
通过 tcpdump 抓包,分析包来源。
计算机中 0.1+0.2 != 0.3?
负数要用补码(正数二进制取反再加1)表示
二进制最高位作为【符号标志位】
如果不是补码,仅仅将【符号标志位】变为1表示负数。做加减法时需要多一步操作来判断是否为负数(加法:先判断符号标志位,要是负数就把操作改为减法)。
对于补码表示,负数的加减法和正数加减法保持一致。使用同一个运算器,减少中间变量存储的开销。
十进制小数与二进制的转换
整数部分:除二
小数部分:乘二取余法(十进制小数部分乘以2作为二进制的一位,继续取小数部分乘以2)。
并不是所有的小数都可以用二进制表示,例如 0.1 转二进制表示是无限循环的。(精度缺失)
有的小数无法用完整的二进制表示,计算机中只能用近似数的方式来保存。
计算机如何存储小数
计算机以浮点数形式保存小数。(浮点:代表小数点可以浮动)
二进制要用到科学计数法,要规范化(基数为2,小数点左侧只有1位且为1)
1000.101表示成 1.000101 * 2^3 【符号位+指数位+尾数位】
- 符号位(1):表示数字是正数还是负数。
- 指数位(3):小数点在数据中的位置。
- 尾数位(000101):小数部分。尾数的长度决定这个数的精度。32位-》单精度 64位-》双精度
- 0.1+0.2 != 0.3
现在用IEEE 754规范的 单精度/双精度 浮点类型储存小数,精度不同,近似值也不相同。decimal底层不用二进制存储,所以可以精确计算。
有的小数无法用完整的二进制表示,计算机中只能用近似数的方式来保存。
负数要用补码(正数二进制取反再加1)表示。 【如果不是补码。做加减法时需要多一步操作来判断是否为负数。使用同一个运算器,减少中间变量存储的开销。】
十进制小数与二进制的转换。乘二取余法(十进制小数部分乘以2作为二进制的一位,继续取小数部分乘以2)。并不是所有的小数都可以用二进制表示,例如 0.1 转二进制表示是无限循环的。
计算机以浮点数形式保存小数。利用科学计数法去表示极大/极小值。【符号位+指数位+尾数位】单精度float用32位,双精度double用64位。
decimal底层不用二进制存储,所以可以精确计算。
金融行业: 使用整数存储其最小单位的值。【禁止直接比较浮点数大小】
ASCII码=》1个Byte由8个bit组成:26字母*2 + 10数字 + 特殊字符 > 64(2的6次),所以用7组信号量。预留一个bit做奇偶校验。
CPU 组成
控制器:控制单元+指令译码器+指令寄存器
运算器:ALU+算数运算器+逻辑运算器 【任何运算/指令,最后一定会以0和1的组合流形式完成计算保存到寄存器】
寄存器:数据寄存器+L1+L2+标志寄存器+程序寄存器+段寄存器+通用寄存器【L1、L2就是CPU的高速缓存】
内存:
C/C++能直接操作内存地址,进行分配释放。Java就是交给JVM喽【内存抽象出来就是线性空间内的字节数组】
操作系统结构
Linux 内核 vs Windows内核
内核的能力
- 管理进程、线程。进程调度的能力。
- 管理内存,决定内存的分配和回收。内存管理能力
- 管理硬件设备。硬件通信能力
-
内核如何工作
内核空间。内存空间只有内核程序可以使用。
- 用户空间。这个内存空间专门给应用程序使用。
用户空间的代码只能访问局部的内存空间,内核空间的代码能访问所有空间。
当应用程序使用系统调用时,会产生一个中断。CPU会中断当前用户程序跳转执行内核程序(内核态),内核处理完成主动触发中断,将执行权限交给用户程序(用户态)
Linux 内核
MutiTask多任务
多个任务可以同时(并发/并行)执行
- 并发:单核CPU,每个任务执行一小段
- 并行:多核CPU,多核CPU,多个任务不同核心同时执行
SMP对称多处理
每个CPU的地位相等,程序能被分配到任何一个CPU执行
ELF可执行文件链接格式
可执行文件存储格式
Monolithic Kernal宏内核
Linux内核是一个完整的可执行程序,拥有最高的权限。
宏内核:系统内核的所有模块。进程调度、内存管理、文件系统、设备驱动等都在内核态。
微内核:将驱动程序、文件系统放在用户空间,提高操作系统的稳定性。但会带来性能损耗(鸿蒙就是微内核)
混合内核:内核里面有一个最小版本的内核,其它模块在这个基础上搭建,最后把整个内核做成完整的程序。(宏内核包裹着微内核)
Windows NT
- Windows内核设计是混合型内核
- 可执行文件格式不同。Window是PE
内存管理
单片机没有操作系统,它的CPU会直接操作内存的【物理内存】,这种情况下在内存中同时运行两个程序是行不通的。
虚拟内存
操作系统为每个进程分配一套虚拟地址,且每个进程不能直接访问物理地址。
进程所持有的虚拟地址会通过CPU芯片中的内存管理单元(MMU)的映射关系,转换成为物理地址,再通过物理地址访问内存。
操作系统如何管理虚拟地址与物理地址之间的关系?
内存分段
程序由代码分段、数据分段、栈段、堆段组成。不同的段有不同的属性,用分段的形式把段分离出来。
分段机制会把程序的虚拟地址分成4个段,每个段在段表中有一个项,在这一项找打段的基地址加上偏移量,就能找到物理内存中的地址。
问题?
- 内存碎片问题。
解决外部内存碎片的问题就是内存交换。先将内存写道硬盘,再从硬盘读入到内存(紧紧挨着被占用的内存后面)。
内存交换空间就是我们看到的Swap空间(从硬盘划分出来的) - 内存交换效率低
如果内存交换的时候,交换的是内存占用很大的程序,机器会显得卡顿。引入内存分页解决。 内存分页
分段的好处是能产生出连续的内存空间,但会出现内存碎片、内存交换效率低的问题。
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续且尺寸固定的内存空间,叫做页(Page)。
虚拟内存和物理内存之间通过页表来映射。页表存储在内存中,内存管理单元MMU会将虚拟内存地址转化为物理地址。
当进程访问的虚拟地址再页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后返回用户空间,恢复进程运行。内存分页
内存分页如何解决内存碎片、内存交换效率低的问题?
- 采用分页,释放的内存以页为单位释放,不会产生无法给进程使用的小内存。
- 内存不够时,操作系统会将最近没被使用的内存页面给释放,暂时写在硬盘上【换出】。需要时再加载进来【换入】。一次写入硬盘的只有少数的页,交换效率较高。
- 加载程序时,不需要一次性将程序加载到物理内存中,只在程序运行中需要用到虚拟内存中的指令/数据时,再去加载到物理内存中。
- 分页机制下,虚拟地址和物理地址如何映射?
虚拟地址=页号+页内偏移
根据页号,从页表里面查询对应的物理页号
拿物理页号,加上偏移量,得到物理内存地址
这里有空间缺陷。每个进程都需要自己的虚拟地址空间(页表) - 多级页表
使用二级分页,一级页表能覆盖4G虚拟地址空间。如果某一个一级页表项没被用到,就不要创建它对应页表的二级页表了,需要时再创建。
对于64位系统,页表变成了4级目录。 - TLB (最常访问页表项的Cache)
多级页表解决了空间上的问题。但虚拟地址到物理地址的转换多了几道转换的过程。
程序是有局限性的,一段时间内程序的执行仅限于一部分。利用这一特点,将最常访问的几个页表项储存到速度更快的硬件中。
在CPU中加入一个专门存放最常访问页表项的Cache,CPU寻址时也会优先查TLB,后去查常规的页表。Linux内存管理-页式内存
Linux内存主要采用页式内存管理,但不可避免的涉及了段机制。
由于Intel处理器一开始使用段内存管理,Linux采取了“惹不起躲着走”的方法。
Linux中每个段都是从0开始的整个虚拟空间,所有段的起始地址一样。系统中所有的代码面对的都是虚拟地址(内核空间+用户空间)。
进程与线程
进程
大量被阻塞的进程可能会占用物理内存空间,所以在虚拟内存管理的操作系统中,会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运行时再换入到内存。这个时候线程的状态叫挂起。
进程的控制结构 PCB
进程控制块PCB数据结构来描述进程。PCB是进程存在的唯一标识。
- 进程描述信息。进程标识符+用户标识符
- 进程控制和管理信息。进程当前状态+进程优先级
- 资源分配清单。虚拟地址空间、I/O
- CPU相关信息。CPU中各个寄存器的值。
PCB通过链表的方式,将具有相同状态的进程链在一起,组成队列。(灵活的插入和删除)
进程的控制
- 创建进程。允许子进程继承父进程所拥有的资源。初始化PCB
- 终止进程。查找要终止进程的PCB-》执行状态。终止执行分配CPU资源-》有子进程。终止子进程。-》将该进程所拥有的全部资源归还给父进程/操作系统-》从PCB所再队列中删除
- 阻塞进程。一旦被阻塞则只能由另一个进程唤醒。 处于运行状态保护现场,将其状态置为阻塞状态。-》将该PCB插入到阻塞队列中。
- 唤醒进程。 如果某个进程调用了阻塞语句,则一定有一个与之对应的唤醒语句。
进程的上下文切换
各个进程共享CPU资源,CPU从一个进程切换到另一个进程叫做上下文切换。
操作系统需要先帮CPU设置好CPU寄存器(速度极快的缓存)和程序计数器(存储CPU执行的指令位置)。它们是CPU运行任何任务前必须依赖的环境。
CPU上下文切换就是把前一个任务的CPU上下文(CPU寄存器+程序计数器)保存起来,加载新任务的上下文,跳转到计数器所指的位置运行新任务。进程的上下文切换在切换什么?
进程由内核管理,所以切换发生在内核态。
不仅包含虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。线程
- 进程之间不好做通信。
- 维护进程的系统开销过大(创建、终止、切换)
- 线程之间可以并发运行且共享相同的地址空间。
线程为什么比进程轻量?
- 线程创建时间短。它不需要资源管理信息(内存、文件),线程只是共享它们。
- 线程终止时间快。释放的资源少。
- 线程切换快。同一个进程的线程具有同一个页表,切换的时候不需要切换页表。
同一进程的各线程间共享内存和文件资源。线程间传递数据不需要经过内核。
线程的上下文切换
操作系统的任务调度,实际上的调度对象是线程,进程只是给线程提供了虚拟内存、全局变量等资源。
- 当两个线程属于同一进程,只要切换线程的私有数据、寄存器等不共享数据。
- 当两个线程属于不同进程,切换过程与进程切换一致。线程的实现
用户线程(用户空间)
基于用户态线程管理库实现,操作系统只能看到整个进程的PCB。- 内核线程(内核空间)
内核线程的管理由操作系统负责。 轻量级线程(内核中支持用户线程)
内核支持的用户线程,轻量级线程和内核线程一一对应。
它只有一个最小执行的上下文和调度程序所需要的信息。线程 VS 进程
进程是资源分配的单位。线程是CPU调度的单位。
- 进程有完整的资源平台。线程只独享必须的资源,如寄存器、栈。
- 线程同样有就绪、阻塞、执行状态。
-
调度
调度时机
进程从一个运动状态到另一个状态时,会触发一次调度。
就绪态-》运行态。操作系统从就绪队列选择一个进程运行。
- 运行态-》阻塞态。进程发生I/O事件而阻塞
- 运行态-》结束态。进程退出后。
非抢占式调度算法。挑选的进程运行到被阻塞/退出,不理会时钟中断。
抢占式调度算法。挑选一个进程只运行某段时间,时间段结束后调度程序从就绪队列中选另一个进程。它需要在时间间隔的末端发生时钟中断(时间片机制)
调度的5种原则。使得进程-快
- CPU利用率:确保CPU时钟忙碌。
- 系统吞吐量:长作业进程会降低吞吐量。
- 周转时间:进程运行时间+阻塞时间的总和,一个进程的周转时间越小越好。
- 等待时间:进程处于就绪队列的时间。
-
调度算法
先来先服务。不利于后到的短作业
- 最短作业优先。对长作业不利
- 高响应比优先。(等待时间+要求服务时间)/要求服务时间
- 时间片轮转调度算法。
- 最高优先级调度算法。
- 多级反馈队列调度算法。【多级】多个队列,队列中优先级从高到低,优先级越高时间片越短。【反馈】新进程加入优先级高的队列,立即停止正在运行的进程,去运行优先级高的队列。
进程间通信
。。。。。。。。待直接粘贴
多线程同步
互斥
保证一个线程再临界区执行时。其它线程应该被阻止进入临界区。(多进程竞争共享资源,也可以使用互斥的方式)
同步
并发进程/线程在一些关键点上需要互通等待与互通消息,相互制约的等待和互通消息称为进程/线程同步。
互斥与同步的实现
- 锁:加锁、解锁
- 信号量:P/V操作
锁- 根据锁的实现方式不同,分为【忙等待锁】和【无忙等待锁】
- 现代CPU体系结构提供的特殊原子操作指令—Test-and-Set(把一个值0跟新为新值1,返回旧值0)【原子性】
Test-and-Set可以实现【忙等待锁】,获取不到锁就一直循环Test-and-Set - 【无忙等待锁】就是获取不到锁的时候不用自旋。当没获取到锁时,把当前线程放入到锁的等待队列,执行调度程序,把CPU让给其它线程执行。
信号量-PV
信号量表示资源的数量,P操作在进入临界区之前,V操作在进入临界区之后,必须成对出现。(PV函数由操作系统管理和实现,执行PV函数是有原子性的)
- 信号量可以实现临界区互斥访问(1,0,-1)。
- 信号量可以实现事件同步(不同的PV)。
生产者-消费者问题
只能有一个线程操作缓冲区,缓冲区是临界代码,需要互斥。
消费者必须等待生产者生产数据,需要同步。
设置互斥信号量为1,缓冲区空槽信号量为N,缓冲区满槽信号量为0
死锁
死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。
避免死锁问题,只需要破坏其中一个条件即可。常见-》使用资源有序分配法来破坏环路等待条件。
互斥锁与自旋锁(最基本的锁)
- 互斥锁加锁失败(线程切换),线程会释放CPU,给其他线程。
- 自旋锁加锁失败(忙等待),线程会忙等待,直到拿到锁。(能确保被锁住的代码执行时间很短。应该选用它避免线程上下文切换的成本)
- 互斥锁
对于互斥锁加锁失败而阻塞的现象(用户态-》内核态),是由操作系统内核实现的。会有两次线程上下文切换的成本。
线程加锁失败,内核把线程状态从【运行】设置为【睡眠】,CPU切换给其他线程执行。
锁被释放,之前【睡眠】的线程会变成【就绪】状态
虚拟内存是共享的,所以在进行线程上下文切换时,只需要切换线程的私有资源、寄存器等不共享的数据。 自旋锁
通过CPU提供的CAS函数(原子指令),在【用户态】完成加锁和解锁操作。
一直自旋,利用CPU周期,直到锁可用。在单核CPU中,需要抢占式的调度器(不断通过时钟中断一个线程,运行其他线程),否则自旋锁在单CPU无法使用。调度算法
进程调度/页面置换/磁盘调度算法
内存页面置换算法
缺页异常:CPU访问的页面不在内存上,会产生缺页中断,请求操作系统将所缺页调入到物理内存。
当出现缺页异常,需调入新界面而内存已满时,选择被置换的物理页面。(选择一个物理界面换出到磁盘,把需要访问的界面换入到物理页)
- 磁盘调度算法
先来先服务
最短寻道时间优先(优先选择从当前磁头位置所需寻道时间最短的请求)
扫描算法(磁头在一个方向上移动)
循环扫描(磁头只响应一个方向上的请求)
LOOK与C-LOOK算法(反向移动的途中会响应请求)
文件系统
Linux会为每个文件分配两个数据结构 索引节点(index node) + 目录项(directory entry),记录文件的 元信息 和 目录层次结构。
- 索引节点:数据在磁盘的位置,也是文件的唯一标识,存储在硬盘中。
- 目录项【内核把已经读过的目录用目录项这个数据结构缓存在内存】:记录索引节点指针,由内核维护,缓存在内存。目录项对索引节点(多对一)
虚拟文件系统
用户层与文件系统层中引入的中间层(VFS)。文件系统的基本操作单位是数据块。
- 磁盘的文件系统。直接把数据存储在磁盘。
- 内存的文件系统。读写内核中相关的数据。
-
文件的存储
连续空间存储
文件存在磁盘【连续的】物理空间中。文件头需要指定【起始块位置】和【长度】。(存在磁盘空间碎片+文件长度不易扩展的缺陷)- 非连续空间存储
链表方式:离散、不用连续。隐式链表+显示链接。(查找效率低,指针信息存放消耗内存/磁盘)
索引方式:为每个文件创建一个索引数据块。(索引表增加存储空间的开销)Unix文件
小文件使用直接查找的方式,减少索引数据块的开销。
大文件以多级索引的方式来支持,需要大量查询。空闲空间管理(位图)
位图法:利用二进制的一位表示磁盘中一个盘块的使用情况。
值为0代表盘块空闲,值为1,表示对应的盘块已分配。目录的存储
目录文件的块保存的是目录里面一项一项的文件信息。(保存目录的格式是哈希表,保存文件名的哈希值)文件目录被使用后会保存在内存,降低磁盘操作次数。软连接和硬链接
硬链接:多个【目录项】的【索引节点】指向一个文件,只有删除文件的所有硬链接及源文件,才会彻底删除文件。
软链接:重新创建一个文件,这个文件有独立的inode,文件的内容是拎一个文件的路径。(可跨文件系统)目标文件被删除,链接文件还在,只是指向的文件找不到了。文件I/O
缓冲与非缓冲I/O【是否利用标准库缓冲】
- 缓存I/O:利用标准库缓冲实现文件的加速访问
- 非缓冲I/O:直接通过系统调用访问文件(CPU上下文切换开销)
直接与非直接I/O【是否利用操作系统缓存】 - 直接I/O:直接经过文件系统访问磁盘。(大文件传输或者应用程序已经实现了磁盘缓存)
- 非直接I/O:读操作(数据从内核拷贝给用户程序)写操作(数据从用户程序拷贝给内核缓存,内核会决定什么时候写入磁盘)
阻塞与非阻塞I/O VS 同步与异步I/O
阻塞I/O:用户程序执行read,线程阻塞,等待【内核准备好数据】,把数据【从内核缓冲区拷贝到应用程序】的缓冲区中。
非阻塞I/O:在数据未准备好时立即返回,轮询查询数据准备好了吗?替换轮询查询的方式(I/O多路复用,通过I/O事件分发,当内核数据准备好,以事件通知应用程序)read调用时,内核将数据从内核空间拷贝到用户程序空间还是需要等待。
它们都属于同步调用。
异步I/O:【内核数据准备好】和【数据从内核态拷贝到用户态】两个过程都不用等待。aio_read*(异步I/O) 后,立即返回。应用程序不需要主动发起拷贝操作。网络系统
Linux系统如何收发网络包- Linux网络协议栈
应用层:应用数据
传输层:应用数据+TCP头
网络层:应用数据+TCP头+IP头
网络接口层:帧尾+应用数据+TCP头+IP头+帧头 (链路层规定了最大传输单元MTU)零拷贝
为什么要有DMA技术?-传输大量数据不能只用CPU来搬运
在进行I/O设备和内存的数据传输的时候,数据搬运的工作全部交给DMA控制器,而CPU不再参与任何与数据搬运相关的事情。(每个I/O设备都有自己的DMA控制器)
传统的文件传输-很糟糕,4次上下文切换(两次系统调用read + write)
磁盘文件 — DMA拷贝 -》 缓存区 — CPU拷贝 —》 用户缓存区 — CPU拷贝 —》 socket缓存区 — DMA拷贝 -》 网卡
提高文件传输的性能
要想提高文件传输的性能,需要减少【用户态与内核态的上下文切换】和【内存拷贝】的次数
一次系统调用会发生2次上下文切换,原因:用户空间没有权限操作磁盘或者网卡。
用户的缓冲区没有必要存在,因为不会对数据再加工。
如何实现零拷贝(小文件)
mmap + write: read() 系统调用的过程中会把内核缓存区的数据拷贝到用户缓存区 ,为了减少开销。用mmap代替read()系统调用函数。
mmap系统调用函数会直接把内核缓冲区的数据【映射】到用户空间。(避免数据拷贝操作,但总体还需要4次上下文切换(两次系统调用))
Sendfile 该系统调用直接会把内核缓冲区数据copy到socket缓冲区,不用过内核态。只有2次上下文切换,3次copy(Linux新版内核的sendfile 直接能copy到网卡缓冲区中,只进行2次copy)
零拷贝:没有在内存层面去拷贝数据,全程未通过CPU来搬运数据,所有数据通过DMA传输。(只需要两次上下文切换+copy)
使用零拷贝的项目
Kafka 调用了Java NIO库里的transferTo方法,Nginx 的sendfile on也是零拷贝。
零拷贝基于PageCache
文件传输过程,第一步都是把磁盘文件数据copy到【内核缓冲区】,也就是PageCahe。PageCahe会缓存最近访问的数据。磁盘读取数据时,优先从PageCahe中找
缓存最近被访问的数据。
预读功能。
传输大文件(GB)级别时,PageCahe不会起作用。白白浪费DMA多做的数据拷贝,使用了PageCahe的零拷贝也会损失性能。所以大文件不会用零拷贝与PageCahe。
大文件传输实现方式
使用【异步I/O+直接I/O】来代替零拷贝技术。
I/O多路复用
最基本的socket模型。只能一对一通信,同步阻塞。
为每个请求分配一个进程/线程的方式不合适,一个进程处理每个请求的耗时控制在1ms,时间拉长来看,多个请求复用了一个进程。进程可以通过一个系统调用函数从内核中获取多个事件。
Select/poll(文件描述符个数不受限制)
首先将已连接的socket都放到一个文件描述符集合,调用select将该集合copy到内核中,遍历该集合。当检查到事件产生,将socket标记为可读或可写,再把整个集合copy到用户态,通过遍历找到socket进行处理。
使用【线性结构】存储进程关注的Socket集合,因此都需要遍历文件描述集合来找到可读或者可写的Socket,而且还需要再用户态与内核态之间拷贝文件描述集合。
Epoll
epoll在内核中使用红黑树跟踪所有待检测的文件描述字。OlogN 减少内核和用户空间大量的数据copy和内存分配。
epoll使用事件驱动的机制,内核维护了一个链表来记录就绪事件。socket有事件发生才会加入这个列表。不会像select/poll那样,轮询扫描这个socket集合。提高检测效率。
epoll同时监听的Socket数目上限为系统定义的进程打开的最大文件描述符个数
高性能网络模式 Reactor 和 Proactor
演进
【资源复用】 线程池可以不用再为每个连接创捷线程
【socket非阻塞+I/O多路复用】可以在一个监控线程中监控很多的连接。
大佬基于面向对象的思想,对I/O多路复用作了封装,使用者不必考虑底层网络API的细节。-》Reactor模式
Reactor模式又叫Dispatcher,收到事件后,根据事件类型分配给某个进程/线程。
- java一般使用线程,如Netty
- C语言进程/线程均可,Nginx用的时线程
Reactor(非阻塞同步网络模式)
来了事件操作系统通知应用进程,让应用进程来处理。
单Reactor单进程/线程
Reactor对象通过select(I/O多路复用接口)监听事件,收到事件后dispatch进行分发,根据事件类型分发给Accopter对象or Handler对象。【通用】
链接建立事件交给Accopter对象处理,Accopter对象会通过accept方法获取链接,并创建一个handler对象处理后续响应事件。【通用】
不是建立事件,交给对应的handler对象响应。【通用】
handler对象通过read-》业务处理-》send的流程来完成完整的业务流程。
该方案不适用于计算密集型场景,适用于业务处理快速的场景。
Redis采用该方案,其业务处理都在内存中。
单Reactor多进程/线程
区别在于handler对象不再负责业务处理,只负责数据接受/发送,read取到数据后发给子线程的Processor对象进行业务处理。【子线程处理完在发送给handler,handler通过send发送给客户端。】
单Reactor对象承担所有事件的监听和响应,面对瞬时高并发的场景时,成为性能瓶颈。
多Reactor多进程/线程
【子线程无须返回数据,直接在子线程就可以将数据send发送给客户端。】
Netty和Memcache采用了多Reactor多线程的方案。
Nginx采用了多Reactor多进程的方案。
Proactor(异步网络模式)
Proactor采用了异步I/O模式。来了事件操作系统来处理,处理完再通知应用。
Reactor和Proactor都是基于【事件分发】的网络编程模式,区别在于Reactor基于【待完成】的I/O事件,Proactor则是基于【已完成】的I/O事件。
基于LInux的仅能使用Reactor方案,由于socket不支持异步操作。而windows可以使用效率更高的Proactor方案。
Linux命令
如何查看网络的性能指标
性能指标有哪些
带宽(链路最大传输速率)、延时(收到对端响应)、吞吐率(单位时间成功传输的数据量)、PPS(以网络包为单位的传输速率)
网络配置如何看
Ifconfig / ip 显示网口的配置 和 收发数据包的统计信息。
- 网络链接状态标识。
- MTU大小。
- 网口的IP地址,子网掩码、MAC地址、网关地址。
- 网络包的收发的统计信息。
socket信息如何查看
Netstat / ss ,这两个命令都能显示出socket的状态(State)、接收队列(Recv-Q)、发送队列(Send-Q)、本地地址、远端地址、进程PID、进程名称
接收队列(Recv-Q)、发送队列(Send-Q)不同socket状态含义不同
Socket-Established:
- Recv-Q表示:socket缓冲区中还有未被应用程序读取的字节数。
- Send-Q表示:socket缓冲区中还有未被远端主机确认的字节数。
Socket-Listen: - Recv-Q表示:全连接队列长度。
- Send-Q表示:全连接队列最大长度。
TCP三次握手中,服务器收到客户端SYN包,内核会把该连接存储到半连接队列,然后再向客户端发送SYN+ACK包,接着客户端返回ACK,
服务端收到第三次握手的ACK后,内核会把链接从半连接队列移除,然后创建新的完全链接,将其添加到全连接队列中,等待进程调用accept()时取出链接。
网络吞吐率和PPS如何查看
Sar 命令能查看网络吞吐率和PPS。ethtool 可以查看带宽
如何从日志分析PV(页面访问次数)、UV(访问人数)
开始分析
先用 ls -lh 命令查看日志文件大小。若日志很大,可以使用 scp 命令将文件传输到闲置的服务器分析。
慎用cat
对于大文件,用 less 命令读取,它按需加载,往下看时才会继续加载。
看日志最新部分内容用 tail 命令
PV 分析
对于nginx的access.log来说,直接使用 wc -l 命令。
PV 分组
awk命令默认以【空格】为分隔符,$4可以过滤到第4列,substrk可以从第x个字符截取到x个字符 ,sort 可以排序,uniq -c 可以进行去重