一, 背景
玩过一段时间 Linux 后, 大家是否发现 Linux 也就是一个操作系统而已, 和 Windows 是类似的, 只不过是 Windows 拥有图形化界面, 而 Linux 大部分情况下只是个黑黑的窗口. Windows 多用于个人, 而 Linux 因为出色的性能而多用于服务器.
服务器不是智能的, Linux 也不是智能的, 所以需要运维人员通过调试, 将服务器的硬件和 Linux 的特性结合起来, 从而达到性能最大输出, 这就是我理解的 Linux 性能调休. 下面展示的是一张服务器和 Linux 系统结合的概览图:
二, 基础知识
调休不是说调就调的, 我们要先了解 Linux 是如何处理任务以及与硬件资源进行交互的. 性能调优需要在深刻理解硬件资源, 操作系统和应用程序的基础上进行. 下面说一下在 Linux 中和性能关系最密切的几个部分.
三, Linux 进程管理
3.1 什么是进程?
进程是在处理器中执行的实例, Linux 内核调度各类资源来满足进程的需求. 我的理解是, 应用程序是存储硬盘上的数据, 通过程序入口将应用程序装载到内存运行起来后就生成了一个进程(运行中的程序). 进程会使用各种资源, 如 CPU, 键盘鼠标, 处理器, 硬盘等, 而这些工作由 Linux 内核调度, 以满足各个进程间的协同, 竞争等.
Linux 的进程管理方式类似于 Unix 的进程管理方式, 包含进程调度, 中断处理, 信号, 进程优先级, 进程切换, 进程状态, 进程的内存等等
所有运行在 Linux 操作系统的进程都被 task_struct 这个结构体管理, task_struct 也被称为进程描述符. 进程描述符包含一个进程运行所需的所有信息, 比如进程的 id, 进程的属性以及构建进程的资源.
下图展示了进程信息相关结构的概览:
3.2 进程的生命周期
每个进程都有自己生命周期, 比如创建, 执行, 终止和删除. 在系统运行过程中, 这些阶段反复执行成千上万次. 因此, 从性能的角度来看, 进程的生命周期十分重要.
下图展示了一般进程的生命周期:
当一个进程创建一个新的进程, 创建进程的进程 (父进程) 使用名为 fork()的系统调用. 当 fork()被调用的时候, 它会为新创建的进程 (子进程) 获得一个进程描述符, 并且设置新的进程 ID. 复制父进程的进程描述符给子进程. 这时候, 不会复制父进程的地址空间, 而是父子进程使用同样的地址空间.
exec()系统调用把新程序复制到子进程的地址空间. 由于共享同样的地址空间, 写入新进程的数据会引发页错误的异常. 此时, 内核给子进程分配新的物理页. 这个延迟的操作叫做 Copy On Write. 子进程和父进程执行的程序通常不一样, 它执行自己的程序. 这个操作避免了不必要的开销, 因为, 复制整个地址空间是很慢且低效率的, 还会消耗很多的处理器时间和资源.
当程序执行完成, 子进程使用 exit()系统调用终止. exit()会释放进程的大部分数据结构, 并且把这个终止的消息通知给父进程. 这时候, 子进程被称为 zombie process(僵尸进程). 直到父进程通过 wait()系统调用知悉子进程终止之前, 子进程都不会被完全的清除. 一旦父进程知道子进程终止, 它会清除子进程的所有数据结构和进程描述符.
3.3 线程 说到进程不得不提的就是线程了. 线程是单个进程中生成的执行单元. 多个线程在同一个进程中并发运行. 它们共享内存, 地址空间, 打开文件等等资源. 还能访问同样的应用数据集. 线程也被称为轻量级进程(Light Weight Process). 由于线程间共享资源, 线程不能同时改变它们共享的资源. 互斥, 锁, 序列化等等都是由用户应用程序来实现.
从性能的角度看, 创建线程比创建进程更加低消耗, 因为创建线程不需要复制资源. 另一方面, 从进程和线程在调度上看, 他们拥有相似的行为. 内核用类似的方法来处理他们.
下图是进程和线程的简单对比:
在当前的 Linux 实现中, 线程由 POSIX(Portable Operating System Interface for UNIX, 可移 植操作系统接口)的兼容库 (pthread) 提供. Linux 支持多线程.
3.4 进程优先级和 nice 级别
进程优先级由动态优先级和静态优先级决定, 它是决定进程在 CPU 中执行顺序的数字. 优先级越高的进程被处理器执行的机会越大.
根据进程的行为, 内核使用启发式算法决定开启或关闭动态优先级. 可以通过 nice 级别直接修改进程的静态优先级, 拥有越高静态优先级的进程会获得更长的时间片(时间片是进程在处理器中的执行时间).
Linux 支持的 nice 级别从 19(最低优先级)到 - 20(最高优先级), 默认只是 0. 只有 root 身份的用户才能把进程的 nice 级别调整为负数(让其具备较高优先级).
3.5 切换上下文
在进程执行过程中, 进程的信息存放在处理器的寄存器和缓存中. 这部分执行中进程存放在寄存器中的数据就叫做 context, 上下文. 在切换进程中, 正在处理的进程上下文被保存起来, 把下一个要执行的进程的上下文恢复到寄存器. 上下文通常存储在进程描述符和内核态栈中. 进程切换就叫做上下文切换(context switching). 因为处理器每次上下文切换都要为新进程刷新寄存器和缓存, 可能引发性能上的问题, 所以应该尽量避免太多的上下文切换. 下图描述上下文切换是如何工作的:
3.6 中断处理
中断处理是最高优先级别的任务之一. 中断通常由 I/O 设备产生, 譬如网络接口, 键盘, 磁盘控制器. 中断处理器把键盘输入, 网络帧到达这类事件通知给内核, 它告诉内核尽快中断进程执行, 因为某些设备需要快速的回应. 这对系统稳定性是一个挑战. 当中断信号到达内核, 内核必须切换当前执行中的进程到新的进程, 处理中断. 这就意味着会发生上下文切换, 同时也意味着大量的的中断会导致系统性能下降.
在 Linux 中有两类中断, 硬中断是由设备产生的需要做出响应的中断, 例如磁盘 I/O 中断, 网卡中断, 键盘和鼠标中断. 软中断用于任务处理, 可以推迟, 例如 TCP/IP 操作, SCSI 协议操作. 可以在 / proc/interrupts 中看到相关的硬中断信息.
3.7 进程状态
TASK_RUNNING 在这个状态中, 进程正在 CPU 中执行, 或者在运行队列 (run queue) 中等待运行.
TASK_STOPPED 进程由于特定的信号 (如 SIGINT,SIGSTOP) 而挂起就会处于这个状态, 等待恢复信号, 比如 SINCONT.
TASK_INTERRUPTIBLE 在此状态中, 进程挂起并且等待一个特定的条件. 假如进程处于 TASK_INTERRUPTIBLE 状态并且收到一个停止信号, 进程状态会发生改变, 操作会中断. TASK_INTERRUPTIBLE 的典型例子是等待键盘中断.
TASK_UNINTERRUPTIBLE 类似于 TASK_INTERRUPTIBLE. 当进程处于 TASK_INTERRUPTIBLE 状态可以被中断, 发送一个信号给 TASK_UNINTERRUPTIBLE 却不会有任何反应. TASK_UNINTERRUPTIBLE 最典型的例子是进程等待磁盘 I/O 操作.
TASK_ZOMBIE 进程在使用 exit()系统调用退出以后, 父进程应该知道进程终结. 在 TASK_ZOMBIE 状态中, 进程在等待父进程收到通知并释放所有的数据结构.
它们之间的关系如下图:
说下僵尸进程:
当进程已经收到信号而终止, 正常情况下, 完全结束之前, 它有一些时间来完成所有的任务(例如关闭打开的文件). 在这个很短的的正常的时间片里, 这个进程是僵尸.
当进程完成了所有的关闭操作, 它向父进程报告它即将终结. 有时候, 僵尸进程不能够结束它自己, 这个状态下, 它就显示状态为 Z(zombie).
因为它已经死了, 所以不可能使用 kill 命令杀死这种进程. 如果无法摆脱僵尸进程, 可以杀死僵尸进程的父进程, 这样僵尸进程也会消失. 然后, 如果僵尸进程的父进程是 init, 你就别这么做了, init 是非常重要的进程, 你可能要重启才能摆脱僵尸进程了.
3.8 进程内存段
进程使用自己的内存区域处理任务, 任务种类由场景和进程用途决定. 进程有不同的工作特性和不同数据大小要求, 进程必须处理各种大小的数据. 为满足这一要求, Linux 内核给各个进程使用动态内存分配机制. 进程内存分配结构如下图:
进程内存区域包含如下段:
文本 : 存储可执行代码的区域
数据 : 数据段由如下三个区域构成
Data: 存储初始化数据, 比如静态变量
BSS: 存储初始化 0 数据, 数据初始化为 0
Heap(堆): 根据需要使用 malloc()分配动态内存. 堆向高地址空间增长.
栈: 该区域存储局部变量, 函数参数和函数的返回地址. 栈向低地址空间增长.
用户进程的地址空间分配可以使用 pmap 命令显示出来. 你可以使用 ps 命令显示总的段大小.
3.9 Linux 的 CPU 调度
计算机的最基本功能就是计算, 为了实现计算功能, 必须要有办法管理计算资源, 处理器, 计算任务, 也就是常说的进程和线程.
下面谈下 Linux 中 CPU 调用进程的算法, 这个算法使用两个进程优先级数组:
- active
- expired
由于调度器根据进程的优先级和先前阻塞率来位分配时间片, 进程的优先级被放在一个 active 数组中. 当时间片到期, 它们被重新分配新的时间片, 并且放置到 expired 数组上. 当所有 active 数组上的进程都到期, active 和 expired 数组发生对换, 重启算法. 对于一般的交互进程(对应实时进程), 高优先级的进程通常会比低优先级进程分配更多的时间片, 但是并不意味着完全不给低优先级进程机会.
如下展示和 Linux 的 CPU 调度器是如何工作的:
新调度器的另外一个大有有点是支持非均匀内存架构 (Non-Uniform Memory Architecture, NUMA) 和对称多线程处理器, 例如 Intel 的超线程技术. 支持 NUMA 保证了正常情况下不会出现负载均衡的情况, 除非一个节点负担过重. 这个机制保证了在 NUMA 系统中, 比较缓慢的链路负载较小. 尽管在一个组中的处理器调度的每一个处理, 会被负载均衡, 但是调度器的组只会在节点负载过高和要求负载均衡的时候产生.
四, 下一节是???
谈完 Linux 的进程管理, 下一节谈一下 Linux 的内存体系
来源: https://juejin.im/post/5c1db0fbe51d4511fb7d76e2