我的个人博客

其架构

互联网
   │
   ▼
lutiehao.xyz
   │
   ▼
腾讯 DNS / 以后可换 Cloudflare
   │
   ▼
腾讯云 Rocky Linux 9.x
   │
   ▼
Nginx :80 / :443
   │
   ▼
PHP-FPM
   │
   ▼
Typecho
   │
   ▼
MariaDB

这里的LNMP就是:

L = Linux            Rocky Linux
N = Nginx            Web服务器
M = MariaDB                数据库
P = PHP                动态程序运行环境

Typecho是运行在这套LNMP上的应用。

提供个人博客的页面模版等。

这次让Nginx和PHP-FPM使用Rocky默认的Unix Socket

/run/php-fpm/www.sock

Rocky官方文档本身就是把这个socket作为默认配置;同机运行Nginx与PHP-FPM时使用Unix socket也省掉了一层TCP/IP通信。

整个项目分为9个阶段

个人博客部署 —— Rocky Linux + LNMP + Typecho

0. 环境检查
1. Rocky Linux 基础初始化
2. 安装和配置 Nginx
3. 安装和配置 PHP / PHP-FPM
4. 安装和配置 MariaDB
5. 创建 Typecho 数据库和用户
6. 部署 Typecho
7. 配置 Nginx 虚拟主机
8. 网站测试、备案后上线
9. HTTPS、安全、备份、Cloudflare

第 0 步:确认快照恢复后的服务器状态

① 确认操作系统

cat /etc/os-release

这个告诉我们:

Rocky Linux ?
VERSION_ID=?

image-20260811194035881

② 看内核

uname -r

截屏2026-08-11 19.41.51

③ 看服务器资源

free -h

这是看内存。

然后:

df -hT /

这是看根文件系统磁盘空间。

截屏2026-08-11 19.42.37

④ 检查 SELinux

getenforce

截屏2026-08-11 19.45.31

这次不为了省事执行setenforce 0。

所以需要永久修改配置文件/etc/selinux/config。

截屏2026-08-11 19.47.50

顺便补充临时修改

# 临时切换为宽容模式
setenforce 0

# 临时切换为强制模式
setenforce 1

⑤ 看 Nginx 是否还存在

rpm -q nginx

截屏2026-08-11 19.51.55

⑥ 检查 PHP

rpm -q php php-fpm

⑦ 检查 MariaDB

rpm -q mariadb-server

均未安装

⑧ 最后看有没有遗留 Nginx 配置

ls -la /etc/nginx/conf.d 2>/dev/null

无

这一阶段需要理解什么?

环境盘点/Baseline Check

真正运维时拿到一台机器,应该先知道

我在哪台机器?
什么系统?
什么版本?
资源多少?
安全机制什么状态?
已经装了什么?
有没有历史配置?

第 1 阶段:服务器基础初始化

1.1 先理解这台机器的资源

内存:1.9G
Swap:0

博客本身很轻,但以后同时运行

Nginx
PHP-FPM
MariaDB
Typecho

再加上系统服务,2GB 内存够用,但没有 Swap 时,如果某个进程突然吃满内存,Linux 只能更激进地杀进程。

所以我们先给它加一个 2GB Swap。

Swap:硬盘上的交换空间。当物理内存压力较大时,Linux 可以暂时把部分不活跃内存页移到 Swap。Swap 不能代替真正的内存,但可以提高小内存服务器面对突发内存压力时的容错能力。

1.2 创建 2GB Swap

确认确实没有

swapon --show

没有输出,即为没有

创建

fallocate -l 2G /swapfile

查看:

ls -lh /swapfile

修改权限

chmod 600 /swapfile

检查

ls -l /swapfile

截屏2026-08-11 20.08.28

为什么要修改权限为仅root用户读写?

Swap里可能包含曾经位于内存里的数据,所以不能让普通用户随意读取

格式化成 Swap

mkswap /swapfile

这里其实和格式化磁盘有点像:

普通文件
/swapfile
   ↓
mkswap
   ↓
Linux可以识别的Swap区域

然后启用:

swapon /swapfile

验证:

swapon --show

再

free -h

截屏2026-08-11 20.13.13

可以看到从Swap:0B变到Swap:2.0Gi

1.3 让 Swap 重启后依然存在

永久修改配置文件

vim /etc/fstab

在最后添加:

/swapfile swap swap defaults 0 0

截屏2026-08-11 20.16.46

重要知识

/etc/fstab是Linux用来描述:

系统启动时应该自动挂载哪些文件系统/交换空间

的配置文件。

错误修改可能导致机器启动异常

所以修改后验证

mount -a

没有报错即没有问题

1.4 检查系统时间

执行:

timedatectl

主要看Time zone

截屏2026-08-11 20.22.48

确认为Asia/Shanghai(CST,+0800)即正常

如果不是可以设置

timedatectl set-timezone Asia/Shanghai

再

timedatectl

服务器时间重要,因为后期查看大量日志,如果时区不对,排障会很难对上

1.5 检查软件源

执行:

dnf repolist

我们需要确认 Rocky 的软件源正常。

然后:

dnf makecache

它的意思不是安装软件,而是:

把软件仓库的元数据信息缓存下来。

截屏2026-08-11 20.26.12

1.6 检查网络和防火墙,但先不修改

执行:

ip addr

截屏2026-08-11 20.27.51

这里有一个很值得学习的现象:

我的腾讯公网IP是:

124.222.7.119

但ip addr不一定能看到124.222.7.119

所以后面配置时不能让PHP-FPM:

listen = 124.222.7.119:9000

云服务器经常存在:

互联网
   ↓
腾讯云公网IP
124.222.7.119
   ↓
云网络/NAT
   ↓
你的Linux虚拟网卡
   ↓
内网IP

这一点现在重新搭建以后就能真正理解了。

然后检查 firewalld:

systemctl status firewalld --no-pager

截屏2026-08-11 20.31.57

目前为关闭状态,先不管

此阶段小总结

服务器配置

Rocky Linux 9.4
2C2G
50GB
SELinux Enforcing

Swap

fallocate -l 2G /swapfile            →创建
chmod 600 /swapfile                        →修改权限
mkswap /swapfile                            →格式化
swapon /swapfile                            →检查

永久挂载

/etc/fstab

/swapfile swap swap defaults 0 0

查看

free -h            → 内存怎么样?
swapon --show      → Swap有没有启用?
timedatectl        → 时间/时区怎么样?
dnf repolist       → 软件源怎么样?
ip addr            → 网卡/IP怎么样?
systemctl status   → 服务运行了吗?

第 2 阶段:安装 Nginx

2.1 安装 Nginx

执行:

dnf install nginx -y
dnf install nginx:通过 Rocky Linux 的软件仓库安装 Nginx,并由 RPM 包管理器记录软件文件、依赖和版本信息。

Rocky官方的DNF文档就是把DNF作为系统的软件包管理方式;Nginx也可以通过软件包方式安装。

确认

rpm -q nginx

截屏2026-08-11 20.42.50

2.2 看看 Nginx 被安装到了哪里

执行

nginx -V

image-20260811204457085

再执行:

rpm -ql nginx | less

这个命令的意思:

