9.1 内核

9.1.1 系统调用

System Call

系统调用,指运行在用户空间的进程 向内核发出的请求,请求使用内核提供的 服务

系统调用是操作系统中的 最小功能单位,运行在 内核空间

系统调用提供用户进程与操作系统之间的 接口,使上层应用能够 访问到内核的资源

大多数系统交互式操作需求在内核态执行,如设备 I/O 操作、创建进程、进程间通信。

要完成一个应用程序的功能,通常要通过多个系统调用来实现。

常见的系统调用

  • 文件子系统调用:open()close()read()write()chmod()chown()
  • 进程控制调用:fork()exec()exit()wait()brk()signal()

函数库

系统调用和普通库函数调用非常相似,只是系统调用由操作系统内核提供,运行于内核态,而 普通的库函数调用 由函数库或用户自己提供,运行于用户态

公用函数库 实现了对系统调用的 封装,将简单的业务逻辑接口呈现给用户,方便用户调用。

典型实现

Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。

操作系统实现系统调用的过程
  1. 应用程序调用库函数(API);
  2. API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  4. 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
  5. 中断处理函数返回到 API 中;
  6. API 将 EAX 返回给应用程序。
应用程序调用系统调用的过程
  1. 把系统调用的编号存入 EAX;
  2. 把函数参数存入其它通用寄存器;
  3. 触发 0x80 号中断(int 0x80)。

阻塞、非阻塞系统调用

Blocking / Unblocking System Call

阻塞系统调用是指调用结果返回之前,当前线程会被 挂起。函数只有在 得到结果之后才会返回

非阻塞和阻塞的概念相对应,即使不能立刻得到结果,该函数也会 立即返回,不会阻塞当前线程。

9.1.2 分级保护域

Privilege Ring,是操作系统的一种保护机制。也叫 保护环

在发生故障时保护数据和功能,提升容错度,避免恶意操作,增强计算机安全。

但是无需使用全部的四个保护级别,因为当前的操作系统如 Windows,Linux 等,大多数使用 页机制,而页机制只有 一位 用来设定级别,因此 只能使用两个级别。通常这两个级别对应为 特权级(超级用户)和 非特权级 (普通用户)。

有 3 种主要的资源受到保护:内存I/O 端口 以及 执行特殊机器指令的能力

9.1.3 特权级

Privilege Level

在 CPU 的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。所以,CPU 将指令分为 特权指令非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。

在任一时刻,x86 CPU 都是在一个特定的特权级下运行的,从而决定了代码可以做什么,不可以做什么。这些特权级即保护环。特权级总共有 4 个,编号从 0(最高特权)到 3(最低特权)。x86 内核只用到其中的 2 个特权级:0 和 3。

在诸多机器指令中,只有大约 15 条指令被 CPU 限制只能在 0 级特权执行,其余指令的操作都受到一定的限制。这些 0 级特权指令如果被用户态的进程所使用,就会颠覆保护机制或引起混乱,所以它们被保留给内核使用。

如果企图在 0 级特权以外运行这些指令,就会导致一个一般保护错(general-protection exception),就像一个程序使用了非法的内存地址一样。

类似的,对内存和 I/O 端口的访问也受特权级的限制。

对于 Linux 来说,只使用了特权级 0 和特权级 3。工作在 0 特权级的指令具有 CPU 可提供的最高权力,而一条工作在 3 级特权级的指令具有 CPU 提供的最低,或者说最基本的权力。

CPU 特权级并不会对操作系统的用户造成什么影响,无论是超级用户还是普通用户。

所有的 用户代码 都在 3 级特权 上执行,所有的 内核代码 都在 0 级特权 执行,与用户身份无关。

9.1.4 用户态与内核态

用户态

User Mode

当进程在执行用户自己的代码时,则称其处于用户运行态,简称用户态。

即此时处理器是在 3 级特权下运行用户的代码。

用户态是上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源。

当正在运行的用户进程突然被中断时,也可以象征性地称为处于进程的内核态,因为中断处理程序将使用当前进程的内核栈。

