进程、系统性能和计划任务
进程和内存管理
什么是进程?
process:程序是静态文件,程序启动后会载入到内存中,运行中的程序就是进程,进程是一个指令合集,是资源分配的单位。
第一个进程是 init,centos7 以后为 systemd
进程都在/proc/
目录下,例如有不明进程 724 占用 CPU 过高,可以cat /dev/null > /proc/724/exe
先清空进程,然后再排查问题,不直接 kill 是怕这个进程是木马程序,kill 掉还会重启。
进程都是由父进程创建,使用 fork()函数创建子进程。fork()会产生一个和父进程完全相同的子进程,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。这种机制就叫写时复制。
写时复制在 php 中的应用:
1 | $a = 'this is string'; |
此时,$a和$b 在内存中指向同一地址, 当修改$a或者$b 的时候, 才会复制一份, 然后对复制的这份进行修改。
1 | 146 typedef struct _zend_refcounted_h { |
进程结构
内核把进程存放在任务队列中,任务队列是双向循环链表结构。
链表中的每一项都是结构体 task_struck,这种结构体称为进程控制块 PCB,PCB 中包含了一个具体进程的所有信息:
- 进程 id、用户 id 和组 id
- 程序计数器
- 进程的状态(有就绪、运行、阻塞)
- 进程切换时需要保存和恢复的 CPU 寄存器的值
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录
- 文件描述符表,包含很多指向 file 结构体的指针
- 进程可以使用的资源上限(ulimit –a 命令可以查看)
- 输入输出状态:配置进程使用 I/O 设备
进程、线程、协程

查看进程中的线程:cat /proc/PID/status | grep -i threads
- 进程是资源分配的单位
- 线程是操作系统调度的单位
- 进程切换需要的资源很大, 效率很低;线程稍好;协程最好
- 多进程、线程根据 cpu 核数不一样可能是并行的,但是协程是在一个线程中,所以肯定是并发
进程相关概念
进程运行在内存中,所以有必要了解一下内存。磁盘给文件分配空间的最小单位是块,一般是 4K,内存给进程分配空间的最小单位是页,又叫页框,大小也是 4K
1 | [root@centos7 kernels]# getconf -a | grep -i size |
getconf 命令可以查看系统参数。比如 系统位数、页面大小等。-a 获取所有系统参数信息。
范例:
1 | [root@centos7 kernels]# getconf LONG_BIT |
物理地址空间和虚拟地址空间
物理地址空间:你插了两根 8G 的内存条,就有 16G 的物理地址空间。
虚拟地址空间 和 交换空间 swap 没有关系,是进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据,以及 malloc、new 分配的堆空间和分配的栈空间等。假如进程申请 100m 的内存,但实际上只使用 了 10m,那么它会增长 100m,而不是实际的使用量。现代操作系统里面分配虚拟地址空间操作不同于分配物理内存。在 64 位操作系统上,可用的最大虚拟地址空间有 16EB,即大概 180 亿 GB。那么在一台只有 16G 的物理内存的机器上,也可以要求获得 4TB 的地址空间以备将来使用:
1 | void *mem = mmap(0, 4ul * 1024ul * 1024ul * 1024ul * 1024ul, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); |
当使用 mmap 并设置 MAP_NORESERVE 标志时,并不会要求实际的物理内存和 swap 空间存在。执行上述代码,可以在 top 中看到使用了 4096g 的 VIRT 虚拟内存,这当然不是真实的内存,它只是表示使用了 4096GB 的地址空间而已。
一般来说不用太在意 VIRT 太高,因为你有 16EB 的空间可以使用。
MMU
Memory Management Unit 内存管理单元,是硬件,负责虚拟地址转换为物理地址。
程序在访问一个内存地址指向的内存时,CPU 不是直接把这个地址送到内存总线上,而是被送到 MMU,然后把这个内存地址映射到实际的物理内存地址上,最后通过总线再去访问内存,程序操作的地址就是虚拟内存地址。
TLB
Translation Lookaside Buffer 翻译后备缓冲器,用于保存虚拟地址和物理地址映射关系的缓存
用户和内核空间
C 代码和内存布局之间的对应关系

每个进程都包括 5 种不同的数据段:
- 代码段:可执行程序在内存中的镜像,只读,不可写。
- 数据段:存放已初始化全局变量
- BSS 段:Block Started by Symbol”的缩写,意为“以符号开始的块,BSS 段包含了程序中未初始化的全局变量,在内存中 bss 段全部置零。
- 堆:heap,存放数组和对象,堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
- 栈:stack,栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括 static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。可以把堆栈看成一个寄存、交换临时数据的内存区
堆和栈的区别
- 程序内存布局场景下,堆与栈表示两种内存管理方式;
- 数据结构场景下,堆与栈表示两种常见的数据结构;
程序内存分区中的堆和栈:
- 栈 由操作系统自动分配释放,用与存放函数的参数值,局部变量等,其操作方式类似与数据结构中的栈。栈区向地址减小的方向增长。程序员无法控制,若分配失败,则提示栈移出错误。
- 堆 由开发人员分配和释放,若开发人员不释放,程序结束时由 OS 回收,分配方式类似与链表。php 中 new 等可以申请内存,delete 等可以删除内存。
数据结构中的堆和栈
- 栈的基本操作包括初始化、判断栈是否为空、入栈、出栈及获取栈顶元素等。
进程使用内存问题
内存泄露:Memory Leak
指程序中用 malloc 或 new 申请了一块内存,但是没有用 free 或 delete 将内存释放,导致这块内存一直处于占用状态
内存溢出:Memory Overflow
指程序申请了 10M 的空间,但是在这个空间写入 10M 以上字节的数据,就是溢出
内存不足:OOM
OOM 即 Out Of Memory,“内存用完了”,在情况在 java 程序中比较常见。
在日志 messages 中看到类似下面的提示:
1 | Jul 10 10:20:30 kernel: Out of memory: Kill process 9527 (java) score 88 or sacrifice child |
当 JVM 因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会选一个进程杀掉。
导致原因:
- 给应用分配的内存太小
- 内存泄露和内存溢出导致内存不够用
相关内核参数:
1 | echo 2 > /proc/sys/vm/overcommit_memory |
Linux 默认是允许 memory overcommit 的,只要你来申请内存我就给你,寄希望于进程实际上用不到那么多内存,但万一用到那么多了呢?Linux 设计了一个 OOM killer 机制挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续。也可通过设置内核参数 vm.panic_on_oom 使得发生 OOM 时自动重启系统。这都是有风险的机制,重启有可能造成业务中断,杀死进程也有可能导致业务中断。所以 Linux
2.6 之后允许通过内核参数 vm.overcommit_memory 禁止 memory overcommit。
overcommit_memory
- 0:默认值,允许值,允许 overcommit,但是过于明目张胆的 overcommit 会被拒绝,内核利用某种算法猜测你的内存申请是否合理,它认为不合理就会拒绝 overcommit。
- 1:允许 overcommit,对内存申请来者不拒。
- 2:禁止 overcommit,申请的内存不允许超过可用内存的大小。
overcommit_ratio
overcomit_memory= 2 时,该参数有效,默认值 50,表示物理内存的 50%。
panic_on_oom
- 0:默认值,出现 oom 时,触发 oom killer
- 1:程序在有 cpuset、memory policy、memcg 的约束情况下的 OOM,可以考虑不 panic,而是启动 OOM killer。其它情况触发 kernel panic,即系统直接重启
- 2:当出现 oom,直接触发 kernel panic,即系统直接重启
进程状态
进程的生命周期:

使用 prtstat 或 top 可以查看命令状态,或者直接查看文件/proc/PID/status
top 命令的 S 字段表示进程的状态:S 表示休眠,R 表示正在运行,Z 表示僵死状态,N 表示该进程优先值是负数
进程的基本状态:
- 创建:创建 PCB,如果无法无法创建(比如资源无法满足)
- 就绪:ready,进程已分配到所需资源,等待 CPU 的调度
- 执行:running,CPU 调度,进入到执行状态
- 阻塞:执行的进程由于某些事件而暂时无法运行,进入阻塞状态,等阻塞解决了,进入到就绪状态。CPU 是不会等你的。
- 终止:进程结束,正常结束或非正常结束。
进程更多状态
- 睡眠:可以当成阻塞处理,区别在于阻塞状态是被动进入,睡眠状态是主动进入,而且可以定时。准确的说,阻塞是一种状态,而睡眠和挂起是一种行为。
- 可中断:interruptable
- 不可中断:uninterruptable
- 停止:stopped,暂停于内存,但不会被调度,除非手动启动
- 挂起:暂停于外存,需要先激活才能使用
- 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
- 僵尸进程:zombie,如果一个进程已经终止,但是它的父进程尚未调用 wait() 或 waitpid() 对它进行清理,这时的进程状态称为僵死状态,处于僵死状态的进程称为僵尸进程(zombie process)。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了。
- 杀死父进程可以关闭僵死态 的子进程
LRU 算法
IPC 进程间通信
同一主机
1 | pipe 管道,单向传输 |
不同主机:socket=ip:port
1 | RPC remote procedure call |
进程优先级

top 命令的 NI 字段表示 nice 优先级,-20 到 19,取值越大,优先级越低。就像人一样,越 nice 的人越被欺负,越 nice 的进程优先级越低。
进程分类
- 守护进程:daemon,在系统引导过程中启动的进程,和终端无关。
- 前台进程:和终端相关,通过终端启动的进程。
IO 调度算法
- NOOP
- Deadline scheduler
- Anticipatory scheduler
- CFQ
查看 IO 调度算法:cat /sys/block/sda/queue/scheduler