申请与释放内存
申请内存的过程
- 用户态使用malloc向操作系统申请内存
- 操作系统查找页面是否有空闲内存,如果有则返回
- 否则发生TLB缺页中断
- 操作系统为进程挂载新页,并映射到一片空闲的物理内存
- 操作系统从新页中,获取一片内存,并返回进程
直接裸用malloc,会让进程在不断的申请和释放内存,造成发生多次TLB中断,导致程序的性能下降
因此引入了内存池,伙伴系统和tcmalloc都是内存池的产物。
伙伴系统
通过对内存的大小分类,例如8字节的内存块,16字节的内存块,256字节的内存块
对不同类别的内存块,使用链表来管理。
当进程需要申请内存时,可以从最小满足的链表中选取一个内存块,来满足使用
tcmalloc
伙伴系统一般适用于单线程,如果是多线程,则需要加锁来管理,而锁的开销也是不容乐观的。
tcmalloc通过对每个线程管理独立的伙伴系统,并存在一个中央的伙伴系统。来减少在多线程场景下对锁的竞争
内存与磁盘
内存与磁盘息息相关
Page cache
内存与磁盘在数据写入和读取的时候,它们之间会使用缓冲和缓存来减少它们之间的速度差带来的影响。
缓冲和缓存也可以统称为Page cache。Page cache会定时定量的批量刷入磁盘,来提高性能。
如果系统内存变得紧张的时候,不仅仅会影响进程申请内存,还会导致写入磁盘和读取磁盘变慢,因为当系统内存紧张时,系统会自动的减少Page cache的大小,导致磁盘的读写变慢
fsync
fsync的作用是把page cache的脏页强制刷入磁盘,虽然fsync保证的数据可以实时落盘,但是这个api却会导致性能急剧下降。所以在设计方案时,fsync的使用是要慎重考虑的。
kafka在处理消息不丢的场景,并没有使用fsync,而是通过把消息成功同步给一个follower以上,则认为满足消息不丢,然后通过page cache,提高写入磁盘的性能
Swap 交换
在内存紧张时,操作系统会把不活跃的进程,从内存交换到磁盘,等到进程再次活跃时,再把进程交换回内存运行。
这对性能有极高要求的进程,swap是不允许的,如果把redis这样的进程,从内存和磁盘中换入换出,会造成redis的性能急剧下降,而达不到高并发的要求。
直接IO
数据从内存写入磁盘,可以不通过page cache,而是直接把内存写入磁盘。
这样带来的好处是,减少了一次内存拷贝,在高性能磁盘的场景,直接IO会是一个更优的方案
内存与内核
系统调用
当进程发生系统调用,进程会从用户态切换到内核态
寄存器信息,上下文信息,都会被保存和切换
这些切换在高性能场景下是要减少的。
优先使用性能高的系统API,优先使用异步API而不是同步API,优先使用批量调用而不是多次单独调用。
在选择操作系统方面,优先使用某些常用的系统API可以在用户态完成。
线程调度
操作系统是一般是多任务的,每个线程会轮番抢占CPU资源,在线程切换的过程中,内存的上下文需要被保存起来,以备线程再次唤醒时,线程可以继续运行。
相同的,这些上下文的切换,同样会带来性能的下降。
线程交互
原子变量
在多核CPU的架构下,每次原子变量的写入,都会在总线上对原子变量的地址进行上锁,直到写入的数据同步完成
可以看到,这种锁总线内存的做法,是会对性能造成一些轻微的影响的
自旋锁
自旋锁使用了原子变量,所以对总线内存上锁是会对性能带来影响
其次,自旋锁会使用CPU阑珊,这会带来进一步的性能影响
互斥锁
互斥锁也使用了原子变量,因此它对总线内存上锁也会对性能带来影响
其次,当它抢不到资源的时候,线程会陷入休眠
休眠会带来上下文切换,会对性能带来进一步的影响
零拷贝
如果一个进程需要把文件的数据发送到网络,进程通过系统api read获取文件的内容到内存,再把内容从内存发送到网络,会导致文件的内容发生多次拷贝
操作系统提供零拷贝API,把读取文件和发送到网络,内容不需要拷贝到用户态,在内核态就可以直接完成。
实现零拷贝的功能,来提升性能。
页表
如果有两个线程,他们各自有两个变量,而这两个变量分贝在两个不同的页表
当线程轮番频繁的操作这两个变量的时候,会导致线程读取的页表切来切去
因此在申请内存时,尽量同一个页表的内存,只给一个线程使用
内存与CPU
普通变量
普通变量,编译器可以把变量放入寄存器中,来实现极致的性能
volatile变量
volatile变量,CPU每次读取都需要在高速缓存的读取,而不能优化成从寄存器读取,导致性能有一定下降
原子变量
原子操作是通过锁总线内存来达到的
高速缓存与主存
局部性和顺序性
在写程序时,需要考虑数据的局部性和顺序性,减少相同的数据块在内存和高速缓存之间的交换次数。
来达到高性能的目的。
网友评论