linux学习(一)
六月开始做 EDR 开发实训,写到 ring3 网络采集器的时候卡住了。
需求听起来很简单:知道机器上每条 TCP 连接属于哪个进程。 就是做个 pid-to-connection 的映射。
但如果不能用 eBPF,也不能插内核模块,纯靠用户态能做吗?
能。内核在 /proc 里已经把答案摊在桌面上了,只不过需要用 inode 做桥梁,把两张表 JOIN 起来。
这篇文章是我在补这条「归因链路」时积累的知识。不是字典式的 Linux 教程——是从一个问题出发,把 inode → socket → /proc → fd → 权限模型这一整条线串起来。
起点:/proc/net/tcp
第一步,得先知道机器上现在有哪些 TCP 连接。
Linux 内核的 TCP 协议栈在 /proc/net/tcp 暴露了一个只读接口。每次 cat 它,内核会实时遍历所有活动 socket,把状态拍出来:
1 | sl local_address rem_address st tx_queue rx_queue uid timeout inode |
第一眼看是乱码——IP 地址是十六进制还是反的。8B80A8C0 按字节倒序 → C0.A8.80.8B → 192.168.128.139。小端序。端口 0016 转十进制 → 22(SSH)。
解码公式:/proc/net/tcp 里的 IP 和端口都是小端十六进制。本地地址 = IP:PORT 倒序解码即可。
字段里最关键的是最后一列——inode。同一个数字 654952 会同时出现在 /proc/net/tcp 和 /proc/<pid>/fd 里。它就是连接和进程之间的桥梁。
为什么是 inode?
inode(索引节点)是文件系统里每个文件、目录、设备的底层身份标识。它不存文件名——文件名存在父目录的数据块里(”文件名 → inode号” 映射表)。inode 自己存的是元数据:权限位、时间戳、数据块位置。
但 socket 不是文件——它是内核内存里的缓冲区 + 协议栈,不落磁盘。那它哪来的 inode?
内核专门在内存里虚拟了一个文件系统,叫 sockfs(Socket File System)。每次创建 socket,sockfs 为它分配一个 inode 号。内核结构长这样:
1 | struct socket_alloc { |
外壳是给文件系统的,内核走同一套 VFS 接口操作一切资源。内芯才是真正处理 TCP 收发的东西。
这就是「一切皆文件」的真正含义——不是”所有东西都是磁盘文件”,而是”所有资源都挂到 VFS 上,用同一套路径解析和权限检查”。
/proc//fd:归因的最后一环
有了 socket inode 号,下一步就是找它属于谁。
每个进程的 /proc/<pid>/fd 目录是其文件描述符表的用户空间镜像。进去 ls -l:
1 | $ sudo ls -l /proc/1/fd |
socket:[41282]——冒号后面的数字就是 socket inode 号。跟 /proc/net/tcp 的 inode 列一比对,匹配上了——这条连接属于这个 PID。
整个归因链就是:/proc/net/tcp inode ↔ /proc/<pid>/fd 符号链接 ↔ PID。两张表 JOIN,查出一个 inode,就能确定一条连接属于哪个进程。
内核里完整的链路更长:task_struct(进程 PCB)→ files_struct(fd 数组)→ struct file(每次 open/socket 创建)→ dentry & inode(VFS 层映射)。但在 /proc 层面,socket:[inode] 三个字已经把底层细节压缩干净了。
权限从哪来?
归因拿到了 PID,下一个问题是:读取其他进程的 /proc/<pid>/fd 需要什么权限?
答案在进程凭据里。内核用 task_struct 表示每个进程,其中 cred 子结构体记录身份:
1 | struct cred { |
ruid 和 euid 平时相等。SUID 机制会打破它——比如 passwd 命令要改 /etc/shadow(只有 root 能写),内核通过 SUID 位让进程的 euid 临时变成 0(root),但 ruid 保持 1000,记录真正触发者。
权限检查:匹配即停止
内核在做文件权限判断时走 generic_permission(),逻辑极简——匹配即停止:
进程 euid == 文件所有者 UID→ 用 User 权限位判断,结束- 进程在文件的组内 → 用 Group 权限位判断,结束
- 前两步都没命中 → 用 Other 权限位判断
三层只走一层,不会叠加。文件所有者的权限覆盖一切——你是文件 owner,就算权限是 ---(000),Group 和 Other 的权限也完全不看。
inode 的 i_mode 字段存着所有权限位。低 9 位就是 [User(rwx)] [Group(rwx)] [Other(rwx)],比如 755 = owner 全权 + 其他人只读执行。
目录的 r 和 x,是两条内核路径
这一部分可能是整条学习链里最颠覆直觉的——目录的 r 和 x 在内核里走的是两条完全不同的检查路径。
| 权限 | 对目录 | 内核路径 |
|---|---|---|
r |
读取目录下的文件名列表 | getdents64() → MAY_READ |
x |
穿透目录,访问子项 | link_path_walk() → MAY_EXEC |
一个极端场景:目录权限 d--x--x--x(只有 x,没有 r)。
1 | $ ls /blind_dir # ❌ 失败——getdents64 需要 MAY_READ |
你能用 cd 进去,能精确访问已知文件名的资源,但不能 ls 看列表。Shell 的通配符展开依赖 readdir(),底层用 getdents64——没有 r 权限,* 直接废掉。
这种「只可穿透、不可窥探」的目录叫盲盒目录(Execute-Only Directory),典型权限 711。核心结论:访问它下面的文件,必须提供绝对精确的文件名。通配符、Tab 补全全部失效。
在多租户隔离场景里,/var/www/userA/ 设 711,Nginx 用精确路径穿透正常服务,但被攻破的 userB 无法 ls 窥探其他租户。getdents64 和 link_path_walk 这条内核分叉线,是这一切的底层基础。
学到了什么
从「怎么知道一条 TCP 连接属于哪个进程」出发,倒推出来的东西远远超出了预期:
/proc/net/tcp的每一列都有信息量,小端十六进制解码是基本功- inode 不是磁盘文件的专利——sockfs 让 socket 也拥有了 inode 身份
- fd 表 + inode 是用户态归因的唯一可靠手段:两张 proc 表 JOIN
- euid ≠ ruid,VFS 只用 euid,SUID 位靠这个机制工作
- 目录的 r 和 x 是两码事:
getdents64vslink_path_walk,MAY_READ vs MAY_EXEC - 盲盒目录把这条内核差异用到了极致——有 x 无 r,穿透但不暴露
本文内容整理自个人知识库(Obsidian vault),每个概念的详细展开、代码片段和参考文献均已存入
wiki/目录。本文属于「Linux 学习合集」系列第一篇,后续随实训进度持续补充。




