目录结构

各种 Linux 系统有着大致统一的标准目录结构:(FHS)

  • / 根目录
  • 可执行文件目录
    • /bin/sbin 存放系统层面所必需的可执行文件,例如cpls等,其中/sbin存放的是需要root权限的部分
    • /usr/bin/usr/sbin 存放用户层面的可执行文件,例如gitwget
  • 库目录
    • /lib 存放系统层面所必需的的动态库,理论上是为/bin/sbin准备的,此外/lib/modules还存放内核模块
    • /usr/lib 存放用户层面的动态库,理论上是为/usr/bin/usr/sbin准备的
    • 除了lib,可能还有lib32lib64,作用类似
  • /usr/local (重要)存放管理员安装的程序,与使用apt等命令直接安装的不同,这里通常存放的是手动编译安装的软件,这个目录很重要,下面单独讨论
  • 用户家目录(重要)
    • /root root 用户的家目录
    • /home 一般用户的家目录的父目录
      • /home/abc 名为 abc 的用户的家目录
  • /opt (重要)一般用于安装第三方提供的,可选的大型应用程序
  • /etc (重要)存放系统的配置文件和脚本,例如/etc/opt对应/opt中的应用程序的配置
  • /var 存放系统层面不断变化的文件
    • /var/log 存放系统日志文件
    • /var/cache 存放应用程序的缓存数据,应用程序需要保证在缓存丢失时不会损失数据
    • /var/tmp 存放临时文件,存续时间比/tmp更久,但是也可能被管理员定时清理
  • /tmp 存放临时文件,在系统重启时会被清除,并且某些情况下可能被限制大小
  • /mut 临时手动挂载外部文件系统
  • /media 自动挂载可移动媒介

下面是一些对系统重要的特殊位置,不能修改否则系统容易崩溃

  • /boot 系统启动引导
  • /dev 对应设备文件,其中包括经典的/dev/null,可以用于丢弃输出内容
  • /proc 提供系统进程和内核状态信息
  • /sys 提供系统设备、驱动和一些内核特性的信息。
  • /run 提供系统运行时数据,例如进程ID等(通常在系统启动时创建,在重启后清除)

虽然Linux系统有一套关于文件系统的约定,但是因为历史原因,实际不同发行版中的目录结构仍然比较混乱,上面列举的很多目录功能都有重叠,定位并不清晰。 不过对于这些定位重复的目录,众多发行版的普遍做法是使用链接进行简化,例如(不同发行版的具体细节可能不同)

1
2
3
4
bin -> usr/bin
lib -> usr/lib
lib64 -> usr/lib64
sbin -> usr/sbin

文件系统会大致生成一个树结构,抽象文件系统与物理磁盘空间存在对应关系,与磁盘的挂载位置有关,lsblk命令可以查询磁盘挂载情况

  • 根目录总是要挂载一个物理磁盘
  • 可以给某个目录挂载一个磁盘,例如/a,此时/a的下级的文件树都会对应到这个挂载磁盘上
  • 任一目录实际的物理位置,是沿着树结构不断往上寻找,最近的挂载磁盘,如果一直没有就找到根目录的磁盘

文件列表和权限

在任何目录下使用ll -a会列表展示目录下的所有信息,每一行开头的缩写码是文件或目录的权限,例如

1
2
drwxrwxr-x 2 abc abc 4096 Sep 30 00:25 test
-rw-r--r-- 1 abc abc 4096 Sep 30 02:25 hello.py
  • 第一部分的字符串代表文件类型和权限
    • 第一位代表文件类型
      • d 目录
      • - 普通文件
      • l 链接(快捷方式)
      • c 字符设备比如鼠标键盘
      • b 块设备比如磁盘
    • 后九位分成三组表明三类用户所拥有的rwx权限(对应位置为横线,则表示没有这个权限)
      • 123 位 文件拥有者(一般是创建者)拥有的权限
      • 456 位 文件所在组的用户拥有的权限
      • 789 位 其他用户拥有的权限
  • 第二部分对于目录而言,就是目录下的文件数,对于普通文件而言,则是链接数
  • 第三部分表示文件的拥有者
  • 第四部分表示文件所在组
  • 第五部分表示文档的大小,单位字节,可以改成ll -h,这样显示的单位会更加直观,但是这里的大小对于目录来说没有什么意义,因为这个指的是目录自身的大小,而不是目录中所有子文件的大小之和
  • 第六部分表示文档最后修改时间,注意不是文档的创建时间
  • 第七部分表示文档名称,以点.开头的是特殊目录(.代表当前目录,..代表上一级目录)和隐藏文件