操作系统设计中的用户态

操作系统中的用户态,指非特权的执行状态。当 程序运行在 3 级特权级 时,可称之为运行在用户态。

用户态是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。

用户态启动的每个进程,根据运行该进程的用户,都被系统赋予特定的权限。

用户态中,进程被禁止执行需要处理器特权级的操作,以避免操作系统崩溃。

与 CPU 级别相比,操作系统容许用户态有更加复杂的权限设定。

如:Unix 系统中,运行在用户态的代码,禁止通过侦听 1024 以下的端口号,来伪装成常见的服务,而超级用户运行的代码则有权这样做。

有时一些内核任务可以被放到用户态执行,但它们只是替内核执行任务的特殊进程而已,而且往往可以被直接删除,不会引起严重后果。

用户态的限制

操作系统的用户态通常是在相应的 CPU 用户态 中运行代码,从而在硬件上防止非法程序。

用户态进程的执行被严格限制在一个由 0 级特权所设定的沙盒之中。

由于限制了对内存和 I/O 端口的访问,用户态代码在不调用系统内核的情况下,几乎不能与外部世界交互。它不能打开文件,发送网络数据包,向屏幕打印信息或分配内存。

内核禁止此状态下的代码进行潜在危险的操作。如写入系统配置文件、杀掉其他用户的进程、重启系统等。所有用于控制内存或打开文件的数据结构,均不能被用户代码直接使用。

因此从设计上就决定了:一个进程所泄漏的内存会在进程结束后被统统回收,之前打开的文件也会被自动关闭。

一旦进程结束,这个沙盒就会被内核拆毁

这就是为什么我们的服务器只要硬件和内核不出毛病,就可以连续正常运行 600 天,甚至一直运行下去。这也解释了为什么 Windows 那么容易死机:这并非因为微软差劲,而是因为系统中的一些重要数据结构,出于 兼容 的目的被设计成 可以由用户直接访问 了。这在当时可能是一个很好的折中,但是代价也很大。

内核态

Kernel Mode

当一个进程执行系统调用,而陷入内核代码中执行时,称进程处于内核运行态,简称内核态。

内核态控制计算机的硬件资源,并提供上层应用程序运行的环境。

内核态是一种 CPU 的特权态,这个特权态下,CPU 可以执行这个特权态才允许执行的指令,访问这个特权态才运行访问的资源。

当进程处于内核态时,处理器是在 0 级特权下执行内核的代码,执行的内核代码会使用当前进程的 内核栈

每个进程都有自己的内核栈。

用户态与内核态的切换

用户态和内核态中工作的进程有很多区别,但最重要的差别就在于 特权级 的不同,即权力的不同。

在系统中执行一个程序时,多数是运行在用户态,运行在用户态下的进程不能直接访问操作系统内核数据结构和程序,如果需要操作系统的帮助,来完成某些它没有权力和能力完成的工作时,就会 切换到内核态

用户态切换到内核态的方式

从触发的对象来看,系统调用是进程 主动 请求切换的,而异常和硬中断则是 被动 的。

系统调用

这是用户态进程 主动要求切换到内核态 的一种方式,用户态进程通过系统调用,申请使用操作系统提供的服务程序完成工作,如 fork() 实际上就是执行了一个创建新进程的系统调用。而系统调用的机制,其核心还是使用了操作系统为用户特别开放的一个 中断 来实现,如 Linux 的 int 80h 中断。

异常

当 CPU 在执行用户态的进程时,发生了某些 不可预知的异常,此时,CPU 会由当前运行进程切换到内核的特定进程,用来处理该异常。如缺页异常。

外围设备的中断

外围设备 完成用户请求的操作后,会向 CPU 发出相应的 中断信号,CPU 此时会 暂停 执行调度队列中的指令,转而去执行与中断信号对应的处理程序

如果先前执行的指令是用户态的程序,则该转换即实现了用户态到内核态的切换。

