按文档读取不断变化的进程 procfs 视图
Linux procfs 通过文件系统接口暴露进程和内核信息。许多入口在读取时生成内容,而不是保存普通磁盘文件字节;每个入口都有具体含义、权限模型和格式。连续读取多个入口,不会冻结进程,也不会得到事务一致快照。因此,有用的观察必须对应正确进程实例和正确解释,不能因为内容都来自 proc,就假定它们是同一时刻的完整状态。 坏掉的样子 普通文件假设可能立刻失效。proc 条目报告大小为零,却仍然返回有用内容;逐进程目录可能随着进程退出消失,数字 PID 之后也可能指向另一个实例。读取失败不一定表示系统安装损坏,某个编号也不会永久代表同一工作负载。生命周期变化本来就是观测需要处理的条件,应避免把正常退出当成异常环境。 不同入口需要按各自格式解析。status 包含有标签字段,stat 则使用固定字段顺序,其中括号包住的命令名可以包含空格和右括号。直接把整行按空白拆分,会让预期字段错位。cmdline 与 environ 使用空字符分隔,普通按行或空格切分,会破坏参数和值的边界。解析方式必须来自接口文档,而不是文件看起来像文本这一点。 视图本身也有语义边界。environ 描述程序执行时建立的环境区域,不保证是所有后续环境修改的实时字典;命令行内容也可以被进程修改。描述符链接以 deleted 结尾,不表示打开对象已经消失。这些入口提供具体状态证据,却不是可以互相替代的身份凭据或访问保证,需要先明确每项观察到底回答什么问题。 快速验证 把脚本保存为 procview.py,在一次性 Linux 环境中运行 python3 procview.py。实测使用 Linux 6.12.76 和 Python 3.11.2。脚本启动一个受控子进程,显式提供少量环境值,只读取自己及该子进程的 proc 条目,不写入内核调节接口: from pathlib import Path from tempfile import TemporaryDirectory import json,os,subprocess,sys worker=r''' from pathlib import Path import ctypes,json,os,sys assert ctypes.CDLL(None).prctl(15,b'worker ) sample',0,0,0)==0 os.environ['XB_PROC_TEST']='changed-after-start' path=Path(sys.argv[1]);path.write_bytes(b'fixture-data') with path.open('rb') as handle: info=os.fstat(handle.fileno());path.unlink() print(json.dumps({'fd':handle.fileno(),'device':info.st_dev,'inode':info.st_ino,'current_environment':os.environ['XB_PROC_TEST']}),flush=True) assert sys.stdin.readline()=='done\n' ''' with TemporaryDirectory(prefix='xbcatm-proc-') as directory: path=Path(directory)/'data' process=subprocess.Popen([sys.executable,'-c',worker,str(path),'argument with spaces'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,env={'PATH':'/usr/bin:/bin','XB_PROC_TEST':'fixture-value'}) try: metadata=json.loads(process.stdout.readline());root=Path('/proc')/str(process.pid) status_path=root/'status';status_text=status_path.read_text() assert status_path.stat().st_size==0 and len(status_text)>0 status=dict(line.split(':',1) for line in status_text.splitlines() if ':' in line) assert int(status['Pid'])==process.pid and int(status['PPid'])==os.getpid() raw=(root/'stat').read_text();pid_text,rest=raw.split(' (',1);comm,fields=rest.rsplit(') ',1) fields=fields.split();assert int(pid_text)==process.pid and comm=='worker ) sample' assert int(fields[1])==os.getpid() and int(fields[19])>0 assert raw.split()[2] not in ('R','S','D','T','Z') print('proc files can have size zero but readable data; stat comm can contain spaces and a closing parenthesis') args=(root/'cmdline').read_bytes().rstrip(b'\0').split(b'\0') environment=(root/'environ').read_bytes().rstrip(b'\0').split(b'\0') assert args[-1]==b'argument with spaces' and b'XB_PROC_TEST=fixture-value' in environment assert metadata['current_environment']=='changed-after-start' assert os.readlink('/proc/self')==str(os.getpid()) print('cmdline and environ use NUL separators; environ retains the initial value here; self resolves for the reader') entry=root/'fd'/str(metadata['fd']);target=os.readlink(entry);actual=entry.stat() assert target==str(path)+' (deleted)' and not path.exists() assert (actual.st_dev,actual.st_ino)==(metadata['device'],metadata['inode']) assert entry.read_bytes()==b'fixture-data' print('a deleted pathname remains inspectable through an open descriptor subject to access checks') output,error=process.communicate('done\n',timeout=5) assert process.returncode==0,error assert not root.exists() print('after the controlled child exits and is reaped, its proc directory disappears') finally: if process.poll() is None:process.kill();process.wait() 子进程把短名称设为带空格和右括号的 worker ) sample,打开临时文件,再删除路径但保持描述符打开。随后它仅报告样本元数据并等待,让控制进程拥有一份已知存活对象。这个名称刻意触发 stat 格式边界,使解析验证不依赖“进程名碰巧没有空格”的理想情况。 ...