模块一:字符设备驱动与系统调用基础
对应原第 28 集核心内容,补充与 VFS、文件操作的关联,是理解所有设备、程序调用的基础。
一、先明确时代局限
课程基于 Linux 0.11 早期内核讲解,部分实现细节有时代局限性,但核心原理通用至今:
- 早期用「数组 + 主设备号」映射驱动函数,现代内核用标准
cdev框架 +file_operations接口 - 早期
int 0x80是 32 位 x86 的系统调用方式,64 位 / ARM 平台有对应指令,但本质都是「系统调用号 + 内核函数表」的索引机制
二、字符设备驱动核心逻辑
字符设备是 Linux「一切皆文件」的重要组成部分,所有按字节流顺序读写的硬件 / 虚拟设备,都通过 VFS 抽象成统一文件接口。
-
核心映射机制
- 每个字符设备有唯一「主设备号 + 次设备号」:主设备号对应驱动程序,次设备号区分同驱动下的不同设备实例
- 上层通过统一的
open/read/write/close系统调用访问 - VFS 层根据设备文件 inode 里的设备号,找到对应驱动函数,调用底层硬件操作
-
常见字符设备(运维必知)
表格
设备文件 类型 核心作用 /dev/tty终端 当前进程的控制终端 /dev/ttyUSB0串口 USB 转串口、嵌入式设备调试 /dev/null虚拟 丢弃所有写入,读返回 EOF /dev/zero虚拟 无限返回 0 字节 /dev/random虚拟 生成加密级随机数
三、TTY 终端与权限控制
- 控制终端:每个进程可以关联一个控制终端,标准输入输出默认指向它
- 前后台权限 :只有前台进程组能直接读写终端;后台进程读写终端会触发
SIGTTIN/SIGTTOU信号,进程暂停 - 运维对应 :
- 后台程序突然变为 Stopped 状态,大概率是尝试读写控制终端触发了信号
nohup/setsid能让程序脱离终端,本质是解除进程与控制终端的关联,避免终端信号影响
四、系统调用:用户态与内核态的桥梁
系统调用是用户程序访问内核资源的唯一受控入口,所有命令、程序的底层都依赖它。
- 本质
- 用户态不能直接访问内核内存、硬件,必须通过系统调用陷入内核态
- 每个系统调用有唯一编号,内核通过系统调用表按编号索引对应函数
- 内核校验权限、执行逻辑后,返回结果给用户态
- 运维神器:strace
- 作用:跟踪进程执行的所有系统调用,拦截并打印调用名、参数、返回值
- 典型场景:程序卡顿、权限报错、IO 异常时,定位卡在哪个系统调用、返回什么错误码
- 示例:程序打开文件失败,strace 能直接看到是
open返回EACCES(权限不足)还是ENOENT(文件不存在)
五、核心系统调用的底层流程
1. open 系统调用
- 在进程的文件描述符表中分配空闲编号
- 路径解析,找到目标 inode,校验访问权限
- 分配系统文件表项,关联对应 inode
- 如果是设备文件,会调用驱动的 open 函数初始化硬件
- 返回文件描述符(fd)给进程
运维对应:
ulimit -n限制进程最大打开文件数;lsof查看进程打开的所有文件描述符,排查句柄泄漏。
2. read 系统调用
- 校验文件描述符有效性
- 找到对应 inode,校验读取权限
- 按文件类型走不同实现分支:
- 普通文件:走文件系统 + 页缓存
- 字符设备:直接调用驱动的 read 函数
- 管道:读取管道缓冲区
- 数据从内核空间拷贝到用户空间,返回实际读取字节数
六、文件与目录操作的系统调用
chmod/chown/chdir这些常用命令,底层都是通过系统调用修改 inode 的对应字段:
chmod:修改 inode 的权限位chown:修改 inode 的属主 UID / 属组 GIDchdir:修改进程的当前工作目录指针,指向目标目录 inode
模块二:程序启动与 execve 完整机制
对应原第 30、31、32、33 集核心内容,去重整合,是理解进程、内存、脚本执行的核心。
一、先明确时代局限
课程基于早期内核讲解,部分细节已演进,核心逻辑不变:
- 早期 fork 是全量拷贝,现代 Linux 是写时复制(COW):fork 后父子共享物理内存,修改时才复制对应页,性能提升数倍
- 早期参数限制 128KB,现代默认支持数 MB
- 早期是固定线性内存布局,现代开启地址空间随机化(ASLR),用于安全防护
- BSS 段不是启动时直接清零,是懒加载:首次访问时才分配物理页并清零
二、execve:程序启动的核心
execve是一个系统调用,是「静态磁盘文件 → 动态内存进程」的转折点。
- 核心作用 用新程序的内容,完全替换当前进程的用户空间 :
- 清空原有的代码段、数据段、栈、堆
- 加载新的可执行文件,建立新的虚拟内存映射
- 保留进程 PID、内核态进程信息不变
- 成功则直接从新程序入口运行,不再返回原代码;只有执行失败才返回错误码
- 标准启动流程:fork + execve 我们平时执行命令、启动程序,底层都是两步:
fork:复制当前进程,创建子进程(写时复制,几乎不耗时)- 子进程调用
execve:加载新程序,替换自身用户空间 - 新程序运行,父进程等待子进程退出
运维对应:
- 为什么 shell 执行命令后自身还在?因为 fork 了子进程去执行命令,父 shell 保留
- 为什么
exec bash会替换当前 shell?因为没有 fork,直接在当前进程执行 execve- 启动报「Exec format error」:execve 校验文件头失败,不是可执行文件或格式不兼容
三、进程的内存布局
一个运行中进程的用户态虚拟地址空间,从低到高经典分段:
表格
| 内存段 | 作用 | 特点 |
|---|---|---|
| 代码段 | 存放程序二进制指令 | 只读、可被多个进程共享 |
| 数据段 | 存放初始化的全局 / 静态变量 | 可读写、进程私有 |
| BSS 段 | 存放未初始化的全局变量 | 初始全零、懒加载,不访问不占物理内存 |
| 堆 | 动态申请内存(malloc) | 向高地址增长 |
| 栈 | 函数调用栈、局部变量 | 向低地址增长,自动分配释放 |
| 参数 / 环境区 | 命令行参数、环境变量 | 位于高地址,栈的上方 |
| 动态库区 | 加载的共享库(.so) | 代码段共享,数据段私有 |
运维工具:
pmap 进程号:直观查看内存段分布、占用大小cat /proc/进程号/maps:查看详细内存映射、权限、映射文件- 排查内存泄漏:看堆段是否持续增长、匿名映射是否只增不减
四、参数与环境变量
1. 传递机制
execve 执行时,调用者传入参数数组(argv)和环境变量数组(envp),内核在新进程高地址分配专门区域,完整拷贝进去,构建指针索引。程序启动后通过 argv、envp 指针访问。
2. 继承特性
子进程默认完全继承父进程的环境变量 ------fork 后 exec 时,父进程的环境变量会完整拷贝到子进程。
运维对应:
export的变量子进程能拿到,是因为加入了 shell 的环境变量区,exec 时会传递- 脚本里修改的环境变量不影响父 shell,因为子进程有自己独立的环境变量区
cat /proc/进程号/environ:查看任意进程的环境变量
3. 常见问题:参数列表过长
- 报错:
Argument list too long - 本质:参数 + 环境变量总大小超过系统限制
- 解决:用
xargs分批传参;用管道传递替代命令行参数
五、Shell 脚本执行原理
脚本是文本文件,为什么能直接运行?核心是 execve 的解释器机制。
- 完整流程
- 执行
./script.sh,内核调用 execve 加载文件 - 读取文件头,发现不是二进制可执行文件
- 读取第一行
#!/bin/bash(shebang 行),提取解释器路径 - 内核重新执行 execve,加载解释器,把脚本文件作为参数传给解释器
- 解释器启动,逐行读取脚本执行
- 执行
- 运维对应
#!不是注释,是给内核看的解释器声明- 不写也能跑?是当前 shell 默认自己解释,但直接 exec 启动就会报错
- 脚本必须有执行权限才能直接运行,因为 execve 会校验执行权限
六、execve 完整执行步骤
- 校验路径、文件类型,读取 inode 验证执行权限
- 读取可执行文件头,校验魔数、格式合法性
- 清空原进程用户空间的代码、数据、栈
- 为新程序建立虚拟内存映射,分配代码段、数据段、BSS 段、栈
- 拷贝参数和环境变量到高地址区域
- 设置栈指针、程序入口地址
- 跳转到入口地址,新程序开始运行
模块三:通用架构思想提炼(来自 Kindle 项目)
对应原第 29 集内容,剔除业务、硬件、开发内容,提炼出通用的 Linux 系统设计思想,适用于所有服务、系统的排障和架构理解。
1. 多进程 + 多线程分层架构
- 设计:拆分为独立的前端交互进程 + 后台业务进程,进程间通过通信机制解耦;每个进程内部按功能拆分为多个线程,各司其职。
- 优势:隔离性强,交互卡顿不影响后台,后台异常不直接卡死前端。
- 运维对应:绝大多数生产服务都是这套架构(Nginx、数据库、微服务网关);排障先按进程定位边界,再下沉到线程。
- 工具:
ps -T、pstree、top -H查看进程线程。
2. 本地 Socket 进程间通信(IPC)
- 设计:进程间通过 Unix Socket 传递数据,进程空间完全隔离,通过标准接口通信。
- 优势:解耦彻底、异步、兼容性好,是 Linux 最常用的 IPC 方式之一。
- 运维对应:服务内部模块、主从进程大量使用本地 socket;模块间无响应先查 socket 状态。
- 工具:
ss -x查看 Unix 套接字;lsof查看进程打开的 socket。
3. 事件驱动模型
- 设计:底层线程只负责采集数据、生成事件、推入队列;主控线程从队列取事件,分发给业务线程处理。
- 优势:底层采集不被慢业务阻塞,数据不丢失;采集与处理解耦。
- 运维对应:内核输入子系统、网络协议栈、高并发服务都是事件驱动;业务响应慢优先排查业务处理线程是否阻塞。
4. mmap 内存映射
- 设计:把内核 / 硬件 / 磁盘文件的内存区域,直接映射到进程虚拟地址空间,数据不用在内核和用户间来回拷贝。
- 优势:零拷贝、低延迟、大文件读写效率高。
- 运维对应:数据库、缓存中间件都用 mmap 提升性能;大文件顺序读写快的核心原因。
- 工具:
pmap查看进程的内存映射分布。
5. 分层解耦设计
- 设计:从下到上硬件层→驱动层→内核层→业务层→交互层,每层只负责自身职责,通过标准接口通信。
- 优势:替换任意一层不影响整体,可维护性强。
- 运维对应:所有系统排障的核心方法论 ------ 从下往上逐层定位,先排除底层硬件 / 内核问题,再查上层应用。