如:当硬盘读写操作完成时,系统会切换到硬盘读写的中断处理程序,以执行后续操作。

用户态与内核态的查看

/proc/<pid>/stat 文件的第 14 字段(utime)和第 15 字段(stime)表明了进程在用户态和内核态分别花费了多长时间。具体可以 man 5 proc

top 命令返回信息中,CPU 行分别显示了用户态(us)和内核态(sy)CPU 的平均负载。

top - 12:27:25 up 2:51, 4 users, load average: 4.37, 3.64, 3.44
Tasks: 194 total, 2 running, 192 sleeping, 0 stopped, 0 zombie
Cpu(s): 57.0%us, 1.3%sy, 0.0%ni, 41.1%id, 0.0%wa, 0.4%hi, 0.1%si, 0.0%st
^^^          ^^      ^^

9.2 CPU

9.2.1 中断

Interrupt

中断,是指处理器接收到来自硬件或软件的信号,提示发生了某个事件,应该被注意,这种情况就称为中断。

通常,在接收到来自外围 硬件(相对于中央处理器和内存)的 异步信号,或来自 软件的同步信号 之后,处理器将会进行相应的硬件/软件处理。发出这样的信号称为进行 中断请求(interrupt request,IRQ)。

硬件中断导致处理器通过一个运行信息切换(context switch)来保存执行状态(以程序计数器和程序状态字等寄存器信息为主);

软件中断则通常作为 CPU 指令集中的一个指令,以可编程的方式直接指示这种运行信息切换,并将处理导向一段中断处理代码。中断在计算机多任务处理,尤其是即时系统中尤为有用。

9.2.2 CPU 设计中的用户态与内核态

在此状态下,执行的代码被硬件限定,不能进行某些操作,以防止给操作系统带来安全隐患。如写入其他进程的存储空间。

就 Intel x86 架构的 CPU 来说,共有 0 ~ 3 四个特权级,0 级最高,3 级最低。

上下文关系

处理器总处于以下状态中的一种:

  • 内核态,运行于 进程上下文,内核代表进程运行于内核空间;
  • 内核态,运行于 中断上下文,内核代表硬件运行于内核空间;
  • 用户态,运行于用户空间。
进程上下文

用户空间的应用程序,通过系统调用,进入内核空间。

此时用户空间的进程要传递很多 变量、参数 给内核,内核态运行的时候,也要保存用户进程的一些 寄存器值、变量等,这些统称为进程上下文。

中断上下文

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。

这个过程中,硬件的一些 变量和参数 也要传递给内核,内核通过这些参数进行中断处理,并保存 当前的进程环境

9.2.3 寄存器

Register

定义

寄存器,是 CPU 的组成部分。

它是容量有限的 高速存储器,可用来暂存指令、数据和地址。

在电脑架构里,处理器中的寄存器是少量且速度快的内存,借由提供快速共同地访问数值来加速计算机程序的运行:典型地说就是在已知时间点所作的之计算中间的数值。

寄存器是内存层次结构中的最顶端,也是系统访问数据的最快途径。

进程如何使用寄存器

CPU 本质上是对寄存器中的数值执行一般简单的操作,这些数值会被写到内存中。

每个进程都被分配相应的内存,并被内核持续监控。

当运行中的进程要放弃处理器,以便其它进程来运行时,它需要保存当下的状态,以便下一次运行时恢复该状态。为此,操作系统需要把 CPU 寄存器的副本保存到内存中,下次轮到该进程运行时,操作系统会把寄存器的值从内存复制回寄存器,这样,进程可以从上次离开的地方继续执行了。

寄存器对于程序的意义

计算机程序有一个共同的特点,称局部引用(Locality of reference),是指频繁地访问相同的数值,把这些频繁使用的值保留在寄存器中,以提高性能。这就是快速寄存器和缓存的使命。

为频繁使用的变量分配寄存器,对于程序的性能至关重要。

寄存器的分配要么是在生成代码时由编译器进行,要么由汇编语言程序员手动进行。

