跳到主要内容

文件描述符

如何查看这个文件描述符呢?

首先来看下进程的概念

在 Linux 万物皆文件,连进程也不例外,例如 ps -ux 查看进程

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
alsritt+ 1773 0.2 0.0 19292 10156 ? Ss 18:00 0:19 /lib/systemd/systemd --user
alsritt+ 1774 0.0 0.0 20804 2924 ? S 18:00 0:00 (sd-pam)
alsritt+ 1784 0.1 0.1 3913236 28716 ? SLsl 18:00 0:12 /usr/bin/startdde

实际上这个 PID 是与路径 /proc 文件夹下的文件夹对应的

image.png

换言之可以获取进程打开的文件描述符

ll /proc/PID/fd
# 统计数量用 ll /proc/1784/fd | wc -l
# 例如下:

➜ ~ ll /proc/1784/fd
total 0
lr-x------ 1 alsritter alsritter 64 Jan 7 20:10 0 -> /dev/null
l-wx------ 1 alsritter alsritter 64 Jan 7 20:10 1 -> /home/alsritter/.xsession-errors
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 10 -> 'socket:[30250]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 11 -> 'anon_inode:[eventfd]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 12 -> 'anon_inode:[eventfd]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 13 -> 'anon_inode:[eventfd]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 14 -> 'socket:[30251]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 15 -> 'anon_inode:[eventfd]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 16 -> 'socket:[30252]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 17 -> 'socket:[40266]'
lrwx------ 1 alsritter alsritter 64 Jan 7 20:10 18 -> 'socket:[814]'
lr-x------ 1 alsritter alsritter 64 Jan 7 20:10 19 -> anon_inode:inotify

这里时间后面跟着的就是文件描述符,例如 0 对应的 /dev/null,1 对应的 /home/alsritter/.xsession-errors

平时怎么使用文件描述符

Linux 中 0 1 2 是一组特殊文件描述符

  • 1 是标准输出(stdout)
  • 2 是标准错误输出(stderr)
  • 0 是标准输入(stdin)

平时我们会把这样使用 1> 两个符号连着一起,它就代表着把标准输出结果重定向到

# 例如这里把标准输出结果重定向到 list.txt 文件里面。
$ ls 1>list.txt

同理

# 这里就是把标准错误输出重定向到 list 文件里面
$ ls 2>list.txt

然后还有一个 & 也很常使用,它就是用于 “取地址”,上面只说了几个特殊的文件描述符,如果需要自定义文件描述符怎么办呢?就是通过这个 & 引用

示例1:

$ exec 6>test
$ echo 'i love linux shell!!!' 1>&6
$ cat test
i love linux shell!!!

首先把文件描述符 6 指向 test 文件。因为不像那些特殊的描述符 0 1 2,所有的输出都有个默认值,当我们想找描述符 6 的时候我们要用 & 来引用它。

其实我们可以把文件描述符想像成一个文件的引用,它可以指向任何一个文件(包括显示器),指向的过程就是我们修改默认位置的过程。而用 & 符号来找到它指向的目标文件,从而向其写入数据。

示例2:

$ exec 3>&1
$ exec 1>test
$ echo "这句话被存到test文件中"
$ echo "还有这句"
$ exec 1>&3
$ echo "这句话输出到显示器"

首先文件描述符 1 默认指向的是显示器,用 & 来找到文件描述符 1 指向的目标文件,也就是显示器。因此文件描述符 3 也指向了显示器。然后,我们修改了文件描述符 1 指向的文件到 test 文件。接着两个 echo 命令的输出会自然去找文件描述符 1,然后它看到文件描述符 1 指向的是 test 文件,所以它会把输出写到 test 文件中。最后,我们用 & 来找到文件描述符 3 指向的目标文件,也就是显示器,然后我们修改了文件描述符1指向的文件到显示器。因此,最后一个 echo 命令会自然的找文件描述符 1 然后输出到显示器上。

文件描述符是什么?

上面说了这么多,那什么是文件描述符呢?

Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行 I/O 操作的系统调用都会通过文件描述符。

