Unix如何工作:您不敢问的一切
文件和进程的故事。 成为更好的软件工程师
Unix很美。 请允许我为您画一些快乐的小树。 我不会解释很多命令,这很无聊,网络上已经有上百万的教程可以做到这一点。
我将让您能够对系统进行推理。
您想做的每件奇特的事都只需一个Google搜索即可。 但是,理解为什么解决方案可以实现您想要的功能并不相同。这就是给您真正的力量,而您不用害怕的力量。[1]
并且,因为它押韵,所以它必须是真实的。
假设您是从头开始,我将只放足够的命令供我们使用。 我们将探索概念,在外壳中实践中查看它们,然后尖叫:'我明白了!'。
在此过程中,我们还将弄清楚shell到底是什么。
但是,我们必须首先进入创造者的思想:探索Unix的哲学。
现在,我们可以假设Linux是Unix。 如果您想知道为什么情况并非如此,则可以跳到底部再返回。 我们将一劳永逸地结束Unix与Linux的混淆。
哲学
让我们从核心开始-Unix背后的哲学。
· 编写可以做一件事并且做得很好的程序。
· 编写程序以协同工作。 (没有额外的输出,不要坚持使用交互式输入。)
· 编写程序来处理文本流,因为这是一个通用接口。
Unix也接受了越差越好的哲学。
这种想法是有力的。 在更高的层次上,我们在函数式编程中看到了很多东西:构建只关注一件事的原子函数,无需额外的输出,然后将它们组合在一起以完成复杂的事情。
组成中的所有功能都是纯函数。 没有全局变量可以跟踪。
也许是直接的结果,Unix的设计侧重于两个主要组件:进程和文件。 Unix中的所有内容都是进程或文件。 没有其他的。
进程和文件之间存在周期性的依存关系-如果我们开始深入解释其中一个,我们将需要另一个来支持它。 为了打破这个循环,我们将先简要概述每个内容,然后再深入研究。我们将重复几次以达到最低要求。
进程
您正在运行的浏览器是一个进程。 如果您将其打开,那么终端也是如此。[2] 如果没有,现在是打开它的好时机。 如果您使用Windows,那么Docker也可以很好地工作。 如果您使用的是Mac,请使用终端-这是Unix环境。
用更抽象的术语来说,流程是运行中的代码实例。 操作系统将资源分配给进程(例如内存),然后将一些元数据附加到进程(例如谁是所有者),然后运行代码。
作为资源的一部分,操作系统还为每个进程提供了三个打开文件:stdin,stdout和stderr。
文件
不是进程的所有内容都是文件。
是的,这意味着您的打印机,扫描仪,终端屏幕以及任何过程的代码! 它们都是文件。 如果这听起来令人困惑,请继续阅读。 我们将清除此问题。
文件系统中的文件就是文件—一串字节串在一起,以创建有意义的东西。 您的照片是文件。 您的目录也是文件! 它们只包含当前目录中存在的文件/目录的列表,就像树一样。
这样做的好处是我可以'打开'目录文件来查看内容!
例如:
$ vim .' ==================================================================== ' Netrw Directory Listing (netrw v162) ' /Users/Neil/examples ' Sorted by size ' Quick Help: <F1>:help -:go up dir D:delete R:rename s:sort-by x:special ' =================================================================== ../ ./ git-example/ unix-git/ unix-file-system-example/
我用Vim打开了一个名为..的文件,听起来很熟悉吗? Unix如何存储当前目录。 如您所见,它包含当前目录中的文件/目录列表。
文件只是数据流。
文件和文件系统
有了一切的想法,文件就是文件,数据就成了数据流,我们可以探索事情如何进一步发展。
在Unix系统上,用于获取输入和写入输出的流是预定义的。 这正是标准输入stdin,标准输出stdout和标准错误stderr的用途。
· stdin是输入数据源。
· stdout是输出数据源。
· stderr是标准错误输出源。
在shell [3]中,stdin是来自键盘的输入数据,而stdout和stderr都是屏幕。[4]
现在,我们可以将这些流重定向到其他地方,而我们的程序不必知道! 无论输入来自何处(键盘或文本文件),对于任何正在运行的进程,输入均来自标准输入。
同样,对于stdout和stderr。 当我们讨论与这些流一起使用的流程时,我们将详细讨论这一点。
索引节点
要安装文件系统,您需要一种结构来管理文件系统。 文件中不仅要处理数据,还涉及有关数据本身的信息,称为元数据。 这包括数据的存储位置,拥有者和查看者。
这就是inode-文件元数据的数据结构。 每个文件都有一个唯一的索引节点号。 当它存在时,它将成为文件的唯一标识符。
$ ls -li
total 0
2015005 drwxr-xr-x 6 neil X 192 23 Oct 07:36 git-example
2514988 drwxr-xr-x 4 neil X 128 9 Oct 11:37 unix-git/
2020303 drwxr-xr-x 4 neil X 128 23 Sep 11:46 unix-file-system-example/
在第一栏中看到那些数字? 这些是inode![5]
索引节点存储所有元数据。 stat对于查看此元数据也很有用。
$ stat -LF .
drwxrwxrwx 7 A B 224 Oct 28 07:15:48 2018 ./
A和B是用户名和组名。 Unix是一个多用户系统。 Unix就是这样-用户和组是文件的属性。
用户属性设置为X的文件表示X拥有该文件。 这就是使用Unix的用户。
224是文件大小,或文件中的字节数。
2018年10月28日7:15:48是最后修改的日期。[6]
我从哪里获得所有这些信息? 男子ls。
现在来看看我遗漏的有趣的数字和字符:drwxrwxrwx和7。
文件权限
每个文件都具有与其关联的权限。
还记得与文件关联的用户和组吗? 每个文件都存储谁拥有该文件以及该文件属于哪个组。 同样,每个用户也都有一个用户名和组。
进入-rwxrwxrwx字符串:这是所有者,组和其他人的权限。
· r用于读。
· w是写。
· x用于执行。 对于目录,这意味着可以搜索。
您只需要三个位来代表每个用户,组和其他用户的权限。
您会注意到该字符串有10个字符。 第一个是特殊的输入类型字符,用于区分目录,符号链接,字符流(stdin)和其他一些字符。 我想知道更多。
$ stat -LF
crw--w---- 1 neil tty 16,1 Dec 12 07:45:00 2019 (stdin)
如果您想更改权限怎么办? 说,我不要任何人搜索我的个人文件夹(哎呀)。
Unix的创建者对此进行了思考。 有一个名为chmod的实用程序,可以修改文件的权限。 在后端,您现在知道chmod正在与文件的inode交互。
由于我们需要三个位来表示每个权限,因此我们可以将其转换为整数并将其传递给chmod。
例如:chmod 721。 表示rwx-w — x表示所有者的所有权限,对该组的写权限以及对其他人的执行权限。
我更喜欢详细形式:
$ chmod u+rwx . # enable user for rwx
$ chmod g+w . # enable group for w
$ chmod o+x . # enable others for x
您在这里做的完全一样。 要为所有人设置权限,chmod a + x 更加简单! 您也可以使用-代替+删除权限。
为了限制对我个人文件夹的访问,我要做:chmod og-x none-interesting-here /。
您还可以限制对自己的访问,从而删除自己的所有读取,写入和执行权限。 如果文件元数据存储在文件本身中,则您将无法再次更改权限(因为您无法写入文件)。
这就是inode很酷的另一个原因:它们总是可以由文件所有者和root修改,因此您可以恢复权限。 尝试这样做。
文件连结
有没有想过为什么将千兆字节文件从一个目录移动到另一个目录会很快,而复制相同文件却要花一些时间呢? 你能猜出现在为什么吗?
这是因为当我们进行MV时,我们将移动目录结构,而不是实际的文件数据。 索引节点是文件系统上非常有用的抽象。
我们可以采取其他行动。 我们可以将文件从一个位置链接到另一位置,或使两个文件名指向同一文件。
指向同一文件的两个文件名是硬链接。 将它们视为文件的别名。 您已经看到了两个硬链接:。 和..是到系统中当前目录和父目录的硬链接。
从一个地方到另一个地方的链接是符号链接。 符号链接是与原始文件分开的新文件,该文件链接到原始文件。
当您要修复需要在新环境中运行的脚本或制作文件副本,以满足希望该文件位于另一个位置的新程序的安装要求时,这些选项很有用。
$ ls -litotal 025280489 -rw-r--r-- 1 neil X 0 8 Dec 08:48 a$ man ln # to check syntax for hard links$ ln a x # create x as a hard link to a$ ls -litotal 025280489 -rw-r--r-- 2 neil X 0 8 Dec 08:48 a25280489 -rw-r--r-- 2 neil X 0 8 Dec 08:48 x# Notice both files have the same inode number.# Modifying x or a is the same thing - both files get modified together.$ ln -s a y # create symbolic link to a$ ls -litotal 025280489 -rw-r--r-- 2 neil X 0 8 Dec 08:48 a25280489 -rw-r--r-- 2 neil X 0 8 Dec 08:48 x25280699 lrwxr-xr-x 1 neil X 1 8 Dec 08:54 y -> a# y is a symbolic link, a new small file - see size is 1. $ cat y # shows that y (a) is empty$ echo lsd >> y$ cat ylsd$ cat a # modifying y modifies alsd$ cat x # a is xlsd
我已经在评论中解释了上面的情况。
现在,如果删除y指向的文件,该怎么办?
$ rm a$ cat ycat: y: No such file or directory# y becomes useless$ ls -li25280489 -rw-r--r-- 1 neil X 12 8 Dec 08:56 x25280699 lrwxr-xr-x 1 neil X 1 8 Dec 08:54 y -> a
这是一个悬挂的符号链接。 没用的。
读写许可权后的数字,即执行stat -LF时的7。 是文件的硬链接数。
当我创建x时,数字增加到两个。 当我删除a时,数字回落到1。
我们也可以确认。 和..确实是硬链接。 你能怎么想?
$ ls -ail25280488 drwxr-xr-x 7 neil X 224 9 Dec 20:19 . 1289985 drwxr-xr-x+ 83 neil X 2656 10 Dec 08:13 ..25390377 drwxr-xr-x 5 neil X 160 9 Dec 19:13 sample_dir$ cd sample_dir$ ls -ail25390377 drwxr-xr-x 5 neil X 160 9 Dec 19:13 .25280488 drwxr-xr-x 7 neil X 224 9 Dec 20:19 ..25390378 -rw-r--r-- 1 neil X 0 9 Dec 19:13 a 】'
检查索引节点号。 ..在sample_dir中是25280488,与..相同 在父目录中。 另外,父目录中的sample_dir是25390377,与相同。 在sample_dir中。
文件结构
它可以帮助我想象文件系统像树形数据结构(实际上就是这样)。 每个节点(索引节点)都有一个指向其父节点,自身及其所有子节点的指针。 这形成目录结构。
/根目录的父目录是什么?
您已有足够的知识来回答这个问题。 我做的第一件事是vim /看看是否有一个父指针。 是的 然后,我做了ls -ail来查看父级的索引节点。 它指向。,即/。
综上所述,
· 文件系统是使用inode和目录文件构建的。
· 用户是文件和进程的属性。 此信息存储在inode中。
· 索引节点在文件系统中是唯一的。
· 可以挂载多个文件系统并将其抽象为一个逻辑树。
进程
首先,让我们摆脱这些定义。 关于进程,要记住三个组件:
· 程序文件:代码和数据。
· 过程映像:这将存储堆栈,当前定义的变量,数据,地址空间等。 在运行时,操作系统完全知道如何使用此映像重新创建过程。
· 进程:内存中正在运行的程序。
进程开始运行时,它将从父进程继承用户ID和组ID。 此信息控制对该进程的访问级别。
注意:访问控制对于安全系统至关重要。 这就是为什么在生产环境中运行裸Docker容器可能会出现这样的问题的原因之一:它需要以root身份运行,这意味着可能会发生不好的事情。
我们可以使用setuid或setgid来使进程继承文件所有者权限。 setuid允许进程继承相关文件的用户ID。
例如,要在Linux上更改密码(请参阅Mac的此链接)-我们需要修改文件/ etc / passwd。 但是,在检查权限时,我们看到只有root有权访问此文件。[7]
$ ls -ail /etc/passwd
3541354 -rw-r--r-- 1 root root 681 Nov 28 08:47 /etc/passwd
因此,当我们调用帮助更改密码的实用程序/ usr / bin / passwd时,它将继承我们的用户ID,该用户ID将导致对/ etc / passwd的访问被拒绝。 这是setuid有用的地方-它允许我们以root身份启动usr / bin / passwd。
$ ls -al /usr/bin/passwd
-rwsr-xr-x 1 root root 27936 Mar 22 2019 /usr/bin/passwd
执行权限中的s而不是x表示此过程将以root用户身份运行。
要设置和删除该位,我们可以再次使用chmod。
$ chmod u-s /usr/bin/passwd
$ ls -al /usr/bin/passwd
-rwxr-xr-x 1 root root 27936 Mar 22 2019 /usr/bin/passwd
我是在Docker上完成的,因此我真正的文件系统是安全的。
属性
就像文件系统上的所有文件都具有唯一的索引节点一样,进程也具有称为进程ID或pid的唯一标识符。
就像所有文件都具有指向其父目录的链接一样,每个进程都具有指向生成它的父进程的链接。
就像文件系统根的存在方式(/)一样,有一个特殊的根父进程称为init。 它通常具有pid 1。
与父目录本身为(/)的文件系统的根目录不同,init的ppid为0,这通常意味着它没有父目录。 pid 0对应于内核调度程序,它不是用户进程。
生命周期
在Unix中,有一个常见的模式说明进程如何工作。
通过克隆现有的父进程(fork()),可以创建一个新的子进程。 这个新的子进程调用(exec())将子进程中运行的父进程替换为该子进程想要运行的进程。
接下来,子进程调用exit()终止自身。 它只会传递退出代码。 0表示成功,其他所有都是错误代码。
父进程需要调用wait()系统调用才能访问此退出代码。 对于产生的每个过程都重复此循环。
这里有些事情可能会出错。
如果父母不致电wait()怎么办? 这会导致僵尸进程,这是资源泄漏,因为操作系统无法在父进程消耗退出代码之前清理进程。
如果父母在子进程之前去世怎么办? 这将导致一个孤立的过程(我保证我不会对此进行弥补)。 初始化进程(特殊的根父进程)采用了孤立进程,然后等待子进程完成。
在计算机世界的自然秩序中,孩子死于父母之前。
父母如何从孩子那里获得更多信息? 它不能通过退出代码,因为这是进程唯一可以返回到父级的东西。
流程与常规函数不同,在常规函数中,您只需将响应返回给调用函数即可。 但是,还有其他方法可以进行进程间通信
我们将通过一个示例来详细介绍事物的工作方式。 在此之前,我们需要更多信息。
文件重定向
还记得操作系统如何为每个正在运行的进程提供三个打开的文件吗? 我们有权将这些文件重定向到我们想要的任何文件。
重定向stdout,2>重定向stderr,和<重定向stdin。
例如,someBinary 2>&1将stderr重定向到stdout。
0、1和2分别是stdin,stdout和stderr文件的简写。
注意:./someBinary 2> 1不会像您期望的那样工作,因为语法是file-descriptor> file。 2> 1表示stderr将被重定向到名为1的文件。&运算符提供文件中的文件描述符。
文件重定向发生在命令运行之前。 操作系统打开新文件(通过>)时,它已经删除了这些文件中的所有内容。
因此,无法对res.txt> res.txt进行排序。
$ cat res.txt # check contents of resdcba$ sort res.txt # sort resabcd$ sort res.txt > res.txt$ cat res# empty
提示:通过在外壳程序中设置noclobber选项,可以确保没有任何重定向破坏现有文件。
$ set -o noclobber
$ sort res.txt > res.txt
-bash: res.txt: cannot overwrite existing file
但是,它将与>>一起使用,因为在这种情况下,您将附加到文件中。
$ sort res.txt >> res.txt
$ cat res.txt
d
c
b
a
a
b
c
d
阅读有关重定向的更多信息。
Unix中的层
我们可以将Unix视为洋葱。 核心是硬件-主板,CPU和许多我不太了解的晶体管。 一层是内核。
内核
内核是负责与文件系统和设备进行交互的核心。 它还处理流程调度,任务执行,内存管理和访问控制。
内核公开了API调用,以构建任何可以利用的东西。 最受欢迎的是exec(),fork()和wait()。
Unix实用程序
另一层是Unix实用程序。 这些是超级有用的进程,可以帮助我们与内核进行交互。 他们通过内核提供的exec()和fork()之类的系统调用来执行此操作。
您可能已经听说过许多实用程序。 您可能使用了最著名的一种:Shell。
其他包括:python,gcc,vi,sh,ls,cp,mv,cat,awk。
您可以从shell调用其中的大多数。 bash,zsh,ksh只是shell的不同变体。 他们做同样的事情。
人们发现令人望而生畏的另一个实用工具是文本编辑器Vim。 Vim值得拥有自己的职位,这就是我在这里创建的
有趣的事实:Shell之所以称为Shell,是因为它是内核之外最近的一层。 它在保护性…外壳中覆盖了内核。
Shell如何工作
还记得shell是一个过程吗? 这意味着操作系统启动时会提供三个文件供其使用:stdin,stdout和stderr。
从终端运行时,stdin连接到键盘输入。 您编写的内容将传递到终端中。 这是通过称为电话打字机或tty的文件发生的。
stdout和stderr也连接到tty,这就是为什么您运行的任何命令的输出和错误都显示在终端上的原因。
您打开的每个终端都会通过tty分配一个新文件,以使来自一个终端的命令不会破坏另一个终端。 您可以通过tty命令找到终端连接到的文件。
$ tty
/dev/ttys001 # on linux, this looks like: /dev/pts/0
现在,您可以做一些时髦的事情:由于Shell从该文件读取,因此您也可以让另一个Shell写入该文件,或者将Shell一起破坏。 我们试试吧。 (还记得如何从上面的过程部分重定向文件吗?)
打开第二个终端。 输入:
$ echo 'Echhi' > /dev/ttys001 # replace /dev/ttys001 with your tty output
注意在第一个终端中发生的事情。
尝试回显ls,这次是列出文件的命令。 为什么第一个终端不运行命令?
它不会运行该命令,因为写入终端的流是第二终端的标准输出,而不是第一终端的stdin流。
请记住,只有通过stdin输入的输入才作为输入传递到shell。 其他所有内容仅显示在屏幕上。 在这种情况下,即使碰巧是同一文件,也无需担心该过程。
上面内容的自然扩展是,当您重定向标准输入时,命令应该运行。 听起来很合理,请尝试一下。
警告:执行此操作的一种方法是bash </ dev / ttys001。 这不太好用,因为现在有两个进程希望从该文件输入信息。
这是一个未定义的状态,但是在我的Mac上,一个字符转到一个终端,另一个字符转到第二个终端,然后继续。 这很有趣,因为要退出新外壳,我必须键入eexxiitt。 然后我丢了两个炮弹。
$ echo ls > ls.txt # write 'ls' to a file$ cat ls.txt # check what's in filels$ bash < ls.txt ApplicationsMusicDocumentsDownloads
有一种更好的方法可以做到这一点,我们将在稍后介绍。
这里有些微妙的事情。 这个新的bash流程(我们从现有的bash流程开始)如何知道在哪里输出内容?
我们从不指定输出流,仅指定输入流。 发生这种情况是因为进程从其父进程继承。
每次在终端上编写命令时,shell都会创建一个重复进程(通过fork())。
从man2叉:
子进程具有其父描述符的副本。 这些描述符引用相同的基础对象,例如,子对象和父对象之间共享文件对象中的文件指针,以便子进程中的描述符上的lseek(2)可以影响后续的读写操作。 父母。
Shell还使用此描述符复制为新创建的进程建立标准输入和输出,以及建立管道。
分叉后,这个新的子进程将从父级继承文件描述符,然后调用exec(execve())执行命令。 这将替换过程映像。
来自man 3 exec:
本手册页中描述的功能是功能execve(2)的前端。
来自man 2 execve [8]:
在调用过程映像中打开的文件描述符在新过程映像中保持打开状态,但已设置close-on-exec标志的文件描述符除外
因此,我们的文件描述符与原始bash进程相同,除非我们通过重定向对其进行更改。
在执行此子进程时,父进程等待子进程完成。 发生这种情况时,控制权将返回给父进程。
请记住,子进程不是bash,而是代替bash的进程。 使用ls,该过程将文件列表输出到stdout后立即返回。
注意:并非Shell上的所有命令都会产生fork和exec。 那些不被称为内置命令的命令。 有些是出于必要内置的。 由于子进程无法将信息传递回父母,因此其他进程可以使事情更快。
例如,设置环境变量在子Shell中不起作用,它无法将值传递回父Shell。 您可以在此处找到列表。
这是我喜欢的示范。
您是否曾经想过,当某事正在运行并将东西输出到终端时,您可以编写下一条命令并在现有过程完成后立即使它们起作用,这有多奇怪?
$ sleep 10;lscat b.txtbrrr# I stop typing here$ lsbcy$ cat b.txtdefbjehb$ brrr-bash: brrr: command not found
只是被阻止的进程,输入流仍在接受数据。 由于我们要读取/写入的文件是相同的(tty),因此我们可以看到键入的内容以及睡眠的时间10; 返回时,shell为ls创建另一个进程,再次等待,然后对cat b.txt相同,然后再次对brrr。
我用了10点睡眠; 进行演示,因为其他命令发生得太快,我无法在控件返回到父bash进程之前键入任何内容。
现在是尝试exec内置命令的好时机(它将替换当前进程,因此它将终止您的shell会话)。
exec echo Bye
echo也是内置命令。
如果您想用C自己实现Shell,我建议您使用以下资源。
管道
掌握了外壳如何工作的知识后,我们便可以冒险进入管道的世界:|。
它把两个过程联系在一起,并且它的工作方式很有趣。
还记得我们开始时的哲学吗? 做一件事,并做好。 现在,我们所有的公用程序都可以正常工作,我们如何使它们一起工作?
这是管道|进入管道的地方。它表示对pipe()的系统调用,它所做的只是重定向进程的stdin和stdout。
由于事情的设计是如此出色,因此原本就很复杂的功能可以简化为这一点。 每当您使用管道或终端上的任何东西时,只要想象一下输入和输出文件的设置方式,就不会有问题。[9]
让我们从一种更好的方法将输入直接定向到bash开始,而不是像以前那样使用临时文件(ls.txt)。
$ echo ls | bash
Applications
Music
Documents
Downloads
该图有点简化了管道的重定向。 您知道外壳现在是如何工作的,因此您知道顶部bash分叉了另一个与tty连接的bash,后者生成了ls的输出。
您还知道顶部bash是从底部bash派生的,这就是为什么它继承了底部bash的文件描述符的原因。 您还知道较低的bash并没有进行新的处理,因为echo是内置命令。
让我们以一个更复杂的示例结束本节:
$ ls -ail | sort -nr -k 6 | head -n 1 | cut -f 9 -d ' '2656
该管道找出当前目录中最大的文件并输出其大小。
这样做可能是一种更优雅的方法,仅需一次Google搜索即可,但这很好地举例说明了。 谁知道这已经内置到ls中了?
注意,stderr是如何始终直接路由到tty的吗? 如果要重定向stderr而不是stdout到管道怎么办? 您可以在管道之前切换流。
$ error-prone-command 2>&1 >/dev/null
关于PATH的一切
局部变量是可以在shell中创建的变量。 它们位于外壳的本地,因此不会传递给子级。 (请记住,每个非内置命令都在没有这些局部变量的新外壳中。)
环境变量(环境变量)类似于全局变量。 他们被传递给孩子。 但是,子进程中对环境变量的更改无法传递给父进程。 请记住,除了退出代码,孩子与父母之间没有任何联系。
试试这个:从bash调用bash。 第一个bash正在等待第二个bash退出,而第二个bash正在等待第三个bash。
当您调用exec时,退出会自动发生。 如果不是,则您要自己键入exit,以将退出代码发送给父级。 退出两次,即可恢复原始状态。
现在,进行思想实验:当将ls输入到shell中时会发生什么? 您知道与tty一起发生的fork(),exec()和wait()循环。
但是,即使在这种情况发生之前,ls只是另一个实用程序功能,对吗? 这意味着某个地方有一个程序文件,其中包含执行fork()和其他所有功能的C代码。
该二进制文件不在当前目录中(您可以使用ls -a检查)。 对我来说,如果这些文件在我的当前目录中,我可以通过在外壳中键入它们的名称来执行它们。 它们是可执行文件。
ls程序文件在哪里?
还记得文件系统树是如何分层的吗? 这是疯狂的命令。
所有基本级别目录都具有特定功能。 例如,所有Unix实用程序和一些其他程序都进入/ bin目录。 Bin代表二进制文件。 如果您想了解更多信息,则有一百万本教程。
这对我们来说已经足够了。 我住在/ bin。 因此,您可以这样做:
$ /bin/ls
a b c
与运行ls相同。
但是,shell如何知道要在bin中查找ls?
这就是神奇的环境变量PATH出现的地方。让我们首先来看它。
$ echo
$PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
并且,要查看所有环境变量,我们可以执行以下操作:
$ env
HOSTNAME=12345XXXX
TERM=xterm
TMPDIR=/tmp
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/test
LANG=en_US.UTF-8
SHLVL=1
HOME=/root
LANGUAGE=en_US:en
LESSOPEN=||/usr/bin/lesspipe.sh %s
container=oci
_=/bin/env
PATH是用冒号分隔的目录列表。 当shell看到没有绝对路径的命令时,它将在$ PATH环境变量中查找,依次进入每个目录,并尝试在其中查找文件。 它执行找到的第一个文件。
注意/ bin在PATH中的方式,这就是ls起作用的原因。
如果我从PATH中删除所有内容,会发生什么? 没有绝对的路径,任何事情都不应该工作。
$ PATH=''
ls-bash: ls: No such file or directory
上面的语法仅用于设置一个命令的环境变量。 旧的PATH值仍然存在于外壳中。
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
注意:由于echo是内置的shell,因此无法执行PATH ='''echo $ PATH。 但是,如果您使用PATH =''启动了一个新的shell进程,然后执行了echo操作,那么它将起作用。
PATH)
()是新子shell的语法。 我知道,我首先不解释很多信息,但这全是语法级别,仅需一次Google搜索。 从正面来看,它可以确保该博客文章不会变成一本书。
您是否听说过./是运行所创建文件的方式? 您为什么不能像bash一样运行它们? 现在你知道了。 当您执行./时,这就是您要执行的文件的确切路径。 bash有效,因为它位于PATH上。
因此,有意义的是,如果当前目录始终位于PATH上,则脚本将按名称运行。
让我们尝试一下。
$ vim script.sh# echo 'I can work from everywhere?'$ chmod a+x script.sh $ lsscript.sh$ script.sh-bash: script.sh: command not found # not on PATH$ ./script.sh # path to file is definedI can work from everywhere?
现在,将其添加到当前PATH中。 然后只运行script.sh。
$ PATH=${PATH}:. script.sh # this appends . to PATH, only for this commandI can work from everywhere?$ export PATH=${PATH}:. # this sets the PATH variable permanently to include .$ script.sh # calling script.sh without the ./I can work from everywhere?$ cd .. # go one directory up$ script.sh # this shows that PATH directories aren't searched recursively-bash: script.sh: command not found # so script doesn't run anymore
警告:不正确的做法是将当前目录包含在PATH中。 有几个问题。 您永远不能确保任何命令的执行都按预期进行。
如果您有一个名为ls的二进制文件,它是当前目录中的一种病毒(可从Internet下载),但您打算执行/ bin / ls怎么办?
阅读更多
下次当您看到'没有这样的文件或目录'错误时,当您知道该文件存在(也许您刚刚安装了该文件)时,便知道问题出在哪里。 路径被破坏!
它安装在PATH以外的位置,因此只能从安装它的位置进行调用。 要解决此问题,您现在知道可以将该目录添加到PATH,也可以通过其绝对路径调用该文件。
有趣的事实:Python在搜索导入时具有类似的结构,该结构使用PYTHONPATH env变量。
编写Shell脚本
这篇文章的长度已经超出了我的预期。 此外,使用Shell进行编程已经在网上广泛讨论了。 但是为了完整起见,这里是手册的链接和不错的教程。
包管理
假设您编写了一个新工具。 它在您的计算机上确实运行良好,现在您想将其出售给其他用户。 等等,我的意思是,本着开源的精神,您想将其提供给他人使用。
您还希望节省他们的PATH麻烦。 更妙的是,您希望将文件安装在正确的位置:二进制文件进入/ usr / bin /(它已经在PATH中),而依赖项则位于主二进制文件可以找到它的地方。
包管理器正好解决了这个问题。 他们没有让您头疼,而是使事情正常进行。
我知道三个主要的软件包管理器:dpkg,rpm和自制软件。 它们每个都在不同的Linux发行版上工作(如果您不确定这是什么意思,请参见下一节)。
但是,就像分发的数量一样,它们中有数百种在野外。
dpkg是Debian软件包管理器,但是您可能已经听说过在它之上构建的用于管理软件包的非常有用的工具:apt。
每次使用apt install来安装新软件包时,您都在利用此软件包管理器的功能,以确保一切都在需要的地方进行。
在开发方面,这意味着确保要创建的工具与程序包管理器兼容。 例如,以下是在C和Python中执行此操作的方法。
rpm是Red Hat软件包管理器,它在顶部还有一个有用的工具:yum,它也处理依赖项。
homebrew是macOS上的软件包管理器,每次酝酿安装某些软件时都在使用它。
它们使生活变得轻松。
它们是如此方便,以至于编程语言也有自己的软件包管理器! 例如,pip是流行的Python工具安装程序。 有用于Ruby的捆绑器,用于Swift / iOS的可可和其他几种捆绑器。
Unix的简要历史
Unix是第一个允许多个用户使用它的操作系统,每个用户可以同时运行多个程序。
现在这听起来似乎很琐碎,因为几乎每个操作系统都具有此功能,但是它首次问世时是革命性的。 在大型主机上租赁时间的日子已经过去。 您可以让程序在后台运行,而其他人来做。
顾名思义,Unix是一个多用户多任务操作系统。
这是专有的,AT&T是唯一可以出售它的公司。 (贝尔实验室在1970年代开发了它)。 他们选择了许可模型,并很快提出了一个规范,称为'单一UNIX规范'。
这些是一组准则,遵循它们的任何系统都可以被认证为Unix系统。
大约在同一时间,一些人对Unix的专有性不满意,并提出了另一个名为Linux的开源操作系统内核。
这些系统受到Unix哲学的启发并实现了可移植性,因此遵循POSIX标准,该标准是Unix的一个子集。 这些系统因此也被称为类Unix。[10]
事情变得有些混乱。 Linux是基于Linux内核的一系列操作系统。 没有称为Linux的单一操作系统。
相反,我们所拥有的是Debian,Ubuntu,Fedora,CentOS,Red Hat,Gentoo等。这些是Linux内核的发行版(通常称为发行版)。 完善的操作系统。
有什么不同? 有些是为特定目的而构建的(例如:Kali Linux附带了安全测试工具)。
包管理,包更新频率和安全性方面的大多数差异。
如果您想了解更多,请访问opensource.com。
有趣的事实:Mac OS X经过Unix认证。
结论
我们已经介绍了很多。 让我们花一点时间将它们放在一起。
Unix是成熟的操作系统,Linux是受Unix启发的内核(操作系统的核心)。 他们专注于做一件事情,并做好。
一切都是过程或文件。 内核是核心,它公开了系统调用,这些实用程序可以利用它们。 进程使用文件作为输入和输出。 我们可以控制这些文件,可以重定向它们,这对过程没有影响。
管道可以将输出从一个过程重定向到另一个过程的输入中。 Shell中的每个命令首先派生,然后执行,然后将退出代码返回给等待的父对象。
还有很多。 如果有可能进行无损压缩,那么我会在后期进行。
因此,欢迎来到野外,您很好。
出口
感谢Vatika Harlalka,Nishit Asnani,Hemanth K. Veeranki和Hung Hoang阅读了此草稿。
脚注
· 这只是一首诗吗?
· 终端也像外壳一样运行着子进程。 您可以通过ps -ef查看所有正在运行的进程。
· Shell是用于与操作系统交互的接口。 它既可以是命令行界面(CLI),也可以是图形用户界面(GUI)。 在本文中,我们仅关注CLI。 打开终端时,欢迎您的默认程序是外壳程序。 阅读更多
· 这不是100%正确的,还有更多细微差别,我们将尽快解决。
· 我花了太多时间在iPhone和iOS上。 它是一个索引节点,而不是一个iNode。
· 另外,这是我开始编写本指南的时间。 大概我完成了。 注意年份。
· 我从man 5 passwd获得了所有这些信息。
· 本手册分为八个部分,每个部分用于特定目的。 阅读更多。
· 这是一个强大的想法,只有在Unix设计上说:'一切都是文件',这才是正确的。
· 有趣的事实-由于它们未经认证,因此不能称为Unix,并且Unix是商标。
(本文翻译自Neil Kakkar的文章《How Unix Works: Everything You Were Too Afraid to Ask》,参考:
https://medium.com/better-programming/how-unix-works-everything-you-were-too-afraid-to-ask-f8396aeb2763)