Linux 上 htop/top 中所有可见内容的详解

Hacker News Top 工具

摘要

一份详尽的技术指南,解释 Linux 下 htop 和 top 命令输出中可见的所有元素,包括负载平均值、进程状态、内存使用等。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/07/04 15:40

# htop 详解 来源:https://peteris.rocks/blog/htop/ 很长一段时间我都不知道 htop 中各项的含义。我曾以为在双核机器上负载平均值 `1.0` 意味着 CPU 使用率为 50%。这并不完全正确。此外,为什么显示的是 `1.0`?我决定查清楚所有内容并在此记录下来。人们也说,学习某样东西最好的方法就是尝试去教它。 ## 目录 - 在 Ubuntu Server 16.04 x64 上的 htop (https://peteris.rocks/blog/htop/#htop-on-ubuntu-server-16-04-x64) - 运行时间 (https://peteris.rocks/blog/htop/#uptime) - 负载平均值 (https://peteris.rocks/blog/htop/#load-average) - 进程 (https://peteris.rocks/blog/htop/#processes) - 进程 ID / PID (https://peteris.rocks/blog/htop/#process-id-pid) - 进程树 (https://peteris.rocks/blog/htop/#process-tree) - 进程用户 (https://peteris.rocks/blog/htop/#process-user) - 进程状态 (https://peteris.rocks/blog/htop/#process-state) - 进程时间 (https://peteris.rocks/blog/htop/#process-time) - 进程优先级和 niceness (https://peteris.rocks/blog/htop/#process-niceness-and-priority) - 内存使用 – VIRT/RES/SHR/MEM (https://peteris.rocks/blog/htop/#memory-usage-virt-res-shr-mem) - 进程 (https://peteris.rocks/blog/htop/#processes) - 附录 (https://peteris.rocks/blog/htop/#appendix) - 待办事项 (https://peteris.rocks/blog/htop/#todo) - 更新 (https://peteris.rocks/blog/htop/#updates) - 最后的说明 (https://peteris.rocks/blog/htop/#final-remarks) - T 恤 (https://peteris.rocks/blog/htop/#t-shirt) ## 在 Ubuntu Server 16.04 x64 上的 htop 下面是我将要描述的 htop 截图。 htop 截图 ## 运行时间 运行时间显示系统已运行了多长时间。你可以通过运行 `uptime` 命令来查看相同的信息: ``` $ uptime 12:17:58 up 111 days, 31 min, 1 user, load average: 0.00, 0.01, 0.05 ``` `uptime` 程序是如何知道的?它从文件 `/proc/uptime` 中读取信息。 ``` 9592411.58 9566042.33 ``` 第一个数字是系统启动以来的总秒数。第二个数字是机器处于空闲状态的时间(以秒为单位)。在多核系统上,第二个值可能大于系统总运行时间,因为它是累加值。 我是怎么知道的?我查看了 `uptime` 程序运行时打开的文件。我们可以使用 `strace` 工具来做到这一点。 ``` strace uptime ``` 会有大量输出。我们可以 `grep` 查找 `open` 系统调用。但这并不完全有效,因为 `strace` 将所有输出写到标准错误(stderr)流。我们可以用 `2>&1` 将 stderr 重定向到标准输出(stdout)流。输出如下: ``` $ strace uptime 2>&1 | grep open ... open("/proc/uptime", O_RDONLY) = 3 open("/var/run/utmp", O_RDONLY|O_CLOEXEC) = 4 open("/proc/loadavg", O_RDONLY) = 4 ``` 其中包含了我提到的文件 `/proc/uptime`。实际上,你也可以使用 `strace -e open uptime`,而无需进行 grep。 那么既然我们可以直接读取文件内容,为什么还需要 `uptime` 程序?`uptime` 的输出格式对用户友好,而秒数更适合用于自己的程序或脚本。 ## 负载平均值 除了运行时间,还有三个数字表示负载平均值。 ``` $ uptime 12:59:09 up 32 min, 1 user, load average: 0.00, 0.01, 0.03 ``` 它们来自 `/proc/loadavg` 文件。如果你再看一眼 `strace` 的输出,你会发现这个文件也被打开了。 ``` $ cat /proc/loadavg 0.00 0.01 0.03 1/120 1500 ``` 前三个列表示最近 1 分钟、5 分钟和 15 分钟的平均系统负载。第四列显示当前正在运行的进程数和总进程数。最后一列显示最近使用的进程 ID。 我们来看看最后一个数字。每当你启动一个新进程时,它会被分配一个 ID 号。进程 ID 通常是递增的,除非用尽后被重用。PID 为 1 的进程是 `/sbin/init`,它在系统启动时启动。 让我们再次查看 `/proc/loadavg` 的内容,然后在后台启动 `sleep` 命令。当它在后台启动时,会显示其进程 ID。 ``` $ cat /proc/loadavg 0.00 0.01 0.03 1/123 1566 $ sleep 10 & [1] 1567 ``` 所以 `1/123` 表示当前有一个进程正在运行或准备运行,总共有 123 个进程。 当你运行 `htop` 并看到只有一个正在运行的进程时,这意味着这个进程就是 `htop` 本身。如果你运行 `sleep 30` 并再次运行 `htop`,你会发现仍然只有一个正在运行的进程。这是因为 `sleep` 并没有在运行,它处于睡眠或空闲状态,也就是在等待某事发生。正在运行的进程是指当前在物理 CPU 上运行或等待轮到它运行在 CPU 上的进程。 如果你运行 `cat /dev/urandom > /dev/null`,它会不断生成随机字节并写入一个从不被读取的特殊文件,你会看到现在有两个正在运行的进程。 ``` $ cat /dev/urandom > /dev/null & [1] 1639 $ cat /proc/loadavg 1.00 0.69 0.35 2/124 1679 ``` 所以现在有两个正在运行的进程(随机数生成和读取 `/proc/loadavg` 内容的 `cat`),并且你会注意到负载平均值增加了。 负载平均值表示一段时间内的平均系统负载。负载数通过计算正在运行(当前运行或等待运行)和不可中断的进程(等待磁盘或网络活动)的数量得出。所以它只是一个进程的数量。 那么负载平均值就是过去 1、5 和 15 分钟内这些进程的平均数量,对吗?其实并不那么简单。负载平均值是负载数的指数加权移动平均值。来自维基百科: > 从数学上讲,这三个值都从系统启动开始始终对所有系统负载进行平均。它们都以指数方式衰减,但衰减速度不同。因此,1 分钟负载平均值将包含过去 1 分钟内 63% 的负载,加上自启动以来除去过去 1 分钟的 37% 的负载。所以,“1 分钟负载平均值仅包含过去 60 秒的活动”在技术上并不准确(因为它仍然包含过去 37% 的活动),但它主要反映的是过去一分钟的情况。 这是你期望的吗?让我们回到随机数生成的例子。 ``` $ cat /proc/loadavg 1.00 0.69 0.35 2/124 1679 ``` 虽然技术上不准确,但我这样简化负载平均值是为了更容易推理。在这个例子中,随机数生成进程是 CPU 密集型的,所以过去一分钟的负载平均值是 `1.00`,即平均有一个正在运行的进程。由于我的系统只有一个 CPU,CPU 利用率为 100%,因为我的 CPU 一次只能运行一个进程。如果我有两个核心,CPU 使用率将是 50%,因为我的计算机可以同时运行两个进程。一个具有 2 个核心、CPU 利用率 100% 的计算机的负载平均值会是 `2.00`。 你可以在 `htop` 的左上角看到你的核心或 CPU 数量,或者运行 `nproc`。 由于负载数还包括处于不可中断状态的进程,这些进程对 CPU 利用率影响不大,因此像我刚才那样从负载平均值推断 CPU 使用率并不完全正确。这也解释了为什么你可能会看到高负载平均值但 CPU 负载却不高的原因。但有像 `mpstat` 这样的工具可以显示瞬时 CPU 利用率。 ``` $ sudo apt install sysstat -y $ mpstat 1 Linux 4.4.0-47-generic (hostname) 12/03/2016 _x86_64_ (1 CPU) 10:16:20 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 10:16:21 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 10:16:22 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 10:16:23 PM all 0.00 0.00 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 # ... # 杀死 cat /dev/urandom # ... 10:17:00 PM all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 10:17:01 PM all 1.00 0.00 0.00 2.00 0.00 0.00 0.00 0.00 0.00 97.00 10:17:02 PM all 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00 ``` 那么我们为什么还要使用负载平均值呢? ``` $ curl -s https://raw.githubusercontent.com/torvalds/linux/v4.8/kernel/sched/loadavg.c | head -n 7 /* * kernel/sched/loadavg.c * * 这个文件包含了计算全局负载平均值的魔法位。这是一个愚蠢的数字,但人们认为它很重要。我们费尽心思让它在大机器和无 tick 内核上工作。 */ ``` ## 进程 在右上角,`htop` 显示总进程数和正在运行的进程数。但它写的是 *Tasks*(任务)而不是进程(Processes)。为什么?进程的另一个名称是 *task*。Linux 内核内部将进程称为任务。`htop` 使用 Tasks 而不是 Processes 可能是因为它更短,节省了一些屏幕空间。你还可以在 `htop` 中看到线程。要切换线程的可见性,请按键盘上的 `Shift` + `H`。如果你看到 `Tasks: 23, 10 thr`,表示线程是可见的。你也可以按 `Shift` + `K` 查看内核线程。当它们可见时,会显示 `Tasks: 23, 40 kthr`。 ## 进程 ID / PID 每当一个新进程启动时,它会被分配一个标识号(ID),称为进程 ID 或简写为 PID。如果你在 `bash` 中将程序放在后台运行(`&`),你会看到方括号中的作业号和 PID。 ``` $ sleep 1000 & [1] 12503 ``` 如果你错过了,可以使用 `bash` 中的 `$!` 变量,它会展开为最后一个后台进程的 ID。 ``` $ echo $! 12503 ``` 进程 ID 非常有用。它可以用来查看进程的详细信息以及控制进程。 `procfs` 是一个伪文件系统,允许用户态程序通过读取文件从内核获取信息。它通常挂载在 `/proc/`,对你来说就像一个可以用 `ls` 和 `cd` 浏览的普通目录。所有与进程相关的信息都位于 `/proc/<pid>/` 下。 ``` $ ls /proc/12503 attr coredump_filter fdinfo maps ns personality smaps task auxv cpuset gid_map mem numa_maps projid_map stack uid_map cgroup cwd io mountinfo oom_adj root stat wchan clear_refs environ limits mounts oom_score schedstat statm cmdline exe loginuid mountstats oom_score_adj sessionid status comm fd map_files net pagemap setgroups syscall ``` 例如,`/proc/<pid>/cmdline` 会给出启动进程时使用的命令。 ``` $ cat /proc/12503/cmdline sleep1000$ ``` 嗯,这不正确。原来命令被 `\0` 字节分隔。 ``` $ od -c /proc/12503/cmdline 0000000 s l e e p \0 1 0 0 0 \0 0000013 ``` 我们可以将它替换为空格或换行符 ``` $ tr '\0' '\n' < /proc/12503/cmdline sleep 1000 $ strings /proc/12503/cmdline sleep 1000 ``` 进程目录可能包含链接!例如,`cwd` 指向当前工作目录,`exe` 是执行的二进制文件。 ``` $ ls -l /proc/12503/{cwd,exe} lrwxrwxrwx 1 ubuntu ubuntu 0 Jul 6 10:10 /proc/12503/cwd -> /home/ubuntu lrwxrwxrwx 1 ubuntu ubuntu 0 Jul 6 10:10 /proc/12503/exe -> /bin/sleep ``` 这就是 `htop`、`top`、`ps` 和其他诊断工具获取进程详细信息的方式:它们从 `/proc/<pid>/` 读取信息。 ## 进程树 当你启动一个新进程时,启动该进程的进程称为父进程。新进程现在是父进程的子进程。这些关系形成树状结构。如果在 `htop` 中按 `F5`,你可以看到进程层次结构。你也可以在 `ps` 中使用 `f` 开关 ``` $ ps f PID TTY STAT TIME COMMAND 12472 pts/0 Ss 0:00 -bash 12684 pts/0 R+ 0:00 \_ ps f ``` 或 `pstree` ``` $ pstree -a init ├─atd ├─cron ├─sshd -D │ └─sshd │ └─sshd │ └─bash │ └─pstree -a ... ``` 如果你曾好奇为什么经常看到 `bash` 或 `sshd` 作为某些进程的父进程,原因如下。当你在 `bash` shell 中运行,比如 `date` 时,会发生以下过程: - `bash` 创建一个新进程,该进程是它自己的副本(使用 `fork` 系统调用) - 然后它会从可执行文件 `/bin/date` 中将程序加载到内存(使用 `exec` 系统调用) - 作为父进程的 `bash` 会等待子进程退出 因此,ID 为 1 的 `/sbin/init` 在启动时启动,它生成了 SSH 守护进程 `sshd`。当你连接到计算机时,`sshd` 会为会话生成一个进程,该进程又会启动 `bash` shell。 当我同时也想查看所有线程时,我喜欢在 `htop` 中使用树形视图。 ## 进程用户 每个进程都由一个用户拥有。用户用数字 ID 表示。 ``` $ sleep 1000 & [1] 2045 $ grep Uid /proc/2045/status Uid: 1000 1000 1000 1000 ``` 你可以使用 `id` 命令来查找该用户的名称。 ``` $ id 1000 uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm) ``` 原来 `id` 从 `/etc/passwd` 和 `/etc/group` 文件中获取这些信息。 ``` $ strace -e open id 1000 ... open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3 open("/lib/x86_64-linux-gnu/libnss_compat.so.2", O_RDONLY|O_CLOEXEC) = 3 open("/lib/x86_64-linux-gnu/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3 open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 3 open("/etc/group", O_RDONLY|O_CLOEXEC) = 3 ... ``` 这是因为名称服务交换机(NSS)配置文件 `/etc/nsswitch.conf` 指示使用这些文件来解析名称。 ``` $ head -n 9 /etc/nsswitch.conf # ... passwd: compat group: compat shadow: compat ``` `compat`(兼容模式)的值与 `files` 相同,只是允许其他特殊条目。`files` 表示数据库存储在文件中(由 `libnss_files.so` 加载)。但你也可以将用户存储在其他数据库和服务中,或者使用轻量级目录访问协议(LDAP)等。 `/etc/passwd` 和 `/etc/group` 是纯文本文件,将数字 ID 映射为人类可读的名称。 ``` $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash $ cat /etc/group root:x:0: adm:x:4:syslog,ubuntu ubuntu:x:1000: ``` `passwd`?但密码在哪里?它们实际上在 `/etc/shadow` 中。 ``` $ sudo cat /etc/shadow root:$6$mS9o0QBw$P1ojPSTexV2PQ.Z./rqzYex.k7TJE2nVeIVL0dql/:17126:0:99999:7::: daemon:*:17109:0:99999:7::: ubuntu:$6$GIfdqlb/$ms9ZoxfrUq455K6UbmHyOfz7DVf7TWaveyHcp.:17126:0:99999:7::: ``` 那一串乱码是什么? - `$6$` 是使用的密码哈希算法,此处代表 `sha512` - 随后是随机生成的盐,用于防范彩虹表攻击 - 最后是你的密码加盐后的哈希 当你运行一个程序时,它会以你的用户身份运行。即使可执行文件不属于你。如果你希望以 `root` 或其他用户身份运行程序,那么就需要用到 `sudo`。 ``` $ id uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm) $ sudo id uid=0(root) gid=0(root) groups=0(root) $ sudo -u ubuntu id uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm) $ sudo -u daemon id uid=1(daemon) gid=1(daemon) groups=1(daemon) ``` 但如果你希望以其他用户身份登录来执行各种命令呢?使用 `sudo bash` 或 `sudo -u user bash`。你将能够以该用户身份使用 shell。如果你不喜欢总是被要求输入 root 密码,只需将你的用户添加到 `/etc/sudoers` 文件中即可禁用。让我们试试: ``` $ echo "$USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -bash: /etc/sudoers: Permission denied ``` 没错,只有 root 可以这样做。 ``` $ sudo echo "$USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -bash: /etc/sudoers: Permission denied ``` 这是什么鬼?这里发生的情况是,你以 root 身份执行 `echo` 命令,但追加行到 `/etc/sudoers` 文件时仍然是以你的用户身份。通常有两种解决方法: - `echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers` - `sudo bash -c "echo '$USER ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers"` 但是,如果你的用户不在 `sudo` 组中,并且你没有一个运行 `sudo` 的 root 密码,就无法得到 root 访问权限。这有点像处于一个糟糕的电影场景中,或者你忘记密码了。你可以尝试启动到恢复模式(grub 选项),以 root 身份进行启动,并手动修复该问题。但如果你能够物理接触计算机,那就不是问题了。 如果我们坚持使用 `htop`,你可以选择按某个列排序。假设你想看到哪个进程是你的用户运行的,你可以找出所有以你的身份运行的进程。按 `F6` 并选择 `USER`。然后你可以上下滚动浏览列表。 ## 进程状态 在 `htop` 的输出中,进程状态 `S` 列代表进程的当前状态。以下是一些常见状态: - `R` - 正在运行或可运行(在运行队列中) - `S` - 可中断睡眠(等待事件完成) - `D` - 不可中断睡眠(通常与 I/O 或磁盘相关,例如 `sync` 命令) - `Z` - 僵尸进程(已终止但父进程尚未回收) - `T` - 已停止(由作业控制信号停止,如 `Ctrl+Z`,或正在被跟踪) - `t` - 跟踪停止(由调试器在跟踪进程时停止) 例如,`sleep` 命令处于 `S` 状态,因为它正在等待超时完成。但等一下,如果运行 `htop` 本身,状态栏显示的是什么状态?`htop` 很可能处于 `R` 状态,因为它在实际运行并刷新其输出。 如何手动检查进程状态?从 `/proc/<pid>/status` 文件的状态行中可以看到。或者使用 `ps` 命令,它也会显示状态。例如,`ps aux` 会显示每个进程的状态。 让我们验证一下。运行 `sleep 60 &` 然后检查其状态。在另一个终端中,运行 `ps -o pid,stat,cmd -p <sleep 的 PID>`。你会看到 `S` 状态。如果你运行 `cat /dev/urandom > /dev/null`,然后检查其 PID 的状态,你会看到 `R` 状态,因为它正在持续运行。 状态 `D` 通常发生在进程等待磁盘或网络 I/O 并不可中断时,即使进程收到信号也无法被中断。所以 D 状态的进程通常“卡在 I/O 上”。在交换情况下,如果内存不足,进入 D 状态的进程可能会变得棘手。 僵尸进程(Z)不是真正的进程,而是已经终止但尚未被父进程收回的子进程。它们几乎不占用资源,但会在进程表中占用一个条目。如果父进程没有正确回收子进程,就会产生大量僵尸进程。通常,父进程可以通过 `wait` 系统调用来回收子进程。 我见过许多次以 `Z` 状态出现在 `htop` 中的进程。它们看起来像幽灵进程。 还有更多状态位?在 `htop` 中,有时你会看到像 `Ss` 这样的状态,其中 `s` 表示该进程是会话领导者。`+` 表示它是前台进程组的成员。`l` 表示是多线程(使用 CLONE_THREAD,即 NPTL 线程)。`<` 表示高优先级(非 nice),`N` 表示低优先级(nice 值高),等等。 让我们用 `ps` 命令的 `stat` 输出来说明。 ``` $ ps -o pid,stat,cmd PID STAT CMD 12472 Ss -bash 12684 R+ ps -o pid,stat,cmd ``` 这里,`bash` 有 `Ss`:`S` 表示可中断睡眠,`s` 表示它是会话领导者(该登录会话的第一个进程)。`ps` 命令有 `R+`:`R` 表示正在运行,`+` 表示它是前台进程组的一员。 你还可以在进程状态中看到 `l` 表示多线程进程(如一些守护进程)。例如,`/lib/systemd/systemd-timesyncd` 可能显示为 `Ssl`,表示睡眠、会话领导者和多线程。 在 `htop` 中,进程状态列通常显示一个字母,如 `R`、`S`、`D`、`Z` 等,除非你配置了更详细的格式。 ## 进程时间 在 `htop` 中,`TIME` 列显示进程在 CPU 上累积运行的时间。这是进程自启动以来在用户模式和内核模式下的总 CPU 时间,以秒或分钟表示。这与墙上时间(wall clock time)不同,墙上时间是进程运行的总经过时间,包括等待 CPU、I/O 等的时间。 例如,一个进程可能已运行了 5 分钟的墙上时间,但只使用了 30 秒的 CPU 时间,因为它大部分时间都在等待磁盘 I/O。 你可以从 `/proc/<pid>/stat` 文件中获取 CPU 时间。该文件包含许多字段,其中第 14 和 15 个字段分别是用户模式下的 CPU 时间(utime)和内核模式下的 CPU 时间(stime),以时钟滴答计数。时钟滴答数每秒由 `CLK_TCK` 定义,通常是 100。所以一秒等于 100 个时钟滴答。 让我们检查一下。运行一个命令并查看其进程的 CPU 时间。 ``` $ sleep 5 & [1] 21345 $ cat /proc/21345/stat 21345 (sleep) S 15737 21345 15737 ...(许多字段) ``` 我们可以分割输出并提取第 14 和 15 个字段。 ``` $ cut -d ' ' -f 14-15 /proc/21345/stat 0 0 ``` 到现在为止,`sleep` 还没有使用任何 CPU 时间,因为它处于睡眠状态。如果运行一个耗时的计算,比如 `sha256sum /dev/zero &`,然后查看其 CPU 时间,你会看到一个正数。 `htop` 显示的是这些时钟滴答的总和转换为人类可读的格式(如 `0:00.05` 表示 5 个百分之一秒)。 ## 进程优先级和 niceness 优先级决定了系统调度器将 CPU 时间分配给进程的顺序。默认情况下,优先级为 0(在 `htop` 的 `PRI` 列中显示)。但你可以通过 `nice` 值来影响优先级,它可以从 -20(最高优先级)到 19(最低优先级)。`htop` 中的 `NI` 列显示 `nice` 值。 普通用户只能降低其进程的优先级(设置正的 nice 值)。只有 root 可以设置负的 nice 值(提高优先级)。 在 `htop` 中,你可以使用 `F7` 和 `F8` 来调整正在运行的进程的 niceness 值。当按下这些键时,`htop` 实际上调用了 `renice` 系统调用。 让我们看看如何通过命令行设置 nice 值: ``` $ nice -n 5 sha256sum /dev/zero & [1] 21400 $ ps -o pid,ni,cmd -p 21400 PID NI CMD 21400 5 sha256sum /dev/zero ``` 你也可以更改正在运行的进程的 nice 值: ``` $ renice -n 10 21400 21400 (process ID) old priority 5, new priority 10 ``` 在 `htop` 中,`PRI` 列显示的是内核使用的实际优先级(在 Linux 上称为 `static_prio`),它基于 nice 值,但可能经过其他调整。公式大约是 `PRI = 20 + NI`,但这不是绝对的,因为内核可能应用其他规则。通常,默认的 `PRI` 是 20(当 NI=0 时),范围从 1(最高)到 139(最低),具体取决于调度器。 例如,运行 `ps -eo pid,ni,pri,cmd | head` 可以显示优先级。 在 `htop` 中,当你按下 `F7` 或 `F8` 时,它会修改 `NI` 列,从而改变 `PRI`。 还有实时优先级(`RT`)用于需要即时响应的进程。在 `htop` 中,这些进程的 `PRI` 列会显示 `RT` 字样。 ## 内存使用 – VIRT/RES/SHR/MEM `htop` 的进程列表中,内存使用情况显示为几个列: - **VIRT**:虚拟内存大小。这是进程可以访问的总虚拟地址空间大小。它包含进程可能分配但尚未使用的内存、共享库、已映射的文件等。单位是字节(通常显示为 MB 或 GB)。VIRT 通常很大,因为每个进程都有 4GB 的虚拟地址空间(在 32 位系统上)或极大(在 64 位系统上)。但它不代表实际物理内存使用量。 - **RES**:常驻内存大小。这是进程当前在物理内存中的部分。它包含共享内存(如共享库)被该进程使用的部分,但仅包含实际驻留在 RAM 中的页面。RES 是实际物理内存使用的一个好指标。 - **SHR**:共享内存大小。这是 RES 中与其他进程共享的部分(例如,共享库、共享内存段)。它被计算在 RES 中,但也标记为共享。SHR 可能大于 RES?不,SHR 是 RES 的一个子集,但通常 SHR 显示的是共享内存的大小,可能包括某些匿名共享映射。 - **MEM%**:进程使用的物理内存占系统总物理内存的百分比。 这些值来自 `/proc/<pid>/status` 文件中的字段: ``` VmSize: VIRT VmRSS: RES VmStk: VIRT 中的栈大小 VmData: VIRT 中的数据段大小 VmExe: VIRT 中的代码段大小 ... VmLib: 共享库使用的内存?实际上 VmLib 是虚拟内存中库部分的大小,而共享内存的物理部分在 SHR 中。 ``` `htop` 还显示每个进程的内存使用量单位,通常为 MB 或 GB。你可以按 `M` 键按内存使用情况排序。 共享内存(SHR)有点微妙。它表示进程使用的共享内存量,但每个共享页面只被计算一次(在物理内存中),但被多个进程共享。因此,所有进程的 RES 总和可能超过物理 RAM,因为共享页面被重复计算。SHR 列让你了解多少常驻内存是共享的。 还有 `swap` 列?在 `htop` 中,默认不显示交换使用情况,但你可以添加它。在按下 `F2` 设置中,可以添加 `SWAP` 列。 要查看更详细的内存信息,可以使用 `free -m` 或 `cat /proc/meminfo`。 ## 进程 现在,让我们将这个知识应用到实际使用 `htop` 中。我将通过一些示例展示如何解释信息。 例如,如果你看到一个进程的 VIRT 很大,比如 2GB,但 RES 只有 50MB,那可能没有问题——它可能分配了大量虚拟内存但没有全部使用(如 Java 虚拟机)。如果 RES 接近物理内存总量,你可能出现了内存压力。如果交换正在使用(`free` 显示 swap used),那么系统可能正在交换,性能会下降。 如果你看到某个进程的 CPU 使用率很高,但负载平均值很低,那可能意味着该进程是 CPU 密集型的,但系统中只有少数进程竞争 CPU。如果负载平均值很高但 CPU 使用率不高,可能是由于 I/O 等待(例如,大量 D 状态进程)。 在 `htop` 中,颜色也有含义: - 绿色:CPU 使用的普通线程 - 蓝色:低优先级线程(nice > 0) - 红色:内核线程 - 黄色/橙色:虚拟化中的“时间窃取”(steal)?实际上,在 CPU 使用率图表中,颜色表示用户时间、系统时间、进程 nice 时间、空闲时间等。在 `htop` 的默认配置中,CPU 使用率图表使用颜色:用户(绿色)、系统(红色)、nice(蓝色)、空闲(灰色)、iowait(黄色)、IRQ(?)等等。 进程列表中的颜色:进程状态为 D 时显示为红色,Z 显示为亮色,等等。 ## 附录 本附录包含一些与 htop 深入相关的额外细节。 例如,`htop` 本身是如何获取信息的?它从 `/proc` 文件系统读取相同的信息。你可以通过运行 `strace htop` 并观察系统调用来验证,但会得到大量输出。相反,你可以查看 `open` 系统调用。 `htop` 还会读取 `/proc/stat`、`/proc/meminfo`、`/proc/net/dev` 等以获取系统信息。 ## 待办事项 - [x] 解释负载平均值 - [x] 解释进程状态 - [x] 解释进程