下面涉及到的 Linux 操作函数可以通过 man 命令(它是单词 manual 的缩写,即使用手册的意思)来查看函数细节

因为有些函数和命令是一样的,这里补充一下 man 的使用方式,例如 read 函数,它也有一个 read 命令与它一样,默认查询的是 read 这个命令的文档,如果需要看它的 C 函数,可以使用 man 2 read,这个数字参考下面的表

1 用户命令, 可由任何人启动的。
2 系统调用, 即由内核提供的函数。
3 例程, 即库函数,比如标准 C 库 libc。
4 设备, 即 /dev 目录下的特殊文件。
5 文件格式描述, 例如 /etc/passwd。
6 游戏
7 杂项, 例如宏命令包、惯例等。
8 系统管理员工具, 只能由root启动。
9 其他(Linux特定的), 用来存放内核例行程序的文档。
n 新文档, 可能要移到更适合的领域。
o 老文档, 可能会在一段期限内保留。
l 本地文档, 与本特定系统有关的。

下面通过 Linux 的几个基本的 I/O 操作函数来理解什么是文件操作符。

fd = open(pathname, flags, mode)
// 返回了该文件的 fd
rlen = read(fd, buf, count)
// IO 操作均需要传入该文件的 fd 值
wlen = write(fd, buf, count)
status = close(fd)

每当进程用 open() 函数打开一个文件,内核便会返回该文件的文件操作符(一个非负的整形值),此后所有对该文件的操作,都会以返回的 fd 文件操作符为参数。

文件描述符可以理解为进程文件描述表这个表的索引,或者把文件描述表看做一个数组的话,文件描述符可以看做是数组的下标。当需要进行 I/O 操作的时候,会传入 fd 作为参数,先从进程文件描述符表查找该 fd 对应的那个条目,取出对应的那个已经打开的文件的句柄,根据文件句柄指向,去系统 fd 表中查找到该文件指向的 inode,从而定位到该文件的真正位置,从而进行 I/O 操作。

无论是文件句柄(Windows中概念),还是文件描述符(linux中概念),其最终目的都是用来定位打开的文件在内存中的位置,只是它们映射的方式不一样。

  • 每个文件描述符会与一个打开的文件相对应
  • 不同的文件描述符也可能指向同一个文件
  • 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开

系统为维护文件描述符,建立了三个表

  • 进程级的文件描述符表
  • 系统级的文件描述符表
  • 文件系统的 i-node 表

每个文件系统会为存储于其上的所有文件(包括目录)维护一个 i-node 表,单个 i-node 包含以下信息:

  • 文件类型(file type),可以是常规文件、目录、套接字或FIFO
  • 访问权限
  • 文件锁列表(file locks)
  • 文件大小
  • 等等

检查某个进程的文件描述符

检查某个进程的文件描述符相关内容

1、找到需要检查的进程 id(例如查询的这个 wslconnect 进程)

$ ps -aux | grep wslconnect
root 11 0.6 0.0 15504 2548 ? Ss 11:11 1:18 ./wslconnect wsl -F -p 127.0.0.1:22003

查询到的 PID 为 11

2、查看该进程的限制

$ cat /proc/11/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 0 bytes
Max resident set unlimited unlimited bytes
Max processes 15714 15714 processes
Max open files 1024 4096 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 15714 15714 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us

在 Max open files 那一行,可以看到当前设置中最大文件描述符的数量为 1024

3、查看该进程占用了多少个文件描述符

$ ll /proc/11/fd/ | wc -l
14

lsof 命令

lsof 最常用的方式就是查询端口号了

lsof -i :8080

但是这个命令是干什么的呢?

例如访问 wslconnect 这个程序的 fd 目录

