/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
通过 /proc 观察 Linux 进程:内存、线程与文件描述符
前几篇已经在 Ubuntu 20.04、Buildroot 和 QEMU ARM64 中运行、调试并追踪用户态程序。本篇继续从 Linux 用户空间观察进程运行时状态:通过 /proc/<PID> 查看内存统计和映射、线程列表、文件描述符,并解释常见数字的单位和边界。
本篇使用一个独立的 proc_demo 示例进程,持有一个普通文件、一个未连接的 TCP socket、一块被实际触碰的匿名映射,并启动两个 worker 线程。观察完成后只终止这个示例进程,不修改现有 debug_demo 服务。
一、实验流程与环境
Ubuntu 20.04 x86_64 主机
Buildroot AArch64 GCC 编译 proc_demo
| SSH 上传,QEMU hostfwd 127.0.0.1:2222
v
QEMU ARM64 guest
proc_demo:4 MiB 匿名映射 + 文件 FD + socket FD + 2 个 worker
| 读取 /proc/<PID>/status、statm、maps、task、fd
v
解释 VmRSS、页数、TID、FD 和 socket inode
| Ubuntu 主机 | Ubuntu 20.04 x86_64 |
| Buildroot | 2025.02.8,~/buildroot-2025.02.8 |
| Buildroot 输出目录 | ~/buildroot-output-arm64-demo |
| 外部树 | ~/buildroot-external-demo |
| 目标系统 | QEMU ARM64、BusyBox init、可写 ext2 rootfs |
| guest SSH | Dropbear,Ubuntu 127.0.0.1:2222 转发到 guest TCP 22 |
默认命令在 Ubuntu 主机执行;标注 QEMU guest 的命令才在 ARM64 系统内执行。本文读取的是 guest 的 /proc,不要把 Ubuntu 主机的 /proc/<PID> 与 guest 中同一个数字 PID 混为一谈。
二、确认 procfs 已挂载
本文接续前文 strace 实验。若 QEMU guest 仍在运行,继续使用该实例,不要重复启动;若已关闭,按前文命令重启。由于本实验主机的 $HOME 和默认临时目录只读,重启前在 Ubuntu 主机设置 export TMPDIR=/dev/shm,并在 QEMU 启动参数中加入 -snapshot,使 guest 写入使用临时 overlay。启动后在 QEMU guest 控制台以 root 登录,再检查 /proc:
mount | grep ' on /proc '
ls /proc/self
本次 guest 中的实测关键输出如下:
# mount | grep ' on /proc '
proc on /proc type proc (rw,relatime)
ls /proc/self 的输出中包含 status、maps、task 和 fd 等条目,确认这些 proc 文件和目录可见;具体列表可能随内核版本略有不同。
BusyBox init 通常会挂载 procfs。若 /proc/self/status 等文件不存在,先检查 /proc 是否挂载;不要把本节的检查命令误在 Ubuntu 主机执行。
三、创建带有可观察资源的示例程序
在 Ubuntu 主机创建源码。前文的 Buildroot 输出目录和 SSH 私钥仍从 $HOME 读取;由于本实验主机的 $HOME 和默认临时目录均不可写,源码、编译产物及 Bash heredoc 临时文件分别改用 /dev/shm/proc-inspection-demo 和 /dev/shm。这些都是临时路径,主机重启后会清空。程序会打开 guest 中的 /tmp/proc_demo.data,建立一个未连接的 IPv4 TCP socket,分配并逐页写入 4 MiB 匿名内存,然后启动两个等待中的 worker 线程:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
export PROC_WORK="/dev/shm/proc-inspection-demo"
export SSH_KEY="$HOME/.ssh/buildroot-qemu-dropbear-ed25519"
export TMPDIR=/dev/shm
if [ ! -d /dev/shm ] || [ ! -w /dev/shm ]; then
printf '/dev/shm is unavailable or not writable\\n' >&2
exit 1
fi
mkdir -p "$PROC_WORK"
cat > "$PROC_WORK/proc_demo.c" <<'EOF'
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <unistd.h>
#define MAP_BYTES (4U * 1024U * 1024U)
#define WORKER_COUNT 2
static void *worker_main(void *argument)
{
(void)argument;
for (;;) {
pause();
}
return NULL;
}
int main(void)
{
const char marker[] = "proc demo file\\n";
int file_fd = open("/tmp/proc_demo.data", O_CREAT | O_EXCL | O_RDWR, 0600);
if (file_fd < 0) {
perror("open proc_demo.data");
return EXIT_FAILURE;
}
if (write(file_fd, marker, sizeof(marker) – 1) != (ssize_t)(sizeof(marker) – 1)) {
perror("write proc_demo.data");
return EXIT_FAILURE;
}
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) {
perror("socket");
return EXIT_FAILURE;
}
long page_size = sysconf(_SC_PAGESIZE);
if (page_size <= 0) {
fprintf(stderr, "invalid page size\\n");
return EXIT_FAILURE;
}
void *mapping = mmap(NULL, MAP_BYTES, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapping == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
for (size_t offset = 0; offset < MAP_BYTES; offset += (size_t)page_size) {
((volatile unsigned char *)mapping)[offset] = 0xA5;
}
pthread_t workers[WORKER_COUNT];
for (int index = 0; index < WORKER_COUNT; ++index) {
int result = pthread_create(&workers[index], NULL, worker_main, NULL);
if (result != 0) {
fprintf(stderr, "pthread_create: %s\\n", strerror(result));
return EXIT_FAILURE;
}
}
printf("proc_demo ready: pid=%ld file_fd=%d socket_fd=%d "
"page_size=%ld mapping=%p mapping_bytes=%u threads=%d\\n",
(long)getpid(), file_fd, socket_fd, page_size,
mapping, MAP_BYTES, WORKER_COUNT + 1);
fflush(stdout);
for (;;) {
pause();
}
}
EOF
文件以 O_EXCL 创建;若 guest 中已存在同名文件,程序会报错退出,不会截断原文件。匿名映射逐页写入,是为了让这些页实际进入进程的常驻集;只调用 mmap() 而不访问映射页,RSS 未必随映射大小增加。示例使用 pause() 保持线程存活,以便在进程运行时查看 /proc。
四、交叉编译并上传到 guest
以下命令在 Ubuntu 主机执行;即使另开了主机终端,也请先重新设置路径。Buildroot 输出目录只用于读取交叉编译器,编译产物写入可写的 /dev/shm 工作目录。-pthread 同时提供线程编译和链接选项:
export DEMO_OUT="$HOME/buildroot-output-arm64-demo"
export PROC_WORK="/dev/shm/proc-inspection-demo"
export SSH_KEY="$HOME/.ssh/buildroot-qemu-dropbear-ed25519"
export CROSS_GCC="$(find "$DEMO_OUT/host/bin" -maxdepth 1 \\
-name 'aarch64-buildroot-linux-*-gcc' -print -quit)"
if [ -z "$CROSS_GCC" ] || [ ! -x "$CROSS_GCC" ]; then
printf 'Buildroot AArch64 compiler not found\\n' >&2
else
"$CROSS_GCC" -Wall -Wextra -O0 -g3 -pthread \\
"$PROC_WORK/proc_demo.c" -o "$PROC_WORK/proc_demo" &&
file "$PROC_WORK/proc_demo"
fi
file 应显示 ARM AArch64 ELF。随后从 Ubuntu 主机通过前文已配置的 SSH 上传:
ssh -i "$SSH_KEY" -p 2222 root@127.0.0.1 \\
'cat > /root/proc_demo && chmod 755 /root/proc_demo' \\
< "$PROC_WORK/proc_demo"
确认 guest 文件存在:
ssh -i "$SSH_KEY" -p 2222 root@127.0.0.1 \\
'ls -l /root/proc_demo && test -x /root/proc_demo && echo executable'
五、启动进程并取得 PID
以下命令在 QEMU guest 执行。让程序在后台运行,PID 保存在当前 shell 的 proc_pid 变量中:
/root/proc_demo &
proc_pid=$!
sleep 1
printf 'proc_pid=%s\\n' "$proc_pid"
grep -E '^(Name|Pid|PPid|Threads|VmSize|VmRSS|RssAnon|RssFile|VmData|VmStk):' "/proc/$proc_pid/status"
本轮实测的 proc_demo ready 输出中,page_size=4096、mapping_bytes=4194304、threads=3。PID、FD 和映射地址会由系统分配,不要照抄示例值。Threads 应为 3(主线程加两个 worker);若不是,检查程序启动输出和线程创建是否成功。
可检查当前 shell 仍持有目标 PID:
case "$proc_pid" in
''|*[!0-9]*) echo 'Invalid PID; stop.' >&2 ;;
*) test -r "/proc/$proc_pid/status" && echo 'process is present' ;;
esac
本轮实测输出为:
process is present
六、观察内存统计与映射
1. /proc/<PID>/status
cat "/proc/$proc_pid/status"
本轮实测的关键字段如下,其他与本节无关的状态和信号字段略去:
Name: proc_demo
State: S (sleeping)
Pid: 138
PPid: 125
Uid: 0 0 0 0
VmSize: 22680 kB
VmRSS: 5176 kB
RssAnon: 4172 kB
RssFile: 1004 kB
RssShmem: 0 kB
VmData: 20688 kB
VmStk: 132 kB
Threads: 3
这些是本轮运行的快照,PID、父 PID 和内存统计会随环境变化;本次 RssAnon、RssFile 与 RssShmem 之和为 VmRSS。
重点字段:
| VmSize | 进程虚拟地址空间总量 | kB;包含已映射但未必驻留 RAM 的区域 |
| VmRSS | 当前常驻集估计值 | kB;不是进程独占的物理内存精确值 |
| RssAnon | 匿名常驻页 | kB;本例逐页写入的映射通常会反映在这里 |
| RssFile | 文件映射的常驻页 | kB;包括可执行文件和共享库等映射页 |
| RssShmem | 共享内存常驻页 | kB;在支持该字段的内核上,RSS 约由 RssAnon、RssFile 和 RssShmem 组成 |
| VmData | 数据段及相关数据映射统计 | kB;不要简单等同于 C 全局变量大小 |
| VmStk | 主线程栈大小统计 | kB;不代表所有线程栈总量 |
| Threads | 当前线程数 | 个;本例预期为主线程加两个 worker |
这些字段是内核统计快照,部分值会随访问模式和内核版本变化,不应据此推导精确的进程独占内存。
2. /proc/<PID>/statm
cat "/proc/$proc_pid/statm"
printf 'page_size_bytes=%s\\n' "$(getconf PAGESIZE)"
本轮实测输出如下:
# cat "/proc/$proc_pid/statm"
5670 1294 251 2 0 5205 0
-sh: getconf: not found
page_size_bytes=
getconf 未安装是当前 BusyBox guest 的配置情况,不影响读取 statm。本轮 proc_demo ready 输出的页大小为 4096 字节,因此前两列分别换算为 5670 × 4 = 22680 kB 虚拟内存和 1294 × 4 = 5176 kB 常驻内存,与前面 status 中的 VmSize、VmRSS 相符;共享页数 251 × 4 = 1004 kB,也与本轮 RssFile 相符。statm 各列单位都是页,依次表示总虚拟页数、常驻页数、共享页数、代码页数、库页数、数据加栈页数、脏页数。Linux 2.6.0 及之后版本保留了 lib 列,但通常恒为 0,不能用它估算共享库占用。其中部分列为快速统计值,可能不精确;若 guest 没有 getconf,使用程序启动时打印的页大小,不要直接假定不同架构的页大小相同。
3. /proc/<PID>/maps
cat "/proc/$proc_pid/maps"
本轮实测中,程序打印的映射起始地址为 0xffffb5600000,在 maps 中对应:
ffffb5600000-ffffb5a00000 rw-p 00000000 00:00 0
起止地址相差 0x400000,即 4 MiB;rw-p 表示可读写的私有映射,末尾没有文件路径,符合匿名映射。本轮地址由内核分配,其他运行中可能不同。每行表示一个虚拟地址区间及权限标记,如 r-xp 表示可读、可执行、私有映射,s 表示共享映射。maps 展示虚拟映射,不是物理内存分布图。
七、观察线程和文件描述符
1. 线程
printf 'Threads in status: '
grep '^Threads:' "/proc/$proc_pid/status"
for task_dir in /proc/"$proc_pid"/task/*; do
[ -d "$task_dir" ] || continue
printf 'TID=%s name=' "${task_dir##*/}"
cat "$task_dir/comm"
done
本轮实测输出如下:
Threads: 3
TID=138 name=proc_demo
TID=140 name=proc_demo
TID=141 name=proc_demo
本轮主线程 TID 138 与进程 PID 相同;另外两个 TID 是 worker。TID 由内核分配,其他运行中会变化。
/proc/<PID>/task/ 下每个子目录对应一个线程,目录名是 TID。主线程的 TID 通常与进程 PID 相同,其余为 worker 的 TID;这些编号由内核运行时分配,不应写成固定值。目录数量应与 Threads: 大致一致。
2. 文件描述符
ls -l "/proc/$proc_pid/fd"
for fd_path in /proc/"$proc_pid"/fd/*; do
[ -e "$fd_path" ] || continue
printf 'fd=%s target=' "${fd_path##*/}"
readlink "$fd_path"
done
本轮实测的 readlink 输出如下:
fd=0 target=/dev/console
fd=1 target=/dev/console
fd=2 target=/dev/console
fd=3 target=/tmp/proc_demo.data
fd=4 target=socket:[76]
本轮标准输入、输出和错误都连接到 QEMU 控制台;FD 3 指向示例文件,FD 4 指向 socket。FD 编号和 socket inode 会随运行变化。
文件描述符是进程内的索引,不是全系统唯一编号。通常 0、1、2 分别是标准输入、输出、错误;示例还会有指向 /tmp/proc_demo.data 的普通文件 FD,以及形如 socket:[inode] 的 socket FD。实际 FD 数字可能不同,应以程序启动时打印的值和 /proc/<PID>/fd 为准。
socket:[inode] 只显示 socket inode,不能单独说明远端地址或连接状态;本例 socket 尚未调用 connect(),因此只是已创建、尚未连接的 socket。/proc/<PID>/fdinfo/<FD> 可提供该描述符的标志、当前位置等附加信息:
cat "/proc/$proc_pid/fdinfo/3"
本轮实测输出如下:
pos: 15
flags: 0400002
mnt_id: 21
ino: 20
pos: 15 表示文件偏移为 15 字节,与程序写入的 proc demo file\\n 长度相符。flags 以八进制显示;flags、mnt_id 和 ino 会随内核及文件系统环境变化。
将 3 替换为程序输出的文件 FD。不要把 FD 编号当作固定值。
八、结束示例进程
完成观察后,在启动进程的 同一个 QEMU guest shell 中确认 PID 变量仍有效,再只终止本次 proc_demo:
case "$proc_pid" in
''|*[!0-9]*) echo 'Invalid PID; refusing to signal.' >&2 ;;
*)
if [ "$(readlink "/proc/$proc_pid/exe")" = /root/proc_demo ]; then
kill -TERM "$proc_pid"
wait "$proc_pid"
if [ ! -e "/proc/$proc_pid/exe" ]; then
rm -f /tmp/proc_demo.data
else
echo 'proc_demo is still present; leaving its data file untouched.' >&2
fi
else
echo 'PID does not refer to /root/proc_demo; refusing to signal.' >&2
fi
;;
esac
这里通过 /proc/<PID>/exe 再次核对目标,避免误向其他进程发送信号。不要对 debug_demo、PID 1 或不确认身份的 PID 执行 kill。
本轮实测中,wait 确认 proc_demo 已被终止,shell 显示 Terminated;后续 rm -f 成功时不输出内容。[1]+ 等 job 编号由 shell 分配,可能不同。
九、常见问题与观察边界
| guest 中没有 /proc/<PID> | 进程可能已退出,或命令在 Ubuntu 主机而非 QEMU guest 执行 |
| Threads 不是 3 | 检查 proc_demo ready 输出及 pthread_create 错误;进程必须仍在运行 |
| VmRSS 没有恰好增加 4 MiB | RSS 包含其他运行时页且是估算快照;确认映射逐页触碰,并结合 statm/maps 判断 |
| /proc/<PID>/fd 中没看到预期项 | 检查程序启动输出中的 FD 编号、进程是否仍存在,以及权限 |
| getconf: not found | 使用程序启动时打印的页大小;statm 本身仍以页为单位 |
| Ubuntu 主机创建 proc_demo.c 提示 Read-only file system | 本实验主机的 $HOME 只读;按第三节将 PROC_WORK 设为 /dev/shm/proc-inspection-demo,并确认 /dev/shm 可写 |
| Bash 提示 cannot create temp file for here-document | 在 Ubuntu 主机先设置 export TMPDIR=/dev/shm,再从头重新运行第三节的源码创建命令 |
| kill 提示权限错误 | 只能由进程所有者或 root 终止;本例 guest 以 root 启动示例进程 |
/proc 反映的是读取瞬间的状态,进程继续运行时数值可能变化。RSS/共享页不是精确的进程独占物理内存;FD 编号也仅在该进程内有意义。进程退出后 /proc/<PID> 随即消失,因此应先读取,再清理。
本文覆盖 QEMU ARM64 Linux 用户态进程,不涉及内核线程、驱动内部内存、真实板卡硬件资源或生产系统性能结论。示例输出为预期,实际字段和数值以运行环境为准。
十、小结
/proc/<PID>/status 适合快速查看进程身份、线程数和内存概况;statm 以页数报告虚拟内存与常驻集估算;maps 展示虚拟地址区间;task/ 枚举线程;fd/ 将进程内 FD 映射到文件、设备或 socket。把这些信息与实际程序行为结合,能更有依据地回答“进程用了多少内存、启动了几个线程、打开了哪些资源”,但应牢记这些接口描述的是运行时快照,不是所有资源的精确归属。