9.3 内存

9.3.1 虚拟内存

物理地址

物理地址是在 地址总线 上,以 电子形式 存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。

在早期的计算机中,程序是直接运行在物理内存上的,也就是说,程序在运行时所访问的地址都是物理地址。当同时运行多个程序时,CPU 的利用率会明显提高,随之而来的问题就是,如何将有限的物理内存分配给多个程序使用

虚拟地址

为了实现多程序同时正常运行,需要考虑几个问题:

  • 每个程序所用的内存空间需要 相互隔离,以避免数据被破坏
  • 需要一个 高效 的内存管理机制来管理内存中数据的换入换出
  • 编写程序时需要使用 固定的内存地址,如访问数据和指令跳转时

解决的方法是通过在程序与物理地址之间增加一个中间层,虚拟地址。

因此,程序所看到的地址都是虚拟地址。

虚拟内存

基于虚拟地址,把地址空间 重定义 为 “连续的虚拟内存地址”,以借此 “欺骗” 程序,使它们以为自己正在使用一大块的 “连续” 地址。

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有 连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成 多个物理内存碎片,还有部分暂时存储在 外部磁盘存储器 上,在需要时进行 数据交换

与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存的使用也 更有效率

虚拟内存不只是 “用磁盘空间来扩展物理内存” 的意思 —— 这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是使用虚拟内存技术的 一个结果,它的作用也完全可以通过其它方式来实现,如覆盖非运行状态的进程,或将非常运行状态的进程及其数据交换到磁盘上。

现代所有用于一般应用的操作系统都对普通的应用程序使用虚拟内存技术,例如文字处理软件、电子表格软件、多媒体播放器等。老一些的操作系统,如 DOS 和早期的 Windows,或者那些 1960 年代的大型机,一般都没有虚拟内存的功能。

那些需要快速访问或者实时反应的嵌入式系统,以及其他特殊应用的计算机系统,为了避免其运算结果的可预测性降低,而选择不使用虚拟内存。

虚拟内存抽象模型

根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的。据此,人们想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是分页(Paging)。

分页的基本方法是把地址空间人为地 等分 成固定大小的页,每一页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。比如 Intel Pentium 系列处理器支持 4KB 或 4MB 的页大小,那么操作系统可以选择每页大小为 4KB,也可以选择每页大小为 4MB,但是在同一时刻只能选择一种大小,所以对整个系统来说,页就是固定大小的。

目前几乎所有的 PC 上的操作系统都使用 4KB 大小的页。我们使用的 PC 机是 32 位的虚拟地址空间,也就是 4GB,那么按 4KB 每页分的话,总共有 1048576个 页。

物理空间也是按照同样的方法进行分页的。

把进程的虚拟地址空间按页分割,把常用的数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里取出来即可。

虚拟内存中的页叫 虚拟页,Virtual Page;物理内存中的页叫 物理页,Physical Page;磁盘中的页叫 磁盘页,Disk Page。

虚拟地址空间的多个页可以被 映射到同一个物理页,这样就可实现 内存共享

MMU

Memory Management Unit,内存管理单元

有时称作分页内存管理单元(paged memory management unit)。它是一种负责处理 CPU 的内存访问请求的计算机 硬件,通常集成于 CPU 内部。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制,在较为简单的计算机体系结构中,负责总线的仲裁以及存储体切换(bank switching,尤其是在8位的系统上)。

虚拟存储的实现需要依靠硬件的支持,对于不同的 CPU 来说是不同的。但是几乎所有的硬件都采用 MMU 来进行页映射。

页、页帧

页,是虚拟内存中一个固定长度的、连续区块,由页表(page table)中的一个条目来代表。页是 虚拟内存 操作系统中内存管理的 最小数据单位

相对应地,页帧(page frame),是 物理内存 中最小的、固定长度的、连续区块。

页被操作系统 映射 到页帧。

在内存与磁盘之间传输页的动作,称为 分页(paging)或 交换(swapping)。