rwx 权限对文件的具体含义为:

  • r 读权限:可以读取文件内容
  • w 写权限:可以修改文件内容,但是不能删除该文件(删除需要对该文件所在目录拥有写权限)
  • x 执行权限:可以当作程序代码执行

rwx 权限对目录的具体含义可能比较难以理解,将目录理解为一个清单文件更好理解:

  • r 读权限:可以查看目录内容,例如ls打印目录
  • w 写权限:可以在目录下创建+删除+移动+重命名文件(因为这些操作都会影响目录清单,修改文件内容并不会影响目录清单)
  • x 执行权限:可以cd进入这个目录

对于目录的权限并不会被子目录继承,可以给目录和子目录赋予不同的权限,例如/var目录对普通用户的权限是r-x,但是其子目录/var/tmp对普通用户的权限的rwt

我们可以通过ls给出的颜色去简单区分类型和权限,默认颜色通常为:(默认开启颜色显示选项)

  • 白色代表普通文件
  • 蓝色代表目录
  • 绿色代表可执行文件/脚本(赋予当前用户执行权限时,颜色就会从白色变成绿色)
  • ...

除了rwx这三种最常见的权限,还有一些不常见的权限:

  • s 强制位权限
  • t 粘滞位权限
  • i 不可修改权限
  • a 只追加权限

这里只介绍对于目录的粘滞位权限t,例如/tmp/var/tmp目录对于普通用户的权限通常设置为rwtt的具体含义为:

  • 只有目录的所有者或root才能无约束地删除或重命名目录中的文件。
  • 其他用户虽然可以在该目录中创建和查看文件,但无法删除或修改非自己拥有的文件。

因此在实践中,粘滞位权限通常用于共享目录。

家目录结构

常见配置文件

在家目录下常见的用户级配置文件如下,在ls命令下会隐藏这些.开头的文件,可以通过ll -a查看,例如

  • 与各种 shell 相关
    • .bash_profile 重要的一个配置文件,它在用户每次登录系统时被读取,里面的所有 命令都会被 bash 执行。
    • .bashrc 重要的一个配置文件,会在 bash shell 调用另一个 bash shell 时读取,也就是在 shell 中再键入 bash 命令启动一个新 shell 时就会去读该文件。这样可有效分离登录和子 shell 所需的环境。一般来说都会在.bash_profile里调用.bashrc脚本以便统一配置用户环境。
    • .bash_logout
    • .bash_history.sh_history bash 的命令历史记录
    • .kshrc.mkshrc.zshrc 别的 shell 的配置文件
  • 与 vim 相关的
    • .vimrc vim 的用户级配置文件
    • .viminfo vim 的命令历史记录
  • 与 Xshell 相关的
    • .Xauthority

/etc目录的也有.bashrcprofile等配置文件,这是系统级(全局)的配置文件,优先级比用户级配置更低,当在用户主目录下找不到.bash_profile.bashrc时,就会读取这两个文件。(其它应用的配置文件也类似,比如 vim 和 git 也存在系统级和用户级的配置文件,都存放在相应的位置)

XDG 规范

历史原因导致了家目录下的各种配置文件和文件夹非常混乱,FreeDesktop.org 推出了XDG 基础目录规范(XDG Base Directory Specification), 用于在 Linux 系统中标准化用户数据、配置文件、缓存和其他运行时文件的存放位置。

