操作系统概述

主板上的组件通过 总线 通信,总线分为 地址总线控制总线数据总线,因为不同组件速度不一样,所以将连接快组件的总线合并在一起叫做 北桥(离CPU很近),连接慢组件的总线合并在一起叫做 南桥(离开IO设备近)

DMA(Direct Memory Access)负责将数据在IO设备和内存之间传输,完成/失败后通过 中断 方式通知CPU,而过程中不需要CPU参与

数据在内存之间拷贝设计四次拷贝,因为可能需要对内存中的数据进行修改,所以需要将数据拷贝到用户态缓存中

后来linux通过sendfile函数将 从硬盘读取到的数据缓冲区地址和长度给到网络socket描述符 省去了向用户态空间的两次拷贝,零拷贝过程不能对读取到的数据进行操作

 

Linux 采用虚拟内存使 每个进程 都有内存大小的空间,只有访问内存资源时候,在会在 虚拟地址表 (页表)中建立虚拟地址和物理地址的映射(Linux下页表不是共享的,每个进程都有自己的页表),避免了直接访问内存进行破坏操作;虚拟地址空间分为 用户态内核态 用户态空间 划分5个区域:代码段、数据段、BSS段、堆、栈,数据段、BSS 段、堆通常是被连续存储在内存中,在位置上是连续的,而代码段和栈往往会被独立存放,堆和栈两个区域在 i386 体系结构中 栈向下扩展、堆向上扩展,相对而生 在堆和栈之间有一块 文件映射区 内核态空间 包括内核镜像、物理页表、驱动程序

  1. 代码段:存放可执行文件的操作指令,可执行程序在内存中的镜像,它是不可写的。

  2. 数据段:存放可执行文件中 已初始化 全局变量(程序静态分配的变量和全局变量)

  3. BSS段:程序中未初始化的全局变量,在内存中 bss 段全部置零

  4. 堆:存放 进程运行中 被动态分配的内存段,它的大小并不固定,可动态扩张或缩减

  5. 栈:存放程序临时创建的局部变量,也就是函数中定义的变量

使用malloc用于申请用户空间的虚拟内存,当申请小于 128KB 小内存的时,malloc使用 sbrk或brk 分配内存;当申请大于 128KB 的内存时,使用 mmap 函数申请内存,由于 brk/sbrk/mmap 属于系统调用,如果每次申请内存都要产生系统调用开销,cpu 在用户态和内核态之间频繁切换,非常影响性能,而且,堆是从低地址往高地址增长,如果低地址的内存没有被释放,高地址的内存就不能被回收,容易产生内存碎片。因此,malloc采用的是内存池的实现方式,先申请一大块内存,然后将内存分成不同大小的内存块,然后用户申请内存时,直接从内存池中选择一块相近的内存块分配出去。

linux进程内存分配

无论是brk还是mmap方式分配内存,都是在虚拟内存上进行的,只有在访问该内存,发生缺页中断时,才会进行内存分配

brk

brk是在分配内存小于128k时通过在堆上移动_edata指针,分配内存,释放brk内存需要先释放最高地址内存后才会释放当前内存,也就是说释放的空间是可重用

mmap

当分配内存大于128k的时候会直接分配内存,该方式分配的内存可以单独释放

排查问题

在linux中通过top命令查看当前进程状态,S表示当前进程在睡眠但可中断,D表示在睡眠但不可中断,也就是无法响应kill -9(磁盘IO/网络IO,出现时间短暂很难捕获) 在linux负载(load average)是计算在CPU上执行&等待的线程数+不可中断且正在睡眠的线程数;(不可中断睡眠态的进程(TASK_UNINTERRUTED)一般都在进行I/O等待,比如磁盘、网络或者其他外设等待。由此我们可以看出,Load Average在Linux中体现的是整体系统负载,即CPU负载 + Disk负载 + 网络负载 + 其余外设负载,并不能完全等同于CPU使用率)即:系统负载=CPU负载+IO负载

排查步骤

  1. 资源瓶颈定位:top、iostat、日志

  2. 定位资源瓶颈进程:pidstat -u(查看单个进程cpu使用情况)、pidstat -d/iotop(查看单个进程io使用情况)

  3. 进程内部分析:jstack(打印堆栈信息)、strace(跟踪进程执行时的系统调用和所接收的信号进程)、tcpdump(抓包,一般RPC调用)

负载高、CPU us高

这种情况说明资源主要消耗在应用进程,且占用CPU资源,可能引发的原因有以下几类:

  • 死循环或代码中存在CPU密集计算。这种情况多核CPU us会同时上涨。

  • 内存问题,导致大量FULLGC,阻塞线程。这种情况一般只有一核CPU us上涨。

  • 资源等待造成线程池满,连带引发CPU上涨。这种情况下,线程池满等异常会同时出现。