因为物理内存比虚拟内存要小的多,操作系统必须避免低效地使用物理内存。节省物理内存的一个办法是:只加载

分页表

分页表是一种数据结构,它用于操作系统中的虚拟内存系统,其存储了虚拟地址到物理地址间的 映射。虚拟地址在访问进程中是唯一的,而物理地址在硬件(比如内存)中是唯一的。

在操作系统中使用虚拟内存,每个进程会认为自己在使用一块大的连续的内存。事实上,每个进程的内存散布在物理内存的不同区域。或者可能被交换到磁盘上。当一个进程请求自己的内存时,操作系统负责把程序生成的虚拟地址,映射到实际存储的物理内存上。操作系统在分页表中存储虚拟地址到物理地址的映射。每个映射被称为 分页表项(PTE,Page Table Entry),或分布表条目。

模型

该模型中,虚拟内存和物理内存都被分成页,每个页被分配给一个独立的数字,称 页帧号(Page Frame Number,PFN)。对于程序的每条指令,CPU 都会进行从虚拟地址到物理地址的映射。另外,如果指令本身引用了内存,那么将为该引用执行翻译。

CPU 使用分页表来进行地址的翻译,每个进程通常都有一个分页表。

Process Page

如图,进程 X 的虚拟页帧号 0 被映射到物理页帧号 1,进程 Y 的虚拟页帧号映射到物理页帧号 4,分页表中的每个条目都包含以下信息:

  • 虚拟页帧号
  • 物理页帧号
  • 该页访问控制信息

要想翻译,CPU 必须首先 计算虚拟页帧号地址 和该虚拟页面内的 偏移量

CPU 在进程的分页表中查找符合该虚拟页帧号的条目,从而可以得到对应的物理页帧号,CPU 再把该页帧号乘以页大小,得到物理内存中该页基址的地址。最后,加上所需指令或数据的偏移量。

内存管理子系统

内存管理子系统是操作系统中最重要的部分之一。虚拟内存的概念是为了解决系统总是需要更多内存的问题。通过在相互竞争的进程中共享内存,虚拟内存让系统看上去比实际上有了更多的内存。

内存管理子系统包括:

大地址空间

虚拟内存可以比物理内存大很多倍

保护

每个进程都有其独立的虚拟地址空间,这些空间在不同进程间是完全相互独立的,因此进程间可以相互不影响。硬件虚拟内存机制允许内存的部分区域实现只读,可以有效地防止恶意篡改。

内存映射

内存映射用于把镜像和数据文件映射到一个进程地址空间中,在内存映射中,文件的内容直接链接到进程的虚拟地址空间。

公平的物理内存分配

内存管理子系统允许每一个运行中的进程都能公平地分享物理内存。

共享的虚拟内存

虽然虚拟内存允许进程享有独立的地址空间,有时候也会需要多个进程共享同一块内存。如多个同时运行的进程可能需要使用同一个文件,此时内存中只需要有该文件的一个副本,即可同时共享给所有进程使用。

内核空间与用户空间

在操作系统中,虚拟内存 通常会被分成内核空间(Kernel Space)与用户空间(User Space)这两个区块。

为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

这是 内存保护机制 中的一环,内核、内核扩展、驱动程序运行在内核空间;而其他应用程序则运行在用户空间。

内核空间

内核空间是 内核运行、提供服务 的内存空间。

内核空间可以执行任意命令,调用系统的所有资源。

用户空间

用户空间是 用户进程运行 的内存空间。

用户空间只能执行简单的运算,不能直接调用系统资源,用户进程只有通过使用系统调用才能访问内核空间。

内核的作用之一是:在用户空间,负责管理每个独立的用户进程,防止它们互相影响。

所有运行在用户空间的应用程序,都被统称为用户级(userland)。

9.3.2 内存布局

在多进程操作系统中,每个进程享有独立的虚拟地址空间。每个地址都指向内存中的一个字节的空间。这些虚拟地址是由 CPU 生成的,然后使用 页转换表(Page Table)将其 映射 到真实的、物理内存地址上,Page Table 由操作系统内核维护。

