Linux:Bash中的文件描述符详解

相关阅读

Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm=1001.2014.3001.5482


Linux中的所有进程,都拥有自己的文件描述符(File Descriptor, FD),它是操作系统在管理进程和文件时的一种抽象概念。每个文件描述符由一个非负整数表示,用来标识进程已打开的文件、输入输出流、网络套接字等资源。一个进程可以打开的文件描述符是有上限的,可以通过ulimit命令查询,如例1所示。

bash 复制代码
# 例1
zhangchen@test:~$ ulimit -n # 查询当每个进程的文件描述符数量上限
1048576

每个正在运行的进程,都会在虚拟文件系统的目录/proc下用一个子目录表示,目录名为进程的id号。当一个进程创建时,操作系统会为其分配一个未使用的id号并在目录/proc下创建相应的目录;当一个进程执行完毕退出时,操作系统会删除相应的目录并回收id号。

在目录/proc/pid/fd(pid指具体的进程id号)中,可以找到名为0、1、2...的链接文件,它们指向了相应的文件描述符代表的资源,例2展示了如何查看当前Bash进程的文件描述符。

bash 复制代码
# 例2
zhangchen@test:~$ ps   # 查询Bash进程的id号
    PID TTY          TIME CMD
2556994 pts/3    00:00:00 bash
2557252 pts/3    00:00:00 ps
zhangchen@test:~$ ls -al /proc/2556994/fd   # 显示虚拟文件系统中bash进程的文件描述符目录
lrwx------ 1 zhangchen test 64  9月 20 13:53 0 -> /dev/pts/3
lrwx------ 1 zhangchen test 64  9月 20 13:53 1 -> /dev/pts/3
lrwx------ 1 zhangchen test 64  9月 20 13:53 2 -> /dev/pts/3
lrwx------ 1 zhangchen test 64  9月 20 13:53 255 -> /dev/pts/3

其中文件描述符0、1、2尤为重要,它们是所有进程在创建时就默认拥有的文件描述符,分别表示标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)。从例2中可以看出它们都指向了/dev/pts/0这个伪终端设备(Pseudo-Terminal Slave),这是因为该终端是从GUI界面启动的(ssh远程连接的终端也是如此),如果是利用Ctrl+Alt+F*启动的终端,则会显示是/dev/tty*之类的设备。

在目录/dev下可以找到三个链接文件stdin、stdout和stderr,它们指向了当前进程的文件描述符0、1、2,如例3所示。

bash 复制代码
# 例3
zhangchen@test:~$ ls -al /dev/std* # 查询标准输入、输出、错误设备
lrwxrwxrwx 1 root root 15  7月 12 17:37 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15  7月 12 17:37 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15  7月 12 17:37 /dev/stdout -> /proc/self/fd/1

其中/proc/self是一个链接文件,指向了当前进程的目录,也就是说如果使用ls /proc命令,则显示其指向的是进程ls的目录,如例4所示。

bash 复制代码
# 例4
zhangchen@test:~$ ls -al /proc/self  # 查询当前进程(即ls)的信息
lrwxrwxrwx 1 root root 0  7月 12 17:37 /proc/self -> 2557940 # 指向了/proc/2557940

zhangchen@test:~$ ls -al /proc/self  # 查询当前进程(即ls)的信息
lrwxrwxrwx 1 root root 0  7月 12 17:37 /proc/self -> 2557972 # 指向了/proc/2557972

zhangchen@test:~$ ls -al /proc/self  # 查询当前进程(即ls)的信息
lrwxrwxrwx 1 root root 0  7月 12 17:37 /proc/self -> 2557975 # 指向了/proc/2557975

从例4中可以看出 ,连续三次使用ls命令得到的结果是不同的,这是因为每次执行ls命令都会创建一个新的进程并分配给一个未使用的id号(它们可能相等,因为执行完毕后id号会被回收,但在该例中不相等)。

有些偏题了,我们回到文件描述符,当创建了一个新的终端并查询其文件描述符时,会发现文件描述符0、1、2指向了另一个伪终端设备/dev/pts/8,如例5所示。

bash 复制代码
# 例5
zhangchen@test:~$ ps   # 查询Bash进程的id号
    PID TTY          TIME CMD
2559706 pts/3    00:00:00 bash
2559728 pts/3    00:00:00 ps
zhangchen@test:~$ ls -al /proc/2559706/fd   # 显示虚拟文件系统中Bash进程的文件描述符目录
lrwx------ 1 zhangchen test 64  9月 20 13:54 0 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 13:54 1 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 13:54 2 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 13:54 255 -> /dev/pts/8

默认情况下,子进程被创建并替换后会继承父进程的文件描述符(可以通过设置FD_CLOEXEC标志改变替换后是否继承文件描述符),为了进行验证,首先介绍一个命令exec。exec命令可以用于进程替换,也可用于操作Bash进程的文件描述符,如例6所示。在此基础上如果使用sleep 100 &命令,查询其文件描述符会发现与Bash进程的相同,如例7所示。

bash 复制代码
# 例6
zhangchen@test:~$ exec 3> output.txt   # 在当前Bash进程以写方式打开output.txt文件,分配文件描述符3
zhangchen@test:~$ ps   # 查询Bash进程的id号
    PID TTY          TIME CMD
