一句话:文件描述符(fd)就是一个整数——进程私有「接线表」的编号。程序只认编号、不认设备,这正是重定向能成立的前提。
Linux笔记1.2---一切皆文件(下)中, 程序同时打开了一堆「文件」,怎么区分「我现在操作的是哪一个」?
答案就是 文件描述符(fd)——一个编号,指向进程那张私有的接线表。
它是一个小整数(0、1、2、3…),是进程用来引用「一个打开的文件」的编号。fd 不是文件本身,是索引。例如fd=0,表示的就是
内核里每个进程都挂着一张私有的 fd 表(task_struct->files)。表里每一项指向一个「打开的文件」;fd 就是这张表的下标。所以fd=1永远是指「我这个进程的表里第2号位置file[1]」——你的 fd 1 和我的 fd 1 可以指向完全不同的东西。
普通文件、目录、设备(/dev/null、/dev/tty)、管道、socket、/proc/...——全都是「文件」,都能用 fd 引用、用 read/write 读写。这就是「一切皆文件」的具体落点:不是它们长得像,是它们共用同一套 fd + read/write 接口。
内核没有规定 0/1/2 必须是什么。它只是一个长期约定(POSIX):
关键在于:程序把这个约定写死在代码里——C 库的 printf 往 fd 1 写、scanf 读 fd 0、perror 往 fd 2 写。程序只认编号,不认设备。fd 1 接的是屏幕、文件还是管道,程序既不知道、也不关心。
正是「程序认编号不认设备」这一点,让重定向成为可能:只要把 fd 1 改接到别处,程序不用改一行代码,输出就换了去向。
一个刚启动、接终端的进程,它的 fd 表长这样:
进程的 fd 表(默认接终端 /dev/pts/0)┌──────┬──────────────────────────┐│ fd 0 │ → /dev/pts/0 (stdin 键盘) ││ fd 1 │ → /dev/pts/0 (stdout 屏幕) ││ fd 2 │ → /dev/pts/0 (stderr 屏幕) │└──────┴──────────────────────────┘/proc/PID/fd 肉眼看 fd 表fd 最大的好处:fd 表是看得见的,不用猜。Linux 把每个进程的 fd 表暴露在 /proc/PID/fd:
ls -l /proc/$$/fd # $$ 是当前 shell 的 PID典型输出(0/1/2 都接终端,255 是 bash 自用的):
lrwx------ 0 -> /dev/pts/0lrwx------ 1 -> /dev/pts/0lrwx------ 2 -> /dev/pts/0lrwx------ 255 -> /dev/pts/0改接线,再回头看(自己跑这一段,最能体会「fd 表是一张真实存在的、可操作的表」):
exec 3>/tmp/y # 当前 shell 手动开一个 fd 3,指向 /tmp/yls -l /proc/$$/fd # 会多出一行:3 -> /tmp/yexec 3>&- # 关掉 fd 3(3>&- = 关闭),别留垃圾看到
3 -> /tmp/y出现的那一刻,你就理解了 fd 表是真实存在的、可操作的接线表。重定向、管道干的也是这件事——只不过由 Shell 在子进程里自动做完。
fd 表不是孤立的,它在进程创建和程序替换时有确定的行为——这两条是 Shell 能「先接好线再启动命令」的根基。
fork() 复制父进程,连 fd 表一起复制。但「复制」要理解准:父子进程的 fd 表项指向同一个「打开的文件」(内核里的 open file description),所以它们共享同一个文件偏移量(offset)。
父进程 fd 表 子进程 fd 表fd 3 → ┐ fd 3 → ┐ └──► 同一个打开文件(含 offset)◄──┘ (父子共享偏移量,互相能看到对方的读写位置)这就是为什么 Shell 在父进程里接好线,fork 出的子进程能直接继承这套接好的 fd 表——子进程不用重新接线。
exec() 换了程序代码,但fd 表默认原样保留。
例外(一句带过):某个 fd 若标记了 close-on-exec(
FD_CLOEXEC),exec 时会被自动关掉。这是给「不想被子进程继承」的 fd 用的,日常重定向碰不到。
fd 三定律: