目录

Linux-01 认识 Linux 与目录结构

这是 Linux 系列的第一篇。整个系列按「基础 → 原理 → 系统 → 排查 → 实战 → 面试」的顺序展开,面向的是后端开发工程师——不是运维手册,重点是「线上出问题时你能不能定位到根因」。

本篇解决三个问题:Linux 到底指什么、目录为什么这么划分、敲下一个命令之后发生了什么。

1. Linux 是什么

一句话:Linux 严格来说只是一个内核,你日常用的 Ubuntu、CentOS 是「内核 + 一堆用户态软件」打包出来的发行版。

这个区分不是抠字眼,它直接决定了你排查问题时该往哪看:

+-----------------------------------------+
|  用户程序(你的 Go 服务、nginx、mysql) |
+-----------------------------------------+
|  Shell / 命令行工具(bash、ls、ps)     | <- 发行版提供
|  C 标准库(glibc / musl)               | <- 发行版提供
+-----------------------------------------+
|  系统调用接口(syscall)                | <- 稳定边界
+-----------------------------------------+
|  Linux 内核(调度、内存、文件、网络)   | <- Linus 维护的那个
+-----------------------------------------+
|  硬件                                   |
+-----------------------------------------+

关键在于:系统调用接口这条线是稳定的。Linus 有一条著名的铁律——「we do not break userspace」,内核可以随便重构,但不能让已有的用户程序失效。这就是为什么一个 2015 年编译的静态二进制,扔到 2026 年的内核上还能跑。

这条线也是本系列的主轴:往上是命令和工具的用法,往下是原理。

1.1 查看内核与发行版

这两个信息要分开看,用的命令也不同:

# 看内核版本
uname -r
# 5.15.0-91-generic
#  |  |  |
#  |  |  +- 修订号(发行版打的补丁)
#  |  +---- 次版本号
#  +------- 主版本号

# 看发行版(这个才是决定包管理器、目录习惯的东西)
cat /etc/os-release
# NAME="Ubuntu"
# VERSION="22.04.3 LTS (Jammy Jellyfish)"
# ID=ubuntu
# ID_LIKE=debian          <- 写脚本判断包管理器时看这一行最靠谱

uname -a 会把所有信息糊在一行,看着方便但不适合脚本解析。写自动化脚本时用 uname -r 取内核、用 /etc/os-releaseID 取发行版。

坑:/etc/issue/etc/redhat-releaselsb_release -a 这些老办法在不同发行版上存在与否不一致,lsb_release 甚至常常没装。只有 /etc/os-release 是 systemd 时代的统一标准,优先用它。

1.2 内核版本号意味着什么

作为后端开发,你需要关心内核版本,因为很多能力有版本门槛:

能力 最低内核版本 为什么后端要关心
epoll 2.6 所有高并发网络框架的地基
SO_REUSEPORT 3.9 多进程监听同一端口,解决惊群
cgroup v2 4.5(5.8 后成熟) 容器资源限制、K8s 的 cpu.max
BBR 拥塞控制 4.9 跨地域链路吞吐提升明显
eBPF / bpftrace 4.9+(5.x 才好用) 生产环境无侵入观测
io_uring 5.1(5.10+ 才稳) 真正的异步 IO

uname -r 看到 3.10 基本等于 CentOS 7——那台机器上 cgroup v2、eBPF、io_uring 全都别想,排查手段会退回到 strace/proc

2. 发行版怎么选

后端开发接触到的无非这几类:

发行版 包管理 典型用途 注意点
Ubuntu LTS apt 开发机、云服务器主力 每 2 年一个 LTS,支持 5 年
Debian apt 求稳的生产环境、Docker 基础镜像 软件版本偏旧但极稳
RHEL / Rocky / Alma dnf/yum 传统企业、金融 CentOS 8 已停更,别再用
Alpine apk 容器镜像(5MB 起) 用 musl 不是 glibc,是最大的坑
Amazon Linux dnf AWS 上 基于 RHEL 系