XDG 基础目录规范定义了一些环境变量,用于指示软件应该将这些文件存储在何处,而不是将它们混乱存放在用户的家目录中,主要包括:

  • $XDG_DATA_HOME:存储数据文件。如果未设置或为空,默认为 $HOME/.local/share
  • $XDG_CONFIG_HOME:存储配置文件。如果未设置或为空,默认为 $HOME/.config
  • $XDG_STATE_HOME:存储状态文件,这些文件是持久性的,在应用程序重启时仍有用,但这些数据相比于$XDG_DATA_HOME其实并不重要,或者不具备可移植性。未设置或为空时默认为 $HOME/.local/state
  • $XDG_CACHE_HOME:存储非重要缓存文件。如果未设置或为空,默认为 $HOME/.cache

目前只有部分软件严格遵循 XDG 规范(例如nvim),还有部分软件在保持自身历史习惯的同时也兼容 XDG 规范(例如git),但是仍然有部分软件没有遵循 XDG 规范(例如vim)。

如果我们严格采用XDG规范,那么家目录中应当具有下面这些目录:

  • ~/.config(对应$XDG_CONFIG_HOME的默认值):存放配置文件,例如fish会将配置文件存放在~/.config/fish目录下
  • ~/.local:用户的程序目录,大致替代/usr/local,因为/usr/local需要管理员权限,普通用户无权限时可以安装到此处
    • ~/.local/bin:存放用户的可执行文件(这是约定俗成的可执行文件路径,有的发行版会自动将这个路径添加到$PATH环境变量)
    • ~/.local/lib~/.local/include:存放用户的库文件以及对应的头文件
    • ~/.local/share(对应XDG_DATA_HOME的默认值):存放程序的数据文件,例如桌面文件、图标、插件、字体、游戏存档等
    • ~/.local/state(对应XDG_STATE_HOME的默认值):存放程序的状态文件,在程序重启时仍然可用,重要性介于数据文件和缓存文件之间
  • ~/.cache(对应XDG_CACHE_HOME的默认值):存储程序的缓存文件

例如

1
2
3
4
5
6
7
8
9
10
~
├── .config/
├── .local/
│ ├── bin/
│ ├── include/
│ ├── lib/
│ ├── share/
│ ├──
│ └── state/
└── .cache/

XDG规范的目录都是一些隐藏目录,实践中显然还需要一些非隐藏的目录,例如在家目录下可以采用下面的目录结构进行补充:

  • ~/opt:作为/opt的替代,存放安装的大型软件
  • ~/software:存放源码编译软件的压缩包和编译目录,以及预编译软件的压缩包
  • ~/tmp:作为/tmp的替代,存放一些临时文件,例如通过scp传输文件的目的地

部分用户会使用~/local来替代/usr/local,但是显然~/.local是更推荐的做法。

环境变量

环境变量就是系统层面需要给应用层面提供的一个"词典",每一项由变量名和变量值(字符串)组成,通常包括应用所需要的路径,系统信息等。 不同的 shell,不同的进程,不同的状态下的环境变量可能是不一样的。

通常由一些配置脚本生成相应的环境变量,这些脚本在 shell 启动时会自动执行一遍;也可以手动修改环境变量,不过这种修改通常是临时性的,退出 shell 后就会失效,想要永久性地设置环境变量,需要修改相应的配置脚本。

env命令可以查看当前状态下的所有环境变量,例如

1
2
3
4
5
6
7
...
HOME=/home/username
PERL_LOCAL_LIB_ROOT=:/home/username/perl5
LOGNAME=username
QTLIB=/usr/lib64/qt-3.3/lib
CVS_RSH=ssh
...

可以输出指定的环境变量,其实环境变量也是 shell 脚本中一种特殊的变量,因此使用美元符号作为前缀访问,注意 shell 变量是大小写敏感的,并且通常环境变量采用全大写的形式

1
2
3
4
5
echo $HOME # 这个环境变量让应用可以找到家目录
/home/username

echo $PATH
/opt/cmake-3.14.1-Linux-x86_64/bin:/opt/intel//impi/5.0.2.044/intel64/bin...