内核栈与用户栈

Linux 系统中,一个进程可以在内核态(Kernal mode)或者用户态(user mode)下执行,因此 Linux 内核栈和用户栈是分开的

用户栈用于进程在用户态下临时保存调用函数的参数,局部变量等数据。内核栈则含有内核程序执行函数调用时的信息。

进程有其自己的栈空间,用来保存函数中的局部变量,以及控制函数的调用和返回。

如:头 3 GB 的虚拟地址可以给用户进程使用,但最后 1 GB 是预留给内核的。

进程的内存空间布局

用户的进程在用户空间中是如下形式分布的:

每个进程的数据,按照不同的功能保存到 不同的区段

Text

这部分保存程序的 可执行指令,也可以放到 Heap 或 Stack 下面,以防止溢出。

通常这部分是可以共享的,于是可以把经常执行的程序如文本编辑器、编译器、Shell 等保存到这里。

这部分通常的权限是读、执行,以防止程序被意外篡改。

Data

这部分保存 初始化的数据,包括全局变量、静态变量,它们在编程时就预先设定好了。

分为两个区域:初始只读区,初始读写区。

之所以有读写区,是因为有些变量在运行时会发生变化。

BSS

这部分保存 未初始化数据,在程序开始执行之前,操作系统内核进行初始化,即清零。

通常这部分包含所有的全局变量和静态变量,在这里,它们被初始化为 0,或在源码中没有显式的初始化。

static int i 该变量将分配给 BSS 段。

权限:读,写

Stack

这部分包含程序 ,LIFO(Last In First Out)结构,通常位于较高的地址空间,仅挨着操作系统内核空间。

X86 架构中,这部分的数据会 向下生长,即从高地址向低地址,其它架构可能相反。

这段空间用于保存程序中函数调用所需要的所有数据,每个函数调用所堆起的一组值称为栈帧,由所有的自动化变量及调用者的返回地址组成。

在当今多数计算机体系架构中,函数的参数传递、局部变量的分配和释放都是通过操纵栈来实现的。

栈还用来存储返回值信息、保存寄存器以供恢复调用前处理机状态。

Stack,内存中有一块空间称栈,可以认为是进程其数据的一部分,与任何程序的执行密切相关。

栈帧

Stack Frame,每次调用一个函数,都要为该次调用的函数实例分配栈空间。为单个函数分配的那部分栈空间就叫做栈帧。

栈的结构

栈是一个通用的数据结构,其工作起来很像一叠盘子:

可以 push 一个项目,(相当于在一叠盘子顶上再摞一个,)使该项目成为最顶上的项目;也可以 pop 一个项目,(撤掉一个盘子,露出下面的那个)。

栈的作用

栈是 函数调用的基础,每次调用一个函数时,该函数都会得到一个新的栈帧。

栈帧也是内存的一小块空间,通常它至少包含程序执行完毕后要返回的地址、函数的输入参数,以及本地变量使用的空间。

在内存中,栈是从高地址向低地址延伸的,即栈底对应高地址,栈顶对应低地址。

栈对函数的影响
  • 每个函数有其独立的输入参数的副本,因为每个函数与其参数都会占用一个新的栈帧
  • 因此函数内定义的变量无法被其它函数看到,全局变量之所以可以,是因为其保存在数据内存单独的区域
  • 有助于递归调用,意味着函数可以随意调用其自身,因为会为其本地变量建立一个新的栈帧
  • 每个栈帧都包含返回地址。C 语言中,一个函数只允许有一个返回值,因此,按照惯例,把该值返回给调用的函数时,使用的是特定的寄存器,而不使用栈。
  • 因为每个栈帧都会参照其前一个栈帧,可以借此特性用于调试,跟随指针一路向上回退,生成栈追踪信息,其中包含该函数一路调用的所有函数,非常有助于调试。
  • 可以看到,函数工作的方式与栈的本质是精准地匹配的,任何函数都可以调用任何其它函数,被放到栈的顶层,成为最顶层的函数。最终,该函数会返回调用它的函数,把自己从栈中撤离。
  • 栈确实使调用函数变慢,因为值必须从寄存器移到内存中。一些架构允许在寄存器中直接传递参数;但是为了保持语义,让每个函数能获取每个参数的独立副本,寄存器必须要轮询。
  • 栈溢出:黑系统常用的方法,通过传递假值来实现。作为一个程序员,如果接受把任意数量的输入交给一个栈变量,如从键盘或网络读取数据,就必须严格指定该数据的大小。
  • 栈在根本上是由编译器管理的,因为它负责生成程序代码。对于操作系统来说,栈看上去与进程在内存中的其它部分没什么分别。