Alpine 的坑值得单独说:它用 musl libc 替代 glibc,导致 CGO 编译的 Go 程序、Java 的某些 JNI 库、Python 的预编译 wheel 在 Alpine 上会诡异地崩溃或找不到符号。纯静态编译的 Go 服务(CGO_ENABLED=0)用 Alpine 没问题,一旦开了 CGO,用 debian:slimdistroless 更省心。

生产建议:开发和生产用同一个发行版大版本。「本地 Ubuntu 22 跑得好、线上 CentOS 7 起不来」这种事,八成是 glibc 版本或内核特性差异,排查起来很浪费时间。

3. 目录结构:FHS 的设计逻辑

死记目录列表没用,理解划分依据才记得住。FHS(Filesystem Hierarchy Standard)的划分是按「谁的东西 + 能不能共享 + 能不能只读」三个维度切的

3.1 按用途分类的目录表

/
+-- bin, sbin, lib     -> 现代发行版都是指向 /usr/xxx 的软链接
+-- usr/               「系统软件」——可以只读挂载、可以多机共享
|   +-- bin            所有人可执行的命令(ls、python、go)
|   +-- sbin           需要 root 的命令(iptables、fdisk)
|   +-- lib            共享库(.so)
|   +-- local/         「本机手动装的软件」——包管理器不会碰这里
|   +-- share          架构无关的数据(文档、man page、时区表)
+-- etc/               「本机的配置」——只放配置,不放数据
+-- var/               「会变大的东西」——日志、缓存、队列、数据库文件
|   +-- log            日志(排查第一站)
|   +-- lib            服务的持久数据(mysql、docker 都在这)
|   +-- run -> /run     运行时状态(pid 文件、socket),重启即清空
+-- home/              普通用户家目录
+-- root/              root 的家目录(独立出来,因为 /home 可能是单独分区)
+-- tmp/               临时文件,重启可能被清空,任何人可写
+-- opt/               整包安装的第三方大软件,卸载 = 删目录
+-- proc/              内核与进程信息(伪文件系统,不占磁盘)
+-- sys/               内核与设备的可调参数(伪文件系统)
+-- dev/               设备文件
+-- boot/              内核镜像与引导程序
+-- mnt, media/        挂载点:mnt 手动挂,media 自动挂(U 盘、光盘)

3.2 三组最容易混的目录

/usr/bin vs /usr/local/bin

判断口诀:包管理器装的进 /usr/bin,你自己 make install 的进 /usr/local/bin

这个分界保证了「系统升级不会覆盖你手动编译的软件」,反过来「你手动装的也不会被 apt 意外卸载」。PATH/usr/local/bin 通常排在 /usr/bin 前面,所以手动装的版本会优先生效——这是自己编译新版 Go/Python 时能覆盖系统版本的原因。

# 验证 PATH 顺序
echo $PATH | tr ':' '\n'
# /usr/local/sbin
# /usr/local/bin      <- 手动装的优先
# /usr/sbin
# /usr/bin

/etc vs /var

/etc 放配置,/var 放数据。判断标准很简单:这个文件丢了能不能用备份的配置重建?能就该在 /etc,不能就该在 /var

这个区分在做备份策略时很关键:/etc 要备份(小、重要、难重建),/var/log 通常不备份(大、可丢),/var/lib/mysql 必须备份(数据)。

/tmp vs /var/tmp vs /dev/shm

目录 生命周期 底层 用途
/tmp 重启清空(或 10 天) 磁盘,有些发行版是 tmpfs 短期临时文件
/var/tmp 重启保留 磁盘 需要跨重启的临时文件
/dev/shm 重启清空 纯内存 tmpfs 进程间共享内存、要极快的临时文件

/dev/shm 是后端会用到的:它是 tmpfs,写进去的文件完全在内存里。两个容量数字要分清——宿主机上默认是物理内存的一半,而容器里默认只有 64MB(见下面的坑)。另外它算进 cgroup 的内存限制,所以在容器里往 /dev/shm 写大文件会直接触发 OOM Killer,而不是报磁盘满。

