ULSAH学习:UNIX/Linux 系统管理入门:第一章深度解析
本文对应《UNIX and Linux System Administration Handbook》第一章,用通俗语言解释每一个概念,帮助你建立系统管理员的整体认知框架。
本书定位:三合一手册
这本书同时承担三个角色:
+------------------------------------------+
| 1. 定向指南(Orientation Guide) |
| → 告诉你"整个地图长什么样" |
+------------------------------------------+
| 2. 快速参考手册(Quick Reference) |
| → 告诉你"常用命令怎么用" |
+------------------------------------------+
| 3. 企业级运维指南(Enterprise Focus) |
| → 告诉你"大规模生产环境怎么玩" |
+------------------------------------------+
类比:就像一本"城市生活百科",既有地图(定向),也有餐厅推荐(快速参考),还有如何在这座城市开公司(企业级)。
第一节:系统管理员的核心职责
系统管理员(Sysadmin)不是一个人,而是一套"责任清单"。下面用通俗方式解释每一项:
重点职责详解
1. 访问控制(Controlling Access)
就像公司门禁管理员:
- 新员工入职 → 创建账号、分配权限
- 员工离职 → 立即禁用账号(防止安全隐患)
- 忘记密码 → 重置处理
现代做法:通常通过配置管理系统或集中目录服务自动完成,而非手动操作。
2. 任务自动化(Automating Tasks)
核心思想:能让机器做的,就不要让人做
好处三连:
- 提高效率(不用每次手动操作)
- 减少人为错误(脚本不会粗心)
- 快速响应变化(触发条件满足就自动执行)
3. 监控(Monitoring)
一个真实的逻辑:
用户发现问题 → 去社交媒体吐槽 → 公司形象受损
vs
管理员提前发现 → 悄悄修好 → 用户无感知
监控任务包括:
- 确保网页服务响应速度正常
- 收集和分析日志文件
- 监控磁盘空间、内存等资源
4. 救火(Fire Fighting)
这是书中最有趣的一节。作者直接承认:帮用户解决各种奇怪问题虽然不在职责描述里,但实际占用了相当多时间。
经典例子:
- “昨天还好好的,今天就不行了!你改了什么?”
- “我把咖啡洒在键盘上了!应该用水冲洗吗?”
作者的建议(非常实用):
一张处理好的工单,比五小时的午夜调试更能体现你的价值。
第二节:建议背景知识
编辑器选择
| 编辑器 | 特点 | 建议 |
|---|---|---|
| vim | 标准、强大、高效,所有系统都有 | 强烈推荐,学习曲线陡但值得 |
| nano | 简单,有屏幕提示 | 入门可用,但别在同行面前用 |
学习 vim 的入口:在终端输入 vimtutor,有交互式教程。
编程语言选择
管理员不是开发者,但需要会写脚本。推荐三种:
| 语言 | 定位 | 特点 |
|---|---|---|
| Bash | 命令行胶水 | 所有系统默认,适合简单自动化 |
| Python | 通用脚本 | 语法清晰,库丰富,社区大 |
| Ruby | 通用脚本 | 开发者称之为"美丽的语言" |
还有一个特殊工具:expect —— 不是编程语言,而是用来驱动交互式程序的前端工具。比如自动化需要手动输入密码的 SSH 登录。
第三节:Linux 发行版
什么是发行版?
Linux 内核(Kernel)是操作系统的核心,但光有内核不能用。发行版 = 内核 + 各种配套软件包。
类比:内核是汽车发动机,发行版是整辆车(包括车身、轮胎、方向盘等)。
主要发行版谱系
选择发行版的四个问题
在选发行版之前,要像评估商业伙伴一样评估它:
- 五年后还在吗? — 避免选到被放弃的发行版
- 安全补丁及时吗? — 安全漏洞修复速度很重要
- 社区活跃、文档充足吗? — 遇到问题能找到答案
- 出问题能联系厂商吗?多少钱? — 商业支持的成本
本书使用的四个示例系统
| 系统 | 版本 | 特点 |
|---|---|---|
| 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 通过加密和证书链验证,防止上述攻击。
第八节:托管选择
三种托管方式对比
公有云的优势
- 无需资本支出,启动成本低
- 不用安装、保护和管理硬件
- 按需调整存储、带宽、计算能力
- 数据库、负载均衡、消息队列等开箱即用
- 高可用/冗余系统更容易实现
主流云平台简评
| 云平台 | 特点 | 适合场景 |
|---|---|---|
| AWS | 市场最大,服务最全,创新最快 | 通用首选 |
| Google Cloud | 技术先进,定价友好 | 数据密集型应用 |
| DigitalOcean | 简单、高性能、面向开发者 | 中小型项目 |
AWS 市场规模:Gartner 数据显示,AWS 规模是所有竞争对手总和的10倍。
第九节:相关岗位生态
系统管理员不是孤立存在的,周围有一系列相关角色:
各岗位一句话描述
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”(自举),意思是计算机必须"靠自己把自己拉起来"——就像一个人抓住自己的鞋带把自己提起来,这听起来很荒谬,但计算机每次开机都在做这件事。
第一节:启动流程总览
完整启动链
管理员能干预哪些步骤?
大部分步骤是自动的,管理员的主要干预点只有两个:
[修改引导加载程序配置] → 决定加载哪个内核、传什么参数
[修改启动脚本/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/grub 的 GRUB_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 的缺点
- 没有依赖管理:必须手动维护启动顺序(用脚本编号 S01xxx, S02xxx…)
- 串行执行:后面的脚本必须等前面的脚本完全结束才能运行,启动慢
- 三层脚本嵌套:难以理解和维护
- 配置分散:改个启动服务要改多个地方
第七节: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)防止误操作。
第十节:系统无法启动时的救援策略
三种策略(按推荐顺序)
单用户模式(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):
总结:第二章核心知识点
一句话总结:计算机启动是一个接力赛,固件找到引导程序,引导程序加载内核,内核启动 systemd,systemd 按依赖顺序启动所有服务,系统就绪。
UNIX / Linux 访问控制与超级用户权力 — 详细中文解读
来源:《UNIX and Linux System Administration Handbook》第 3 章
目录
- 什么是访问控制
- 标准 UNIX 访问控制模型
- 文件系统访问控制
- 进程所有权
- root 账户
- Setuid 与 Setgid
- root 账户的管理
- su 命令
- sudo 命令
- 标准模型的缺陷与扩展
- 现代访问控制系统
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 存在以下问题:
- 没有操作记录:不知道以 root 身份做了什么
- 无法追责:多人共用 root 时不知道谁做了什么、何时做的
- 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 被遗留无人看管的风险更小 | 操作完即结束 |
| 单一配置文件管控整个网络 | 方便统一管理 |
缺点:
- 任何 sudo 用户的账户被攻破,等于 root 被攻破
- 日志记录可被绕过(如 shell escape、
sudo sh、sudo 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)
访问决策流程:
- 查出当前用户的所有角色
- 检查这些角色是否包含所需权限
- 有 → 允许;无 → 拒绝
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 的很多复杂性。
整体架构图
sudo 权限判断流程图
关键概念速查
| 术语 | 全称 | 一句话解释 |
|---|---|---|
| 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 章
目录
- 什么是进程
- 进程的组成部分
- 进程的生命周期
- 信号(Signals)
- ps:监控进程
- top:实时监控
- nice 与 renice:调度优先级
- /proc 文件系统
- strace / truss:系统调用追踪
- 失控进程的处理
- 定时任务
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 没有"从零创建新进程"的系统调用。新进程的创建分两步:
- 现有进程**克隆(fork)**自身,产生子进程
- 子进程再**替换(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: −20≤niceness≤+19
FreeBSD: − 20 ≤ niceness ≤ + 20 \text{FreeBSD: } -20 \leq \text{niceness} \leq +20 FreeBSD: −20≤niceness≤+20 - 高 niceness(正数)= 低优先级 = 对其他进程"很友好"
- 低 niceness(负数)= 高优先级 = 对其他进程"不友好"
控制终端(Control Terminal)
大多数非守护进程都有一个关联的控制终端,它决定了: - 标准输入(stdin)、标准输出(stdout)、标准错误(stderr)的默认连接
- 键盘事件(如
Ctrl+C)产生的信号发送给哪个进程
3. 进程的生命周期
fork 和 exec
fork 的独特之处:它返回两个不同的值:
- 子进程视角:返回
0 - 父进程视角:返回新创建子进程的 PID
两个进程必须检查返回值来判断自己扮演哪个角色。
init / systemd(进程 1)
系统启动时,内核自主创建几个进程,其中最重要的是:
PID 1 = 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 的 ps、top 等工具的数据都来自 /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(周一到周五) |
*/n 或 a-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
注意:
weekday和dom(月中日期)字段同时指定时,满足其中任意一个就会执行,而不是同时满足。例如
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 保持镜像最新 |
整体流程图
关键命令速查
# 进程查看
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. 文件系统是什么
在 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 种文件类型,任何新事物加入文件树都必须伪装成其中一种。
查看文件类型
# 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 |
rmdir 或 rm -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 对比
权限体系总览
常用命令速查
# 文件类型和权限查看
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 流程图:
设置 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 完整示例(带详细注释):
# 使用文本模式安装(不用图形界面,避免需要人工干预)
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 找到配置文件:
- 启动时在 boot 提示符输入:
boot: linux inst.ks=http:server:/path - 使用 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 工具,自动化能力相对较弱,步骤较繁琐:
解压 ISO 的命令:
# FreeBSD 的 tar 命令支持直接读取 ISO 格式
# -x 解压,-p 保留权限,-C 指定目标目录,-f 指定文件
sudo mkdir FreeBSD
sudo tar xpCf FreeBSD FreeBSD-11.0.iso
installerconfig 文件有两个部分:
- 前导部分:设置安装参数(如磁盘分区方式)
- Shell 脚本:安装完成后执行的自定义命令
四、软件包管理
4.1 为什么需要包管理系统?
早期 UNIX/Linux 软件以 .tar.gz 压缩包形式分发,每次安装都要手动编译,既繁琐又容易出错。包管理系统的出现解决了这些问题:
| 问题 | 包管理系统的解决方案 |
|---|---|
| 手动编译 | 提供预编译二进制文件 |
| 安装失败难回滚 | 尽量保证安装原子性,支持回滚 |
| 依赖关系混乱 | 自动解析并安装依赖 |
| 配置文件被覆盖 | 自动备份或保留本地修改 |
| 版本管理困难 | 支持版本查询和升级 |
注意: 包的版本号和软件本身的版本号不一定一致!例如:
包名:docker-engine-1.13.0-1.el7.centos.x86_64 实际版本:docker version 显示 1.13.1这是因为发行版维护者可能回移植了某些补丁,并调整了包的次版本号。
五、Linux 包管理系统
Linux 上有两大主流包格式:
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)
高层包管理系统的三大目标:
- 简化查找和下载软件包的工作
- 自动化系统更新/升级过程
- 自动处理包与包之间的依赖关系
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 的软件分为三个层次:
推荐优先级: 尽量用 pkg 安装二进制包,只有在需要自定义编译选项或没有对应二进制包时才用 ports。
7.1 基础系统的分支
FreeBSD 的源代码仓库有三个分支:
# 查看当前系统的分支版本
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 编码问题,美国办公室可能根本测不出来)
九、关键知识点总结
两个包管理体系的完整对应关系:
| 层次 | 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 |
更多推荐




所有评论(0)