2559706 pts/3    00:00:00 bash
2559947 pts/3    00:00:00 ps
zhangchen@test:~$ ls -al /proc/2559706/fd   # 显示虚拟文件系统中Bash进程的文件描述符目录
lrwx------ 1 zhangchen test 64  9月 20 13:54 0 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 13:54 1 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 13:54 2 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 13:54 255 -> /dev/pts/8
l-wx------ 1 zhangchen test 64  9月 20 17:02 3 -> /home/zhangchen/output.txt
bash 复制代码
# 例7
zhangchen@test:~$ sleep 100 & # 一个后台执行的测试命令
[1] 2560074
zhangchen@test:~$ ls -al /proc/2560074/fd   # 显示虚拟文件系统中sleep进程的文件描述符目录
lrwx------ 1 zhangchen test 64  9月 20 17:03 0 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 17:03 1 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 17:03 2 -> /dev/pts/8
lrwx------ 1 zhangchen test 64  9月 20 17:03 255 -> /dev/pts/8
l-wx------ 1 zhangchen test 64  9月 20 17:03 3 -> /home/zhangchen/output.txt

例8展示了在Python中打开一个文件,并显示其文件描述符。

python 复制代码
# 例8
# 文件:test.py

import time
file = open('example.txt', 'w') # 打开文件
fd = file.fileno() # 获取文件描述符
print("File descriptor assigned: {}".format(fd)) # 输出文件描述符
time.sleep(60) # 等待60秒
file.close() # 关闭文件



zhangchen@test:~$ python test.py & # 一个后台执行的Python进程
[2] 11491
File descriptor assigned: 3
zhangchen@test:~$ ls -al /proc/11491/fd   # 显示虚拟文件系统中python进程的文件描述符目录
lrwx------ 1 zhangchen test 64  9月 20 17:06 0 -> /dev/pts/0
lrwx------ 1 zhangchen test 64  9月 20 17:06 1 -> /dev/pts/0
lrwx------ 1 zhangchen test 64  9月 20 17:06 2 -> /dev/pts/0
l-wx------ 1 zhangchen test 64  9月 20 17:06 3 -> /home/zhangchen/example.txt

看起来像是Linux会选择优先当前未使用最小的文件描述符,这是对的!但是否Python进程只打开过example.txt一个文件?答案是否定的(显然,Python进程肯定还打开了test.py文件)。

一个命令的执行可能牵涉到多次打开文件、关闭文件的过程,例9使用了strace命令观察了cat命令执行的过程。

python 复制代码
# 例9
[zhangchen@EDA Desktop]$ strace cat tt
execve("/usr/bin/cat", ["cat", "tt"], 0x7fff10389408 /* 67 vars */) = 0
brk(NULL)                               = 0x1472000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8c9f2f0000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/tls/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/tls/x86_64", 0x7ffe8d6e3750) = -1 ENOENT (No such file or directory)
open("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/tls/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/tls", 0x7ffe8d6e3750) = -1 ENOENT (No such file or directory)
open("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/x86_64/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/x86_64", 0x7ffe8d6e3750) = -1 ENOENT (No such file or directory)
open("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/opt/Synopsys/LC2018/lc/O-2018.06-SP1/linux64/lc/shlib", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=126574, ...}) = 0
mmap(NULL, 126574, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8c9f2d1000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`&\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2156240, ...}) = 0
mmap(NULL, 3985920, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8c9ed02000
mprotect(0x7f8c9eec5000, 2097152, PROT_NONE) = 0
mmap(0x7f8c9f0c5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c3000) = 0x7f8c9f0c5000
mmap(0x7f8c9f0cb000, 16896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f8c9f0cb000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8c9f2d0000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8c9f2ce000
arch_prctl(ARCH_SET_FS, 0x7f8c9f2ce740) = 0
mprotect(0x7f8c9f0c5000, 16384, PROT_READ) = 0
mprotect(0x60b000, 4096, PROT_READ)     = 0
mprotect(0x7f8c9f2f1000, 4096, PROT_READ) = 0
munmap(0x7f8c9f2d1000, 126574)          = 0
brk(NULL)                               = 0x1472000
brk(0x1493000)                          = 0x1493000
brk(NULL)                               = 0x1493000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=106172832, ...}) = 0
mmap(NULL, 106172832, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8c987c0000
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
open("tt", O_RDONLY)                    = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=6, ...}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
read(3, "test\n\n", 65536)              = 6
write(1, "test\n\n", 6test

)                 = 6
read(3, "", 65536)                      = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

可以看出,cat命令在执行时,打开过三类文件(标准输入、输出和错误不用打开,因为它们继承自父进程),而且文件描述符的值其实就是系统调用open函数的返回值。

python 复制代码
# 动态链接相关的文件
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 # 分配文件描述符3
*****
close(3) # 关闭文件描述符3
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 # 分配文件描述符3
*****
close(3) # 关闭文件描述符3

# 本地语言环境文件
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3 # 分配文件描述符3
*****
close(3) # 关闭文件描述符3

# 目标文件(tt)
open("tt", O_RDONLY) = 3 # 分配文件描述符3
*****
close(3) # 关闭文件描述符3
相关推荐
大树8820 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠20 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质21 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush421 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52021 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz21 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工1 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智1 天前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩1 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_1 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化