其中的 PATH 是最重要的环境变量之一,包括一些存放可执行文件,链接库等等的目录(路径之间通过冒号:分隔),许多应用都需要在 PATH 里面添加指定的路径才能正常执行,我们需要修改这个环境变量,或者创建一些新的环境变量。常见的做法是,在 PATH 中添加新的路径,如下在 PATH 的最后加了一个路径,也可以加在最前面,但是注意路径的顺序就是检索的顺序,因此不同的顺序会导致不同的优先级,可能产生 bug。

1
PATH=$PATH:$HOME/bin
  • 如果希望临时性的修改环境变量,那么可以直接在 shell 中输入语句,例如PATH=$PATH:$HOME/bin,但是如果重新开一个 shell,比如执行一个 shell 脚本,或者退出重新登录,环境变量就会恢复原装
  • 如果希望永久性修改环境变量,通常习惯在用户家目录下的.bashrc配置文件中使用语句进行添加,例如
1
2
3
export PATH=$PATH:$HOME/bin # 可以使用export命令

declare -x HISTSIZE="1000" # 也可以使用declare -x命令,设置环境变量,具有同样的效果

注意:

  • 这里如果路径在引号中必须使用$HOME而不是~代表家目录,否则有些命令会无法识别这里的路径;不含引号时可以直接用~。总之使用echo $PATH检查时不能出现~,必须是解析出来的绝对路径。
  • 除了~/.bashrc,还有其他文件中也存在着部分配置,例如~/.profile~/.bash_profile等,执行哪些配置文件与启动方式有关,还和系统配置有关。在Ubuntu中的.profile会尝试主动加载.bashrc,还会修改环境变量!但是CentOS中对应角色的配置文件是.bash_profile

bash 脚本

在 bash 中执行一个 bash 脚本时,相当于开了一个子进程,脱离了当前的环境,因此

  • bash 脚本中的修改通常只能继承父进程的部分内容,比如父进程中定义的变量,在子进程中仍然未定义。
  • 子进程的修改通常不会反过来影响父进程,比如在脚本中定义了变量,退出脚本后这种定义通常是无效的。

这种设置是为了安全考虑,隔离了父进程和子进程之间的相互影响,但是有时候我们需要继承或者反馈信息:

  • 对于第一种继承问题,我们需要指定这个变量是具有导出属性的,使用 export 命令,此时变量会被子进程继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义变量的同时增加导出属性
export a b=3
# 当然也可以先定义后增加导出属性
b=3
export b

# 删除变量的导出属性
export -n a b

# 为已定义函数增加导出属性
export -f func_1 func_2

# 删除函数的导出属性
export -fn a b
  • 对于第二种反馈问题,如果我们希望 bash 脚本 a.sh 在调用另一个 bash 脚本 b.sh 时,影响可以反馈给自己,可以在 a.sh 中使用 source 语句
1
2
source b.sh
. b.sh # 等价写法 小数点后有空格

bash 脚本的第一行通常用于指定脚本解释器,这一行内容以 Shebang 字符 #! 开头,随后紧跟着脚本解释器的路径,通常这一行也称为 Shebang 行。 Shebang 行不是必须的,但是建议加上。

常见的 Shebang 行例如

1
2
#!/usr/bin/env bash
...

此外还有

1
2
#!/bin/bash
...

两种写法的细微区别在于:/usr/bin/env bash会自动在PATH环境变量中查找并调用bash,而/bin/bash直接指定了bash的路径。 虽然Linux系统中几乎都会保证/bin/bash的存在,并且优先调用,但是前者的语法可能更加稳妥。

软件安装

大致介绍一下在Linux系统中安装软件的几种方式。

通过包管理器安装

系统自带的包管理器可以用于安装软件,这是最简单的一种方法,只需要一条命令即可,例如在Ubuntu中使用apt命令安装nginx

1
sudo apt-get install nginx

但是这种方法有几个显著的局限性:

  • 考虑到系统的稳定性,这种方式获取的软件版本很可能不是最新的;
  • 安装命令需要root权限,普通用户无法安装;
  • 安装过程默认需要联网,当然这个问题可以通过一些方式解决,让包管理器直接从本地文件中获取。

