本文对应《UNIX and Linux System Administration Handbook》第一章,用通俗语言解释每一个概念,帮助你建立系统管理员的整体认知框架。

本书定位:三合一手册

这本书同时承担三个角色:

+------------------------------------------+
|  1. 定向指南(Orientation Guide)         |
|     → 告诉你"整个地图长什么样"           |
+------------------------------------------+
|  2. 快速参考手册(Quick Reference)        |
|     → 告诉你"常用命令怎么用"             |
+------------------------------------------+
|  3. 企业级运维指南(Enterprise Focus)     |
|     → 告诉你"大规模生产环境怎么玩"       |
+------------------------------------------+

类比:就像一本"城市生活百科",既有地图(定向),也有餐厅推荐(快速参考),还有如何在这座城市开公司(企业级)。

第一节:系统管理员的核心职责

系统管理员(Sysadmin)不是一个人,而是一套"责任清单"。下面用通俗方式解释每一项:

系统管理员
职责

访问控制

创建用户账号

删除离职账号

重置密码

硬件管理

安装网卡

配置存储阵列

任务自动化

写脚本

配置自动化工具

备份管理

定期备份

定期测试恢复

软件管理

安装软件

打安全补丁

持续交付流程

监控

监控Web服务响应

分析日志

监控磁盘空间

故障排查

诊断问题

联系专家

文档维护

记录操作决策

画网络拓扑图

安全监控

实施安全策略

防止入侵

性能调优

分析瓶颈

优化配置

策略制定

合规政策

数据保留策略

供应商协作

选型

谈合同

救火

处理用户投诉

紧急故障响应

重点职责详解

1. 访问控制(Controlling Access)

就像公司门禁管理员:

  • 新员工入职 → 创建账号、分配权限
  • 员工离职 → 立即禁用账号(防止安全隐患)
  • 忘记密码 → 重置处理
    现代做法:通常通过配置管理系统集中目录服务自动完成,而非手动操作。
2. 任务自动化(Automating Tasks)

核心思想:能让机器做的,就不要让人做
好处三连:

  • 提高效率(不用每次手动操作)
  • 减少人为错误(脚本不会粗心)
  • 快速响应变化(触发条件满足就自动执行)
3. 监控(Monitoring)

一个真实的逻辑:

用户发现问题 → 去社交媒体吐槽 → 公司形象受损
          vs
管理员提前发现 → 悄悄修好 → 用户无感知

监控任务包括:

  • 确保网页服务响应速度正常
  • 收集和分析日志文件
  • 监控磁盘空间、内存等资源
4. 救火(Fire Fighting)

这是书中最有趣的一节。作者直接承认:帮用户解决各种奇怪问题虽然不在职责描述里,但实际占用了相当多时间。
经典例子:

  • “昨天还好好的,今天就不行了!你改了什么?”
  • “我把咖啡洒在键盘上了!应该用水冲洗吗?”
    作者的建议(非常实用):

一张处理好的工单,比五小时的午夜调试更能体现你的价值。

第二节:建议背景知识

编辑器选择


编辑器 特点 建议
vim 标准、强大、高效,所有系统都有 强烈推荐,学习曲线陡但值得
nano 简单,有屏幕提示 入门可用,但别在同行面前用

学习 vim 的入口:在终端输入 vimtutor,有交互式教程。

编程语言选择

管理员不是开发者,但需要会写脚本。推荐三种:

语言 定位 特点
Bash 命令行胶水 所有系统默认,适合简单自动化
Python 通用脚本 语法清晰,库丰富,社区大
Ruby 通用脚本 开发者称之为"美丽的语言"

还有一个特殊工具:expect —— 不是编程语言,而是用来驱动交互式程序的前端工具。比如自动化需要手动输入密码的 SSH 登录。

第三节:Linux 发行版

什么是发行版?

Linux 内核(Kernel)是操作系统的核心,但光有内核不能用。发行版 = 内核 + 各种配套软件包。
类比:内核是汽车发动机,发行版是整辆车(包括车身、轮胎、方向盘等)。

主要发行版谱系

Linux 内核

Debian 系

Red Hat 系

其他

Debian GNU/Linux

Ubuntu

Linux Mint

Kali

Red Hat Enterprise Linux
RHEL

CentOS

Fedora
测试床

Oracle Linux

Arch Linux

openSUSE

CoreOS
容器化

Alpine Linux
极简

选择发行版的四个问题

在选发行版之前,要像评估商业伙伴一样评估它:

  1. 五年后还在吗? — 避免选到被放弃的发行版
  2. 安全补丁及时吗? — 安全漏洞修复速度很重要
  3. 社区活跃、文档充足吗? — 遇到问题能找到答案
  4. 出问题能联系厂商吗?多少钱? — 商业支持的成本

本书使用的四个示例系统


系统 版本 特点
Debian GNU/Linux 9.0 “Stretch” 最自由的发行版,非商业,千名贡献者
Ubuntu 17.04 “Zesty Zapus” 基于Debian,Canonical公司维护
RHEL / CentOS 7.1 企业级首选,CentOS是免费版
FreeBSD 11.0 UNIX而非Linux,Netflix等大公司使用

Ubuntu 版本号规则

版本号格式:年份.月份
例如:16.10 = 2016年10月发布,代号 “Yakkety Yak”
LTS(长期支持版)规律:每两年的4月发布,承诺5年维护更新,生产环境推荐使用。

RHEL vs CentOS 关系

RHEL (付费,有红帽支持)
  |
  | 去掉品牌和专有工具,代码完全相同
  v
CentOS(免费,无官方支持,现已被红帽收购团队维护)

实用策略:

  • 前线生产服务器 → 用 RHEL(有支持保障)
  • 非生产/测试环境 → 用 CentOS(节省成本)

FreeBSD 的特殊性

Linux 只是内核,用户空间工具是第三方的。
FreeBSD 是完整操作系统(内核+用户空间都由同一团队维护),这让它更加一致。
许可证:BSD License(比 GPL 更宽松),允许商业公司修改后闭源使用,这也是为什么 macOS 基于 BSD。

第四节:排版约定

理解这些约定有助于正确阅读命令示例:

写法 含义 示例
粗体 字面命令或文件名 cp
斜体 需要替换的占位符 filename
代码字体 终端输出或配置文件内容 $ ls -la
$ 提示符 普通用户 $ whoami
# 提示符 root 用户 # passwd
debian# 特定发行版的 root debian# dpkg -l

命令语法约定:

符号 含义 例子
[选项] 方括号内为可选 [ -x ]
... 省略号表示可重复 filename ...
{a|b} 花括号选一个 {on|off}
* 匹配零或多个字符 /etc/rc*.d
? 匹配一个字符 file?.txt
~ 当前用户主目录 ~/Documents
~user 指定用户的主目录 ~bob/

第五节:单位换算

这一节看似简单,实则是IT行业长期存在的混乱。

问题的根源

数学定义: 1  kilo = 10 3 = 1000 1 \text{ kilo} = 10^3 = 1000 1 kilo=103=1000
计算机惯例: 1  "kilo" = 2 10 = 1024 1 \text{ "kilo"} = 2^{10} = 1024 1 "kilo"=210=1024
这导致同一个词有两种含义!

IEC 标准解决方案

国际电工委员会(IEC)定义了新前缀,专门表示2的幂次:
1  KiB = 2 10 = 1024  B 1 \text{ KiB} = 2^{10} = 1024 \text{ B} 1 KiB=210=1024 B
1  MiB = 2 20 = 1,048,576  B 1 \text{ MiB} = 2^{20} = 1{,}048{,}576 \text{ B} 1 MiB=220=1,048,576 B
1  GiB = 2 30 = 1,073,741,824  B 1 \text{ GiB} = 2^{30} = 1{,}073{,}741{,}824 \text{ B} 1 GiB=230=1,073,741,824 B

记忆规律


前缀 公制(10的幂) IEC(2的幂) 缩写
kilo- 10 3 = 1000 10^3 = 1000 103=1000 2 10 = 1024 2^{10} = 1024 210=1024 KB vs KiB
mega- 10 6 = 1,000,000 10^6 = 1{,}000{,}000 106=1,000,000 2 20 = 1,048,576 2^{20} = 1{,}048{,}576 220=1,048,576 MB vs MiB
giga- 10 9 10^9 109 2 30 2^{30} 230 GB vs GiB
tera- 10 12 10^{12} 1012 2 40 2^{40} 240 TB vs TiB

上下文判断法(实用)

  • 内存(RAM):永远是2的幂(8 GiB = 8 × 2 30 8 \times 2^{30} 8×230 字节)
  • 网络带宽:永远是10的幂(1 Gb/s = 10 9 10^9 109 bit/s)
  • 存储容量:通常标注的是10的幂(1 TB硬盘实际只有约 0.91 0.91 0.91 TiB)
    硬盘实际可用字节数计算示例(100 MB 分区):
    实际字节 = ⌊ 10 8 512 ⌋ × 512 = 99,999,744  字节 \text{实际字节} = \left\lfloor \frac{10^8}{512} \right\rfloor \times 512 = 99{,}999{,}744 \text{ 字节} 实际字节=512108×512=99,999,744 字节
    其中 512 是磁盘块大小,需向下取整到整块。

本书用法

  • 2的幂 → 用 IEC 单位(KiB, MiB, GiB)
  • 10的幂 → 用公制单位(KB, MB, GB)
  • bit 缩写:b(小写)
  • byte 缩写:B(大写)

第六节:man 手册页

什么是 man 页?

man 页是 UNIX 传统的"本地文档系统",每个命令、系统调用、配置文件都有对应的说明页。

man 页的章节划分


