显示文件内容的最简单方法是使用以下cat
命令:
cat file.txt
我可以使用输入重定向获得相同的结果:
cat < file.txt
那么,它们之间到底有什么区别呢?
答案1
cat file
该cat
程序将打开、读取和关闭该文件。
cat < file
您的 shell 将打开该文件并将内容连接到cat
stdin。cat
识别到它没有文件参数,并将从 stdin 读取。
答案2
从用户的角度来看,没有什么区别。这些命令的作用是相同的。
从技术上讲,区别在于打开文件的程序:cat
是程序还是运行它的 shell。重定向由 shell 在运行命令之前设置。
(因此在某些其他命令——也就是说,不是问题中显示的命令——可能会有差异。特别是,如果您无法访问file.txt
但 root 用户可以访问,则sudo cat file.txt
有效但sudo cat < file.txt
无效。)
您可以使用任何一种方便您情况的方法。
几乎总是有多种方法可以得到相同的结果。
cat
接受来自参数的文件,或者stdin
没有参数。
看man cat
:
SYNOPSIS
cat [OPTION]... [FILE]...
DESCRIPTION
Concatenate FILE(s) to standard output.
With no FILE, or when FILE is -, read standard input.
答案3
一个巨大的区别
一个很大的区别是*
、?
、 或[
通配符(通配符)或 shell 可能扩展为多个文件名的其他任何字符。shell 扩展为两个或多个项目(而不是将其视为单个文件名)的任何内容都无法打开进行重定向。
如果没有重定向(即没有<
),shell 会将多个文件名传递给cat
,然后逐个输出文件的内容。例如,下面的代码可以工作:
$ ls hello?.py
hello1.py hello2.py
$ cat hello?.py
# Output for two files 'hello1.py' and 'hello2.py' appear on your screen
但是使用重定向(<
)会出现错误消息:
$ ls < hello?.py
bash: hello?.py: ambiguous redirect
$ cat < hello?.py
bash: hello?.py: ambiguous redirect
一个微小的差异
我以为重定向后速度会变慢,但没有明显的时间差异:
$ 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
笔记:
- 本次测试中,差异约为 1/1000 秒。在其他测试中,差异为 1/100 秒,这仍然无法察觉。
- 交替进行测试几次,以便尽可能多地将数据缓存到 RAM 中,并返回更一致的比较时间。另一种选择是在每次测试之前删除所有缓存。
答案4
TL;DR 版本的答案:
应用
cat file.txt
程序(在本例中cat
)收到一个位置参数,对其执行 open(2) 系统调用,并在应用程序内进行权限检查。shell
cat < file.txt
将执行dup2()
系统调用,将 stdin 转换为与文件描述符(通常是下一个可用文件描述符,例如 3)对应的副本,file.txt
然后关闭该文件描述符(例如 3)。应用程序不对文件执行 open(2),并且不知道文件的存在;它严格按照其 stdin 文件描述符进行操作。权限检查由 shell 负责。打开的文件描述将与 shell 打开文件时相同。
介绍
表面上看cat file.txt
和cat < file.txt
行为相同,但这个字符的差异背后还有很多事情。这个<
字符改变了 shell 如何理解file.txt
、谁打开文件以及文件如何在 shell 和命令之间传递。当然,为了解释所有这些细节,我们还需要了解在 shell 中如何打开文件和运行命令,这就是我的答案旨在实现的目标——用最简单的术语让读者了解这些看似简单的命令中到底发生了什么。在这个答案中,你会发现多个例子,包括那些使用斯特拉斯命令来支持幕后实际发生的事情的解释。
由于 shell 和命令的内部工作原理基于标准系统调用,因此将其视为cat
众多命令之一非常重要。如果您是初学者,正在阅读此答案,请保持开放的心态,并注意 并不总是prog file.txt
与 相同prog < file.txt
。当将两种形式应用于不同的命令时,其行为可能会完全不同,这取决于权限或程序的编写方式。我还请您暂停判断,从不同用户的角度来看待这个问题——对于普通 shell 用户来说,需求可能与系统管理员和开发人员完全不同。
execve() 系统调用和可执行文件看到的位置参数
Shell 通过创建子进程来运行命令叉(2)系统调用和调用执行(2)syscall,使用指定的参数和环境变量执行命令。内部调用的命令execve()
将接管并替换该进程;例如,当 shell 调用时,cat
它将首先创建一个 PID 为 12345 的子进程,之后execve()
PID 12345 变为cat
。
cat file.txt
这让我们看到了和之间的区别cat < file.txt
。在第一种情况下,cat file.txt
是一个使用一个位置参数调用的命令,shell 将execve()
适当地组合在一起:
$ strace -e execve cat testfile.txt
execve("/bin/cat", ["cat", "testfile.txt"], 0x7ffcc6ee95f8 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
在第二种情况下,<
部分是 shell 操作符,< testfile.txt
它告诉 shell 打开testfile.txt
并将 stdin 文件描述符 0 转换为对应于 的文件描述符的副本testfile.txt
。这意味着< testfile.txt
不会作为位置参数传递给命令本身:
$ strace -e execve cat < testfile.txt
execve("/bin/cat", ["cat"], 0x7ffc6adb5490 /* 50 vars */) = 0
hello, I am testfile.txt
+++ exited with 0 +++
$
如果程序需要位置参数才能正常运行,那么这一点就很重要。在这种情况下,cat
如果没有提供与文件对应的位置参数,则默认接受来自 stdin 的输入。这也引出了下一个主题:stdin 和文件描述符。
STDIN 和文件描述符
谁打开文件 -cat
或者 shell?他们如何打开它?他们有权限打开它吗?这些都是可以问的问题,但首先我们需要了解打开文件的工作原理。
当进程对文件执行open()
或openat()
时,这些函数会为进程提供一个与打开的文件相对应的整数,然后程序可以通过引用该整数来调用read()
、调用和无数其他系统调用。当然,系统(又称内核)将在内存中保存特定文件的打开方式、权限类型、模式类型(只读seek()
、write()
只写、读/写)以及我们当前在文件中的位置(字节 0 或字节 1024),这称为偏移量。这称为打开文件描述。
从最基本的层面cat testfile.txt
上讲,打开文件的位置cat
,它将被下一个可用的文件描述符引用,即 3(注意 中的 3阅读(2))。
$ strace -e read -f cat testfile.txt > /dev/null
...
read(3, "hello, I am testfile.txt and thi"..., 131072) = 79
read(3, "", 131072) = 0
+++ exited with 0 +++
相比之下,cat < testfile.txt
将使用文件描述符 0 (又名 stdin):
$ strace -e read -f cat < testfile.txt > /dev/null
...
read(0, "hello, I am testfile.txt and thi"..., 131072) = 79
read(0, "", 131072) = 0
+++ exited with 0 +++
还记得我们之前学到的 shellfork()
首先通过exec()
进程类型来运行命令吗?好吧,结果如何文件打开后将移交给使用fork()/exec()
模式创建的子进程。引用打开(2) 手册:
当文件描述符被复制时(使用 dup(2) 或类似方法),重复项指的是相同的打开文件描述作为原始文件描述符,因此两个文件描述符共享文件偏移量和文件状态标志。 这种共享也可以发生在进程之间:通过 fork(2) 创建的子进程继承了其父进程的文件描述符的副本,并且这些副本引用相同的打开文件描述
cat file.txt
这对vs来说意味着什么cat < file.txt
?实际上有很多。在打开文件cat file.txt
时cat
,这意味着它可以控制文件的打开方式。在第二种情况下,shell 将打开,file.txt
并且打开方式对于子进程、复合命令和管道将保持不变。我们当前在文件中的位置也将保持不变。
我们以此文件为例:
$ cat testfile.txt
hello, I am testfile.txt and this is first line
line two
line three
last line
请看下面的例子。为什么line
第一行的单词没有变化?
$ { head -n1; sed 's/line/potato/'; } < testfile.txt 2>/dev/null
hello, I am testfile.txt and this is first line
potato two
potato three
last potato
答案就在打开(2)上面的手册:shell 打开的文件被复制到复合命令的标准输入上,并且运行的每个命令/进程都共享打开文件描述的偏移量。head
只需将文件向前倒退一行,然后sed
处理其余部分。更具体地说,我们会看到 2 个 // 系统调用序列dup2()
,fork()
并且execve()
在每种情况下,我们都获得了引用打开的相同文件描述的文件描述符的副本testfile.txt
。困惑吗?让我们举一个更疯狂的例子:
$ { head -n1; dd of=/dev/null bs=1 count=5; cat; } < testfile.txt 2>/dev/null
hello, I am testfile.txt and this is first line
two
line three
last line
这里我们打印了第一行,然后将打开文件描述向前倒回 5 个字节(这样就消除了单词line
),然后只打印其余部分。我们是如何做到的?打开文件描述testfile.txt
保持不变,文件上的偏移量是共享的。
现在,除了编写像上面这样的疯狂复合命令之外,为什么理解这一点很有用?作为开发人员,您可能希望利用或警惕这种行为。假设cat
您编写了一个 C 程序,该程序需要将配置作为文件传递或从 stdin 传递,然后您像 一样运行它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
,但不能以用户身份读取第二个示例中的文件?这又回到了打开(2)前面提到过手册页。使用< file.txt
shell 打开文件,因此open
权限检查发生在/时openat()
由 shell 执行。此时的 shell 以文件所有者的权限运行,而文件所有者对文件确实具有读取权限。由于打开文件描述在dup2
调用之间继承,shell 将打开文件描述符的副本传递给 sudo
,后者将文件描述符的副本传递给cat
,并且cat
对其他任何事情都一无所知,因此可以愉快地读取文件的内容。在最后一个命令中,cat
土豆用户下的命令对文件执行open()
,当然该用户无权读取文件。
更实际、更常见的是,这就是为什么用户对为什么这样的事情不起作用感到困惑(运行特权命令来写入他们无法打开的文件):
$ sudo echo 100 > /sys/class/drm/*/intel_backlight/brightness
bash: /sys/class/drm/card0-eDP-1/intel_backlight/brightness: Permission denied
但是这样的事情是可行的(使用特权命令写入 dos 需要权限的文件):
$ echo 100 |sudo tee /sys/class/drm/*/intel_backlight/brightness
[sudo] password for administrator:
100
与我之前展示的情况相反的一个理论示例(privileged_prog < file.txt
失败但privileged_prog file.txt
有效)是 SUID 程序。SUID 程序例如passwd
,允许以可执行文件所有者的权限执行操作。这就是为什么passwd
命令允许您更改密码,然后将该更改写入/etc/shadow即使该文件的所有者是 root 用户。
为了举例和娱乐,我实际上cat
用 C 语言编写了快速演示应用程序(源代码此处)并设置了 SUID 位,但如果您明白了这一点 - 请随意跳到本答案的下一部分并忽略此部分。附注:操作系统会忽略带有 的解释可执行文件的 SUID 位#!
,因此此相同操作的 Python 版本将失败。
让我们检查一下该程序的权限和testfile.txt
:
$ ls -l ./privileged
-rwsr-xr-x 1 administrator administrator 8672 Nov 29 16:39 ./privileged
$ ls -l testfile.txt
-rw-r----- 1 administrator administrator 79 Nov 29 12:34 testfile.txt
看起来不错,只有文件所有者和属于administrator
该组的人才能读取此文件。现在让我们以 potato 用户身份登录并尝试读取该文件:
$ su potato
Password:
potato@my-PC:/home/administrator$ cat ./testfile.txt
cat: ./testfile.txt: Permission denied
potato@my-PC:/home/administrator$ cat < ./testfile.txt
bash: ./testfile.txt: Permission denied
看起来不错,shell 和cat
具有 potato 用户权限的用户都无法读取他们无权读取的文件。还请注意谁报告了错误 - cat
vs bash
。让我们测试一下我们的 SUID 程序:
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
按预期工作!再次强调,这个小演示的要点是,打开文件的人不同,打开文件的权限也不同prog file.txt
。prog < file.txt
程序如何响应 STDIN
我们已经知道< testfile.txt
重写 stdin 的方式是数据将来自指定的文件而不是键盘。理论上,基于 Unix 哲学“做一件事并做好”,从 stdin(又名文件描述符 0)读取的程序应该表现一致,因此应该prog1 | prog2
类似于prog2 file.txt
。但是,如果prog2
想用 倒带,该怎么办?查找系统调用,例如为了跳转到某个字节或倒回到最后来查看有多少数据?
某些程序不允许从管道读取数据,因为管道不能用lseek(2)系统调用或数据无法加载到内存中mmap(2)以便更快地处理。史蒂芬·查泽拉斯在这个问题中: “cat file | ./binary” 和 “./binary < file” 有什么区别?我强烈建议阅读该书。
幸运的是,cat < file.txt
andcat file.txt
的行为一致,并且cat
不会以任何方式反对管道,尽管我们知道它读取完全不同的文件描述符。这在prog file.txt
vsprog < file.txt
中如何应用?如果程序真的不想对管道做任何事情,缺少位置参数file.txt
就足以退出并出现错误,但应用程序仍然可以使用lseek()
stdin 来检查它是否是管道(尽管伊萨蒂(3)或检测 S_ISFIFO 模式fstat (2)更有可能用于检测管道输入),在这种情况下,执行类似./binary <(grep pattern file.txt)
或的操作./binary < <(grep pattern file.txt)
可能不起作用。
文件类型的影响
文件类型可能会影响prog file
vs 的prog < file
行为。这在某种程度上意味着,作为程序的用户,即使您没有意识到,您也在选择系统调用。例如,假设我们有一个 Unix 域套接字,并且我们运行nc
服务器来监听它,也许我们甚至准备了一些要提供的数据
$ nc -U -l /tmp/mysocket.sock < testfile.txt
在这种情况下,/tmp/mysocket.sock
将通过不同的系统调用打开:
socket(AF_UNIX, SOCK_STREAM, 0) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_UNIX, sun_path="/tmp/mysocket.sock"}, 20) = 0
现在,让我们尝试在不同的终端从该套接字读取数据:
$ 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
shell 和 cat 都在执行open(2)
系统调用,而这需要完全不同的系统调用 - socket(2) 和 connect(2) 对。甚至这个也不起作用:
$ nc -U < /tmp/mysocket.sock
bash: /tmp/mysocket.sock: No such device or address
但是如果我们了解文件类型以及如何调用正确的系统调用,我们就可以得到所需的行为:
$ nc -U /tmp/mysocket.sock
hello, I am testfile.txt and this is first line
line two
line three
last line
注释和其他建议阅读:
引自打开(2)手册指出文件描述符上的权限是可以继承的。理论上,有一种方法可以更改文件描述符的读/写权限但这必须在源代码级别上完成。
什么是打开文件描述?。 也可以看看POSIX 定义
为什么 的行为
command 1>file.txt 2>file.txt
与 不同command 1>file.txt 2>&1
?