rpm
 |
 |-q   query 查询
 |
 `-l   list files 列出文件

也就是:

“这个 nginx RPM 包到底往系统里安装了哪些东西?”

截屏2026-08-11 20.46.15

以后想知道软件安装到哪了,可以先想到:

rpm -ql 软件包名

这对RHEL/Rocky系非常实用

2.3 认识我们以后主要用到的几个目录

重点关注:

/etc/nginx/
    ↓
Nginx 配置


/etc/nginx/nginx.conf
    ↓
Nginx 主配置文件


/etc/nginx/conf.d/
    ↓
我们以后自己的站点配置


/var/log/nginx/
    ↓
Nginx 日志

网站文件这次不打算全都堆在Nginx默认欢迎页目录里

给自己的服务规划

/var/www/

├── blog/           ← 现在的个人博客
│
└── cloud/          ← 以后个人网盘再考虑

这样以后:

Nginx配置:

/etc/nginx/conf.d/blog.conf
/etc/nginx/conf.d/cloud.conf

对应

网站文件:

/var/www/blog
/var/www/cloud

网盘以后再建。

2.4 创建博客网站目录

执行:

mkdir -p /var/www/blog

查看:

ls -ld /var/www/blog

截屏2026-08-11 20.54.53

现在目录是空的。

创建第一个真正属于你的网站页面:

vim /var/www/blog/index.html

随便写点

保存并确认:

cat /var/www/blog/index.html

截屏2026-08-11 20.56.59

2.5 现在启动 Nginx

执行:

systemctl enable --now nginx

这里同时做了两件事:

启动Nginx服务

设置开机自动启动

检查

systemctl status nginx --no-pager

截屏2026-08-11 21.00.16

2.6 不急着打开浏览器,先看它到底监听没监听

执行:

ss -lntp | grep nginx

可以看到

截屏2026-08-11 21.01.50

这里

0.0.0.0:80

不是服务器IP

意思是:

Nginx 在所有 IPv4 网络接口的 TCP 80 端口上监听。

而:

127.0.0.1:80

则意味着:

只能服务器自己访问。

2.7 先在服务器内部测试 Nginx

执行:

curl -I http://127.0.0.1

-I 的意思是只看 HTTP 响应头。

截屏2026-08-11 21.03.46

由此结果,已经可以证明

Linux本机
   ↓
127.0.0.1:80
   ↓
Nginx

是通的

但现在看到的还只是Rocky自带的Nginx默认页面,不是我们/var/www/blog/index.html

因为我们还没告诉 Nginx:

“我的博客文件在 /var/www/blog。”

这正是下一步。

2.8 给自己的博客创建虚拟主机

创建:

vim /etc/nginx/conf.d/blog.conf

现在只写一个纯静态站点配置:

server {
    listen 80;

    server_name 124.222.7.119 lutiehao.xyz www.lutiehao.xyz;

    root /var/www/blog;

    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

逐行理解这个配置文件

server {}

server {
}

表示:

一个虚拟 Web 站点。

以后完全可以同时有:

blog.conf
    ↓
博客

cloud.conf
    ↓
个人网盘

monitor.conf
    ↓
Grafana

它们都由同一个 Nginx 提供服务。


listen 80;

listen 80;

意思:

这个网站接收 TCP 80 上的 HTTP 请求。

以后 HTTPS 会增加:

listen 443 ssl;

现在不碰。


server_name

server_name 124.222.7.119 lutiehao.xyz www.lutiehao.xyz;

意思:

这些 Host 请求属于这个站点。

所以目前可以用:

http://124.222.7.119
http://lutiehao.xyz
http://www.lutiehao.xyz

进入同一个博客配置。

这样我们不需要现在写:

server_name _;

也就不会再次和 Rocky 默认配置里的 _ 打架。


root

root /var/www/blog;

这就是:

网站根目录。

例如浏览器请求:

/index.html

Nginx 就会去找:

/var/www/blog/index.html

这也是上次你遇到 File not found 时一直在排查的核心概念:

URL路径
+
root
=
服务器上的实际文件

index

index index.html;

意思:

访问:

http://124.222.7.119/

而没有指定文件时,优先找:

index.html

try_files

try_files $uri $uri/ =404;

现阶段理解成:

优先寻找用户请求的真实文件或目录,找不到就返回 404。

以后 Typecho 阶段我们会把这里改成支持 Typecho 路由的写法。

现在不提前改。

2.9 每次改 Nginx 配置后,都养成这个习惯

千万别写完就直接 reload。

先:

nginx -t

我们希望看到这个结果

截屏2026-08-11 21.14.20

nginx -t 就是:

先测试配置,不真正应用。

测试成功后

systemctl reload nginx

2.10 在服务器内部测试“我们的站点”

执行:

curl http://127.0.0.1 -H 'Host: 124.222.7.119'

这里故意加了:

Host: 124.222.7.119

因为我们要告诉 Nginx:

我要访问 server_name 124.222.7.119 那个虚拟主机。

成功后,可以直接看到

截屏2026-08-11 21.17.26

这一刻意味着

curl
 ↓
Nginx
 ↓
匹配 blog.conf
 ↓
root /var/www/blog
 ↓
index.html

已经全部正常。

2.11 最后才从公网访问

先确认腾讯云轻量服务器防火墙仍然有:

TCP
80
全部 IPv4
允许

image-20260811211919792

然后浏览器访问

http://124.222.7.119

成功的话可以看到

image-20260811214647792

此阶段小结

需要能够回答

为什么输入一个 IP 可以看到网页?

因为:

IP找到服务器
→ 80找到Nginx
→ server_name找到server块
→ root找到网站目录
→ index找到index.html
→ Nginx把文件通过HTTP返回给浏览器

这样才真正理解静态Web服务

第 3 阶段:PHP + PHP-FPM

3.1 先理解 PHP 在 LNMP 里的位置

刚才的静态网页

浏览器
   ↓
Nginx
   ↓
index.html
   ↓
直接返回给浏览器

因为HTML本身不需要“运行”

但Typecho的核心文件是:

index.php
admin/*.php
...

Nginx不会执行PHP

所以动态页面的链路会变成:

浏览器
   ↓
Nginx
   ↓
发现请求的是 .php
   ↓
FastCGI
   ↓
PHP-FPM
   ↓
PHP解释器执行程序
   ↓
生成 HTML
   ↓
Nginx
   ↓
浏览器

这里特别重要:

Nginx负责接请求,PHP-FPM负责执行PHP。

以后看到:

502 Bad Gateway
File not found
PHP源码直接显示

就应该从这条链路判断问题在哪。


3.2 安装 PHP 和 PHP-FPM

执行:

dnf install php php-fpm -y

安装后执行

php -v            #查看PHP CLI版本

这里的 CLI 是:

Command Line Interface,命令行模式。

截屏2026-08-11 21.54.40

也就是说现在已经可以:

php xxx.php

在命令行执行 PHP,但 Nginx 还不能执行它。

3.3 认识 PHP-FPM 的配置,但现在不修改

先执行:

grep -E '^(user|group|listen|listen\.acl_users)' /etc/php-fpm.d/www.conf

截屏2026-08-11 21.58.25

listen

listen = /run/php-fpm/www.sock

意思是:

PHP-FPM 创建一个 Unix Socket,等待 Nginx 把 PHP 请求送进来。

它不是 TCP 端口。

listen.acl_users

listen.acl_users = apache,nginx

表示允许:

apache
nginx

这些用户访问这个 socket。

你的 Nginx worker 是 nginx 用户,所以已经处理好了权限。

我们不改它。

3.4 启动 PHP-FPM

执行:

systemctl enable --now php-fpm

检查:

systemctl status php-fpm --no-pager

截屏2026-08-11 22.01.30

3.5 验证 socket 真的存在

这一步非常重要。

执行:

ls -l /run/php-fpm/www.sock

image-20260811220320087

最前面的s表示socket文件

-rw-r--r--   普通文件
d           目录
l           软链接
s           socket

再执行

ss -lx | grep php

-x 表示查看 Unix socket。

截屏2026-08-11 22.05.22

应该能看到 PHP-FPM 在监听。

3.6 修改现有的 blog.conf

前面已经创建了

/etc/nginx/conf.d/blog.conf

以后一个博客就主要维护一个blog.conf

打开

vim /etc/nginx/conf.d/blog.conf

修改为

server {
    listen 80;

    server_name 124.222.7.119 lutiehao.xyz www.lutiehao.xyz;

    root /var/www/blog;

    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include /etc/nginx/fastcgi.conf;
        fastcgi_pass unix:/run/php-fpm/www.sock;
    }
}

3.7 一行一行理解新增内容

第一处

原来:

index index.html;

现在:

index index.php index.html;

意思是:

访问:

/

Nginx依次寻找:

index.php
   ↓ 没有

index.html
   ↓ 找到

所以你现在的静态网页不会消失。

以后 Typecho 放进来之后有 index.php,自然优先执行 Typecho。


第二处

location ~ \.php$ {

这里:

~

表示使用正则表达式匹配。

\.php$

大致表示:

以 .php 结尾的请求。

所以:

/index.php        ✅
/test.php         ✅
/abc.html         ❌
/logo.png         ❌

HTML、CSS、图片仍然直接由 Nginx 返回。


第三处

include /etc/nginx/fastcgi.conf;

这是 FastCGI 参数。

其中一个非常关键的作用就是告诉 PHP-FPM:

你应该执行服务器上的哪个 PHP 文件。

这次我们不用手写你上次出错的:

fastcgi_param SCRIPT_FILENAME ...

直接使用发行版提供好的:

/etc/nginx/fastcgi.conf

第四处

fastcgi_pass unix:/run/php-fpm/www.sock;

这是整个配置最核心的一句:

Nginx
   │
   │ 把PHP请求交过去
   ▼
/run/php-fpm/www.sock
   │
   ▼
PHP-FPM

3.8 先测试 Nginx 配置

还是我们的固定动作:

nginx -t

必须看到:

syntax is ok
test is successful

然后:

systemctl reload nginx

截屏2026-08-11 22.16.33

这条以后形成肌肉记忆:

修改
 ↓
nginx -t
 ↓
成功
 ↓
reload

3.9 创建一个最简单的 PHP 测试文件

这次不用 phpinfo()。

因为 phpinfo() 会把服务器 PHP 配置、模块、路径等大量信息暴露出来,不适合公网服务器长期存在;Rocky 官方也特别提醒测试完要删除这类页面。

我们创建:

vim /var/www/blog/test.php

只写:

<?php
echo "PHP is working!";
?>

保存。

查看:

cat /var/www/blog/test.php

截屏2026-08-11 22.19.05

3.10 先不用浏览器,服务器内部测试

执行:

curl -H 'Host: 124.222.7.119' http://127.0.0.1/test.php

截屏2026-08-11 22.20.22

这里发生了什么?

Nginx
   ↓
识别 .php
   ↓
PHP-FPM
   ↓
执行 PHP
   ↓
echo
   ↓
Nginx返回结果

LNMP里的P层已经打通

3.11 再从 Mac 浏览器测试

访问:

http://124.222.7.119/test.php

可以看到

截屏2026-08-11 22.23.23

3.12 再检查日志

即使成功,也建议看一次:

tail -20 /var/log/nginx/access.log

可以看到

image-20260811222644600

再

tail -20 /var/log/nginx/error.log

正常没有新错误

PHP-FPM 日志:

journalctl -u php-fpm -n 20 --no-pager

截屏2026-08-11 22.28.34

这一步是培养一个非常重要的习惯:

服务正常也要知道日志在哪。

3.13 最后删除测试 PHP

确认浏览器测试成功后:

rm /var/www/blog/test.php

为什么删?

因为:

测试文件
≠
生产网站需要的文件

上线服务器应该尽量减少不必要的入口。

此阶段小结

HTTP 请求 /test.php
        │
        ▼
      Nginx
        │
        │ location ~ \.php$
        ▼
 /run/php-fpm/www.sock
        │
        ▼
     PHP-FPM
        │
        ▼
 /var/www/blog/test.php
        │
        ▼
  PHP解释并执行
        │
        ▼
 "PHP is working!"
        │
        ▼
      Nginx
        │
        ▼
      浏览器

几个配置文件的职责

/etc/nginx/conf.d/blog.conf
→ Nginx如何处理博客请求

/etc/php-fpm.conf
→ PHP-FPM全局配置

/etc/php-fpm.d/www.conf
→ PHP-FPM的www进程池配置

/run/php-fpm/www.sock
→ Nginx和PHP-FPM之间的Unix Socket

特别记住:

www.sock 是运行时产生的,不是你手工创建的。

系统重启后它会重新生成。

第 4 阶段:安装和配置 MariaDB

本阶段目标

MariaDB 服务
    ↓
只允许本机使用
    ↓
创建 typecho 数据库
    ↓
创建专用 typecho 用户
    ↓
验证这个用户能正常登录数据库

4.1 先理解 LNMP里的M

现在已经完成:

L → Rocky Linux            ✅
N → Nginx                  ✅
P → PHP + PHP-FPM          ✅
M → MariaDB                ← 现在

Typecho 的页面、文章、用户等数据不是全部直接写在 PHP 文件里的,而是需要数据库保存。

以后大致是:

访问博客
   ↓
Nginx
   ↓
PHP-FPM
   ↓
Typecho
   ↓
MariaDB
   ↓
查文章/用户/配置
   ↓
Typecho生成HTML
   ↓
返回浏览器

Rocky Linux 9 可以直接通过 DNF 安装 mariadb-server,并使用 systemd 管理它。


4.2 安装 MariaDB

执行:

dnf install mariadb-server -y

安装完成先看版本:

mariadb --version

再确认 RPM:

rpm -q mariadb-server

截屏2026-08-11 22.48.54

mariadb
→ MariaDB 客户端命令

mariadb-server
→ 真正运行数据库服务的服务端软件

4.3启动数据库

执行:

systemctl enable --now mariadb

检查:

systemctl status mariadb --no-pager

截屏2026-08-11 22.51.25

4.4 看看数据库监听在哪里

执行:

ss -lntp | grep 3306

MariaDB 默认 TCP 端口是:

3306

image-20260811225333676

这里的*为通配符,*:3306可以理解为:

MariaDB 正在监听所有可用网络接口的 3306 端口。

而我的博客架构里,MariaDB只需要让本机的PHP/Typecho连接,不需要让互联网直接访问数据库。所以更适合限制成:

127.0.0.1:3306

所以现在要修改

创建一个单独的本地配置文件:

vim /etc/my.cnf.d/99-local.cnf

写入:

[mysqld]
bind-address=127.0.0.1

保存后先检查一下:

cat /etc/my.cnf.d/99-local.cnf

截屏2026-08-11 23.12.22

然后重启MariaDB

systemctl restart mariadb

确认服务没挂:

systemctl status mariadb --no-pager

截屏2026-08-11 23.14.04

再查:

ss -lntp | grep 3306

截屏2026-08-11 23.15.05

已经变为127.0.0.1:3306

此时只会监听本机回环接口

外部主机无法直接连接

腾讯云防火墙也不要添加 3306 入站规则。

这就是一个非常重要的安全原则:

需要公网访问:
80   HTTP
443  HTTPS
22   SSH

不需要公网访问:
3306 MariaDB

4.5 做 MariaDB 安全初始化

执行:

mariadb-secure-installation

如果提示命令不存在,再用:

mysql_secure_installation

很可能会遇到类似这些问题。

第一次:

Enter current password for root (enter for none):

这是刚安装好的数据库:

直接回车。

然后可能出现:

Switch to unix_socket authentication [Y/n]

选:

Y

或者直接回车接受默认 Y。

这里非常值得记笔记。

MariaDB 10.4+ 默认支持 unix_socket 身份认证,也就是说数据库可以根据当前 Linux 用户身份认证管理员;因此本机 Linux root 可以管理 MariaDB,而不必把一个数据库 root 密码暴露给应用程序。

如果接下来问:

Change the root password? [Y/n]

这套部署里可以:

n

我们使用 socket 认证管理数据库,Typecho 更不会使用 root。

接下来这些都选:

Remove anonymous users?
Y

Disallow root login remotely?
Y

Remove test database and access to it?
Y

Reload privilege tables now?
Y

这些分别是在:

删除匿名账户
禁止数据库 root 远程登录
删除测试数据库
立即重新加载权限

Rocky 官方的 MariaDB 安全初始化指南同样建议删除匿名用户、禁止 root 远程登录、删除 test 数据库并重新载入权限。

4.6 第一次进入 MariaDB

现在直接:

mariadb

如果 socket 认证正常,你作为 Linux root 应该直接进去:

Welcome to the MariaDB monitor.

MariaDB [(none)]>

截屏2026-08-11 23.21.24

这里MariaDB [(none)]>是

MariaDB SQL 命令行

从这一刻开始敲的不是Linux命令,而是 SQL。

输入

show databases;

可以看到系统自带数据库

截屏2026-08-11 23.24.52

4.7 创建属于 Typecho 的数据库

现在在:

MariaDB [(none)]>

里面执行:

CREATE DATABASE typecho
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

这里解释一下。

数据库名字:

typecho

字符集:

utf8mb4

utf8mb4 可以完整保存 Unicode 字符,包括中文以及 Emoji 等字符。

检查:

SHOW DATABASES;

截屏2026-08-11 23.28.28

4.8 不让 Typecho 使用 root

这一步特别重要。

不要以后 Typecho 安装界面这样填:

数据库用户:root

我们要创建:

Linux root
    ↓
管理整个服务器

MariaDB root
    ↓
管理整个数据库服务器

typecho 用户
    ↓
只管理 typecho 数据库

这叫:

最小权限原则

即使 Typecho 日后出现安全问题,攻击者拿到这个数据库账号,也只能操作:

typecho

而不是整个 MariaDB。


创建 Typecho 数据库用户

现在自己准备一个独立的强密码。

不要用:

  • 腾讯云 root 密码
  • SSH密码
  • 阿里云密码
  • 其他账户密码

然后在 MariaDB 中执行:

CREATE USER 'typecho'@'localhost'
IDENTIFIED BY '这里换成你自己的数据库密码';

例如结构上是:

typecho
   @
localhost

含义非常重要:

typecho 这个数据库账户只能从服务器本机连接。

不是互联网任意主机。

额外补充

在MariaDB命令行中可能会因为单双引号未闭合等原因卡在

>

中退不出来

这是是因为MariaDB认为:

你这条SQL还没输入完

所以下一行继续等待输入

遇到这种情况可以直接输入:

\c

它会取消当前这条还没执行的SQL,重新回答:

MariaDB [(none)]>

刚刚因为这个问题我重新设置了用户密码

可以进行验证是否成功创建,在MariaDB中输入

SELECT User, Host
FROM mysql.user
WHERE User='typecho';

截屏2026-08-12 00.16.30

验证创建的用户和密码是否正确,直接用密码登陆。

先从当前 MariaDB 管理会话退出:

EXIT;

然后在 Linux Shell 里执行:

mariadb -u typecho -p

出现:

Enter password:

输入设置的密码

截屏2026-08-12 00.19.36

成功登陆

然后再执行:

SELECT CURRENT_USER(), USER();

截屏2026-08-12 00.20.55

这里有两个概念

USER()
→ 你是以什么身份发起连接的

CURRENT_USER()
→ MariaDB 最终用哪个账户完成权限认证

接下来检查它是不是能访问博客数据库:

先退出到root,重新用maraidb登陆为数据库管理员,然后

USE typecho;

应该:

Database changed

然后:

SHOW TABLES;

现在还没装 Typecho,所以正常应该是:

Empty set

最后检查权限:

SHOW GRANTS FOR CURRENT_USER;

截屏2026-08-12 00.27.18

这就同时验证了四件事:

账户:typecho@localhost                ✅
密码:第二次设置的密码                        ✅
数据库:typecho                                ✅
权限:只管理 typecho.*                ✅

还可以顺手验证一下“最小权限原则”。回到 typecho 用户登录状态,执行:

CREATE DATABASE test123;

应该报类似:

ERROR 1044/1045 ... Access denied

这反而是正确结果,因为我们不希望博客账户拥有创建其他数据库的权限。

截屏2026-08-12 00.31.10

然后:

EXIT;

如果上面全部通过,MariaDB 这部分就可以彻底收工,不再修改账户配置了。

4.9用户授权

创建用户 ≠ 有权限

正确授权:

GRANT ALL PRIVILEGES
ON typecho.*
TO 'typecho'@'localhost';

含义:

拆开:

ALL PRIVILEGES

允许:

  • 创建表
  • 修改表
  • 插入数据
  • 查询数据

typecho.*

重点!

不是:

*.*

而是:

数据库名.所有表

也就是:

typecho数据库
    |
    ├── typecho_options
    ├── typecho_contents
    ├── typecho_users
    └── ...

只能管理这里。


localhost

表示:

只能本机连接:

服务器内部

Typecho程序
    |
    |
localhost
    |
MariaDB

而不是:

互联网
 |
 |
MariaDB:3306

这也是安全设计。

截屏2026-08-12 00.59.18

4.10 刷新权限

执行:

FLUSH PRIVILEGES;

作用:

让 MariaDB 重新加载权限表。

虽然 MariaDB 新版本很多授权操作会自动生效,但习惯上生产环境仍保留这个步骤。

截屏2026-08-12 01.00.53

4.11 用普通用户测试登陆

以用户身份登陆MariaDB

mariadb -u typecho -p

然后:

USE typecho;

成功:

Database changed

截屏2026-08-12 01.03.33

说明:用户➡️密码➡️权限➡️数据库 全部正常

4.12 安全检查

检查 MariaDB 监听地址

执行:

ss -lntp | grep 3306

截屏2026-08-12 01.06.08

只允许监听本机

MariaDB不会暴露公网。

此阶段小结

第四阶段:MariaDB部署与安全配置

4.1 安装MariaDB
    ✅ MariaDB 10.5.29

4.2 启动服务
    ✅ mariadb.service running

4.3 初始化安全配置
    ✅ 删除匿名用户
    ✅ 禁止root远程登录
    ✅ 删除测试数据库

4.4 创建博客数据库

    CREATE DATABASE typecho;


4.5 创建博客专用用户

    CREATE USER
    'typecho'@'localhost'
    IDENTIFIED BY '密码';


4.6 用户授权

    GRANT ALL PRIVILEGES
    ON typecho.*
    TO 'typecho'@'localhost';


4.7 权限验证

    SHOW GRANTS

    结果:
    typecho.*权限正常


4.8 数据库监听安全

    3306监听:

    127.0.0.1:3306

    只允许本机访问


第四阶段完成 ✅

第五阶段:完善PHP运行环境

5.1 安装PHP扩展

dnf install php-mysqlnd php-curl php-mbstring -y

5.2 重启 PHP-FPM

现在执行:

systemctl restart php-fpm

为什么需要?

因为:

安装扩展只是:

磁盘上出现模块文件

但是:

正在运行的 PHP-FPM

还是旧进程。

所以需要重新加载:

旧PHP-FPM
       ↓
停止
       ↓
启动
       ↓
加载新扩展

截屏2026-08-12 14.43.26

5.3 查看 PHP 模块

接着执行:

php -m | grep -E "mysqli|pdo_mysql|curl|mbstring"

截屏2026-08-12 14.45.09

正常

解释一下:

curl

PHP访问网络:

例如:

PHP
 |
 |
外部API

mbstring

中文支持:

博客一定需要。

例如:

文章:

你好,SRE学习笔记

如果没有:

可能出现乱码。

mysqli

PHP连接MariaDB的一种方式。

pdo_mysql

另一种数据库接口。

现代 PHP 应用更常用 PDO。

5.4 为什么现在还不安装 Typecho?

因为我们要保持分层排错。

现在先确认:

PHP环境
    ↓
数据库驱动
    ↓
MariaDB

没问题。

否则直接安装 Typecho:

出现:

数据库连接失败

你不知道到底:

  • 用户密码错?
  • 权限错?
  • PHP没驱动?
  • Nginx配置错?

现在一步一步验收。


所以下一步执行:

systemctl restart php-fpm

然后:

systemctl status php-fpm --no-pager

5.3 PHP-FPM通信方式确认

目标:

确定:

Nginx → PHP-FPM

怎么通信。

推荐:

Unix Socket

检查:

grep "^listen" /etc/php-fpm.d/www.conf

截屏2026-08-12 15.13.55

重点第一行listen = /run/php-fpm/www.sock没问题

5.4 创建网站目录结构

不直接使用:

/usr/share/nginx/html

而采用更接近生产环境:

/var/www

创建:

mkdir -p /var/www/blog                ->博客
mkdir -p /var/www/drive                ->网盘

5.5 配置Nginx虚拟主机

目标:

让:

lutiehao.xyz

对应博客目录。

创建:

vim /etc/nginx/conf.d/blog.conf

写:

server {

    listen 80;

    server_name lutiehao.xyz www.lutiehao.xyz;

    root /var/www/blog;

    index index.php index.html;


    location / {

        try_files $uri $uri/ /index.php?$args;

    }


    location ~ \.php$ {

        fastcgi_pass unix:/run/php-fpm/www.sock;

        fastcgi_index index.php;

        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        include fastcgi_params;

    }

}

然后检查Nginx配置

nginx -t
systemctl reload nginx 

5.6 测试PHP解析

创建:

vim /var/www/blog/index.php

写:

<?php

phpinfo();

?>

进入浏览器访问http://124.222.7.119

截屏2026-08-12 15.24.01

成功

然后删除测试文件

5.7 下载Typecho

进入博客目录:

cd /var/www/blog

下载

wget https://github.com/typecho/typecho/releases/latest/download/typecho.zip

解压缩安装包

unzip typecho.zip 

5.8 调整文件权限

让Nginx可以读取:

chown -R nginx:nginx /var/www/blog

检查

ls -ld /var/www/blog

截屏2026-08-12 15.32.26

5.9 浏览器安装Typecho

浏览器访问

http://124.222.7.119

进入安装页面

截屏2026-08-12 15.37.02

重新进入到博客目录

cd /var/www/blog

检查

ls usr

截屏2026-08-12 15.42.05

目录存在

设置权限

chown -R nginx:nginx /var/www/blog/usr/uploads        
chmod -R 755 /var/www/blog/usr/uploads

chown为改变所有者nginx,因为最后写文件的是nginx用户

chmod 755表示权限

7 5 5

所有者:
读 写 执行

其他:
读 执行

检查

ls -ld /var/www/blog/usr/uploads

截屏2026-08-12 15.46.18

PHP-FPM运行用户问题

查看:

ps aux | grep php-fpm

如果看到:

apache php-fpm: pool www

说明PHP-FPM使用apache用户。

修改:

vim /etc/php-fpm.d/www.conf

将:

user = apache
group = apache

修改为:

user = nginx
group = nginx

重启:

systemctl restart php-fpm

验证:

ps aux | grep php-fpm

应该:

nginx php-fpm: pool www

继续安装Typecho

截屏2026-08-12 15.59.21

密码填写创建用户时的密码

然后创建管理员用户,之后便能进入到控制台了

5.10最终验证

检查:

网站

124.222.7.119

打开博客。

数据库

进入:

mariadb

查看:

show databases;

截屏2026-08-12 16.08.21

有typecho正常

Linux 存储管理练习笔记

实验平台:VMware Fusion
操作系统:Rocky Linux 9.5(aarch64)
实验目录:/opt/storage_lab
实验日期:2026 年 7 月 24—25 日
说明:本文按本次实际操作顺序整理,保留了重要的错误现象、排查过程和修正方法。截图中的 UUID 只属于本次虚拟机,重新格式化后会改变。

[TOC]


一、实验环境与安全边界

1. 实验目标

本次环境用于练习:

  • 识别磁盘、分区、LVM、文件系统和挂载点
  • 使用 fdisk、parted 创建 GPT 分区
  • 创建 XFS 文件系统并设置标签
  • 临时挂载、卸载、只读重挂载
  • 理解挂载覆盖和 target is busy
  • 区分 df 与 du
  • 创建、启用和关闭 swap
  • 理解软链接、硬链接及跨文件系统限制
  • 在不修改真实 /etc/fstab 的情况下练习 UUID 挂载配置
  • 启用并设置 XFS 用户磁盘配额

2. 三块虚拟磁盘

设备容量用途安全要求
/dev/nvme0n120GRocky Linux 系统盘禁止分区、格式化或写入 swap 签名
/dev/nvme0n25GXFS、挂载、swap、链接等实验只操作明确创建的实验分区
/dev/nvme0n33Gparted 和 XFS quota 实验只操作明确创建的实验分区

判断 /dev/nvme0n1 是系统盘的依据,不是它叫“第一块盘”,而是它的下级设备承载了:

  • /boot/efi
  • /boot
  • 根文件系统 /
  • 系统原有的 [SWAP]

其中 /dev/nvme0n1p3 是 LVM 成员,内部包含:

rl-root  → /
rl-swap  → [SWAP]

初始块设备结构

/dev/nvme0n1 在整个实验中始终只查看、不修改。

3. 实验目录

/opt/storage_lab/
├── cover_test/
│   └── host_file.txt
├── links/
│   └── original.txt
├── mount_data/
├── notes/
└── quota_data/

各目录用途:

目录用途
mount_data普通挂载以及 df、du 实验
cover_test演示挂载到非空目录后的覆盖现象
links软链接和硬链接实验
notes保存安全的 fstab.practice
quota_dataXFS 用户配额实验

实验中曾恢复过快照,恢复后这些目录被手动重新创建。早期截图中出现过 mount_date 的拼写,重建后统一使用 mount_data。

4. 存储管理的基本层次

整块磁盘
   ↓ 创建分区表、划分空间
分区
   ↓ mkfs 或 mkswap
文件系统 / swap 签名
   ↓ mount 或 swapon
目录树中的挂载点 / 活动交换空间

必须区分:

  • 检测到磁盘,不代表已经有分区。
  • 有分区,不代表已经有文件系统。
  • 有文件系统,不代表已经挂载。
  • 写入 swap 签名,不代表 swap 已经启用。
  • 日常文件存储通常会经过“分区 → 文件系统 → 挂载”,但某些程序也能直接使用原始块设备,所以“必须分区”并不是绝对规则。

二、练习 1:识别磁盘、文件系统和挂载关系

目标

使用多个只读查询命令识别系统盘与两块实验盘,并理解各命令观察的是哪个层次。

1. 使用 lsblk 查看块设备树

lsblk

命令名称可以理解为 list block devices。

常见字段:

字段含义
NAME设备名称
MAJ:MIN内核使用的主、次设备号
RM是否为可移除设备
SIZE容量
RO底层块设备是否被标记为只读
TYPE设备类型
MOUNTPOINTS当前挂载位置

本次结果中的 TYPE:

  • disk:完整块设备;本实验中是 VMware 虚拟磁盘,不应笼统理解成实体物理硬盘。
  • part:普通分区。
  • lvm:LVM 逻辑卷。
  • rom:光盘设备。

使用 lsblk 识别系统盘和实验盘

结论:

  • nvme0n1 下有启动、根文件系统和 swap,所以是系统盘。
  • nvme0n2 和 nvme0n3 当时没有子分区、文件系统和挂载点,是两块空实验盘。

2. 使用 lsblk -f 查看文件系统信息

lsblk -f

-f 表示显示 filesystem 信息,不是普通文件的属主、权限或大小。

主要字段:

字段含义
FSTYPE文件系统或签名类型,例如 xfs、vfat、swap
FSVER文件系统版本
LABEL文件系统标签
UUID文件系统唯一标识
FSAVAIL可用空间
FSUSE%使用率
MOUNTPOINTS挂载点

lsblk -f 的文件系统信息

3. 自定义 lsblk 字段

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL

-o 表示只输出指定列。加入 MODEL 后可以看到设备来自 VMware:

VMware Virtual NVMe Disk

自定义 lsblk 输出字段

4. 使用 blkid 查看设备签名

blkid

blkid 适合精确查看:

  • UUID
  • LABEL
  • TYPE
  • PARTUUID
  • PARTLABEL

blkid 初始结果

UUID 与 PARTUUID 不同:

  • UUID:文件系统或 swap 签名的标识。
  • PARTUUID:分区表中分区本身的标识。

5. 使用 df -Th 查看已挂载文件系统

df -Th

参数:

  • -T:显示文件系统类型。
  • -h:用易读单位显示容量。

df -Th 初始结果

df 只显示当前已经挂载的文件系统。因此,虽然 lsblk 能看到空的 nvme0n2、nvme0n3,它们当时不会出现在 df 中。

本题结论

命令最适合观察
lsblk磁盘、分区、LVM 的树形关系和挂载点
lsblk -f文件系统类型、标签、UUID
blkid指定设备的文件系统签名和各种 ID
df -Th已挂载文件系统的容量和使用率

三、练习 2:使用 fdisk 创建 GPT 分区

目标

在 /dev/nvme0n2 上创建 GPT 分区表,并划分:

  • /dev/nvme0n2p1:2G
  • /dev/nvme0n2p2:1G
  • 剩余约 2G 暂不分配

1. 第一次操作:误建 DOS/MBR 分区表

执行:

fdisk /dev/nvme0n2

空盘进入 fdisk 后直接输入 n,工具自动创建了 DOS disklabel:

Created a new DOS disklabel
Disklabel type: dos

第一次误建 MBR 分区表

第一、第二个分区虽然成功出现,但分区表类型是 dos:

第一次得到的 MBR 分区结果

这不是本题目标。恢复快照后重新添加实验盘,并按 GPT 方式重做。

2. 正确操作:先建立 GPT 分区表

fdisk /dev/nvme0n2

交互过程:

g          创建新的 GPT 分区表

n          创建第一个分区
1          分区号;也可以直接回车接受默认值
回车       接受默认起始扇区
+2G        分区大小

n          创建第二个分区
2          分区号
回车       接受默认起始扇区
+1G        分区大小

p          在写入前预览分区表
w          写入磁盘并退出

重新创建 GPT 分区表和两个分区

参数和交互指令:

输入作用
g创建 GPT 分区表
n创建新分区
p打印当前内存中的分区方案
w把修改写入磁盘并退出
q不保存退出

fdisk 的修改通常先保存在内存中,只有 w 才正式写入。输入 p 只是预览。

3. 通知内核并验证

partprobe /dev/nvme0n2
fdisk -l /dev/nvme0n2
lsblk -o NAME,SIZE,TYPE,PTTYPE,FSTYPE

partprobe 的作用是通知内核重新读取磁盘上的分区表。

fdisk -l 显示 GPT 分区结果

使用 PTTYPE 验证 GPT

验证结果:

/dev/nvme0n2    5G   PTTYPE=gpt
├─nvme0n2p1     2G
└─nvme0n2p2     1G

两个分区的 FSTYPE 仍为空,这是正常现象:分区只划定了磁盘区域,还没有建立文件系统或 swap 签名。

GPT 与 MBR 的区别

项目MBR/DOSGPT
传统容量上限约 2TiB远大于常见单盘容量
分区结构主分区、扩展分区、逻辑分区没有 MBR 的主/扩展分区限制
分区标识类型编号等使用 GUID 类型和分区 GUID
UEFI 系统可以配合,但不是标准组合常与 UEFI 配合

安全提醒

以下命令中的设备名必须反复确认:

fdisk /dev/nvme0n2

如果误写成 /dev/nvme0n1 并保存,可能破坏系统盘分区表。


四、练习 3:创建 XFS 文件系统并设置标签

目标

在 /dev/nvme0n2p1 上建立 XFS 文件系统,并设置文件系统标签 LAB_DATA。

1. 创建 XFS 文件系统

mkfs.xfs /dev/nvme0n2p1

命令拆解:

  • mkfs:make filesystem,建立文件系统。
  • .xfs:指定创建 XFS。
  • /dev/nvme0n2p1:目标必须是实验分区 p1。

在 p1 上创建 XFS

mkfs.xfs 的输出展示了:

  • inode 大小
  • block 大小
  • allocation group 数量
  • 内部日志
  • reflink、bigtime 等 XFS 特性

这些是创建成功后的文件系统结构信息,不是错误。

mkfs 会破坏目标分区中的原有文件系统和数据。不能对 /dev/nvme0n1* 执行本实验命令,也不能把整个 /dev/nvme0n2 当成 p1 格式化。

2. 验证文件系统和 UUID

lsblk -f
blkid /dev/nvme0n2p1

创建 XFS 后的 lsblk -f

创建文件系统时自动生成了本次实验的 UUID:

d63071f7-13f7-4e86-a3a7-4d8fc7f54428

重新格式化会生成新的 UUID,因此笔记中的值不能直接照搬到另一台机器。

3. 设置文件系统标签

由于格式化时没有同时指定标签,后续使用:

xfs_admin -L LAB_DATA /dev/nvme0n2p1

验证:

lsblk -f

设置 LAB_DATA 标签后的结果

也可以在第一次格式化时直接写:

mkfs.xfs -L LAB_DATA /dev/nvme0n2p1

参数:

  • -L LAB_DATA:把 XFS 文件系统标签设为 LAB_DATA。

LABEL、PARTLABEL 和 UUID

名称所属层次说明
LABEL文件系统人工设置的可读名称
UUID文件系统创建文件系统时生成的唯一标识
PARTLABELGPT 分区表GPT 中的分区名称
PARTUUIDGPT 分区表GPT 分区的唯一标识

五、练习 4:临时挂载并验证读写

目标

把 /dev/nvme0n2p1 挂载到 /opt/storage_lab/mount_data,验证文件确实写入实验分区。

1. 挂载

mount /dev/nvme0n2p1 /opt/storage_lab/mount_data

基本格式:

mount 文件系统所在设备 挂载目录

挂载不是复制数据,而是把一个文件系统接入 Linux 的统一目录树。

2. 多角度验证

findmnt /opt/storage_lab/mount_data
lsblk -f
df -Th

重新搭建后完成临时挂载并验证

三个命令的侧重点:

  • findmnt:确认目标路径当前真正由哪个设备提供,并查看挂载选项。
  • lsblk -f:从设备树角度确认文件系统和挂载点。
  • df -Th:查看已经挂载的文件系统容量。

3. 创建并读取测试文件

cd /opt/storage_lab/mount_data
touch mount_test.txt
echo "hello world" > mount_test.txt
ls -l
cat mount_test.txt

在挂载文件系统中创建并读取文件

命令解释:

  • touch mount_test.txt:文件不存在时创建空文件。
  • >:把标准输出覆盖写入文件。
  • ls -l:查看文件属性。
  • cat:读取文件内容。

此时:

路径:/opt/storage_lab/mount_data/mount_test.txt
数据:保存在 /dev/nvme0n2p1 的 XFS 中

临时挂载的持久性

本次只执行了 mount,没有修改真实 /etc/fstab:

  • 重启后挂载关系消失。
  • 分区、XFS、标签、UUID 和文件数据仍保存在虚拟磁盘上。
  • 下次重新挂载同一文件系统后,可以再次看到磁盘中的文件。

六、练习 5:处理 target is busy

目标

理解为什么文件系统正在使用时不能正常卸载,并使用只读排查命令找出占用者。

1. 复现问题

Shell 当前位于:

/opt/storage_lab/mount_data

此时直接执行:

umount /opt/storage_lab/mount_data

出现:

target is busy

2. 查看占用者

fuser -vm /opt/storage_lab/mount_data

挂载点正忙及 fuser 排查

参数:

  • -v:详细显示。
  • -m:把目标视为一个已挂载文件系统,列出正在使用它的进程。

截图中的 bash 表示当前 Shell 正把这个文件系统里的目录作为工作目录。

3. 正确处理

cd /
umount /opt/storage_lab/mount_data

其他可能导致 busy 的情况:

  • 另一个 Shell 的当前目录位于挂载点内。
  • 某个程序打开了里面的文件。
  • 程序正在写日志或访问该文件系统。
  • 还有下级挂载点没有卸载。

规范做法是找出占用者并正常退出或关闭文件,不把 umount -l、umount -f 当作日常解决方案。


七、练习 6:理解挂载覆盖与当前目录引用

目标

观察把文件系统挂载到非空目录后,原目录内容发生什么变化。

1. 删除实验分区中的测试文件

当 /dev/nvme0n2p1 仍挂载在 mount_data 时执行了:

cd
ls /opt/storage_lab/mount_data
rm -rf /opt/storage_lab/mount_data/mount_test.txt

删除实验分区中的 mount_test.txt

这里确实删除了实验分区中的 mount_test.txt。不过删除单个普通文件更规范的写法是:

rm /opt/storage_lab/mount_data/mount_test.txt

-r 用于递归删除目录,-f 会跳过部分提示,对单个普通文件没有必要。

2. 准备被覆盖的目录

根文件系统中的目录:

/opt/storage_lab/cover_test

里面预先存在:

host_file.txt

将实验文件系统挂载到该目录:

mount /dev/nvme0n2p1 /opt/storage_lab/cover_test

正常情况下,通过路径 /opt/storage_lab/cover_test 访问时,看到的是实验文件系统的根目录;原来的 host_file.txt 被暂时遮住,但没有被删除。卸载后它会重新出现。

3. 本次出现的特殊现象

实验时 Shell 在挂载发生前已经进入了 cover_test,随后从当前目录使用:

cat host_file.txt

仍读到了原目录中的文件:

挂载覆盖时当前工作目录保留旧引用

原因:

  • Shell 在挂载前已经取得了底层目录对象的引用。
  • 在另一个文件系统覆盖该路径后,这个 Shell 的当前工作目录引用并不会自动替换。
  • 因此从旧的当前目录使用相对路径,仍可能访问被遮住的底层目录。
  • 从 / 重新使用绝对路径进入 cover_test,才会经过新的挂载关系。

更清晰的演示方法:

cd /
mount /dev/nvme0n2p1 /opt/storage_lab/cover_test
ls -la /opt/storage_lab/cover_test
umount /opt/storage_lab/cover_test
ls -la /opt/storage_lab/cover_test

结论:

  • 挂载到非空目录不会删除原文件,只会暂时遮住。
  • 卸载后原目录内容恢复可见。
  • 观察挂载覆盖前最好先 cd /,避免当前目录旧引用干扰判断。

八、练习 7:只读和读写重挂载

目标

不卸载文件系统,把已有挂载从读写模式切换为只读模式,再恢复为读写模式。

1. 切换为只读

mount -o remount,ro /opt/storage_lab/cover_test
findmnt /opt/storage_lab/cover_test

参数:

  • -o:指定挂载选项。
  • remount:修改现有挂载的选项。
  • ro:read-only,只读。

测试:

cat /opt/storage_lab/cover_test/mount_test.txt
touch /opt/storage_lab/cover_test/ro_test.txt
echo "new line" >> /opt/storage_lab/cover_test/mount_test.txt

只读重挂载及写入失败

结果:

  • 读取已有文件成功。
  • 创建新文件失败。
  • 追加写入失败。
  • 报错是 Read-only file system。

文件系统级只读状态对 root 同样有效,不能靠 root 身份正常绕过。

2. 恢复为读写

mount -o remount,rw /opt/storage_lab/cover_test
findmnt /opt/storage_lab/cover_test
touch /opt/storage_lab/cover_test/rw_test.txt
cd /
umount /opt/storage_lab/cover_test

rw 表示 read-write。

恢复读写并完成卸载

findmnt 与 lsblk 的只读含义不同

  • findmnt 的 OPTIONS 中出现 ro 或 rw,表示这次挂载的模式。
  • lsblk 的 RO=0 表示底层块设备没有被整体标记为只读。

因此 RO=0 不等于当前文件系统一定以 rw 挂载。


九、练习 8:理解 df 与 du

目标

比较文件系统整体空间与指定目录中文件占用量。

1. 未挂载时查看路径

df -Th /opt/storage_lab/mount_data
du -sh /opt/storage_lab/mount_data

未挂载时 df 显示根文件系统

此时 mount_data 只是根文件系统中的普通目录,所以:

  • df 显示 /dev/mapper/rl-root。
  • du 显示该空目录自身几乎没有占用。

df 路径 显示的是“当前承载该路径的文件系统”,并不固定显示某个设备。

2. 挂载实验文件系统后再次查看

mount /dev/nvme0n2p1 /opt/storage_lab/mount_data
df -Th /opt/storage_lab/mount_data
du -sh /opt/storage_lab/mount_data

挂载后的 df 与 du 基线

此时:

  • df 显示 /dev/nvme0n2p1,已用约 47M。
  • du 只显示约 4K。

新建 XFS 后即使目录中几乎没有普通文件,文件系统仍需要:

  • 超级块和其他元数据
  • allocation group 元数据
  • XFS 内部日志
  • inode 等结构

因此 df 的 47M 并不代表目录中有 47M 的普通文件。

3. 分配 200M 文件

cd /opt/storage_lab/mount_data
fallocate -l 200M /opt/storage_lab/mount_data/bigfile.bin
ls -lh
du -h bigfile.bin
du -sh /opt/storage_lab/mount_data
df -Th /opt/storage_lab/mount_data

参数:

  • fallocate:为文件快速预分配磁盘空间。
  • -l 200M:长度为 200MiB。
  • ls -lh:显示文件的逻辑大小。
  • du -h:显示文件实际占用的数据块。
  • du -sh:汇总目录占用。

创建 200M 文件后的 df 与 du

结果:

  • bigfile.bin 的逻辑大小为 200M。
  • du 显示文件约占 200M。
  • 整个目录约占 201M。
  • df 的文件系统已用空间从约 47M 增加到约 247M。

4. 删除并观察空间回收

本次输入了:

rm -rf /opt/storage_lab/mount_data/bigfile.bin

更规范的命令是:

rm /opt/storage_lab/mount_data/bigfile.bin

验证:

df -Th /opt/storage_lab/mount_data
du -sh /opt/storage_lab/mount_data
umount /opt/storage_lab/mount_data

删除大文件并回收空间

df 与 du 总结

命令数据来源统计范围
df文件系统空间统计整个文件系统,包含文件和元数据
du遍历目录树从指定目录能访问到的文件数据块

补充:

  • ls -lh 看文件逻辑大小。
  • du -h 看文件实际分配的数据块。
  • 本次 fallocate 实际分配了空间;如果是稀疏文件,ls 与 du 可能相差很大。

十、练习 9:创建、启用和关闭 swap

目标

把 /dev/nvme0n2p2 变成 1G swap,临时启用后再关闭,并保持系统原有 swap 不受影响。

1. 查看系统原有 swap

swapon --show
free -h

实验前系统已有的 2G swap

结果:

/dev/dm-1  partition  2G

这是系统原有的 rl-swap,本实验不能关闭或重新初始化它。

2. 修改 p2 的 GPT 分区类型

fdisk /dev/nvme0n2

需要使用:

t       修改分区类型
2       选择第二个分区
L       列出可用分区类型
q       退出类型列表分页器
19      在本机列表中选择 Linux swap
p       检查分区表
w       保存

第一次问题:输入了 p

在提示:

Partition type or alias:

输入 p 不是“打印分区表”,而是把 p 当成分区类型别名,所以结果是:

Type of partition 2 is unchanged: Linux filesystem

分区类型没有改变

同一个字母在不同提示位置可能有不同含义,必须先看清当前提示。

第二次问题:在分页器中输入 19

输入 L 后类型列表由分页器显示。屏幕底部出现类似:

:19

在类型列表分页器中输入 19

这时的 19 被分页器当作搜索内容,没有传给 fdisk。随后类型仍然没有改变:

退出后仍显示 Linux filesystem

正确操作:

  1. 在类型列表中按 q。
  2. 等屏幕重新出现 Partition type or alias:。
  3. 再输入本机列表对应的 19,也可以尝试清晰的别名 swap。
  4. 输入 p 检查,再用 w 保存。

3. 验证分区类型

partprobe /dev/nvme0n2
fdisk -l /dev/nvme0n2

p2 已变为 Linux swap 类型

此时只完成了“分区表中的类型标记”,还没有真正的 swap 签名。

4. 写入 swap 签名和标签

mkswap -L LAB_SWAP /dev/nvme0n2p2
lsblk -f
blkid /dev/nvme0n2p2

命令解释:

  • mkswap:在目标分区写入 swap 数据结构和签名。
  • -L LAB_SWAP:设置 swap 标签。
  • lsblk -f、blkid:验证 FSTYPE=swap、标签和 UUID。

创建 LAB_SWAP 并验证

本次 swap UUID:

fb96146e-638a-4e3c-b9ce-b050975b89df

mkswap 和 mkfs 一样会覆盖目标设备上的原有签名,因此设备名必须准确。

5. 临时启用实验 swap

swapon /dev/nvme0n2p2
swapon --show
free -h

启用实验 swap 后总量变成 3G

结果:

  • 系统原有 swap:2G。
  • 实验 swap:约 1G。
  • free -h 中总 swap:约 3G。

swapon --show 中的 PRIO 是优先级,数值越大优先级越高。本次系统 swap 是 -2,实验 swap 是 -3,因此系统 swap 优先级更高。

6. 只关闭实验 swap

swapoff /dev/nvme0n2p2
swapon --show
free -h

关闭实验 swap 后保留系统 swap

结果:

  • /dev/nvme0n2p2 不再活动。
  • /dev/dm-1 仍正常启用。
  • 总 swap 回到约 2G。

swapoff 只停止使用,不会擦除 LAB_SWAP 标签、UUID 或 swap 签名。

swap 的三个独立步骤

  1. 在 GPT 中把分区类型改成 Linux swap。
  2. 用 mkswap 写入实际 swap 签名。
  3. 用 swapon 激活。

前一步完成不代表后一步自动完成。

持久性

本次没有向真实 /etc/fstab 添加 swap 条目,因此:

  • swap 签名会保留。
  • 重启后 /dev/nvme0n2p2 不会因本实验自动启用。

十一、练习 10:软链接与硬链接

目标

观察 inode、链接计数、写入效果、目标改名后的行为。

1. 创建两个链接

cd /opt/storage_lab/links
ln original.txt hard_link.txt
ln -s original.txt soft_link.txt
ls -li

命令解释:

  • ln 源 新名称:创建硬链接。
  • ln -s 目标 新名称:创建软链接。
  • ls -li:显示 inode 编号和链接计数。

创建软硬链接并比较 inode

结果:

original.txt 与 hard_link.txt  inode 相同
两者链接计数均为 2
soft_link.txt                  inode 不同

软链接行显示:

soft_link.txt -> original.txt

2. 通过硬链接写入

echo "hello world" >> hard_link.txt
cat original.txt
cat soft_link.txt

通过硬链接写入后其他路径读取到同一内容

>> 表示追加,而不是覆盖。

original.txt 和 hard_link.txt 不是“一份原件、一份副本”,而是两个目录项共同指向同一个 inode,所以修改的是同一份数据。

3. 使用 readlink

readlink soft_link.txt
readlink hard_link.txt
cat hard_link.txt

readlink 对软硬链接的不同结果

结果:

  • readlink soft_link.txt 输出 original.txt,因为软链接保存目标路径。
  • readlink hard_link.txt 没有输出,因为硬链接只是普通目录项,不保存“目标路径”。

4. 重命名目标文件

mv original.txt test.txt
ls
cat hard_link.txt
cat soft_link.txt

结果:

  • hard_link.txt 仍能访问数据。
  • soft_link.txt 报 No such file or directory,成为悬空链接。

恢复名称:

mv test.txt original.txt
readlink soft_link.txt

目标改名导致软链接失效并在恢复名称后修复

软链接保存的是路径字符串。路径暂时不存在时链接失效;路径重新出现后链接又能工作。

软硬链接对比

项目硬链接软链接
inode与其他硬链接相同有自己的 inode
保存内容直接指向同一 inode保存目标路径字符串
目标改名仍有效原路径不存在时失效
跨文件系统不可以可以
链接目录通常不允许普通用户这样做可以

十二、练习 11:验证硬链接不能跨文件系统

目标

从根文件系统中的源文件向 /dev/nvme0n2p1 中创建链接,比较硬链接和软链接。

源文件:

/opt/storage_lab/links/original.txt

目标目录:

/opt/storage_lab/mount_data

其中 mount_data 当时挂载的是 /dev/nvme0n2p1,所以源和目标属于两个不同文件系统。

1. 创建跨文件系统硬链接

ln /opt/storage_lab/links/original.txt \
   /opt/storage_lab/mount_data/cross_hard.txt

结果:

Invalid cross-device link

2. 创建跨文件系统软链接

ln -s /opt/storage_lab/links/original.txt \
      /opt/storage_lab/mount_data/cross_soft.txt

ls -li /opt/storage_lab/mount_data
readlink /opt/storage_lab/mount_data/cross_soft.txt
cat /opt/storage_lab/mount_data/cross_soft.txt

跨文件系统硬链接失败而软链接成功

原理

inode 编号只在所属文件系统内部有意义。硬链接必须新增一个指向同一 inode 的目录项,因此不能跨越文件系统边界。

软链接只是保存路径字符串,所以可以从一个文件系统指向另一个文件系统。


十三、练习 12:安全练习 fstab

目标

理解 /etc/fstab 的六个字段和 UUID 挂载,但不修改真实 /etc/fstab,避免影响系统启动。

1. 查询挂载状态和 UUID

findmnt -S /dev/nvme0n2p1
blkid /dev/nvme0n2p1

查找 LAB_DATA 的 UUID

findmnt -S 按源设备查询;当设备未挂载时没有结果是正常的。

本次 UUID:

d63071f7-13f7-4e86-a3a7-4d8fc7f54428

2. 创建独立的练习文件

文件:

/opt/storage_lab/notes/fstab.practice

内容:

UUID=d63071f7-13f7-4e86-a3a7-4d8fc7f54428 /opt/storage_lab/mount_data xfs defaults,nofail 0 0

3. 让本次 mount 只读取练习文件

mount -a -T /opt/storage_lab/notes/fstab.practice
findmnt /opt/storage_lab/mount_data
lsblk -f
df -Th /opt/storage_lab/mount_data

使用练习 fstab 完成挂载和验证

参数:

  • -a:挂载配置文件中符合条件的全部条目。
  • -T 文件:使用指定的替代 fstab 文件,而不是默认读取 /etc/fstab。

验证后:

umount /opt/storage_lab/mount_data

六个字段

字段本次内容含义
1UUID=...要挂载的文件系统
2/opt/storage_lab/mount_data挂载点
3xfs文件系统类型
4defaults,nofail挂载选项
50不使用传统 dump 备份标志
60不安排传统启动时 fsck 顺序

nofail 表示设备缺失时不把该条目作为必须成功的启动条件,但它不能修复写错的 UUID、文件系统类型或挂载点。

为什么本次方法安全

  • 没有编辑真实 /etc/fstab。
  • mount -a -T 只在本次命令中读取练习文件。
  • 系统启动不会自动读取 fstab.practice。
  • 因此本题练习的是配置格式和验证方法,并没有实现永久挂载。

十四、练习 13:使用 parted 创建 GPT 分区

目标

在第二块实验盘 /dev/nvme0n3 上建立 GPT,并创建一个约 2GB 的分区,为 XFS quota 做准备。

1. 建立 GPT 分区表

parted /dev/nvme0n3 mklabel gpt
parted /dev/nvme0n3 print

2. 创建分区

parted /dev/nvme0n3 mkpart primary 1 2G
parted /dev/nvme0n3 print

使用 parted 建立 GPT 和第一个分区

命令拆解:

  • /dev/nvme0n3:目标实验盘。
  • mklabel gpt:创建 GPT 分区表。
  • mkpart:创建分区。
  • primary:在本次 GPT 下是分区名称,并不是 MBR 中受数量限制的“主分区”类别。
  • 1:起点约为十进制 1MB。
  • 2G:终点为十进制 2GB。

parted 的修改通常立即写入,不像 fdisk 那样最后统一输入 w。因此使用时要更加谨慎。

3. 使用 MiB 查看空闲空间

partprobe /dev/nvme0n3
parted /dev/nvme0n3 unit MiB print free
lsblk -o NAME,SIZE,TYPE,PTTYPE,FSTYPE,MOUNTPOINTS

使用 MiB 验证分区和剩余空间

结果:

磁盘总量:3072MiB
p1:约 1906MiB
剩余未分配:约 1165MiB
lsblk 显示 p1:1.9G

为什么命令写 2G,结果不是 2GiB:

  • parted 中这里的 G 按十进制 GB 处理。
  • 终点 2000MB 换算后约为 1907MiB。
  • 扣除起点后,分区约为 1906MiB,也就是约 1.86GiB。
  • lsblk 四舍五入显示为约 1.9G。

这不是错误。本次实验无需重做。

如果需要精确约 2GiB,可以明确使用 MiB 边界,例如:

parted /dev/nvme0n3 mkpart quota 1MiB 2049MiB

本次未使用这条命令,它只是单位换算的补充示例。

开头约 0.98MiB 的空隙来自 GPT 元数据和分区对齐,不是空间异常。


十五、练习 14:XFS 用户磁盘配额

目标

在 /dev/nvme0n3p1 上创建 XFS,挂载时开启用户配额,并给 alice 设置:

  • 软限制:50MiB
  • 硬限制:60MiB

这一部分是根据 Rocky Linux 默认使用 XFS 补充的实操,xfs_quota 的具体语法属于扩展内容。

1. 创建带标签的 XFS

mkfs.xfs -L QUOTA_LAB /dev/nvme0n3p1
blkid /dev/nvme0n3p1

创建 QUOTA_LAB 文件系统

参数:

  • mkfs.xfs:创建 XFS。
  • -L QUOTA_LAB:创建时直接设置文件系统标签。
  • /dev/nvme0n3p1:目标是分区 p1,而不是整个 nvme0n3。

结果中:

LABEL="QUOTA_LAB"
TYPE="xfs"
PARTLABEL="primary"

其中:

  • QUOTA_LAB 是 XFS 文件系统标签。
  • primary 是前面 parted mkpart primary 创建的 GPT 分区名称。
  • 它们属于不同层次。

2. 挂载时启用用户配额

mkdir -p /opt/storage_lab/quota_data
mount -o uquota /dev/nvme0n3p1 /opt/storage_lab/quota_data

参数:

  • -o uquota:挂载 XFS 时开启用户配额统计和执行。
  • uquota、usrquota 在这里是同类用户配额选项;findmnt 可能规范化显示成 usrquota。

验证:

findmnt /opt/storage_lab/quota_data
xfs_quota -x -c 'state' /opt/storage_lab/quota_data

用户配额挂载选项和状态

用户配额结果:

Accounting: ON
Enforcement: ON

含义:

  • Accounting: ON:内核正在统计各用户的使用量。
  • Enforcement: ON:达到限制时会真正执行限制。

组配额和项目配额显示 OFF 是正常的,因为本题只启用了用户配额。

状态还显示默认块宽限期为 7 天。

XFS 配额应在挂载时启用。如果已经在不带 uquota 的情况下挂载,稳妥做法是正常卸载后,再带 uquota 重新挂载,而不是假定可以随意通过 remount 开启。

3. 创建 Alice 的实验目录

mkdir -p /opt/storage_lab/quota_data/alice_data
chown alice:alice /opt/storage_lab/quota_data/alice_data
ls -ld /opt/storage_lab/quota_data/alice_data

命令解释:

  • mkdir -p:创建目录;父目录不存在时一起创建,已存在时不因此报错。
  • chown alice:alice:把属主和属组都设为 alice。
  • ls -ld:查看目录本身的详细属性,而不是列出目录内部内容。

chown 的作用只是让 Alice 有权限在该目录中写入。它不能代替 quota,也不决定 quota 的统计范围。

4. 设置 Alice 的软、硬限制

xfs_quota -x \
  -c 'limit bsoft=50m bhard=60m alice' \
  /opt/storage_lab/quota_data

命令解释:

部分含义
xfs_quotaXFS 配额管理工具
-x进入专家/管理模式
-c执行引号中的一条内部命令
limit设置限制
bsoft=50m块空间软限制为 50MiB
bhard=60m块空间硬限制为 60MiB
alice按 Alice 的 UID 设置
最后的路径指定要管理的 XFS 挂载点

这里的 b 表示块空间容量。如果限制的是 inode,则针对的是文件数量。

5. 查看配额报表

xfs_quota -x -c 'report -h -u' /opt/storage_lab/quota_data

参数:

  • report:显示配额报表。
  • -h:以易读容量单位显示。
  • -u:显示用户配额。

Alice 的 XFS 配额报表

结果:

alice  Used=0  Soft=50M  Hard=60M

说明配额限制已经保存成功。

截图中多行命令前的:

>

是 Shell 的续行提示。因为上一行末尾写了反斜杠 \,Shell 正在等待剩余命令;> 不是要手动输入的字符,也不是重定向或错误。

软限制、硬限制和宽限期

  • 软限制:可以短期超过;第一次超过后开始宽限期倒计时。
  • 宽限期:本次状态显示默认 7 天。
  • 硬限制:绝对上限,不能正常突破,没有额外宽限期。
  • 使用量降回软限制以下后,宽限计时会清除。

配额的统计范围

用户配额按 UID 统计整个 /dev/nvme0n3p1 文件系统,不是只限制:

/opt/storage_lab/quota_data/alice_data

如果 Alice 在这个 XFS 的其他目录中创建文件,同样会计入配额。alice_data 只是为了方便授权和测试。

本次完成程度

本次已经完成:

  • 以 uquota 挂载。
  • 确认用户配额 Accounting、Enforcement 均为 ON。
  • 为 Alice 设置 50M/60M 限制。
  • 通过报表确认限制存在。

本次尚未实际以 Alice 创建大文件验证:

  • 超过 50M 后的宽限期表现。
  • 超过 60M 后的 Disk quota exceeded。

因此准确结论是“配额设置成功”,不能写成“已经验证硬限制拦截成功”。


十六、练习错误与纠正复盘

这一部分集中整理本次练习中出现的错误、误判和容易混淆的正常现象。复习时应重点看“为什么错”,而不只是记住最终命令。

错误 1:把 lsblk -f 理解成查看普通文件属性

当时的理解

认为:

lsblk -f 用于显示文件的属性和信息

问题

lsblk 的操作对象是 block device(块设备)。这里的 -f 是 filesystem,不是普通文件。

正确理解

lsblk -f

查看的是块设备上的:

  • 文件系统类型
  • 文件系统版本
  • 标签
  • UUID
  • 可用空间
  • 挂载点

普通文件的权限、属主、大小等应使用:

ls -l 文件
stat 文件

错误 2:把 disk 直接说成“物理磁盘”

问题

lsblk 的 TYPE=disk 表示完整磁盘形态的块设备,但本实验中的设备是:

VMware Virtual NVMe Disk

它们是虚拟磁盘,不是 Mac 中真实存在的三块 NVMe 硬盘。

正确表述

disk 表示完整块设备;本实验中是 VMware 提供的虚拟磁盘。

错误 3:第一次创建分区时没有先建立 GPT

当时操作

进入空盘后直接输入 n 创建分区。

现象

fdisk 显示:

Created a new DOS disklabel
Disklabel type: dos

错误复盘:第一次误建 MBR

原因

空盘没有分区表,当前版本的 fdisk 自动准备了 DOS/MBR 分区表。创建出 p1、p2 并不代表分区表类型就是题目要求的 GPT。

正确操作

进入 fdisk 后先输入:

g

看到:

Created a new GPT disklabel

再使用 n 创建分区,最后 p 检查、w 保存。

复习判断

可使用:

fdisk -l /dev/nvme0n2
lsblk -o NAME,PTTYPE

确认出现 gpt,不能只凭“已经有两个分区”判断成功。


错误 4:恢复快照后,误以为实验目录和文件也会保留

现象

恢复快照后发现:

  • /opt/storage_lab 不存在或内容回到了旧状态。
  • host_file.txt 等准备文件不存在。
  • 两块后来添加的实验盘需要重新添加。

原因

快照恢复的是“创建快照时”的虚拟机状态。快照之后在系统盘中创建的目录、文件和部分虚拟硬件状态不会自动保留。

正确处理

恢复后先重新盘点:

lsblk
ls -la /opt
findmnt

再按实际状态补建实验目录,不要假定前一次操作仍在。


错误 5:挂载点命名出现 mount_date 与 mount_data 混用

现象

早期命令使用了:

/opt/storage_lab/mount_date

后续实验统一使用:

/opt/storage_lab/mount_data

风险

Linux 路径必须完全匹配。一个字母不同就是两个不同目录,可能导致:

  • 在一个目录挂载,却去另一个目录检查。
  • df 显示了根文件系统而不是实验盘。
  • 误以为文件丢失或挂载失败。

规范做法

本次最终统一为:

/opt/storage_lab/mount_data

每次操作后用下面的命令确认真实挂载关系:

findmnt /opt/storage_lab/mount_data

错误 6:当前目录位于挂载点内时直接卸载

当时操作

Shell 提示符显示当前位于 mount_data,仍执行:

umount /opt/storage_lab/mount_data

现象

target is busy

错误复盘:当前 Bash 占用挂载点

原因

Shell 的当前工作目录本身就是对该文件系统的使用。内核不能在进程仍引用它时完成正常卸载。

正确处理

先排查:

fuser -vm /opt/storage_lab/mount_data

退出挂载点:

cd /
umount /opt/storage_lab/mount_data

不要一遇到 busy 就使用强制或懒卸载。应先找出并解除占用。


错误 7:混淆“文件被删除”和“文件被挂载覆盖”

实际过程

mount_test.txt 当时位于已经挂载的 /dev/nvme0n2p1 中,随后执行:

rm -rf /opt/storage_lab/mount_data/mount_test.txt

这条命令确实删除了实验分区中的文件。

之后把同一个文件系统挂载到 cover_test,不能再期待其中出现已经被删除的 mount_test.txt。

与挂载覆盖的区别

  • 删除:目录项被移除,文件在该文件系统中不再存在。
  • 挂载覆盖:挂载点原目录中的内容只是暂时不可见,卸载后重新出现。

正确判断方法

先确认设备当前挂载在哪里:

findmnt -S /dev/nvme0n2p1

再通过该挂载点查看设备中的实际内容。


错误 8:在被覆盖目录中使用旧的当前工作目录,误判挂载结果

现象

Shell 在挂载前已经进入 cover_test。挂载完成后执行相对路径:

cat host_file.txt

仍然读到了底层根文件系统中的 host_file.txt,于是看起来像是“挂载没有遮住文件”。

错误复盘:当前工作目录仍引用底层目录

原因

Shell 在挂载前已经持有底层目录对象的引用。挂载改变了从父目录重新解析该路径时看到的内容,但不会自动替换进程已有的当前目录引用。

正确演示

挂载前先退出:

cd /
mount /dev/nvme0n2p1 /opt/storage_lab/cover_test
ls -la /opt/storage_lab/cover_test

使用绝对路径重新解析时,看到的才是当前挂载文件系统。


错误 9:未挂载时对目录执行 df,把根文件系统当成实验盘

现象

df -Th /opt/storage_lab/mount_data

输出:

/dev/mapper/rl-root

错误复盘:未挂载路径由根文件系统承载

原因

此时 mount_data 只是 / 文件系统中的普通目录。df 路径 显示承载该路径的文件系统,所以显示根逻辑卷完全正常。

正确判断

先确认:

findmnt /opt/storage_lab/mount_data

如果没有结果,说明它不是独立挂载点。挂载 p1 后再执行 df,才会显示 /dev/nvme0n2p1。


错误 10:删除普通文件时使用 rm -rf

当时操作

rm -rf /opt/storage_lab/mount_data/bigfile.bin

问题

  • -r 用于递归删除目录。
  • -f 会跳过部分提示并忽略不存在的目标。
  • 对普通文件不需要这两个选项。

虽然本次明确路径下删除成功,但这种习惯会扩大输错路径时的破坏范围。

规范写法

rm /opt/storage_lab/mount_data/bigfile.bin

执行前可先确认:

ls -lh /opt/storage_lab/mount_data/bigfile.bin

错误 11:认为 p2 “还没有做分区”

当时状态

lsblk 已显示:

nvme0n2p2  1G  part

所以 p2 已经是分区。它缺少的是 swap 类型和 swap 签名。

正确区分

状态验证命令
是否存在分区lsblk、fdisk -l
GPT 分区类型fdisk -l
是否有 swap 签名lsblk -f、blkid
是否已经启用swapon --show

错误 12:在 fdisk 的“分区类型”提示中输入 p

现象

Partition type or alias: p
Type of partition 2 is unchanged: Linux filesystem

错误复盘:分区类型未改变

原因

p 只有在 fdisk 主菜单中才表示打印分区表。当前提示正在等待“类型编号或别名”,所以 p 被当成了无效或不匹配的类型输入。

正确做法

先看当前提示。回到主菜单后:

t → 选择 p2 → 输入 Linux swap 的编号或别名

错误 13:在分区类型列表的分页器中输入 19

现象

输入 L 后看到很长的类型列表,在页面底部直接输入 19,屏幕出现:

:19

错误复盘:19 被分页器当成搜索内容

退出后检查,类型仍是 Linux filesystem。

原因

当时仍在分页器中,键盘输入由分页器处理,不是由 fdisk 的类型提示处理。

正确做法

  1. 按 q 退出分页器。
  2. 确认重新看到:

    Partition type or alias:
  3. 再输入本机列表对应的 19 或可用别名。
  4. 用 p 检查,并用 w 保存。

错误 14:把“Linux swap 分区类型”当成已经可用的 swap

问题

分区表显示 Linux swap 只是 GPT 元数据。它不能替代真正的 swap 签名和启用动作。

完整流程

fdisk t         → 修改 GPT 分区类型
mkswap          → 写入 swap 签名
swapon          → 激活
swapon --show   → 验证活动状态

每一步观察的是不同层次。


错误 15:没有先区分硬链接和软链接的跨文件系统能力

现象

尝试从根文件系统向挂载的 XFS 创建硬链接:

Invalid cross-device link

错误复盘:跨文件系统硬链接失败

原因

硬链接需要指向同一个 inode,而 inode 只在各自文件系统内部有意义。

正确结论

  • 硬链接不能跨文件系统。
  • 软链接保存目标路径,可以跨文件系统。
  • 这里的报错正是实验预期,不是磁盘损坏或权限问题。

易误判 16:findmnt -S 没有输出

现象

findmnt -S /dev/nvme0n2p1

没有输出。

正确解释

-S 是按挂载源查询。设备当时没有挂载,所以没有匹配结果是正常现象,不代表设备不存在。

设备是否存在应使用:

lsblk
blkid /dev/nvme0n2p1

易误判 17:parted 创建“2G”后 lsblk 只显示 1.9G

现象

parted /dev/nvme0n3 mkpart primary 1 2G

结果约为:

1906MiB
lsblk:1.9G

正确解释

这里的 G 使用十进制边界,2GB 不等于 2GiB。换算后约为 1.86GiB,显示 1.9G 是正常的。

如果题目要求精确二进制容量,应明确使用 MiB 或 GiB 边界。


易误判 18:findmnt 显示 usrquota,而命令输入的是 uquota

现象

挂载命令:

mount -o uquota ...

findmnt 却显示:

usrquota

正确解释

这是工具对同类用户配额选项的规范化显示,不是选项失效。随后:

Accounting: ON
Enforcement: ON

已经证明用户配额正常启用。


教学补充 19:xfs_quota 命令没有出现在原笔记中

这一点不是用户漏记,也不是应当自行猜出的命令。它是根据 Rocky Linux 的 XFS 环境补充的扩展实操。

需要掌握的最小主线:

mount -o uquota
        ↓
xfs_quota ... state
        ↓
xfs_quota ... limit
        ↓
xfs_quota ... report

其中:

  • -x 是管理模式。
  • -c 执行一条内部命令。
  • bsoft、bhard 分别设置块空间软、硬限制。

截图中的 > 是反斜杠续行造成的 Shell 提示,不是需要输入的字符。


错误复盘总表

现象根本原因以后如何检查
建成两个分区却不是 GPT空盘上直接 n,默认得到 DOS disklabel看 fdisk -l 的 Disklabel type
umount: target is busy当前 Shell 或其他进程仍在使用挂载点fuser -vm,再退出目录
挂载后仍看到底层文件Shell 保留了挂载前当前目录引用cd / 后用绝对路径重新访问
df 显示根逻辑卷目标目录当时没有挂载实验盘先执行 findmnt 目录
p2 被说成“未分区”混淆分区、签名、启用三个层次分别用 lsblk、blkid、swapon
swap 类型没有变化在错误提示位置输入 p看清 Partition type or alias
输入 19 后仍没变化19 输入给了分页器先按 q 回到 fdisk 提示
硬链接跨盘失败inode 不能跨文件系统使用软链接或复制文件
2G 最终显示 1.9G十进制 GB 与二进制 GiB 不同parted ... unit MiB print free
uquota 显示为 usrquota挂载选项被规范化显示看 xfs_quota state 是否 ON

十七、实验后的设备状态

/dev/nvme0n1                 20G 系统盘,全程未修改
├── nvme0n1p1                vfat → /boot/efi
├── nvme0n1p2                xfs   → /boot
└── nvme0n1p3                LVM2_member
    ├── rl-root              xfs   → /
    └── rl-swap              swap  → 系统原有 swap

/dev/nvme0n2                 5G,GPT 实验盘
├── nvme0n2p1                2G XFS,LABEL=LAB_DATA
├── nvme0n2p2                1G swap,LABEL=LAB_SWAP,目前已 swapoff
└── 约 2G 未分配

/dev/nvme0n3                 3G,GPT 实验盘
├── nvme0n3p1                约 1.9G XFS,LABEL=QUOTA_LAB
│                            当前挂载到 /opt/storage_lab/quota_data
│                            用户配额已启用
└── 约 1165MiB 未分配

十八、重启后哪些内容会保留

内容是否保留
GPT 分区表和分区保留
XFS 文件系统保留
文件系统标签、UUID保留
分区中的普通文件保留
mkswap 写入的 swap 签名保留
swapoff 后的 LAB_SWAP 签名仍保留
Alice 的 XFS quota 限制元数据保留在该 XFS 中
手动 mount 建立的挂载关系不保留
手动 swapon 的活动状态不因本实验自动恢复
fstab.practice 文件文件保留,但启动过程不会读取
uquota 的当前挂载状态不保留

由于真实 /etc/fstab 没有被修改,重启后若继续 quota 实验,应重新使用:

mount -o uquota /dev/nvme0n3p1 /opt/storage_lab/quota_data

十九、命令与参数速查

块设备和文件系统

命令作用
lsblk查看块设备树、分区、LVM 和挂载点
lsblk -f查看文件系统类型、标签、UUID
lsblk -o ...自定义输出字段
blkid查看设备签名、标签、UUID、PARTUUID
df -Th查看已挂载文件系统类型和空间
du -sh 路径汇总指定目录实际占用的数据块

分区

命令或参数作用
fdisk 设备交互式管理分区
g在 fdisk 中创建 GPT
n创建分区
t修改分区类型
p在主菜单中打印分区表
w写入并退出
q不保存退出,或退出分页器
parted 设备 mklabel gpt使用 parted 创建 GPT
parted 设备 print查看分区表
parted 设备 unit MiB print free以 MiB 查看分区和空闲空间
partprobe 设备通知内核重新读取分区表

文件系统与挂载

命令或参数作用
mkfs.xfs 分区创建 XFS 文件系统
mkfs.xfs -L 标签 分区创建 XFS 并设置标签
xfs_admin -L 标签 分区修改 XFS 标签
mount 设备 目录临时挂载
mount -o remount,ro 目录重挂载为只读
mount -o remount,rw 目录重挂载为读写
findmnt 路径查看实际挂载源、类型和选项
fuser -vm 路径查找占用挂载文件系统的进程
umount 路径正常卸载

swap

命令作用
mkswap -L 标签 分区写入 swap 签名并设置标签
swapon 分区启用指定 swap
swapon --show列出活动 swap
swapoff 分区关闭指定 swap
free -h查看内存和总 swap

链接和空间

命令作用
ln 源 新名称创建硬链接
ln -s 目标 新名称创建软链接
ls -li查看 inode 和链接计数
readlink 软链接输出软链接中保存的目标路径
fallocate -l 200M 文件预分配 200MiB 文件

XFS 配额

命令或参数作用
mount -o uquota ...挂载时启用 XFS 用户配额
xfs_quota -x -c 'state' 挂载点查看配额状态
limit bsoft=... bhard=... 用户设置用户块空间软、硬限制
report -h -u以易读单位显示用户配额报表

二十、本次练习的核心结论

  1. 系统盘应根据 /、/boot、/boot/efi 等挂载关系判断,不能只看设备编号。
  2. 磁盘、分区、文件系统、挂载点是不同层次。
  3. lsblk -f 查看文件系统属性,不是普通文件属性。
  4. GPT 和 MBR 是分区表格式,不是文件系统。
  5. fdisk 通常到 w 才保存;parted 操作通常立即生效。
  6. mkfs、mkswap 都会覆盖目标设备上的原有签名,必须先确认设备名。
  7. 文件系统标签 LABEL 与 GPT 分区名称 PARTLABEL 不属于同一层次。
  8. 挂载是把文件系统接入目录树,不会把数据复制进挂载目录。
  9. 挂载到非空目录只会遮住原内容,卸载后原内容重新可见。
  10. Shell 当前目录位于挂载点内,可能导致 umount: target is busy。
  11. 文件系统只读时报 Read-only file system,与普通权限不足不同。
  12. df 统计整个文件系统,du 统计指定目录树中的数据块。
  13. swap 分区类型、swap 签名和 swap 激活是三个独立步骤。
  14. swapoff 停止使用 swap,但不会清除签名和标签。
  15. 硬链接共享 inode,不能跨文件系统。
  16. 软链接保存目标路径,可以跨文件系统。
  17. 安全 fstab 练习使用 mount -a -T 读取独立文件,没有实现永久挂载。
  18. parted 中十进制 2G 换算后不是精确 2GiB。
  19. XFS 用户配额要在挂载时使用 uquota/usrquota 启用。
  20. 用户配额按 UID 统计整个文件系统,而不是只统计某个目录。
  21. 软限制允许在宽限期内暂时超过,硬限制是不可正常突破的绝对上限。

Linux 进程管理练习笔记

实验系统:Rocky Linux 虚拟机
实验目录:/opt/process_lab
说明:本文中的 PID 是当次实验的实际结果。进程重新启动或系统重启后,PID 会重新分配,复习时应以现场查询结果为准。

一、实验环境说明

1. 实验目标

这个环境用于在不修改 SSH、网络、系统启动等关键配置的前提下,练习以下内容:

  • 进程与程序的区别
  • PID、PPID 和进程树
  • ps、pgrep、pstree、top
  • 进程状态 R、S、T、Z
  • 信号与 kill
  • 前台任务、后台任务及 Shell 作业控制
  • nohup、disown
  • nice 值与调度优先级
  • systemd 服务和 transient 临时单元
  • 僵尸进程
  • 模拟 CPU 异常的排查流程

2. 实验用户

沿用权限管理实验中的用户:

用户用途
alice运行第一个普通睡眠进程
bob运行第二个普通睡眠进程及 CPU 异常进程
tom运行包含父子关系的进程树
root查看系统中的全部进程、发送信号和管理实验服务

3. 目录结构

/opt/process_lab/
├── bin/
│   ├── lab_sleep
│   ├── lab_cpu
│   ├── lab_worker.sh
│   ├── lab_tree.sh
│   └── lab_zombie.py
├── logs/
└── run/

各程序的作用:

程序作用
lab_sleepsleep 的实验副本,用于观察普通等待进程
lab_cpu持续运行并消耗 CPU 的实验程序;它会不断输出内容,运行时必须妥善处理标准输出
lab_worker.sh循环执行 sleep 5,并捕获 TERM、HUP 信号写入日志
lab_tree.sh启动两个 lab_worker.sh 子进程,构造父子进程树
lab_zombie.py子进程退出后,父进程暂不调用 wait(),用于制造一个可控的僵尸进程

4. 初始进程布局

环境恢复后,主要实验进程如下:

systemd(1)
└── login
    └── bash
        ├── runuser -u alice
        │   └── lab_sleep
        ├── runuser -u bob
        │   └── lab_sleep
        └── runuser -u tom
            └── lab_tree.sh
                ├── lab_worker.sh child_a
                │   └── sleep 5
                └── lab_worker.sh child_b
                    └── sleep 5

此外还创建了一个由 systemd 直接管理的临时服务:

process-lab-service.service
└── lab_worker.sh service_worker
    └── sleep 5

lab_cpu 和 lab_zombie.py 不在初始状态运行,只在对应练习中启动,以免长期占用 CPU 或保留僵尸进程。


二、练习 1:查找进程并读取基本属性

目标

找到两个同名的 lab_sleep 进程,并理解 USER、PID、PPID、STAT、NI、TTY 和 COMMAND。

操作

pgrep -l lab_sleep
ps -C lab_sleep -o user,pid,ppid,stat,ni,tty,comm

实际结果

两个 lab_sleep 进程及属性

结果中:

  • PID 1460 属于 alice
  • PID 1462 属于 bob
  • 两个进程名称相同,但 PID 不同
  • PPID 分别是 1459、1461
  • STAT=S
  • NI=0
  • TTY=tty1

概念

程序和进程

  • 程序是磁盘中的可执行文件。
  • 进程是程序的一次运行实例。
  • 同一个程序可以同时运行多次,每次运行都会形成独立进程,并获得不同的 PID。

常见字段

字段含义
USER进程的运行用户
PID当前进程 ID
PPID直接父进程 ID
STAT进程状态及附加标志
NInice 值
TTY关联终端
COMMAND命令名

S 状态

S 表示可中断睡眠(interruptible sleep)。本例中的 lab_sleep 正在等待计时器到期,因此处于睡眠状态,并不是异常“卡死”。

pgrep -l 与 pgrep -a

  • pgrep -l:显示 PID 和进程名。
  • pgrep -a:显示 PID 和完整命令行。

三、练习 2:理解 PPID 和祖先进程链

目标

查找 lab_sleep 的直接父进程,并沿进程树追溯到 PID 1。

操作

ps -p 1459,1461 -o user,pid,ppid,stat,comm,args
pstree -sp 1460
pstree -sp 1462

实际结果

runuser 与 lab_sleep 的祖先进程链

进程链为:

systemd(1) → login(862) → bash(1374)
→ runuser(1459/1461) → lab_sleep(1460/1462)

结论

  • lab_sleep 的直接父进程是 runuser。
  • bash 是其祖先进程,但不是直接父进程。
  • 两条进程链最终都能追溯到 systemd(1)。

原理

启动过程是:

  1. bash 启动 runuser。
  2. runuser 把运行身份切换成 Alice 或 Bob。
  3. runuser 再启动 lab_sleep。

因此 lab_sleep 的 PPID 指向 runuser,而不是当前用于查看进程的 Shell。


四、练习 3:观察父进程、子进程和孙进程

目标

观察 Tom 的进程树,验证子进程的 PPID。

操作

pgrep -af 'lab_tree|lab_worker'
pstree -p 1464
ps -p 1464,1465,1466 -o user,pid,ppid,stat,comm,args

实际结果

Tom 的父子进程树

分析

lab_tree.sh(1464)
├── lab_worker.sh(1465)
│   └── sleep
└── lab_worker.sh(1466)
    └── sleep
  • 1464 是父进程。
  • 1465、1466 是它的两个子进程,PPID 都是 1464。
  • 两个 sleep 是更下一层的孙进程。
  • service_worker 属于另一个 systemd 服务,不在 1464 的子树中。

命令侧重点

  • pstree:适合直观看进程之间的层级关系。
  • ps:适合查看指定进程的用户、PID、PPID、状态、优先级和完整命令等属性。

五、练习 4:使用 SIGTERM 正常终止一个子进程

目标

只终止 child_a,观察父进程、兄弟进程以及内部 sleep 是否受影响。

操作

kill -TERM 1465
sleep 1
pstree -p 1464
ps -p 1464,1465,1466 -o pid,ppid,stat,comm
tail /opt/process_lab/logs/child_a.log

实际结果

SIGTERM 终止 child_a

结果

  • 1465 收到 SIGTERM 后退出。
  • 它内部的 sleep 子进程也消失。
  • 父进程 1464 继续运行。
  • 兄弟进程 1466 继续运行。
  • 日志中出现 term pid=1465,证明脚本捕获了 SIGTERM 并执行清理逻辑。

概念

kill 的本质是“向进程发送信号”,不等于一定强制杀死进程。

  • SIGTERM:请求进程正常退出;程序可以捕获它并执行清理。
  • SIGKILL:由内核立即终止进程;程序不能捕获、忽略或清理。

排障时通常先尝试 SIGTERM,只有进程拒绝退出且确认可以强制终止时,才考虑 SIGKILL。


六、练习 5:SIGSTOP 与 SIGCONT

目标

暂停一个进程但不终止它,再恢复运行。

一次常见错误

错误使用:

ps -s 1466 -o pid,ppid,stat,comm

误用 ps -s 后没有结果

这里的 -s 是按会话 ID(SID)筛选,不是按 PID 查询,因此看起来像“无事发生”。实际的暂停和恢复信号已经发送成功,只是查询方式不对。

正确参数是 -p:

kill -STOP 1466
ps -p 1466 -o pid,ppid,stat,comm
pstree -p 1464

kill -CONT 1466
ps -p 1466 -o pid,ppid,stat,comm

正确结果

STOP 后为 T,CONT 后恢复为 S

结论

  • 收到 SIGSTOP 后,STAT 从 S 变为 T。
  • 暂停期间 PID 仍然存在,父子关系也仍保留。
  • 收到 SIGCONT 后恢复运行,状态重新变为 S。

暂停和终止的区别

  • 暂停:进程仍存在,只是暂时不被调度执行,可以恢复。
  • 终止:进程退出,正常情况下 PID 消失并释放资源。

七、练习 6:前台、后台和 Shell 作业控制

目标

掌握 Ctrl+Z、jobs、bg、fg 和 Ctrl+C。

操作

sleep 300

运行后按 Ctrl+Z,再执行:

jobs -l
bg
jobs -l
fg

fg 后按 Ctrl+C,最后检查:

jobs -l

实际结果

前台和后台作业控制

概念

  • Ctrl+Z:向前台作业发送终端暂停信号,使其进入 Stopped 状态。
  • bg:让暂停的作业在后台继续运行。
  • fg:把指定作业调到前台。
  • Ctrl+C:向前台作业发送中断信号,通常使它终止。
  • jobs -l:显示当前 Shell 管理的作业,并附带 PID。

当 jobs -l 没有输出时,说明当前 Shell 没有后台或暂停作业。


八、练习 7:多个后台作业、作业编号和 PID

目标

区分 Shell 作业编号与系统 PID,并练习指定作业。

操作

sleep 500 &
sleep 600 &
jobs -l
fg %1

第一个作业回到前台后按 Ctrl+C,然后:

jobs -l
kill %2
jobs -l

实际结果

多个后台作业及作业编号

概念

  • [1]、[2]:当前 Shell 分配的作业编号。
  • 8604、8608:内核分配的 PID。
  • %2:引用当前 Shell 的第 2 号作业。
  • 8608:引用系统中的 PID 8608。

jobs 只能查看当前 Shell 自己管理的作业,不能显示其他终端启动的进程或系统服务。

作业标记:

  • +:当前默认作业,执行不带参数的 fg、bg 时优先选它。
  • -:次选作业。

作业刚被终止时,Shell 可能显示一次 Terminated;再次执行 jobs 后通常会从作业表中清除。


九、练习 8:nohup 与 disown

目标

让程序忽略终端挂断信号,并把它从当前 Shell 的作业表中移除。

操作

nohup sleep 600 > /opt/process_lab/logs/nohup_test.log 2>&1 &
labpid=$!
echo $labpid
jobs -l

disown %+
jobs -l
ps -p "$labpid" -o pid,ppid,stat,tty,comm

清理:

kill "$labpid"

实际结果

nohup 与 disown

分析

  • disown 后,jobs 查不到该作业。
  • ps 仍能查到进程,说明它仍存在于内核进程表中。
  • 当时 Shell 尚未退出,所以 PPID 仍是当前 Shell。
  • TTY 仍显示 pts/0,说明 nohup 并不等于彻底脱离终端。

概念

  • nohup 主要让程序忽略 SIGHUP。
  • disown 只是从当前 Shell 的作业表中移除作业,不是终止进程。
  • nohup 可以帮助程序在退出终端或 SSH 断开后继续运行。
  • nohup 不能让进程跨越关机或重启;系统重启后需要由 systemd 等机制重新启动程序。

$! 表示最近一个后台进程的 PID。本例中先保存为 labpid,后续即可使用 "$labpid" 准确引用。


十、练习 9:nice 值和进程调度优先级

目标

观察并调整 CPU 实验进程的 nice 值。

操作

nice -n 10 /opt/process_lab/bin/lab_cpu > /dev/null &
cpupid=$!
ps -p "$cpupid" -o pid,stat,ni,pri,pcpu,comm

renice -n 15 -p "$cpupid"
ps -p "$cpupid" -o pid,stat,ni,pri,pcpu,comm

top -p "$cpupid"

退出 top 后清理:

kill "$cpupid"

实际结果

nice 从 10 调整到 15

top 中观察 nice 进程

结果

  • NI 从 10 变为 15。
  • STAT=RN:

    • R 表示运行中。
    • N 表示具有正的 nice 值。
  • CPU 使用率仍然很高。

原理

nice 数值越大,表示进程对其他进程越“友好”,在发生 CPU 竞争时愿意让出更多资源,因此自身调度优先级越低。

nice 是相对调度权重,不是 CPU 使用率上限。如果系统中没有足够强的竞争者,即使 NI 已经提高到 15,进程仍然可以使用大量空闲 CPU。

注意:该实验命令本身没有时间限制,必须手动清理。若希望自动结束,应配合 timeout 或由 systemd 设置 RuntimeMaxSec。

十一、练习 10:观察 systemd 临时服务

目标

理解 systemd 服务的 active、enabled、transient、Main PID 和 cgroup。

操作

systemctl status process-lab-service --no-pager
systemctl is-active process-lab-service
systemctl is-enabled process-lab-service
pgrep -af service_worker

实际结果

systemd 临时服务状态

分析

  • 服务为 active (running)。
  • Main PID 为 1470。
  • 服务中有两个任务:lab_worker.sh 和它当前的 sleep 5 子进程。
  • 两者都位于:
/system.slice/process-lab-service.service
  • is-enabled 返回 transient。

概念

  • active:服务当前是否正在运行。
  • enabled:是否配置为在对应启动目标中自动启动。
  • transient:临时运行单元,由运行时命令创建,定义通常位于 /run/systemd/transient/。

/run 是运行时目录,重启后会重新建立。因此 transient 服务即使当前 active,也不会因为这一状态就在下次开机自动恢复。

systemd 通过 cgroup 管理服务内的一组进程,不只记录一个 Main PID。这使它可以统一跟踪和停止服务的主进程及其子进程。


十二、练习 11:停止 transient 服务

目标

观察停止临时服务后,进程和单元定义的变化。

操作

systemctl stop process-lab-service
systemctl is-active process-lab-service
pgrep -af service_worker
systemctl status process-lab-service --no-pager

实际结果

停止 transient 服务

结论

  • 服务先显示 inactive。
  • pgrep 没有输出,主进程及其 sleep 子进程均已消失。
  • 临时单元随后被 systemd 清除,status 提示 Unit ... could not be found。

systemctl start process-lab-service 只能启动已经存在的服务单元。临时定义消失后,仅凭服务名无法知道应运行什么程序。


十三、练习 12:用 systemd-run 重新创建临时服务

目标

理解 systemctl start 与 systemd-run 的区别。

操作

systemd-run --unit=process-lab-service \
  /opt/process_lab/bin/lab_worker.sh service_worker

systemctl status process-lab-service --no-pager
pgrep -af service_worker

实际结果

重新创建 transient 服务

结论

  • systemd-run 同时创建临时单元定义并启动程序。
  • 新 Main PID 是 9483,不再是原来的 1470。
  • 原进程已经结束,新进程由内核重新分配 PID。

不能依赖某个程序永远使用固定 PID。管理长期服务时,应通过服务名、PID 文件或 systemd 查询 Main PID,而不是在脚本中写死旧 PID。


十四、练习 13:观察僵尸进程

目标

观察已经退出但尚未被父进程回收的子进程。

操作

python3 /opt/process_lab/bin/lab_zombie.py &
zparent=$!
sleep 1

ps -p "$zparent" -o pid,ppid,stat,comm,args
ps --ppid "$zparent" -o pid,ppid,stat,comm,args
pstree -p "$zparent"

实际结果

僵尸进程状态 Z

分析

  • 父进程 9586 为 S 状态。
  • 子进程 9587 为 Z 状态。
  • <defunct> 表明该子进程已经结束,但退出状态尚未被父进程读取。

原理

僵尸进程已经停止执行,不再消耗 CPU,也不再像普通进程那样占用主要内存;它只保留 PID、退出状态和少量进程表信息,供父进程通过 wait() 等系统调用回收。

少量僵尸通常不会造成明显负载,但大量僵尸会占用 PID 和进程表项,严重时可能导致系统无法创建新进程。


十五、练习 14:为什么 kill -9 无法清除僵尸

操作与结果

尝试 kill 僵尸并终止父进程

实验中执行了:

kill -9 9587

但僵尸进程本身已经死亡,无法再被信号“杀死一次”。正确处理方向是让父进程调用 wait();如果父进程异常且无法修复,可以结束父进程,使僵尸被 PID 1 或其他子进程回收者接管并回收。

随后使用:

kill "$zparent"
sleep 1
ps -p 9586,9587 -o pid,ppid,stat,comm

父进程和僵尸进程最终都消失。

本次操作中的两个易错点

  1. 查询时曾把 9587 误写成 9687,因此那一次 ps 输出不能用于判断僵尸是否还存在。
  2. kill "%zparent" 是错误写法:

    • %2 这类形式表示 Shell 作业号。
    • $zparent 才表示变量值。

正确写法:

kill "$zparent"

十六、练习 15:CPU 异常排查中的一次真实故障

原始目标

用 systemd 启动一个最长运行五分钟、属于 Bob 的 CPU 异常进程,再通过 top 定位并处理。

最初使用:

systemd-run --unit=process-lab-cpu \
  --uid=bob \
  --property=RuntimeMaxSec=5min \
  /opt/process_lab/bin/lab_cpu

意外现象

top 中首先发现高 CPU 进程是 systemd-journald,而不是预期的 lab_cpu。

原因是 lab_cpu 实际为持续输出内容的实验程序,而 systemd-run 默认把服务标准输出交给日志系统。大量输出进入 journal,导致 systemd-journald 忙于处理日志。

此时不应直接杀掉 systemd-journald,因为它只是承受工作量的系统日志服务,真正的来源是 process-lab-cpu。

先停止实验服务

systemctl stop process-lab-cpu
systemctl is-active process-lab-cpu

当时显示:

CPU 实验服务处于 failed

failed 表示服务已经不在运行,但 systemd 仍保留其失败结果,并不表示 stop 命令没有执行。

查看失败原因和日志占用

systemctl status process-lab-cpu --no-pager -l
systemctl reset-failed process-lab-cpu
journalctl --disk-usage

实际结果

运行时限、日志输出与磁盘占用

结果表明:

  • 服务实际运行约 5 分钟。
  • 达到 RuntimeMaxSec 后被 systemd 发送 SIGTERM。
  • Result: timeout 指运行时限达到,不代表系统自身发生故障。
  • 日志中的大量 y 来自 lab_cpu。
  • journal 总占用约 70.6M,无需清理。

这次故障带来的排障认识

CPU 占用最高的进程有时只是“受害者”。看到 systemd-journald 高负载时,需要继续追查谁在制造大量日志,而不是直接终止系统日志服务。


十七、练习 16:修正 CPU 实验并定位真正异常进程

修正启动方式

把标准输出和标准错误丢弃,避免再次冲击日志系统:

systemd-run --unit=process-lab-cpu \
  --uid=bob \
  --property=RuntimeMaxSec=5min \
  --property=StandardOutput=null \
  --property=StandardError=null \
  /opt/process_lab/bin/lab_cpu

用 top 定位

top

在 top 中按大写 P,按 CPU 使用率排序,找到 lab_cpu 后记录 PID,并按 q 退出。

实际结果

top 中定位 bob 的高 CPU 进程

定位到:

  • 用户:bob
  • PID:10148
  • CPU:约 92%
  • COMMAND:lab_cpu

进一步查询:

ps -p 10148 -o user,pid,ppid,stat,ni,pcpu,comm,args

结果:

  • PPID 为 1,因为进程由 systemd 直接启动。
  • STAT=Rs:

    • R:正在运行。
    • s:会话首进程。
  • nice 值为 0。
  • 完整命令为 /opt/process_lab/bin/lab_cpu。

十八、练习 17:降低影响、正常终止并验证

调整优先级

renice -n 15 -p 10148
ps -p 10148 -o user,pid,ppid,stat,ni,pcpu,comm

正常终止

kill -TERM 10148
sleep 1
ps -p 10148 -o pid,ppid,stat,comm
systemctl is-active process-lab-cpu
systemctl status process-lab-cpu --no-pager

实际结果

renice、SIGTERM 与最终验证

分析

  • NI 从 0 成功调整为 15。
  • STAT=RNs:

    • R:运行中。
    • N:具有正 nice 值。
    • s:会话首进程。
  • CPU 仍约 92%,因为 nice 不是 CPU 限额;没有足够竞争时,它仍可使用空闲 CPU。
  • 发送 SIGTERM 后,ps 查不到 PID 10148,说明进程已退出。
  • 服务先显示 inactive。
  • 该服务是 transient 单元,退出后定义被自动清除,因此随后 status 提示找不到单元。

本场景中,SIGTERM 被 systemd 视为正常终止信号,服务没有保留为 failed 状态。


十九、进程状态速查

状态含义典型场景
RRunning / Runnable,正在运行或等待 CPUCPU 计算任务
SInterruptible Sleep,可中断睡眠等待计时器、输入或事件
TStopped,已暂停SIGSTOP、Ctrl+Z
ZZombie,僵尸子进程已退出,父进程尚未回收
DUninterruptible Sleep,不可中断睡眠通常在等待某些内核 I/O
IIdle,空闲内核线程常见于内核工作线程

常见附加标志:

标志含义
N具有正 nice 值
s会话首进程
+位于前台进程组

二十、常用信号和操作速查

操作常见信号/作用说明
Ctrl+CSIGINT中断当前前台任务
Ctrl+ZSIGTSTP暂停当前前台任务
kill -TERM PIDSIGTERM请求正常退出,可捕获和清理
kill -STOP PIDSIGSTOP强制暂停,不能被捕获
kill -CONT PIDSIGCONT恢复暂停进程
kill -9 PIDSIGKILL强制终止,不能被捕获或清理
nohup忽略 SIGHUP常用于终端断开后继续运行

推荐终止顺序:

确认目标 → SIGTERM → 等待并验证 → 必要时才使用 SIGKILL

二十一、规范的进程故障排查流程

1. 发现异常

top
ps aux --sort=-%cpu

先看 CPU、内存、负载和异常进程,不要看到高占用就立即强制杀进程。

2. 确认目标

pgrep -af 关键字
ps -p PID -o user,pid,ppid,stat,ni,pcpu,pmem,tty,comm,args

确认运行用户、PID、PPID、状态、优先级、终端和完整命令,避免误伤同名进程。

3. 查看上下文

pstree -sp PID
systemctl status 服务名

判断它是普通 Shell 进程、其他进程的子进程,还是由 systemd 管理的服务。

4. 降低影响

必要时先降低优先级:

renice -n 15 -p PID

但要记住 nice 只是调度权重,不是 CPU 配额。

5. 正常处理

  • 普通进程:优先 kill -TERM PID。
  • systemd 服务:通常优先 systemctl stop 服务名,让 systemd 统一管理整个 cgroup。
  • 不要一开始就使用 kill -9。

6. 验证

ps -p PID
pgrep -af 关键字
systemctl is-active 服务名
systemctl status 服务名

既要确认进程消失,也要确认服务状态和子进程是否清理干净。

本轮练习形成的记忆链

发现异常 → 确认进程 → 查看父子关系或服务归属
→ 降低影响 → 正常终止 → 验证进程 → 验证服务

二十二、复习自测题

  1. PID 与 PPID 分别表示什么?
  2. 为什么同一个可执行程序可以同时出现多个不同 PID?
  3. S、R、T、Z 分别代表什么?
  4. pstree 和 ps 分别更适合查看什么?
  5. SIGTERM 与 SIGKILL 的主要区别是什么?
  6. 为什么 kill -9 无法清理僵尸进程?
  7. Ctrl+Z、bg、fg、Ctrl+C 分别做什么?
  8. Shell 作业编号 %2 与 PID 有何区别?
  9. 为什么 disown 后 jobs 查不到进程,而 ps 仍可找到?
  10. nohup 能否让进程在系统重启后继续运行?为什么?
  11. nice 值从 0 调到 15,进程优先级升高还是降低?
  12. 为什么 nice 值已经是 15,CPU 使用率仍可能接近 100%?
  13. systemd 的 active、enabled、transient 各表示什么?
  14. 为什么 transient 服务退出后可能提示 Unit could not be found?
  15. 为什么排查到 systemd-journald 高 CPU 时,不应立即杀掉它?
  16. 管理 systemd 服务时,为什么通常优先 systemctl stop,而不是只杀 Main PID?

Linux第八天

网络管理实操与防火墙

这一节的排错顺序很重要:

服务是否启动并监听端口
        ↓
本机网络/IP/路由是否正常
        ↓
防火墙是否允许这类流量
        ↓
若要跨网段或发布内网服务,再检查转发与 NAT

不要一遇到访问失败就关闭防火墙。先定位是哪一层拦住了,才知道该改服务、网络还是规则。

本笔记不整理 ifconfig 配 IP、MTU、SELinux 和 iptables 的“规则保存与恢复”。

一、网络管理实操:NetworkManager、nmcli 与 ip

1. 三个名字的关系

nmcli  ->  NetworkManager  ->  网卡设备(如 ens160)
  • NetworkManager:后台服务,负责管理网络连接和配置文件。
  • nmcli:在命令行里控制 NetworkManager 的工具。
  • ens160:真正的网卡设备名;虚拟机不一定都叫这个名字,先查看实际名称。

先查看网卡设备与连接:

nmcli device status
nmcli connection show
ip addr
ip route

device 是网卡设备,connection 是一份连接配置。一个网卡可以关联不同的配置文件,例如一个静态 IP 配置、一个 DHCP 配置。

2. 使用 nmcli 配置静态 IPv4

先确认连接名,下面假设连接名和网卡名都为 ens160:

nmcli connection modify ens160 \
  ipv4.addresses 192.168.88.140/24 \
  ipv4.gateway 192.168.88.2 \
  ipv4.dns "114.114.114.114,114.114.115.115" \
  ipv4.method manual

含义:

  • ipv4.addresses:IP/前缀长度,/24 相当于 255.255.255.0。
  • ipv4.gateway:默认网关,访问其他网段时交给它。
  • ipv4.dns:域名解析服务器。
  • ipv4.method manual:使用手动静态配置。

修改后让连接重新生效:

nmcli connection down ens160
nmcli connection up ens160

ip addr show ens160
ip route
ping -c 4 网关IP
ping -c 4 114.114.114.114
ping -c 4 www.baidu.com

这三次测试分别大致验证:本地到网关、能否出网、DNS 是否正常。

远程 SSH 修改网卡有断连风险。最好在 VMware 控制台操作,或至少准备另一个登录窗口和回滚方案。

3. DHCP 配置和临时创建连接

创建一份 DHCP 自动获取地址的连接配置:

nmcli connection add type ethernet ifname ens160 con-name dhcp_ens160 ipv4.method auto
nmcli connection up dhcp_ens160

不再需要时删除的是“连接配置”,不是物理网卡:

nmcli connection delete dhcp_ens160

4. ip link set ens160 down:直接关闭网卡

ip link show ens160
ip link set ens160 down     # 立即禁用这块网卡
ip link set ens160 up       # 立即重新启用

它直接改变网卡当前的链路状态,效果立刻出现,但不是在修改 NetworkManager 的长期连接配置。

  • down 后,这张网卡上的 SSH、ping、网站访问都会中断。
  • up 只表示网卡重新启用;是否恢复 IP/路由,还取决于对应连接是否由 NetworkManager 激活。
  • 遇到远程机器网络异常时,优先在虚拟机控制台做这类操作。

5. 端口和进程:确认服务到底有没有在工作

网络请求到了服务器后,内核还要根据端口号交给对应进程。先检查监听状态:

ss -tlnp       # TCP 监听端口,并显示进程
ss -tulnp      # TCP + UDP 监听端口,并显示进程
ss -lntp | grep ':80'

常见输出思路:

LISTEN  0  511  *:80  *:*  users:(("nginx",pid=1234,fd=6))
  • LISTEN:正在等待连接。
  • *:80:本机所有 IPv4 地址的 80 端口都在监听。
  • nginx、pid=1234:真正接收请求的进程和 PID。

这也解释了“端口 + 进程”关系:端口并不属于某个命令,而是由一个正在运行的进程绑定。防火墙只负责决定包能不能到达端口;进程不存在时,即使端口放行也无法提供服务。

6. 用 Python 快速验证网络服务

python3 -m http.server 8000

它会启动一个简单的 HTTP 服务。另一个主机可测试:

curl http://服务器IP:8000

若只希望它绑定到某个地址:

python3 -m http.server -b 192.168.18.142 8000

绑定具体 IP 后,发往其他本机 IP 的请求不会被它接收;绑定 0.0.0.0 则表示接受本机所有 IPv4 地址的请求。


二、防火墙的基本概念

Linux 防火墙本质上是在内核网络栈中按规则检查数据包:满足条件就执行对应动作。

可以把一条规则读成一句话:

来自谁 -> 去哪里 -> 什么协议/端口 -> 放行、拒绝还是丢弃

两套常见管理方式:

iptables       直接管理 netfilter 规则,适合理解底层规则和 NAT
firewall-cmd   管理 firewalld,适合 Rocky Linux 9 的日常配置

DROP 和 REJECT 的区别

动作对方看到的现象适用理解
DROP没有回应,通常一直等待直到超时像“静默丢弃”
REJECT立即收到拒绝回应像“明确说不允许”

例如屏蔽某台主机对本机的 ping:

iptables -A INPUT -s 192.168.88.1 -p icmp -j DROP

ping 使用 ICMP 协议。此规则只影响源地址为 192.168.88.1 的 ICMP 包;并不是关闭本机所有网络。


三、iptables:表、链、规则

1. 五条内置链:包在什么阶段被检查

链什么时候经过最好这样理解
PREROUTING包刚进入本机、路由决定前先改目的地,例如 DNAT
INPUT目标是本机的包外部访问本机 SSH/HTTP
FORWARD包经过本机、但目标不是本机本机充当路由器
OUTPUT本机自己发出的包本机访问外网、服务端回复
POSTROUTING包即将离开本机出口前改源地址,例如 SNAT

最容易混淆的是:

访问本机服务      -> INPUT
本机作为路由器    -> FORWARD
本机发出的流量    -> OUTPUT

2. 常见表

表作用常见场景
filter过滤、放行、拒绝默认表,INPUT/FORWARD/OUTPUT
nat地址/端口转换SNAT、DNAT、MASQUERADE
mangle修改包的特殊字段较少在初学阶段使用
raw更底层的连接跟踪控制较少在初学阶段使用

不写 -t 表名 时,iptables 默认操作 filter 表。

3. 规则的通用结构

iptables -A 链 [匹配条件] -j 动作

常用部分:

-A INPUT              追加规则到 INPUT 链末尾
-I INPUT 1            插入到 INPUT 链第 1 条
-D INPUT 1            按序号删除
-s 192.168.88.10      匹配源 IP
-d 192.168.88.140     匹配目标 IP
-p tcp                匹配协议
--dport 80            匹配目标端口
--sport 12345         匹配源端口
-j ACCEPT/DROP/REJECT 指定动作

规则从上到下依次匹配,遇到匹配且执行终结动作的规则,通常就不会继续往下看。因此“更具体的规则”通常应该放在“更宽泛的规则”前面。

4. 查看、添加、删除和修改规则

iptables -vnL --line-numbers               # 查看默认 filter 表的所有链
iptables -vnL INPUT --line-numbers         # 只看 INPUT 链
iptables -t nat -vnL --line-numbers        # 看 nat 表

iptables -A INPUT -p tcp --dport 80 -j DROP
iptables -D INPUT 1                        # 删除 INPUT 第 1 条规则
iptables -D INPUT -p tcp --dport 80 -j DROP # 按完整规则删除
iptables -R INPUT 1 -p tcp --dport 8080 -j ACCEPT
iptables -F                                # 清空当前表的规则,实验机慎用
  • -n:不反向解析 IP/端口,显示更快、结果更直观。
  • -v:显示命中包数、字节数、网卡等详情。
  • --line-numbers:显示规则序号,方便删除和替换。
千万不要在 SSH 远程连接里随手加“拒绝 22 端口”的规则。连接会立刻中断;若要练习,使用虚拟机控制台并先准备删除命令。

5. 两个基础实验

屏蔽本机的 SSH:

iptables -A INPUT -p tcp --dport 22 -j REJECT
# 在 VMware 控制台恢复:
iptables -D INPUT -p tcp --dport 22 -j REJECT

只允许指定来源访问 80 端口:

iptables -I INPUT 1 -s 192.168.88.100 -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j DROP

第一条必须在前面,才能让 192.168.88.100 先被放行;其余访问 80 的 TCP 包会命中第二条并被丢弃。

6. 连接状态:理解 NEW 与 ESTABLISHED

iptables 可通过状态模块识别连接处于什么阶段:

NEW          新建连接
ESTABLISHED  已建立连接
RELATED      与已有连接有关联
INVALID      无法识别/无效

例:允许新建 SSH 连接:

iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT

实际规则常配合允许已建立连接的返回流量;课堂阶段先理解:状态匹配不是只看端口,而是在看“这是不是一段已经认识的通信”。

7. 自定义链:把相关规则收纳起来

当 Web 规则很多时,可以创建专门的链,而不是把所有规则堆在 INPUT:

iptables -t filter -N WEB_CHAIN
iptables -t filter -A INPUT -s 192.168.18.1 -j WEB_CHAIN
iptables -t filter -A WEB_CHAIN -p tcp -m multiport --dports 80,443 -j ACCEPT
iptables -t filter -A INPUT -p tcp -m multiport --dports 80,443 -j DROP

这段规则的意思是:来自 192.168.18.1 的 Web 流量先跳去 WEB_CHAIN,其中 80/443 被放行;其他来源访问 80/443 则被丢弃。


四、NAT:让一台服务器充当网关

1. 开启内核转发

一台机器要转发别人的包,首先要开启 IPv4 转发:

sysctl -w net.ipv4.ip_forward=1
sysctl net.ipv4.ip_forward

第一条立即生效,但重启后会恢复默认。课堂实验可以先用它;长期配置通常写入 /etc/sysctl.d/ 的配置文件。

2. SNAT / MASQUERADE:让内网机器能上网

实验拓扑:

server1:一张可上网网卡 ens160;一张内网网卡 192.168.199.1/24
server2:192.168.199.129/24,网关指向 192.168.199.1

在 server1 上:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 192.168.199.0/24 -o ens160 -j MASQUERADE

含义拆开看:

  • POSTROUTING:包将要从 server1 出口发出时。
  • -s 192.168.199.0/24:只处理来自内网的包。
  • -o ens160:只处理经可上网网卡离开的包。
  • MASQUERADE:把源地址动态伪装成 server1 出口网卡的地址。

出口 IP 固定时也可使用 SNAT:

iptables -t nat -A POSTROUTING -s 192.168.199.0/24 -o ens160 \
  -j SNAT --to-source 192.168.18.129

一般来说:出口 IP 经常变化时使用 MASQUERADE 更方便;固定公网 IP 时 SNAT --to-source 更明确。

在 server2 上依次测试:

ping -c 4 192.168.199.1     # 先验证内网网关
ping -c 4 114.114.114.114   # 再验证 NAT/出网
ping -c 4 www.baidu.com     # 最后验证 DNS

3. DNAT:把外部访问转给内网服务

假设 server2 上的 Web 服务监听 192.168.199.129:80,希望外部访问 server1 的 192.168.18.129:8080 时能转过去:

iptables -t nat -A PREROUTING -d 192.168.18.129 -p tcp --dport 8080 \
  -j DNAT --to-destination 192.168.199.129:80

为什么用 PREROUTING:它在路由选择之前修改目的地址。若等到路由完成后,server1 会以为“8080 是发给我自己的”,而 server1 又未必有程序监听 8080。

访问方式:

浏览器或 curl 访问:http://192.168.18.129:8080
实际被转发到:      http://192.168.199.129:80

映射内网 SSH 的思路一样:

iptables -t nat -A PREROUTING -d 192.168.18.129 -p tcp --dport 3322 \
  -j DNAT --to-destination 192.168.199.129:22

ssh root@192.168.18.129 -p 3322

五、firewalld:Rocky Linux 9 的日常首选

firewalld 是管理内核 netfilter 的服务,firewall-cmd 是控制它的命令行工具。

firewall-cmd  ->  firewalld  ->  netfilter

1. zone:按网络信任程度分区

常见 zone:

zone大意
public默认普通网络,只开放明确允许的服务
drop直接丢弃,无回应
block拒绝,并给出回应
home / work更信任的家庭/办公网络
dmz对外服务但与内网隔离的区域
trusted信任所有连接

流量通常按“源地址 -> 网卡所属 zone -> 默认 zone”的顺序归类。

2. 最常用的查看命令

systemctl status firewalld
firewall-cmd --get-default-zone
firewall-cmd --get-active-zones
firewall-cmd --get-zones
firewall-cmd --get-services
firewall-cmd --zone=public --list-all

先做“查看”,确认网卡在哪个 zone、当前已放行哪些服务,再做修改。

3. 放行服务或端口:临时与永久

# 仅当前运行时生效,重启 firewalld 后消失
firewall-cmd --zone=public --add-service=http
firewall-cmd --zone=public --add-port=8000/tcp

# 写入永久配置
firewall-cmd --zone=public --add-service=http --permanent
firewall-cmd --zone=public --add-port=8000/tcp --permanent
firewall-cmd --reload

--add-service=http 是按预定义服务名放行;--add-port=8000/tcp 是按具体端口和协议放行。

运行时配置和永久配置是两套:加了 --permanent 后,需要 --reload 才会立即加载到当前运行状态。

删除规则同理:

firewall-cmd --zone=public --remove-service=http
firewall-cmd --zone=public --remove-port=8000/tcp --permanent
firewall-cmd --reload

4. 富规则:按来源做更精细控制

例如只丢弃来自某个 IP 对 80 端口的访问:

firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.18.1" port port="80" protocol="tcp" drop'
firewall-cmd --reload

富规则更接近“来源 + 协议 + 端口 + 动作”的一句完整防火墙规则,适合需要精细限制的情形。


六、访问失败时的排错清单

以“浏览器打不开服务器 80 端口”为例:

# 1. 服务是否运行
systemctl status nginx

# 2. 是否有进程在监听 80
ss -lntp | grep ':80'

# 3. 本机访问是否成功
curl -I http://127.0.0.1

# 4. IP 和路由是否正常
ip addr
ip route

# 5. firewalld 是否放行
firewall-cmd --zone=public --list-all

# 6. 若实验使用 iptables,检查命中情况
iptables -vnL INPUT --line-numbers

判断思路:

  • 本机 curl 127.0.0.1 失败:优先查服务本身和端口监听。
  • 本机成功、其他机器失败:查 IP、路由和防火墙。
  • 跨网段/NAT 场景失败:再查 IP 转发、FORWARD、SNAT/DNAT 和两端网关。

今日小结

  1. nmcli 管连接配置,NetworkManager 在后台执行,ip link set ens160 down/up 直接切换网卡当前状态。
  2. 端口属于监听它的进程;先用 ss -tulnp 确认服务存在,再谈防火墙放行。
  3. DROP 静默丢弃,REJECT 明确拒绝;iptables 规则从上到下匹配。
  4. 访问本机服务看 INPUT,转发别人的流量看 FORWARD,本机发出流量看 OUTPUT;SNAT 在 POSTROUTING,DNAT 在 PREROUTING。
  5. server1 做网关时需要开启 net.ipv4.ip_forward=1,再用 MASQUERADE/SNAT 让内网主机出网。
  6. Rocky Linux 9 的日常防火墙配置优先使用 firewall-cmd;牢记运行时规则和 --permanent 规则是两套配置。

安装

YUM安装

yum -y install nginx
systemctl start nginx && systemctl enable nginx

编译安装

# 准备编译环境
yum -y install gcc pcre-devel openssl-devel zlib-devel
# 源码包
wget http://nginx.org/download/nginx-1.22.0.tar.gz -P /usr/local/src/

Nginx如何接受请求并根据规则处理请求?

虚拟主机

虚拟主机就是在一台物理服务器上,通过Nginx配置多个“网站服务器”

一台机器:
192.168.133.130

安装一个Nginx

可以同时运行

www.baidu-test.com
blog.test.com
shop.test.com

三个网站

用户访问不同域名时,Nginx根据规则把请求分别发到不同的网站配置。

现实中购买了一台云服务器

公网IP:
1.2.3.4

若不使用虚拟主机

则此公网通过nginx只可运行一个网站,很浪费。

但使用虚拟主机

             nginx
               |
   ---------------------------
   |            |            |
 blog.com   shop.com    api.com
   |            |            |
博客程序    商城程序     后端接口

一台服务器托管多个业务。

为什么叫“虚拟”?

因为它不是实际存在的服务器,比如你只有:

一台Linux服务器
一个Nginx进程
一个80端口

但通过配置:

server {
    server_name blog.test.com;
}

server {
    server_name shop.test.com;
}

就能让用户感觉像访问两个不同服务器。

Nginx虚拟主机靠什么区分?

1.域名

例如:

server {
    listen 80;
    server_name blog.test.com;

    location / {
        root /data/blog;
        index index.html;
    }
}


server {
    listen 80;
    server_name shop.test.com;

    location / {
        root /data/shop;
        index index.html;
    }
}

访问blog.test.com,将进入/data/blog

访问shop.test.com,将进入/data/shop

2.端口

例如:

server {
    listen 80;

    location / {
        root /data/web1;
    }
}


server {
    listen 8080;

    location / {
        root /data/web2;
    }
}

访问http://IP:80进入一个网站

访问http://IP:8080进入另一个网站

3.IP地址

例如:

服务器有两个IP:

192.168.1.10
192.168.1.20

配置:

server {
    listen 192.168.1.10:80;
}


server {
    listen 192.168.1.20:80;
}

Nginx中的虚拟主机对应什么配置?

核心即为:

server {}

一个server {}就是一个虚拟主机

例如:

server {

    listen 80;

    server_name www.test.com;


    location / {

        root /code/www;

        index index.html;

    }

}

这一整个块就是一个虚拟主机。

虚拟主机有什么作用?

1.一台服务器部署多个网站

2.不同域名对应不同业务

3.实现反向代理入口

一个完整的虚拟主机(server块)配置

server {

    listen 80;

    server_name www.test.com;

    root /data/www;

    index index.html index.php;


    location / {

        ...

    }


    access_log /var/log/nginx/www_access.log;

    error_log /var/log/nginx/www_error.log;

}

1.server块(虚拟主机主体)

server{

}

这是虚拟主机的边界

一个server{}代表一个虚拟主机

nginx收到请求后,会在所有的server中选择一个进行处理

2.listen(监听地址和端口)

listen 80;

作用:定义这个虚拟主机监听哪个端口

此处表示监听HTTP 80端口

listen 8080;则表示监听8080端口

还可以指定IP:

listen 192.168.133.130:80;

表示:只监听这个IP的80端口。

3.server_name(域名匹配)

server_name www.test.com;

作用:

根据HTTP请求中的Host字段匹配虚拟主机。

例如用户访问:

http://www.test.com

浏览器发送:

Host: www.test.com

Nginx寻找:

server_name www.test.com;

匹配成功后进入这个server。

4.root(网站根目录)

root /data/www;

作用:指定静态文件存放位置

例如:

目录:

/data/www
    |
    └── index.html

请求:

http://www.test.com/index.html

Nginx实际寻找:

/data/www/index.html

5.index(默认首页)

index index.html index.php;

作用:指定访问目录时默认打开的文件。

例如访问:

http://www.test.com/

配置

server {

    listen 80;
    server_name www.test.com;

    root /data/www;

    index index.html index.php;

}

Nginx会依次寻找:

index.html
index.php

Nginx会去找root指定的目录/data/www,然后查看index配置

如果/data/www/index.html存在,nginx就读取这个文件的内容然后返回给浏览器。

如果/data/www/index.html不存在,nginx就继续找/data/www/index.php,如果存在nginx就会继续判断这个文件是不是要交给PHP处理,如果有:

location ~ \.php$ {

    fastcgi_pass 127.0.0.1:9000;

}

那么:

index.php
   |
   |
 PHP-FPM
   |
执行PHP代码
   |
生成HTML
   |
返回浏览器

如果两个都不存在

像:

/data/www/
    a.html
    b.html

没有index.html和index.php

那么默认返回

404 Forbidden

或者根据配置可能显示目录列表(autoindex开启时)。

index不是文件,而是一组“默认首页候选名单”,Nginx按照配置顺序逐个寻找,找到第一个存在的文件后停止。

6.location(请求匹配规则)

这是虚拟主机中最重要的部分

location负责根据用户请求的URI,决定如何处理这个请求。

URI是什么?

请求路径

比如用户访问:

http://www.test.com/images/a.jpg

其中

/images/a.jpg

就是URI

Nginx收到请求后:

第一步:

server匹配:

Host: www.test.com            #Host请求头

找到:

server_name www.test.com;

第二步:

location匹配:

看URI:

/images/a.jpg

应该交给哪个location。

匹配到请求后,Nginx下一步干什么?

格式:

location 匹配规则 {

        处理方式

}

常见:

1.返回静态文件

配置:

location / {

    root /data/www;

}

意思是所有请求/进入这里

2.精确处理某类文件

例如:

location ~ \.jpg$ {

    root /data/images;

}

意思:

如果URL结尾是:

.jpg

进入这里。

访问:

/test.jpg

寻找:

/data/images/test.jpg

返回图片。

公式:

root目录 + URI

3.转发给PHP

例如:

location ~ \.php$ {

    fastcgi_pass 127.0.0.1:9000;

}

意思:

请求:

index.php

不要Nginx自己处理。

交给:

PHP-FPM

流程:

用户
 |
index.php
 |
Nginx
 |
fastcgi_pass
 |
PHP-FPM
 |
执行代码
 |
返回结果

4.反向代理

例如:

location / {

    proxy_pass http://backend;

}

意思:

请求不要本机处理。

转发给后端。

例如:

用户:

GET /api/user

Nginx:

proxy_pass http://backend

变成:

GET /api/user

↓

后端服务器

流程:

用户
 |
Nginx
 |
proxy_pass
 |
后端应用
 |
返回数据
 |
Nginx转给用户

5.直接返回内容

如echo模块:

location /main {

    echo "hello";

}

访问:

/main

直接返回:

hello

请求处理流程一条线

客户端请求
      |
      ↓
HTTP请求中的Host
      |
      ↓
server_name匹配虚拟主机
      |
      ↓
URI匹配location
      |
      ↓
location决定处理方式
      |
      ↓
静态文件 / PHP / 代理 / 返回内容
      |
      ↓
响应客户端

Alias

location负责匹配请求,alias负责修改请求对应的文件路径。

location:决定“这个请求交给谁处理”

alias:决定“去哪个真实目录找文件”

location通过root + URI拼接路径得到指定文件位置

而alias不是拼接,而是替换

例如:

location /images/ {

    alias /data/pictures/;

}

访问:

http://www.test.com/images/a.jpg

Nginx会:

把:

/images/

替换成:

/data/pictures/

最终找:

/data/pictures/a.jpg

关系:

请求:
/images/a.jpg


alias替换:


/data/pictures/a.jpg

日志配置

生产排障必须。

访问日志:

/var/log/nginx/access.log

错误日志:

/var/log/nginx/error.log

重点:

tail -f access.log

看:

  • 状态码
  • IP
  • URL
  • User-Agent