df -h /tmp /dev/shm
# Filesystem      Size  Used Avail Use% Mounted on
# /dev/vda1        40G   12G   26G  32% /
# tmpfs           7.8G     0  7.8G   0% /dev/shm    <- tmpfs = 内存

坑:Docker 容器的 /dev/shm 默认只有 64MB。Chrome headless、某些数据库客户端、PyTorch 的 DataLoader 都会因此崩溃,报错通常是含糊的 Bus errorNo space left on device。用 --shm-size=1g 解决。

4. /proc 与 /sys:内核的文件接口

这两个目录是伪文件系统——里面的文件不占磁盘,是内核在你 read() 的那一刻实时生成的。它们是 Linux 设计哲学「一切皆文件」最彻底的体现,也是后端排查问题的主战场。

分工/proc(进程和系统状态,只读为主),/sys(设备和内核参数,可写)。

4.1 /proc 里排查用得最多的

# 系统整体
cat /proc/cpuinfo        # CPU 型号、核数、标志位
cat /proc/meminfo        # 内存详情(free 命令就是读它)
cat /proc/loadavg        # 平均负载(uptime 读它)
cat /proc/stat           # CPU 累计时间(算 CPU 使用率的原始数据)
cat /proc/net/dev        # 网卡收发包统计

/proc/<pid>/ 下是单个进程的全部真相,这几个是排查时的常客:

PID=12345

ls -l /proc/$PID/fd | wc -l      # 打开了多少 fd —— 查 fd 泄漏
cat /proc/$PID/limits            # 该进程的真实 ulimit
cat /proc/$PID/status            # 线程数、内存占用、状态
cat /proc/$PID/cmdline | tr '\0' ' '   # 完整启动命令(参数用 \0 分隔)
ls -l /proc/$PID/cwd             # 进程的工作目录
cat /proc/$PID/environ | tr '\0' '\n'  # 进程的环境变量

/proc/<pid>/limits 值得单独强调ulimit -n 显示的是你当前 shell 的限制,而 systemd 启动的服务不读 /etc/security/limits.conf——它读 unit 文件里的 LimitNOFILE。所以「我改了 limits.conf,为什么服务还是 Too many open files」这个经典问题,唯一可信的验证方式是:

cat /proc/$(pgrep -f myapp)/limits | grep -i "open files"
# Max open files            1024      1024      files
#                           ^^^^ 真相在这里,不是 ulimit -n 说的 65535

/proc/<pid>/fd 是找回被删文件的后门。如果一个进程正在写日志、你 rm 掉了这个日志文件,磁盘空间不会释放(inode 引用计数还在),而且文件内容还能救:

# 场景:误删了正在写入的日志,磁盘也没释放
ls -l /proc/$PID/fd/
# lrwx------ 1 root root 64 Aug  6 20:00 3 -> /var/log/app.log (deleted)
#                                                              ^^^^^^^^^ 关键标记

# 内容还能读出来
cp /proc/$PID/fd/3 /tmp/recovered.log

这条技巧在「df 说磁盘满了但 du 加起来对不上」时也是答案——被删但仍被进程持有的文件,du 数不到。第 2 篇 §3.1 会详细讲这个机制,第 11 篇 §7 补全另外两种原因。

4.2 /sys 里改内核参数

/sys 可写,改的是内核运行时参数。改法有两种,效果一样但持久性不同:

# 方式一:直接写 /proc/sys(立即生效,重启丢失)
echo 65535 > /proc/sys/net/core/somaxconn

# 方式二:sysctl(同一个东西,路径分隔符从 / 换成 .)
sysctl -w net.core.somaxconn=65535

# 持久化:写进配置文件
echo "net.core.somaxconn = 65535" >> /etc/sysctl.d/99-tuning.conf
sysctl --system                  # 重新加载所有配置

