$ time for f in * ; do cat "$f" > /dev/null ; done
real 0m3.399s
user 0m0.130s
sys 0m1.940s
$ time for f in * ; do cat < "$f" > /dev/null ; done
real 0m3.430s
user 0m0.100s
sys 0m2.043s
现在,除了编写像上面这样疯狂的复合命令之外,为什么这有助于理解?作为开发人员,您可能希望利用或提防此类行为。假设cat您编写了一个需要作为文件传递或从标准输入传递的配置的 C 程序,而您像myprog myconfig.json. 如果你跑了会发生什么{ head -n1; myprog;} < myconfig.json?最好的情况是你的程序会得到不完整的配置数据,最坏的情况是——破坏程序。我们还可以利用它作为产生子进程的优势,并让父进程回退到子进程应该处理的数据。
权限和特权
这次让我们从一个对其他用户没有读取或写入权限的文件的示例开始:
$ sudo -u potato cat < testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
$ sudo -u potato cat testfile.txt
cat: testfile.txt: Permission denied
potato@my-PC:/home/administrator$ ./privileged testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
potato@my-PC:/home/administrator$ ./privileged < testfile.txt
bash: testfile.txt: Permission denied
$ cat /tmp/mysocket.sock
cat: /tmp/mysocket.sock: No such device or address
$ cat < /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
程序将
cat
打开、读取和关闭文件。您的 shell 将打开文件并将内容连接到
cat
的标准输入。cat
识别它没有文件参数,并将从标准输入读取。从用户的角度来看没有区别。这些命令做同样的事情。
从技术上讲,区别在于打开文件的程序是什么:
cat
程序或运行它的外壳。重定向由 shell 在运行命令之前设置。(所以在其他一些命令中——也就是说,不是问题中显示的命令——可能会有所不同。特别是,如果您无法访问
file.txt
但 root 用户可以访问,则可以sudo cat file.txt
工作但sudo cat < file.txt
不能。)您可以使用在您的情况下方便的任何一种。
几乎总是有很多方法可以得到相同的结果。
cat
接受来自参数的文件,或者stdin
如果没有参数。见
man cat
:一大区别
一个很大的区别是
*
,?
或[
globbing 字符(通配符)或任何其他 shell 可能扩展为多个文件名的内容。任何外壳扩展为两个或多个项目,而不是视为单个文件名,都不能打开以进行重定向。如果没有重定向(即 no
<
),shell 会将多个文件名传递给cat
,它会一个接一个地输出文件的内容。例如这有效:但是使用重定向 (
<
) 会出现错误消息:一个微小的差异
我认为重定向会更慢,但没有可察觉的时间差异:
笔记:
主要区别在于谁打开文件、shell 或 cat。他们可能在不同的许可制度下运行,所以
可能会工作,而
将失败。当只是想用于简单的脚本编写时,这种权限机制可能有点难以解决
echo
,因此存在滥用的tee
权宜之计由于权限问题,这实际上并不能使用重定向来代替。
TL;DR 答案版本:
应用
cat file.txt
程序(在本例中cat
)接收到一个位置参数,对其执行 open(2) 系统调用,并在应用程序内进行权限检查。使用
cat < file.txt
shell 将执行dup2()
系统调用以使stdin 成为与该文件描述符(例如3)相对应的文件描述符(通常是下一个可用的,例如3)的副本,file.txt
并关闭该文件描述符(例如3)。应用程序不对文件执行 open(2) 并且不知道文件的存在;它严格按照其标准输入文件描述符运行。权限检查取决于外壳。打开文件的描述将与外壳打开文件时保持相同。介绍
从表面上看
cat file.txt
,cat < file.txt
行为相同,但在幕后还有更多的事情发生,只有一个性格差异。这个<
字符会改变 shell 的理解方式file.txt
、打开文件的人以及文件在 shell 和命令之间传递的方式。当然,为了解释所有这些细节,我们还需要了解如何在 shell 中打开文件和运行命令,这就是我的答案旨在实现的目标 - 用最简单的术语教育读者这些看似简单的命令。在这个答案中,您会找到多个示例,包括那些使用strace命令备份幕后实际发生的解释的示例。由于 shell 和命令的内部工作方式是基于标准系统调用的,
cat
因此仅将其视为许多其他命令中的一个很重要。如果您是阅读此答案的初学者,请保持开放的心态,并注意这prog file.txt
并不总是与prog < file.txt
. 当两种形式应用于不同的命令时,它的行为可能完全不同,这取决于权限或程序的编写方式。我还请您暂停判断,并从不同用户的角度看待这一点——对于一个临时的 shell 用户来说,他们的需求可能与系统管理员和开发人员的需求完全不同。execve() 系统调用和可执行文件看到的位置参数
Shell 通过使用fork(2) syscall创建一个子进程并调用execve(2) syscall 来运行命令,后者使用指定的参数和环境变量执行命令。内部调用的命令
execve()
将接管并替换进程;例如,当 shell 调用cat
时,它将首先创建一个 PID 为 12345 的子进程,然后execve()
PID 12345 变为cat
.cat file.txt
这给我们带来了和之间的区别cat < file.txt
。在第一种情况下,cat file.txt
是使用一个位置参数调用的命令,shell 将execve()
适当地组合在一起:在第二种情况下,该
<
部分是 shell 运算符,并< testfile.txt
告诉 shell 打开testfile.txt
并将 stdin 文件描述符 0 转换为文件描述符的副本,该副本对应于testfile.txt
. 这意味着< testfile.txt
不会作为位置参数传递给命令本身:如果程序需要位置参数才能正常运行,这可能很重要。在这种情况下,
cat
如果没有提供与文件对应的位置参数,则默认接受来自标准输入的输入。这也将我们带到了下一个主题:标准输入和文件描述符。STDIN 和文件描述符
谁打开文件
cat
或外壳?他们如何打开它?他们甚至有权打开它吗?这些是可以提出的问题,但首先我们需要了解打开文件的工作原理。当一个进程执行
open()
或openat()
处理一个文件时,这些函数为进程提供一个与打开的文件相对应的整数,然后程序可以通过引用该整数来调用read()
、seek()
、 和调用以及无数其他系统调用。write()
当然,系统(又名内核)将在内存中保存特定文件的打开方式、权限、模式 - 只读、只写、读/写 - 以及我们当前在文件中的位置- 在字节 0 或字节 1024 - 这称为偏移量。这称为打开文件描述。在最基本的层面上,
cat testfile.txt
是cat
打开文件的位置,它将被下一个可用的文件描述符引用,即 3(注意read(2)中的 3 )。相比之下,
cat < testfile.txt
将使用文件描述符 0(又名 stdin):还记得早些时候我们了解到 shell
fork()
先通过然后exec()
类型的进程运行命令吗?好吧,事实证明文件是如何打开的,从而转移到使用fork()/exec()
模式创建的子进程。引用open(2) 手册:这对
cat file.txt
vs意味着什么cat < file.txt
?其实很多。在cat file.txt
打开cat
文件时,这意味着它可以控制文件的打开方式。在第二种情况下,shell 将打开file.txt
它,并且对于子进程、复合命令和管道,它的打开方式将保持不变。我们目前在文件中的位置也将保持不变。我们以这个文件为例:
看下面的例子。为什么
line
第一行的单词没有变化?答案就在上面open(2)手册的引用中:shell 打开的文件被复制到复合命令的标准输入上,并且每个运行的命令/进程共享打开文件描述的偏移量。
head
只需将文件向前倒退一行,然后sed
处理其余部分。更具体地说,我们会看到 2 个dup2()
/fork()
/execve()
系统调用序列,并且在每种情况下,我们都会获得文件描述符的副本,它在 open 上引用相同的文件描述testfile.txt
。使困惑 ?让我们举一个更疯狂的例子:在这里,我们打印了第一行,然后将打开的文件描述倒回前 5 个字节(消除了单词
line
),然后只打印了其余部分。我们是如何做到的?打开的文件描述testfile.txt
保持不变,文件上的共享偏移量。现在,除了编写像上面这样疯狂的复合命令之外,为什么这有助于理解?作为开发人员,您可能希望利用或提防此类行为。假设
cat
您编写了一个需要作为文件传递或从标准输入传递的配置的 C 程序,而您像myprog myconfig.json
. 如果你跑了会发生什么{ head -n1; myprog;} < myconfig.json
?最好的情况是你的程序会得到不完整的配置数据,最坏的情况是——破坏程序。我们还可以利用它作为产生子进程的优势,并让父进程回退到子进程应该处理的数据。权限和特权
这次让我们从一个对其他用户没有读取或写入权限的文件的示例开始:
这里发生了什么 ?为什么我们可以在第一个示例中以
potato
用户身份读取文件,但不能在第二个示例中读取文件?这可以追溯到前面提到的open(2)手册页中的相同引用。使用< file.txt
shell 打开文件,因此权限检查发生在shell 执行open
/openat()
时。当时的 shell 以对文件具有读取权限的文件所有者的权限运行。由于跨dup2
调用继承了打开文件描述,shell 将打开文件描述符的副本sudo
传递给 ,将文件描述符的副本传递给cat
,并且cat
不知道其他任何内容,因此愉快地读取文件的内容。在最后一个命令中,cat
土豆下的用户执行open()
在文件上,当然该用户没有读取文件的权限。更实际和更常见的是,这就是为什么用户对为什么这样的东西不起作用(运行特权命令写入他们无法打开的文件)感到困惑:
但是这样的事情是有效的(使用特权命令写入需要特权的文件):
privileged_prog < file.txt
与我之前展示的情况(失败但确实有效)相反的情况的理论示例privileged_prog file.txt
是使用 SUID 程序。SUID 程序,例如passwd
,允许执行具有可执行所有者权限的操作。这就是为什么passwd
命令允许您更改密码,然后将该更改写入/etc/shadow,即使该文件由 root 用户拥有。为了示例和乐趣,我实际上用
cat
C 语言编写了类似演示的应用程序(此处为源代码)并设置了 SUID 位,但如果你明白了 - 随时跳到这个答案的下一部分并忽略这部分. 旁注:操作系统忽略解释可执行文件上的 SUID 位#!
,因此同一事物的 Python 版本会失败。让我们检查程序的权限和
testfile.txt
:看起来不错,只有文件所有者和属于
administrator
组的人才能读取此文件。现在让我们以土豆用户身份登录并尝试读取文件:看起来不错,shell 和
cat
拥有土豆用户权限的人都不能读取他们不允许读取的文件。还要注意谁报告了错误 -cat
vsbash
。让我们测试一下我们的 SUID 程序:按预期工作!同样,这个小演示的要点是,
prog file.txt
打开prog < file.txt
文件的人不同,打开文件的权限也不同。程序如何对 STDIN 做出反应
我们已经知道
< testfile.txt
以这样的方式重写标准输入,即数据将来自指定的文件而不是键盘。理论上,并且基于 Unix 的“做一件事并把它做好”的哲学,从标准输入(又名文件描述符 0)读取的程序应该表现一致,因此prog1 | prog2
应该类似于prog2 file.txt
. 但是,如果prog2
想用lseek系统调用倒带,例如为了跳到某个字节或倒带到最后以找出我们有多少数据,该怎么办?某些程序不允许从管道读取数据,因为管道无法使用lseek(2)系统调用倒带,或者数据无法使用mmap(2)加载到内存中以加快处理速度。Stephane Chazelas在这个问题中 的出色回答已经涵盖了这一点: “cat file | ./binary”和“./binary < 文件”?我强烈推荐阅读。
幸运的是,
cat < file.txt
它的cat file.txt
行为始终如一,并且cat
不以任何方式反对管道,尽管我们知道它读取完全不同的文件描述符。prog file.txt
这在一般情况下如何应用prog < file.txt
?如果一个程序真的不想对管道做任何事情,缺少位置参数file.txt
就足以出错退出,但应用程序仍然可以在标准输入上使用lseek()
来检查它是否是管道(尽管isatty(3)或检测fstat (2)中的S_ISFIFO 模式更有可能用于检测管道输入),在这种情况下,做类似./binary <(grep pattern file.txt)
或./binary < <(grep pattern file.txt)
可能不起作用的事情。文件类型影响
文件类型可能会影响
prog file
vsprog < file
行为。这在某种程度上意味着作为程序的用户,即使您不知道这样做,您也在选择系统调用。例如,假设我们有一个 Unix 域套接字并且我们运行nc
服务器来监听它,也许我们甚至准备了一些要服务的数据在这种情况下,
/tmp/mysocket.sock
将通过不同的系统调用打开:现在,让我们尝试从不同终端中的该套接字读取数据:
shell 和 cat 都
open(2)
在对需要完全不同的系统调用的东西执行系统调用——socket(2) 和 connect(2) 对。即使这样也行不通:但是,如果我们知道文件类型以及如何调用正确的系统调用,我们可以获得所需的行为:
注释和其他建议阅读:
open(2)手册中的引用指出文件描述符的权限被继承。理论上,有一种方法可以更改文件描述符的读/写权限,但这必须在源代码级别完成。
什么是打开文件描述?. 另见POSIX 定义
Linux如何检查文件描述符的权限?
为什么 的 行为
command 1>file.txt 2>file.txt
不同于command 1>file.txt 2>&1
?