栈指针

Stack Pointer

为了追踪栈的当前状态,硬件会定义一个寄存器:栈指针,用来 追踪 栈的 最顶层,便于了解进程当前使用了多少栈空间。每次有新值被堆到栈顶时,会自动调整指针的位置。

当栈指针遇到 heap 指针时,说明可用内存已经耗尽。

Heap

这段空间用于 动态分配给一些变量,在编译时是无法预期这些变量需要多少内存的。

Heap 是从 BSS 的末尾开始的,向上,向更高的地址生长,由 malloc/newfree/delete 管理,它们可能会用到函数 brk 来调整大小。

此区域由进程中的所有共享库和动态加载模块共享。

9.4 查看系统资源

9.4.1 free

查看 内存使用情况

free [-b|-k|-m|-g|-h] [-t] [-s N -c N]

-b Bytes,指定显示内存的单位,还可用 -m,-k,-g,-h

-t 在最后多显示一行,内容为实体内存与 swap 的总量

-s 每隔几秒钟就输出一次,每次在之前的结果后面显示

-c 与 -s 配合使用,输出几次

查询结果: total 总量 used 已用 free 可用 shared / buffers / cached 已用内存中,用于缓冲和高速缓存的量 available 系统繁忙时可释放的量。

9.4.2 uname

查看 系统信息

uname [-asrmpi]

-a 所有系统信息

-s 核心名称

-r 核心版本

-m 硬件名称

-p CPU 的类型

-i 硬件平台

9.4.3 uptime

查看 系统启动时间与工作负载

uptime 用于查看系统运行时间,及最近 1, 5, 15分钟的平均负载。

其显示结果与 top 命令的第一行结果相同。

9.4.4 netstat

监控网络和套接字。

用于查看系统上所有的 套接字连接情况,包括 tcp, udp 以及 Linux 套接字,另外它还能查看处于监听状态的套接字。

netstat -[atunlp]

-a 查看所有当前的连接

-t 查看 TCP 协议的连接

-u 查看 UDP 协议的连接

-n 禁用反向域名解析,加快查询速度

-l 只查看监听中的连接

-p 查看网络服务的 PID

默认情况下 netstat 会通过反向域名解析技术查找每个 IP 地址对应的主机名。这会降低查找速度。如果不需要知道主机名,有 IP 地址就可以的话,用 -n 选项禁用 域名解析 功能。

~]# netstat
# 网络连接
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0     96 zion.matrix:ssh         192.168.1.5:4194        ESTABLISHED
# unix 域套接字
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags       Type       State         I-Node   Path
unix  2      [ ]         DGRAM                    8966     /run/systemd/shutdownd
unix  2      [ ]         DGRAM                    7380     /run/systemd/notify

在查询结果中,分两部分,分别是 网络连接unix 域套接字

网络连接部分

Proto 网络协议,主要为 TCP 与 UDP

Recv-Q 数据包接收队列

Send-Q 数据包发送队列

Local Address 本地套接字

Foreign Address 远端套接字

State 连接状态

Recv-QSend-Q

分别表示 网络接收队列,网络发送队列

这两个值通常应该为 0,如果不为 0 则可能存在问题。数据包在两个队列里都不应该有 堆积状态。但短暂的 Send-Q 队列 非 0 为正常状态。

