这和很多事情一样,比表面看起来更复杂。
文件系统中的每个实体都指向
inode
,它描述文件的内容。实体就是你看到的东西-文件、目录、套接字、块设备、字符设备等等。。。
一个“的内容”
文件
“可以通过一个或多个路径访问-这些路径中的每一个都称为”
硬链接
". 硬链接只能指向同一文件系统上的文件,它们不能跨越文件系统的边界。
也可以通过路径来处理“
符号链接
如果不扫描整个树,就不可能定位指向特定实体的所有链接(符号链接或硬链接)。
在我们进入这之前。。。一些评论:
-
请看最后的一些基准。我不相信这是一个重要的问题,尽管无可否认,这个文件系统在一个6磁盘的ZFS阵列上,在i7上,所以使用较低规格的系统需要更长的时间。。。
-
鉴于这是
不可能的
不打电话
stat()
如前所述,我们
必须
扫描(索引)整棵树。我知道这不是你想做的,但不这样做是不可能的。。。
要做到这一点,你需要收集
,而不是文件名,并在事后复查。。。这里可能有一些优化,但我尽量保持简单,以优先考虑理解。
def get_map(scan_root):
# this dict will have device IDs at the first level (major / minor) ...
# ... and inodes IDs at the second level
# each inode will have the following keys:
# - 'type' the entity's type - i.e: dir, file, socket, etc...
# - 'links' a list of all found hard links to the inode
# - 'symlinks' a list of all found symlinks to the inode
# e.g: entities[2049][4756]['links'][0] path to a hard link for inode 4756
# entities[2049][4756]['symlinks'][0] path to a symlink that points at an entity with inode 4756
entity_map = {}
for root, dirs, files in os.walk(scan_root):
root = '.' + root[len(scan_root):]
for path in [ os.path.join(root, _) for _ in files ]:
try:
p_stat = os.stat(path)
except OSError as e:
if e.errno == 2:
print('Broken symlink [%s]... skipping' % ( path ))
continue
if e.errno == 40:
print('Too many levels of symbolic links [%s]... skipping' % ( path ))
continue
raise
p_dev = p_stat.st_dev
p_ino = p_stat.st_ino
if p_dev not in entity_map:
entity_map[p_dev] = {}
e_dev = entity_map[p_dev]
if p_ino not in e_dev:
e_dev[p_ino] = {
'type': get_type(p_stat.st_mode),
'links': [],
'symlinks': [],
}
e_ino = e_dev[p_ino]
if os.lstat(path).st_ino == p_ino:
e_ino['links'].append(path)
else:
e_ino['symlinks'].append(path)
return entity_map
我制作了一个示例树,如下所示:
$ tree --inodes
.
âââ [ 67687] 4 -> 5
âââ [ 67676] 5 -> 4
âââ [ 67675] 6 -> dead
âââ [ 67676] a
â  âââ [ 67679] 1
âââ [ 67677] b
â  âââ [ 67679] 2 -> ../a/1
âââ [ 67678] c
â  âââ [ 67679] 3
âââ [ 67687] d
âââ [ 67688] 4
4 directories, 7 files
此函数的输出为:
$ places
Broken symlink [./6]... skipping
Too many levels of symbolic links [./5]... skipping
Too many levels of symbolic links [./4]... skipping
{201: {67679: {'links': ['./a/1', './c/3'],
'symlinks': ['./b/2'],
'type': 'file'},
67688: {'links': ['./d/4'], 'symlinks': [], 'type': 'file'}}}
如果我们对
./c/3
./a/1
...
通过随后搜索我们感兴趣的路径,我们可以在此树中找到所有其他引用:
def filter_map(entity_map, filename):
for dev, inodes in entity_map.items():
for inode, info in inodes.items():
if filename in info['links'] or filename in info['symlinks']:
return info
$ places ./a/1
Broken symlink [./6]... skipping
Too many levels of symbolic links [./5]... skipping
Too many levels of symbolic links [./4]... skipping
{'links': ['./a/1', './c/3'], 'symlinks': ['./b/2'], 'type': 'file'}
此演示的完整源代码如下。请注意,我使用了相对路径来保持简单,但最好将其更新为使用绝对路径。另外,任何指向树外部的符号链接当前都没有对应的
link
... 这是给读者的练习。
也可以在填充树时收集数据(如果这是与您的过程一起工作的话)。。。你可以用
inotify
为了处理好这件事-甚至有一个
python module
#!/usr/bin/env python3
import os, sys, stat
from pprint import pprint
def get_type(mode):
if stat.S_ISDIR(mode):
return 'directory'
if stat.S_ISCHR(mode):
return 'character'
if stat.S_ISBLK(mode):
return 'block'
if stat.S_ISREG(mode):
return 'file'
if stat.S_ISFIFO(mode):
return 'fifo'
if stat.S_ISLNK(mode):
return 'symlink'
if stat.S_ISSOCK(mode):
return 'socket'
return 'unknown'
def get_map(scan_root):
# this dict will have device IDs at the first level (major / minor) ...
# ... and inodes IDs at the second level
# each inode will have the following keys:
# - 'type' the entity's type - i.e: dir, file, socket, etc...
# - 'links' a list of all found hard links to the inode
# - 'symlinks' a list of all found symlinks to the inode
# e.g: entities[2049][4756]['links'][0] path to a hard link for inode 4756
# entities[2049][4756]['symlinks'][0] path to a symlink that points at an entity with inode 4756
entity_map = {}
for root, dirs, files in os.walk(scan_root):
root = '.' + root[len(scan_root):]
for path in [ os.path.join(root, _) for _ in files ]:
try:
p_stat = os.stat(path)
except OSError as e:
if e.errno == 2:
print('Broken symlink [%s]... skipping' % ( path ))
continue
if e.errno == 40:
print('Too many levels of symbolic links [%s]... skipping' % ( path ))
continue
raise
p_dev = p_stat.st_dev
p_ino = p_stat.st_ino
if p_dev not in entity_map:
entity_map[p_dev] = {}
e_dev = entity_map[p_dev]
if p_ino not in e_dev:
e_dev[p_ino] = {
'type': get_type(p_stat.st_mode),
'links': [],
'symlinks': [],
}
e_ino = e_dev[p_ino]
if os.lstat(path).st_ino == p_ino:
e_ino['links'].append(path)
else:
e_ino['symlinks'].append(path)
return entity_map
def filter_map(entity_map, filename):
for dev, inodes in entity_map.items():
for inode, info in inodes.items():
if filename in info['links'] or filename in info['symlinks']:
return info
entity_map = get_map(os.getcwd())
if len(sys.argv) == 2:
entity_info = filter_map(entity_map, sys.argv[1])
pprint(entity_info)
else:
pprint(entity_map)
出于好奇,我在我的系统上运行了这个。它是i7-7700K上的一个6x磁盘的ZFS RAID-Z2池,有大量的数据可供使用。诚然,在低规格的系统上运行会慢一些。。。
需要考虑的一些基准:
-
包含~850个目录中~3.1k文件和链接的数据集。
-
包含~2.2k目录中~30k个文件和链接的数据集。
-
包含~73.5k文件和~8k目录中链接的数据集。
这大约需要60秒,后续运行约800毫秒
用简单的数学计算,大约是1140
统计()
使用空缓存每秒调用数,或~90k
统计()
缓存填满后每秒的调用数-我不这么认为
统计()
就像你想的那样慢!