相似文章

终端全栈详解

Hacker News Top

全面解析终端、Shell、TTY、控制台及其相关概念(如POSIX和ANSI转义码)之间的区别与联系,并包含动手创建TUI的实践指导。

终端、TTY 与 Shell --- I've been curious about how terminals and shells work for a while now. I know that when I open a terminal emulator like iTerm and run `ls`, somehow the shell and the program `ls` run and print out results. But how does the terminal emulator communicate with the shell? What is a TTY? What even is a pseudoterminal? I've found a lot of existing resources on this kind of confusing or incomplete, so I'm going to try to write the clearest explanation I can. 我对终端和 Shell 的工作原理一直很好奇。我知道当我打开像 iTerm 这样的终端模拟器并运行 `ls` 时,Shell 和 `ls` 程序会运行并打印出结果。但终端模拟器是如何与 Shell 通信的呢?TTY 是什么?伪终端又是什么? 我发现现有的很多资料要么令人困惑,要么不够完整,所以我打算尽力写出最清晰的解释。 --- ## some history: physical terminals Before we talk about terminal emulators, let's discuss their predecessor: physical terminals. The very first computers were programmed not with a terminal but with physical switches. You would flip the switches to enter a binary program and see the output on some LEDs. Then computers got terminals. The earliest terminals were teletypes. These were basically fancy typewriters: they had a keyboard that you could type commands to the computer on, and a printer that would print out the results. Later, CRT screens replaced the printers. But the terminals still worked the same way: you type a character on the keyboard, the character gets sent to the computer, the computer sends the character back (this is called "echoing"), and the character gets displayed on the screen. The terminal itself wasn't doing a lot of computing – it was mainly just a way to communicate with the computer. Here's a picture of a VT100 terminal, which was introduced by DEC in 1978. This terminal was very important historically because it introduced support for ANSI escape codes (more on those later). ## 一些历史:物理终端 在谈论终端模拟器之前,我们先来聊聊它的前身:物理终端。 最早的计算机并不是通过终端来编程的,而是通过物理开关。你需要拨动开关来输入二进制程序,并通过一些 LED 灯查看输出结果。 后来,计算机有了终端。最早的终端是电传打字机(teletype)。这些基本上就是高级打字机:它们有一个键盘,可以向计算机输入命令,还有一台打印机,用于打印输出结果。 再后来,CRT 屏幕取代了打印机。但终端的工作方式仍然相同:你在键盘上输入一个字符,字符被发送到计算机,计算机再将字符发回(这称为"回显"),然后字符显示在屏幕上。终端本身并不承担太多计算工作——它主要只是一种与计算机通信的方式。 这是 VT100 终端的图片,由 DEC 于 1978 年推出。这款终端在历史上非常重要,因为它引入了对 ANSI 转义码的支持(后面会详细介绍)。 --- ## what's a TTY? TTY is short for "teletypewriter" – basically just another name for "terminal". In Linux, if you look at `/dev/`, you'll see a bunch of TTY devices. Historically, these TTY devices corresponded to physical terminals plugged into the computer. Each physical terminal connected to the computer had a corresponding TTY device in `/dev/`, and the operating system would communicate with those physical terminals through those TTY devices. ## 什么是 TTY? TTY 是"teletypewriter"(电传打字机)的缩写——基本上就是"终端"的另一种叫法。在 Linux 中,如果你查看 `/dev/` 目录,会看到一堆 TTY 设备。 从历史上看,这些 TTY 设备对应的是连接到计算机的物理终端。每台连接到计算机的物理终端在 `/dev/` 下都有一个对应的 TTY 设备,操作系统通过这些 TTY 设备与物理终端进行通信。 --- ## terminal emulators These days, most of us aren't using physical terminals. We're using terminal emulators – programs like iTerm2, GNOME Terminal, Terminal.app, etc. A terminal emulator emulates a physical terminal in software. Terminal emulators need to: 1. Emulate some physical terminal (usually VT100 or a variant of it), including ANSI escape codes. For example, if your shell sends the escape code `\x1b[34m`, that means "change the text colour to blue" and your terminal emulator needs to render the following text as blue. 2. Show the terminal on your screen 3. Accept keyboard/mouse input and send it to the shell Now, how does the terminal emulator communicate with the shell? ## 终端模拟器 如今,我们大多数人都不再使用物理终端,而是使用终端模拟器——如 iTerm2、GNOME Terminal、Terminal.app 等程序。 终端模拟器在软件层面模拟物理终端。终端模拟器需要: 1. 模拟某种物理终端(通常是 VT100 或其变体),包括 ANSI 转义码。例如,如果你的 Shell 发送转义码 `\x1b[34m`,这意味着"将文本颜色改为蓝色",你的终端模拟器需要将随后的文本以蓝色渲染。 2. 在屏幕上显示终端界面 3. 接收键盘/鼠标输入并将其发送给 Shell 那么,终端模拟器是如何与 Shell 通信的呢? --- ## pseudoterminals The way terminal emulators communicate with shells is through a **pseudoterminal** (also called a pty). A pseudoterminal is a fake terminal: it's an API that looks to programs like a terminal but is just two file descriptors in a Linux program: a "master" (or "controller") side and a "secondary" (or "replica") side. The way pseudoterminals work is: 1. The terminal emulator opens the master side of a pseudoterminal 2. The shell (and its child processes, like `ls`) gets the secondary side of the pseudoterminal as its stdin/stdout/stderr 3. The terminal emulator reads characters from the master side (which is the shell's output) and displays them on the screen 4. The terminal emulator also writes characters to the master side (which is the keyboard input) and the shell reads them from the secondary side Here's a diagram: ``` keyboard --> terminal emulator --> PTY master --> PTY secondary --> shell screen <-- terminal emulator <-- PTY master <-- PTY secondary <-- shell ``` The PTY (pseudoterminal) is managed by the operating system – the terminal emulator writes to the master side and the OS is responsible for making the data available on the secondary side. ## 伪终端 终端模拟器与 Shell 通信的方式是通过**伪终端**(也称为 pty)。 伪终端是一种虚拟终端:它是一个 API,对程序来说看起来像一个终端,但在 Linux 程序中实际上只是两个文件描述符:一个"主"(master,也称 controller)端和一个"从"(secondary,也称 replica)端。 伪终端的工作方式如下: 1. 终端模拟器打开伪终端的主端 2. Shell(及其子进程,如 `ls`)将伪终端的从端作为其 stdin/stdout/stderr 3. 终端模拟器从主端读取字符(即 Shell 的输出)并显示在屏幕上 4. 终端模拟器也向主端写入字符(即键盘输入),Shell 从从端读取这些字符 下面是一个示意图: ``` 键盘输入 --> 终端模拟器 --> PTY 主端 --> PTY 从端 --> Shell 屏幕显示 <-- 终端模拟器 <-- PTY 主端 <-- PTY 从端 <-- Shell ``` PTY(伪终端)由操作系统管理——终端模拟器向主端写入数据,操作系统负责将数据在从端提供给 Shell。 --- ## what is the TTY driver? But there's something else in the middle: the TTY driver. The TTY driver sits between the PTY master and secondary. ``` keyboard --> terminal emulator --> PTY master --> TTY driver --> PTY secondary --> shell screen <-- terminal emulator <-- PTY master <-- TTY driver <-- PTY secondary <-- shell ``` The TTY driver does a few things: 1. Line editing. The TTY driver keeps a buffer of the current line. When you press backspace, the TTY driver removes the last character from the buffer, and the character gets deleted from the terminal screen. When you press enter, the TTY driver sends the buffer to the shell. This is called "cooked mode" and is the default mode for terminals. 2. Echo. The TTY driver echoes characters back to the terminal emulator, so that the characters you type are displayed on the screen. 3. Handling special characters like Ctrl+C, Ctrl+Z, and Ctrl+D: when you press Ctrl+C, the TTY driver sends a SIGINT signal to the foreground process group, which usually causes the process to exit. The reason for the TTY driver is historical: back in the day, the TTY driver was a kernel module that handled communication with physical terminals. The pseudoterminal was designed to fit into the same framework. So the OS has a TTY driver even when there's no physical terminal. ## 什么是 TTY 驱动? 但中间还有另一个组件:TTY 驱动。TTY 驱动位于 PTY 主端和从端之间。 ``` 键盘输入 --> 终端模拟器 --> PTY 主端 --> TTY 驱动 --> PTY 从端 --> Shell 屏幕显示 <-- 终端模拟器 <-- PTY 主端 <-- TTY 驱动 <-- PTY 从端 <-- Shell ``` TTY 驱动负责以下几件事: 1. **行编辑**。TTY 驱动维护当前行的缓冲区。当你按下退格键时,TTY 驱动从缓冲区中删除最后一个字符,该字符也会从终端屏幕上消失。当你按下回车键时,TTY 驱动将缓冲区内容发送给 Shell。这称为"熟模式"(cooked mode),是终端的默认模式。 2. **回显**。TTY 驱动将字符回显给终端模拟器,使你输入的字符显示在屏幕上。 3. **处理特殊字符**,如 Ctrl+C、Ctrl+Z 和 Ctrl+D:当你按下 Ctrl+C 时,TTY 驱动向前台进程组发送 SIGINT 信号,通常会导致进程退出。 TTY 驱动存在的原因是历史性的:早年间,TTY 驱动是一个内核模块,负责处理与物理终端的通信。伪终端的设计是为了融入同一套框架。因此,即使没有物理终端,操作系统中也有 TTY 驱动。 --- ## raw mode and cooked mode As I mentioned above, in "cooked mode", the TTY driver does line editing. Programs can put the TTY into "raw mode" which disables line editing. Most interactive programs (like `vim`, `python`, `bash`) put the terminal into raw mode and handle all the keyboard input themselves, because they need to do their own line editing (e.g. handling arrow keys for history in bash) and they don't want the TTY driver to intercept special characters like Ctrl+C. For example, when you run vim, vim puts the terminal into raw mode. If you press Ctrl+C in vim, vim will handle the Ctrl+C itself (by cancelling the current operation), instead of the TTY driver sending a SIGINT signal to vim and killing it. ## 原始模式与熟模式 如前所述,在"熟模式"(cooked mode)下,TTY 驱动负责行编辑。程序可以将 TTY 设置为"原始模式"(raw mode),这会禁用行编辑。 大多数交互式程序(如 `vim`、`python`、`bash`)会将终端切换到原始模式,自行处理所有键盘输入,因为它们需要实现自己的行编辑功能(例如 bash 中用方向键浏览历史记录),并且不希望 TTY 驱动拦截 Ctrl+C 等特殊字符。 例如,当你运行 vim 时,vim 会将终端设置为原始模式。如果你在 vim 中按下 Ctrl+C,vim 会自行处理这个 Ctrl+C(取消当前操作),而不是让 TTY 驱动向 vim 发送 SIGINT 信号并将其终止。 --- ## ANSI escape codes Earlier I mentioned ANSI escape codes. These are sequences of characters that terminals interpret as commands. For example: - `\x1b[34m` means "change the text colour to blue" - `\x1b[0m` means "reset the text colour to the default" - `\x1b[1m` means "bold text" - `\x1b[2J` means "clear the screen" The `\x1b` is the escape character (ASCII code 27). The `[` character is the "Control Sequence Introducer". The rest of the sequence is the command. When a shell script uses `echo -e "\x1b[34mhello\x1b[0m"`, it's telling the terminal emulator to display "hello" in blue. ## ANSI 转义码 前面我提到了 ANSI 转义码。这些是终端将其解释为命令的字符序列。例如: - `\x1b[34m` 表示"将文本颜色改为蓝色" - `\x1b[0m` 表示"将文本颜色重置为默认值" - `\x1b[1m` 表示"粗体文本" - `\x1b[2J` 表示"清屏" `\x1b` 是转义字符(ASCII 码 27)。`[` 字符是"控制序列引导符"(Control Sequence Introducer)。序列的其余部分是具体命令。 当 Shell 脚本使用 `echo -e "\x1b[34mhello\x1b[0m"` 时,它是在告诉终端模拟器以蓝色显示"hello"。 --- ## what's a shell? I've been talking about shells throughout, but I haven't defined what a shell is. A shell is a program that: 1. Reads commands from the user (from a TTY or from a script file) 2. Runs those commands 3. Shows the output to the user Examples of shells are bash, zsh, fish, and sh. How does a shell run a command? The shell uses the `fork` and `exec` system calls. `fork` creates a copy of the shell process, and `exec` replaces the copy with the new program. The shell then waits for the program to finish. The shell also sets up the child process's stdin/stdout/stderr, and handles job control (foreground/background processes). ## 什么是 Shell? 我在整篇文章中一直在提 Shell,但还没有给出定义。Shell 是一个程序,它: 1. 从用户处读取命令(来自 TTY 或脚本文件) 2. 执行这些命令 3. 将输出显示给用户 Shell 的例子有 bash、zsh、fish 和 sh。 Shell 是如何运行命令的?Shell 使用 `fork` 和 `exec` 系统调用。`fork` 创建 Shell 进程的一个副本,`exec` 将该副本替换为新程序。然后 Shell 等待程序执行完毕。 Shell 还负责设置子进程的 stdin/stdout/stderr,以及处理任务控制(前台/后台进程)。 --- ## what happens when you run `ls`? Let's walk through what happens when you type `ls` in a terminal emulator. 1. You type `l` on the keyboard 2. The terminal emulator receives the `l` character and sends it to the PTY master 3. The TTY driver receives the `l` character and (in cooked mode) echoes it back to the PTY master 4. The terminal emulator receives the echoed `l` character and displays it on the screen 5. Same for `s` and then Enter 6. When Enter is pressed, the TTY driver sends the buffer `ls\n` to the PTY secondary 7. The shell reads `ls\n` from the PTY secondary 8. The shell forks and execs `ls` 9. `ls` writes its output to its stdout (which is the PTY secondary) 10. The TTY driver passes the output through to the PTY master 11. The terminal emulator reads the output from the PTY master and displays it on the screen ## 运行 `ls` 时发生了什么? 让我们来梳理一下在终端模拟器中输入 `ls` 时发生的事情。 1. 你在键盘上按下 `l` 2. 终端模拟器接收到字符 `l`,并将其发送到 PTY 主端 3. TTY 驱动接收到字符 `l`,并(在熟模式下)将其回显给 PTY 主端 4. 终端模拟器接收到回显的字符 `l`,并将其显示在屏幕上 5. `s` 和回车键同理 6. 当按下回车键时,TTY 驱动将缓冲区内容 `ls\n` 发送到 PTY 从端 7. Shell 从 PTY 从端读取 `ls\n` 8. Shell 执行 fork 和 exec 来运行 `ls` 9. `ls` 将其输出写入 stdout(即 PTY 从端) 10. TTY 驱动将输出传递到 PTY 主端 11. 终端模拟器从 PTY 主端读取输出,并将其显示在屏幕上 --- ## what about SSH? SSH is interesting because it involves two computers. Here's what happens when you run `ssh user@server` and then run `ls`: 1. You type `ls` on the keyboard 2. Your terminal emulator sends `ls` to the PTY master on your local computer 3. The TTY driver echoes the characters back 4. The terminal emulator displays the echoed characters on the screen 5. The **SSH client** reads the characters from the PTY master and sends them over the network to the SSH server 6. The SSH server receives the characters and writes them to the PTY secondary on the remote computer 7. The shell running on the remote computer reads the characters from the PTY secondary 8. The shell runs `ls` and writes the output to the PTY secondary on the remote computer 9. The SSH server reads the output from the PTY master on the remote computer and sends it over the network to the SSH client 10. The SSH client writes the output to the PTY secondary on the local computer 11. The terminal emulator on the local computer reads the output from the PTY master and displays it on the screen Wait, that doesn't make sense. Let me reconsider. The SSH client is running on the local computer and the SSH server is running on the remote computer. The SSH client replaces the shell in the local PTY. ## SSH 呢? SSH 很有趣,因为它涉及两台计算机。下面是当你运行 `ssh user@server` 后再运行 `ls` 时发生的事情: 1. 你在键盘上输入 `ls` 2. 你的终端模拟器将 `ls` 发送到本地计算机的 PTY 主端 3. TTY 驱动将字符回显 4. 终端模拟器将回显的字符显示在屏幕上 5. **SSH 客户端**从 PTY 主端读取字符,并通过网络将其发送到 SSH 服务器 6. SSH 服务器接收字符,并将其写入远程计算机的 PTY 从端 7. 在远程计算机上运行的 Shell 从 PTY 从端读取字符 8. Shell 运行 `ls`,并将输出写入远程计算机的 PTY 从端 9. SSH 服务器从远程计算机的 PTY 主端读取输出,并通过网络发送给 SSH 客户端 10. SSH 客户端将输出写入本地计算机的 PTY 从端 11. 本地计算机上的终端模拟器从 PTY 主端读取输出,并将其显示在屏幕上 等等,这说不通。让我重新理一理。SSH 客户端运行在本地计算机上,SSH 服务器运行在远程计算机上。SSH 客户端在本地 PTY 中取代了 Shell 的角色。 --- Here's a better diagram for what happens with SSH: **Local computer:** ``` keyboard --> terminal emulator --> PTY master --> TTY driver --> PTY secondary --> ssh client screen <-- terminal emulator <-- PTY master <-- TTY driver <-- PTY secondary <-- ssh client ``` **Remote computer:** ``` (network) --> ssh server --> PTY master --> TTY driver --> PTY secondary --> shell (network) <-- ssh server <-- PTY master <-- TTY driver <-- PTY secondary <-- shell ``` So the SSH client is the "shell" from the perspective of the local terminal emulator, and the SSH server is the "terminal emulator" from the perspective of the remote shell. The SSH client and server communicate over the network using the SSH protocol. One interesting thing about SSH is that both the local and remote computers have a PTY. The local PTY handles things like Ctrl+C on the local side (in addition to the remote PTY handling Ctrl+C on the remote side). 下面是 SSH 场景下更清晰的示意图: **本地计算机:** ``` 键盘输入 --> 终端模拟器 --> PTY 主端 --> TTY 驱动 --> PTY 从端 --> ssh 客户端 屏幕显示 <-- 终端模拟器 <-- PTY 主端 <-- TTY 驱动 <-- PTY 从端 <-- ssh 客户端 ``` **远程计算机:** ``` (网络) --> ssh 服务器 --> PTY 主端 --> TTY 驱动 --> PTY 从端 --> Shell (网络) <-- ssh 服务器 <-- PTY 主端 <-- TTY 驱动 <-- PTY 从端 <-- Shell ``` 因此,从本地终端模拟器的角度来看,SSH 客户端扮演的是"Shell"的角色;而从远程 Shell 的角度来看,SSH 服务器扮演的是"终端模拟器"的角色。SSH 客户端和服务器通过 SSH 协议在网络上进行通信。 SSH 有一个有趣的地方:本地和远程计算机各自都有一个 PTY。本地 PTY 在本地端处理 Ctrl+C 等操作(同时远程 PTY 在远程端也处理 Ctrl+C)。 --- ## what about `tmux`? `tmux` is a terminal multiplexer: it lets you have multiple terminal sessions in a single window. It's also interesting from a PTY perspective. When you run `tmux`, `tmux` creates a new PTY for each window/pane and runs a shell in each PTY. `tmux` itself is the "terminal emulator" for those shells, but `tmux` is also running inside a PTY (the one created by your actual terminal emulator). So if you're running `tmux` inside iTerm, the chain is: ``` iTerm --> PTY --> tmux --> PTY --> shell ``` `tmux` acts as both a terminal emulator (for the shells it manages) and as a program (from iTerm's perspective). This is why `tmux` can keep running even if you close iTerm – the `tmux` process and its shells are attached to PTYs that don't depend on iTerm. When you reconnect to `tmux`, it re-attaches to the existing PTYs. ## `tmux` 呢? `tmux` 是一个终端复用器:它允许你在单个窗口中拥有多个终端会话。从 PTY 的角度来看,它也很有趣。 当你运行 `tmux` 时,`tmux` 为每个窗口/面板创建一个新的 PTY,并在每个 PTY 中运行一个 Shell。`tmux` 本身充当这些 Shell 的"终端模拟器",但 `tmux` 自身也运行在一个 PTY 中(由你实际使用的终端模拟器创建的那个)。 因此,如果你在 iTerm 中运行 `tmux`,整个链路是: ``` iTerm --> PTY --> tmux --> PTY --> shell ``` `tmux` 既充当终端模拟器(对其管理的 Shell 而言),又作为一个普通程序(从 iTerm 的角度而言)。 这就是为什么即使你关闭 iTerm,`tmux` 仍然可以继续运行——`tmux` 进程及其 Shell 附加在不依赖 iTerm 的 PTY 上。当你重新连接到 `tmux` 时,它会重新附加到已有的 PTY 上。 --- ## summary Here's a summary of what we've covered: - **Physical terminals**: the historical predecessors to terminal emulators. Teletypes that sent characters to the computer and received characters back. - **TTY**: short for teletypewriter. In Linux, TTY devices are in `/dev/`. Historically they corresponded to physical terminals. - **Terminal emulators**: programs that emulate physical terminals in software (iTerm2, GNOME Terminal, etc.) - **Pseudoterminals (PTYs)**: fake terminals – an API that looks like a terminal to programs. Has a master side (used by the terminal emulator) and a secondary side (used by the shell). - **TTY driver**: sits between PTY master and secondary. Handles line editing (cooked mode), echo, and special characters (Ctrl+C, etc.) - **Raw mode vs cooked mode**: in cooked mode, the TTY driver does line editing. In raw mode, the program handles all input itself. - **ANSI escape codes**: sequences of characters that terminals interpret as commands (e.g. change text colour, clear screen). - **Shell**: a program that reads commands, runs them, and shows output. Uses `fork` and `exec` system calls. Examples: bash, zsh, fish. ## 总结 以下是我们所涵盖内容的总结: - **物理终端**:终端模拟器的历史前身。向计算机发送字符并接收字符的电传打字机。 - **TTY**:teletypewriter 的缩写。在 Linux 中,TTY 设备位于 `/dev/` 下。历史上对应物理终端。 - **终端模拟器**:在软件层面模拟物理终端的程序(iTerm2、GNOME Terminal 等)。 - **伪终端(PTY)**:虚拟终端——一种对程序来说看起来像终端的 API。有主端(由终端模拟器使用)和从端(由 Shell 使用)。 - **TTY 驱动**:位于 PTY 主端和从端之间。处理行编辑(熟模式)、回显和特殊字符(Ctrl+C 等)。 - **原始模式与熟模式**:熟模式下,TTY 驱动负责行编辑;原始模式下,程序自行处理所有输入。 - **ANSI 转义码**:终端将其解释为命令的字符序列(例如更改文本颜色、清屏)。 - **Shell**:读取命令、执行命令并显示输出的程序。使用 `fork` 和 `exec` 系统调用。例如:bash、zsh、fish。