举个例子,通过apt安装nginx,安装过程大致可能为:

  • 把可执行文件存放到 /usr/sbin/nginx
  • 把库文件存放到 /usr/lib/nginx/modules
  • 把配置文件存放到 /etc/nginx,例如配置文件/etc/nginx/nginx.conf
  • 把文档文件存放到 /usr/share/doc/nginx
  • ...

apt包管理器的常用命令如下(可能需要sudo

1
2
3
4
5
6
7
apt update # 更新包列表信息

apt list --upgradeable # 列出可升级的包

apt upgrade # 升级包

apt autoremove # 删除无用的包

apt命令也可以换成apt-get,两者大致等效,但是前者更适合交互式使用,输出信息更丰富,后者更适合在脚本中使用。

可以使用下面的命令查询包的详细信息(不需要已经下载安装,可以在安装之前查看)

1
apt-cache show <package-name>

源码编译安装

/usr/local是大部分软件通过源码编译安装时设置的默认位置,对于普通用户可能因为权限失败而无法安装到此目录,习惯上使用~/.local作为替代的安装位置,这也是符合XDG规范的做法。

我们先关注/usr/local目录的结构,它通常包括如下内容:

  • /usr/local/bin/usr/local/sbin 存放可执行文件,通常是由源代码构建安装得到的
  • /usr/local/lib 存放程序的共享库文件,通常是为 /usr/local/bin/usr/local/sbin 中的可执行程序准备
  • /usr/local/include 存放 C 和 C++ 头文件
  • /usr/local/share 存放共享数据(如文档、图标、字体、locale 数据、示例配置等)
  • /usr/local/man 存放软件的手册页文件。这些文件可以通过 man 命令进行访问,帮助用户查看自定义安装软件的说明文档
  • /usr/local/etc 存放软件的配置文件。与系统的 /etc 类似,但用于存放本地安装的程序的配置
  • /usr/local/src 存放软件的源代码文件
  • /usr/local/tmp 用于存放本地程序运行时生成的临时文件

值得注意的是,usr不是user的缩写,是unix system resources的缩写。

管理员如果选择手动通过源码编译安装一个软件,那么在编译完成之后,需要将软件的对应部分正确存放在对应位置,这个行为通常会在安装脚本中提供。 这里举一个例子,例如我们编译一个名为demo的C++程序,从编译到安装大致包括如下几个命令

1
2
3
./configure
make
make install

在运行 ./configure 之前,一般可以运行 ./configure -h 查看其选项,有时需要指定一些选项并传递一些参数。 有的程序需要安装使用,有的程序只需要将可执行程序目录添加到PATH环境变量中。

安装脚本中设置的默认路径通常为/usr/local/,安装过程大致包括:

  • 把可执行文件存放到 /usr/local/bin(这样可以直接运行,不需要修改PATH环境变量)
  • 把库文件存放到 /usr/local/lib,对应的头文件存放在/usr/local/include(如果在其它位置,我们还需要手动处理环境变量,否则找不到库文件)
  • 把全局配置文件存放到 /usr/local/etc
  • 把文档文件存放到 /usr/local/share/doc/demo
  • 把手册页文件存放到 /usr/local/share/man(这样才能支持man命令)
  • ...

通常都支持在 ./configuremake install 命令中指定安装路径,否则才会使用默认路径。

除此之外,程序在运行时:

  • 如果需要生成临时文件,可以将其放置在/tmp
  • 如果需要生成日志,可以把日志文件放置在/var/log/demo
  • ...

预编译安装

对于用户数量较多的主流发行版,软件提供商还可能直接提供预编译好的二进制文件,可以开箱即用,直接下载到本地就可以运行,这种软件的安装卸载与包管理器完全无关。

举个例子,下载预编译版本的clang+llvm

1
2
3
mkdir clang
tar -xf clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz -C clang
cd clang

进入解压得到的目录后,可以发现当前的目录下包括与/usr/local相似的目录结构(binincludelib等),因为这就是为了移动到/usr/local/所准备的。 我们可以将其们全部复制到/usr/local/目录下,对应的子目录中的内容会自动进行合并(需要sudo权限)

1
sudo cp -R * /usr/local/

对于没有权限的普通用户,我们也可以换一个位置,例如使用~/.local/代替默认是/usr/local/

1
cp -R * ~/.local/

但是无论是将其移动合并到哪个位置,都会导致新安装的软件的各个部分和系统中原有的内容混到一起去了,这样不便于软件的独立管理和卸载等操作, 在某些情况下,我们还是希望保持软件的独立性,可以将软件的主要路径添加到PATH中,这样也可以正常地使用软件。

1
export PATH="xxxx:$PATH"

因为我们保持了软件的独立性,在卸载时只需要将整个目录删除即可,此时系统中通常只仅剩下软件的一些配置文件。

对于那些与发行版无关的大型第三方软件(例如MATLAB),这个问题是更为典型的。 官方提供的通常只是一个大型的预编译文件的压缩包(如 .tar.gz.zip 等,动辄几个G), 此时非常不推荐将各个部分的目录都拷贝到系统的对应目录下,否则容易导致问题。

主流的做法是将安装包直接解压到/opt的对应目录中,例如 /opt/google/chrome,然后根据安装文档进行相关配置。普通用户同样也没有/opt目录的权限,可以使用~/opt替代。

这类预编译的大型软件通常都是自包含的,即自身运行需要的所有二进制文件、库文件、文档、配置文件都会主要存放在自己的根目录下,维护一套类似的目录,例如

  • 主可执行文件通常存放在 /opt/<name>/bin/opt/<name> 根目录下
  • 专用库文件通常存放在 /opt/<name>/lib
  • 配置文件可能存放在 /opt/<name>/etc
  • 文档文件会存放在 /opt/<name>/doc/opt/<name>/share/doc
  • ...

对于通过源码编译和预编译安装的软件,还需要注意一件事:软件对glibc的版本要求。对于某些旧系统,因为使用的glibc版本太低,可能无法满足新版本的软件正常运行的最低要求,导致软件在运行时报错,报错信息例如

1
fd: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by fd)

