程序和进程
虚拟化
One of the most fundamental abstractions that the OS provides to users: the process
把物理计算机“抽象”成“虚拟计算机”,程序好像都独占计算机运行
5.1 程序和进程
程序是状态机的静态描述
描述了所有可能的程序状态
程序(动态)运行起来,就成了进程(进行中的程序)
进程:程序的运行时状态随时间的演进
除了程序状态,操作系统还会保存一些额外的(只读)状态
- 进程号 pid,不在程序中,通过系统调用
getpid()
向操作系统获取
- 进程号 pid,不在程序中,通过系统调用
proc.c
getpid
getppid
getuid
(user)getgid
(group)geteuid
(effective)getegid
getpriority
process status from
/proc/self/status
command line from
/proc/self/cmdline
current working directory from
/proc/self/cwd
注意代码质量,利用大语言模型code review
Prompt: 我发现随着进程的创建,进程的 pid 是递增的;而 pid 是有限的 (32-bit 整数),这是否意味着会循环导致 pid 重用?
系统会维护一个 PID 位图或类似的数据结构来跟踪已使用和可用的 PID
- 新进程创建时,内核会分配下一个可用的 PID;当 PID 达到最大值时,内核会回绕(wrap around)到最小值(通常是 300 或 1)重新分配
系统不会立即重用刚刚释放的 PID,通常会有一个”冷却期”
这是为了避免将新进程的 PID 分配给可能仍被其他进程引用的已终止进程,否则可能导致权限问题
- 竞态条件:在多线程或多进程环境中,PID 重用可能导致意外行为
Testkit
5.2 进程 (状态机) 管理
操作系统 = 状态机的管理者
- 进程管理 = 状态机管理
一个直观的想法
创建状态机:
spawn(path, argv)
(孵化)销毁状态机:
_exit()
exit()
已经被libc占用了这是一个合理的设计 (例子:Windows)
UNIX 的答案
复制状态机:
fork()
复位状态机:
execve()
1. 创建状态机
pid_t fork(void);
立即复制状态机
包括所有状态的完整拷贝
寄存器 & 每一个字节的内存
- 写时复制(Copy-On-Write, COW):复制父进程的虚拟内存空间,但物理内存页仅在修改时复制
Caveat: 进程在操作系统里也有状态: ppid, 文件, 信号, …
共享文件表项、文件偏移量共享、独立关闭
小心这些状态的复制行为
复制失败返回 -1
- errno 会返回错误原因 (man fork)
如何区分两个状态机?
新创建进程返回 0
执行 fork 的进程返回子进程的进程号——“父子关系”
进程的创建关系形成了进程树
,如果 终止了, 的 ppid 是什么?
错误认知:认为 的ppid会变成 (祖父进程)
实际规则:
子进程终止会通知父进程
通过 SIGCHLD 信号,通知父进程其子进程的状态发生了变化(例如终止、停止或恢复)
父进程可以捕获这个信号 (
waitpid()
)
可以捕获信号并回收 ,但不影响 的
ppid
托孤机制
父进程退出(正常退出或崩溃)后,内核会遍历其所有存活的子进程(成为孤儿进程)
将所有子进程的父进程 PID(
ppid
)设置为 init 进程(PID=1,现代系统可能是systemd
或launchd
)确保子进程不会因父进程消失而失去管理,避免成为僵尸进程
init 进程会定期调用
wait()
系统调用,回收孤儿进程的退出状态若子进程终止,init 会立即回收它,避免其长期滞留为僵尸进程
fork bomb,创建一棵进程子树
一行shell命令: :(){ :|:& };:
(bash允许冒号作为标识符)
|
强制 Shell 为左右两侧命令各创建一个进程&
将整个管道命令放到后台执行,避免阻塞 Shell1 变 2, 2 变 4, 第 k 层有 个进程
曾经会使系统资源耗尽、彻底卡死,但现在 Linux 有 OOM(Out Of Memory) 保护,当内存严重不足时,内核的
OOM Killer
会终止高内存占用的进程
缓冲区 输出为terminal和pipe的区别
for (int i = 0; i < 2; i++) {
fork();
printf("Hello\n");
}
./a.out
输出6行./a.out | cat
输出8行终端设备(TTY):默认使用 行缓冲模式(Line-buffered),遇到换行符
\n
时立即刷新缓冲区管道/文件:默认使用 全缓冲模式(Fully-buffered),缓冲区满或程序结束时才刷新
fork()
会复制父进程的未刷新缓冲区到子进程
2. 复位状态机
将当前进程重置成一个可执行文件描述状态机的初始状态
操作系统维护的状态不变:进程号、目录、打开的文件
O_CLOEXEC(Close-On-Exec)
若在打开文件时指定
O_CLOEXEC
标志,当进程调用exec()
执行新程序时,该文件描述符会自动关闭防止子进程继承父进程的无关文件描述符,避免资源泄漏或安全问题
int execve(const char *pathname, char *const argv[],
char *const envp[]);
execve 是唯一能够 “执行程序” 的系统调用
- 因此也是一切进程 strace 的第一个系统调用
execve() 设置了进程的初始状态
argc & argv: 命令行参数
main 的参数由 execve 的 argv 参数传递
argc 是 argv 数组的长度
envp: 环境变量
通过 env 命令查看当前环境变量(PATH, PWD, HOME, DISPLAY, PS1, …)
export: 在 Shell 中设置环境变量,子进程会通过
execve
继承
程序被正确加载到内存
- 代码、数据、PC 位于程序入口(如ELF的e_entry地址)
所有
exec
系列函数(如execl
,execvp
)最终通过execve
实现程序加载- 如
execl
通过可变参数列表传递命令行参数
- 如
int execl(const char *pathname, const char *arg, ...
/* (char *) NULL */);
例子:对于可执行文件搜索路径,搜索顺序恰好是 PATH 环境变量里指定的顺序
3. 销毁状态机
void _exit(int status);
立即摧毁状态机,允许有一个返回值,返回值可以被父进程获取
进程的地址空间
UNIX 中状态机的生命周期管理 API
fork
execve
_exit
操作系统视角:状态机的复制、重置和删除
“创建新状态机” Spawn = fork + execve
int pid = fork();
if (pid == -1) { // 错误
perror("fork"); goto fail;
} else if (pid == 0) { // 子进程
execve(...);
perror("execve"); exit(EXIT_FAILURE);
} else { // 父进程
...
int status;
waitpid(pid, &status, 0); // testkit.c 中有
}
6.1 进程的初始状态
进程 execve 后的初始状态
(暂时忽略操作系统管理的状态: pid, 打开的文件……)
- 寄存器
gdb 中通过
starti
命令从执行第一条指令前停下,这时打印出来就行了包括 GPRs
RIP
(pc)RSP
(sp)
- 内存
内存区域的权限通常由 4 个字符 表示
前三个字符:表示内存的访问权限(读、写、执行)
r
: Read(可读)w
: Write(可写)x
: Execute(可执行)-
: 无对应权限
第四个字符:表示内存的 映射类型
p
:Private(私有映射)- 写时复制(Copy-on-Write, COW) 仅在需要修改数据时,才真正复制原始数据的副本。在此之前,多个进程或线程可以共享同一份数据,而无需提前复制全部内容。
s
:Shared(共享映射)- 多个进程共享同一物理内存页,修改会直接反映到其他进程或磁盘文件
(具体数值仅做参考,为运行address-space/alloc
的结果)
低地址(
0x1000
为 4KiB,内存页大小)0x400000–0x401000
0x1000
r--p
- ELF 文件头和只读数据段(如
.rodata
,.eh_frame
)
- ELF 文件头和只读数据段(如
0x401000–0x498000
0x97000
r-xp
- 代码段(
.text
),包含程序的可执行指令
- 代码段(
0x498000–0x4c1000
0x29000
r--p
- 只读数据段(如静态字符串、常量表)
0x4c1000–0x4c8000
0x7000
rw-p
- 数据段(
.data
和.bss
),存放全局变量和静态变量
- 数据段(
0x4c8000–0x4cd000
0x5000
rw-p
堆(Heap)用于动态内存分配(如
malloc
/free
)通过
brk
或mmap
系统调用动态增长
[vvar]
0x7ffff7ff9000-0x7ffff7ffd000
0x4000
r--p
- 内核向用户空间暴露的只读数据(如系统时间、时钟频率),优化频繁读取的内核数据
[vdso]
(Virtual Dynamic Shared Object)0x7ffff7ffd000-0x7ffff7fff000
0x2000
r-xp
- 在用户态执行某些系统调用(如
gettimeofday
),避免陷入内核
- 在用户态执行某些系统调用(如
[stack]
0x7ffffffdd000-0x7ffffffff000
0x22000
rw-p
用户栈,存储函数调用链、局部变量和返回地址
读写 ,不可执行(现代系统默认开启栈不可执行保护)
从高地址向低地址扩展
[vsyscall]
0xffffffffff600000-0xffffffffff601000
0x1000
--xp
旧版快速系统调用机制,现代系统已由
vdso
替代,保留仅为兼容无读写权限 ,仅执行(实际已弃用)
高地址
6.2 进程的地址空间管理
进程的初始只有ELF 文件里声明的内存和一些操作系统分配的内存
任何其他指针的访问都是非法的
如果我们从输入读一个 size,然后
malloc(size)
,内存从哪来呢?一定有一个系统调用可以改变进程的地址空间
Memory Map
// 映射
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
// 修改映射权限
int mprotect(void *addr, size_t length, int prot);
addr
:建议的映射起始地址- 常设为
NULL
由内核自动选择
- 常设为
length
:映射区域的长度(字节)prot
:内存保护标志(可读、可写、可执行、禁止访问)flags
:映射类型(匿名、私有、共享等)MAP_ANONYMOUS
匿名映射,不与任何文件关联的内存区域,通常用于动态分配内存(类似malloc
)
fd
:文件描述符(匿名映射时设为 -1)offset
:文件映射的偏移量(通常为 0)
int msync(void *addr, size_t length, int flags);
默认情况下,操作系统会延迟将内存中的修改写回磁盘(通过页缓存机制),
msync
允许用户强制同步addr
必须是页对齐的(通常由mmap
返回的地址直接使用)可通过
sysconf(_SC_PAGE_SIZE)
获取系统页大小munmap
不会自动触发同步。未调用sysnc
修改可能丢失
确保其他进程或程序(如直接操作文件的进程)能看到最新的修改
MA_SYNC
MS_ASYNC
MS_INVALIDATE
(同步,异步,使其他进程的映射失效)
用 pmap 命令查看进程的地址空间
pmap
查看进程的内存映射信息(如地址范围、权限、映射文件等)
读取 /proc/[pid]/maps
文件,该文件由内核生成,记录了进程的所有内存映射
申请大量内存空间,瞬间完成
但是物理内存的占用是延迟的(访问时触发缺页中断)
操作系统分配内存不需要真的给你内存,所以申请很大的内存速度依然很快
malloc 对大内存(如超过
MMAP_THRESHOLD
,默认 128KB)直接调用 mmap
缺页异常(Page Fault)是CPU在访问虚拟内存时,因页面未加载、权限不足或写时复制(写入共享内存页时,需复制新页以保持私有性)等原因触发的异常。
CPU 暂停当前指令,保存现场并切换到内核态
应用场景
动态内存分配:物理页延迟分配,触发缺页时再分配
内存共享:多个进程共享同一物理页,减少冗余
文件映射(mmap):访问文件时按需加载,避免一次性读取全部
- 映射大文件、只访问其中的一小部分
with open('/dev/sda', 'rb') as fp:
mm = mmap.mmap(fp.fileno(),
prot=mmap.PROT_READ, length=128 << 30)
hexdump.hexdump(mm[:512])
将硬盘设备文件
/dev/sda
的前 128GB 映射到内存,但仅读取前 512 字节无需将整个文件加载到内存,按需访问(操作系统自动处理分页)
文件映射到内存后,读写操作通过内存地址直接完成,由操作系统负责与磁盘同步(若为
MAP_SHARED
)
6.3 入侵进程的地址空间
进程 (状态机) 在 “无情执行指令机器” 上执行
状态机是一个封闭世界
但如果允许一个进程对其他进程的地址空间有访问权?
- 意味着可以任意改变另一个程序的行为
“入侵” 进程地址空间的例子
- 调试器 (gdb),gdb 可以任意观测和修改程序的状态
金手指:直接物理劫持内存
听起来很离谱,但 “卡带机” 时代的确可以做到!
物理入侵进程地址空间
Game Genie: 一个 Look-up Table (LUT)
简单、优雅:当 CPU 读地址 a 时读到 x,则替换为 y
今天我们有 Debug Registers 和 Intel Processor Trace 帮助系统工具 “合法入侵” 地址空间
地址空间那么大,哪个才是 “金钱”?
包含动态分配的内存,每次地址都不一样
思路:Everything is a state machine
- 观察状态机的 trace,筛选内存中符合其变化模式的地址,就知道哪个是金钱了
查找 + Filter
进入游戏时 exp=4950
打了个怪 exp=5100
符合 4950→5100,4950→5100 变化的内存地址是很少的
- 好了,出门就是满级了
访问操作系统对象
7.1 文件描述符
文件描述符:文件描述符是指向操作系统对象的 “指针”(不恰当的比喻?)——系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。我们有 open, close, read/write(解引用), lseek(指针内赋值/运算), dup(指针间赋值) 管理文件描述符。
int dup(int oldfd);
复制一个现有的文件描述符,生成一个新的描述符指向同一个文件/资源
int dup2(int oldfd, int newfd);
强制复制一个文件描述符到指定的数值。若目标描述符 newfd
已打开,则先关闭它,再复制
文件描述符的分配
总是分配最小的未使用描述符(最小未使用优先)
0, 1, 2 是标准输入、输出和错误
新打开的文件从 3 开始分配
文件描述符是进程文件描述符表(进程内核数据结构的一部分)的索引
关闭文件后,该描述符号可以被重新分配
进程可打开文件数限制
ulimit -n (进程限制)
sysctl fs.file-max (系统限制)
文件描述符中的 offset
文件描述符的 offset 是 “进程状态的” 的一部分,由操作系统维护;程序只能通过整数编号间接访问
Quiz: fork() 和 dup() 之后,文件描述符共享 offset 吗?
dup 新旧描述符会共享 offset,open 会得到独立的 offset
父子进程的文件描述符独立维护 offset(写操作可能互相覆盖,需注意竞态)
Windows 中的文件描述符 Handle
- 默认 handle 是不继承的 (和 UNIX 默认继承相反)
fcntl(fd, F_SETFD, FD_CLOEXEC)
,fcntl
用于对已打开的文件描述符进行底层控制
7.2 访问操作系统中的对象
Filesystem Hierarchy Standard FHS
enables software and user to predict the location of installed files and directories
例如 macOS 就不遵循 FHS
只要拷对了文件,操作系统就能正常执行啦!
创建 UEFI 分区,并复制正确的 Loader
创建文件系统
- mkfs (格式化主分区)
cp -ar 把文件正确复制 (保留权限)
注意 fstab 里的 UUID(Universally Unique Identifier,每个磁盘分区在格式化时生成的唯一标识符)
复制系统文件到新硬盘后,必须用
blkid
获取新分区的 UUID,并更新到新系统的/etc/fstab
(系统启动时自动挂载磁盘分区的配置文件)中。如果配置错误,系统可能无法挂载根分区,导致启动失败你就得到了一个可以正常启动的系统盘!
运行时挂载必要的其他文件系统
磁盘上的 /dev, /proc, … 都是空的
mount -t proc proc /mount/point 可以 “创建” procfs
任何 “可读写” 的东西都可以是文件(“一切皆文件”)
- 真实设备
- 磁盘
/dev/sda
终端/dev/tty
- 虚拟设备 (文件)
/dev/urandom (随机数), /dev/null (黑洞), …
无实体文件,操作系统为它们实现了特别的 read 和 write 操作
内核驱动(如
/drivers/char/mem.c
)可以通过
/sys/class/backlight
控制屏幕亮度
procfs 也是用类似的方式实现的
- 进程/系统信息以文件形式暴露(如
/proc/cpuinfo
)
- 进程/系统信息以文件形式暴露(如
- 管道:一个特殊的 “文件” (流)
特殊文件流,支持单向通信
读端(read)和写端(write)分离
- 匿名管道:
int pipe(int pipefd[2]);
返回两个文件描述符,配合
fork
使用(父子进程通信)先分配读端,再分配写端
没有数据就会一直等待
非匿名管道 - int mkfifo(const char *pathname, mode_t mode);
“实现一切” 的基础知识
- 进程管理
- fork, execve, waitpid, exit
- 内存管理
- mmap, munmap, mprotect, msync
- 文件管理
- open, close, read, write, lseek, dup
7.3 一切皆文件
文件描述符适合什么?
字节流
顺序读/顺序写
没有数据时等待
典型代表:管道
字节序列
其实就有一点点不方便了
需要到处 lseek 再 read/write
mmap 不香吗?指针指哪打哪
madvise, msync 提供了更精细的控制
int madvise(void *addr, size_t length, int advice);
向内核提供内存访问模式的建议,优化性能
MADV_SEQUENTIAL
:预期会顺序访问(内核可提前预读或释放已用页)MADV_RAMDOM
:预期会随机访问(内核减少预读,避免浪费内存带宽)MADV_DONTNEED
:告知内核某些内存区域可提前释放
反思 Everything is a File
优点
- 优雅,文本接口,就是好用
缺点
和各种 API 紧密耦合
对高速设备不够友好
额外的延迟和内存拷贝
单线程 I/O
终端和 UNIX Shell
8.1 终端
Some history:
打字机时代的遗产
- Shift
- 使字锤或字模向上移动一段距离,切换字符集
- CR & LF
\r
CR (Carriage Return): 回车,将打印头移回行首print('Hel\rlo')
\n
LF (Line Feed): 换行,将纸张向上移动一行- UNIX 的
\n
同时包含 CR 和 LF
- UNIX 的
- Tab & Backspace
- 位置移动 (
Backspace + 减号 = 错了划掉)
tty: from Teletypewriter 电传打字机(仍然无法纠错)
VT100: Video Teletypewriter (DEC, 1978)
成为事实上的行业标准
首个完整实现 ANSI Escape Sequence 的终端
- Escape Sequence 指通过特定的字符组合(通常以
\
开头)来表示无法直接输入或显示的字符,例如换行、引号、制表符等)
- Escape Sequence 指通过特定的字符组合(通常以
80×24 字符显示成为标准布局
计算机(物理)终端:原理
- 作为输出设备
- 接受 UART 信号并显示 (支持 Escape Sequence 就非常自然了)
- 作为输入设备
- 把按键的 ASCII 码输出到 UART (所以有很多控制字符)
UART 是一种硬件设备,负责串行通信中的数据传输与格式转换。早期计算机通过 UART 连接物理终端(如电传打字机),实现输入与输出
伪终端 (Pseudo Terminal)和终端模拟器 (Terminal Emulator)
- 伪终端PTY:一对 “管道”(虚拟设备而非物理) 提供双向通信通道
主设备 (PTY Master): 终端模拟器直接控制
从设备 (PTY Slave): 连接到 shell 或其他程序(例如
/dev/pts/0
)PTY也是内核管理的复杂对象
- 创建伪终端
openpty()
: 通过/dev/ptmx
申请一个新伪终端- 返回两个文件描述符 (master/slave)
如果直接在程序中访问 /dev/ptmx
(即依赖linux的特性),那么代码就会不可移植
ssh, tmux new-window, ctrl-alt-t, …
- ssh远程连接:本地终端模拟器通过主设备与远程Shell(从设备)通信
- 实现终端模拟器
openpty
+fork
子进程(如Shell进程):将 stdin/stdout/stderr 重定向(dut)至 slave
父进程:从 master 读取子进程输出显示到屏幕; 将键盘输入写入 master,传递至子进程
Kitty 终端模拟器扩展 Escape Sequence 来显示图片
VSCode 的终端模拟器没有为不同操作系统单独开发底层终端逻辑,而是通过以下前后端分层实现跨平台兼容性
终端:更多功能
终端模式
Canonical Mode: 按行处理
- 回车发送数据 (终端提供行编辑功能)
Non-canonical Mode: 按字符处理
每个字符立即发送给程序
用于实现交互式程序: vim, ssh
终端属性控制
tcgetattr()
/tcsetattr()
(<termios.h>
)(用于控制终端设备的属性)可以控制终端的各种行为:回显、信号处理、特殊字符等
- (你输密码的时候关闭了终端的回显)
8.2 终端和操作系统
程序和终端的 “配对”
用户登录的起点
系统启动 (内核 → init → getty)
getty
打开终端设备(如/dev/tty1
),后续的login
程序和 Shell 继承该终端的文件描述符fork() 会继承文件描述符 (指针),因此,子进程也会指向同一个终端
通过
Ctrl+Alt+F1~F12
切换不同的虚拟控制台(如tty1
到tty12
)
远程登录 (sshd → fork → openpty)
- Shell 进程的 stdin, stdout, stderr 都会指向分配的slave端
vscode (fork → openpty)
CLI 到 GUI
CLI: 用户直接与物理终端或虚拟控制台(
tty
)交互,通过getty/login
启动 Shell。GUI: 终端模拟器,本质是图形应用程序,通过创建
pty
模拟终端行为- 虽然运行在 GUI 中,但终端模拟器仍依赖内核的
pty
机制与 Shell 通信
- 虽然运行在 GUI 中,但终端模拟器仍依赖内核的
进程管理:要解决的问题
我们有那么大一棵进程树,都指向同一个终端,有的在前台,有的在后台,Ctrl-C 到底终止哪个进程?
答案:终端才不管呢
它只管传输字符
Ctrl-C: End of Text (ETX),
\x03
Ctrl-D: End of Transmission (EOT),
\x04
(退出python,cat)Ctrl-\: quit SIGQUIT
stty -a: 可以看到按键绑定
stty
(set teminal type) 用于显示和修改终端行设置
但操作系统收到了这个字符,就可以对 “当前” 的进程采取行动,给这个进程发信号
作为操作系统的设计者,需要在收到 Ctrl-C 的时候找到一个 “当前进程”
你会怎么做?
fork() 会产生树状结构 (还有托孤行为)
Ctrl-C 应该终止所有前台的 “进程们”,但不能误伤后台的 “进程们”
会话 (Session) 和进程组 (Process Group):机制
操作系统怎么知道ctrl-c终止谁?
给进程引入一个额外编号 (Session ID,大分组)
子进程会继承父进程的 Session ID(SID,Session Leader(通常是登录shell)的pid)
一个 Session 关联一个控制终端 (controlling terminal,如
/dev/pts/0
),包含 foreground PGID和 controlling SIDLeader 退出时,全体进程收到 Hang Up (SIGHUP)
再引入另一个编号 (Process Group ID,小分组)
进程组是一组相关进程的集合(如一条命令及其子进程)
只能有一个前台进程组
- 其他进程组为后台进程组,无法直接接收终端输入
操作系统收到 Ctrl-C,向前台进程组所有进程发送 SIGINT
Ctrl-Z,SIGTSTP 将job并放到后台;jobs,查看后台进程;fg x,SIGCONT 将x号程序回到前台
Leader 退出时,进程组仍存在,直到所有进程退出
会话和进程组:API
太不优雅了
setsid()
/getsid(pid)
- setsid 会脱离 controlling terminal
setpgid(pid, pgid)
/getpgid(pid)
tcsetpgrp(fd, pgid)
/tcgetpgrp(fd)
将终端(文件描述符
fd
)的前台进程组设置为pgid
获取终端的前台进程组 ID
迷惑 API
以及……uid, effective uid (?), saved uid (???)
uid:进程的实际所有者。
euid:用于权限检查(如文件访问)。
suid:允许程序临时提升权限后恢复原权限。
是的,历史的糟粕
最后:Ctrl-C 到底做了什么?
终端驱动将
Ctrl-C
转换为SIGINT
,发送给前台进程组。操作系统在目标进程返回用户态时,强制插入信号处理逻辑:
若无自定义处理函数,默认终止进程。
若有处理函数,模拟一次函数调用,执行后恢复原状态。
进程可以注册自定义函数(如 signal(SIGINT, handler)
)来响应信号;使用 sigaction
替代传统的 UNIX signal
机制。
8.3 UNIX Shell 编程语言
语言机制
预处理:
$()
(命令替换),<()
(进程替代,将命令的输出作为文件描述符提供给其他命令diff
)重定向:
cmd > file < file 2> /dev/null
2> /dev/null
将 cmd 的标准错误输出(stderr)重定向到 /dev/null
顺序结构:
cmd1; cmd2
,cmd1 && cmd2
,cmd1 || cmd2
- 顺序,短路机制
管道:
cmd1 | cmd2
- 这些命令被翻译成系统调用序列 (open, dup, pipe, fork, execve, waitpid, …)
例子:实现重定向
有趣的例子
ls > a.txt | cat
- 我已经重定向给 a.txt 了,cat 是不是就收不到输入了?
bash/zsh 的行为是不同的(前者收不到)
- 所以脚本用
#!/bin/bash
甚至#!/bin/sh
保持兼容
- 所以脚本用