负载高、CPU低

这种情况出现的根本原因在于不可中断睡眠态(TASK_UNINTERRUPTIBLE)进程数较多,即CPU负载不高,但I/O负载较高。可进一步定位是磁盘I/O还是网络I/O导致。(网络IO也会产生大量软中断,因此si指标也会升高)

负载高、CPU sys高

这种情况CPU主要开销在于系统内核,可进一步查看上下文切换情况。

  • 如果非自愿上下文切换较多,说明CPU抢占较为激烈,大量进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。

  • 如果自愿上下文切换较多,说明可能存在I/O、内存等系统资源瓶颈,大量进程无法获取所需资源,导致的上下文切换。

CPU si高

这种情况CPU大量消耗在软中断,可进一步查看软中断类型。一般而言,网络I/O 或者 线程调度 引起软中断最为常见:

  • NET_TX & NET_RX。NET_TX是发送网络数据包的软中断,NET_RX是接收网络数据包的软中断,这两种类型的软中断较高时,系统存在网络I/O瓶颈可能性较大。

  • SCHED。SCHED为进程调度以及负载均衡引起的中断,这种中断出现较多时,系统存在较多进程切换,一般与非自愿上下文切换高同时出现,可能存在CPU瓶颈。

 

进程(Process)

进程是操作系统资源分配基本单位,一个进程不能访问另外一个进程中的资源,操作系统对进程控制是仅通过PCB(进程控制块),PCB在内存中占用一个连续区域(链表),无论单核还是多核系统,任意时刻运行的进程只能有一个,操作系统将cpu控制权在不同进程直接切换实现了并发,进程间切换机制是上下文切换:保存当前进程上下文,恢复新进程上线问,cpu控制权转移到新进程, 进程切换需要切换页表,需要通过内核调用

PCB保存内容

  1. 进程标识符

  2. 进程描述信息

  3. 进程状态

  4. 进程优先级

  5. 资源分配信息

  6. cpu状态信息

进程5种基本状态

  1. 初始状态:start

  2. 执行状态:run

  3. 就绪状态:pending,等待时间片轮转

  4. 等待状态:waiting,等待时间调用

  5. 停止状态:ending

 

进程间数据共享

不同的进程都是在同一操作系统下执行,因此不同进程之间可以共享操作系统资源,如:cpu控制权,内存空间,I/O控制权等

使用虚拟内存相当于在程序和真实内存空间加了一层 中间层 程序不需要考虑代码真实的存放位置方式了与其他进程之间的内存使用冲突,以及实现了内存空间的自动分配

不同进程之间通过虚拟内存映射表来管理使用的内存空间(即:swap区),这是一种空间维度上的存储扩展,还可以通过GC机制实现时间维度上的存储扩展

线程(Thread)

线程是操作系统调度的基本单位,线程视是程序中的顺序执行流程,是程序执行的最小单位;同一进程中可以开启多个线程(执行流程)来运行,不同线程之间并发执行任务由操作系统调度,开发者无法参与;线程是依赖于进程的,是进程的执行流程,线程共享进程中的全部资源。操作系统通过TCP来(线程控制块)控制不同的线程。线程不具备存储空间,但每个线程都有独有的栈、程序计数器、本地方法区都是存储在依赖的进程中(TLS例外),同一进程的所有线程共享同一块虚拟地址(页表)因此线程之间切换不需要经过内核,TCB对内核不可见

TCB(线程控制块)包括:

  1. 线程标识符

  2. 寄存器

  3. 线程运行状态

  4. 优先级

  5. 线程专有存储区(TLS)

  6. 信号屏蔽

协程(Coroutine)

协程是由程序控制,在同一 线程 内中断当前函数,执行其他函数,在适当的时候再回来执行的机制,是用户态实现单线程并发的机制。协程由开发者控制,没有创建线程的开销,适用于I/O密集型程序

DMA:直接完成内存和设备间的数据传输,不需要cpu的参与(寄存器中转)

python中多线程效率不高,因为cpython解释器中唯一的GIL(全局解释锁)规定:线程执行前必须先获取GIL

一个进程中含有两个线程,分别为线程0和线程1,两个线程全都引用对象a。当两个线程同时对a发生引用(并未修改,不需要使用同步性原语),就会发生同时修改对象a的引用计数器,造成计数器引用少于实质性的引用,当进行垃圾回收时,造成错误异常。因此,需要一把全局锁(即为GIL)来保证对象引用计数的正确性和安全性。

原文地址:https://www.cnblogs.com/leon618/p/13783466.html