再例如VSCode remote目前对于glibc的版本要求为至少2.28。(可能对低版本保持兼容性,但是默认的版本要求可能还会随着nodejs等的升级而提高)

可以通过如下命令查看系统中glibc的版本:

1
2
3
strings /lib64/libc.so.6 | grep ^GLIBC
# or
ldd --version

例如:

  • CentOS 7.9:glibc版本只有2.17;
  • Ubuntu 20:glibc版本为2.31;
  • Ubuntu 22:glibc版本为2.35。

鉴于glibc在Linux系统中具有的特殊地位,我们无法轻易地进行glibc的升级或替换,否则很容易导致系统出现问题,还不如直接换系统。

补充

家目录实际配置

家目录~/home/username,主要包含

  • ~/.local 大部分软件的源码安装位置
  • ~/.config 配置文件夹
  • ~/tmp 存放一些临时文件
  • ~/opt 一些大型软件的安装位置

~/.local相关目录添加到环境变量中

.bashrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi

# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=

# User specific aliases and functions
export PATH="$HOME/.local/bin:$PATH"

export LIBRARY_PATH="$HOME/.local/lib:$LIBRARY_PATH"
export LD_LIBRARY_PATH="$HOME/.local/lib:$LD_LIBRARY_PATH"

export PKG_CONFIG_PATH="$HOME/.local/lib/pkgconfig:$PKG_CONFIG_PATH"

export C_INCLUDE_PATH="$HOME/.local/include:$C_INCLUDE_PATH"
export CPLUS_INCLUDE_PATH="$HOME/.local/include:$CPLUS_INCLUDE_PATH"

对于Ubuntu,默认会在~/.bash_profile中会将~/.local/bin~/bin添加到PATH的最后,但是这样的优先级太低,可能安装的新版本软件在查找时会被系统自带的旧版本覆盖,因此这里手动添加到最前面。