注意sysctl -wecho > /proc/sys/... 是完全等价的两种写法,net.core.somaxconn 就是 /proc/sys/net/core/somaxconn。理解这一点后,看到任何调优文章里的 net.ipv4.tcp_xxx 参数,你都知道去哪确认它的当前值。

5. 敲下一个命令之后发生了什么

这一节是「命令行心智模型」。搞清楚它,可以省掉大量「为什么我装了但找不到命令」「为什么 sudo 之后行为变了」的困惑。

5.1 命令的四种身份与查找顺序

输入 ls 之后,Shell 按固定优先级决定执行什么:

1. 别名 alias        <- alias ls='ls --color=auto'
   v 没有
2. 函数 function     <- 你在 .bashrc 里定义的
   v 没有
3. 内建命令 builtin  <- cd、echo、export(Shell 自己实现,不 fork)
   v 没有
4. 按 PATH 顺序找外部可执行文件,第一个命中的胜出
   v 都没有
   command not found

type 查身份,这比 which 靠谱得多:

type -a ls
# ls is aliased to `ls --color=auto'     <- 别名
# ls is /usr/bin/ls                       <- 实际的二进制

type cd
# cd is a shell builtin                   <- 内建,没有 /usr/bin/cd

type -a python3
# python3 is /usr/local/bin/python3       <- 手动装的,优先
# python3 is /usr/bin/python3

为什么用 type 不用 whichwhich 是外部程序,它看不到别名、函数和内建命令,只搜 PATH。所以 which cd 什么都找不到,而 cd 明明能用。type 是 Shell 内建,它知道 Shell 的全部上下文。

坑:cd 是内建命令,这解释了一个常见困惑——为什么 cd /tmp 写在脚本里能改目录,但 (cd /tmp)cd /tmp | cat 就不行。管道和子 shell 会 fork 出新进程,工作目录的改变留在了子进程里。第 5 篇讲 Shell 编程时会展开。

5.2 命令找不到的三种原因

# 原因一:真的没装
myapp
# bash: myapp: command not found

# 原因二:装了但不在 PATH 里
ls /opt/myapp/bin/myapp        # 文件明明在
echo $PATH | grep -o /opt      # 但 PATH 里没有 /opt/... -> 空输出

# 原因三:Shell 缓存了旧路径(最阴的一种)
hash -r                        # 清掉命令路径缓存
# 场景:你把 /usr/local/bin/go 升级了,但 shell 还记着老的 inode

第三种值得记住:Bash 会缓存命令的绝对路径以加速查找。如果你移动或替换了一个二进制,同一个 shell 里可能仍然执行老的。hash -r 清缓存,或者干脆开个新 shell。

5.3 sudo 会改变环境

这是后端最常踩的一个坑:

echo $PATH
# /usr/local/go/bin:/usr/local/bin:/usr/bin:/bin

sudo echo $PATH
# /usr/local/go/bin:/usr/local/bin:/usr/bin:/bin
#  ^ 这里显示的是【当前用户】的 PATH,因为 $PATH 在传给 sudo 之前就被展开了

sudo bash -c 'echo $PATH'
# /usr/sbin:/usr/bin:/sbin:/bin        <- 这才是 sudo 里真实的 PATH

原因sudo 默认启用 secure_path(在 /etc/sudoers 里配置),会把 PATH 重置为一个安全的固定值。所以「我能跑 go build,但 sudo go build 报 command not found」就是这个原因。

解决办法按推荐度排序:

# ✅ 用绝对路径(最稳)
sudo /usr/local/go/bin/go build

# ✅ 让 sudo 保留环境(需要 sudoers 允许)
sudo -E env "PATH=$PATH" go build

# ❌ 改 /etc/sudoers 的 secure_path —— 降低了安全性,不推荐

6. 帮助系统:不要靠搜索引擎

线上机器经常没有外网。学会本地查文档是基本功。

# 内建命令用 help
help cd

# 外部命令用 man,注意分节
man ls              # 第 1 节:用户命令
man 2 read          # 第 2 节:系统调用  <- 后端最该看的一节
man 5 crontab       # 第 5 节:文件格式  <- 配置文件语法在这
man 7 signal        # 第 7 节:概念综述  <- 讲原理的在这

