Linux与IO
开篇:程序员必备的 Linux 技能
作为程序员,Linux 是你绕不开的技能。不管你用什么语言、什么框架,最终你的代码大概率跑在 Linux 服务器上。半夜收到报警、线上排查问题时,Linux 命令就是你手里的听诊器和手术刀。
但 Linux 不只是一堆命令。当你深入了解它的 IO 机制——数据怎么从磁盘到内存、从内存到网卡、中间经历了几次拷贝和切换——你就能理解为什么 Nginx 那么快、Redis 为什么选择单线程、Kafka 为什么吞吐量那么高。
本文从两个维度展开:日常命令速查和 IO 模型深度理解。
一、常用命令速查
文件操作
| 命令 | 用途 | 常用示例 |
|---|---|---|
ls / ll | 列出目录内容 | ls -a(含隐藏文件)、ll(详细列表) |
cd / pwd | 切换/显示当前目录 | cd ~、pwd |
mkdir / rmdir | 创建/删除目录 | mkdir -p a/b/c |
cp / mv / rm | 复制/移动/删除 | rm -rf(慎用!) |
touch | 创建空文件 | touch test.txt |
find | 搜索文件 | find . -name "*.log" -mtime -7 |
chmod / chown | 修改权限/所有者 | chmod 755 script.sh |
# find 进阶用法
find . -type f -size +10M # 大于 10MB 的文件
find . -type d # 查找所有目录
find . -type f # 查找所有普通文件
find . -name "*.log" -mtime -7 # 最近 7 天修改的日志日志查看(高频!)
| 命令 | 用途 | 常用示例 |
|---|---|---|
cat | 查看小文件全部内容 | cat config.yml |
less / more | 分页查看大文件 | less 可翻页,more 不行 |
tail | 查看文件末尾 | tail -f app.log(实时跟踪) |
head | 查看文件开头 | head -20 app.log |
grep | 搜索关键字 | grep "ERROR" app.log | grep "Biz" |
vi / vim | 编辑文件 | 线上最常用的编辑器 |
# 日志排查三板斧
tail -f application.log # 实时看日志
tail -f application.log | grep ERROR # 只看报错
grep "ERROR" application.log | grep "Biz" # 精确定位管道(|)的用途很广。它的基本特征是:前一个命令的输出作为后一个命令的输入。比如获取文件第 8 行的内容:
head -8 test.txt | tail -1 # 先取前 8 行,再取最后 1 行进程与系统
| 命令 | 用途 | 常用示例 |
|---|---|---|
top | 实时显示进程资源占用 | CPU 飙高时第一个看的命令 |
ps | 查看当前进程快照 | ps aux | grep java |
kill | 终止进程 | kill -9(强制,慎用) |
df / du | 查看磁盘使用 | df -h、du -sh * |
uptime | 查看系统负载 | 显示 1/5/15 分钟平均负载 |
vmstat | 虚拟内存统计 | vmstat 2 3(每 2 秒采样,共 3 次) |
pstree | 查看进程树结构 | 可以看到进程的父子关系 |
在 Bash 中,进程是有父子关系的。用 pstree 可以看到树状结构。子进程无法直接访问父进程的变量(进程隔离),需要使用 export 将变量导出为环境变量后,子进程才能读取。
网络
| 命令 | 用途 | 常用示例 |
|---|---|---|
ping | 检测网络连通性 | ping google.com |
curl / wget | 发 HTTP 请求/下载 | curl -X GET http://... |
netstat | 查看网络连接 | netstat -natp |
ssh / scp | 远程登录/复制 | ssh user@host |
telnet | 测试端口可达性 | telnet host 8080 |
ifconfig | 查看网络接口配置 | 查 IP 地址时常用 |
压缩与包管理
# 压缩/解压
tar -czf archive.tar.gz dir/ # 打包压缩
tar -xzf archive.tar.gz # 解压
zip -r archive.zip dir/ # zip 压缩
unzip archive.zip # zip 解压
# 包管理
apt-get install xxx # Debian 系
yum install xxx # RedHat 系Git & Maven 速查
# Git 日常
git add . && git commit -m "msg" && git push
# Maven 日常
mvn clean install -Dmaven.test.skip=true # 跳过测试构建
mvn clean install -Dmaven.test.skip=true -U # 强制更新 snapshot
mvn dependency:tree > tree # 依赖树导出二、磁盘 IO 与文件系统
虚拟文件系统(VFS)
Linux 有一个核心设计哲学:everything is a file。磁盘、网络、设备,统统抽象为文件来操作。
内核中的 VFS(虚拟文件系统) 是一个抽象层,提供统一的文件操作接口。不管底层是 ext4、XFS 还是 NFS,应用程序用的都是同一套 read/write/open/close。
VFS 的核心数据结构:
- Superblock:描述文件系统的整体信息
- inode:文件的元数据(权限、大小、数据块指针等,不含文件名)
- dentry:目录项,文件名到 inode 的映射
- 文件描述符(FD):内核为每个打开的文件维护的句柄,记录偏移量、状态标志等
每个进程都有自己的文件描述符表。其中 0、1、2 是预分配的:
fd 0:标准输入(stdin)fd 1:标准输出(stdout)fd 2:标准错误(stderr)
可以在 /proc/$$/fd 目录下查看当前进程的所有文件描述符,用 lsof -op $$ 查看详细信息。
Page Cache 与磁盘读写
应用程序不直接和磁盘打交道。当程序发起系统调用(触发 CPU 指令 int 0x80,即中断 128)时,CPU 会去中断描述符表查找对应的回调方法,然后保护现场、从用户态切换到内核态。
内核通过 DMA 协处理器让磁盘缓冲区和内核的 Page Cache 之间交换数据。DMA 工作期间 CPU 无需等待,可以去做其他事,从而提高效率。
Page Cache 是内核维护的中间层。内核会充分利用可用内存空间,尽量在内存中维护你访问过的数据,优化 IO 性能。但异常断电时 Page Cache 中未刷盘的脏页会丢失。
写入磁盘的流程:应用程序写入的数据先进入 Page Cache。当缓存达到阈值继续添加时,内核将脏页写入磁盘,然后淘汰长时间没用到的缓存,腾出空间缓存新数据。注意:淘汰的一定是干净的页(已刷盘的),脏页必须先刷盘才能淘汰。
三、五种 IO 模型
为了保护系统安全和利用缓存加速读写,内存分为用户空间和内核空间。用户程序要操作内核空间的数据,必须先把数据从内核空间拷贝到用户空间。
一个网络 IO 操作(比如接收客户端数据)要经历两个阶段:
- 等待数据准备好:数据从网卡到内核缓冲区(可能因为分组传输而有延迟)
- 数据拷贝:从内核缓冲区到用户进程空间
围绕这两个阶段的不同处理方式,衍生出五种 IO 模型。
用一个去银行办事的故事来理解:
同步阻塞 IO
小 J 到银行柜台办事,柜台排满了人,他就站在那一直等,什么都不做,直到排到他办完事才离开。
进程发起 recv 调用后一直阻塞,直到数据从内核拷贝到用户空间并返回。
同步非阻塞 IO
小 J 到银行发现柜员休息了,他离开去逛街,过一会儿回来看看营业了没,反复如此,直到终于碰到柜员在了,才办理业务。
recv 不管有没有数据都立即返回。没有数据就过段时间再调用,如此循环轮询。小 J 在中间离开的时间可以做自己的事,但回来检查时只有在无数据的时候是非阻塞的,数据到达后仍然要等待拷贝完成,所以它还是同步 IO。
IO 多路复用
小 J 到银行,大堂经理说"现在所有柜台都忙,等有空位我通知你"。小 J 就在大厅等(select/poll/epoll),大堂经理通知他有空位了,但具体哪个柜台空了,他得自己去找(select/poll 需要遍历,epoll 直接告诉你)。
进程阻塞在 select/poll/epoll 上,而不是阻塞在具体的 recv 上。一个线程可以同时监控多个 IO 连接,任何一个就绪就去处理。
信号驱动 IO
小 J 是银行 VIP 客户,他告诉大堂经理"有空位直接叫我的号"(注册信号函数),然后就去做自己的事了。大堂经理叫到他时,他才过来办理。
通过 sigaction 注册信号函数,数据准备好时内核发中断,进程在信号处理函数中调用 recv。等待过程是非阻塞的,但办理业务(数据拷贝)时仍然是同步的。
异步 IO
小 J 交代大堂经理:"帮我把业务办好了再通知我来验收。"在此过程中小 J 完全自由,不用等也不用轮询。
调用 aio_read 后立即返回,内核完成数据准备和拷贝后,通过回调通知进程。这是真正的异步——进程全程不阻塞。
前四种都属于同步 IO——因为在内核数据拷贝到用户空间这一步,进程仍然是阻塞等待的。只有异步 IO 将整个 IO 操作交给操作系统处理,进程通过回调或信号量拿到结果,才是真正的全程非阻塞。
四、select / poll / epoll 深度对比
这三者都是 Linux 的 IO 多路复用技术,用于同时监听多个文件描述符(fd),当任意一个就绪时进行非阻塞读写。
select
int select(int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);select 可以监听 read、write、except 三类事件。返回后需要遍历所有 fd 才能找到就绪的那个。单进程最多监听 1024 个 fd(宏定义限制,修改需重编译内核)。每次调用都要把整个 fd 集合从用户态拷贝到内核态。
优点:几乎所有平台都支持,兼容性最好。
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);基于链表实现,没有 1024 的数量限制。但同 select 一样,返回后仍需遍历所有 fd,且每次调用仍需在内核态和用户态间来回拷贝整个 fd 集合。
epoll
int epoll_create(int size); // 创建 epoll 句柄
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改/删除
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待就绪epoll 是 Linux 2.4.5 引入的,核心解决了 select/poll 的两大痛点:
- 避免 fd 的重复传递:
epoll_create在内核中开辟空间,用红黑树存储监听的 fd。通过epoll_ctl注册后,fd 只拷贝到内核一次 - 避免 fd 的全量遍历:当网卡有数据到达时,中断机制将就绪的 fd 拷贝到一个链表中。
epoll_wait直接从链表取就绪 fd,O(1) 复杂度 - 无数量限制:上限是系统最大可打开文件数(
cat /proc/sys/fs/file-max),1GB 内存大约支持 10 万左右
对比总结
| 维度 | select | poll | epoll |
|---|---|---|---|
| fd 数量限制 | 1024 | 无限制 | 无限制 |
| 遍历所有 fd | 是 | 是 | 否(就绪链表) |
| fd 重复拷贝到内核 | 每次都要 | 每次都要 | 只需一次 |
| 时间复杂度 | O(n) | O(n) | O(1) |
NIO 和 select/poll 都需要遍历所有 IO 询问状态。区别在于:NIO 的遍历成本在用户态和内核态的切换(每个 fd 一次系统调用),select/poll 的优势是只需要一次系统调用就完成所有 fd 的状态检查。epoll 则更进一步,连遍历都省了。
epoll 的两种触发模式
- LT(Level Trigger,水平触发,默认模式):只要 fd 处于就绪状态,每次
epoll_wait都会通知。应用可以不立即处理,下次还会再通知。 - ET(Edge Trigger,边缘触发):只在 fd 状态变化时通知一次。如果不立即处理,下次
epoll_wait不会再通知。
ET 模式大幅减少了重复触发次数,效率更高。但要求应用程序必须使用非阻塞 socket,并在收到通知时立即读完所有数据,否则数据会被"饿死"。
五、常见面试题精选
Q1:同步/异步 vs 阻塞/非阻塞怎么区分?
同步/异步描述的是被调用方的行为:同步是调用后立即执行并返回结果,异步是先返回"收到了",做完后再通知。阻塞/非阻塞描述的是调用方的行为:阻塞是干等着不做别的,非阻塞是不等可以做别的事。两者没有必然关联。
经典类比:老张烧水。普通水壶 + 一直守着 = 同步阻塞。普通水壶 + 时不时看看 = 同步非阻塞。响水壶 + 一直守着 = 异步阻塞(虽然没必要)。响水壶 + 去看电视等它响 = 异步非阻塞。
Q2:为什么 epoll 比 select/poll 快?
两个关键改进:(1) fd 只需一次性注册到内核红黑树,不用每次调用都重复传递;(2) 通过就绪链表直接获取有数据的 fd,不需要遍历所有 fd。连接数越大,优势越明显。
Q3:BIO、NIO、IO 多路复用的演进逻辑是什么?
BIO:每个连接一个线程阻塞等待数据,accept 和 recv 都是阻塞的,连接多了线程爆炸。NIO:将 socket 设为非阻塞,一个线程用循环轮询所有连接,但每次轮询都有大量无意义的 recv 系统调用。IO 多路复用:一次系统调用(select/poll/epoll)就能知道哪些连接有数据,从 N 次系统调用降到 1 次。epoll 更进一步,连遍历 fd 都省了。
小结
Linux 的命令是工具,IO 模型是灵魂。
日常工作中,tail -f、grep、top、ps 这些命令用得溜,排查问题效率翻倍。而理解了五种 IO 模型和 select/poll/epoll 的演进逻辑,你就能明白为什么 Nginx 用 epoll + 事件驱动就能扛住百万并发,为什么 Redis 单线程也能那么快——因为瓶颈从来不在 CPU,而在 IO。掌握了这些底层机制,你看技术选型和架构设计的眼光会完全不一样。