Lobsters Hottest

# 终端背后的秘密:终端模拟器、TTY 与 Shell 作为开发者,我们每天都在使用终端。但你有没有想过,当你打开一个"终端窗口"时,究竟发生了什么?你输入的字符经过怎样的路径,最终变成命令的输出? 大多数人将整个体验笼统地称为"终端",但实际上,这背后存在三个截然不同的层次,它们各司其职、协同工作。理解这三个层次,不仅能帮助你排查奇怪的问题,更能让你对自己每天使用的工具有更深刻的认识。 ## 三个层次概览 在深入细节之前,先来认识这三位主角: 1. **终端模拟器(Terminal Emulator)**:你在屏幕上看到的那个窗口 2. **TTY / 伪终端(Pseudo-terminal)**:操作系统内核中的一个抽象层 3. **Shell**:真正解释并执行你命令的程序 它们的关系可以这样理解: ``` 你的键盘输入 ↓ 终端模拟器(GUI 窗口) ↓ 伪终端(内核层,PTY) ↓ Shell(bash / zsh / fish …) ↓ 命令输出原路返回 ``` --- ## 第一层:终端模拟器 ### 它是什么 终端模拟器是一个**图形界面程序**。常见的有 iTerm2、GNOME Terminal、Alacritty、Windows Terminal、Kitty 等。它的核心职责是: - 渲染文字到屏幕上 - 捕获你的键盘输入 - **模拟**老式硬件终端(如 VT100、xterm)的行为 "模拟器"这个词至关重要。在个人电脑普及之前,终端是一台真实的硬件设备——一台带显示器和键盘的哑终端,通过串口连接到大型主机。现代的终端模拟器用软件重现了那台硬件的行为。 ### 它做什么 当你按下键盘上的一个键,终端模拟器会将这个按键**转换成字节序列**,然后写入伪终端。例如,方向键 `↑` 通常被转换为转义序列 `\x1b[A`。 反过来,当程序向终端写入转义序列时,终端模拟器负责**解释**这些序列,并作出相应动作,例如: ``` \x1b[31m → 将后续文字渲染为红色 \x1b[2J → 清空整个屏幕 \x1b[1A → 光标上移一行 ``` 这就是为什么同一个程序(比如 `vim`)在不同的终端模拟器里,行为可能略有差异——不同的模拟器对转义序列的支持程度不同。 ### 一个常见的误解 很多人以为终端模拟器"运行"了 Shell。实际上,终端模拟器只是**启动**了 Shell,并为它提供一个可以读写的伪终端接口。之后,终端模拟器和 Shell 是两个独立运行的进程,通过内核中的伪终端相互通信。 --- ## 第二层:TTY 与伪终端(PTY) 这是三层中最容易被忽视、也最难理解的一层,但它是整个系统的**核心枢纽**。 ### TTY 的历史渊源 TTY 是 **Teletype**(电传打字机)的缩写。在计算机的早期历史中,人们用电传打字机与计算机交互——输入字符,打印机打印出响应。这套设备通过串口连接到计算机。 Unix 操作系统从一开始就将这些串口设备抽象为文件,放在 `/dev/tty*` 路径下。程序只需要读写这些文件,就能与用户交互,而不需要关心底层是什么硬件。 ### 伪终端(PTY)的出现 当我们转向图形界面,不再有真实的硬件串口时,Unix 需要一种方式来保持这套抽象,同时支持终端模拟器这样的软件。于是**伪终端(Pseudo-Terminal,PTY)**诞生了。 PTY 是内核提供的一对相互连接的虚拟设备: - **主端(master side)**:终端模拟器持有这一端 - **从端(slave side)**:Shell 及其子进程持有这一端,对应 `/dev/pts/0`、`/dev/pts/1` 这样的设备文件 你可以把它想象成一条**双向管道**,但这条管道远比普通管道聪明——它内置了一个叫做**行规程(line discipline)**的模块。 ### 行规程:被遗忘的功能 行规程是 TTY 层中最精妙的设计之一。它在内核中处理大量"低级"的终端行为,让每个 Shell 和应用程序不必自己重新实现这些功能: | 功能 | 说明 | |------|------| | **回显(Echo)** | 将你输入的字符显示在屏幕上 | | **行编辑** | `Backspace` 删除字符,`Ctrl+U` 清除整行 | | **信号生成** | `Ctrl+C` 发送 `SIGINT`,`Ctrl+Z` 发送 `SIGTSTP` | | **规范模式** | 缓冲整行输入,直到你按下回车 | 这解释了一个有趣的现象:即使在 Shell 还没启动、或 Shell 崩溃的情况下,`Backspace` 键依然"有效"——因为删除字符这个操作是由**内核**在 TTY 层处理的,而不是由 Shell 处理的。 ### 用命令亲眼验证 在终端里输入: ```bash tty ``` 你会看到类似这样的输出: ``` /dev/pts/3 ``` 这就是当前 Shell 正在使用的伪终端从端设备文件。你可以用 `ls -la /dev/pts/` 查看系统中所有活跃的伪终端。 更有趣的是,你可以直接向另一个终端窗口写入文字: ```bash # 在终端 A 中运行 tty,假设输出是 /dev/pts/3 # 然后在终端 B 中运行: echo "你好,终端 A" > /dev/pts/3 ``` 终端 A 的屏幕上会直接出现这段文字——这直观地展示了 TTY 设备文件的本质。 --- ## 第三层:Shell Shell 是你**最熟悉**却往往被与终端混为一谈的那一层。 ### Shell 是一个普通进程 Shell(bash、zsh、fish 等)本质上是一个**普通的用户空间进程**。它的特别之处在于: - 它将 `/dev/pts/N` 作为自己的**标准输入(stdin)**、**标准输出(stdout)**和**标准错误(stderr)** - 它读取你输入的文本,解析为命令,然后通过 `fork()` + `exec()` 创建子进程来执行这些命令 - 子进程同样继承了对 TTY 设备的连接 ### 原始模式 vs 规范模式 Shell 在启动后,通常会让 TTY 层工作在**规范模式(canonical mode)**下。此时行规程会帮 Shell 缓冲输入、处理退格键等,Shell 直接读取一整行已处理好的输入。 但当你运行 `vim` 或其他全屏程序时,情况就不同了。`vim` 会将 TTY 切换到**原始模式(raw mode)**: - 行规程的大部分处理被**绕过** - 每个按键立即传递给应用程序 - 应用程序自行决定如何处理每一个字节 这就是为什么在 `vim` 里,`Backspace` 的行为可以被完全自定义,而在普通 Shell 提示符下,`Backspace` 的行为是由内核保证的。 ### Shell 不是终端 这个区别在实践中非常重要。考虑以下场景: ```bash # 这会失败,因为 ssh 命令没有分配 TTY ssh user@host vim /etc/hosts # 这会成功,-t 参数强制分配一个伪终端 ssh -t user@host vim /etc/hosts ``` `vim` 需要一个真实的 TTY 才能工作(它需要将终端切换到原始模式)。当 `ssh` 不分配 TTY 时,远端的 `vim` 无法正常运行——因为它的标准输入只是一个普通管道,而不是一个 TTY 设备。 --- ## 三层如何协同工作:一次完整的按键之旅 让我们追踪一次按键——假设你在 Shell 提示符下输入字母 `l`,准备输入 `ls` 命令: ``` 1. 你按下键盘上的 "l" 键 2. 操作系统检测到按键事件 3. 终端模拟器收到按键事件, 将其编码为字节 0x6C(ASCII 'l'), 写入 PTY 主端 4. 内核 TTY 层(行规程)收到这个字节: - 将字节追加到输入缓冲区 - 因为开启了回显,将 'l' 写回 PTY 主端 5. 终端模拟器从 PTY 主端读取到回显的 'l', 将其渲染到屏幕上 (这就是你"看到"自己输入的原因) 6. Shell 此时还没有收到任何东西—— 它在等待一个完整的行(规范模式) 7. 你继续输入 "s",然后按下回车 8. 行规程收到回车,将完整的行 "ls\n" 送入 Shell 可读取的队列 9. Shell 的 read() 调用返回,得到 "ls\n" 10. Shell 解析命令,fork() 出子进程, exec() 执行 /bin/ls 11. ls 将输出写入其标准输出(同一个 PTY 从端) 12. 内核 TTY 层将输出传递到 PTY 主端 13. 终端模拟器读取输出,渲染到屏幕上 ``` 整个过程在毫秒之内完成,但涉及了用户空间和内核空间之间多次切换,以及三个独立组件的协作。 --- ## 为什么这些知识对开发者有用 理解这三个层次,能帮助你解释和解决很多实际问题: **1. 为什么有些程序检测到自己的输出被重定向后,行为会改变?** ```bash ls --color=auto # 输出彩色 ls --color=auto | cat # 输出变成黑白 ``` `ls` 通过检查标准输出是否是 TTY(使用 `isatty()` 系统调用)来决定是否输出颜色代码。管道不是 TTY,所以颜色被关闭。 **2. 为什么 `sudo` 有时会提示输入密码失败?** `sudo` 需要从 TTY 读取密码。如果你在一个没有 TTY 的环境中运行 `sudo`(例如某些 CI 环境),它就无法工作。 **3. 为什么 `screen` 和 `tmux` 能"保持"会话?** `screen` 和 `tmux` 本身就是终端模拟器(运行在终端里的终端模拟器)。它们创建自己的 PTY,Shell 连接到这个 PTY。当你断开 SSH 连接时,真正的终端模拟器消失了,但 `tmux` 创建的 PTY 和连接到它的 Shell 仍然存在于服务器上。 **4. 理解 `stty` 命令** `stty` 命令直接操作 TTY 层的设置: ```bash stty -echo # 关闭回显(输入密码时脚本里常用) stty echo # 重新开启回显 stty -a # 查看当前 TTY 的所有设置 ``` --- ## 总结 | 层次 | 代表 | 职责 | |------|------|------| | 终端模拟器 | iTerm2, GNOME Terminal, Alacritty | 图形渲染、转义序列解释、按键捕获 | | TTY / PTY | `/dev/pts/N`(内核模块) | 数据路由、行规程、信号生成 | | Shell | bash, zsh, fish | 命令解析、进程管理、脚本执行 | 这三层各自解决了不同的问题,通过清晰的接口相互协作。Unix 的设计哲学在这里体现得淋漓尽致:每个组件做好一件事,通过标准化的接口(文件描述符、设备文件)组合在一起,形成一个灵活而强大的整体。 下次当你打开终端窗口,看到那个闪烁的光标时,你知道自己看到的不只是一个"终端"——而是三个精心设计的软件层,在内核与用户空间之间默默协作的成果。