跳到主要内容

Linux 文件系统结构

本节目标

理解 Linux/Unix 文件系统从根目录 / 组织路径的方式,区分系统目录与用户目录,并把所有写操作限制在显式指定的 dev-lab

开始前状态

从已通过 check-shell.sh 的 shell checkpoint 开始。本节只新增 notes/linux-notes.md,不修改 checkpoint 内已有文件。先在 shell checkpoint 根目录保存绝对路径;不要把 DEVLAB_DIR 设为 /、用户主目录或符号链接目录。

运行位置: shell checkpoint 根目录。预期输出: 显示当前练习目录的绝对路径。失败处理: 如果输出不是你准备好的 dev-lab,停止并进入正确目录,不要继续执行状态迁移。

export DEVLAB_DIR="$PWD"
printf '%s\n' "$DEVLAB_DIR"

必要原理

Linux 和 WSL 从根目录 / 开始组织文件。/home 通常保存普通用户目录,/tmp 保存临时文件;/etc 放系统配置,/usr 放大量系统提供的程序与资源。普通练习只观察这些系统目录,不向其中写入。

macOS 同样使用 Unix 风格路径,但用户目录位于 /Users,常见临时目录会映射到 /private/tmp。因此课程统一使用 Linux/Unix 概念,并在实际出现差异时明确说明。

/ 开头的是绝对路径,例如 $DEVLAB_DIR/notes;从当前目录解释的是相对路径,例如 notes/shell-notes.md。两者都必须先确认目标,再执行写操作。

动手完成

先只读观察几个常见目录和练习目录。命令不会创建或修改文件。

运行位置: 已设置 DEVLAB_DIR 的任意目录。预期输出: ls -ld 显示存在的系统目录与 dev-lab;macOS 上可能没有 /home失败处理: 某个平台目录不存在时记录差异即可,不要创建系统目录或提升权限补齐。

ls -ld / /home /tmp /etc /usr 2>/dev/null || true
ls -ld /Users /private/tmp 2>/dev/null || true
ls -ld "$DEVLAB_DIR"

下面的唯一变更块会固定 DEVLAB_DIR,完整调用权威 shell checker,拒绝意外路径、类型、权限或字节变化,再原子新增确定性笔记。它不读取当前用户名、平台名或实际工作路径作为笔记内容。

运行位置: 仓库中能够访问 code/devenv/dev-lab/checks/check-shell.sh 的目录,且 DEVLAB_DIR 指向待转换的 shell checkpoint。预期输出: checker 输出 shell checkpoint: ok,随后新增 notes/linux-notes.md失败处理: 任一检查失败都会保持 lab 不变并清理草稿;检查提示中的路径,不要删除或覆盖意外内容。

