【notes2】并发,IO,内存
文章目录
- 1.并发
- 2.1 可见性volatile
- 2.2 原子性(读写原子)AtomicInteger/synchronized
- 2.IO多路复用
- 2.1 select
- 2.2 poll
- 2.3 epoll
- 3.内存管理
- 3.1 分页
- 3.2 分段
- 3.3 brk
- 3.4 mmap
1.并发
并发对应硬件资源是cpu,线程是操作系统如何利用cpu资源的一种抽象,是cpu调度的最小单位。一个进程的内存空间是一套完整的虚拟内存地址空间,这个进程中所有线程都共享这一套地址空间。
cpu并不在意是哪个进程,左边单核cpu(不是单线程),3个线程(任务都是读取文件)交叉
运行完,通过以下两点大大提高了cpu利用率(线程想提高效率和io相关):1.
DMA过程中cpu一段时间不被线程阻塞。2.
DMA进行数据读取时可复用,因为cpu的总线程具有多条线路,所以DMA可充分利用这些线路,实现并行读取这些文件。
多线程需调用系统底层API才能开辟(线程本质向cpu申请计算资源),在多线程开辟过程中浪费时间,并且在线程运行中上下文切换部分(左边切换多次,右边切换三次)有用户态和内核态
转换浪费在cpu切换时间点上。所以服务端连接的客户端不活跃多(即io次数少)
时,考虑单线程(io多路复用或nio)
或协程
。上面的1,2,3线程都有io,所以多线程效率高。协程不进入内核态。
内存,cpu,io
是编程中三个最重要的点。南桥(桥就是连接)连接带宽要求低的设备如是一些鼠标键盘硬盘usb设备等。北桥(集成到了cpu内部)负责带宽比较高的设备如pcie显卡,pcie硬盘,内存RAM需高速访问。如下是cpu常见参数,8核16线程(超线程)。
如下常见的6种指令集(方框):系统架构=处理器指令集,soc是A系列高端,ARM/MIPS都是公司名。
2个物理cpu,1个物理cpu有38个逻辑核【76个线程/频率/处理单元processor)】。CPU就intel和amd。CPU(S):所有cpu的总逻辑核数。socket:物理cpu数量。top -d 1。
2.1 可见性volatile
c语言中也有volatile:一般用于中断程序,寄存器内存映射场景。
如下第一个core为主线程,第二个core为开辟的线程。
如上线程2不能立即读到线程1写后的最新变量值(线程1写,线程2读),多线程不可见性
。如何解决多线程不可见性:加volatile关键字使a在主存和localcache间强制刷新一致。
2.2 原子性(读写原子)AtomicInteger/synchronized
如果线程1和2都进行基于读的变量再对读的变量再进行写,最典型操作i++,T1和T2都进行i++操作。
一开始i=0,经过两个线程两次i++操作结果变成了1,这显然是不对的,并且这种情况下不能用volatile保证这样操作的正确性(两个线程既有读操作,又有基于读操作的写操作,可见性只保证一个线程写另一个线程读是正确的
,这里可见性不适用)。
现在想做的是将读操作和写操作合为一步,要么同时发生要么同时不发生(原子性)。在保证原子性同时一定以保证可见性为前提
(不是并列关系,AtomicInteger类里本质上就是volatile),本身不可见的话没办法保证原子性。
也可用synchronized同步关键字来保证原子性发生,同步关键字同一时间只有一个线程进入代码段。
volatile
可见性关键字最轻量级(保证一个线程写,一个线程读能读到最新的值),AtomicInteger
(保证既有读操作又有写操作如i++这种场景下能保证操作的原子性)基于volatile,synchronized
最重量级(能保证整个代码块中所有操作都是原子性的)。多线程情况下需要自增请使用Atomicxxx类来实现
。
2.IO多路复用
如下A,B。。都是客户端,方框是服务端。首先想到应对并发,写一个多线程程序,每个传上来的请求都是一个线程,现在很多rpc框架用了这种方式,多线程存在弊端:cpu上下文切换,因而多线程不是最好的解决方案,转回单线程。如下while(1)…for…就是单线程。硬件是硬盘和网卡。
2.1 select
select是系统调用函数,system是一个C/C++的库函数。
while(1)中FD_ZERO将rset初始化0,用FD_SET将有数据的fd插个旗子,并赋给rset。
2.2 poll
pollfds数组替代bitmap。
2.3 epoll
epoll_wait和前面select和poll不一样,有返回值。最后只遍历nfds,不需要轮询,时间复杂度为O(1)。epoll解决select的4个缺点。
IO模型(BIO/NIO/AIO):阻塞:发起io读取数据的线程中函数不能返回。同步:拿到io读取完的数据之后,对数据的处理是在接收数据线程的上下文后紧接着处理。异步:回调函数中进行数据处理。redis,nginx,javaNIO/AIO都用的是epoll,多路io复用借助了硬件上优势DMA。
如上看出java比C语言系统调用多的多,因为java要启动jvm虚拟机,jvm要读jdk的lib库等很多操作。如上并没有发现open…xml操作,因为java程序主要启动jvm进程,jvm进程可能又起了很多线程去真正运行main函数,所以加-f。
3.内存管理
dma访问的物理内存不经过cpu cache,假设cpu写一段数据,数据还缓存在cache中,dma从内存中搬运,此时数据不对,所以要将cpu cache刷新到内存。同样,假设网卡发了一段数据到内存,dma去读,此时不能将cpu cache刷新到内存,在dma中断时将cpu cache无效掉。
cpu在保护模式下,内存访问不再是直接通过物理内存,而是基于虚拟内存,虚拟内存模式下,很多空间被分成连续的内存页,每个内存页大小固定,比如64k,这样cpu访问内存中数据时,都要先计算出先访问哪个内存页,cpu再通过一个地址映射表把虚拟的内存地址转化为物理的内存地址。地址映射表是一个数组,下标是内存页的页号,值是该内存页对应物理内存的首地址,也有可能某个内存页对应物理内存地址不存在,cpu就会发起缺页的中断请求。
2个插口是sata,1个是pcie,都叫m.2。引脚是cpu引到m.2上,m.2兼容sata,pcie,usb,uart,smbus等,mini pcie没有pcie。
如下m.2也是走pcie协议,m.2有的管脚,pcie插槽都有。pcie m.2里面有4个config管脚,4个管脚全部接地0000代表什么设备,0001代表什么设备,物理定死了。
读写文件和申请内存
是用户态转内核态的两个例子:malloc的两种实现方式brk和mmap,两者只选一种。brk和mmap申请的都是虚拟内存,不是物理内存,想真拿到物理内存空间还要第一次访问时发现虚拟内存地址未映射到物理内存地址
,于是促发一个缺页中断(也叫缺页异常,os中异常和中断有很多类似地方)
。C语言是malloc,而java和c++中new对象
申请内存空间,也是经过这么过程。
查看linux内核中有多少系统调用:man syscalls如下。
逻辑地址(逻辑/虚拟/进程内存)
:逻辑地址需映射到物理内存
才能完成对内存操作,那为什么程序操作是虚拟的逻辑地址,不能直接操作物理地址即对内存条操作?因为程序是写死的(操作的地址是固定的),而硬件内存条哪些地址被占用了一直变化,因为os是多进程的,当前进程需要操作的地址,其他进程在使用,这样不能使用这块地址了,所以说除非是单进程机器,否则为了进程安全
必须做出逻辑地址和物理地址的映射。
如下固定偏移量映射:程序1的偏移量(初始位置)是0
,程序2的偏移量(初始位置)200
:如果程序1操作的逻辑地址是100,那么映射的物理地址也100(因为偏移量0)
;如果程序2操作的逻辑地址是50,映射到物理内存250(因为偏移量200)
,存在两个缺陷:
第一个缺陷
:程序使用的内存无法计算的,随时间推移,进程使用的内存不断变化。这里我们说程序1使用200的内存,这种说法本身不太对的,因为我们没法去限定一个程序使用的内存大小,当然你可以说我估算了这程序使用的最大内存就是200,但这也代表整个200的一段内存中,程序使用的内存绝大多数时间都小于200。蓝色区域
中内存使用率并不高,其中存在很多没有利用起来的内存,我们把没利用起来的内存叫内碎片
。
第二个缺陷
:当程序运行完,内存被释放,比如程序1执行完后,0-200这块地址被释放出来了,此时程序3
使用了内存大小是201,这时程序3没法直接使用0-200这段内存了,假设很长一段时间内都没有占用200以内的内存这样的程序被创建,那么0-200一直被闲置,称这段内存为外碎片
。
3.1 分页
页表存在我们主存中即存在内存中,先读取页表,从页表中拿到对应帧号,再拿帧号去内存中再查询一遍,对内存操作有两次读取【时间上要优化(将页表集成到cpu中的mmu硬件中称为快表,先查快表,查不到查页表)】。页表存在主存中占空间【空间上要优化(多级页表)】。
将内存空间包括逻辑内存(左,页,地址连续)和物理内存(右,帧,地址不连续)
都进行切分,分成固定大小很多片,每一片称它为页,降低了内存碎片问题。
页表
是每个进程都需要维护的,因为每个进程映射关系是互相独立的,所以不能共用映射表,每个进程有自己的pagetable。
32位os物理地址有2的32次方个即4000000000个地址(内存的一个地址里住着一字节Byte数据)即4GB
。32位程序以为自己拥有4GB内存,如两个32位程序,一个使用了2GB内存,另一个使用了3GB内存。但整个物理机只有4GB内存,造成虚拟地址可能比物理地址大,多出来部分可将虚拟地址的页映射到磁盘上。但映射到磁盘上导致下一次读
映射到磁盘上这一页内存时会触发一个缺页中断进入到内核态
,整个会产生一个大(major)错误
。linux下这磁盘部分又叫swapping
(与物理帧交换)。
3.2 分段
对虚拟地址分成多个段:堆区heap和栈区stack也是程序的段,text(代码段,存程序本身二进制字节码),data(数据段,存程序中一些静态的变量)。不同程序共享内核(kernel space)这1G空间,共享内存如Libraries函数库(so/dll文件)。
如下free206M和available1.6G
能用的是哪个?是1.6G
。used包含shared,free是真正的空闲,没有任何东西在使用的大小。文件磁盘缓存指读过的文件暂时帮我们缓存到内存中
下次再读的时候直接从内存中拿出来就能加速对文件读写操作。比如说现在free的空间只有206M,我有个程序要用1G内存,能用吗?能
,buffer/cache这边1.6G中有800M扔出去释放掉+206M=1G给程序。
buff/cache中间为什么有个/,较早内核中free-h看分buffcache
(以磁盘扇区【硬件扇区】为单位直接对磁盘缓存)和pagecache
(以页【文件系统】为单位对磁盘文件缓存)两项。两项有重复的地方,文件本质也是磁盘。
3.3 brk
C语言中有sbrk库函数是对brk的一个封装,如下brk申请内存,内存是连续的,并不是说在堆空间随便找内存就把空间给你。用户无法操作硬件如内存条,必须交给内核帮我们操作完了再把结果给我们
当前我们对第5,6,7,8四个字节赋int值123。只有第一个字节通brk申请出,却给第5-8字节赋值,这样会不会报错呢?不会,主要原因是在上节讲到的操系内存的分页管理所导致的,也就是说brk申请内存申请最小单位为1页,一般系统中页大小4k,所以brk看似申请1字节其实申请了一页(4096个字节)
,所以第5-8字节也属于4096字节里,也是当前进程所能支配的内存,所以不报错。
如下+号相当于向后移动1024个int,如下报段错误。
3.4 mmap
mmap还有直接将磁盘文件映射到内存作用,类似read,不是malloc。
如下触发大错误因为对文件的映射,将文件映射到内存,也是惰性的,这文件没有直接读到内存里,而是当真正需读文件里内容
时才会映射到内存里。第一次触发是上面for循环里打印文件内容时到内存中读,发现这一页在查页表时对应是磁盘就触发一个缺页错误,对应是磁盘,触发majflt
,将磁盘内容加载到内存中,之后就是一些小错误了。-p:指定进程号。-r:显示各个进程的内存使用统计。grep -v grep过滤掉包含grep的行。最后1是输出1次信息。
read系统调用进入内核态,内核态将文件内容加载到内核空间(如下kernel space),内核空间给它复制到用户空间,再从内核态切换到用户态,然后用户的程序就可读到文件的内容了,有个文件-内核空间-用户空间
周转过程。
mmap直接将文件进行了映射,一开始在页表中填充的是磁盘disk即FILE文件,一开始mmap是惰性的直接对应磁盘文件,真正读取时触发缺页
将文件加载到内存。
mmap这么牛干嘛还用read函数?mmap虽减少了内核空间到用户空间拷贝(0拷贝)
,但mmap没法利用前面讲的buffer/cache对文件缓冲这么一块空间,而且mmap第一次触发的缺页异常耗时不一定比read少。