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-release 的 ID 取发行版。
坑:
/etc/issue、/etc/redhat-release、lsb_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:slim 或 distroless 更省心。
生产建议:开发和生产用同一个发行版大版本。「本地 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 error或No 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 -w 和 echo > /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 不用 which:which 是外部程序,它看不到别名、函数和内建命令,只搜 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 xxx 和 man 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:which 和 type 有什么区别?该用哪个?
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 却仍算它占空间——这就是「df 和 du 对不上」的经典原因。用 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: OOMKilled 和 Last State 也是同一件事的体现。另外要注意容器的 /dev/shm 默认只有 64MB,往里写大文件同样会触发 OOM。
xingliuhua