$ ll /proc/11/fd/
总用量 0
dr-x------ 2 root root 0 1月 8 11:11 ./
dr-xr-xr-x 9 root root 0 1月 8 11:11 ../
lrwx------ 1 root root 64 1月 8 11:11 0 -> '/dev/pts/0 (deleted)'
lrwx------ 1 root root 64 1月 8 11:11 1 -> '/dev/pts/0 (deleted)'
l-wx------ 1 root root 64 1月 8 11:11 10 -> 'pipe:[17472]'
lr-x------ 1 root root 64 1月 8 11:11 11 -> 'pipe:[17473]'
lr-x------ 1 root root 64 1月 8 11:11 13 -> 'pipe:[17474]'
lrwx------ 1 root root 64 1月 8 11:11 2 -> '/dev/pts/0 (deleted)'
lr-x------ 1 root root 64 1月 8 11:11 3 -> 'pipe:[17462]'
lrwx------ 1 root root 64 1月 8 11:11 4 -> 'socket:[13864]'
l-wx------ 1 root root 64 1月 8 11:11 5 -> 'pipe:[17462]'
lrwx------ 1 root root 64 1月 8 11:11 6 -> 'socket:[17465]'
lrwx------ 1 root root 64 1月 8 11:11 7 -> /dev/ptmx

可以发现一个 socket 连接实际上也被分配了文件描述符,而通过这样查看端口号非常不方便,所以衍生出了 lsof(list open files) 工具,这个是一个列出当前系统打开文件的工具。

在 Linux 环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件。所以如传输控制协议 (TCP) 和用户数据报协议 (UDP) 套接字等,系统在后台都为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。

换句话说,这个 lsof 就是收集文件信息并将其收集整理的工具,因为应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,因此通过 lsof 工具能够查看这个列表对系统监测以及排错将是很有帮助的。

lsof 可以收集到如下信息(文件)

  1. 普通文件
  2. 目录
  3. 网络文件系统的文件
  4. 字符或设备文件
  5. (函数)共享库
  6. 管道,命名管道
  7. 符号链接
  8. 网络文件(例如:NFS file、网络 socket、unix 域名 socket)
  9. 还有其它类型的文件

对应的参数:

-a 列出打开文件存在的进程
-c<进程名> 列出指定进程所打开的文件
-g 列出GID号进程详情
-d<文件号> 列出占用该文件号的进程
-n<目录> 列出使用 NFS 的文件
-i<条件> 列出符合条件的进程。(4、6、协议、:端口、 @ip )
-p<进程号> 列出指定进程号所打开的文件
-u 列出UID号进程详情
-h 显示帮助信息
-v 显示版本信息

+d<目录> 列出目录下被打开的文件
+D<目录> 递归列出目录下被打开的文件

lsof 命令使用例

查看谁正在使用某个文件,也就是说查找某个文件相关的进程

# 检查谁在使用 bash
$ lsof /bin/bash
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 12 root txt REG 8,16 1113504 69241 /bin/bash
bash 30 root txt REG 8,16 1113504 69241 /bin/bash

通过某个进程号显示该进行打开的文件

$ lsof -p 1

# 列出多个进程号对应的文件信息
$ lsof -p 1,2,3

# 列出除了某个进程号,其他进程号所打开的文件信息
$ lsof -p ^1

列出所有的网络连接

$ lsof -i

# 列出所有tcp 网络连接信息
$ lsof -i tcp

# 列出所有udp网络连接信息
$ lsof -i udp

# 列出谁在使用某个端口
$ lsof -i :3306

# 列出谁在使用某个特定的udp端口
$ lsof -i udp:55

# 列出某个用户的所有活跃的网络端口
$ lsof -a -u username -i

列出某个用户打开的文件信息

$ lsof -u username

# 列出除了某个用户外的被打开的文件信息
$ lsof -u ^root

列出某个程序进程所打开的文件信息

$ lsof  -c mysql

# 列出多个进程多个打开的文件信息
$ lsof -c mysql -c apache

# 列出某个用户以及某个进程所打开的文件信息
lsof -u username -c mysql

这个 -c 选项将会列出所有以 mysql 这个进程开头的程序的文件,其实等价于

$ lsof | grep mysql

References

Linux 文件描述符详解 linux下使用man查看C函数用法 文件句柄和文件描述符的区别和理解 理解Linux的文件描述符FD与Inode - engru的文章 - 知乎 文件描述符(File Descriptor)简介 每天一个linux命令(51):lsof命令