Recv-Q: 表示程序总共还有多少字节的数据 没有从内核空间的套接字缓存拷贝到用户空间。即收到的数据已经在本地接收缓冲,但是还有多少没有被进程取走。

如果接收队列 Recv-Q 一直处于阻塞状态,可能是遭受了拒绝服务攻击。

Send-Q: 表示 本地缓冲区中存在的数据包,对方仍没有收到或没有 Ack

如果发送队列Send-Q不能很快的清零,可能是有应用向外发送数据包过快,或者是对方接收数据包不够快。

套接字进程部分

这部分显示的是 unix 域的套接字,和网络套接字一样,但是 只能用于本机通信

套接字文件 用于进程之间信息的沟通,即 进程间数据的传递

Proto 协议,一般为 unix

RefCnt 连接到套接字的进程数量

Flags 连接的标签

Type 存取的类型,有确认连接的 STREAM 与不需确认的 DGRAM 两种

State 若为 CONNECTED 表示进程间已创建连接

Path 连接到此套接字的程序的路径或数据输出的路径

范例

查看当前系统上正在监听的网络连接及其 PID
~]# netstat -tulnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
udp        0      0 0.0.0.0:57808              0.0.0.0:*               LISTEN      743/avahi-daemon: r

能找到 PID 就方便杀掉指定的进程

~]# kill -9 743
~]# killall -9 avahi-daemon

9.4.5 dmesg

分析 内核产生的信息

无论是在系统启动中,还是系统运行中,内核检测到的信息,都会被记录到 内存 中的某个 保护区段dmesg 命令能够读取该区段的信息。因为信息太多,所以执行时可以加入管道指令“ | more ”使画面暂停。

dmesg | more 查看系统启动时内核产生的信息

dmesg | grep -i vda 查看系统启动时硬盘的相关信息

9.4.6 vmstat

监测系统资源变化

用于显示 虚拟内存状态 ,也可用于查看 进程、内存、I/O 等系统整体运行状态。

可以设置时间间隔,监测系统资源的动态变化。

vmstat(选项)(参数)

-a 显示活动内页

-f 显示启动后创建的进程总数

-m 显示slab信息

-n 头信息仅显示一次

-s 以表格方式显示事件计数器和内存状态

-d 报告磁盘状态

-p 显示指定的硬盘分区状态

-S 输出信息的单位

vmstat 1 3 统计当前主机 CPU 状态,每秒一次,共三次

vmstat 5 持续动态监测,每 5 秒一次

监测字段说明

procs

进程

r 等待运行中的进程数量

b 不可被唤醒的进程数量

这两个项目越多,表示系统越忙碌

memory

内存

swpd 虚拟内存已用量

free 可用内存

buff 缓冲内存

cache 高速缓存

swap

内存交换空间

si 每秒从磁盘读入虚拟内存的大小。如果 si 大于 0,表示物理内存不够用或者内存泄露了,要查找耗内存的进程解决掉。

so 每秒虚拟内存写入磁盘的大小。如果 si/so 的数值太大,表示数据频繁在磁盘与内存之间传来传去,系统性能会很差。

I/O

磁盘读写

bi 块设备每秒接收的块数量

bo 块设备每秒发送的块数量。读取文件时,bo 就会大于 0。

bi 和 bo 一般都要接近 0,不然就是 I/O 过于频繁,需要调整。

system

系统

in 每秒 CPU 的中断次数,包括时间中断

cs 每秒上下文切换次数,越小越好,否则要考虑调低线程或者进程的数目

这两个数值越大,表示系统与周边设备的沟通非常频繁,包括磁盘、网卡、时钟等

CPU

us 用户 CPU 时间

sy 系统 CPU 时间,太高表示系统调用时间长,如 IO 操作频繁

id 闲置 CPU 时间

wa 等待 I/O 所耗费的 CPU 时间

st 被虚拟机盗用的 CPU 时间