分类: LINUX
2012-01-12 14:09:47
我在另一篇文章<<为linux下ls添加 dir /ad dir/a-d的功能>>的末尾, 说对于递归地列出某个目录下的文件(指目录之外的所有其它类型), 只会列出当前目录下的所有文件.
要想把这个小小的功能做到完善, 花费了我很多的时间. 我在下文中列出这一过程中碰到的问题和解决的办法.
上一次的方案中还有一个问题:
libls.so 这个动态库, 在运行时会需要 libdl.so 这个库, 对于
LD_PRELOAD=libls.so ls
来说, 这没有问题, 但对于其它程序, 如果指定了这个库, 程序就会在符号解析阶段失败. 原因是ls本身就需要libdl.so 这个动态库, 所以libls.so 需要解析动态符号dlsym时, 不会失败, 而其它程序, 如xargs, 它不需要libdl.so, 但libls.so 却在解析时需要, 就会造成xargs在进入main之前就失败了.
问题一: 让ibls.so对非ls进程安全无公害这个问题需要解决, 原因是有些情况下, 我们需要为ls的父进程指定这个环境变量, 如下:
cat nul_separated_dir_list.txt | LD_PRELOAD=./libls.so xargs -0 ls ...
首先在进程之前用 env_var_name=value 的方式指定临时的环境变量的用法, 即使是在管道之后也有效.
虽然可以用export LD_PRELOAD的办法, 但这个办法会引起潜在的更大的风险, 可能使当前环境中的所有使用都会加载该动态库. 也可能会覆盖了原来的LD_PRELOAD变量的设置.
所以我需要把libls.so 这个库做成对ls之外的程序是无害的.
第一步是让libls.so 本身就链接到libdl.so 这个动态库, 在编译选项中加入-ldl:
gcc -Wall -Wextra -D_GNU_SOURCE -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Wl,-init,my_init -std=c99 -fPIC -shared -o libls.so lsd.c -ldl
第二步是检测当前进程是不是ls, 如果不是, 则要保持所有原函数的功能不受到影响.
在so的init函数中, 加入了这么一段:
char proc_exe[PATH_MAX] = {};
char real_exe[PATH_MAX] = {};
snprintf(proc_exe, sizeof(proc_exe) - 1, "/proc/%u/exe", getpid() );
readlink(proc_exe, real_exe, sizeof(real_exe) );
g_is_ls = false;
g_is_ls = (0 == strcmp("/ls", strrchr(real_exe, '/') ));
这个方案假设/proc文件系统可用, 然后通过查看进程名字是否为ls来判断是不是ls进程, 这不能100%保证此ls就是彼ls, 但在现实中, 发生这种冲突的可能性几乎不存在, 因为只有用户有意指定LD_PRELOAD环境变量时, 这一切才会发生.
被截获的每个动态符号, 如fstat64, 需要检测g_is_ls, 如果不是, 则直接调用原有功能后返回.
问题二: 要递归显示某目录下的文件, 先要递归获取该目录下的所有子目录如下的目录结构:
.
|-- --x
| `-- a.txt
`-- dx
|-- a
| `-- b
| `-- c.txt
|-- d^Md
| `-- special.txt
|-- file.txt
|-- new
`-- old
7 directories, 4 files
如果用lsf -R dx
对比"ls" -R dx的结果:
dx:
a/ d d/ file.txt new/ old/dx/a:
b/dx/a/b:
c.txtdx/d?d:
special.txtdx/new:
dx/old:
我希望的结果除了不列出每个目录下的目录项之外, 其它方面保持不变, 比如一个目录下如果没有任何文件, 也要显示一个空的目录项:
dx:
d file.txtdx/a:
dx/a/b:
c.txtdx/d?d:
special.txtdx/new:
dx/old:
整个解决方案中, 如果没有递归, 则只显示目录和只显示文件都是非常简单的: libls.so 先于main的运行就被加载, 并且其init函数会被执行, 在该函数中, 它获取下一个fstat64函数的地址并保存起来, 在libls.so的加载期间, 它用自己提供的fstat64函数替代了原本执行ls时会用到的libc.so中的fstat64函数, 这样ls在调用fstat64函数时, 调用的实际上是libls.so所提供的那个fstat64, 这是通过在环境变量LD_PRELOAD中指定预先加载该动态库实现的. 私有版本的fstat64当然需要调用真正的fstat64获取一个目录项, 但获取该目录项之后, 它会判断该目录项的内容, 然后根据 LS_DIR_ONLY和LS_NON_DIR_ONLY两个环境变量的存在与否, 决定是否忽略该目录项, 如果忽略, 则需要取下一个目录项, 没有下一个目录项时, 返回NULL, 结束对当前目录项的处理.
递归显示目录也是同样简单.
但递归显示文件, 却是那个1%的功能中, 需要花费99%精力的地方.
原因是此时不能在fstat64获取的目录项是目录时, 就简单地忽略了, 因为一个目录项一旦被忽略, 它就不会被递归地处理了, ls在递归处理时, 在内部需要先构造出整个子目录树和各个子目录下的内容.
所以我选择的办法是: 先要得到整个目录树, 然后对整个目录树, 显示其下的文件.
问题二: 如何得到目录树并显示其下的文件.
最显而易见的办法是
find dir_name -type d -print0 | LD_PRELOAD=libls.so LS_NON_DIR_ONLY=1 xargs -0 ls
但是, 这个办法可能会导致跟原始ls不一致的结果, 而保持结果与原始ls高度一致, 是我整个方案的一个出发点. 这个方案不可行的一个原因是:
首先xargs会决定对全部参数进行分组, 并决定哪些参数作为同一批传递给命令ls, 这么做对ls的影响是什么? 首先ls可能会被运行多次, 而原始ls在递归时对各个子目录的显示则是: 除最后一个目录之外, 其它的所有目录内容显示之后都会有一个空行. 通过xargs来运行的多次ls结果是不严格一致的. 其次, 另一个更严重的问题是, find找到的目录的顺序可能与用户原始期望的顺序不一致, 因为ls可以指定不同的排序来显示结果. 而find是深度优先地找出所有目录, 无法排序. 第三个问题是, 用户使用原始的ls可能被alias了, 为了保持原来alias的ls选项, 需要在xargs之后使用alias之后的命令和选项. 这个问题在稍后会解决.
为了让显示的各个目录以正确的顺序出现, 我们需要让ls自己来输出目录结构. 这个功能是上述方案中已有的. 但为了能正确处理任何的目录名, 需要让输出结果中每一项以'\0'结尾, 就象find的 -print0和xargs 的-0参数所做的那样. 这通过重定向ls 对目录结构的输出就很困难, 我最后选择的办法是, 观察 ls 在递归显示每个目录项时, 是通过什么函数调用输出目录项本身然后后面跟一个冒号和换行符, 举例来说,
"ls" -R dx
的输出是
dx:
a/ ......
其中对每一个目录, 都会有一个单独的行来显示, 后面跟一个冒号然后换行. 通过
strace 容易知道这一行的输出是通过 fwrite_unlocked 调用实现的. 我们可以象对待fstat64一样截获这个调用, 从而获得ls本身所要输出的目录, 在每个目录后面输出一个'\0'字符, 这样输出的结果就跟find 的-print0效果一样了. 这一办法需要一个额外的环境变量就是用来保存目录列表的文件名.
LS_DIR_LIST_TO_FILE=list.txt LD_PRELOAD=libls.so LS_DIR_ONLY ls -R >& /dev/null
这个命令运行完之后, 文件list.txt中就保存了正确的, 由ls本尊所决定的目录名顺序. 把它保存为文件是另有用途的. 原因是这个文件的内容会多于一遍被处理.
问题三: 如何让ls的多次运行跟一次运行的结果一致这个问题略作解释, ls -R dx的结果是:
dx:
a/ d?d/ file.txt new/ old/dx/a:
b/dx/a/b:
c.txtdx/d?d:
special.txtdx/new:
dx/old:
可以看到, 每一个目录项一个冒号. 如果我们把各个目录分多次运行, 结果将会是
ls dx dx/new dx/old; ls dx/a/b
dx/new:
dx/old:
dx/a/b:
c.txt
可以看到, dx/old和下一个命令的dx/a/b挤在了一块. 也就是说, 需要有个办法知道哪一次的ls命令, 将会是最后一次运行, 对最后一次运行, 需要输出一个额外的空行.
要做到这一点, 需要知道总目录项的个数, 并且把多次ls处理之后的结果保存在一个地方, 供每一次的ls运行时可以检查. 对上面的例子, 如果 ls 最终要显示 dx/new dx/old /dx/a/b 3个目录, 但分为2批, 则总的上期同个数是3, 第一次命令运行完后, 需要把该次命令所处理的目录项个数2保存起来. 下一次ls运行时, 把它命令行上的参数个数加上保存起来的2, 发现是最后一次的ls调用, 所以不输出空行, 此前的ls调用则要输出一个空行, ls命令本身当然不具备做这个判断的能力, 我也不想把这个功能再加入到libls.so 中. 保存为一个脚本, 如下:
问题四: 如何得到目录项的总个数#!/bin/bash
total_args="$1"
handled_arg_count_fname="$2"
possible_aliased_ls="$3"
shift
shift
shift
LS_NON_DIR_ONLY=1 LD_PRELOAD=~/.libls.so $possible_aliased_ls "$@"
# A blank linesdeclare -i handled_args="${#@}"
let handled_args+="$(cat $handled_arg_count_fname)"if [ "${handled_args}" -lt $total_args ]; then
echo ""
fi
echo $handled_args > $handled_arg_count_fname
这个本可以在问题二中一并解决, 但我把它用一个脚本来解决, 目录列表文件实质是二进制文件, 因为每个目录项之后都有一个额外的'\0', 用下面的办法可以得到总个数:
n_all_dirs="$(tr -d '\n' < $all_dirs_fname | tr '\0' '\n' | wc -l)"
tr 先把文件中的\n字符都删除, 如果有'\n'字符的话, 它是作为目录名的一部分出现的. 然后把'\0'替换为'\n', 最后用wc一统计行数.
另一个办法, 也可以得到:
n_all_dirs="$(od -t x1 -w1 $all_dirs_fname | grep -Fc ' 00')"
注意00前面有一个空格.
最后, 临时文件都是用mktemp生成的. 最终的方案显得很复杂, 但却是我能想到的完美地改变ls行为的办法. 其它已知的办法, 都存在这样那样的问题而与原始的ls输出不一致. 幸亏这种最复杂的情况, 只是在非常少的情况下需要用到, 所以整个方案的运行时开销相对还是比较小的.
东西虽然实现了. 但它的价值如何呢, 如果这个功能真的是需求很大, 为什么它没有出现在ls本身里面? 老实说, 我觉得这个功能价值不是很大. 显示当前目录下的所有目录, 或所有文件, 还有些价值, 但递归显示文件和目录, 几乎没有什么意义. 这种情况下, 用户借助于find 可以得到更好的效果.
问题五: 如何用find 查找一个首字母为-的目录?这个问题不是最终解决方案的一部分, 只是我在中间过程中发现的
必需变通地处理
find -wholename "./-dir_name/*"
find 不能在--选项之后指定argument