章节 内容
1 用户命令(如 ls, grep
2 系统调用(如 open(), read()
3 库函数(如 printf(), malloc()
4 设备驱动和网络协议
5 标准文件格式(如 /etc/passwd
6 游戏和演示
7 杂项文件和文档
8 系统管理命令(如 mount, fdisk
9 内核接口(内核开发者用)

常用 man 命令

# 查看 ls 命令的手册
man ls
# 查看第2节的 sync 系统调用(而不是第1节的 sync 命令)
man 2 sync
# 关键词搜索(找所有和 translate 相关的手册)
man -k translate
# 等价于:
apropos translate

man 页存储位置

/usr/share/man/         ← man 页源码(nroff 格式,gzip 压缩)
/var/cache/man/         ← 格式化后的缓存(有安全风险,多数系统禁用)

查看当前搜索路径:

ubuntu$ manpath
/usr/local/man:/usr/local/share/man:/usr/share/man

自定义搜索路径:

export MANPATH=/home/share/localman:/usr/share/man

第七节:查找和安装软件

判断软件是否已安装

方法一:which(查找可执行文件是否在 PATH 中)

ubuntu$ which gcc
/usr/bin/gcc
# 有输出 = 已安装
# 无输出 = 未安装或不在 PATH 中

方法二:whereis(搜索范围更广,不依赖 PATH)

whereis python3

方法三:locate(通过预建索引快速查找任意文件)

freebsd$ locate signal.h
/usr/include/machine/signal.h
/usr/include/signal.h
/usr/include/sys/signal.h

注意:locate 的数据库由 updatedb 定期更新,不反映最新文件变化。
方法四:包管理工具查询

# Red Hat / CentOS:查询 Python 是否已安装
redhat$ rpm -q python
python-2.7.5-18.el7_1.1.x86_64
# 查询某个文件属于哪个包
redhat$ rpm -qf /etc/httpd
httpd-2.4.6-31.el7.centos.x86_64
freebsd$ pkg which /usr/local/sbin/httpd
/usr/local/sbin/httpd was installed by package apache24-2.4.12
ubuntu$ dpkg-query -S /etc/apache2
apache2: /etc/apache2

各系统安装软件命令

以安装 tcpdump(网络抓包工具)为例:

系统 包管理器 安装命令
Ubuntu / Debian APT sudo apt-get install tcpdump
RHEL / CentOS YUM sudo yum install tcpdump
FreeBSD pkg sudo pkg install -y tcpdump

从源码编译安装(经典三步曲)

当包管理器没有你要的版本时,需要手动编译:

# 第一步:下载源码
cd /tmp
git clone https://github.com/the-tcpdump-group/tcpdump.git
cd tcpdump
git checkout tags/tcpdump-4.7.4 -b tcpdump-4.7.4
# 第二步:配置(检测当前环境,生成 Makefile)
./configure
# 可选:指定安装目录
# ./configure --prefix=/opt/tcpdump
# 第三步:编译和安装
make                  # 编译(生成可执行文件)
sudo make install     # 安装到系统目录

这个 configure → make → make install 三步流程适用于绝大多数 C 语言项目。

从 Web 脚本安装(注意安全!)

现代软件提供了更便捷的安装方式:

# 方式一:先下载,再运行(推荐)
curl -o /tmp/saltboot -sL https://bootstrap.saltstack.com
sudo sh /tmp/saltboot
# 方式二:管道直接运行(不推荐)
curl -L https://example.com/install.sh | sudo sh

为什么不推荐管道方式?

curl 下载部分脚本 → 网络中断 → curl 失败
                                    |
                                    v
                           但 sudo sh 已经在运行!
                           执行了半截脚本 → 结果不可预知

安全警告:永远用 HTTPS,不要用 HTTP!
HTTP 安装的风险:

你的电脑 ──HTTP──> 中间人攻击者 ──> 恶意脚本 ──> 以 root 运行
                   (把合法脚本换成恶意脚本)

HTTPS 通过加密和证书链验证,防止上述攻击。

第八节:托管选择

三种托管方式对比

服务器托管方式

自建数据中心
Private Data Center

租用机柜
Co-location

公有云
Public Cloud

完全控制
高资本投入
运维复杂

硬件自有
网络租用
适合特殊需求

按需付费
弹性扩展
无需管硬件

公有云的优势

  • 无需资本支出,启动成本低
  • 不用安装、保护和管理硬件
  • 按需调整存储、带宽、计算能力
  • 数据库、负载均衡、消息队列等开箱即用
  • 高可用/冗余系统更容易实现

主流云平台简评


云平台 特点 适合场景
AWS 市场最大,服务最全,创新最快 通用首选
Google Cloud 技术先进,定价友好 数据密集型应用
DigitalOcean 简单、高性能、面向开发者 中小型项目

AWS 市场规模:Gartner 数据显示,AWS 规模是所有竞争对手总和的10倍

第九节:相关岗位生态

系统管理员不是孤立存在的,周围有一系列相关角色:

系统管理员
Sysadmin

DevOps 工程师
打通开发与运维

SRE
站点可靠性工程师
保障系统可用性

安全运维工程师
防攻击、找漏洞

网络管理员
管理交换机路由器

数据库管理员 DBA
管理数据库系统

NOC 工程师
实时监控大屏前的人

数据中心技术员
管理物理硬件

架构师
设计整体系统结构

各岗位一句话描述

DevOps:文化理念,打破开发和运维的壁垒,追求快速、可靠地交付软件。
SRE(Site Reliability Engineer):把软件工程方法用于运维问题,最怕"单点故障",随时待命处理故障。
安全运维工程师:系统管理员中的"安全专家",日常扫描漏洞,模拟攻击测试防御效果。
网络管理员:管物理交换机、路由器、防火墙的人,数据中心里最常见。
DBA:数据库的主人,精通 SQL,懂集群,能把慢查询优化成飞。
NOC 工程师:坐在大屏监控室里的人,监控系统健康,协调多团队处理故障。
数据中心技术员:管理服务器机架、走线、电力、空调的人。作者建议:用咖啡和饮料贿赂他们,关键时刻他们能救你。
架构师:经验最丰富的人,设计整个分布式系统,考虑安全分区、单点故障、未来扩展。

附:完整可运行的 C++ 演示——软件查找逻辑模拟

下面用 C++ 模拟 which 命令的核心逻辑,演示如何在 PATH 中查找可执行文件:

// 文件名: which_demo.cpp
// 模拟 which 命令:在 PATH 环境变量指定的目录中查找可执行文件
// 编译: g++ -std=c++17 -o which_demo which_demo.cpp
// 运行: ./which_demo gcc python3 nonexistent
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <fstream>
#include <sys/stat.h>   // stat() 检查文件是否存在
#include <unistd.h>     // access() 检查执行权限
#include <cstdlib>      // getenv()
// 将 PATH 字符串按 ':' 分割成目录列表
// 例如 "/usr/bin:/usr/local/bin:/bin" → {"usr/bin", "/usr/local/bin", "/bin"}
std::vector<std::string> splitPath(const std::string& pathEnv) {
    std::vector<std::string> dirs;
    std::stringstream ss(pathEnv);
    std::string dir;
    while (std::getline(ss, dir, ':')) {
        if (!dir.empty()) {
            dirs.push_back(dir);
        }
    }
    return dirs;
}
// 检查指定路径的文件是否存在且可执行
// 返回 true 表示可执行
bool isExecutable(const std::string& filepath) {
    // access() 检查调用进程对文件的权限
    // X_OK = 可执行权限
    return access(filepath.c_str(), X_OK) == 0;
}
// 在 PATH 中查找命令,返回完整路径或空字符串
std::string findInPath(const std::string& command,
                       const std::vector<std::string>& pathDirs) {
    // 如果命令本身包含 '/',直接检查该路径(绝对路径或相对路径)
    if (command.find('/') != std::string::npos) {
        if (isExecutable(command)) {
            return command;
        }
        return "";
    }
    // 在每个 PATH 目录中查找
    for (const auto& dir : pathDirs) {
        std::string fullPath = dir + "/" + command;
        if (isExecutable(fullPath)) {
            return fullPath;  // 找到第一个就返回
        }
    }
    return "";  // 没找到
}
int main(int argc, char* argv[]) {
    // 获取 PATH 环境变量
    const char* pathEnv = getenv("PATH");
    if (pathEnv == nullptr) {
        std::cerr << "错误:PATH 环境变量未设置\n";
        return 1;
    }
    std::string pathStr(pathEnv);
    std::cout << "当前 PATH:\n";
    // 解析并显示 PATH 中的目录
    std::vector<std::string> pathDirs = splitPath(pathStr);
    for (size_t i = 0; i < pathDirs.size(); i++) {
        std::cout << "  [" << i << "] " << pathDirs[i] << "\n";
    }
    std::cout << "\n";
    // 如果没有提供命令行参数,使用默认的测试命令列表
    std::vector<std::string> commands;
    if (argc > 1) {
        for (int i = 1; i < argc; i++) {
            commands.push_back(argv[i]);
        }
    } else {
        // 默认测试几个常见命令
        commands = {"ls", "gcc", "python3", "vim", "this_does_not_exist"};
    }
    // 逐个查找
    std::cout << "查找结果:\n";
    std::cout << std::string(40, '-') << "\n";
    int notFound = 0;
    for (const auto& cmd : commands) {
        std::string result = findInPath(cmd, pathDirs);
        if (result.empty()) {
            std::cout << cmd << ": 未找到\n";
            notFound++;
        } else {
            std::cout << cmd << " -> " << result << "\n";
        }
    }
    std::cout << std::string(40, '-') << "\n";
    std::cout << "共查找 " << commands.size() << " 个命令,"
              << notFound << " 个未找到\n";
    return 0;
}

运行示例

当前 PATH:
  [0] /usr/local/sbin
  [1] /usr/local/bin
  [2] /usr/sbin
  [3] /usr/bin
  [4] /sbin
  [5] /bin
查找结果:
----------------------------------------
ls -> /bin/ls
gcc -> /usr/bin/gcc
python3 -> /usr/bin/python3
vim -> /usr/bin/vim
this_does_not_exist: 未找到
----------------------------------------
共查找 5 个命令,1 个未找到

总结:第一章的核心认知

系统管理 = 技术 + 沟通 + 文档 + 安全意识 + 自动化思维

用一句话概括本章:

系统管理员是企业 IT 系统的"全科医生"——既要能处理急诊(救火),又要做好预防保健(监控、备份),还要管理整个"医疗档案"(文档),同时要防止外来"病原体"(安全),并且还要学会让很多事情"自动化"(工具和脚本)。


关键词 对应概念
Sysadmin 系统管理员,职责多样
Distro 发行版 = 内核 + 软件包
LTS 长期支持版,生产环境首选
Package Manager 包管理器,软件安装的核心工具
man pages 命令手册,随时可查
HTTPS 安全下载的基本要求
IEC units KiB/MiB/GiB,2的幂的正确表达
DevOps 打通开发和运维的文化
SRE 用软件工程方法做运维

UNIX/Linux 系统管理:第二章深度解析——启动与系统管理守护进程

本文对应《UNIX and Linux System Administration Handbook》第二章,用通俗语言解释每一个概念,帮助你彻底理解计算机从按下电源键到可以登录使用的全过程。

核心比喻:计算机启动就像医院开门

凌晨 4:00  保安开门(固件/BIOS)
凌晨 5:00  后勤人员到岗,准备设施(引导加载程序)
早上 7:00  各科室主任到岗,安排工作(内核初始化)
早上 8:00  医院正式开门,所有部门就绪(用户空间服务启动完毕)

“Booting”(启动)一词来自 “bootstrapping”(自举),意思是计算机必须"靠自己把自己拉起来"——就像一个人抓住自己的鞋带把自己提起来,这听起来很荒谬,但计算机每次开机都在做这件事。

第一节:启动流程总览

完整启动链

按下电源键

从 NVRAM 加载固件
BIOS 或 UEFI

探测硬件设备
CPU/内存/磁盘/网卡

选择启动设备
硬盘/DVD/USB/网络

加载引导加载程序
如 GRUB

确定要启动哪个内核

将内核加载到内存

内核初始化
建立数据结构

启动 init 或 systemd
作为 PID 1

执行启动脚本
挂载文件系统 启动服务

系统就绪
等待用户登录

管理员能干预哪些步骤?

大部分步骤是自动的,管理员的主要干预点只有两个:

[修改引导加载程序配置] → 决定加载哪个内核、传什么参数
[修改启动脚本/unit文件] → 决定启动哪些服务、以什么顺序

第二节:系统固件(BIOS / UEFI)

固件是什么?

固件(Firmware)是永久存储在主板 ROM 芯片中的程序。计算机一通电,CPU 就会自动跳转到固件代码开始执行——没有固件,计算机连从哪里找操作系统都不知道。
固件能做什么:

  • 识别主板上的所有硬件(SATA控制器、网卡、USB控制器、温度传感器)
  • 允许你配置或禁用这些硬件设备
  • 执行简单的硬件健康检查(POST,开机自检)
  • 找到下一阶段的引导代码

进入固件界面

开机瞬间按某个键(各厂商不同):

常见按键:Delete / F2 / F10 / F11 / F12 / Ctrl
技巧:开机后立即连续多次按下,再按住不放

BIOS vs UEFI:老爷车 vs 新能源汽车

传统 BIOS(旧标准)

关键概念:MBR(主引导记录)
磁盘最开头的 512 字节,结构如下:

磁盘起始位置(字节):
+--------+----------------+----------------+
| 0~445  |   446~509      |   510~511      |
| 引导代码| 分区表(64字节)| 魔数 0x55AA   |
| 446字节 | 最多4个主分区  | 标识这是可引导盘|
+--------+----------------+----------------+

BIOS 启动链:

BIOS → MBR中的第一阶段引导程序(446字节,极小)
     → 第二阶段引导程序(在分区开头或MBR后的空白区)
     → 操作系统内核

为什么需要多个阶段?因为 446 字节放不下能读文件系统的代码,只够放"找到第二阶段然后跳过去"的代码。

UEFI(新标准)

UEFI = Unified Extensible Firmware Interface(统一可扩展固件接口)
关键改进:

特性 传统 BIOS UEFI
分区格式 MBR(最大 2TB,最多4个主分区) GPT(几乎无限大,128个分区)
文件系统 不懂文件系统 理解 FAT32 文件系统
引导方式 靠神秘的引导扇区 直接读取 EFI 系统分区中的 .efi 文件
安全性 支持安全启动(Secure Boot)
接口 粗糙、难用 现代图形界面,甚至支持鼠标

EFI 系统分区(ESP) 是 UEFI 引导的关键:

  • 格式:FAT32(任何操作系统都能读写)
  • 内容:各操作系统的引导文件(.efi 可执行文件)
  • 路径示例:/efi/ubuntu/grubx64.efi(Ubuntu 的引导文件)
    UEFI 启动路径:
开机
  |
  v
UEFI 固件读取 GPT 分区表
  |
  v
找到 EFI 系统分区(ESP,FAT32格式)
  |
  v
读取并执行 /efi/boot/bootx64.efi(或配置的路径)
  |
  v
引导加载程序(如 GRUB) 或 直接加载内核

用 efibootmgr 查看和修改 UEFI 配置

# 查看当前 UEFI 引导配置
$ efibootmgr -v
BootCurrent: 0004          # 当前从哪个条目启动的
BootOrder: 0000,0001,0002,0004,0003  # 尝试顺序
Boot0000* EFI DVD/CDROM    # 第一优先级:DVD
Boot0001* EFI Hard Drive   # 第二优先级:硬盘
Boot0002* EFI Network      # 第三优先级:网络启动
Boot0004* ubuntu           # 第五优先级:Ubuntu
# 修改启动顺序:先试硬盘(0004),再试网络(0002)
$ sudo efibootmgr -o 0004,0002

安全警告:UEFI 配置通过 /sys 可读写,执行 rm -rf / 会把 UEFI 变量也删掉,导致系统在固件层面永久损坏!

第三节:引导加载程序(Boot Loader)

引导加载程序的职责

职责1:找到并加载合适的操作系统内核
职责2:提供启动菜单(选择多个内核或操作系统)
职责3:把配置参数传递给内核

类比:引导加载程序就像机场的值机台——它不飞行,但它决定哪架飞机(内核)起飞,并给乘客(内核参数)分配座位。

第四节:GRUB 引导加载程序

GRUB 是什么?

GRUB = GRand Unified Bootloader(大一统引导加载程序),GNU 项目开发,几乎所有 Linux 发行版的默认引导加载程序。
GRUB 有两个版本:

  • GRUB Legacy(老版本):基本已淘汰
  • GRUB 2(现行标准):本书讨论的版本,简称 GRUB

GRUB 的配置文件

GRUB 能理解常见文件系统,所以配置文件可以存放在普通文本文件中:

文件路径 用途
/boot/grub/grub.cfg GRUB 主配置文件(自动生成,勿手动修改)
/etc/default/grub 用户配置(修改这里,然后重新生成 grub.cfg)
/etc/grub.d/40_custom 自定义菜单项(手动添加条目)

生成 grub.cfg 的命令:

# Ubuntu / Debian
sudo update-grub
# Red Hat / CentOS
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

/etc/default/grub 常用配置

# /etc/default/grub 示例(含中文注释)
# 默认启动菜单第几项(从0开始),或菜单标题
GRUB_DEFAULT=0
# 显示启动菜单多少秒后自动启动默认项
# 设为 -1 表示一直等待用户选择
GRUB_TIMEOUT=5
# 传递给内核的额外参数(每次启动都会用)
GRUB_CMDLINE_LINUX="quiet splash"
# 背景图片(支持 png/jpg/tga)
GRUB_BACKGROUND="/boot/grub/background.png"
# 不生成"恢复模式"菜单项(生产服务器建议开启)
GRUB_DISABLE_RECOVERY="true"

修改后执行 update-grub 使配置生效。

自定义引导菜单条目(40_custom)

#!/bin/sh
exec tail -n +3 $0
# 在此行下方添加自定义菜单项
menuentry '我的定制内核' {
    # 指定根设备(第一块硬盘的第一个分区)
    set root='(hd0,msdos1)'
    # 加载内核,root= 指定根文件系统,ro=只读挂载
    linux /awesome_kernel root=UUID=XXX-XXX-XXX ro quiet
    # 加载 initrd(初始内存盘,内核启动时需要)
    initrd /initrd.img-awesome_kernel
}

GRUB 命令行(紧急救援用)

在 GRUB 启动界面按 c 进入命令行模式:

命令 功能
boot 从指定内核镜像启动
linux 加载 Linux 内核
reboot 重启系统
search 按文件、文件系统标签或 UUID 搜索设备
help 获取帮助

内核启动参数

传给内核的参数,就像给程序传命令行参数一样:

参数 含义
single-s 进入单用户模式(用于紧急修复)
debug 开启内核调试信息
init=/bin/bash 跳过 init,直接启动 bash(极端紧急情况)
root=/dev/sda1 指定根文件系统设备
这些参数临时有效(只影响本次启动)。要永久生效,在 /etc/default/grubGRUB_CMDLINE_LINUX 变量中添加。
内核升级不替换旧版本:新旧内核并存,启动菜单会列出所有版本,方便回滚。

第五节:FreeBSD 的启动流程

FreeBSD 的最终引导程序叫 loader,使用 Forth 语言编写(这是一个有趣的历史事实)。

BIOS 路径(boot0)

MBR (boot0) → 卷引导记录 → loader → 内核

UEFI 路径

UEFI固件 → /boot/bootx64.efi → loader.efi → 内核

查看 FreeBSD 分区表:

$ gpart show
=>  40  134217648  ada0  GPT  (64G)
    40       1600     1  efi         (800K)   ← EFI 系统分区
  1640  127924664     2  freebsd-ufs (61G)    ← 根文件系统
127926304   6291383  3  freebsd-swap (3.0G)  ← 交换分区

FreeBSD loader 配置


文件 用途
/boot/defaults/loader.conf 默认配置,不要修改
/boot/loader.conf 用户自定义,在此覆盖默认值

loader 交互示例(切换到旧内核):

OK ls              # 列出当前目录文件
OK unload          # 卸载已加载的内核
OK load /boot/kernel/kernel.old   # 加载旧内核
OK boot            # 继续启动

第六节:系统管理守护进程(init)

什么是 PID 1?

内核启动完成后,它会创建第一个用户空间进程,PID(进程ID)固定为 1,这就是 init(或 systemd)。
类比:init 就是"公司CEO"——公司开业后,所有员工(进程)都是由CEO直接或间接雇用的。

内核(老板的老板)
  |
  v
init/systemd(PID=1,CEO)
  |
  +→ 网络服务进程
  +→ Web 服务进程
  +→ 数据库进程
  +→ 登录管理进程
  +→ ... 所有其他用户空间进程

init 的工作模式

init 维护系统的"运行状态",不同状态对应不同的服务组合:

模式 描述
单用户模式(Single-user) 最小化,只挂载根文件系统,无网络,只有 root shell
多用户模式(Multiuser) 完整,挂载所有文件系统,启动所有服务
服务器模式(Server) 类似多用户但无图形界面

init 在切换到多用户模式时会完成的任务:

设置主机名 → 设置时区 → 检查磁盘(fsck) → 挂载文件系统
→ 清理 /tmp → 配置网络接口 → 配置防火墙 → 启动各种守护进程

init 的三大流派

系统管理守护进程 init

SysV init
AT&T System V 风格
传统Linux用

BSD init
BSD UNIX 风格
FreeBSD用

systemd
现代Linux标准
全面接管

其他
如 macOS 的 launchd
Ubuntu 曾用的 Upstart

传统 init 的缺点

  1. 没有依赖管理:必须手动维护启动顺序(用脚本编号 S01xxx, S02xxx…)
  2. 串行执行:后面的脚本必须等前面的脚本完全结束才能运行,启动慢
  3. 三层脚本嵌套:难以理解和维护
  4. 配置分散:改个启动服务要改多个地方

第七节:systemd 详解

systemd 的核心理念

systemd 不是单个程序,而是一套工具集,完整构建产生 69 个二进制文件
类比:传统 init 是一把螺丝刀,systemd 是一套完整的工具箱(你必须全套接受)。
systemd 管理:

服务进程 + 网络(networkd) + 日志(journald) + 登录(logind) + 定时任务 + 设备 + 挂载点 + ...

Unit(单元)和 Unit 文件

systemd 管理的所有实体统称为 Unit(单元),包括:服务、套接字、设备、挂载点、定时器等。
每个 Unit 的行为由一个 Unit 文件定义。
Unit 文件存放位置(优先级从高到低):

/etc/systemd/system/        ← 管理员本地配置(最高优先级)
/run/systemd/system/        ← 运行时临时单元
/usr/lib/systemd/system/    ← 软件包安装时放置(不要修改)

一个简单的 Unit 文件示例

# /usr/lib/systemd/system/rsync.service
# rsync 守护进程的 unit 文件
[Unit]
# 服务描述(显示在 systemctl status 中)
Description=fast remote file copy program daemon
# 条件:只有当 /etc/rsyncd.conf 存在时才启动
ConditionPathExists=/etc/rsyncd.conf
[Service]
# 启动命令:--daemon 表示以守护进程方式运行
# --no-detach 表示不脱离 systemd 的控制
ExecStart=/usr/bin/rsync --daemon --no-detach
[Install]
# 当系统进入 multi-user.target 时,把我加入依赖
WantedBy=multi-user.target

NGINX 的完整 Unit 文件(含详细注释)

# /usr/lib/systemd/system/nginx.service
# NGINX Web 服务器的 unit 文件
[Unit]
Description=The nginx HTTP and reverse proxy server
# 我需要在这三个 target 之后才能启动:
# network.target    = 网络已就绪
# remote-fs.target  = 远程文件系统已挂载
# nss-lookup.target = DNS 解析已就绪
After=network.target remote-fs.target nss-lookup.target
[Service]
# forking 类型:启动命令会启动一个子进程然后退出
# systemd 通过 PIDFile 跟踪真正的守护进程
Type=forking
PIDFile=/run/nginx.pid
# 启动前先执行的命令(按顺序执行):
# 1. 删除可能残留的旧 PID 文件
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
# 2. 测试 NGINX 配置文件语法是否正确
ExecStartPre=/usr/sbin/nginx -t
# 真正的启动命令
ExecStart=/usr/sbin/nginx
# 重载配置命令(不重启,只重读配置)
# $MAINPID 由 systemd 自动设置为守护进程的 PID
ExecReload=/bin/kill -s HUP $MAINPID
# 停止方式:给主进程发 SIGQUIT 信号
KillMode=process
KillSignal=SIGQUIT
# 如果 5 秒内没有停止,systemd 强制 kill
TimeoutStopSec=5
# 安全特性:给这个服务一个私有的 /tmp 目录
# 防止和其他进程共享 /tmp 带来的安全问题
PrivateTmp=true
[Install]
WantedBy=multi-user.target

systemctl 常用命令

# 查看所有运行中的服务
systemctl list-units --type=service
# 查看所有已安装的服务(包括未运行的)
systemctl list-unit-files --type=service
# 查看某个服务的详细状态和最近日志
sudo systemctl status -l nginx
# 立即启动服务
sudo systemctl start nginx
# 立即停止服务
sudo systemctl stop nginx
# 重启服务
sudo systemctl restart nginx
# 重新加载配置(不停止服务)
sudo systemctl reload nginx
# 设置开机自启
sudo systemctl enable nginx
# 取消开机自启
sudo systemctl disable nginx
# 彻底屏蔽服务(连手动启动都不行)
sudo systemctl mask nginx
# 解除屏蔽
sudo systemctl unmask nginx
# 重新加载所有 unit 文件(修改配置后必须执行)
sudo systemctl daemon-reload

Unit 状态详解


状态 含义 类比
enabled 已安装,会开机自启 员工在职且排班
disabled 已安装,不会开机自启(可手动启动) 员工在职但未排班
masked 被管理员强制禁止,连手动启动都不行 员工被停职
static 没有安装程序,只能被其他服务依赖或手动启动 临时工
linked 通过符号链接关联 外包员工
bad unit 文件有问题 档案有误

运行级别 vs systemd Targets

传统 init 用"运行级别"(0~6数字),systemd 用"target":

传统运行级别 systemd Target 含义
0 poweroff.target 关机
1 / s / single rescue.target 单用户救援模式
2, 3, 4 multi-user.target 多用户命令行模式
5 graphical.target 多用户图形界面模式
6 reboot.target 重启
emergency emergency.target 紧急模式(比单用户更精简)

# 查看当前默认 target
systemctl get-default
# 修改默认 target(服务器不需要图形界面)
sudo systemctl set-default multi-user.target
# 立即切换到某个 target(类似老的 telinit N)
sudo systemctl isolate multi-user.target

依赖关系管理

systemd 中依赖和启动顺序是分开的两个概念!

依赖关系:决定"要不要启动"
执行顺序:决定"谁先谁后启动"

依赖关键字:

关键字 含义 严格程度
Wants 软依赖:尽量启动,失败也没关系 最宽松
Requires 硬依赖:依赖失败则本服务也失败 严格
Requisite 更严格:依赖必须已经在运行 更严格
BindsTo 强绑定:依赖停止则本服务也停止 最严格
Conflicts 冲突:不能同时运行 负依赖

顺序关键字(在 [Unit] 部分):

# 我在 network.target 之后启动
After=network.target
# 我在 shutdown.target 之前停止
Before=shutdown.target

重要Requires=B 不代表 B 在 A 之前启动!必须同时加 After=B 才能保证顺序。

修改第三方 Unit 文件的正确方式

错误做法:直接编辑 /usr/lib/systemd/system/nginx.service(软件更新会覆盖你的修改)
正确做法:创建覆盖文件(override)

# 方法一:手动创建(以修改 NGINX 启动参数为例)
sudo mkdir /etc/systemd/system/nginx.service.d
sudo cat > /etc/systemd/system/nginx.service.d/override.conf << 'EOF'
[Service]
# 先清空原来的 ExecStart(赋空值)
ExecStart=
# 再设置新的 ExecStart(使用自定义配置文件路径)
ExecStart=/usr/sbin/nginx -c /usr/local/www/nginx.conf
EOF
sudo systemctl daemon-reload    # 重新加载配置
sudo systemctl restart nginx    # 重启服务
# 方法二:更简单(systemctl 自动打开编辑器)
sudo systemctl edit nginx.service
sudo systemctl restart nginx

systemd 日志系统(journal)

传统日志系统在云环境中有问题:虚拟机没有物理控制台,早期启动日志往往丢失。
systemd 的 journald 从开机到关机全程记录所有日志:

# 查看所有日志(从最早开始)
journalctl
# 查看某个服务的日志
journalctl -u nginx
# 查看上一次启动的日志(需要先配置持久化)
journalctl -b -1
# 实时跟踪日志(类似 tail -f)
journalctl -f
# 查看内核日志
journalctl -k

配置日志持久化(默认日志存在内存中,重启丢失):

# 编辑 /etc/systemd/journald.conf
[Journal]
Storage=persistent    # 改为持久化存储到磁盘

查看历史启动记录:

$ journalctl --list-boots
-1  a73415fa...  Fri 2016-02-26 15:01:25 UTC  # 上一次启动
 0  0c563fa3...  Fri 2016-02-26 15:11:03 UTC  # 本次启动

第八节:FreeBSD 的 init 和启动脚本

FreeBSD 用 BSD 风格的 init,比 systemd 简单得多。

启动流程

内核启动
  |
  v
/etc/rc(主启动脚本,不要修改)
  |
  +→ 读取 /etc/defaults/rc.conf (默认值,不要改)
  +→ 读取 /etc/rc.conf          (用户配置,改这里)
  +→ 读取 /etc/rc.conf.local    (可选的本地覆盖)
  |
  v
按依赖顺序运行 /etc/rc.d/ 和 /usr/local/etc/rc.d/ 中的启动脚本

FreeBSD 启动脚本示例

#!/bin/sh
# /etc/rc.d/sshd 启动脚本(简化版)
# PROVIDE 声明:我提供的服务名称
# PROVIDE: sshd
# REQUIRE 声明:我依赖哪些服务(必须在我之前启动)
# REQUIRE: LOGIN FILESYSTEMS
# KEYWORD 声明:关机时也需要执行我
# KEYWORD: shutdown
# 加载公共函数库
. /etc/rc.subr
name="sshd"                      # 服务名
rcvar="sshd_enable"              # 控制变量名(在 rc.conf 中设置)
command="/usr/sbin/${name}"      # 守护进程路径

启用 sshd 服务:在 /etc/rc.conf 中添加:

sshd_enable="YES"    # YES=开机启动,NO=不启动

实时操作服务:

# 停止 sshd
sudo service sshd stop
# 启动 sshd
sudo service sshd start
# 重启 sshd
sudo service sshd restart
# 如果服务未在 rc.conf 中 enable,用 one 前缀强制执行
sudo service sshd onestart

第九节:关机和重启

关机命令

# 关机(halt)
sudo halt           # 停止系统
sudo halt -p        # 停止系统并断电
# 重启
sudo reboot
# 定时关机(旧式命令,主要用于多用户系统)
sudo shutdown -h now          # 立即关机
sudo shutdown -r +5 "系统将在5分钟后重启"  # 5分钟后重启并广播通知

云主机的关机注意事项

云环境关机有特殊风险,以 AWS 为例:

操作 含义 危险性
Stop 停止实例,数据保留 安全
Reboot 重启实例 安全
Terminate 销毁实例,默认删除根磁盘 高危!数据可能丢失

建议:启用"终止保护"(Termination Protection)防止误操作。

第十节:系统无法启动时的救援策略

三种策略(按推荐顺序)

系统无法启动

有可用的
系统镜像备份?

策略1: 直接恢复到
已知好的状态
云环境首选

能进入
单用户模式?

策略2: 启动到shell
交互式调试

策略3: 用独立启动介质
挂载故障磁盘后检查

单用户模式(Single-User Mode)

相当于"安全模式":

  • 只挂载根文件系统
  • 不启动网络
  • 只有 root 账户的 shell
  • 可以修复文件系统、重置密码、修改配置
    进入方式:在 GRUB 界面按 e 编辑内核参数,在内核行末尾添加:
# 进入 rescue 模式(单用户,相当于传统 runlevel 1)
systemd.unit=rescue.target
# 进入 emergency 模式(更精简,不挂载本地文件系统)
systemd.unit=emergency.target

进入后根文件系统通常是只读的,需要先重新挂载为读写:

# Linux:重新挂载根文件系统为读写
# mount -o rw,remount /
# FreeBSD:
# mount -o rw /dev/gpt/rootfs /

FreeBSD 在启动菜单直接提供选项:

1. Boot Multi User [Enter]
2. Boot Single User         ← 选这个
3. Escape to loader prompt
4. Reboot

云主机的救援流程(以 AWS 为例)

云主机没有物理控制台,单用户模式通常不可用。解决方案:磁盘迁移调试法

1. 在相同可用区启动一个新实例(救援机)
      |
      v
2. 停止问题实例(注意:是Stop,不是Terminate!)
      |
      v
3. 从问题实例卸载 EBS 卷,挂载到救援机
      |
      v
4. 登录救援机,挂载该卷,进行修复
      |
      v
5. 卸载该卷,重新挂载回问题实例,启动

各云平台的调试工具


云平台 调试方式
AWS 磁盘迁移法(EBS 卷可跨实例挂载)
DigitalOcean Web VNC 控制台 + 恢复内核启动
Google Cloud 查看串口输出:gcloud compute instances get-serial-port-output + 磁盘迁移法

云主机的哲学

物理服务器是宠物(生病了要治疗),云主机是牲口(生病了就换一头)。

拥抱"不可变基础设施"的思路:问题实例直接销毁,用已知的好镜像重新部署。

附:完整可运行的 C++ 演示——模拟 systemd 的依赖解析

下面用 C++ 模拟 systemd 解析服务依赖并确定启动顺序的核心逻辑(拓扑排序):

// 文件名: systemd_deps.cpp
// 模拟 systemd 的服务依赖解析和启动顺序计算
// 使用拓扑排序(Kahn算法)确定启动顺序
// 编译: g++ -std=c++17 -o systemd_deps systemd_deps.cpp
// 运行: ./systemd_deps
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <queue>
#include <algorithm>
// =============================================
// 服务单元(模拟 systemd unit)
// =============================================
struct Unit {
    std::string name;               // 服务名称
    std::vector<std::string> after; // 必须在这些服务之后启动(After=)
    std::vector<std::string> wants; // 软依赖(Wants=)
    bool enabled;                   // 是否开机自启
    std::string status;             // 当前状态
};
// =============================================
// 简化的服务管理器(模拟 systemd 核心逻辑)
// =============================================
class ServiceManager {
private:
    std::map<std::string, Unit> units;      // 所有已注册的 unit
    // 拓扑排序(Kahn 算法)确定启动顺序
    // 解决问题:A 依赖 B,B 依赖 C,应该按 C → B → A 的顺序启动
    std::vector<std::string> topologicalSort(
            const std::vector<std::string>& targets) {
        // 构建"谁必须在我之前启动"的图
        // in_degree[X] = 有多少个服务必须在 X 之前启动
        std::map<std::string, int> inDegree;
        // adj[A] = A 必须在 adj[A] 中所有服务之前启动
        std::map<std::string, std::vector<std::string>> adj;
        // 初始化:所有目标服务的入度为 0
        for (const auto& name : targets) {
            inDegree[name] = 0;
        }
        // 建图:处理 After 依赖关系
        // "After=B" 意味着 B 必须先于 A 启动
        // 即在图中 B → A(B 的完成触发 A 的启动)
        for (const auto& name : targets) {
            if (units.count(name) == 0) continue;
            for (const auto& dep : units[name].after) {
                if (inDegree.count(dep) > 0) {
                    // dep 必须在 name 之前启动
                    adj[dep].push_back(name);
                    inDegree[name]++;
                }
            }
        }
        // Kahn 算法:从入度为 0 的节点开始(没有前置依赖)
        std::queue<std::string> q;
        for (const auto& [name, degree] : inDegree) {
            if (degree == 0) {
                q.push(name);
            }
        }
        std::vector<std::string> order;
        while (!q.empty()) {
            std::string cur = q.front();
            q.pop();
            order.push_back(cur);
            // 当前服务启动后,减少所有依赖它的服务的入度
            if (adj.count(cur) > 0) {
                for (const auto& next : adj[cur]) {
                    inDegree[next]--;
                    if (inDegree[next] == 0) {
                        q.push(next);  // 所有前置依赖都满足了
                    }
                }
            }
        }
        // 检测循环依赖
        if (order.size() < targets.size()) {
            std::cerr << "警告:检测到循环依赖!\n";
        }
        return order;
    }
public:
    // 注册一个 unit
    void addUnit(const Unit& unit) {
        units[unit.name] = unit;
        units[unit.name].status = "inactive";
    }
    // 启动服务(解析依赖后按顺序启动)
    void startTarget(const std::string& targetName) {
        std::cout << "\n=== 启动目标: " << targetName << " ===\n";
        // 收集所有需要启动的服务(BFS 展开依赖)
        std::set<std::string> toStart;
        std::queue<std::string> bfsQueue;
        bfsQueue.push(targetName);
        while (!bfsQueue.empty()) {
            std::string cur = bfsQueue.front();
            bfsQueue.pop();
            if (toStart.count(cur) > 0) continue;
            toStart.insert(cur);
            if (units.count(cur) == 0) continue;
            // 将 After 依赖的服务也加入待启动列表
            for (const auto& dep : units[cur].after) {
                if (toStart.count(dep) == 0) {
                    bfsQueue.push(dep);
                }
            }
            // Wants 的服务(软依赖,尽量启动)
            for (const auto& dep : units[cur].wants) {
                if (toStart.count(dep) == 0) {
                    bfsQueue.push(dep);
                }
            }
        }
        // 转换为有序列表进行拓扑排序
        std::vector<std::string> toStartVec(toStart.begin(), toStart.end());
        std::vector<std::string> startOrder = topologicalSort(toStartVec);
        // 按顺序启动
        std::cout << "\n启动顺序:\n";
        for (size_t i = 0; i < startOrder.size(); i++) {
            const std::string& name = startOrder[i];
            std::cout << "  [" << (i+1) << "] 启动 " << name;
            // 检查单元是否存在
            if (units.count(name) > 0) {
                units[name].status = "active";
                std::cout << " ... [OK]\n";
            } else {
                std::cout << " ... [跳过,单元未注册]\n";
            }
        }
    }
    // 查看服务状态(类似 systemctl status)
    void status(const std::string& name) const {
        std::cout << "\n● " << name;
        if (units.count(name) == 0) {
            std::cout << " - 未找到\n";
            return;
        }
        const auto& unit = units.at(name);
        std::cout << "\n   状态: " << unit.status
                  << "\n   开机自启: " << (unit.enabled ? "是" : "否")
                  << "\n   依赖(After): ";
        for (const auto& dep : unit.after) {
            std::cout << dep << " ";
        }
        std::cout << "\n   软依赖(Wants): ";
        for (const auto& dep : unit.wants) {
            std::cout << dep << " ";
        }
        std::cout << "\n";
    }
    // 列出所有服务
    void listUnits() const {
        std::cout << "\n=== 已注册的服务 ===\n";
        std::cout << std::string(45, '-') << "\n";
        printf("%-25s %-10s %-6s\n", "服务名称", "状态", "自启");
        std::cout << std::string(45, '-') << "\n";
        for (const auto& [name, unit] : units) {
            printf("%-25s %-10s %-6s\n",
                   name.c_str(),
                   unit.status.c_str(),
                   unit.enabled ? "是" : "否");
        }
        std::cout << std::string(45, '-') << "\n";
    }
};
// =============================================
// 主程序:模拟系统启动过程
// =============================================
int main() {
    std::cout << "=== systemd 服务依赖解析演示 ===\n";
    ServiceManager mgr;
    // 注册各个服务(类似安装软件包时放置 unit 文件)
    // 注意 After= 字段定义了启动顺序约束
    // 基础硬件层
    mgr.addUnit({"local-fs.target",     {}, {}, true, ""});
    mgr.addUnit({"sysinit.target",      {"local-fs.target"}, {}, true, ""});
    // 网络层(依赖系统初始化完成)
    mgr.addUnit({"network.target",      {"sysinit.target"}, {}, true, ""});
    mgr.addUnit({"network-online.target",{"network.target"}, {}, true, ""});
    // 基础服务(依赖文件系统)
    mgr.addUnit({"rsyslog.service",     {"sysinit.target"}, {}, true, ""});
    mgr.addUnit({"sshd.service",        {"network.target", "sysinit.target"}, {}, true, ""});
    // 应用服务(依赖网络)
    mgr.addUnit({"postgresql.service",  {"network.target", "local-fs.target"}, {}, true, ""});
    mgr.addUnit({"nginx.service",       {"network.target", "postgresql.service"}, {}, true, ""});
    // 最终目标
    mgr.addUnit({"multi-user.target",
                 {"sysinit.target", "network.target"},
                 {"sshd.service", "rsyslog.service", "nginx.service"},
                 true, ""});
    // 列出所有服务
    mgr.listUnits();
    // 模拟启动到 multi-user.target
    mgr.startTarget("multi-user.target");
    // 查看某个服务的状态
    mgr.status("nginx.service");
    mgr.status("sshd.service");
    return 0;
}

运行示例输出

=== systemd 服务依赖解析演示 ===
=== 已注册的服务 ===
---------------------------------------------
服务名称              状态     自启
---------------------------------------------
local-fs.target           inactive   是   
multi-user.target         inactive   是   
network-online.target     inactive   是   
network.target            inactive   是   
nginx.service             inactive   是   
postgresql.service        inactive   是   
rsyslog.service           inactive   是   
sshd.service              inactive   是   
sysinit.target            inactive   是   
---------------------------------------------
=== 启动目标: multi-user.target ===
启动顺序:
  [1] 启动 local-fs.target ... [OK]
  [2] 启动 sysinit.target ... [OK]
  [3] 启动 network.target ... [OK]
  [4] 启动 rsyslog.service ... [OK]
  [5] 启动 multi-user.target ... [OK]
  [6] 启动 postgresql.service ... [OK]
  [7] 启动 sshd.service ... [OK]
  [8] 启动 nginx.service ... [OK]
● nginx.service
   状态: active
   开机自启: 是
   依赖(After): network.target postgresql.service 
   软依赖(Wants): 
● sshd.service
   状态: active
   开机自启: 是
   依赖(After): network.target sysinit.target 
   软依赖(Wants): 

总结:第二章核心知识点

系统启动

固件层

BIOS 传统标准

UEFI 现代标准

ESP 系统分区

引导加载程序

GRUB Linux首选

FreeBSD loader

配置文件修改

内核

接收启动参数

创建PID 1

系统管理守护进程

传统init 已淘汰

BSD init FreeBSD用

systemd Linux标准

systemd

Unit 文件

systemctl 命令

Target 运行目标

Journal 日志系统

救援

单用户模式

紧急模式

云主机磁盘迁移

一句话总结:计算机启动是一个接力赛,固件找到引导程序,引导程序加载内核,内核启动 systemd,systemd 按依赖顺序启动所有服务,系统就绪。

UNIX / Linux 访问控制与超级用户权力 — 详细中文解读

来源:《UNIX and Linux System Administration Handbook》第 3 章

目录

  1. 什么是访问控制
  2. 标准 UNIX 访问控制模型
  3. 文件系统访问控制
  4. 进程所有权
  5. root 账户
  6. Setuid 与 Setgid
  7. root 账户的管理
  8. su 命令
  9. sudo 命令
  10. 标准模型的缺陷与扩展
  11. 现代访问控制系统

1. 什么是访问控制

访问控制(Access Control) 是指内核(操作系统核心)如何决定"谁能做什么"的一套机制。
和"安全(Security)"的区别:

  • 访问控制 = 机械性规则,内核按规则判断是否放行
  • 安全 = 更宏观的策略,比如如何防止入侵者
    过去十年,UNIX/Linux 的访问控制经历了爆发式发展,出现了大量新选项。但传统模型依然是默认标准,适用于绝大多数场景。

2. 标准 UNIX 访问控制模型

标准 UNIX 访问控制遵循以下几条基本规则:

规则 说明
基于用户身份做决策 某些情况也看用户所在的组
对象有所有者 所有者对自己的对象有广泛(但不是无限)的控制权
你创建的对象归你所有 比如你新建的文件,你就是它的主人
root 是万能所有者 root 可以充当任何对象的所有者
某些敏感操作仅 root 可做 普通用户被拒绝

内核如何判断权限

某个系统调用(如 settimeofday)
        |
        v
  检查当前用户是否是 root?
        |
   是 --> 允许执行
   否 --> 拒绝

另一些系统调用(如 kill)还会综合考虑"所有权匹配 + root 特权"。

文件系统与内核的关系

文件系统有自己独立的访问控制,和内核 VFS 层协作。/dev 下的设备文件就是通过文件系统访问控制来管理设备权限的。

3. 文件系统访问控制

所有者与组

每个文件都有:

  • 一个所有者(owner):单个用户
  • 一个组所有者(group owner):可以是一组用户
    所有者可以设置文件的权限,甚至可以设置得极其严格,让其他人无法访问。

查看文件权限示例

$ ls -l ~garth/todo
# 输出示例:
-rw-r-----  1 garth  staff   1259 May 29 19:55 /Users/garth/todo

解读:

-rw-r-----
 |||||||||||
 ||||||++++--- 其他人(other):无任何权限
 |||+++-------- 组(staff):只读(r)
 +++----------- 所有者(garth):可读可写(rw)

UID 和 GID

  • 内核和文件系统内部用数字追踪所有权,而不是字符串名称
  • UID(User ID):用户编号,映射在 /etc/passwd
  • GID(Group ID):组编号,映射在 /etc/group
  • ls 等命令显示时会查表把数字转成名字,方便人类阅读

4. 进程所有权

每个进程实际上有多重身份

身份类型 说明
真实 UID(real UID) 用于记账(追踪是谁启动的进程),现基本是历史遗留
有效 UID(effective UID) 实际用于权限判断
保存的 UID(saved UID) 暂存用,允许进程反复进出特权模式
文件系统 UID(filesystem UID) 仅用于文件访问权限判断,通常和有效 UID 相同,是 NFS 的实现细节
真实/有效/保存 GID 与 UID 类似,但针对组

通常情况下,真实 UID = 有效 UID,两者相同。
保存 UID 的作用:进程可以在"普通模式"和"特权模式"之间反复切换,减少意外风险。

5. root 账户

什么是 root

  • root 账户的本质特征:UID = 0
  • 也叫"超级用户(superuser)",但实际账户名是 root
  • 拥有对系统上任何有效操作的完全控制权

注意:你可以改 root 的用户名,也可以创建多个 UID=0 的账户,但这两者都是极坏的主意,会造成安全漏洞和混乱。

root 可以做哪些普通用户不能做的事


操作 说明
创建设备文件 /dev 下的设备节点
设置系统时钟 settimeofday 系统调用
提高资源使用上限和进程优先级 ulimit 相关
设置系统主机名 hostname
配置网络接口 ifconfig/ip 等
绑定特权网络端口(< 1024) 如 80/443 端口
关闭系统 shutdown/reboot

login 程序的工作原理

用户输入用户名和密码
        |
        v
login 程序(以 root 身份运行)
        |
   验证通过?
        |
   是 --> 调用 setuid/setgid,把自己的 UID/GID 改为用户的 UID/GID
        |
        v
   启动用户的 shell(此时进程已不是 root,无法恢复)

一旦 root 进程把自己的 UID 改成普通用户,就再也无法恢复超级用户状态。

6. Setuid 与 Setgid

什么是 setuid

Setuid 是一种让可执行文件"以文件所有者的身份运行"的机制,而不是以启动它的用户身份运行。
典型例子:passwd 命令

用户执行 passwd(改密码)
        |
        v
passwd 文件的所有者是 root,且设置了 setuid 位
        |
        v
内核把进程的有效 UID 临时改为 root(文件所有者)
        |
        v
passwd 可以访问受保护的 /etc/shadow 文件
        |
   是普通用户? --> 只允许改自己的密码
   是 root?   --> 可以改任何人的密码

setuid 的安全风险

  • setuid 程序(尤其是 setuid root)是安全漏洞的重灾区
  • 最小化 setuid 程序数量是降低风险的最佳方法
  • 可以在挂载文件系统时使用 nosuid 选项禁用 setuid
# 挂载时禁用 setuid,适合用户家目录或不信任的文件系统
mount -o nosuid /dev/sdb1 /mnt/untrusted

7. root 账户的管理

为什么不应该直接以 root 登录

直接登录 root 存在以下问题:

  1. 没有操作记录:不知道以 root 身份做了什么
  2. 无法追责:多人共用 root 时不知道谁做了什么、何时做的
  3. Ubuntu 默认禁止 root 直接登录
    建议:只在控制台(console)允许 root 登录,禁止通过终端、图形界面、网络登录 root。

8. su 命令

su(substitute user,切换用户)是访问 root 的一种方式。

# 切换到 root(会提示输入 root 密码)
su
# 切换到指定用户(需要知道对方密码)
su - username
# - (dash) 表示以登录模式启动 shell,会读取 ~/.bash_profile 等配置文件
# 推荐用完整路径,防止恶意程序冒充 su
/bin/su

su 的特点

  • 会记录"谁在什么时间变成了 root",但不记录以 root 身份执行的具体命令
  • 在大多数系统上,需要是 wheel 组成员才能使用 su
  • 现在基本被 sudo 取代,su 主要用于紧急情况或 sudo 损坏时

9. sudo 命令

sudo 是目前最广泛使用的 root 访问管理工具,由 Todd Miller 维护,官网:sudo.ws

sudo 工作原理

用户执行: sudo <命令>
        |
        v
sudo 读取 /etc/sudoers 配置文件
        |
   该用户被授权执行该命令?
        |
   是 --> 提示输入用户自己的密码(不是 root 密码)
        |
        v
   密码验证通过 --> 以 root 或指定用户身份执行命令
        |
        v
   记录日志(谁、在哪台主机、从哪个目录、什么时间、执行了什么)

5 分钟免再次输密码:成功 sudo 一次后,5 分钟内再次 sudo 不需要重新输密码(可配置)。

sudoers 配置文件详解

# 用 visudo 命令安全地编辑 sudoers 文件
# visudo 会检查语法,防止写错导致无法再用 sudo
visudo

下面是一个典型的 sudoers 配置示例,附详细注释:

# ===== 定义主机别名 =====
# CS 组包含这几台机器
Host_Alias CS = tigger, anchor, piper, moet, sigi
# PHYSICS 组包含这几台机器
Host_Alias PHYSICS = eprince, pprince, icarus
# ===== 定义命令别名 =====
# 备份相关命令
Cmnd_Alias DUMP = /sbin/dump, /sbin/restore
# 监控守护程序
Cmnd_Alias WATCHDOG = /usr/local/bin/watchdog
# 常见 shell(危险!)
Cmnd_Alias SHELLS = /bin/sh, /bin/dash, /bin/bash
# ===== 权限规则 =====
# mark 和 ed 在 PHYSICS 组的机器上可以以 root 身份执行任何命令
mark, ed  PHYSICS = ALL
# herb 在 CS 机器上可以跑 tcpdump;
# 在 PHYSICS 机器上可以跑 dump/restore,但必须以 operator 身份(不是 root)
herb   CS = /usr/sbin/tcpdump : PHYSICS = (operator) DUMP
# lynda 在所有机器上可以以任何用户身份运行任何命令,但不能直接运行 shell
# (注意:这其实防不住,见下方说明)
lynda  ALL = (ALL) ALL, !SHELLS
# wheel 组成员在非 PHYSICS 机器上可以无需密码运行 watchdog
%wheel  ALL, !PHYSICS = NOPASSWD: WATCHDOG

为什么 !SHELLS 防不住

# lynda 可以复制 sh 到 /tmp,然后 sudo 运行它
cp -p /bin/sh /tmp/sh
sudo /tmp/sh
# 结果:得到了 root shell!

结论:“允许所有命令,但排除某些” 的配置几乎注定失败,只能用作提醒而非真正的安全防护。

sudo 的日志示例

Dec 7 10:57:19 tigger sudo: randy: TTY=ttyp0 ; PWD=/tigger/users/randy;
USER=root ; COMMAND=/bin/cat /etc/sudoers

记录了:用户名、终端、当前目录、以谁的身份运行、具体命令。

sudo 的优缺点

优点:

优点 说明
命令级别的审计日志 知道谁做了什么
细粒度授权 用户只能做特定的事
root 密码可以只有极少数人知道(甚至没人知道) 降低泄露风险
操作比 su/直接登录 root 更快 直接在当前 shell 里用
无需改 root 密码就能撤销权限 只需改 sudoers
维护一份授权用户的权威名单 一处管理全局
root shell 被遗留无人看管的风险更小 操作完即结束
单一配置文件管控整个网络 方便统一管理

缺点:

  1. 任何 sudo 用户的账户被攻破,等于 root 被攻破
  2. 日志记录可被绕过(如 shell escape、sudo shsudo su

典型的最简 sudoers 配置(90% 的场景够用)

# 定义管理员用户别名
User_Alias ADMINS = alice, bob, charles
# 管理员在所有主机上可以以任何用户身份执行任何命令
ADMINS  ALL = (ALL) ALL

环境变量管理

sudo 默认只传递一个干净、最小化的环境变量集给被执行的命令,防止恶意环境变量(如 EDITOR)被利用。

# 在 sudoers 中白名单某些环境变量
Defaults  env_keep += "SSH_AUTH_SOCK"
Defaults  env_keep += "DISPLAY XAUTHORIZATION XAUTHORITY"
# 也可以在命令行里临时指定
sudo EDITOR=emacs vipw

无密码 sudo(危险!)

# 允许 ansible 用户无需密码以任何人身份运行任何命令
# 注意:这非常危险,只在有充分理由时才用
ansible  ALL = (ALL) NOPASSWD: ALL  # 不要这样做!

更安全的替代方案:用 SSH Agent 转发和 PAM 模块实现无交互认证。

匹配优先级规则

sudo 遵循最后一条匹配规则(匹配依据:用户、主机、目标用户、命令四元组)。
因此 NOPASSWD 的例外必须放在更通用规则之后

# 正确顺序:
%wheel   ALL = (ALL) ALL                          # 通用规则(需密码)
ADMINS   ALL = (ALL) NOPASSWD: /usr/sbin/logrotate  # 例外放后面(无需密码)
# 错误顺序(如果反过来,NOPASSWD 会被后面的规则覆盖,alice 总得输密码)

禁用 root 账户

如果整个站点都用 sudo,可以考虑完全禁用 root 密码登录:

# Linux 上锁定 root 账户(在加密密码前加 !,使其无法验证)
passwd -l root
# Ubuntu 解锁 root(如需恢复)
sudo passwd -u root

禁用后:

  • root 无法直接登录(包括控制台)
  • su 也无法切换到 root
  • 但 root 账户本身仍存在,以 root 身份运行的系统软件不受影响
  • sudo 正常工作

建议:物理服务器最好保留 root 密码,以便硬件或引导出问题时紧急恢复;云/虚拟机实例则可以放心锁定。

10. 标准模型的缺陷与扩展

标准模型的主要缺陷


缺陷 说明
root 是单点故障 root 被攻破,整个系统沦陷
分权只能靠 setuid 程序 安全的 setuid 程序难以编写
网络场景无力 物理访问的机器可以伪造 UID
组的定义是特权操作 普通用户无法自定义"只有 alice 和 bob 可以访问"这样的规则
规则硬编码在程序里 不改源码无法调整行为
缺乏审计支持 无法追踪特权的使用情况

PAM(可插拔认证模块)

PAM 解决的问题:“我怎么知道这真的是用户 X?”(认证,而非授权)

程序(login/sudo/su)调用 PAM
        |
        v
PAM 根据管理员配置,选择认证方法
(密码 / 生物识别 / 两步验证 / 网络身份系统 / ...)
        |
        v
返回认证结果

好处:程序不需要自己实现认证逻辑,只需调用 PAM 接口。

Kerberos

  • 和 PAM 一样处理认证,不处理授权
  • 使用可信第三方服务器(KDC)为整个网络做认证
  • 用户向 Kerberos 服务提交凭证,获取加密票据,凭票据访问其他服务
  • Windows Active Directory 的标准认证机制

文件系统访问控制列表(ACL)

标准 UNIX 权限只支持"所有者/组/其他人"三档,ACL 允许对多个用户和组分别设置权限:

# 示例:给 bob 单独赋予读权限
setfacl -m u:bob:r-- myfile.txt
# 查看 ACL
getfacl myfile.txt

Linux Capabilities(能力)

把 root 的全部权力拆分成约 30 个独立的"能力(capability)":

能力名称 对应权限
CAP_NET_BIND_SERVICE 绑定 1024 以下的特权端口
CAP_SYS_TIME 修改系统时钟
CAP_KILL 向任意进程发送信号
CAP_CHOWN 修改文件所有者

# 查看所有 capability 的说明(强烈推荐)
man 7 capabilities

实际上,capabilities 现在主要作为底层技术被 AppArmor、Docker 等高层系统使用,很少直接用于日常管理。

Linux 命名空间(Namespaces)

将进程隔离在一个"沙盒"中,让它只能看到系统的一个子集(文件、网络、进程等)。

命名空间(Namespace)
┌─────────────────────────────────┐
│  进程看到的文件系统(一部分)       │
│  进程看到的网络端口(一部分)       │
│  进程看到的其他进程(一部分)       │
│                                 │
│  在此范围内,普通访问控制规则生效   │
│  进程甚至不知道自己被隔离了        │
└─────────────────────────────────┘

命名空间是 Docker 容器的核心基础技术。

11. 现代访问控制系统

Linux Security Modules(LSM)

2001 年,NSA 希望把 SELinux 合并入内核,但内核维护者不愿选边站,于是创造了 LSM API——一个允许任何访问控制系统以内核模块形式接入的接口。
目前 Linux 内核自带的 LSM 模块:

模块 说明
SELinux NSA 出品,功能最全,但极难管理
AppArmor Canonical 出品,Ubuntu 默认启用,更易用
Smack 轻量级 MAC
TOMOYO 路径名为基础的 MAC
Yama 进程追踪相关的限制

限制:同时只能激活一个 LSM 附加模块(模块之间没有协作机制)。

强制访问控制(MAC)vs 自主访问控制(DAC)

  • DAC(Discretionary AC):你自己决定你的文件谁能访问(标准 UNIX 模型)
    • 缺点:用户可能配置错误,管理员无法强制覆盖
  • MAC(Mandatory AC):管理员写的策略可以覆盖用户的权限设置
    • 例:无论用户怎么设,家目录只有所有者能看
      多级安全(MLS)模型(常见于政府/军事):
      用户安全级别 ≤ 资源安全级别 ⇒ 允许访问 \text{用户安全级别} \leq \text{资源安全级别} \Rightarrow \text{允许访问} 用户安全级别资源安全级别允许访问
      用户安全级别 < 资源安全级别 ⇒ 拒绝访问 \text{用户安全级别} < \text{资源安全级别} \Rightarrow \text{拒绝访问} 用户安全级别<资源安全级别拒绝访问
      例:拥有"秘密(Secret)"级别的用户,可读写"秘密"级别的文档,但不能访问"绝密(Top Secret)"文档。

基于角色的访问控制(RBAC)

在用户和权限之间加一层"角色(Role)"的间接层:

权限(Permissions)
     |
     v
角色(Roles)  ←── 可以有层级关系(如"高级管理员"继承"管理员"的所有权限 + 额外权限)
     |
     v
用户(Users)

访问决策流程:

  1. 查出当前用户的所有角色
  2. 检查这些角色是否包含所需权限
  3. 有 → 允许;无 → 拒绝

SELinux

  • NSA 出品,历史最悠久的 Linux MAC 实现
  • 实现了几乎所有可以想象的 MAC 和 RBAC 变体
  • 极其复杂,难以管理和排错
  • 在政府、金融、医疗等强合规场景有一定使用
  • Android 平台的标准访问控制机制
    关键配置文件 /etc/selinux/config
# SELinux 工作模式
SELINUX=enforcing   # enforcing=强制执行, permissive=只记录不拦截, disabled=完全关闭
SELINUXTYPE=targeted # 使用的策略集名称(targeted=只保护特定服务,其余不管)
# 从违规日志自动生成策略(audit2allow 工具)
# 先用 permissive 模式跑一段时间,收集日志,再生成策略
audit2allow -a -M mypolicy
semodule -i mypolicy.pp

AppArmor

  • Canonical 出品,Ubuntu 和 SUSE 默认启用
  • 目标:服务加固(service securement),限制被攻破的服务能做的损害
  • 基于路径名定义策略,配置文件相对可读
    AppArmor 配置文件示例/etc/apparmor.d/ 目录下):
# cups-browsed 打印服务的 AppArmor 配置文件
#include <tunables/global>   # 引入全局变量定义
/usr/sbin/cups-browsed {    # 保护的程序路径
  # 引入通用模块(每个都展开后是大量规则)
  #include <abstractions/base>          # 基础库访问权限
  #include <abstractions/nameservice>   # DNS/主机名解析权限
  #include <abstractions/cups-client>   # CUPS 打印客户端权限
  #include <abstractions/dbus>          # D-Bus 通信权限
  #include <abstractions/p11-kit>       # PKCS#11 密钥权限
  # 具体文件访问权限(r=读, w=写, rw=读写)
  /etc/cups/cups-browsed.conf r,        # 只读配置文件
  /etc/cups/lpoptions r,                # 只读打印选项
  /{var/,}run/cups/certs/* r,           # 只读证书(路径可带或不带 var/)
  /var/cache/cups/* rw,                 # 读写缓存
  /tmp/** rw,                           # 读写 /tmp 下所有文件(** 匹配多级目录)
  # 允许站点级别的自定义覆盖
  #include <local/usr.sbin.cups-browsed>
}

AppArmor 和 SELinux 在 Red Hat targeted 模式下的角色类似,但 AppArmor 专门针对服务加固设计,绕过了 SELinux 的很多复杂性。

整体架构图

是 root

非 root

有权限

无权限

AppArmor

SELinux

策略允许

策略拒绝

策略允许

策略拒绝

用户请求操作

标准 UNIX 访问控制

有效 UID = 0?

几乎允许所有操作

检查文件权限
owner/group/other

允许

拒绝

LSM 模块是否激活?

检查路径名策略

检查 MAC 标签策略

sudo 权限判断流程图

需要密码

不需要密码

用户执行 sudo 命令

读取 /etc/sudoers

遍历所有规则
找最后一条匹配的

找到匹配规则?

拒绝执行

需要密码?
NOPASSWD?

提示输入用户自己的密码

密码正确?

以指定身份执行命令

记录到系统日志 syslog

关键概念速查


术语 全称 一句话解释
UID User ID 用户编号,内核识别用户的方式
GID Group ID 组编号
root - UID=0 的超级用户账户
setuid Set User ID 让程序以文件所有者身份运行的机制
su Substitute User 切换用户身份的命令
sudo Super User Do 以 root 或其他身份执行单条命令的工具
PAM Pluggable Auth Modules 可插拔认证模块,解决"如何验证用户身份"
ACL Access Control List 访问控制列表,比传统三档权限更细粒度
MAC Mandatory Access Control 强制访问控制,管理员策略可覆盖用户设置
DAC Discretionary AC 自主访问控制,用户自己控制自己对象的权限
RBAC Role-Based AC 基于角色的访问控制,权限先赋给角色再赋给用户
LSM Linux Security Modules Linux 内核安全模块接口
SELinux Security-Enhanced Linux NSA 出品的功能最全但最复杂的 MAC 实现
AppArmor - Canonical 出品的易用 MAC,基于路径名策略
capability - 将 root 权力细分的约 30 个独立权限单元
namespace - 进程隔离机制,Docker 的基础

UNIX / Linux 进程控制 — 详细中文解读

来源:《UNIX and Linux System Administration Handbook》第 4 章

目录

  1. 什么是进程
  2. 进程的组成部分
  3. 进程的生命周期
  4. 信号(Signals)
  5. ps:监控进程
  6. top:实时监控
  7. nice 与 renice:调度优先级
  8. /proc 文件系统
  9. strace / truss:系统调用追踪
  10. 失控进程的处理
  11. 定时任务

1. 什么是进程

进程(Process) 是一个正在运行的程序的抽象。通过进程,操作系统可以管理和监控:

  • 内存使用
  • 处理器时间(CPU 时间)
  • I/O 资源(输入输出)
    UNIX 哲学的一条公理:尽可能多地在进程上下文中完成工作,而不是让内核特殊处理。系统进程和用户进程遵循同一套规则,因此可以用同一套工具管理它们。

2. 进程的组成部分

地址空间

一个进程由以下两部分组成:

进程 = 地址空间 + 内核中的数据结构
         |
         +-- 代码和库(正在执行的程序)
         +-- 变量(全局变量、局部变量)
         +-- 栈(函数调用栈)
         +-- 内核运行时需要的额外信息

内存以页(page)为单位管理,通常每页大小为 4 KiB 4\text{KiB} 4KiB 8 KiB 8\text{KiB} 8KiB
进程的虚拟地址空间在物理内存中是
随机分布
的,由内核的**页表(page table)**跟踪管理。

内核为每个进程记录的关键信息


数据项 说明
地址空间映射 进程占用哪些内存页
当前状态 运行中、睡眠、停止等
执行优先级 决定分配多少 CPU 时间
资源使用统计 已用 CPU 时间、内存等
打开的文件和网络端口 文件描述符表
信号掩码 哪些信号被屏蔽
进程的所有者 UID

线程(Thread)

线程是进程内部的一个执行上下文

一个进程
├── 线程 1(有自己的栈和 CPU 上下文)
├── 线程 2(有自己的栈和 CPU 上下文)
└── 线程 3(有自己的栈和 CPU 上下文)
        |
        所有线程共享同一个地址空间

现代多核 CPU 可以让同一进程的多个线程真正并行运行在不同的核心上。Apache、BIND 等服务器软件正是利用多线程来同时处理多个请求。

关键进程属性详解

PID(进程 ID)
内核为每个进程分配一个唯一的数字编号。PID 按创建顺序递增分配。
Linux 的**命名空间(namespace)**机制会让同一个进程在不同命名空间中看起来有不同的 PID——就像爱因斯坦相对论里"观察者不同,测量结果不同"一样。
PPID(父进程 ID)
UNIX/Linux 没有"从零创建新进程"的系统调用。新进程的创建分两步:

  1. 现有进程**克隆(fork)**自身,产生子进程
  2. 子进程再**替换(exec)**自己正在执行的程序
    PPID 就是克隆来源(父进程)的 PID。当遇到不认识的进程时,顺着 PPID 往上追溯,往往能找到它的来源和用途。
    UID 和 EUID

属性 全称 说明
UID Real User ID 创建该进程的用户编号,用于记账
EUID Effective User ID 实际用于权限判断的 UID
Saved UID 保存的 UID 临时存放,供进程在特权模式和普通模式之间切换
FSUID Filesystem UID 仅用于文件系统权限判断,Linux 特有,通常等于 EUID

为什么要同时有 UID 和 EUID?

  • 普通情况:UID = EUID(两者相同)
  • setuid 程序运行时:EUID 临时变为文件所有者的 UID,UID 保持不变
  • 这样程序可以知道"谁启动了我"(UID)同时又能"以特权身份工作"(EUID)
    一个保守编写的 setuid 程序会这样工作:
启动时 EUID = root(获得特权)
         |
         v
完成需要特权的操作
         |
         v
暂时降低 EUID(从 saved UID 恢复为普通用户)
         |
         v
执行不需要特权的操作(更安全)
         |
         v
需要特权时再从 saved UID 恢复 EUID

GID 和 EGID
GID/EGID 的关系和 UID/EUID 完全类比。需要特别注意的是:

  • GID 本身现在基本是历史遗留,实际权限判断用的是 EGID + 补充组列表
  • GID 唯一真正有意义的时机:进程创建新文件时,新文件可能继承进程的 GID
    Niceness(友好度)
    Niceness 是给内核的一个调度提示,取值范围:
    Linux:  − 20 ≤ niceness ≤ + 19 \text{Linux: } -20 \leq \text{niceness} \leq +19 Linux: 20niceness+19
    FreeBSD:  − 20 ≤ niceness ≤ + 20 \text{FreeBSD: } -20 \leq \text{niceness} \leq +20 FreeBSD: 20niceness+20
  • 高 niceness(正数)= 低优先级 = 对其他进程"很友好"
  • 低 niceness(负数)= 高优先级 = 对其他进程"不友好"
    控制终端(Control Terminal)
    大多数非守护进程都有一个关联的控制终端,它决定了:
  • 标准输入(stdin)、标准输出(stdout)、标准错误(stderr)的默认连接
  • 键盘事件(如 Ctrl+C)产生的信号发送给哪个进程

3. 进程的生命周期

fork 和 exec

调用 fork

fork 返回子进程的 PID

fork 返回 0

调用 exec

父进程

子进程
是父进程的完整拷贝

父进程继续运行

子进程知道自己是子进程

子进程加载新程序
替换自身的内存空间

新程序开始执行

fork 的独特之处:它返回两个不同的值

  • 子进程视角:返回 0
  • 父进程视角:返回新创建子进程的 PID
    两个进程必须检查返回值来判断自己扮演哪个角色。

init / systemd(进程 1)

系统启动时,内核自主创建几个进程,其中最重要的是:

PID 1 = init 或 systemd
         |
         +-- 执行系统启动脚本
         +-- 所有其他进程都是它的后代
         +-- 负责"收养"孤儿进程(父进程先死掉的子进程)

进程的死亡与僵尸

调用 _exit

父进程调用 wait

父进程先死掉

init 调用 wait

进程完成工作

进程进入僵尸状态
等待父进程确认

内核彻底销毁进程

init/systemd 收养该进程

僵尸进程(Zombie Process):已经执行完毕,但父进程还没有调用 wait 来确认其死亡的进程。ps 输出中显示为 <exiting><defunct>

进程状态


状态代码 含义 说明
R Runnable 正在运行或随时可以运行
S Sleeping 短时睡眠(等待某个事件,< 20 秒)
D Uninterruptible sleep 不可中断的睡眠(通常是等待 I/O,无法被信号唤醒)
T Traced or stopped 被暂停或被调试器跟踪
Z Zombie 僵尸进程

附加标志:

标志 含义
W 进程已被换出内存(swap out)
< 优先级高于正常
N 优先级低于正常(nice 值为正)
L 部分页面被锁定在内存中
s 进程是会话领导者

不可中断睡眠(D 状态) 最常见的原因是等待 NFS 文件系统(以 hard 模式挂载时)响应。这种进程连 KILL 信号都无法处理,通常只能重启系统解决。

4. 信号(Signals)

信号是进程级别的中断请求,相当于操作系统给进程发的"通知"。

信号的来源

信号的发送者
├── 其他进程(进程间通信)
├── 终端驱动(Ctrl+C、Ctrl+Z 等按键)
├── 管理员(用 kill 命令手动发送)
├── 内核(进程犯了错误,如除以零)
└── 内核(通知进程某个"有趣"的事件,如子进程死亡)

信号的处理方式

收到信号后,进程有三种选择:

收到信号
    |
    +-- 已注册处理函数(catch)? --> 调用处理函数,完成后从中断点继续执行
    |
    +-- 设置为忽略(ignore)?   --> 信号被丢弃,什么都不发生
    |
    +-- 设置为阻塞(block)?    --> 信号进入队列,等解除阻塞后再处理
    |
    +-- 都没有?                 --> 执行内核的默认行为(通常是终止进程)

重要信号速查表


编号 名称 默认行为 可捕获 可阻塞 产生 core dump
1 HUP 终止
2 INT 终止
3 QUIT 终止
9 KILL 终止
11 SEGV 终止
15 TERM 终止
17 STOP 停止
18 TSTP 停止
19 CONT 忽略(继续运行)
28 WINCH 忽略
30 USR1 终止
31 USR2 终止

容易混淆的信号:KILL / INT / TERM / HUP / QUIT

这几个信号名字听起来都差不多,实际用途差异很大:

信号 真正的含义 典型使用场景
KILL(9) 内核级强制杀死,进程永远不会收到,直接被销毁 最后手段,kill -9 PID
INT(2) 打断当前操作,Ctrl+C 发出,进程可以清理后退出 中断正在运行的前台程序
TERM(15) 礼貌请求退出,进程应该清理状态再退出 kill PID 默认发送这个
HUP(1) 重新加载配置(守护进程)或关闭终端时清理(终端场景) kill -HUP nginx 让 nginx 重读配置
QUIT(3) 类似 TERM,但默认会产生 core dump 调试时获取内存快照

USR1 / USR2:没有固定含义,留给程序自定义。例如 Apache 用 HUP 做立即重启,用 USR1 做"优雅重启"(让已有连接处理完再切换)。
TSTP vs STOP

  • STOP(17):强制暂停,不可捕获,不可阻塞
  • TSTP(18):Ctrl+Z 发出的"软暂停请求",进程可以捕获它,做好清理后再自己发 STOP 信号暂停自己

kill / killall / pkill 命令

# 向指定 PID 的进程发送 TERM 信号(默认)
kill 1234
# 向指定 PID 发送 KILL 信号(强制杀死)
kill -9 1234
# 或者用信号名
kill -KILL 1234
# 按名字杀死所有匹配的进程
sudo killall httpd
# 按属性查找并发送信号,杀死所有 ben 用户的进程
sudo pkill -u ben
# 查看所有信号名称和编号
kill -l

kill -9 并不是百分之百保证进程立即消失。处于**不可中断睡眠(D 状态)**的进程即使收到 KILL 也不会响应,通常需要重启系统。

5. ps:监控进程

ps 是系统管理员监控进程的主要工具,显示的是某一时刻的进程快照。

常用命令组合

# 查看所有进程(最常用)
# a = 显示所有进程,u = 用户友好格式,x = 包括没有控制终端的进程
ps aux
# 查看更多技术细节(包含 PPID、niceness 等)
# l = 长格式
ps lax
# 增加列宽,适合参数很长的进程(如 Java)
ps auxww

ps aux 输出字段说明

以下是一行典型输出:

USER    PID  %CPU %MEM    VSZ   RSS TTY   STAT  TIME COMMAND
root   2384   0.0  0.6   4080  1660   ?   Ss   0:00 /usr/sbin/sshd

字段 含义
USER 进程所有者的用户名
PID 进程 ID
%CPU 占用 CPU 的百分比
%MEM 占用物理内存的百分比
VSZ 进程的虚拟内存大小(KB)
RSS 实际占用的物理内存大小(常驻集,KB)
TTY 关联的控制终端(? 表示没有)
STAT 当前进程状态(见上节状态表)
TIME 已消耗的 CPU 时间
COMMAND 命令名及参数

快速找到特定进程的 PID

# 方法 1:grep(最灵活)
ps aux | grep sshd
# 去掉 grep 自身(grep 也会出现在结果里)
ps aux | grep -v grep | grep sshd
# 方法 2:pidof(精确匹配完整路径)
pidof /usr/sbin/sshd
# 方法 3:pgrep(按名字搜索,返回 PID)
pgrep sshd

6. top:实时监控

ps 只是快照,top实时刷新的进程监视器(每 1~2 秒更新一次)。

top 输出解读

top - 20:07:43 up 1:59, 3 users, load average: 0.45, 0.16, 0.09
Tasks: 251 total, 1 running, 250 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 1.2 sy, 0.0 ni, 98.0 id, 0.0 wa, 0.0 hi, 0.2 si, 0.0 st
KiB Mem: 1013672 total, 128304 free, 547176 used, 338192 buff/cache

字段 说明
load average: 0.45, 0.16, 0.09 过去 1 分钟、5 分钟、15 分钟的平均负载(可运行进程数)
us 用户态 CPU 占用率
sy 内核态 CPU 占用率
id 空闲 CPU 百分比
wa 等待 I/O 的 CPU 百分比

多核显示:在 top 运行时按数字键 1,可以切换到显示每个 CPU 核心的独立使用率。

常用 top 内置操作


按键 效果
1 切换单核/总览显示
k 向某个 PID 发送信号(kill)
r renice(调整某个进程的优先级)
f 自定义显示的列(如添加 DATA 列查看实际内存用量)
q 退出

7. nice 与 renice:调度优先级

什么是 niceness

Niceness 是给内核调度器的提示,告诉它这个进程相对其他进程应该得到多少 CPU 时间。
niceness 高(正数) ⇒ 优先级低 ⇒ "对别人很友好" \text{niceness 高(正数)} \Rightarrow \text{优先级低} \Rightarrow \text{"对别人很友好"} niceness 高(正数)优先级低"对别人很友好"
niceness 低(负数) ⇒ 优先级高 ⇒ "对别人不友好" \text{niceness 低(负数)} \Rightarrow \text{优先级高} \Rightarrow \text{"对别人不友好"} niceness 低(负数)优先级高"对别人不友好"
取值范围:

  • Linux: [ − 20 , + 19 ] [-20, +19] [20,+19]
  • FreeBSD: [ − 20 , + 20 ] [-20, +20] [20,+20]

权限规则

  • 普通用户:只能提高自己进程的 niceness(降低优先级),不能降低
  • root:可以任意设置 niceness

使用示例

# 以 niceness +5 启动一个低优先级的长时间任务(降低优先级,提高友好度)
nice -n 5 ~/bin/longtask
# 将 PID 为 8829 的进程的 niceness 设置为 -5(提高优先级,需要 root)
sudo renice -5 8829
# 把用户 boggs 的所有进程 niceness 设为 5
sudo renice 5 -u boggs

注意:niceness 只影响 CPU 调度,不影响内存和 I/O 的分配。
如需调整 I/O 优先级,使用 ionice 命令。

nice vs renice 语法对比


工具 作用 语法
nice 启动新进程时设置优先级 nice -n <值> <命令>
renice 修改已运行进程的优先级 renice <值> <PID>renice <值> -u <用户>

建议用完整路径 /usr/bin/nice 避免与 shell 内置 nice 命令混淆。

8. /proc 文件系统

Linux 的 pstop 等工具的数据都来自 /proc 目录——这是一个伪文件系统,内核在这里实时暴露系统状态信息。

# /proc 的内容由内核动态生成,ls 显示为 0 字节,但实际有内容
cat /proc/1/status   # 查看 init/systemd 的状态
cat /proc/cpuinfo    # 查看 CPU 信息
cat /proc/meminfo    # 查看内存信息

每个进程的专属目录

格式:/proc/<PID>/,例如 /proc/1 永远是 init/systemd 的信息目录。

文件 内容
cmdline 完整命令行(用 null 字符分隔参数)
environ 环境变量列表
exe 符号链接,指向正在执行的可执行文件
cwd 符号链接,指向进程当前工作目录
fd/ 子目录,包含每个打开文件描述符的链接
maps 内存映射信息(共享库、内存段等)
stat 通用进程状态信息
statm 内存使用信息
cgroup 进程所属的 cgroup
ns/ 进程使用的每个命名空间的链接

实用技巧

# cmdline 和 environ 的内容用 null 字符分隔,转换成换行更易读
cat /proc/1234/cmdline | tr "\000" "\n"
cat /proc/1234/environ | tr "\000" "\n"
# 查看某进程链接了哪些库
cat /proc/1234/maps

9. strace / truss:系统调用追踪

ps、日志等常规工具不够用时,可以用 strace(Linux)或 truss(FreeBSD)窥探进程正在做什么
这两个工具能显示:

  • 进程发出的每一个系统调用(及其参数和返回值)
  • 进程收到的每一个信号
    而且可以在不干扰进程运行的情况下附加和分离。

使用示例

# 附加到 PID 为 5810 的正在运行的进程
sudo strace -p 5810
# 跟踪 fork 出的子进程(适合追踪 httpd 等会 fork 的守护进程)
sudo strace -f -p 1234
# 只显示文件相关操作(找配置文件位置非常有用)
sudo strace -e trace=file ls /tmp

输出示例解读

open("/proc", O_RDONLY|O_NONBLOCK|O_DIRECTORY) = 7
# open 系统调用:打开 /proc 目录,只读模式,返回文件描述符 7
read(8, "1 (init) S 0 0 0 0 ...", 1023) = 191
# read 系统调用:从文件描述符 8 读取最多 1023 字节,实际读到 191 字节
stat("/tmp/pw", 0x7ff...) = ERR#2 'No such file or directory'
# stat 系统调用失败:文件不存在(这是正常行为,cp 在创建前先检查目标是否存在)

错误排查技巧:关注返回值不为 0 的系统调用,通常能直接看出权限问题、文件不存在、端口冲突等错误。

10. 失控进程的处理

失控进程(Runaway Process):消耗的 CPU、磁盘或网络资源远超正常水平的进程。

排查步骤

第一步:发现异常
    |
    +-- ps aux 或 top 查看 CPU 占用最高的进程
    +-- uptime 查看系统负载均值
    |
第二步:判断是负载过重还是 Bug
    |
    +-- 系统进程异常 --> 几乎肯定是 Bug 或被入侵
    +-- 用户进程(Web 服务器、数据库)异常 --> 可能只是负载过高
    |
第三步:分析内存问题
    |
    +-- top 中查看 VIRT(虚拟内存总量)
    +-- top 中查看 RES(实际驻留内存)
    +-- top 中添加 DATA 列(最能反映进程真实内存用量)
    |
第四步:深入调试
    |
    +-- 先用 strace/truss 收集证据,再考虑杀进程
    +-- 杀掉进程后,大部分证据就消失了

关于负载均值

对于 CPU 密集型系统,理想状态:
负载均值 ≤ CPU 核心总数 \text{负载均值} \leq \text{CPU 核心总数} 负载均值CPU 核心总数
若负载均值持续超过核心数,系统处于过载状态。

磁盘被写满的处理

# 第一步:找出哪个文件系统满了
df -h
# 输出中找 Use% 接近或超过 100% 的行
# 第二步:在该文件系统上找占用最大的目录
du -h /var | sort -rh | head -20
# 第三步:反复缩小范围,直到找到具体的大文件
# 如果文件已删除但进程还在持有文件描述符(df 和 du 数字不一致时)
fuser /path/to/file   # 找出哪个进程在用这个文件
lsof /path/to/file    # 更详细的信息

df 和 du 数字不一致的原因:文件已被删除(unlink),但某个进程还持有它的文件描述符,磁盘空间直到文件描述符关闭才会释放。

11. 定时任务

cron:传统定时工具

cron 守护进程在系统启动时运行,负责按预定时间执行命令。

crontab 文件格式

每一行(非注释行)代表一个定时任务,格式如下:

分钟  小时  日期  月份  星期  命令
  |     |     |     |     |
  |     |     |     |     +-- 0=周日, 1=周一, ..., 6=周六
  |     |     |     +-------- 1-12
  |     |     +-------------- 1-31
  |     +-------------------- 0-23
  +-------------------------- 0-59

时间字段可以使用:

写法 含义 示例
* 匹配所有值 *(每分钟/每小时…)
单个整数 精确匹配 5(第 5 分钟)
a-b 范围 1-5(周一到周五)
*/na-b/n 步长 */10(每 10 分钟),0-18/3(0 到 18 每 3 小时)
逗号分隔 列举 1,3,5(周一、三、五)

典型 crontab 示例及注释
# 每天凌晨 1:20,删除 /tmp 下超过 7 天未修改的文件
20  1  *  *  *  find /tmp -mtime +7 -type f -exec rm -f {} ';'
# 每周日凌晨 4:00,运行 MySQL 数据库维护
0  4  *  *  Sun  (/usr/bin/mysqlcheck -u maintenance --optimize --all-databases)
# 每 10 分钟在周一、三、五检查 TCP 2181 端口并发邮件
*/10  *  *  *  1,3,5  echo ruok | /usr/bin/nc localhost 2181 | mail -s "TCP 2181 status" admin@example.com
# 每月 25 日 4:30 发送提醒邮件(% 表示换行,%之前是命令,之后是邮件正文)
30  4  25  *  *  /usr/bin/mail -s "月报提醒" ben@admin.com%月报到期了!%速速提交!
# 每分钟记录系统时间和负载(测试用,生产环境不要这样用)
*  *  *  *  *  echo $(/bin/date) - $(/usr/bin/uptime) >> ~/uptime.log

注意weekdaydom(月中日期)字段同时指定时,满足其中任意一个就会执行,而不是同时满足。

例如 0,30 * 13 * 5 的意思是"每个周五每月 13 日,每半小时执行一次",而不是"周五 13 号"。

crontab 管理命令
# 编辑当前用户的 crontab(推荐,会检查语法)
crontab -e
# 查看当前用户的 crontab
crontab -l
# 删除当前用户的 crontab
crontab -r
# root 操作其他用户的 crontab
crontab -e jsmith     # 编辑 jsmith 的 crontab
crontab -r jsmith     # 删除 jsmith 的 crontab
系统级 crontab

除了用户个人的 crontab,还有几个系统级的位置:

位置 说明
/etc/crontab 管理员手动维护的系统级 crontab,多一个"用户名"字段
/etc/cron.d/ 软件包安装的 crontab 片段
/etc/cron.hourly/ 放在此处的脚本每小时执行一次
/etc/cron.daily/ 每天执行一次
/etc/cron.weekly/ 每周执行一次

cron 访问控制
# Linux 下控制哪些用户可以使用 crontab
/etc/cron.allow   # 白名单:文件存在则只有列出的用户可用 crontab
/etc/cron.deny    # 黑名单:文件存在(且 allow 不存在)则列出的用户不可用 crontab
cron 的环境注意事项

cron 执行命令时不是登录 shell,不会读取 ~/.bash_profile。因此:

  • 建议在 crontab 顶部显式设置 PATH
  • 所有命令建议用完整路径(如 /usr/bin/python3 而不是 python3

systemd timers:现代定时方案

systemd 提供了比 cron 更强大的定时机制,支持相对时间触发(“开机后 15 分钟”)和绝对时间触发(“每周三中午”)。

systemd timer 的文件结构

一个 timer 由两个文件组成:

/etc/systemd/system/
├── mytask.timer    <-- 定义什么时候触发
└── mytask.service  <-- 定义执行什么
定时器类型

类型 时间基准
OnBootSec 相对于系统启动时间
OnStartupSec 相对于 systemd 启动时间
OnActiveSec 相对于定时器本身激活的时间
OnUnitActiveSec 相对于目标 unit 上次激活的时间
OnUnitInactiveSec 相对于目标 unit 上次停止的时间
OnCalendar 绝对日历时间(类似 cron,但语法更强大)

实际示例:每日清理临时文件
# /usr/lib/systemd/system/systemd-tmpfiles-clean.timer
[Unit]
Description=Daily Cleanup of Temporary Directories
[Timer]
OnBootSec=15min        # 开机后 15 分钟首次触发
OnUnitActiveSec=1d     # 此后每 24 小时触发一次
# (注意:没有指定 Unit= 时,默认激活同名的 .service 文件)
# /usr/lib/systemd/system/systemd-tmpfiles-clean.service
[Unit]
Description=Cleanup of Temporary Directories
[Service]
Type=simple
ExecStart=/usr/bin/systemd-tmpfiles --clean   # 实际执行的命令
IOSchedulingClass=idle                         # 以最低 I/O 优先级运行
管理 systemd timer 的命令
# 查看所有定时器及下次触发时间
systemctl list-timers
# 手动立即运行 timer 关联的 service(调试非常方便!)
systemctl start systemd-tmpfiles-clean
# 启用并立即启动一个 timer
systemctl enable --now mytask.timer
systemd 时间表达式示例

时间表达式 含义
2017-07-04 2017 年 7 月 4 日 00:00:00
Mon 17:00:00 每周一 17:00
Mon-Wed *-*-* 12:00:00 周一到周三每天中午
*:0/10 每 10 分钟(整点起步)
weekly 每周一 00:00
monthly 每月 1 日 00:00
*-*-* 11/12:10:0 每天 11:10 和 23:10

临时定时器(transient timer)

不想写配置文件,直接用命令行创建一次性定时任务:

# 每 10 分钟拉取一次 Git 仓库
systemd-run --on-calendar '*:0/10' /bin/sh -c "cd /app && git pull"
# 查看临时 timer 的状态
systemctl list-timers run-8823.timer
# 取消并删除临时 timer
sudo systemctl stop run-8823.timer
cron vs systemd timers 对比

特性 cron systemd timer
相对时间触发 是(如"开机后 X 分钟")
绝对时间触发
配置方式 单行 crontab 两个 unit 文件
随机延迟(防止洪峰) 需要手写脚本 AccuracySec 参数内置支持
手动立即运行调试 不方便 systemctl start 即可
日志 syslog (cron facility) journald,journalctl -u mytask
复杂度 简单 较复杂

实际上,系统中两种方案往往共存(软件包随意选择),因此管理员必须同时了解两套系统。

定时任务的常见用途


用途 示例
发送定期报告邮件 每月 25 日发 TPS 报告提醒
清理临时文件和 core dump 每天删除 /tmp 下超过 7 天的文件
日志轮转(log rotation) 按大小或日期切割日志文件
数据库维护 每周日凌晨运行 MySQL optimize
批量数据处理(ETL) 队列消息的定时处理和导入数据仓库
系统备份 每周全量、每晚增量,凌晨低峰期运行
目录镜像同步 定时运行 rsync 保持镜像最新

整体流程图

shell 调用 fork

exec 加载新程序

等待 I/O

等待 NFS 等

收到 STOP 信号

正常运行

I/O 完成

收到 CONT 信号

工作完成调用 _exit

父进程调用 wait

父进程已死

用户执行命令

子进程诞生
PPID = 父进程 PID

进程正式运行

进程状态

短时睡眠 S

不可中断睡眠 D
无法被 kill

停止 T

可运行 R

僵尸状态 Z
等待父进程 wait

进程彻底消亡

被 init/systemd 收养

关键命令速查

# 进程查看
ps aux                    # 查看所有进程
ps lax                    # 详细格式(含 PPID、niceness)
top                       # 实时进程监控
htop                      # 更好用的 top(需单独安装)
pgrep sshd                # 按名字查 PID
pidof /usr/sbin/sshd      # 按完整路径查 PID
# 进程控制
kill -TERM 1234           # 礼貌请求进程退出
kill -9 1234              # 强制杀死进程(最后手段)
killall httpd             # 按名字杀死所有匹配进程
pkill -u ben              # 杀死某用户的所有进程
# 优先级调整
nice -n 10 ./longtask     # 以低优先级启动程序
sudo renice -5 1234       # 提升已运行进程的优先级
# 系统调用追踪
sudo strace -p 1234       # 附加到进程并追踪系统调用
sudo strace -f -p 1234    # 同时追踪子进程
sudo strace -e trace=file ./myprogram  # 只看文件操作
# 磁盘诊断
df -h                     # 查看各文件系统使用率
du -h /var | sort -rh | head -20  # 找出占空间最多的目录
lsof /path/to/file        # 查看哪个进程在用某文件
# cron 管理
crontab -e                # 编辑 crontab
crontab -l                # 查看 crontab
systemctl list-timers     # 查看 systemd 定时器

UNIX / Linux 文件系统 — 详细中文解读

来源:《UNIX and Linux System Administration Handbook》第 5 章

目录

  1. 文件系统是什么
  2. 路径名
  3. 文件系统的挂载与卸载
  4. 文件树的组织结构
  5. 文件类型
  6. 文件属性与权限
  7. 访问控制列表(ACL)

1. 文件系统是什么

在 UNIX/Linux 中,"文件系统"不只是存储文件的地方,它还包含:

  • 进程信息(/proc
  • 音频设备、硬件设备(/dev
  • 内核数据结构和调优参数(/sys
  • 进程间通信管道(命名管道、本地套接字)
    核心思想:程序员不想重复发明轮子,于是把各种对象都映射到文件系统的命名空间里,用统一的接口访问。

文件系统的四大组成部分

文件系统
├── 命名空间(Namespace)  ── 如何命名、如何组织层级结构
├── API                   ── 一组系统调用,用于导航和操作对象
├── 安全模型(Security)   ── 保护、隐藏、共享文件的方案
└── 实现(Implementation)── 将逻辑模型连接到硬件的软件

常见文件系统类型


类型 说明
ext4 Linux 最常用的磁盘文件系统
XFS 高性能日志文件系统,适合大文件
UFS BSD 的传统文件系统
ZFS Oracle 出品,功能强大(快照、校验等)
Btrfs Linux 新一代文件系统,类似 ZFS
FAT / NTFS Windows 文件系统,Linux 也支持
NFS 网络文件系统,跨机器共享

2. 路径名

单一层级结构

UNIX 的文件系统是/(根目录)开始的单一树形层级,不像 Windows 有 C:\D:\ 等多个分区命名空间。

/(根目录)
├── etc/
│   ├── passwd
│   └── hosts
├── home/
│   └── alice/
│       └── notes.txt
└── usr/
    └── bin/
        └── ls

绝对路径 vs 相对路径


类型 说明 示例
绝对路径 从根目录 / 开始 /tmp/foo
相对路径 从当前目录开始 book4/filesystem

每个进程都有自己的"当前目录",相对路径基于它解析。

路径长度限制

  • 每个路径组件(目录名/文件名):最长 255 个字符
  • 整条路径传给内核的上限:
    Linux:  4095  字节 \text{Linux: } 4095 \text{ 字节} Linux: 4095 字节
    BSD:  1024  字节 \text{BSD: } 1024 \text{ 字节} BSD: 1024 字节
    超过限制时,需要先 cd 到中间目录,再用相对路径访问。

3. 文件系统的挂载与卸载

概念:挂载点

整个文件树由若干个"文件系统"拼接而成,拼接操作叫挂载(mount)

文件树(file tree)整体
        /
       / \
     etc  users   <── 挂载点(mount point)
              \
               (这里挂载了 /dev/sda4 上的文件系统)
               alice/
               bob/

挂载点通常是一个空目录。挂载后,该目录之前的内容暂时不可见,直到卸载才恢复。

mount 命令

# 将磁盘分区 /dev/sda4 上的文件系统挂载到 /users 目录
sudo mount /dev/sda4 /users
# 查看当前所有已挂载的文件系统(Linux 上可能有 30+ 个)
mount
# 挂载时指定选项(禁用 setuid,提高安全性)
sudo mount -o nosuid /dev/sdb1 /mnt/untrusted

/etc/fstab 文件

系统启动时自动挂载的文件系统配置写在 /etc/fstab,格式类似:

# 设备           挂载点    文件系统类型  选项        dump  fsck顺序
/dev/sda1        /         ext4         defaults    1     1
/dev/sda2        /home     ext4         defaults    1     2
tmpfs            /tmp      tmpfs        defaults    0     0

umount 命令与 fuser

# 卸载文件系统(文件系统必须没有被使用)
sudo umount /users
# 强制卸载(不推荐,尤其是日志文件系统如 XFS/ext4)
sudo umount -f /users
# 惰性卸载:从命名空间移除,但等所有文件引用关闭后再真正卸载
sudo umount -l /users

卸载失败时,先用 fuser 找出谁在用这个文件系统:

# 列出所有正在使用 /usr/home 的进程 PID
fuser -c /usr/home
# 输出示例:
# /usr/home:  15897c  87787c  67124x
# Linux 上用 -v 看更详细的信息(含命令名)
fuser -cv /usr
# 根据 PID 查看具体是什么进程
ps up "87787 11201"
# 更详细的工具:lsof(list open files)
lsof /path/to/file

fuser 字母码 含义
c 进程的当前工作目录在该文件系统上
x 正在执行该文件系统上的程序
r 根目录在该文件系统上

4. 文件树的组织结构

UNIX 的目录组织历史上比较混乱,各种文件随机散布。建议不要轻易改动默认布局,因为文件树存在大量隐藏依赖关系。

标准目录速查


路径 内容
/bin 核心系统命令(ls、cp、mv 等)
/boot 引导加载程序、内核及其所需文件
/dev 设备文件(磁盘、终端、伪终端等)
/etc 系统关键配置文件
/home 用户家目录
/lib / /lib64 库文件,/bin/sbin 依赖的库
/media 可移动媒体的挂载点
/mnt 临时挂载点
/proc 所有运行进程的信息(伪文件系统)
/root root 用户的家目录
/run 运行中程序的会合点(PID 文件、套接字等)
/sbin 系统管理命令(与 /bin 区别已基本消失)
/sys 各种内核接口(Linux)
/tmp 临时文件,重启后可能消失
/usr 次级文件层级,大多数标准程序在此
/usr/bin 大多数用户命令
/usr/lib 标准程序的库和支持文件
/usr/local 本地安装的软件或配置
/usr/share/man 在线手册页
/var 会增长或变化的数据
/var/log 系统日志文件
/var/spool 打印、邮件等的队列目录
/var/tmp 临时文件(重启后保留)

为什么要单独分区

常见的分区策略:把 /var(日志容易爆满)、/tmp、用户家目录单独放到独立分区,防止一个区域的数据填满导致整个系统崩溃。

5. 文件类型

UNIX 定义了 7 种文件类型,任何新事物加入文件树都必须伪装成其中一种。

UNIX 文件类型

普通文件
Regular file -

目录
Directory d

字符设备文件
Character device c

块设备文件
Block device b

本地域套接字
Local domain socket s

命名管道 FIFO
Named pipe p

符号链接
Symbolic link l

查看文件类型

# file 命令:识别文件类型(还能识别文件内容格式)
file /usr/include       # 输出: directory
file /bin/sh            # 输出: ELF 64-bit LSB executable...
# ls -l:第一个字符是类型代码
ls -ld /usr/include
# drwxr-xr-x  27 root  root  4096 Jul 15 20:57 /usr/include
# ^ 这个 d 就是"目录"类型的代码

各类型创建与删除方式


文件类型 符号 创建方式 删除方式
普通文件 - 编辑器、cp 等 rm
目录 d mkdir rmdirrm -r
字符设备 c mknod rm
块设备 b mknod rm
本地域套接字 s socket() 系统调用 rm
命名管道 p mknod rm
符号链接 l ln -s rm

普通文件

就是最常见的文件——文本文件、数据文件、可执行程序、共享库等,文件系统对其内容没有任何结构要求,就是一串字节。

目录

目录是对其他文件的"命名引用"集合。

  • 特殊条目 .:指向目录自身
  • 特殊条目 ..:指向父目录
  • 根目录的 .. 等于 .(都指向 /
mkdir mydir        # 创建目录
rmdir mydir        # 删除空目录
rm -r mydir        # 递归删除目录及其所有内容

硬链接(Hard Links)

文件名存储在父目录里,而不是文件本身。一个文件可以有多个名字(硬链接),这些名字对文件系统来说完全等价。

目录 /usr/bin/
├── gzip    ──┐
├── gunzip  ──┤  都指向同一个 inode(同一份数据)
├── gzcat   ──┤
└── zcat    ──┘
$ ls -l /usr/bin/gzip
-rwxr-xr-x  4 root  wheel  37432 Nov 11 2016 /usr/bin/gzip
              ^
              链接计数 = 4,说明有 4 个硬链接指向这个文件
# 创建硬链接(语法和 cp 镜像)
ln oldfile newfile        # cp 是复制内容,ln 是增加名字
# 查看 inode 编号(相同 inode = 硬链接到同一文件)
ls -li /usr/bin/gzip /usr/bin/gunzip

硬链接的限制:

  • 不能跨越文件系统边界
  • 不能链接到目录(会造成文件系统循环)

符号链接(Symbolic Links)

符号链接(软链接)存储的是一个路径字符串,内核遇到它时会重定向到那个路径。

/var/data/secure  -->  archived/secure(相对路径)
                         |
                         实际文件:/var/data/archived/secure
# 创建符号链接(使用相对路径)
sudo ln -s archived/secure /var/data/secure
# 查看符号链接
ls -l /var/data/secure
# lrwxrwxrwx 1 root root 18 Aug 3 12:54 /var/data/secure -> archived/secure
# ^ 第一个字符 l 表示符号链接
# 删除符号链接
rm /var/data/secure

符号链接的权限(lrwxrwxrwx)是假的,实际权限由链接目标的权限决定。

硬链接 vs 符号链接对比


特性 硬链接 符号链接
能否跨文件系统
能否链接目录 否(通常)
目标删除后 数据仍存在 链接失效(悬空链接)
影响链接计数
独立于文件的权限 无(共享权限) 有(但是假的)

设备文件

设备文件让程序与硬件通信。设备文件本身不是驱动,只是"会合点"。

程序 open("/dev/sda", ...) 
        |
        v
文件系统把请求转发给 主设备号(major)对应的驱动
        |
        v
驱动根据 次设备号(minor)决定操作哪个物理单元
$ ls -l /dev/tty0
crw--w----. 1 root tty 4, 0 Aug 3 15:12 /dev/tty0
                        ^  ^
                   主设备号  次设备号
# 主设备号 4 = 终端驱动
# 次设备号 0 = 第一个虚拟控制台
  • 字符设备(c):按字节流读写,如终端、串口
  • 块设备(b):按块(扇区)读写,如磁盘(FreeBSD 已废弃块设备)
    /dev 目录现在是一个特殊文件系统,由内核和用户层守护进程自动维护,不再需要手动 mknod

本地域套接字与命名管道

两者都用于同一主机上进程间的通信:

类型 符号 创建 删除 用途
本地域套接字 s socket() rm syslog、X Window、数据库等
命名管道(FIFO) p mknod rm 先进先出的进程间通信

两者功能相近,都是历史遗留。如果今天重新设计,可能只会保留网络套接字。

删除奇怪文件名的技巧

# 删除以 - 开头的文件(不能直接 rm -f,会被当作选项)
rm -- -f            # -- 告诉 rm 后面的都是文件名
rm ./-f             # 或者用相对路径
# 删除含控制字符的文件(用 shell 通配符 + 交互确认)
rm -i foo*          # -i 让 rm 逐个确认,防止误删
# 查看文件名中的控制字符(以八进制显示)
ls -b

6. 文件属性与权限

权限位的结构

每个文件有 12 个模式位(mode bits),分两部分:

 特殊位(3位)    所有者权限(3位)  组权限(3位)  其他人权限(3位)
 setuid setgid sticky   r w x         r w x          r w x
   4000  2000  1000   400 200 100    40  20  10      4   2   1

用八进制表示非常方便,因为每个八进制数字恰好对应 3 个二进制位。

ls -l 输出解读

$ ls -l /usr/bin/gzip
-rwxr-xr-x  4 root  wheel  37432 Nov 11 2016 /usr/bin/gzip

逐字段解读:

-  rwx  r-x  r-x    4    root   wheel   37432   Nov 11 2016  /usr/bin/gzip
|   |    |    |      |     |      |        |         |            |
|   |    |    |      |     |      |        |         |            +-- 文件名
|   |    |    |      |     |      |        |         +-------------- 最后修改时间
|   |    |    |      |     |      |        +------------------------ 文件大小(字节)
|   |    |    |      |     |      +--------------------------------- 组所有者
|   |    |    |      |     +---------------------------------------- 所有者
|   |    |    |      +---------------------------------------------- 链接计数(4个硬链接)
|   |    |    +----------------------------------------------------- 其他人权限:r-x(读+执行)
|   |    +---------------------------------------------------------- 组权限:r-x(读+执行)
|   +--------------------------------------------------------------- 所有者权限:rwx(全部)
+------------------------------------------------------------------- 文件类型:-(普通文件)

权限位的含义

对于普通文件:

权限位 含义
r(读) 可以打开并读取文件内容
w(写) 可以修改或截断文件内容(删除/重命名文件由父目录权限控制)
x(执行) 可以执行这个文件(二进制程序或脚本)

对于目录:

权限位 含义
r(读) 可以列出目录内容(ls)
x(执行/搜索) 可以进入目录或通过它访问子项(cd)
r+x 可以列出内容并访问其中文件
w+x 可以在目录中创建、删除、重命名文件

八进制权限速查表

rwx = 4 + 2 + 1 = 7 \text{rwx} = 4+2+1 = 7 rwx=4+2+1=7
r-x = 4 + 0 + 1 = 5 \text{r-x} = 4+0+1 = 5 r-x=4+0+1=5
r– = 4 + 0 + 0 = 4 \text{r--} = 4+0+0 = 4 r–=4+0+0=4

八进制 二进制 权限
0 000 ---
1 001 --x
2 010 -w-
3 011 -wx
4 100 r--
5 101 r-x
6 110 rw-
7 111 rwx

chmod:修改权限

# 八进制语法:给所有者全权限,其他人仅执行
chmod 711 myprog
# 7=rwx(所有者),1=--x(组),1=--x(其他人)
# 八进制语法:设置 setuid 位(4 在最前面)
chmod 4755 myprog
# 4=setuid,7=rwx,5=r-x,5=r-x
# 助记符语法示例
chmod u+w file          # 给所有者加写权限
chmod ug=rw,o=r file    # 所有者和组:读写;其他人:只读
chmod a-x file          # 所有人去掉执行权限
chmod -R g+w mydir      # 递归给整个目录加组写权限
# 只对普通文件去掉执行位(不影响目录的执行位)
find mydir -type f -exec chmod a-x {} ';'
# 从另一个文件复制权限(Linux)
chmod --reference=filea fileb

助记符记忆法:用 “Hugo” 记住顺序:user(所有者)、group(组)、other(其他人)。

特殊权限位

setuid 位(4000):可执行文件运行时,以文件所有者的身份执行(而非运行者)。

  • ls 中显示:-rwsr-xr-x(所有者执行位变成 s
  • 若 execute 位未设但 setuid 设了:显示大写 S
    setgid 位(2000) 在目录上的作用:目录内新建文件继承目录的组,而不是创建者的默认组。便于多人共享目录。
    sticky 位(1000) 在目录上的作用:只有文件所有者、目录所有者或 root 才能删除或重命名文件(即使对目录有写权限也不行)。
  • 典型应用:/tmp 目录。
  • ls 中显示:最后一个执行位变成 t(或 T)。

chown 和 chgrp:修改所有权

# 修改文件所有者(需要 root)
sudo chown matt file.txt
# 修改文件所属组
sudo chgrp staff file.txt
# 同时修改所有者和组(推荐写法)
sudo chown matt:staff file.txt
# 递归修改目录下所有文件
sudo chown -R matt:staff ~matt/restore
# 注意:不要用 ~matt/.* 匹配 dot 文件,会意外匹配 .. 导致修改父目录

umask:默认权限

umask 是一个"权限屏蔽值",从新建文件的权限中减去对应的位。
实际权限 = 程序请求的权限 − umask 屏蔽的权限 \text{实际权限} = \text{程序请求的权限} - \text{umask 屏蔽的权限} 实际权限=程序请求的权限umask 屏蔽的权限

# 查看当前 umask
umask
# 输出:022
# 设置 umask
umask 027
# 027 的含义:
# 0 = 所有者不屏蔽任何权限(rwx 全保留)
# 2 = 屏蔽组的写权限(rw- 变 r-x)
# 7 = 屏蔽其他人的所有权限(--- 全屏蔽)

常见 umask 值对比:

umask 新建文件权限 新建目录权限 说明
022 644(rw-r–r–) 755(rwxr-xr-x) 最常见的默认值
027 640(rw-r-----) 750(rwxr-x—) 禁止其他人访问
077 600(rw-------) 700(rwx------) 完全私有

Linux 特有的文件属性标志

lsattr 查看,chattr 修改:

# 查看文件的特殊属性
lsattr myfile
# 设置不可删除、不可修改(只有 root 能设)
sudo chattr +i myfile    # i = immutable(不可变)
# 设置只追加模式(日志文件常用)
sudo chattr +a myfile    # a = append-only(只能追加)
# 去掉不可变标志
sudo chattr -i myfile

标志 含义 支持的文件系统
A 不更新访问时间(提升性能) XFS、Btrfs、ext
a 只允许追加写入 Btrfs、ext
i 不可变,不可删除(root 才能设置) XFS、Btrfs、ext
d 备份工具跳过此文件 Btrfs、ext
S 强制同步写入(不缓冲) XFS、Btrfs、ext

警告:i(不可变)标志有时被管理员用来阻止配置管理系统(如 Ansible)的修改。这会造成极大混乱。永远不要这样做——在配置管理系统内部解决问题。

7. 访问控制列表(ACL)

为什么需要 ACL

传统 9 位权限(所有者/组/其他人)有一个根本局限:只能设置一个组的权限。如果需要给多个不同用户或组设置不同的权限,传统模型无能为力。
ACL(Access Control List,访问控制列表)允许为任意多个用户和组分别指定权限。

重要警告:ACL 很复杂,且经常导致意想不到的问题。只在真正需要时才使用,大多数管理需求传统权限模型完全够用。

ACL 的两大标准


标准 来源 Linux 支持 FreeBSD 支持 特点
POSIX ACL 1990 年代草案 是(主要) 扩展 rwx,相对简单
NFSv4 ACL NFSv4 标准 仅网络层 是(本地文件) 超集,兼容 Windows,更复杂

POSIX ACL

基本操作
# 查看文件的 ACL
getfacl myfile
# 修改/添加 ACL 条目(-m = modify)
setfacl -m user:trent:rw myfile       # 给用户 trent 读写权限
setfacl -m group:admin:rw myfile      # 给组 admin 读写权限
setfacl -m other::-- myfile           # 其他人:无权限
# 一次设置多个条目
setfacl -m user::r,user:trent:rw,group:admin:rw myfile
# 删除某个条目
setfacl -x user:trent myfile
# 完全删除 ACL,恢复传统权限
setfacl -bn myfile
ACL 条目格式

格式 示例 作用对象
user::perms user::rw 文件所有者
user:username:perms user:trent:rw 指定用户
group::perms group::r-x 文件所属组
group:groupname:perms group:staff:rw- 指定组
other::perms other::-- 所有其他人
mask::perms mask::rwx ACL 权限的上限(掩码)

ACL 与传统权限的一致性

传统权限和 ACL 自动保持一致,但有一个重要的**掩码(mask)**机制:
当 ACL 变复杂(包含具名用户或具名组条目)时,传统权限中的"组"位对应的是 mask,而不是 group:: 条目

# 完整示例流程
$ touch example
$ ls -l example
-rw-rw-r--  1 garth  garth  0 Jun 14 15:57 example
$ getfacl example
# owner: garth
# group: garth
user::rw       # 所有者:读写
group::rw      # 所属组:读写
other::r-      # 其他人:只读
# 添加具名用户和组后,自动生成 mask
$ setfacl -m user::r,user:trent:rw,group:admin:rw example
$ ls -l example
-r--rw----+  1 garth  garth  0 Jun 14 15:57 example
           ^
           + 号表示有真实 ACL
$ getfacl --omit-header example
user::r-          # 所有者:只读
user:trent:rw     # trent:读写
group::r-         # 所属组:只读
group:admin:rw    # admin 组:读写
mask::rw          # 掩码:最高只能读写(自动生成)
other::--         # 其他人:无权限

掩码的作用:限制所有具名用户、具名组和默认组能获得的最高权限。类似 umask,但掩码指定允许的权限。
实际有效权限 = ACL 条目权限 ∩ mask \text{实际有效权限} = \text{ACL 条目权限} \cap \text{mask} 实际有效权限=ACL 条目权限mask

ACL 继承(POSIX)

可以在目录上设置默认 ACL,新建的文件和子目录会自动继承:

# 设置目录的默认 ACL(新建文件会继承这个权限)
setfacl -dm user:trent:rw mydir
# 或者
setfacl -m default:user:trent:rw mydir
# 注意:继承是一次性拷贝,之后修改父目录的默认 ACL 不影响已有子目录

NFSv4 ACL(FreeBSD)

NFSv4 ACL 比 POSIX ACL 更细粒度,权限种类更多,与 Windows ACL 兼容:

NFSv4 权限码速查

代码 详细名称 权限说明
r read_data 读取文件内容 / 列出目录
w write_data 写入文件 / 在目录中创建文件
x execute 执行程序
p append_data 追加数据 / 在目录中创建子目录
d delete 删除自身
D delete_child 删除目录中的子项
a read_attributes 读取文件属性
A write_attributes 写入文件属性
R read_xattr 读取扩展属性
W write_xattr 写入扩展属性
c read_acl 读取 ACL
C write_acl 写入 ACL
o write_owner 更改所有权

NFSv4 ACL 操作(FreeBSD)
# 查看 NFSv4 ACL(简短格式)
getfacl -q example
# 输出:
#    owner@:rwxp--aARWcCos:------:allow
#    group@:r-x---a-R-c--s:------:allow
# everyone@:r-x---a-R-c--s:------:allow
# 查看详细格式
getfacl -qv example
# ACL 条目格式:
# entity : permissions : inheritance_flags : type(allow/deny)
# 在位置 0 插入一条"拒绝 ben 所有权限"的条目
setfacl -a 0 user:ben:full_set::deny ben_keep_out
# 批量编辑 ACL(推荐方式)
getfacl -q file > /tmp/file.acl    # 导出
vi /tmp/file.acl                    # 编辑
setfacl -b -M /tmp/file.acl file   # 清除旧 ACL 并导入新的
# -b = 先清除现有 ACL,-M = 从文件读取新的 ACL 条目
NFSv4 ACL 继承标志

NFSv4 比 POSIX 更灵活,每个 ACE 都可以单独标记继承行为:

代码 详细名称 含义
f file_inherit 传播到新建文件
d dir_inherit 传播到新建子目录
i inherit_only 仅传播,不应用于当前目录
n no_propagate 传播到直接子目录,但子目录不再向下传播

NFSv4 访问判断流程

不同于 POSIX 的"找到第一个匹配就决定",NFSv4 按顺序遍历所有 ACE,逐步积累权限:

读取 ACL,按顺序处理每个 ACE
        |
        v
ACE 的 entity 匹配当前用户?
        |
   是 --> 这是 allow ACE? --> 将其权限加入"已授权"集合
       --> 这是 deny ACE?  --> 将其权限加入"已拒绝"集合,立即停止
        |
   否 --> 跳过
        |
遍历完所有 ACE,仍未明确拒绝的请求
        |
        v
实现通常默认拒绝(与 Windows 行为一致)

POSIX ACL vs NFSv4 ACL 对比

扩展

超集

被包含于

影响

用于

用于

传统 9 位权限

POSIX ACL

NFSv4 ACL

Windows ACL

NFS 网络文件共享

SMB/Samba 与
Windows 共享

权限体系总览

进程请求访问文件

有效 UID =
文件 UID?

用 user::
权限判断

有匹配的具
名用户 ACE?

用该 ACE 权限
AND mask 判断

有匹配的组 ACE?

用组 ACE 权限
AND mask 判断

用 other:: 权限判断

权限足够?

允许访问

拒绝访问

常用命令速查

# 文件类型和权限查看
file myfile             # 查看文件类型(及内容格式)
ls -l myfile            # 查看权限、所有者、大小等
ls -ld mydir            # 查看目录自身属性(不列目录内容)
ls -li myfile           # 查看 inode 编号(用于识别硬链接)
ls -a mydir             # 显示 dot 文件(隐藏文件)
lsattr myfile           # 查看 Linux 特有属性标志
# 权限修改
chmod 755 myfile            # 八进制方式设置权限
chmod u+x,g-w myfile        # 助记符方式修改权限
chmod -R g+w mydir          # 递归修改目录权限
chown user:group myfile     # 修改所有者和组
chown -R user:group mydir   # 递归修改
umask 022                   # 设置默认权限掩码
chattr +i myfile            # 设置不可变标志
chattr -i myfile            # 取消不可变标志
# 链接
ln oldfile newfile          # 创建硬链接
ln -s target linkname       # 创建符号链接
# ACL(POSIX)
getfacl myfile              # 查看 ACL
setfacl -m user:bob:rw myfile   # 添加/修改 ACL 条目
setfacl -x user:bob myfile      # 删除 ACL 条目
setfacl -bn myfile              # 完全删除 ACL
# 挂载相关
mount                       # 查看所有已挂载的文件系统
mount /dev/sda4 /mnt/data   # 挂载文件系统
umount /mnt/data            # 卸载文件系统
fuser -cv /mnt/data         # 查看谁在用这个文件系统
lsof /path/to/file          # 查看哪个进程打开了某文件
df -h                       # 查看各文件系统使用率

第六章:软件安装与管理 — 详细中文解析

一、总览

系统管理员(sysadmin)的日常工作中,软件的安装、配置和管理占据了大量时间。本章围绕以下核心任务展开:

  • 批量自动化安装操作系统
  • 维护自定义的操作系统配置
  • 给系统和应用打安全补丁、保持更新
  • 追踪软件许可证
  • 管理附加软件包
    “本地化”(localization)在这里不是指语言翻译,而是指把一个通用的操作系统或软件,按照你们公司/组织的安全规范、文件存放位置、网络结构等要求进行定制配置的过程。

二、操作系统安装

2.1 从本地媒介安装(基础)

Linux 发行版和 FreeBSD 的基本安装流程都很简单:

从 USB 或光盘启动 → 回答几个问题 → 配置磁盘分区 → 选择软件包 → 完成

大多数系统还支持"Live"模式——不安装到硬盘,直接在内存里运行体验。

系统 安装文档地址
Red Hat redhat.com/docs/manuals/enterprise
CentOS wiki.centos.org/Manuals/ReleaseNotes/CentOS7
Debian debian.org/releases/stable/installmanual
Ubuntu help.ubuntu.com/lts/serverguide/installation.html
FreeBSD freebsd.org/doc/handbook/bsdinstall.html

2.2 从网络安装(适合大规模部署)

当你要给几十台甚至上百台电脑装系统时,一台一台手动装是不现实的。这时候就需要网络安装
基本原理:

  • 用 DHCP 给电脑分配网络地址
  • 用 TFTP 传输启动文件
  • 用 HTTP / NFS / FTP 传输操作系统安装文件

2.3 PXE 启动详解

PXE(Preboot eXecution Environment,预启动执行环境)是 Intel 制定的标准,让电脑可以从网卡启动,不需要 U 盘或光盘。
PXE 的工作原理:
PXE 就像一个微型操作系统,烧录在网卡的 ROM 芯片里。它通过标准 API 暴露网络能力给系统 BIOS,这样一个引导程序就能引导任何支持 PXE 的电脑,无需为每种网卡单独写驱动。
完整启动流程(ASCII 图示):

+-----------+                          +-----------+
|           |--1. DHCP 广播请求------->|           |
|  客户端   |<-2. DHCP 响应(含TFTP信息)|  服务器   |
|  (网络    |--3. 通过TFTP请求启动镜像->|  (DHCP +  |
|   启动)   |<-4. 返回启动镜像和配置---|  TFTP +   |
|           |--5. 通过HTTP/NFS请求系统->|  文件服务)|
|           |<-6. 返回安装文件---------|           |
+-----------+                          +-----------+

DHCP、TFTP、文件服务三个角色可以分别部署在不同的服务器上,非常灵活。
Mermaid 流程图:

文件服务器(HTTP/NFS) TFTP服务器 DHCP服务器 客户端(网卡PXE) 文件服务器(HTTP/NFS) TFTP服务器 DHCP服务器 客户端(网卡PXE) 开始无人值守安装 广播 DHCP Discover (带PXE标志) DHCP响应 (含TFTP服务器地址和启动文件名) 下载启动引导文件 传输引导镜像和配置 请求操作系统安装文件 传输安装文件

设置 PXE 需要的软件:

  • PXELINUX(最常用):来自 SYSLINUX 套件(syslinux.org),硬件无关,不只支持 Linux,也能引导 FreeBSD、Windows
  • iPXE(ipxe.org):支持更多引导方式,包括无线网络
    DHCP 服务推荐使用 ISC DHCP 服务器,或轻量级的 Dnsmasq(同时支持 DNS、DHCP、网络启动)。

三、自动化安装工具

3.1 Kickstart(Red Hat / CentOS 专用)

Kickstart 是 Red Hat 开发的自动化安装工具,本质上是给 Red Hat 的图形安装器 Anaconda 提供一个"脚本接口"。
Kickstart 的特点:

  • 支持裸机和虚拟机
  • 可从光盘、硬盘、NFS、FTP、HTTP 安装
  • 通过一个叫 ks.cfg 的配置文件全程控制安装
    ks.cfg 文件的三个部分:

ks.cfg 配置文件

第一部分: 命令段
语言/键盘/时区/分区等

第二部分: 软件包段
%packages 指令

第三部分: 脚本段
%pre 安装前脚本
%post 安装后脚本

ks.cfg 完整示例(带详细注释):

# 使用文本模式安装(不用图形界面,避免需要人工干预)
text
# 设置安装过程中使用的语言
lang en_US
# 设置运行时支持的语言
langsupport en_US
# 键盘布局:美式键盘
keyboard us
# 时区设置:美国东部时间,--utc 表示硬件时钟使用 UTC 时间
timezone --utc America/EST
mouse
# root 密码设置
# --iscrypted 表示后面的字符串是加密后的哈希值(不是明文密码)
# 使用 openssl passwd -1 命令可以生成这个哈希值
rootpw --iscrypted $6$NaCl$X5jRlREy9DqNTCXjHp075/
# 安装完成后自动重启
reboot
# 把引导程序(bootloader)安装到主引导记录(MBR)
bootloader --location=mbr
# 执行全新安装(不是升级)
install
# 指定从哪个 HTTP 服务器获取安装文件
url --url http://installserver/redhat
# 清除所有现有分区(--initlabel 重新初始化磁盘标签)
clearpart --all --initlabel
# 创建根分区:ext3 格式,4096 MB
part / --fstype ext3 --size 4096
# 创建交换分区:1024 MB
part swap --size 1024
# 创建 /var 分区:ext3 格式,初始 1MB,--grow 表示用满剩余所有空间
# 这样不同大小的硬盘都能兼容
part /var --fstype ext3 --size 1 --grow
# 网络配置:通过 DHCP 自动获取 IP 地址
network --bootproto dhcp
# 启用密码哈希,使用 MD5 算法
auth --useshadow --enablemd5
# 关闭防火墙(生产环境慎用)
firewall --disabled
# 图形界面配置:使用 GNOME 桌面,开机自动启动图形界面,分辨率 1280x1024,色深 24 位
xconfig --defaultdesktop=GNOME --startxonboot --resolution 1280x1024 --depth 24
# ===== 软件包选择段 =====
%packages
@ Networked Workstation    # 网络工作站软件包组
@ X Window System          # X 图形系统
@ GNOME                    # GNOME 桌面环境
mylocalpackage             # 自定义的本地软件包
# ===== 安装后执行的脚本(在 chroot 环境中运行) =====
# 注意:此处不能解析主机名,建议用 IP 地址访问网络
%post
# 在这里写 shell 命令,例如配置 SSH 密钥、安装额外软件等

安全提示: root 密码务必使用 --iscrypted 加密,不要明文存放。但即使加密,所有机器共用同一个 root 密码仍然有风险,建议安装后用脚本自动修改密码。
如何让 Kickstart 找到配置文件:

  1. 启动时在 boot 提示符输入:
    boot: linux inst.ks=http:server:/path
    
  2. 使用 PXE 网络启动(更自动化)
    使用 pykickstart 批量生成配置文件(Python):
    pykickstart 是一个 Python 库,可以用代码读写 ks.cfg 文件。比如你有两个办公室、两类机器(服务器/客户端),就需要 4 份配置文件。用 pykickstart 可以从一份主配置自动生成所有变体,改动时只改主配置即可。

3.2 Preseed(Debian / Ubuntu 专用)

Preseed(预填充)是 Debian 系统的自动化安装方案,原理类似 Kickstart:提前回答安装程序会问的所有问题。
工作原理:
Debian 安装器使用一个叫 debconf 的工具来决定问什么问题、用什么默认值。给 debconf 提供一个预先写好的答案数据库,安装过程就完全自动化了。
生成 preseed.cfg 的两种方式:

# 方式一:先在一台机器上手动安装,然后导出所有 debconf 答案
sudo debconf-get-selections --installer > preseed.cfg
sudo debconf-get-selections >> preseed.cfg
# 然后把文件放到 HTTP 服务器,启动时传给内核
preseed/url=http://host/path/to/preseed

preseed.cfg 示例(带注释):

# 设置安装语言为美式英语
d-i debian-installer/locale string en_US
# 禁用键盘布局手动选择对话框(自动检测)
d-i console-setup/ask_detect boolean false
# 键盘布局代码:us(美式)
d-i console-setup/layoutcode string us
# 网络接口选择:自动选择已连接的网卡
d-i netcfg/choose_interface select auto
# 主机名和域名由 DHCP 提供,这里不强制设置
d-i netcfg/get_hostname string unassigned-hostname
d-i netcfg/get_domain string unassigned-domain
# 磁盘分区:指定安装到 /dev/sda
d-i partman-auto/disk string /dev/sda
# 分区方式:使用 LVM(逻辑卷管理)
d-i partman-auto/method string lvm
# 分区方案:atomic = 所有文件放一个分区
# 其他选项:home(单独 /home)、multi(/home /usr /var /tmp 各自独立)
d-i partman-auto/choose_recipe select atomic
# 创建用户:全名
d-i passwd/user-fullname string Daffy Duck
# 用户名
d-i passwd/username string dduck
# 加密后的用户密码(同样不建议明文存放)
d-i passwd/user-password-crypted password $6$/mkq9/$G//i6tN.
# 不加密用户主目录
d-i user-setup/encrypt-home boolean false
# 选择安装类型:ubuntu-desktop(Ubuntu 桌面版)
# 其他选项:standard、dns-server、lamp-server、kubuntu-desktop 等
tasksel tasksel/first multiselect ubuntu-desktop
# 在 MBR 中安装 GRUB 引导程序
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
# 安装完成提示(无操作,仅记录状态)
d-i finish-install/reboot_in_progress note
# 自动检测显示器
xserver-xorg xserver-xorg/autodetect_monitor boolean true

分区方案对比:

方案 说明 适用场景
atomic 所有文件放一个根分区 简单桌面、测试机
home 单独 /home 分区 多用户工作站
multi /home、/usr、/var、/tmp 各自独立 生产服务器

3.3 Cobbler(统一管理工具)

Cobbler 是一个开源的 Linux 自动部署服务器,把 DHCP、DNS、TFTP、Kickstart/Preseed 全部整合在一起,大幅减少重复配置工作。
Cobbler 的核心特性:

  • 模板(Template):不同机器类型可以共享部分配置,用"代码片段(snippet)"插入
  • 代码片段(Snippet):可复用的 shell 命令块
    Snippet 示例——自动添加 SSH 公钥:
# 这个 snippet 保存为 root_pubkey_snippet
# 功能:为 root 用户添加 SSH 公钥,实现免密登录
# 创建 .ssh 目录,权限设为 700(只有 root 可读写执行)
mkdir -p --mode=700 /root/.ssh
# 追加公钥到授权密钥文件(>> 是追加,不会覆盖原有内容)
cat >> /root/.ssh/authorized_keys << EOF
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKErzVdarNkL4bzAZotSzU/... Rooy2R6TCzc1Bt/oqUK1RlkuV
EOF
# 设置文件权限为 600(只有 root 可读写,SSH 要求这个权限)
chmod 600 /root/.ssh/authorized_keys

在 Kickstart 模板中引用这个 snippet:

%post
# 使用 SNIPPET:: 语法引用代码片段
SNIPPET::root_pubkey_snippet
# 标记 kickstart 完成
$kickstart_done

3.4 FreeBSD 自动化安装

FreeBSD 使用 bsdinstall 工具,自动化能力相对较弱,步骤较繁琐:

下载 FreeBSD ISO 镜像

解压 ISO 到本地目录

编辑 etc/resolv.conf 等配置文件

在 etc/ 目录创建 installerconfig 文件

用 mkisofs 重新打包成 ISO

刻录光盘或制作 PXE 镜像

无人值守安装

解压 ISO 的命令:

# FreeBSD 的 tar 命令支持直接读取 ISO 格式
# -x 解压,-p 保留权限,-C 指定目标目录,-f 指定文件
sudo mkdir FreeBSD
sudo tar xpCf FreeBSD FreeBSD-11.0.iso

installerconfig 文件有两个部分:

  1. 前导部分:设置安装参数(如磁盘分区方式)
  2. Shell 脚本:安装完成后执行的自定义命令

四、软件包管理

4.1 为什么需要包管理系统?

早期 UNIX/Linux 软件以 .tar.gz 压缩包形式分发,每次安装都要手动编译,既繁琐又容易出错。包管理系统的出现解决了这些问题:

问题 包管理系统的解决方案
手动编译 提供预编译二进制文件
安装失败难回滚 尽量保证安装原子性,支持回滚
依赖关系混乱 自动解析并安装依赖
配置文件被覆盖 自动备份或保留本地修改
版本管理困难 支持版本查询和升级

注意: 包的版本号和软件本身的版本号不一定一致!例如:

包名:docker-engine-1.13.0-1.el7.centos.x86_64
实际版本:docker version 显示 1.13.1

这是因为发行版维护者可能回移植了某些补丁,并调整了包的次版本号。

五、Linux 包管理系统

Linux 上有两大主流包格式:

Linux 包管理体系

RPM 体系
Red Hat / CentOS / SUSE

.deb 体系
Debian / Ubuntu

底层工具: rpm

高层工具: yum / DNF

底层工具: dpkg

高层工具: APT

5.1 rpm 命令(底层)

rpm 命令有多种"人格",通过模式选项切换:

选项 功能
-i 安装(install)
-U 升级(upgrade)
-e 卸载(erase)
-q 查询(query)
-qa 列出所有已安装的包
--whatrequires 查询哪些包依赖于指定包

实际操作示例(升级 OpenSSH):

# 第一步:尝试升级,发现依赖冲突
sudo rpm -U openssh-6.6.1p1-33.el7_2.x86_64.rpm
# 输出:
# error: failed dependencies:
# openssh = 6.6.1p1-23 is needed by openssh-clients-6.6.1p1-23
# openssh = 6.6.1p1-23 is needed by openssh-server-6.6.1p1-23
# 说明:当前其他包依赖旧版本的 openssh,直接升级会破坏它们
# 第二步:查询哪些包依赖 openssh
rpm -q --whatrequires openssh
# 输出:
# openssh-server-6.6.1p1-23.el7_2.x86_64
# openssh-clients-6.6.1p1-23.el7_2.x86_64
# 第三步:一次性升级所有相关包(rpm 会自动按依赖顺序排列)
sudo rpm -U openssh-*
# 第四步:验证安装结果
rpm -q openssh
# 输出:openssh-6.6.1p1-33.el7_3
# 注意:rpm 不会自动重启 sshd 服务,需要手动重启!
# sudo systemctl restart sshd

为什么不用 --force 强制升级?
依赖信息是为了保护系统稳定性而存在的,强制忽略可能导致其他软件出错。在远程服务器上强制升级 SSH 并导致 SSH 服务崩溃,那你就彻底失去对服务器的控制了。

5.2 dpkg 命令(底层)

dpkg 是 Debian/Ubuntu 的底层包管理工具,比 rpm 输出信息更详细。

# 查询已安装的软件(通过管道过滤)
# ii 开头表示已安装
dpkg -l | grep -i http
# 输出:
# ii  lighttpd  1.4.35-4+deb8u1  amd64  fast webserver with minimal memory footprint
# 安装/升级软件包(如果已安装旧版,会先卸载再安装新版)
sudo dpkg --install ./nvi_1.81.6-12_amd64.deb
# dpkg 会详细报告每一步操作
# 验证安装结果
dpkg -l nvi
# 输出:
# ii  nvi  1.81.6-12  4.4BSD re-implementation of vi.

六、高层包管理系统(APT 和 yum)

高层包管理系统的三大目标:

  1. 简化查找和下载软件包的工作
  2. 自动化系统更新/升级过程
  3. 自动处理包与包之间的依赖关系

6.1 核心概念

发行版(Release): 整个软件包宇宙在某个时间点的自洽快照。比如"Ubuntu 16.04 LTS"就是一个发行版,它随时间逐渐更新(主要是安全补丁),但保持整体兼容性。
组件(Component): 发行版内部的软件子集划分。Ubuntu 的组件:

组件名 说明
main Ubuntu 官方完全支持的开源软件
universe 社区维护的开源软件,不保证官方支持
multiverse 有许可证限制的非自由软件
restricted 有专有驱动等限制性软件

架构(Architecture): 硬件平台类型,如 x86_64(64位PC)、arm64(ARM芯片)等。同一发行版针对不同架构有不同的二进制包。

6.2 APT(Advanced Package Tool)

APT 是 Debian/Ubuntu 的高层包管理工具,也是目前最接近通用标准的 Linux 包管理系统。
日常常用命令:

# 刷新本地包信息缓存(从服务器获取最新包列表)
sudo apt update
# 安装或升级指定软件包
sudo apt install sudo
# 升级所有已安装的软件包到最新版本
sudo apt upgrade
# 无人值守自动升级(-y 自动回答"是")
# 警告:慎用,可能有意外
sudo apt upgrade -y
# 只下载不安装(先下载,人工审查后再决定是否安装)
sudo apt --download-only upgrade
# 下载的包保存在 /var/cache/apt/
# 清理 /var/cache/apt/ 中的过期缓存文件
sudo apt-get autoclean

升级单个安全补丁的完整流程:

# 第一步:刷新包信息
sudo apt update
# 输出:Get:1 http://http.us.debian.org stable/main Packages [824kB] ...
# 第二步:安装新版(apt 会自动处理依赖)
sudo apt install sudo
# 输出:1 packages upgraded, 0 newly installed, 0 to remove...

配置文件 /etc/apt/sources.list
这是 APT 最核心的配置文件,告诉 APT 去哪里下载包。每行格式:

类型  URL  发行版名称  [组件列表]

完整示例(Ubuntu Xenial / 16.04):

# deb = 二进制包,deb-src = 源代码包
# xenial 是 Ubuntu 16.04 的开发代号
# main restricted = 官方支持的软件(含部分受限软件)
# 核心仓库
deb http://archive.ubuntu.com/ubuntu xenial main restricted
deb-src http://archive.ubuntu.com/ubuntu xenial main restricted
# 更新仓库(包含 bug 修复包)
deb http://archive.ubuntu.com/ubuntu xenial-updates main restricted
deb-src http://archive.ubuntu.com/ubuntu xenial-updates main restricted
# 社区软件(不受 Ubuntu 官方支持,但开源)
deb http://archive.ubuntu.com/ubuntu xenial universe
deb-src http://archive.ubuntu.com/ubuntu xenial universe
# 带许可限制的非自由软件
deb http://archive.ubuntu.com/ubuntu xenial multiverse
deb-src http://archive.ubuntu.com/ubuntu xenial multiverse
# 安全更新(重要!必须包含)
deb http://security.ubuntu.com/ubuntu xenial-security main restricted
deb http://security.ubuntu.com/ubuntu xenial-security universe
deb http://security.ubuntu.com/ubuntu xenial-security multiverse

建立本地镜像仓库(内网缓存):
如果你管理大量机器,让每台机器都去外网下载包非常浪费带宽。建立内网镜像可以大幅提速并节省流量。

# 安装 apt-mirror 工具
sudo apt install apt-mirror
# 配置文件:/etc/apt/mirror.list(格式同 sources.list)
# 运行镜像同步(第一次很慢,约 40GB 数据)
sudo apt-mirror
# 默认保存到 /var/spool/apt-mirror/
# 通过 Web 服务器共享:
ln -s /var/spool/apt-mirror/us.archive.ubuntu.com/ubuntu /var/www/ubuntu
# 设置 cron 定时任务,定期同步更新
# 清理过期文件
bash /var/spool/apt-mirror/var/clean.sh

客户端只需修改 sources.list,把地址指向内网服务器即可使用本地镜像。

6.3 yum(RPM 的高层管理工具)

yum(Yellowdog Updater, Modified)是 Red Hat/CentOS 的高层包管理工具,功能与 APT 类似。

重要区别,容易踩坑!


命令 APT 的含义 yum 的含义
apt update / yum makecache 刷新包信息缓存 刷新包信息缓存(类似)
apt upgrade 升级所有已安装的包
yum update 升级所有已安装的包
apt update 刷新缓存 yum update 等于 apt upgrade!

注意:yum update 不是刷新缓存,而是直接升级所有包!这是 APT 和 yum 最大的行为差异。

# yum 常用命令示例
# 安装软件包(自动解决依赖)
yum install foo
# 更新所有包(注意:这是真正的升级,不是刷新缓存)
yum update
# 升级并处理过时包
yum upgrade
# 通配符匹配(不加通配符不支持模糊匹配)
# 更新所有 lib 开头的包(引号防止 shell 展开通配符)
yum update 'lib*'
# 使用本地缓存,不去验证网络仓库(速度更快)
yum -C install foo
# 更新本地缓存
yum makecache

yum 的配置文件是 /etc/yum.conf,可以配置多个仓库,每个仓库可以有多个 URL。
DNF(yum 的继任者):
DNF(Dandified Yum)已经成为 Fedora 的默认包管理器,将来会完全取代 yum。改进点:

  • 更好的依赖解析算法
  • 更优秀的 API 接口

七、FreeBSD 软件管理

FreeBSD 的软件分为三个层次:

FreeBSD 软件体系

基础系统 Base System
内核+核心工具,作为整体维护

二进制包 Binary Packages
用 pkg 命令管理

Ports 系统
从源代码编译安装

推荐优先级: 尽量用 pkg 安装二进制包,只有在需要自定义编译选项或没有对应二进制包时才用 ports。

7.1 基础系统的分支

FreeBSD 的源代码仓库有三个分支:

CURRENT
最新开发版
仅供开发者使用

STABLE
即将发布的新特性
保持包兼容性
适合勇于探索者

RELEASE
正式发布版
只有安全补丁
生产环境必用

# 查看当前系统的分支版本
uname -r
# 输出:11.0-RELEASE
# 获取并安装基础系统更新(只对 RELEASE 分支有效)
sudo freebsd-update fetch install
# 跨版本升级(如从 11.0 升级到 11.1)
sudo freebsd-update -r 11.1-RELEASE upgrade

7.2 pkg 命令


命令 功能说明
pkg install -y package 安装包,不询问确认
pkg backup 备份本地包数据库
pkg info 列出所有已安装的包
pkg info package 查看某个包的详细信息
pkg search -i package 搜索仓库(不区分大小写)
pkg audit -F 显示有已知安全漏洞的包
pkg which file 查询某个文件属于哪个包
pkg autoremove 删除不再需要的包
pkg delete package 卸载软件包
pkg clean -ay 清理 /var/cache/pkg/ 中的缓存
pkg update 更新本地包目录
pkg upgrade 将所有包升级到最新版本

查看需要更新的包:

# -v 详细显示,-I 对比索引,-L 只显示不是(-)最新版本的,= 代表"等于当前版本"
pkg version -vIL=
# 输出示例:
# dri-11.2.2,2    <   needs updating (index has 13.0.4,2)
# harfbuzz-1.4.1  <   needs updating (index has 1.4.2)

7.3 Ports 系统

Ports 是 FreeBSD 所有可从源码编译的软件的集合,安装在 /usr/ports/ 目录下。

# 初始化 ports 树(下载所有 ports 元数据)
portsnap fetch extract
# 更新 ports 树
portsnap fetch update
# 搜索并安装 zsh(示例)
whereis zsh
# 输出:bash: /usr/ports/shells/zsh
cd /usr/ports/shells/zsh
# make install:编译并安装
# clean:清理编译产生的临时文件
make install clean
# 用 portmaster 管理 ports(更方便)
# 安装 portmaster
cd /usr/ports/ports-mgmt/portmaster
make install clean
# 查看有更新的 ports
portmaster -L
# 更新所有 ports
portmaster -a
# 用 portmaster 安装(不需要切换目录)
portmaster shells/zsh
# 清理编译临时文件,释放磁盘空间
portmaster -c

八、软件本地化与配置管理建议

8.1 优秀本地化设计的原则

  • 普通用户不应该有 root 权限
  • 系统应该方便用户工作,而不是制造障碍
  • 如果用户绕过规定,可能是你的规定设计得不合理
  • 以用户为中心,多听用户的反馈
  • 保持本地文档更新

8.2 更新策略

灰度发布(Canary 发布):

新版本准备好

先在少量非关键机器测试

测试通过?

逐步扩大范围

回滚,修复问题

全量推送

“Canary”(金丝雀) 这个名称来源于矿工的旧习俗:把金丝雀带入矿井,如果空气中有毒气,金丝雀会先发出警报。在软件发布中,少量机器就扮演"金丝雀"的角色——先发现问题。
重要建议:

  • 永远不要在周五更新关键系统(否则你的周末就没了)
  • 永远不要让关键系统直连厂商的自动更新服务,应该先在内部镜像测试
  • 安全补丁要单独跟踪,及时应用
  • 更新前通知用户,给他们反馈的机会
  • 跨时区团队要让其他办公室参与测试(比如 Unicode 编码问题,美国办公室可能根本测不出来)

九、关键知识点总结

软件安装与管理

操作系统安装

包管理

本地化配置

手动安装

PXE 网络启动

自动化工具
Kickstart / Preseed / Cobbler

底层工具
rpm / dpkg

高层工具
APT / yum / pkg

仓库管理
本地镜像缓存

灰度发布策略

版本控制

测试流程

两个包管理体系的完整对应关系:

层次 RPM 体系 (Red Hat/CentOS) .deb 体系 (Debian/Ubuntu)
包格式 .rpm .deb
底层命令 rpm dpkg
高层命令 yum / DNF apt / apt-get
仓库配置文件 /etc/yum.conf /etc/apt/sources.list
自动安装工具 Kickstart Preseed

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