advance_linux_filesystem() (
set -euo pipefail

devlab_input="${DEVLAB_DIR:-}"
[[ -n "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
[[ -d "$devlab_input" ]] || { printf 'DEVLAB_DIR 必须是真实目录。\n'; exit 1; }
devlab_logical="$(cd -- "$devlab_input" && pwd -L)" || exit 1
devlab_dir="$(cd -- "$devlab_input" && pwd -P)" || exit 1
[[ "$devlab_logical" == "$devlab_dir" && ! -L "$devlab_input" ]] || {
printf 'DEVLAB_DIR 不得经过符号链接。\n'
exit 1
}
[[ "$devlab_dir" != / ]] || { printf 'DEVLAB_DIR 必须是明确的练习目录。\n'; exit 1; }
if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
home_dir="$(cd -- "$HOME" && pwd -P)" || exit 1
[[ "$devlab_dir" != "$home_dir" ]] || { printf 'DEVLAB_DIR 不得是用户主目录。\n'; exit 1; }
fi

checker="$PWD/code/devenv/dev-lab/checks/check-shell.sh"
[[ -f "$checker" && ! -L "$checker" ]] || { printf '找不到明确的 shell checker。\n'; exit 1; }
bash "$checker" "$devlab_dir"

notes_dir="$devlab_dir/notes"
final_notes="$notes_dir/linux-notes.md"
draft_notes="$notes_dir/.linux-notes.md.next"
[[ -d "$notes_dir" && ! -L "$notes_dir" ]] || { printf 'notes 必须是真实目录。\n'; exit 1; }
file_identity() {
local identity
if identity="$(LC_ALL=C stat -L -c '%d:%i' "$1" 2>/dev/null)"; then :; else
identity="$(LC_ALL=C stat -L -f '%d:%i' "$1")" || return 1
fi
printf '%s' "$identity"
}
fd_matches_identity() {
local fd_identity
fd_identity="$(file_identity "$1")" || return 1
[[ "$fd_identity" == "$2" || "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin \
&& "${fd_identity##*:}" == "${2##*:}" ]]
}
remove_if_owned() {
local owned_path="$1"
local owned_identity="$2"
local owned_name="${owned_path##*/}"
[[ -n "$owned_identity" ]] || return 0
(
cd -P -- "$notes_dir" || exit 1
cleanup_owned_boundary="$owned_name"
[[ "$(file_identity .)" == "$notes_dir_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || exit 1
[[ ! -e "./$owned_name" && ! -L "./$owned_name" ]] && exit 0
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name" 2>/dev/null)" == "$owned_identity" ]] || exit 1
/bin/rm -f -- "./$owned_name"
)
}
create_owned_file() {
local owned_path="$1"
local parent_identity="$2"
local mode="$3"
local identity_variable="$4"
local parent_path="${owned_path%/*}"
local owned_name="${owned_path##*/}"
local caller_directory="$PWD" created_identity=''
cd -P -- "$parent_path" || return 1
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || return 1
set -o noclobber
exec 6> "./$owned_name" || { set +o noclobber; return 1; }
set +o noclobber
created_identity="$(file_identity "./$owned_name")" || { exec 6>&-; return 1; }
printf -v "$identity_variable" '%s' "$created_identity"
owned_post_create_boundary="$owned_name"
fd_matches_identity /dev/fd/6 "$created_identity" || { exec 6>&-; return 1; }
/bin/cat >&6 || { exec 6>&-; return 1; }
owned_post_write_boundary="$owned_name"
/bin/chmod "$mode" /dev/fd/6 || { exec 6>&-; return 1; }
owned_post_mode_boundary="$owned_name"
fd_matches_identity /dev/fd/6 "$created_identity" || { exec 6>&-; return 1; }
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$created_identity" \
&& "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || { exec 6>&-; return 1; }
exec 6>&-
cd -P -- "$caller_directory" || return 1
}
publish_owned_file() {
local owned_path="$1"
local owned_identity="$2"
local final_path="$3"
local parent_identity="$4"
local parent_path="${owned_path%/*}"
local owned_name="${owned_path##*/}"
local final_name="${final_path##*/}"
(
cd -P -- "$parent_path" || exit 1
publication_boundary="$owned_name->$final_name"
[[ "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]] || exit 1
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" ]] || exit 1
[[ ! -e "./$final_name" && ! -L "./$final_name" ]] || exit 1
publication_commit_boundary="$owned_name->$final_name"
[[ -f "./$owned_name" && ! -L "./$owned_name" \
&& "$(file_identity "./$owned_name")" == "$owned_identity" \
&& ! -e "./$final_name" && ! -L "./$final_name" ]] || exit 1
if [[ "$(/usr/bin/uname -s 2>/dev/null || /bin/uname -s)" == Darwin ]]; then
/bin/mv -n -h -- "./$owned_name" "./$final_name" || exit 1
else
/bin/mv -n -T -- "./$owned_name" "./$final_name" || exit 1
fi
[[ ! -e "./$owned_name" && ! -L "./$owned_name" \
&& -f "./$final_name" && ! -L "./$final_name" \
&& "$(file_identity "./$final_name")" == "$owned_identity" \
&& "$(file_identity .)" == "$parent_identity" \
&& "$(file_identity ..)" == "$devlab_identity" ]]
)
}
devlab_identity="$(file_identity "$devlab_dir")" || exit 1
notes_dir_identity="$(file_identity "$notes_dir")" || exit 1
[[ ! -e "$final_notes" && ! -L "$final_notes" ]] || { printf '目标已存在,停止并检查:notes/linux-notes.md\n'; exit 1; }
[[ ! -e "$draft_notes" && ! -L "$draft_notes" ]] || { printf '草稿路径已存在,停止并检查。\n'; exit 1; }

draft_notes_identity=''
cleanup_linux_filesystem_draft() {
local status="$?"
trap - EXIT
remove_if_owned "$draft_notes" "$draft_notes_identity" || exit 1
exit "$status"
}
trap cleanup_linux_filesystem_draft EXIT

create_owned_file "$draft_notes" "$notes_dir_identity" u=rw,go=r draft_notes_identity <<'EXPECTED'
# Linux 基础练习笔记

## 文件系统
- 系统目录只观察;所有练习写入明确的 dev-lab。
- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。
EXPECTED
cmp -s <(printf '%s\n' \
'# Linux 基础练习笔记' \
'' \
'## 文件系统' \
'- 系统目录只观察;所有练习写入明确的 dev-lab。' \
'- Linux 与 WSL 的用户目录通常位于 /home,macOS 用户目录位于 /Users。') \
"$draft_notes" || exit 1
[[ "$(file_identity "$notes_dir")" == "$notes_dir_identity" \
&& "$(file_identity "$draft_notes")" == "$draft_notes_identity" ]] || exit 1
publish_owned_file "$draft_notes" "$draft_notes_identity" "$final_notes" "$notes_dir_identity"
[[ "$(file_identity "$notes_dir")" == "$notes_dir_identity" \
&& "$(file_identity "$final_notes")" == "$draft_notes_identity" ]] || exit 1
)

advance_linux_filesystem
transition_status="$?"
unset -f advance_linux_filesystem
[[ "$transition_status" -eq 0 ]]

观察结果

系统根目录和用户目录属于不同边界:查看 /etc/usr 是只读观察,课程练习则只写入 DEVLAB_DIR。新笔记只保存固定学习结论,不泄漏真实路径、用户名或平台输出。

常见问题

为什么 macOS 没有常用的 /home macOS 通常把用户目录放在 /Users;这是平台差异,不影响绝对路径与相对路径的基本规则。

为什么不能把主目录当作 dev-lab? 主目录包含真实项目和配置,范围过大。显式独立目录能让每次写入与恢复都可检查。

完成检查

  • 能解释 //home/tmp/etc/usr 的基本边界;
  • 知道 macOS 的 /Users/private/tmp 差异;
  • 能区分绝对路径与相对路径;
  • DEVLAB_DIR 指向真实、非符号链接的独立目录;
  • 迁移后只新增规范的 notes/linux-notes.md

下一步

下一节会在完整验证本节状态后,创建一个固定的小脚本,并只为新建的非符号链接草稿增加用户执行位。