简介:《Unix/Linux基础讲义》是一份面向操作系统入门者与运维、开发初学者的文档型学习材料,帮助读者建立从操作系统整体定位到 Unix、Linux 两大分支演进的完整认知框架。内容自操作系统向下封装硬件、向上提供操作接口的承上启下作用讲起,梳理 Unix 家族从 MULTICS、UNICS 到 BSD 与 AT&T 双线竞争及版权纠纷的发展脉络,也交代 Linux 由 Minix、Linus 内核、GNU 计划、POSIX 标准与互联网五大支柱共同催生的过程。压缩包内含 1 个 doc 文档,体积约 13.46MB,篇幅完整,可直接作为课堂讲义或自学底稿翻阅。讲义还涉及 Linux 的两类用户、内核稳定版与开发版之别、主流发行版对比、X Window 的客户机/服务器结构,以及服务器与嵌入式等应用领域,便于把零散概念串成体系。目前已有 325 人学习,适合需要系统梳理基础概念、准备课程复习或计划转行入门的技术人员。
1. 一切皆文件:一条斜杠背后藏着的统一抽象
有人第一次在终端敲cat /proc/cpuinfo会愣一下:CPU 信息凭什么能用「看文件」的方式读出来?更别扭的是echo x > /dev/null和ls | grep conf——一个往「设备」里写字符串,一个把某个进程的输出当数据流喂给另一个进程。这三件事能被同一组open / read / write / close覆盖,不是巧合,而是 Unix/Linux 在几十年前做的一次取舍:把设备、管道、目录、网络连接统统抽象成字节流,用一套接口来管理。
这套抽象解决的是多用户系统最头疼的问题——对象太多、驱动太多、接口太多。统一之后,命令不需要认识硬件,脚本不需要认识分区,权限模型也只需要管一种东西。值得把它搞懂的原因很实在:后面的路径解析、挂载、权限位、重定向,全都是这条抽象推下去的必然结果;只背结论,第一次踩坑就会全盘失效。
2. 系统调用层:为什么同一套接口能跨硬件、跨发行版
2.1 从汇编到高级语言重写:可移植性是怎么换来的
多用户分时系统最初的三条目标写得很朴素:让机器好用、让资源用得更满、让新功能容易加进来。这三条听着像口号,落到实现上却有一个硬约束——内核必须统一掌管 CPU 时间、内存、设备和文件,否则「多用户同时跑多个任务」这件事根本无法保证公平与隔离。而统一掌管的前提,是这份掌管逻辑本身能被持续维护、能搬到新机器上。
早期系统几乎全用汇编写,代价很直接:换一套硬件,中断入口、寄存器操作、内存映射全部要重写一遍,逻辑部分也跟着遭殃。真正的转折点是用高级语言重写内核——把「跟机器打交道」的代码压缩成薄薄一层,剩下的调度、内存管理、文件系统逻辑用与硬件无关的方式表达。这层划分至今还在,可以粗略对照:
| 层次 | 典型内容 | 是否随硬件变化 |
|---|---|---|
| 机器相关层 | 启动入口、中断向量、页表操作、上下文切换汇编段 | 换架构就要改 |
| 机器无关层 | 进程调度、内存分配策略、VFS、网络协议栈 | 基本不动 |
| 接口层 | 系统调用号表、参数校验、用户态与内核态切换 | 按 ABI 稳定 |
正是这条切分,让「在通用 x86 机器上跑出大型机的全部特性」从不可能变成了工程量问题。用户态程序看到的永远是一组稳定的调用号,底下的寄存器怎么摆、页表几级,跟它没关系。写一段 C 就能验证这种「只能通过接口触达内核」的边界:
#include <stdio.h> #include <unistd.h> #include <sys/utsname.h> int main(void) { struct utsname u; /* uname 是系统调用封装:用户态无法直接读内核结构,只能要一份拷贝 */ if (uname(&u) == 0) { printf("sysname=%s release=%s machine=%s\n", u.sysname, u.release, u.machine); } /* getpid 同样是「问内核要个数字」,而不是读某个全局变量 */ printf("pid=%d ppid=%d\n", getpid(), getppid()); return 0; }uname和getpid属于同一类东西:用户态进程没有任何合法手段直接访问内核里的数据结构,唯一通道是「陷入内核 → 内核填好结构体 → 拷回用户空间」。utsname这个结构体的字段顺序和大小由 ABI 固定,改它等于改接口,所以几十年没人动。理解这一点,后面看到/proc里那些「看起来像文件的东西」就不会觉得玄学——它们只是内核把内部状态按文件接口重新暴露了一遍。
内核自身的版本也遵循类似的分层思路。传统上稳定版与开发版交替循环:开发版快速试验新方案,稳定版只收 bug 修复和驱动补充。早年用次版本号的奇偶来区分两者,后来这条约定被取消,改成按固定周期滚动发布,稳定分支靠长期维护窗口兜底。规则变了,动机没变——要让下游敢用,就必须有一份「只修不加」的基线。
2.2 GNU 与 POSIX:接口标准如何把内核和外壳拆开
只靠内核跑不起来任何有用的东西。一份能用的系统至少还需要:能执行命令的 shell、能编译代码的编译器、能调试的调试器、以及一大堆基础库。这些东西由另一条线独立发展而来,形成的工具集与内核是两套代码、两套发布节奏。它们的结合点就是接口标准。
POSIX 干的事情可以一句话概括:把「应用程序能向操作系统要什么」写成清单,而不是规定「操作系统内部怎么做」。清单里包括open、read、write、fork、exec、pipe、信号语义、终端行为等等。于是出现了清晰的三段式调用链:
# 你在终端敲的 cat /etc/hostname # 实际发生的事(简化): # 1. shell 解析出命令名 cat 与参数 /etc/hostname # 2. shell 调 fork 复制自身,子进程调 execve 加载 /bin/cat # 3. cat 内部调 open("/etc/hostname", O_RDONLY) -> 得到 fd # 4. cat 循环调 read(fd, buf, n),再调 write(1, buf, n) 写到标准输出 # 5. 读完调 close(fd),进程退出,shell 用 wait 收尸这条链上,cat完全不知道/etc/hostname在哪个分区、是不是真实磁盘、甚至是内存里的伪文件系统——它只知道「有个 fd,可以 read」。接口标准把「谁实现」和「谁使用」彻底解耦,这也是为什么同一份源码换个发行版、换个 CPU 架构还能编译通过。
这种解耦在帮助文档里留下了非常直观的痕迹。手册按章节分家:第 1 节是标准命令,第 2 节是系统调用,第 3 节是库函数,第 4 节是设备文件说明,第 5 节是文件格式,第 8 节是管理员命令。所以查open要写man 2 open,查printf的库函数行为要写man 3 printf,而man printf命中的是第 1 节的同名命令。名字撞车时,手册按章节号顺序搜索,先命中的就是命令那一节——这就是不带章节号常常查到错误的原因。
man 2 open # 系统调用:内核提供的 open man 3 printf # 库函数:C 标准库的 printf man 5 passwd # 文件格式:/etc/passwd 的字段含义 man 8 mount # 管理员命令:挂载参数说明:man的第一个数字参数就是章节号,不是选项;省略时按 1 到 8 顺序搜。日常排查里,「文档说的和我看到的不一样」十有八九是章节号没指定,查到了另一个同名对象。
2.3 内核版本与发行版:一个心脏,两套外衣
同一个词常被用来指两样东西,这是很多混淆的源头。内核心脏负责的是进程调度、内存管理、虚拟文件系统、网络协议栈和设备驱动,它对外只承诺系统调用;发行版则是「内核 + 一大批用户态软件 + 包管理 + 安装器 + 桌面环境」的整包交付。两者的关系可以用三层来看:
| 层 | 内容 | 变化频率 | 谁负责 |
|---|---|---|---|
| 内核 | 调度、内存、VFS、网络栈、驱动 | 跟随内核主线 | 内核社区 |
| 基础用户态 | shell、核心工具集、C 库、编译器 | 跟随工具链版本 | 各上游项目 |
| 发行版附加 | 包管理器、服务管理、桌面、预置配置 | 跟随发行版节奏 | 发行版维护方 |
这个结构解释了两个常见现象。第一,同一条ls -l在不同发行版上输出格式几乎一致,因为工具集来自同一套上游实现,行为由标准约束;第二,/etc一定放配置、/var一定放可变数据,因为目录布局由文件系统层次标准统一,不是各家随意发挥。桌面和应用是外衣,文件组织与目录结构才是内核之外真正稳定的那部分。
3. 文件系统:单根树与挂载点是怎么把多块磁盘缝成一棵树的
3.1 挂载:为什么没有 C 盘 D 盘
另一类系统的做法是给每个分区一个盘符,形成多棵树并列;Unix/Linux 反过来,只保留一棵树,所有分区都挂到某个目录上,成为这棵树的一个子树。这个差异看着只是习惯问题,实际上决定了路径的可移植性——一个脚本里写/var/log/app.log,在任何机器上都指向「当前系统里负责放可变日志的那个位置」,不需要先判断数据落在哪个盘。
挂载的机制要说清楚,得先有「路径解析」的概念。内核解析/mnt/data/report.csv时,会从根开始逐段查目录项:走到/mnt,发现这个目录上挂着一个文件系统,于是切换到这个文件系统继续往下查,data、report.csv都在新文件系统里解析。挂载点本质上是「目录项 → 另一个文件系统根」的跳转,路径解析过程中可以多次跨越。
mount /dev/sdb1 /mnt/data # 把 sdb1 这个文件系统接到 /mnt/data 上 df -h # 看各文件系统的容量与挂载点 cat /proc/mounts # 内核视角的挂载表,最权威 findmnt /mnt/data # 反查某个目录属于哪个文件系统mount的第一个参数是设备或镜像,第二个参数是挂载点目录,必须已存在。df -h的-h是以人类可读单位显示容量。/proc/mounts由内核实时生成,比mount不带参数的输出更接近真相,排查「到底挂没挂上」时优先看它。
这里有个容易被忽略的细节:挂载点目录本身的原有内容会被「盖住」。如果/mnt/data里原来放着几个文件,挂上新文件系统后这些文件不会消失,但通过路径访问不到了,卸载后才重新可见。很多「文件明明在却读不到」的事故就出在这一层——写入时挂载还没生效,数据落在了根分区的挂载点目录里,等挂载完成后一看是空的。判断方法很直接:df看该路径属于哪个文件系统,和预期不一致就是没挂上。
3.2 FHS 的分工:/etc、/var、/proc 各自解决什么问题
目录布局不是随便定的,它按「内容性质」分类:哪些是配置、哪些是可变的运行时数据、哪些是只读的程序与库、哪些是设备接口。这套约定统一之后,备份、只读挂载、容器镜像分层才有了可操作的边界。
| 目录 | 放什么 | 可变性 | 典型误用 |
|---|---|---|---|
/etc | 系统与服务的配置文件 | 低频、需持久 | 往里放可执行文件或日志 |
/var | 日志、缓存、队列、运行时数据 | 高频变化 | 单独分区不给容量,日志写满根分区 |
/usr | 程序、共享库、文档 | 装完基本不变 | 手工往/usr/bin塞自编译程序 |
/proc | 内核与进程状态的内存视图 | 实时生成 | 当成磁盘目录做备份 |
/tmp | 临时文件,任何人可写 | 可能被清理 | 当数据仓库长期存放 |
/dev | 设备文件 | 由内核/设备管理生成 | 手工mknod造设备节点 |
/home | 用户家目录 | 用户自由 | 与系统分区混放,重装系统丢数据 |
/usr与/usr/local的分工值得单独说一句:前者归发行版包管理器管,后者留给管理员自己装的软件。把自编译程序装进/usr/bin,下次包管理器升级时会被覆盖或冲突,这就是「装完能用,升级后消失」的常见来源。
/proc是「一切皆文件」最激进的落地。它不占磁盘,内容全是内核在读取瞬间现算出来的:
cat /proc/cpuinfo # 每个逻辑 CPU 的型号、频率、标志位 cat /proc/meminfo # 内存总量、可用、缓存、交换分区 ls -l /proc/self/fd # 当前进程打开的所有文件描述符,像普通目录一样列 cat /proc/self/cmdline | tr '\0' ' ' # 当前进程的启动参数/proc/self是个特殊软链接,指向「读它的那个进程」自己的目录,所以同一行命令在不同进程里执行会看到不同结果。cmdline的参数之间用\0分隔,直接cat会看到连成一串,用tr换成空格才可读。这类细节只有在理解「伪文件系统是接口不是存储」之后才不会踩坑。
3.3 inode 与链接:文件名其实不是文件
目录项与文件实体的分离,是这套文件系统设计里最反直觉、也最有解释力的一点。一个文件的真实身份是 inode——记录类型、权限、属主、大小、时间戳和数据块位置;目录里存的只是「名字 → inode 号」的映射。名字可以换,inode 不变;同一个 inode 可以被多个名字指向,这就是硬链接。
ln report.txt report-hard.txt # 硬链接:两个名字指向同一 inode ln -s /data/report.txt report-soft.txt # 软链接:独立 inode,内容是一条路径 stat report.txt # 看 inode 号、链接计数、访问/修改/变更时间 ls -li report.txt report-hard.txtstat输出里的 Links 就是指向该 inode 的名字个数。给硬链接再加一个名字,计数加一;删除一个名字,计数减一,减到零且没有进程打开它,数据块才真正释放。这解释了一个经典困惑:rm掉日志文件后df显示空间没变——某个服务还开着这个文件的 fd,inode 引用计数没归零,空间自然收不回,得重启服务或截断文件才行。
软链接与硬链接的差别不只是「能不能跨分区」。软链接是一个独立文件,内容是路径字符串,目标删掉后链接变悬空;硬链接没有「目标」概念,它就是同一个 inode 的另一个名字,删掉任何一个名字数据都还在。硬链接有两条硬限制:不能跨文件系统,普通用户不能给目录建——后者是为了防止目录树出现环,让遍历算法死循环。
4. 权限位:九个字符为什么能管住多用户系统
4.1 rwx 与 4/2/1:权限位的编码方式
多用户系统的前提是「不同人对同一份资源有不同待遇」。这套模型把主体分成三类:文件所有者、所有者所在的组、其他所有人。每类主体配三个动作:读、写、执行。三三得九,正好九位,用三位八进制表达最省事——读是 4、写是 2、执行是 1,相加就是一位数字。
chmod 754 script.sh # 属主 rwx(7),组 r-x(5),其他 r--(4) chmod u+x script.sh # 等价写法:给属主单独加执行位 chmod g-w,o= script.sh # 组去掉写位,其他全部清空 ls -l script.sh # 输出形如 -rwxr-xr-- 1 user group ...ls -l那串字符要分段读:第一位是类型(-普通文件、d目录、c字符设备、b块设备、l软链接、p管道、s套接字),后面九位三三分组,对应属主、组、其他。chmod的数字写法一次改九位,符号写法适合做增量修改——脚本里推荐数字写法,因为它幂等;手工调权限推荐符号写法,避免误清已有位。
只有文件属主或管理员才能改权限,这条限制是整个模型能自洽的基础。否则任何用户都能给自己不拥有的文件加写位,隔离就形同虚设。实际判定时还有一条容易记错的规则:属主匹配就只看属主位,不再往后看组位和其他位。给文件设成--x------,属主自己反而读不了,这不算 bug,是判定顺序决定的。
4.2 目录上的 x 和文件上的 x 不是一回事
同一个字母在两个对象上含义完全不同,这是权限模型里翻车率最高的一处。对文件,r是读内容、w是改内容、x是把文件交给内核去执行;对目录,r是能列出目录项名字、w是能在目录里增删改条目、x是能穿过这个目录去访问下一层。
# 目录只给 r 不给 x 会怎样 mkdir /tmp/d && touch /tmp/d/a.txt chmod 400 /tmp/d ls /tmp/d # 能看到 a.txt 这个名字(r 生效,目录项可读) cat /tmp/d/a.txt # 报 Permission denied(没有 x,穿不过去) stat /tmp/d/a.txt # 同样失败,连 inode 都拿不到反过来,目录只有x没有r,表现更反直觉:你可以cd进去,也可以访问已知文件名,但ls列不出来。因为列目录需要读目录项,而穿越只需要执行位。这种配置在某些「允许访问但隐藏列表」的场景里是有意为之。
还有一条:目录的w位管的是「增删目录项」,不是「改文件内容」。一个有目录写权限的人,可以删掉目录里任何一个文件,哪怕他对该文件本身没有任何权限——删除动的是目录项,不是文件内容。所以把/tmp设成所有人可写的同时,必须挂上粘滞位(chmod +t),否则谁都能删别人的临时文件。这也是/tmp权限位里那个t的来历。
4.3 判定顺序与 umask:新建文件为什么默认是 644
新建文件不会一上来就777,中间有一道掩码。规则是:文件的基础权限是666(默认不给执行位,执行权必须显式授予),目录的基础权限是777,然后与umask取反后做按位与。
umask # 常见输出 0022 touch a.txt && ls -l a.txt # -rw-r--r-- = 666 & ~022 mkdir d && ls -ld d # drwxr-xr-x = 777 & ~022 umask 077 # 改成只允许自己访问 touch b.txt && ls -l b.txt # -rw------- = 666 & ~077umask 022的含义是「从默认权限里剥掉组写位和其他写位」。文件默认不给执行位,是因为执行一个来路不明的文件风险远高于读它,默认保守;目录必须给执行位,否则谁都进不去,所以目录默认755。
这套模型的取舍很清楚:简单到能在脑子里算,代价是表达力有限。三个主体、三个动作,遇到「只允许某两个人访问」这种需求就无解了,得靠访问控制列表或者干脆换一种授权方式。日常 90% 的场景够用,剩下 10% 才是复杂机制登场的地方。
5. 排查与避坑:这几类翻车现场,几乎都出在同一层
5.1 权限类:报错信息只说结果,不说原因
现象:用sudo执行完一条写文件的命令,之后普通用户改不动这个文件了,编辑器提示无权限。
原因:sudo执行时进程的有效用户变成了管理员,open带O_CREAT创建出来的文件属主就是管理员,普通用户自然没有写位。问题不在权限配置,在「谁创建了它」。
解决:先ls -l看属主,确认是自己误用提权造成的,用chown改回;更稳的做法是写文件时不要整条命令提权,只在必须提权的环节用,或者用sudo tee接收标准输入而不用sudo >。
ls -l /etc/app.conf sudo chown "$USER" /etc/app.conf # 改回普通用户所有 # 要提权写内容又不改属主时,用 tee 承接重定向 echo "key=value" | sudo tee -a /etc/app.conf > /dev/null现象:静态服务启动成功,但浏览器访问返回 403,日志里写着「Permission denied」,文件权限看着没问题。
原因:路径上某一级目录缺少x位。服务进程要打开文件,必须能穿过从根到该文件的所有目录,任何一级目录没有执行位都会被拦下。文件本身644完全没问题,卡在父目录。
解决:从根开始逐级namei -l /path/to/file看每级目录的权限,把缺的x补上;注意别用chmod -R 777一把梭,那只是把问题换成安全问题。
5.2 路径与挂载类:相对路径和挂载点最会骗人
现象:脚本手工跑正常,加进定时任务就失败,报找不到文件。
原因:定时任务的执行环境没有登录时的当前目录,工作目录通常落在某个系统目录,脚本里的相对路径全部解析到错误位置。手工执行时恰好cd在正确目录,所以看不出问题。
解决:脚本开头统一cd "$(dirname "$0")"或者全部改用绝对路径;脚本里涉及的外部命令也写绝对路径或显式设置PATH,因为定时任务的PATH往往比登录 shell 短得多。
现象:往/mnt/data里写了一批文件,df显示该挂载点容量没变化,重启后文件不见了。
原因:写入时挂载还没生效,数据落到了根分区上那个「挂载点目录」里;挂载完成之后,该目录被新文件系统的根覆盖,原来写进去的东西通过路径访问不到,看起来就像丢了。
解决:写入前先确认df -h /mnt/data显示的文件系统是不是预期的那个;更彻底的办法是在写入脚本里加一道findmnt检查,挂载不成功就直接退出,不要让数据落到错误的位置。
注意:挂载点目录里的旧内容不会消失,只是被遮住。卸载后如果发现文件回来了,说明当时确实没挂上。
5.3 命令行与重定向类:一次失误就能清空配置
现象:想给配置文件追加一行,敲了echo "x=1" > /etc/app.conf,整份配置被一行覆盖。
原因:单个>是截断写入,打开文件时就把长度清零了,跟「追加」完全是两件事。命令执行成功、没有报错,所以很容易以为没问题。
解决:追加用>>;改关键配置前先cp app.conf app.conf.bak留一份后悔药;养成「重定向到系统文件前先ls -l看一眼」的习惯。
cp /etc/app.conf /etc/app.conf.bak # 改之前先备份 echo "x=1" >> /etc/app.conf # 追加,不是覆盖现象:在错误的目录下执行了带通配符的删除命令,一批文件没了。
原因:通配符是 shell 在调用命令之前展开的,rm *里的*由 shell 替换成当前目录下所有非隐藏文件名,然后才交给rm。命令本身根本不知道你原本想删哪个目录,它只收到一串已经展开好的路径。
解决:危险操作前先pwd确认位置,再用ls加同样的通配符预演一遍展开结果,确认无误后把ls换成目标命令;批量删除优先用find配合更精确的条件,别依赖当前目录。
提示:
?、*、[]这些字符在方括号内会失去通配符含义,需要匹配字面量时用反斜杠转义,比如ls \*a找的是名字真叫*a的文件。
6. 设计取舍:抽象层的边界划在哪里,决定了这套系统能活多久
回头看,这套系统活了几十年,靠的不是某一项技术领先,而是几条抽象约定的边界划得足够克制。
第一处取舍是单根树对多盘符。多盘符直观,用户一眼能看出数据在哪个物理设备上;单根树牺牲了这种直观,换来了路径的稳定语义——/var/log在任何机器上都指向同一个逻辑位置,脚本、配置、容器镜像都不用关心底层是几块盘。代价是挂载点被覆盖这类反直觉行为会一直存在,只能靠约定和检查习惯去消化。
第二处是「一切皆文件」。它把设备、管道、套接字、内核状态全部压进一组open/read/write/close,让命令和脚本有了极强的组合能力。代价是表达力被压平了:文件接口只能表达「字节流」,表达不了「设置波特率」「查询网络接口状态」这类带参数的操作,于是必须长出一个旁路接口来补——各种设备控制调用就是这道补丁。统一性和表达力之间的这条缝,至今没能完全缝上。
第三处是权限模型的三类主体。它简单到能在脑子里算,代价是粒度粗:要表达「只允许某两个特定用户访问」就得换机制。这套模型的价值在于默认安全、理解成本低,而不是覆盖所有场景。
| 取舍点 | 选了什么 | 放弃了什么 | 事后看是否值得 |
|---|---|---|---|
| 目录结构 | 单一根、挂载点接入 | 物理设备的直观可见性 | 值得,路径可移植性收益极大 |
| 资源抽象 | 一切皆文件 | 富语义的专用接口 | 基本值得,靠旁路接口打补丁 |
| 权限模型 | 三类主体 × 三动作 | 精细化授权 | 值得,但必须承认边界 |
| 内核边界 | 只暴露系统调用 | 用户态直接操作硬件的能力 | 值得,隔离与可移植性都靠它 |
想验证这些取舍是否成立,最直接的办法是做几组对照实验:把一个服务部署到不同分区布局的机器上,看配置里有没有硬编码设备路径;把一段读写逻辑从普通文件改成管道和设备文件,看代码要不要改;给同一个文件配不同 umask 新建,看权限差异。能通过这几组的,就是从抽象层受益的部分;通不过的,就是抽象层没覆盖到、必须靠额外约定兜住的地方。
我早年接手过一个服务,配置文件里写着/mnt/disk2/data,换机器后那台机器的分区布局不同,路径照样存在、目录也是空的,服务启动成功、日志正常、数据一直写进根分区,直到磁盘写满才暴露。从那以后我每次上线前都会强制走一遍findmnt加df确认路径落在哪个文件系统上,再动手改任何配置。这点笨功夫,比事后翻日志省事得多。希望帮到你。
本文还有配套的精品资源,点击获取