A beginner-friendly introduction to Unix-like systems, including Linux filesystem layout, permissions, common shell commands, text processing, process and network tools, package managers, shells, terminal multiplexers, and SSH.
Unix-like 系统, 最常见的是 Linux 和 MacOS(它就是 Unix 系统), 它们的命令行工具和文件系统结构都遵循 POSIX 标准, 因此在这些系统上的命令行工具用法非常相似. 这部分内容主要介绍在 Unix-like 系统 (以 Linux 为例) 的核心命令行工具, 以及它们背后的权限管理和文件系统结构.
在 AI Agent 时代, 很多自动化任务和开发环境都基于 Unix-like 系统, 尤其是 Linux. 原因无他, 就是 POSIX 标准是开放的, 积累了大量的用户和训练集, 使得 AI 模型在理解和生成相关内容时表达更好. 相比之下, Powershell 由于权限管理机制, 命令结构都与 POSIX 不太相似, 加上 Powershell 的出现时间相比 Bash/Zsh 实在是太晚了, 训练集非常少. 在本文撰写的时候, 笔者已经听到多起 Codex 写错 Powershell 命令导致删盘的事故了. 因此在 Agent 时代, 使用 MacOS/Linux 作为开发环境已经成为更好的选择.
笔者一直认为, 除了计算机组成原理/计算机体系结构导论类课程应该成为所有 CS/ECE 学生的必修课以外, 操作系统 (Operating System) 课程也应该成为必修课. 下面将会讲到的一些内容相信在 OS 课上应该早有提及. 但遗憾的是笔者所在的学校 ECE 课程里甚至没有 OS 的选修课. 导致很多同学在上手做项目的时候才发现基本功严重匮乏.
即使你已经读完了这篇文章且你并不研究 OS 相关课题, 我仍然强烈建议系统性学习 OS 类课程, 包括用户/内核空间, 动态内存分配, 进程调度, 虚拟内存, 设备驱动, 同步机制, Multithreading 等等. 这些内容大概率不是你需要研究的内容, 但非常有助于排查各种诡异问题, 以及快速理解各种工具和库的设计原理. 例如, 你可能不需要自己写一个线程库, 但使用 Python 的多线程模块时, 了解操作系统层面线程是如何调度的, 以及 GIL 是什么, 会让你更好地理解为什么 Python 多线程在 CPU 密集型任务上表现不好, 以及为什么 multiprocessing 模块能绕过这个限制. 又例如, 你可能不需要自己写一个内存分配器, 但了解 malloc/free 的基本原理和常见实现, 会让你更好地理解内存泄漏是什么, 为什么会发生, 以及如何排查和修复它.
这部分的目标主要是帮助没有学过 OS 的 ECE 同学快速熟悉 Unix-like 系统的使用. 它并不打算详细介绍那些本应在 OS 课上学到的内容.
tmux 保持长时间任务, 并写简单 Shell 脚本自动化编译, 仿真, 日志处理和数据整理.grep/find/awk/sed 检查日志, 用 rsync 同步工程文件, 并能在误删/权限错误/磁盘满/远程连接断开时判断问题大概在哪里.Linux 的文件系统是从根目录 / 开始的一棵树. 这点和 Windows 很不一样: Windows 常见的是 C:\, D:\ 这样的盘符; Linux 没有盘符概念, 所有磁盘, 分区, U 盘, 网络文件系统都会被"挂载"到这棵目录树上的某个位置.
例如:
Windows:
C:\Users\kai\Desktop
D:\Projects\demo
Linux:
/home/kai/Desktop
/home/kai/projects/demo
/mnt/data另一个差异是路径分隔符. Windows 使用反斜杠 \, Linux 使用正斜杠 /. 并且 Linux 文件名通常大小写敏感, README.md 和 readme.md 是两个不同文件; Windows 默认情况下通常不区分大小写.
常见目录如下:
/: 根目录, 所有路径的起点./home: 普通用户的家目录, 类似 Windows 里的 C:\Users./root: root 用户的家目录, 不是普通用户的目录./etc: 系统和软件的配置文件目录, 很多服务的配置都在这里./bin, /usr/bin: 常用可执行程序目录, 例如 ls, cat, grep./sbin, /usr/sbin: 系统管理相关程序目录./usr: 系统中的大部分用户态程序, 库和资源文件./usr/local: 本机手动安装的软件常放在这里, 避免和包管理器安装的软件混在一起./var: 经常变化的数据, 例如日志, 缓存, 数据库文件./tmp: 临时文件目录, 通常重启后可能被清理./dev: 设备文件目录, 例如硬盘, 终端, 随机数设备./proc, /sys: 内核导出的虚拟文件系统, 用来查看进程和硬件状态./mnt, /media: 常见挂载点, 例如临时挂载硬盘或 U 盘./opt: 一些第三方软件会安装到这里.普通用户最常打交道的是自己的家目录:
cd ~
pwd
# /home/kai~ 表示当前用户的家目录. 如果当前用户是 kai, 那么 ~ 通常等价于 /home/kai.
Linux 还有一个习惯: 以 . 开头的文件或目录是隐藏文件, 例如 .bashrc, .git, .ssh. 这和 Windows 上通过文件属性标记隐藏文件不太一样. 使用 ls -a 可以显示隐藏文件.
Unix-like 系统里经常说"一切皆文件". 这句话不是说所有东西都是普通文本文件, 而是说很多系统资源都可以用类似文件的接口来访问: 打开, 读取, 写入, 关闭.
常见类型包括:
main.cpp, README.md./dev/null, /dev/sda, /dev/tty.几个例子:
cat /proc/cpuinfo # 查看 CPU 信息
echo "hello" > /dev/null # 写入黑洞设备, 内容会被丢弃
tty # 查看当前终端对应的设备文件Windows 也有设备和内核对象, 但它们通常不会以统一的普通路径形式暴露给用户. Linux 把很多东西放进文件系统树里, 因此命令行工具可以用统一方式组合起来.
可以用 file 查看一个路径大概是什么类型:
file README.md
file /dev/null
file /bin/ls既然 Unix-like 系统把很多东西都抽象成"文件", 那么进程读写文件时也有一套统一接口. 当一个进程打开文件, 管道, Socket 或终端时, 内核会给它一个小整数, 这个整数叫文件描述符 (File Descriptor, fd).
每个进程启动时通常已经打开了三个标准文件描述符:
0: stdin 标准输入
1: stdout 标准输出
2: stderr 标准错误可以把它们理解成程序默认连接的三根线:
stdout 和 stderr 分开很重要. 例如一个程序可以把正常结果输出给下一个命令处理, 同时把错误信息留在屏幕上给人看. Windows 也有标准输入输出错误流的概念, 但在 Linux 命令行里, 这种模型被用得更频繁, 也更自然.
重定向的本质, 是在程序启动前改变这些文件描述符指向的位置:
echo "hello" > out.txt # 等价于 1> out.txt, 把 stdout 写入文件
echo "hello" >> out.txt # 追加 stdout 到文件
sort < names.txt # 从文件读取 stdin
command 2> error.log # 把 stderr 写入文件
command > output.log 2>&1 # stdout 写入文件, stderr 也指向同一个位置2>&1 的意思是"让 2 号文件描述符指向 1 号当前指向的位置". 因此顺序很重要:
command > output.log 2>&1
command 2>&1 > output.log第一条命令会把 stdout 和 stderr 都写进 output.log. 第二条命令会先让 stderr 指向当前的 stdout(通常是终端), 然后才把 stdout 改到 output.log, 所以 stderr 仍然会显示在终端上.
如果想丢弃输出, 可以重定向到 /dev/null:
command > /dev/null
command > /dev/null 2>&1第一条只丢弃 stdout, 第二条同时丢弃 stdout 和 stderr.
在 Linux 上, 还可以通过 /proc 查看当前 Shell 打开的文件描述符:
ls -l /proc/$$/fd其中 $$ 是当前 Shell 进程的 PID. 这不是日常必须使用的命令, 但它能帮助你理解: 标准输入输出错误流并不是抽象魔法, 它们确实是进程打开的一组文件描述符.
Linux 里有两种常见链接: 符号链接 (Symbolic Link, Symlink) 和硬链接 (Hard Link).
符号链接更接近 Windows 里的快捷方式, 它本身保存的是另一个路径:
ln -s /var/log/nginx/access.log access.log
ls -l access.log
# access.log -> /var/log/nginx/access.log如果目标文件被移动或删除, 符号链接就会失效, 这种链接通常叫 broken symlink.
硬链接则不同. Linux 文件系统内部用 inode 表示文件对象, 文件名只是指向 inode 的名字. 硬链接相当于给同一个 inode 多起一个名字:
echo "hello" > a.txt
ln a.txt b.txt
ls -li a.txt b.txt你会看到 a.txt 和 b.txt 的 inode 编号相同. 修改其中一个文件, 另一个也会看到变化, 因为它们本质上是同一个文件对象.
硬链接有一些限制:
日常使用中, 符号链接更常见. 例如把配置文件链接到统一目录, 或者把某个版本的软件链接成默认版本:
ln -s /opt/mytool-1.2/bin/mytool /usr/local/bin/mytool从底层到用户看到的路径, 大致有几层:
硬盘/SSD -> 分区 -> 文件系统 -> 挂载点 -> 文件路径在 Windows 中, 你通常会看到类似 C: 和 D: 的盘符. 一个分区格式化成 NTFS 后, 系统给它分配一个盘符, 用户就通过这个盘符访问文件.
Linux 的模型更统一: 磁盘设备通常出现在 /dev 下, 例如:
/dev/sda
/dev/sda1
/dev/nvme0n1
/dev/nvme0n1p1其中 /dev/sda 可能表示一块硬盘, /dev/sda1 表示这块硬盘上的第一个分区. 分区上创建文件系统后, 还需要挂载到某个目录, 例如 /mnt/data, 才能通过普通路径访问.
查看磁盘和挂载情况:
lsblk
df -h
mountlsblk 适合看块设备和分区结构, df -h 适合看已经挂载的文件系统容量, mount 可以看到当前所有挂载关系.
文件系统决定了文件如何组织在磁盘上. Windows 常见文件系统是 NTFS, U 盘常见 FAT32/exFAT. Linux 上常见文件系统包括 ext4, btrfs, xfs 等.
ext4 是 Linux 上非常常见的默认选择. 它成熟, 稳定, 工具完善, 对普通服务器和个人开发环境都足够好. 如果你不知道该选什么, ext4 通常是稳妥选择.
btrfs 提供更多高级能力, 例如:
btrfs 的功能更强, 但也意味着理解成本更高. 对初学者来说, 先理解 ext4 这样的传统文件系统更重要.
查看某个文件系统类型:
df -T
lsblk -f挂载 (mount) 是把某个文件系统接到目录树上的过程. 例如把 /dev/sdb1 挂载到 /mnt/data:
sudo mkdir -p /mnt/data
sudo mount /dev/sdb1 /mnt/data
ls /mnt/data此时访问 /mnt/data 就是在访问 /dev/sdb1 这个分区里的文件.
卸载命令是 umount, 注意不是 unmount (这是历史遗留的拼写问题):
sudo umount /mnt/data如果提示 target is busy, 说明有进程正在使用这个目录. 常见原因是当前终端就在这个目录里, 或者某个程序打开了里面的文件. 可以先离开目录:
cd ~
sudo umount /mnt/data系统启动时自动挂载的配置通常在 /etc/fstab. 这个文件类似 Windows 里"开机自动识别某个盘符"的配置, 但 Linux 是指定设备挂载到哪个目录. 修改 /etc/fstab 要谨慎, 写错可能导致系统启动时挂载失败.
Linux 的传统权限模型围绕用户 (User) 和用户组 (Group) 展开. 每个进程都以某个用户身份运行, 文件也有自己的所有者和所属组.
查看当前用户:
whoami
id
groupsid 会显示 UID, GID 和当前用户所在的组:
uid=1000(kai) gid=1000(kai) groups=1000(kai),27(sudo)其中 UID 是用户 ID, GID 是用户组 ID. root 用户的 UID 是 0, 拥有系统最高权限.
和 Windows 相比, Linux 传统权限模型更简单: 主要分成所有者, 所属组, 其他人三类. Windows 的 NTFS ACL 更细, 可以给很多用户和组设置复杂规则. Linux 也支持 ACL, 但日常命令行开发中最常遇到的仍然是 rwx 权限位.
常见用户信息文件:
/etc/passwd: 用户列表和基本信息./etc/group: 用户组列表./etc/shadow: 用户密码哈希, 只有高权限用户能读.不要被 /etc/passwd 的名字误导. 现代 Linux 的密码哈希通常不直接存在这里, 而是在 /etc/shadow.
运行 ls -l 会看到文件权限:
ls -l script.sh
# -rwxr-xr-- 1 kai dev 123 Apr 28 10:00 script.sh第一段 -rwxr-xr-- 可以拆成:
- rwx r-x r--
类型 所有者 所属组 其他人其中:
r: read, 读权限.w: write, 写权限.x: execute, 执行权限.对普通文件来说, r 表示能读取内容, w 表示能修改内容, x 表示能作为程序或脚本执行.
对目录来说, 含义略有不同:
r: 能列出目录中的文件名.w: 能在目录中创建, 删除, 重命名文件.x: 能进入目录, 以及访问目录内已知名字的文件.目录的 x 权限很重要. 如果一个目录没有 x, 即使你知道里面有文件, 也无法正常访问.
权限也可以用数字表示:
r = 4
w = 2
x = 1所以:
7 = 4 + 2 + 1 = rwx6 = 4 + 2 = rw-5 = 4 + 1 = r-x4 = r--常见权限:
755 = rwxr-xr-x # 程序或目录常见权限
644 = rw-r--r-- # 普通文件常见权限
600 = rw------- # 私钥等敏感文件常见权限
700 = rwx------ # 私有目录常见权限新建文件和目录的默认权限受 umask 影响:
umask
# 0022umask 022 的意思是默认去掉 group 和 others 的写权限, 因此常见结果是:
新文件: 666 - 022 = 644
新目录: 777 - 022 = 755这里的减法只是帮助理解, 实际上是按权限位屏蔽. 初学时记住 umask 022 通常会让新文件是 644, 新目录是 755 就够了.
还有几个特殊权限位:
/tmp, 表示用户只能删除自己拥有的文件.查看 /tmp:
ls -ld /tmp
# drwxrwxrwt ...最后的 t 就是 sticky bit.
chmod, chown, chgrpchmod 用来修改权限:
chmod 755 script.sh
chmod +x script.sh
chmod u+x script.sh
chmod g-w config.yaml
chmod o-r secret.txt含义:
u: user, 所有者.g: group, 所属组.o: others, 其他人.a: all, 所有人.例如, 给脚本添加执行权限:
printf '#!/usr/bin/env bash\necho hello\n' > hello.sh
chmod +x hello.sh
./hello.shchown 用来修改文件所有者:
sudo chown kai file.txt
sudo chown kai:dev file.txtchgrp 用来修改所属组:
sudo chgrp dev file.txt递归修改目录权限要非常谨慎:
sudo chown -R kai:dev project/
chmod -R u+rwX,g+rX project/这里的 X 是一个特殊形式: 只给目录和已经有执行权限的文件添加执行权限. 它比无脑 chmod -R 755 project/ 更适合递归修改目录, 因为普通文本文件通常不需要执行权限.
不要随便对系统目录执行递归 chmod 或 chown, 例如 /, /usr, /etc. 这类操作很容易把系统权限破坏到无法正常启动.
sudo 和 susudo 表示以另一个用户身份执行命令, 默认是以 root 身份执行. 它类似 Windows 里的"以管理员身份运行", 但粒度更细: 你可以只让某一条命令以管理员权限执行.
sudo apt update
sudo systemctl restart nginxsudo 通常会要求输入当前用户的密码, 并且当前用户必须在 sudoers 配置允许的范围内. 常见做法是把用户加入 sudo 或 wheel 组.
su 用来切换用户:
su -
su - otherusersu - 会切换到 root 并加载 root 的登录环境. 如果只是临时执行一条命令, 更推荐使用 sudo command; 如果需要进入 root shell, 可以使用:
sudo -i基本原则是: 能不用 root 就不用 root, 能只给一条命令提权就不要长时间待在 root shell. 这样可以减少误操作造成的破坏.
ls, cd, pwd这三个命令用于浏览目录:
pwd # 显示当前目录
ls # 列出当前目录内容
ls -l # 长格式显示
ls -a # 显示隐藏文件
ls -lh # 人类可读大小, 例如 1K, 20M
ls -la # 常用组合
cd /path/to/dir # 切换目录
cd ~ # 回到家目录
cd - # 回到上一个目录ls -l 输出示例:
drwxr-xr-x 3 kai dev 4.0K Apr 28 10:00 src
-rw-r--r-- 1 kai dev 1.2K Apr 28 10:00 README.md第一列表示文件类型和权限. d 表示目录, - 表示普通文件, l 表示符号链接.
Linux 路径有绝对路径和相对路径:
cd /home/kai/project # 绝对路径, 从 / 开始
cd ../project # 相对路径, 从当前目录开始. 表示当前目录, .. 表示上一级目录.
cp, mv, rmcp 复制文件:
cp a.txt b.txt
cp a.txt backup/
cp -r src src-backup-r 表示递归复制目录.
保留权限, 时间戳等元信息:
cp -a project project-backupmv 用来移动或重命名:
mv old.txt new.txt
mv file.txt /tmp/
mv src/ project-src/rm 删除文件:
rm file.txt
rm -r old-dir需要特别注意, Linux 命令行删除通常不会进入 Windows 那种回收站. rm 删除后恢复成本很高. 对初学者来说, 删除目录前可以先用 ls 确认路径:
ls old-dir
rm -r old-dir如果命令里出现变量或通配符, 更要先确认展开结果:
printf '%s\n' *.log
rm *.logrm -rf 是强制递归删除, 很危险. 它适合清理明确知道可以删除的构建目录, 不适合在不理解路径时随手使用.
cat, less, head, tail这些命令用于查看文件内容.
cat 直接把文件内容输出到终端:
cat README.md
cat a.txt b.txtless 适合查看长文件:
less /var/log/syslog在 less 里常用按键:
Space: 下一页.b: 上一页./keyword: 搜索.n: 下一个搜索结果.q: 退出.head 查看文件开头:
head file.txt
head -n 20 file.txttail 查看文件结尾:
tail file.txt
tail -n 50 file.txt实时查看日志常用:
tail -f /var/log/nginx/access.logtail -f 会持续等待新内容, 很适合观察服务日志.
find, xargsfind 用来按条件查找文件. 它比很多图形文件管理器里的搜索更强, 但语法也更像编程语言.
按名字查找:
find . -name "*.cpp"
find /var/log -name "*.log"按类型查找:
find . -type f # 普通文件
find . -type d # 目录
find . -type l # 符号链接按大小和时间查找:
find . -type f -size +100M
find . -type f -mtime -7-size +100M 表示大于 100 MB, -mtime -7 表示最近 7 天内修改过.
find 可以直接执行命令:
find . -name "*.tmp" -delete
find . -name "*.sh" -exec chmod +x {} \;
find . -name "*.tmp" -exec rm {} +{} 表示找到的文件路径. \; 和 + 都表示 -exec 命令结束, 但执行方式不同.
使用 \; 时, find 会对每个文件执行一次命令:
find . -name "*.sh" -exec chmod +x {} \;
# 类似于:
# chmod +x ./a.sh
# chmod +x ./b.sh
# chmod +x ./scripts/c.sh使用 + 时, find 会把多个结果尽量合并到同一次命令里:
find . -name "*.tmp" -exec rm {} +
# 类似于:
# rm ./a.tmp ./b.tmp ./cache/c.tmp因此 + 通常更快, 尤其是文件很多的时候, 因为它减少了启动命令的次数. 但 + 的限制也更多: {} 通常要放在命令参数的最后并紧挨着 +. 如果你的命令必须一次只处理一个文件, 或者 {} 必须出现在命令中间, 就使用 \;.
xargs 用来把前一个命令的输出变成后一个命令的参数. 例如删除所有 .tmp 文件:
find . -name "*.tmp" -print0 | xargs -0 rm这里使用 -print0 和 xargs -0 是为了正确处理文件名里的空格和特殊字符. 这是比简单 find . -name "*.tmp" | xargs rm 更稳妥的写法.
再比如统计所有 C++ 文件行数:
find src -name "*.cpp" -print0 | xargs -0 wc -lgrepgrep 用来在文本中搜索匹配的行.
grep "TODO" main.cpp
grep -n "TODO" main.cpp
grep -r "TODO" src/
grep -ri "error" logs/常用选项:
-n: 显示行号.-r: 递归搜索目录.-i: 忽略大小写.-v: 反向匹配, 显示不匹配的行.-E: 使用扩展正则表达式.例子:
grep -rn "std::move" src/
grep -v "^#" config.txt
grep -E "error|warning" build.loggrep 经常和管道一起使用:
ps aux | grep nginx
cat build.log | grep -i error现代开发中, 如果安装了 ripgrep, 更推荐用 rg, 它通常更快, 默认会尊重 .gitignore:
rg "TODO"
rg -n "std::move" src管道和重定向是 Unix 命令行最重要的组合能力. 很多命令只做一件小事, 但可以通过管道拼起来完成复杂任务.
管道 | 会把前一个命令的标准输出交给后一个命令的标准输入:
cat access.log | grep "404" | wc -l这个命令表示: 输出日志内容, 筛选包含 404 的行, 统计行数.
重定向用于把输入输出接到文件:
echo "hello" > hello.txt # 覆盖写入
echo "world" >> hello.txt # 追加写入
sort names.txt > sorted.txt常见标准流:
把错误输出重定向到文件:
command 2> error.log把标准输出和错误输出都写入同一个文件:
command > output.log 2>&1或者在 Bash 中使用更短写法:
command &> output.log从文件作为标准输入:
sort < names.txt一个稍复杂但很常见的例子: 找出访问日志里出现最多的 IP:
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head这条命令的含义是: 取第一列 IP, 排序, 统计重复次数, 按次数倒序排序, 显示前几名.
top, free, df, du这些命令用来查看系统资源. 比如跑仿真, 综合, 编译 LLVM, 训练小模型, 或者处理大数据文件的时候, 你经常需要判断到底是 CPU 忙, 内存爆了, 还是磁盘满了.
top 用来实时查看进程和资源占用:
top进入 top 后常用按键:
q: 退出.P: 按 CPU 使用率排序.M: 按内存使用率排序.k: 结束进程, 会提示输入 PID.如果系统安装了 htop, 它的交互界面更友好:
htop查看内存:
free -h-h 表示 human-readable, 会用 GB/MB 显示. 输出里常见的 available 比 free 更有参考价值, 因为 Linux 会把暂时不用的内存拿来做缓存, 这部分缓存必要时可以释放.
查看文件系统剩余空间:
df -h
df -h .df -h . 表示查看当前目录所在文件系统的容量. 当编译或仿真报错说 No space left on device 时, 第一反应应该是运行 df -h.
查看某个目录占了多少空间:
du -sh build/
du -h --max-depth=1 .du -sh build/ 会给出 build/ 目录总大小. du -h --max-depth=1 . 可以查看当前目录下每个一级子目录的大小, 很适合找出是谁把磁盘占满了.
一个常见清理流程:
df -h .
du -h --max-depth=1 . | sort -h
rm -r build/先确认磁盘是否满, 再找大目录, 最后只删除确定可以重新生成的目录. 不要看到大文件就直接删, 尤其是在共享服务器上.
ps, killtop 适合实时观察系统, 但如果你想在命令行里查看进程列表, 更常用 ps.
进程 (Process) 是正在运行的程序实例. 每个进程都有一个 PID (Process ID), 父进程也有 PPID. Shell 启动一个程序时, 本质上就是创建一个新进程.
常用命令:
ps
ps aux
ps -ef
ps -p 12345ps 默认只显示当前终端相关的进程. ps aux 和 ps -ef 会显示更完整的进程列表, 两者风格不同, 但日常用途接近.
配合 grep 查找进程:
ps aux | grep nginx不过这个命令经常会把 grep nginx 自己也搜出来. 更直接的方式是使用 pgrep:
pgrep nginx
pgrep -a nginxpgrep -a 会显示 PID 和命令行.
kill 用来给进程发送信号, 不一定是"杀掉"进程. 默认信号是 TERM, 表示请求进程正常退出:
kill 12345
kill -TERM 12345常见信号:
TERM 或 15: 请求进程正常退出, 默认选择.KILL 或 9: 强制终止, 进程无法捕获和清理现场.INT 或 2: 类似在终端按 Ctrl-C.HUP 或 1: 常用于让服务重新加载配置.如果普通退出无效, 最后才考虑:
kill -9 12345不要把 kill -9 当成默认操作. 它会让进程没有机会保存状态, 删除临时文件, 释放资源. 在共享服务器上, 也不要随便 kill 不属于自己的进程.
按名字结束进程可以使用:
pkill nginx
pkill -f "python train.py"pkill -f 会按完整命令行匹配, 威力更大, 使用前建议先用 pgrep -af 预览:
pgrep -af "python train.py"wget, curlcurl 和 wget 都可以从网络获取内容. 简单理解:
curl: 更适合调试 HTTP/API 请求, 默认把响应输出到终端.wget: 更适合下载文件, 默认保存到当前目录.从原理上看, 它们都是命令行 HTTP 客户端: 向服务器发送请求, 接收响应头和响应体. 响应头里包含状态码, 内容类型, 重定向位置等元信息; 响应体才是真正的网页, JSON 或文件内容.
使用 curl 查看网页或接口返回:
curl https://example.com
curl -I https://example.com-I 只查看 HTTP 响应头, 常用于检查状态码, 重定向, content-type 等.
跟随重定向:
curl -L https://github.com保存到文件:
curl -o index.html https://example.com
curl -O https://example.com/file.tar.gz-o 后面自己指定文件名, -O 使用 URL 里的文件名.
发送 JSON 请求:
curl -X POST https://api.example.com/jobs \
-H "Content-Type: application/json" \
-d '{"name": "test"}'这类用法在调试本地服务, REST API, Webhook 时很常见.
wget 下载文件:
wget https://example.com/file.tar.gz
wget -O model.bin https://example.com/download断点续传:
wget -c https://example.com/big-file.zip下载到指定目录:
wget -P downloads/ https://example.com/file.tar.gz在脚本里下载文件时, 建议同时保存校验信息, 或者下载后验证 hash. 尤其是工具链, 预训练模型, 数据集这类大文件, 下载损坏后错误可能会在很后面才暴露.
curl 和 wget 都会受到代理环境变量影响:
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890如果你在中国大陆网络环境下使用 GitHub, 下载工具链或访问部分国外资源, 经常会需要配置代理. 代理配置应该尽量放在当前 Shell 或项目脚本里明确管理, 不要在系统全局到处散落, 否则后续排查网络问题会很麻烦.
awk, sedawk 和 sed 是两个经典文本处理工具. 初学时不必掌握所有细节, 先理解它们适合做什么即可:
awk: 更适合按列处理结构化文本.sed: 更适合按行做替换, 删除, 插入等流式编辑.awk 默认按空白字符分列. 例如:
echo "alice 90" | awk '{print $1}'
# alice打印文件第一列和第三列:
awk '{print $1, $3}' data.txt指定分隔符为逗号, 处理 CSV 的简单场景:
awk -F ',' '{print $1, $3}' users.csv求第二列总和:
awk '{sum += $2} END {print sum}' scores.txt过滤第二列大于 80 的行:
awk '$2 > 80 {print $0}' scores.txtsed 常见用法是替换:
sed 's/old/new/' file.txt
sed 's/old/new/g' file.txt第一个命令每行只替换第一次出现的 old, 第二个命令里的 g 表示替换每一行中所有出现的 old.
直接修改文件需要 -i:
sed -i 's/foo/bar/g' config.txt在 macOS 上, BSD sed 的 -i 语法和 GNU/Linux 不同, 通常需要:
sed -i '' 's/foo/bar/g' config.txt删除空行:
sed '/^$/d' file.txt打印第 10 到 20 行:
sed -n '10,20p' file.txt这些命令看起来像魔法, 但本质上都是"一行一行读文本, 对匹配的行执行动作".
正则表达式 (Regular Expression) 用来描述文本模式. grep, sed, awk, 很多编辑器搜索功能都支持正则.
常见符号:
.: 匹配任意单个字符.*: 前一个模式重复 0 次或多次.+: 前一个模式重复 1 次或多次, 通常需要扩展正则.?: 前一个模式出现 0 次或 1 次, 通常需要扩展正则.^: 行首.$: 行尾.[]: 字符集合.|: 或, 通常需要扩展正则.(): 分组, 通常需要扩展正则.例子:
grep "^ERROR" app.log # 以 ERROR 开头的行
grep "done$" app.log # 以 done 结尾的行
grep -E "error|warning" log.txt # 包含 error 或 warning
grep -E "[0-9]+" data.txt # 包含连续数字查找简单邮箱格式:
grep -E "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}" users.txt这个正则并不完美, 但足以展示正则的用途: 用模式描述一类字符串.
需要注意的是, 不同工具支持的正则语法略有差异. grep 默认是基本正则, grep -E 是扩展正则. 如果发现 +, |, () 不生效, 先检查是否需要加 -E.
正则表达式很强, 但也容易写得难懂. 如果只是固定字符串搜索, 不要为了使用正则而使用正则:
grep -F "a.b" file.txt-F 表示按固定字符串搜索, 此时 . 不会被当成"任意字符".
apt, yum/dnf, pacmanLinux 发行版通常自带包管理器, 用来安装, 升级, 删除软件. 这和 Windows 上到网站下载安装包的方式不同. 包管理器会从软件源安装程序, 同时处理依赖关系和安全更新.
不同发行版使用不同包管理器:
aptyum 或 dnfpacmanUbuntu/Debian 常用命令:
sudo apt update # 更新软件包索引
sudo apt upgrade # 升级已安装软件
sudo apt install git curl # 安装软件
sudo apt remove nginx # 删除软件
apt search ripgrep # 搜索软件
apt show ripgrep # 查看软件信息apt update 不会升级软件, 它只是更新本地软件包索引. 真正升级已安装软件的是 apt upgrade.
Fedora/RHEL 常用命令:
sudo dnf check-update
sudo dnf upgrade
sudo dnf install git curl
sudo dnf remove nginx
dnf search ripgrep较老的系统可能使用 yum:
sudo yum install gitArch Linux 常用命令:
sudo pacman -Syu # 同步软件源并升级系统
sudo pacman -S git curl # 安装软件
sudo pacman -R nginx # 删除软件
pacman -Ss ripgrep # 搜索软件包管理器安装的软件通常会放到系统目录中, 例如 /usr/bin, /usr/lib, /etc. 这也是为什么安装软件常常需要 sudo.
homebrewHomebrew 最初是 macOS 上最流行的包管理器, 现在也支持 Linux. 对 macOS 用户来说, 它类似 Linux 发行版自带包管理器, 可以用命令安装开发工具.
常用命令:
brew update
brew install git ripgrep
brew upgrade
brew uninstall wget
brew search cmake
brew info cmakeHomebrew 有两个常见概念:
git, cmake, ripgrep.安装图形应用:
brew install --cask visual-studio-code在 Apple Silicon Mac 上, Homebrew 默认安装在 /opt/homebrew; 在 Intel Mac 上, 常见位置是 /usr/local. 如果安装后终端找不到 brew, 通常是 PATH 没配置好.
最后需要强调: 不同包管理器不要混用到同一套系统文件里. 例如一个软件如果已经由 apt 管理, 就不要随手再用源码安装覆盖到 /usr/bin. 如果确实需要手动安装, 通常放到 /usr/local 或 /opt 更清晰.
Shell 是用户和操作系统交互的命令解释器. 你在终端里输入的 ls, cd, grep, 管道和重定向, 都由 Shell 解析后再交给系统执行.
常见 Shell 包括:
bash: Linux 上最常见, 脚本兼容性最好.zsh: macOS 默认 Shell, 交互体验更好.fish: 更现代, 补全和提示友好, 但脚本语法不兼容 POSIX Shell.查看当前正在使用的 Shell:
echo $SHELL
ps -p $$$SHELL 表示用户默认 Shell, ps -p $$ 显示当前 Shell 进程. 两者有时不完全一样, 因为你可以在一个 Shell 里手动启动另一个 Shell.
环境变量是进程继承的一组 key=value 配置. 很多开发工具通过环境变量查找编译器, 库, 许可证服务器, Python 包路径等. 对 ECE 学生来说, EDA 工具链和交叉编译工具链经常依赖环境变量.
查看环境变量:
env
printenv PATH
echo $PATH设置一个只对当前 Shell 有效的变量:
export MY_PROJECT=/home/kai/project
cd "$MY_PROJECT"PATH 是最重要的环境变量之一. Shell 执行命令时, 会按 PATH 里的目录顺序查找可执行文件:
echo $PATH
which gcc
which python3临时把一个工具目录加入 PATH:
export PATH=/opt/riscv/bin:$PATH这里把 /opt/riscv/bin 放在前面, 表示优先使用这个目录里的工具. 这在使用 RISC-V 交叉编译器, FPGA 工具链, 自己编译的 LLVM 时很常见.
一些常见环境变量:
PATH: 可执行程序搜索路径.HOME: 当前用户家目录.PWD: 当前目录.SHELL: 默认 Shell.EDITOR: 默认文本编辑器.LD_LIBRARY_PATH: 动态库搜索路径, Linux 上调试本地编译库时常见.PYTHONPATH: Python 模块搜索路径.环境变量只会从父进程传给子进程, 不会反过来传. 这是理解 source 和直接运行脚本差别的关键.
直接运行脚本时, Shell 会启动一个子进程去执行它:
./setup.sh如果 setup.sh 里写了:
export PATH=/opt/riscv/bin:$PATH这个修改只发生在子进程里. 脚本结束后, 子进程退出, 父 Shell 的 PATH 不会改变.
source 则是在当前 Shell 里逐行执行脚本内容:
source setup.sh
# 或者
. setup.sh因此, 如果一个脚本的目的只是编译, 测试, 下载文件, 通常直接运行它; 如果一个脚本的目的是修改当前终端环境, 例如设置 PATH, LD_LIBRARY_PATH, license server 等, 就应该 source 它.
很多 EDA 工具会要求你 source /path/to/settings.sh, 原因就是它需要修改当前 Shell 的 PATH, LD_LIBRARY_PATH 等变量.
Shell 可以按两个维度理解:
一个 Shell 可以既是 login shell, 也是 interactive shell; 也可以只是执行脚本的 non-interactive shell.
这会影响配置文件加载顺序. 以 Bash 为例:
~/.bash_profile, ~/.bash_login 或 ~/.profile.~/.bashrc.以 Zsh 为例:
~/.zprofile: login shell 常用.~/.zshrc: interactive shell 常用.实际建议是:
~/.bashrc 或 ~/.zshrc.~/.profile 或 ~/.zprofile.查看当前 Shell 是否是交互式:
case $- in
*i*) echo interactive ;;
*) echo non-interactive ;;
esac$- 里包含 i 时表示 interactive shell.
Bash 和 Zsh 的日常命令基本相同, 大多数简单命令, 管道, 重定向, 环境变量写法都能通用.
Bash 的优势是兼容性. 服务器, Docker 镜像, CI 环境中几乎一定有 Bash. 如果你写脚本给别人运行, Bash 是更稳妥的选择.
Zsh 的优势是交互体验. macOS 默认使用 Zsh, 并且它的补全, glob 扩展, prompt 生态都更强. 很多人会配合 Oh My Zsh 或其他插件管理器使用.
查看版本:
bash --version
zsh --version脚本第一行通常叫 shebang, 用来指定解释器:
#!/usr/bin/env bash如果脚本使用 Bash 特性, 就不要写成 #!/bin/sh. 很多系统上的 /bin/sh 并不是 Bash, 而是更小的 POSIX Shell, 例如 dash.
查看系统支持哪些 Shell:
cat /etc/shells修改默认 Shell:
chsh -s /bin/zsh有些系统上 Zsh 路径可能是 /usr/bin/zsh 或 Homebrew 下的 /opt/homebrew/bin/zsh, 可以先用:
which zsh如果 chsh 报错说目标 Shell 不在 /etc/shells, 需要把路径加入 /etc/shells, 这通常需要管理员权限.
在共享服务器上不建议大幅修改默认 Shell, 因为一些老旧环境或 EDA 工具脚本可能假设 Bash. 更稳妥的方式是保持默认 Bash, 需要时手动运行 zsh 或在本机使用 Zsh.
Shell 配置文件用来设置环境变量, alias, 函数和 prompt. 它们本质上就是在 Shell 启动时自动 source 的脚本. 也就是说, 你写在 ~/.bashrc 或 ~/.zshrc 里的命令, 会在对应的交互式 Shell 启动时自动执行.
常见文件:
~/.bashrc, ~/.bash_profile, ~/.profile.~/.zshrc, ~/.zprofile.添加 alias:
alias ll='ls -lah'
alias gs='git status'添加常用工具路径:
export PATH="$HOME/.local/bin:$PATH"
export PATH="/opt/riscv/bin:$PATH"修改配置后, 可以重新加载:
source ~/.bashrc
source ~/.zshrc这等价于手动执行一次 Shell 启动时会自动做的加载过程. 所以修改 ~/.bashrc 后不一定要关闭终端重开, 直接 source ~/.bashrc 即可让改动在当前 Shell 生效.
一个常见错误是把会输出大量文字或启动交互程序的命令放进配置文件, 导致每次打开终端或运行远程命令都出问题. 配置文件应该尽量保持简单, 尤其是在服务器上.
如果你改坏了配置文件, 导致终端启动报错, 可以用一个不加载配置的 Shell 进入:
bash --noprofile --norc然后编辑对应配置文件修复.
Shell 脚本适合把一串命令保存下来重复执行. 对 ECE 项目来说, 它常用于自动化编译, 跑仿真, 收集日志, 批量转换数据.
一个最小脚本:
#!/usr/bin/env bash
set -e
mkdir -p build
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure保存为 build.sh 后:
chmod +x build.sh
./build.shset -e 表示命令失败时脚本立刻退出, 避免前面已经失败, 后面还继续执行造成更混乱的结果. 对更严格的脚本, 常见写法是:
set -euo pipefail含义:
-e: 命令失败时退出.-u: 使用未定义变量时报错.-o pipefail: 管道中任意一段失败, 整个管道算失败.读取参数:
#!/usr/bin/env bash
set -euo pipefail
build_type="${1:-Release}"
cmake -S . -B build -DCMAKE_BUILD_TYPE="$build_type"
cmake --build build${1:-Release} 表示如果第一个参数不存在, 就使用 Release.
遍历文件:
#!/usr/bin/env bash
set -euo pipefail
for log in logs/*.log; do
echo "checking $log"
grep -n "ERROR" "$log" || true
done这里变量都用双引号包起来, 是为了正确处理路径中的空格. grep 找不到匹配时会返回非零状态, 所以在 set -e 下用 || true 表示这个情况可以接受.
脚本里最需要避免的是不加确认地删除大范围路径:
rm -rf "$build_dir"这种命令执行前要确保变量一定被正确设置, 并且路径确实是可删除的构建产物目录.
Fish 是一个偏交互体验的 Shell. 它默认提供很好的自动补全, 历史建议和语法高亮, 对初学者很友好.
但 Fish 的变量语法和 Bash/Zsh 不同:
set -x PATH /opt/riscv/bin $PATH这也是它最大的取舍: 交互体验很好, 但脚本语法不兼容 POSIX Shell. 所以很多工业软件的脚本不能直接在 Fish 上执行. 因此建议:
source setup.sh, 通常应该在 Bash/Zsh 中执行, 不要直接在 Fish 中执行.tmux 和 screentmux 和 screen 是终端复用器. 它们可以在一个 SSH 会话里开多个终端窗口, 更重要的是, 即使 SSH 断开, 里面的任务仍然可以继续运行.
例如你在实验室服务器上跑 FPGA 综合, 长时间仿真, 大型 C++ 编译, 如果本地网络断开, 普通 SSH 会话里的任务可能会被终止. 使用 tmux 后, 任务在服务器端的 tmux session 里继续运行, 你之后重新 SSH 上去再接回去即可.
tmux 基本用法:
tmux new -s work这会创建一个名为 work 的 session. 在里面正常运行命令即可.
脱离当前 session, 但不结束里面的程序就是按 Ctrl-b, 然后按 d (detach).
重新连接:
tmux attach -t work查看已有 session:
tmux ls结束 session:
tmux kill-session -t work常用快捷键都以 prefix 开始, 默认 prefix 是 Ctrl-b:
Ctrl-b c: 新建窗口.Ctrl-b n: 下一个窗口.Ctrl-b p: 上一个窗口.Ctrl-b ": 水平分屏.Ctrl-b %: 垂直分屏.Ctrl-b d: detach.一个常见工作流:
ssh lab-server
tmux new -s synth
make bitstream
# Ctrl-b d第二天回来:
ssh lab-server
tmux attach -t synthscreen 是更老的工具, 很多服务器默认安装. 基本用法:
screen -S work
# Ctrl-a d detach
screen -ls
screen -r work如果能选, 一般推荐 tmux; 如果服务器上没有 tmux, screen 也足够完成"断线后任务继续跑"这个核心需求.
ssh 和远程连接SSH (Secure Shell) 是连接远程 Linux 机器最常用的方式. 实验室服务器, 云服务器, 开发板, FPGA SoC 板卡, 很多都是通过 SSH 登录.
最基本的用法:
ssh kai@example.com
ssh kai@192.168.1.10如果远程 SSH 服务不是默认的 22 端口:
ssh -p 2222 kai@example.com第一次连接某台机器时, SSH 会提示确认 host key. 这是在确认"这台服务器的身份指纹". 如果以后突然提示 host key changed, 不要直接忽略, 可能是服务器重装了, 也可能是连接被劫持了. 在实验室环境里, 可以先问管理员或确认机器是否重装.
SSH 支持密码登录, 也支持密钥登录. 密钥登录更常见, 也更适合自动化.
公钥和私钥是一对配套生成的密钥. 私钥只放在你自己的机器上, 必须保密; 公钥可以公开, 放到服务器上也没关系. 它们的核心关系是: 私钥能证明"我是这把公钥对应的持有人", 而别人不能从公钥反推出私钥.
更具体一点:
SSH 登录时, 用户密钥主要用于身份验证, 更接近"签名"模型. 服务器保存你的公钥. 登录时服务器给客户端一个挑战, 客户端用私钥生成签名, 服务器用公钥验证签名. 如果验证通过, 服务器就知道连接者确实持有对应私钥, 于是允许登录.
SSH 连接本身的加密还会经过密钥交换过程, 双方协商出临时会话密钥, 后续数据通常使用对称加密传输. 所以不要把 SSH 简化成"一直用公钥加密所有数据". 实际系统会组合使用非对称加密, 签名, 密钥交换和对称加密.
还有一类密钥是服务器的 host key. 第一次连接服务器时, SSH 提示你确认的指纹就是服务器 host key 的指纹, 它用来确认"我连到的是不是同一台服务器".
生成一对密钥:
ssh-keygen -t ed25519 -C "kai@example.com"默认会生成:
~/.ssh/id_ed25519 # 私钥
~/.ssh/id_ed25519.pub # 公钥私钥必须保密, 不要发给别人, 不要提交到 Git 仓库. 公钥可以放到服务器的 ~/.ssh/authorized_keys 中.
把公钥复制到服务器:
ssh-copy-id kai@example.comssh-copy-id 的作用是把你本机的公钥追加到远程服务器用户家目录下的 ~/.ssh/authorized_keys 中, 并尽量处理好 .ssh 目录和文件权限. 做完这一步后, 服务器就知道这把公钥对应的私钥可以用于登录这个账户.
如果没有 ssh-copy-id, 可以手动追加:
cat ~/.ssh/id_ed25519.pub | ssh kai@example.com 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'服务器端常见权限要求:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys本机私钥权限也不能太宽:
chmod 600 ~/.ssh/id_ed25519如果权限太宽, SSH 可能会拒绝使用这个私钥.
ssh 配置文件如果经常连接同一台服务器, 可以把配置写进 ~/.ssh/config:
Host lab
HostName example.com
User kai
Port 22
IdentityFile ~/.ssh/id_ed25519之后只需要:
ssh lab配置多个机器也很方便:
Host fpga-board
HostName 192.168.1.50
User root
Host gpu-server
HostName gpu.example.edu
User kai
IdentityFile ~/.ssh/id_ed25519常用选项:
Host: 给这个连接起的别名.HostName: 真实域名或 IP.User: 登录用户名.Port: SSH 端口.IdentityFile: 使用哪把私钥.ProxyJump: 通过跳板机连接内网机器.跳板机例子:
Host internal-board
HostName 192.168.10.20
User root
ProxyJump kai@gateway.example.edu之后 ssh internal-board 会先连接 gateway.example.edu, 再跳到内网板卡.
scp 和 rsyncscp 用来通过 SSH 复制文件. 从本机复制到远程:
scp bitstream.bit lab:~/bitstreams/从远程复制到本机:
scp lab:~/logs/run.log .复制目录:
scp -r project/ lab:~/project/rsync 更适合同步目录. 它会比较两边差异, 只传变化部分, 适合大工程反复同步.
从本机同步到远程:
rsync -av project/ lab:~/project/从远程同步到本机:
rsync -av lab:~/results/ results/常用选项:
-a: archive 模式, 保留目录结构, 权限, 时间戳等.-v: verbose, 显示过程.-z: 压缩传输, 慢网络上有用.--delete: 删除目标端多余文件, 让两边完全一致.--exclude: 排除某些文件.同步工程到服务器, 排除构建产物和 Git 目录:
rsync -av --exclude build --exclude .git project/ lab:~/project/让远程目录和本地完全一致:
rsync -av --delete project/ lab:~/project/--delete 很有用也很危险, 目标端多出来的文件会被删掉. 第一次使用前可以加 --dry-run 预览:
rsync -av --delete --dry-run project/ lab:~/project/ssh -R 端口转发SSH 端口转发可以把一台机器上的端口通过 SSH 隧道暴露到另一台机器. 常见有两类:
ssh -L: 本地端口转发, 把远程服务映射到本地.ssh -R: 远程端口转发, 把本地服务映射到远程.ssh -L 更常见. 例如远程服务器上有一个只监听本机的 Jupyter:
ssh -L 8888:localhost:8888 lab之后你在本机浏览器访问 http://localhost:8888, 实际连接的是远程服务器的 localhost:8888.
ssh -R 的方向相反. 它把本机或当前网络里的服务暴露给远程机器. 例如你本机运行了一个 Web 服务:
python3 -m http.server 8000然后另开一个终端:
ssh -R 9000:localhost:8000 lab此时在远程服务器 lab 上访问:
curl http://localhost:9000流量会通过 SSH 隧道回到你本机的 localhost:8000.
这个功能适合几类场景:
ssh -R 把本机 Clash 等代理软件的本地端口临时转发到远程服务器, 让远程服务器通过这条 SSH 隧道访问本机代理.以 Clash 常见的 HTTP 代理端口 7890 为例:
ssh -R 7890:localhost:7890 lab然后在远程服务器上:
export http_proxy=http://127.0.0.1:7890
export https_proxy=http://127.0.0.1:7890
curl https://github.com这里的流量路径是: 远程服务器访问自己的 127.0.0.1:7890, SSH 把这部分流量带回你的本机, 再交给本机的 Clash 代理端口. 这适合临时下载依赖或访问开发资源, 但不应该在不了解安全边界时长期暴露.
需要注意:
ssh -R 能否让远程机器的其他用户或外部网络访问, 受服务器 sshd_config 中 GatewayPorts 等配置影响.如果只需要保持连接不断开, 可以加 keepalive:
ssh -o ServerAliveInterval=60 -R 9000:localhost:8000 lab这会每 60 秒发送保活消息, 在网络不稳定时更可靠一些.