# 不知道在哪一节
man -f signal       # 等价于 whatis
# signal (7)  - overview of signals
# signal (2)  - ANSI C signal handling

man 的分节是最容易被忽略的一点man 2 xxxman 7 xxx 是后端工程师的宝藏:

想知道 该看
epoll 到底怎么用、ET/LT 区别 man 7 epoll
write() 返回值可能小于请求长度吗 man 2 write
信号的完整列表和默认动作 man 7 signal
crontab 里的 % 为什么要转义 man 5 crontab
TCP 的 socket 选项有哪些 man 7 tcp

这些内容比大多数中文博客准确,而且是你机器上那个版本的准确行为。

# man 内部快捷键(其实就是 less)
# /关键字   向下搜索
# n / N     下一个 / 上一个匹配
# G / g     跳到末尾 / 开头
# q         退出

7. 登上一台陌生机器先做什么

把前面的内容串成一个实用流程。接手一台不熟悉的服务器,按这个顺序看,两分钟就能建立基本认知:

# ① 我是谁,在哪
whoami; hostname; pwd
id                       # uid/gid/所属组 —— 决定你能干什么

# ② 什么系统,什么内核
cat /etc/os-release | head -2
uname -r

# ③ 硬件规格(决定后面所有指标的判读基准)
nproc                    # CPU 核数
free -h                  # 内存
df -h                    # 磁盘和挂载点
# ④ 现在忙不忙
uptime
# 20:14:02 up 42 days,  3:11,  2 users,  load average: 0.52, 0.61, 0.58
#                                                      ^^^^ 和 nproc 比:0.52/4 核 = 很闲

# ⑤ 谁在占资源
ps aux --sort=-%cpu | head -6
ps aux --sort=-%mem | head -6

# ⑥ 开了哪些端口 —— 最快搞清这台机器在干什么
ss -tlnp
# LISTEN 0  4096  0.0.0.0:8080  0.0.0.0:*  users:(("myapp",pid=1234,fd=3))
# ⑦ 有什么服务在跑
systemctl list-units --type=service --state=running | head -20

# ⑧ 最近出过什么事
journalctl -p err -n 30 --no-pager     # 只看 error 级别
dmesg -T | tail -30                    # 内核日志(OOM、磁盘错误都在这)

第 ⑧ 步的 dmesg -T | grep -i oom 是排查「服务莫名消失」的第一反应。进程被 OOM Killer 杀掉时,应用日志里往往什么都没有(因为是 SIGKILL,没机会写日志),唯一的证据在内核日志里:

dmesg -T | grep -i "killed process"
# [Wed Aug  6 03:12:44 2026] Out of memory: Killed process 12345 (myapp)
#   total-vm:8388608kB, anon-rss:7340032kB, file-rss:0kB

规律:应用日志里查不到死因时,去内核日志里找。

8. 一些量级感

排查问题时,「这个数字是不是不正常」需要基准。先记住这些量级,后面各篇会逐个展开:

典型量级
一次系统调用(如 getpid 50 ~ 200 ns
一次进程上下文切换 1 ~ 5 μs
内存随机访问 ~100 ns
SSD 随机读一次(4K) 50 ~ 150 μs
机械盘随机读一次 5 ~ 10 ms
同机房网络往返 0.2 ~ 0.5 ms
跨地域网络往返(国内) 20 ~ 50 ms
单核每秒能处理的空 HTTP 请求 数万
平均负载「满载」的判断线 ≈ CPU 核数

用法举例:接口 P99 是 200ms,而下游都在同机房。同机房 RTT 只有 0.5ms,说明 199ms 花在计算或等锁上,不是网络问题——这一步判断就能砍掉一半排查方向。

9. 面试题

Q:Linux 和 Unix 是什么关系?CentOS 和 Linux 是什么关系?

Linux 是一个从零重写的、兼容 POSIX 接口的内核,它不包含任何 Unix 源码,所以只是「类 Unix」而非 Unix 的分支。真正的 Unix 后裔是 BSD 系(FreeBSD、macOS 的 Darwin)和 System V 系(AIX、Solaris)。CentOS 是发行版,等于「Linux 内核 + GNU 工具链 + 包管理器 + 一套默认配置」,同一个内核版本可以被打包成完全不同的发行版。

Q:/usr/bin/usr/local/bin 有什么区别?为什么要分?

/usr/bin 归包管理器管,/usr/local/bin 留给手动编译安装的软件。分开的目的是让两者互不干扰:系统升级不会覆盖你 make install 的版本,apt remove 也不会误删。PATH/usr/local/bin 通常排在前面,所以手动装的版本优先生效。注意/usr 的设计前提是「可只读挂载、可多机共享」,所以它下面不该出现会变化的数据,那些东西属于 /var

Q:/proc 占磁盘空间吗?df 为什么显示它是 0?

不占。/proc 是 procfs 伪文件系统,所有文件都是内核在被读取的瞬间动态生成的,不落盘。所以 ls -l /proc/meminfo 显示大小为 0,但 cat 有内容;du -sh /proc 也是 0。同理 /sys(sysfs)、/dev/shm(tmpfs,这个占内存)都不占磁盘。

Q:whichtype 有什么区别?该用哪个?

which 是外部程序,只按 PATH 搜可执行文件,看不到别名、Shell 函数和内建命令,所以 which cd 会失败。type 是 Shell 内建,能识别全部四种身份并按真实优先级给出结果,type -a 还会列出所有同名候选。排查「为什么执行的不是我以为的那个」时用 type -a

Q:为什么 ulimit -n 显示 65535,服务还是报 Too many open files?

因为 ulimit -n 反映的是当前 shell 继承到的限制,而进程的限制是启动时从父进程继承的。systemd 启动的服务不读 /etc/security/limits.conf(那是 PAM 的机制,只对登录会话生效),它读 unit 文件里的 LimitNOFILE=。唯一可信的验证方式是 cat /proc/<pid>/limits,看那个进程真实的限制值。

Q:删了一个大日志文件,为什么 df 显示的磁盘空间没有释放?

因为写这个文件的进程还持有它的文件描述符。Linux 删除文件本质是删目录项(unlink),只有当「硬链接数为 0」且「没有任何进程打开它」时,inode 和数据块才真正释放。此时 du 数不到这个文件(目录项已没了),df 却仍算它占空间——这就是「dfdu 对不上」的经典原因。用 lsof | grep deleted 定位,然后重启或 reload 该进程释放 fd;文件内容还能从 /proc/<pid>/fd/<n> 里复制出来。

Q:sudo 之后为什么找不到命令了?

sudo 默认开启 secure_path,会把 PATH 重置为 /usr/sbin:/usr/bin:/sbin:/bin 这样一个固定的安全值,不继承调用者的 PATH。所以装在 /usr/local/go/bin 之类非标准位置的命令就找不到了。用绝对路径调用,或 sudo -E env "PATH=$PATH" cmd注意sudo echo $PATH 看不出问题,因为 $PATH 在传给 sudo 之前就已经被当前 shell 展开了,要用 sudo bash -c 'echo $PATH' 才能看到真实值。

Q:容器里的服务莫名重启,应用日志什么都没留下,怎么查?

优先怀疑被 OOM Killer 杀了。因为 OOM Killer 发的是 SIGKILL,进程没有任何机会执行清理逻辑或写日志,应用侧必然一片空白。证据只在内核日志里:dmesg -T | grep -i "killed process",能看到被杀进程的 PID、名字和当时的内存占用。如果是 K8s,kubectl describe pod 里的 Reason: OOMKilledLast State 也是同一件事的体现。另外要注意容器的 /dev/shm 默认只有 64MB,往里写大文件同样会触发 OOM。


下一篇:Linux-02 文件、链接与归档压缩