Input Output
Linux的输入输出模型
Linux一切皆文件(Everything is file),终端、设备、文件、网络连接等都抽象成“文件”。命令的输入和输出最终都是文件I/O
File Descripter(FD)
每个进程启动时,都会默认打开三个文件描述符(File Descripter, FD)
文件描述符是一个非负整数,它是一个索引,进程通过这个索引在内核维护的“打开文件表”中查找对应的文件信息
简单来说:文件描述符是进程和内核之间交互文件的“句柄”或“令牌”
FD的本质与内核数据结构
内核管理打开文件涉及到三个关键的数据结构
- 进程级的文件描述符表
- 每个进程都有一个私有的表
- 表的索引就是文件描述符(FD)
- 表中的每一项包含一个指向内核打开文件表中的某个条目的指针
- 此外,它还包含一些进程级的标志,比如
FD_CLOEXEC
- 系统级的打开文件表
- 这是一个全局的表,被所有进程共享
- 表中的每一项(称为一个
file结构体)代表一个被打开的文件,它包含了- 文件状态标志:如
O_RDONLY(只读)、O_WRONLY(只写)、O_APPEND(追加)等 - 当前文件偏移量:这是文件读/写操作开始的位置。当使用
read()或write()时,这个偏移量会自动更新 - 指向该文件的v-node表的指针
- 文件状态标志:如
- v-node表
- 这也是一个全局的表
- v-node(虚拟节点)包含了文件的静态信息,例如
- 文件类型(普通文件、目录、套接字等)
- 文件的inode
- 文件的大小
- 文件的所有者和权限
- 指向文件操作函数的指针(如
read,write)
多个文件描述符(甚至来自不同进程)可以指向同一个“打开文件表”条目。这意味着它们共享文件状态和文件偏移量。这通常是通过
fork()(子进程继承父进程的FD)或dup()系列函数实现的 多个“打开文件表”条目可以指向同一个v-node。这意味着同一个文件被独立打开了多次。每个打开都有自己的文件状态和偏移量,互不干扰
标准文件描述符
| FD | 名称 | 常用缩写 | 默认绑定对象 | 说明 |
|---|---|---|---|---|
| 0 | 标准输入 | stdin | 键盘 | 程序的输入源 |
| 1 | 标准输出 | stdout | 屏幕 | 程序的正常输出 |
| 2 | 标准错误 | stderr | 屏幕 | 程序的错误输出 |
这就是为什么在终端运行命令时,输入来自键盘,结果和错误都显示在屏幕上
内核通过文件描述符表来管理进程打开的文件,0、1、2是固定的下标索引
这就是为什么可以在C语言中用read(0, ...)从标准输入读取,用write(1, ...)向标准输出写入
输出重定向(Redirection)
重定向的本质是修改文件描述符的默认绑定目标
| 语法 | 作用 | 原理 |
|---|---|---|
command > file | 将stdout重定向到file(覆盖) | 内核将FD 1从屏幕改为指向file,进程对此无感知 |
command >> file | 将stdout重定向到file(追加) | 同上,但以O_APPEND模式打开文件 |
command 2> file | 将stderr重定向到file(覆盖) | 修改FD 2的指向 |
command 2>&1 | 将stderr合并到stdout | 让FD 2成为 FD 1的一个副本,两者指向同一个地方 |
command &> file command >& file | 将stdout和stderr都重定向到file | 2>&1和> file的语法糖(注意顺序:先重定向1,再让2指向1) |
command < file | 将stdin重定向为file | 修改FD 0的指向,使其从file读取数据 |
原理:Shell在fork()出子进程后、exec()执行命令前,会根据重定向符号调用open(),dup2()等系统调用,修改子进程的文件描述符表。进程本身只管对FD 0,1,2进行读写,完全不知道底层目标已被改变\
管道
在Linux中,管道不仅仅是一个|符号——它是一个完整的进程间通信机制。它是Unix哲学的核心:编写只做好一件事的小程序,并将它们链接在一起
核心概念
- 管道是一个由内核管理的缓冲区(匿名内存区域),用于将数据从一个进程传递到另一个进程
- Shell中的
|操作符将左侧命令的标准输出连接到右侧命令的标准输入
ls -l | grep ".txt"
ls -l将文件列表写入管道,grep从管道中读取并过滤出只包含.txt的行
内核的行为
当输入一个管道命令时,Shell执行以下步骤:
- 使用
pipe()系统调用创建一个管道,它返回两个文件描述符
fd[0]= read endfd[1]= write end
- 为管道中的每个命令创建子进程
- 重定向数据流:
- 在第一个命令中,标准输出被替换为
fd[1] - 在第二个命令中,标准输入被替换为
fd[0]
- 关闭为使用的端口以防止死锁
- 并发运行命令 内核通过缓冲区移动数据。如果缓冲区(通常约为64KB)填满,写入会阻塞,直到读取消费数据——这称之为反压
标准流
默认情况下,只有标准输出会通过管道
如果也想传递错误信息:
command 2>&1 | nextcommand
2>&1merges stderr(2) into stdout(1)
多级管道
管道可以无限连接
cat access.log \
| grep "404" \
| cut -d' ' -f1 \
| sort \
| uniq -c \
| sort -nr
数据流:cat -> grep -> cut -> sort -> uniq -> sort
每个命令执行一个小的、单一的任务;组合起来,它们就形成了一条强大的处理流水线
匿名管道 vs 命名管道
- 匿名管道:由
|创建,仅在关联进程的生命周期内存在 - 命名管道:使用
mkfifo mypipe创建,作为一个特殊文件出现,可以被不相关的进程读写
重要性
- 无需临时文件——数据保留在内存中
- 并行性——所有阶段同时运行
- 可组合的设计——小程序可以粘合在一起解决复杂问题
示例:重定向与管道混合使用
- 管道默认只传输标准输出,除非使用
2>&1显式合并标准错误 - 重定向的顺序很重要。Shell从左到右解析,但像
2>&1这样的合并操作,如果想包含标准错误,必须在管道符之前出现
- 传输标准输出,将标准错误保存到文件
command 2>errors.log | next
- stdout ->
next - stderr ->
errors.log
- 同时传输标准输出和标准错误
command 2>&1 | next
2>&1将标准错误合并到标准输出- 合并后的流 ->
next
- 传输数据的同时保存副本到文件
command | tee output.log | next
tee复制标准输出:一份到output.log,一份到next- 如果想包含标准错误,添加
2>&1
command 2>&1 | tee output.log | next
- 将管道的输出重定向到文件
cat file | grep keyword > result.text
- 管道:
cat->grep - final stdout of
grep->result.txt
- 多级管道示例
grep "ERROR" app,log 2>grep_err.log \
| cut -d' ' -f1 \
| sort \
| uniq -c \
| tee summary.txt \
| sort -nr > final.txt
Flow:
grepstdout ->cut->sort->uniq->tee->sortgrepstderr ->grep_err.logteewrite a copy tosummary.txt- last
sortwrite final output tofinal.txt
进程组
基本概念
- 进程组:一组共享一个进程组ID的进程
- 每个进程仍然有自己的PID
- PGID通常等于该组中的第一个进程(组长)的PID
- 会话:一个或多个进程组组成一个会话
- 会话有一个会话首进程
- 用于管理终端访问和信号分发
目的
- 信号控制:终端可以向整个进程组发送信号(例如,Ctrl+C -> SIGINT),而不仅仅是单个进程
- 作业控制:Shell可以暂停、恢复或将整个进程组置于后台(如管道中的所有命令)
- 终端管理:进程组和会话隔离了输入/输出的控制
Shell管道中的进程组
ls -l | grep ".txt" | sort
- Shell为整个管道创建一个进程组
ls、grep和sort都属于同一个组- 按下Ctrl+C时
- 终端向整个进程组发送SIGINT信号
- 所有三个命令都接收到信号并同时终止,而不仅仅是最后一个命令