CPython `pwd` 模块完全指南:Unix 密码数据库的结构、API 与源码级实现

CPython `pwd` 模块完全指南:Unix 密码数据库的结构、API 与源码级实现 CPythonpwd模块完全指南Unix 密码数据库的结构、API 与源码级实现【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonpwd是 CPython 标准库中访问Unix 用户账户 / 密码数据库即/etc/passwd所代表的系统用户信息源的专用模块。本指南将围绕官方文档 Doc/library/pwd.rst 展开先讲清passwd数据库条目的 7 字段结构再逐一剖析getpwuid()、getpwnam()、getpwall()三个函数与各自的参数、返回值和异常行为并结合 Modules/pwdmodule.c 与 Lib/test/test_pwd.py 展示其底层基于getpwuid_r/getpwnam_r/getpwent系统调用的实现细节与线程安全策略最终给出可直接上手的实战代码。模块定位一个面向 Unix 用户数据库的只读接口pwd模块提供对Unix 用户账户与密码数据库password database的访问官方文档明确其在全部 Unix 版本上可用可用性标注为Unix, not WASI, not iOS即不适用于 WASI 与 iOS 平台。该模块本质是对 C 标准库pwd.h中struct passwd相关系统调用的薄封装。它提供的是只读查询能力只能查找与枚举用户条目不能修改任何用户信息。在 CPython 的 Unix 专属服务章节中它与组数据库接口grp模块互为姊妹篇参见 Doc/library/unix.rst 的模块索引。从构建系统看pwd属于随解释器一起构建的扩展模块它在 Modules/Setup.bootstrap.in 中被注册启用条件则写在 configure.ac#L8507 中要求目标系统提供getpwuid()或getpwuid_r()PY_STDLIB_MOD([pwd], [], [test $ac_cv_func_getpwuid yes -o $ac_cv_func_getpwuid_r yes])一旦系统缺少上述系统调用例如某些受限运行时环境该模块就不会被编译进解释器。struct_passwd元组式条目结构与 7 个字段密码数据库的每条记录在 Python 中被表示为一个元组式对象tuple-like objectpwd.struct_passwd。它的字段与 C 语言struct passwd见pwd.h的成员一一对应。官方文档用下面这张表定义了字段布局索引属性Attribute含义Meaning0pw_name登录名Login name1pw_passwd可选加密密码Optional encrypted password2pw_uid数值用户 IDNumerical user ID3pw_gid数值组 IDNumerical group ID4pw_gecos用户名或注释字段User name or comment field5pw_dir用户主目录User home directory6pw_shell用户命令解释器User command interpreter其中pw_uid与pw_gid是整数其余字段均为字符串。在源码中这套字段被声明为一个结构序列struct sequence见 Modules/pwdmodule.c#L17-L39 中struct_pwd_type_fields与struct_pwd_type_desc的定义它明确规定了 7 个字段、顺序与各自的描述user name、password、user id、group id、real name、home directory、shell program。结构序列的双重访问方式由于struct_passwd继承自PyStructSequence它可以像元组一样按下标索引也可以按属性名访问两种方式完全等价 import pwd, os u pwd.getpwuid(os.getuid()) u[0], u.pw_name # 索引与属性等价 (root, root) len(u) # 固定为 7 7 name, pw, uid, gid, gecos, home, shell u # 支持解包该对象的官方 docstring见 Modules/pwdmodule.c#L28-L32也明确说明This object may be accessed either as a tuple of (pw_name, pw_passwd, pw_uid, pw_gid, pw_gecos, pw_dir, pw_shell) or via the object attributes。因此它具备元组全部行为可迭代、可索引、可切片、可比较、可哈希字段全部可哈希时。各字段的语义与易踩的坑pw_name登录名用户登录使用的名字。不同系统在本地文件与 NIS/LDAP 目录等来源之间可能存在差异某些特殊网络条目如 NIS 中以开头的行在遍历时需要留意——这正是 Lib/test/test_pwd.py#L53 在测试中跳过pw_name为空或以开头的条目的原因。pw_passwd加密密码与 shadow password 机制传统 Unix 中该字段存放使用DES 派生算法加密的密码。但现代绝大多数 Unix 发行版采用了shadow password影子密码系统真正的加密哈希被移入普通用户不可读的/etc/shadow文件而pw_passwd字段通常只保留一个占位符——星号*或字母x。因此文档特别提醒pw_passwd字段是否含有任何有用信息完全取决于具体系统system-dependent程序不应假设可以从这里取到可验证的哈希。从源码看该字段的处理还带有平台差异在 Modules/pwdmodule.c#L96-L100 中仅当定义了HAVE_STRUCT_PASSWD_PW_PASSWD且非 Android!defined(__ANDROID__)时才读取p-pw_passwd否则一律填充空字符串。pw_uid/pw_gid数值标识符分别是用户的数值用户 ID 与主组 IDPython 侧以整数呈现。构建时通过_PyLong_FromUid/_PyLong_FromGid从 C 层的uid_t/gid_t转换而来见 Modules/pwdmodule.c#L101-L102因此其数值范围与平台 C 类型一致。注意同一个 uid 在数据库中可能出现多条重复记录NIS 等来源下并不罕见这也是Lib/test/test_pwd.py注释中专门说明“不能简单地用pwd.getpwuid(e.pw_uid) e反向校验”的原因。pw_gecos注释字段可能是NoneGECOS 字段通常存放真实姓名或补充注释历史上源自通用电气综合操作系统。文档将其描述为字符串但从源码的mkpwent()转换逻辑可见一个易被忽略的边界情况字段在 C 层为**空指针NULL**时会被映射为None而非空字符串见 Modules/pwdmodule.c#L83-L84 的SET_STRING宏。这与测试中的断言一致——Lib/test/test_pwd.py#L26 允许pw_gecos的类型是str或NoneType。所以严谨的代码应对pw_gecos做空值判断。pw_dir/pw_shell主目录与登录 Shellpw_dir是用户主目录的绝对路径pw_shell是登录后启动的命令解释器常为/bin/sh、/bin/bash等也可能是/usr/sbin/nologin这类禁止登录的 shell。两者通常总是存在但个别系统同样可能出现空指针故字段也可能为None。模块 API 详解pwd模块只导出三个函数见官方文档pwd.getpwuid(uid)按数值用户 ID返回对应的密码数据库条目struct_passwd对象。 pwd.getpwuid(0) pwd.struct_passwd(pw_nameroot, pw_passwdx, pw_uid0, pw_gid0, pw_gecosroot, pw_dir/root, pw_shell/bin/bash)参数必须是可以转换为uid_t的整数传入浮点数等非整数会抛出TypeError。当 uid超出平台uid_t范围时如2**128内部转换溢出会被转译并抛出KeyError参见 Modules/pwdmodule.c#L142-L147 的异常映射逻辑。当 uid 在范围内但查无此用户时抛出KeyError。常见的配合写法是用os.getuid()拿到当前进程的真实用户 ID 再查询。pwd.getpwnam(name)按用户名返回对应的密码数据库条目。 pwd.getpwnam(root) pwd.struct_passwd(pw_nameroot, pw_passwdx, pw_uid0, ...)参数必须是str传入bytes如broot会抛出TypeError这一点与许多其它接受路径参数的 Unix 接口不同依据测试 Lib/test/test_pwd.py#L68。名字中不能包含内嵌空字节\x00否则抛出ValueError源码在 Modules/pwdmodule.c#L236-L238 先做编码再显式检查空字节防止 C 字符串截断引发安全隐患。名字会按文件系统编码处理无法编码的字符如孤立代理项会触发UnicodeEncodeError。查无此用户时抛出KeyError。pwd.getpwall()返回所有可用密码数据库条目组成的列表条目顺序不保证arbitrary order。 all_users pwd.getpwall() len(all_users) 35在大型组织如 LDAP 目录中getpwall()未必覆盖全部真实用户它只反映当前系统可枚举到的数据源。由于struct_passwd是可变性受限的不可变对象列表本身可以放心遍历。该函数仅在系统提供getpwent()系列函数时可用详见下文源码解析这也是 Lib/test/test_pwd.py#L9 用skipUnless(hasattr(pwd, getpwall), ...)跳过测试的原因。源码级解析它如何调用系统库三个查询入口背后的系统调用链pwd的底层实现在 Modules/pwdmodule.c三函数与 C 库的对应关系如下Python API首选 C 调用回退方案getpwuid(uid)getpwuid_r()可重入版无_r版时退化为getpwuid()getpwnam(name)getpwnam_r()可重入版无_r版时退化为getpwnam()getpwall()setpwent()getpwent()endpwent()系统无getpwent()时不提供该函数可重入版本的缓冲策略与 GIL 释放对于getpwuid_r/getpwnam_r模块使用动态扩容缓冲区的经典模式见 Modules/pwdmodule.c#L148-L182 与 Modules/pwdmodule.c#L245-L273先通过sysconf(_SC_GETPW_R_SIZE_MAX)探测推荐缓冲区大小若系统未定义该值返回-1则回退到宏DEFAULT_BUFFER_SIZE1024 字节用PyMem_RawRealloc分配缓冲区调用_r函数若返回ERANGE缓冲区不足则将缓冲区大小加倍后重试直到成功或触发内存上限保护。值得指出的是整个_r调用发生在Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS之间即执行期间会释放 GIL从而避免在查询本地账号数据库如慢速网络目录时阻塞其它 Python 线程。非可重入版本如何保证线程安全getpwuid()、getpwnam()、getpwent()等传统接口会返回指向静态存储区的指针任何后续调用都可能覆盖先前结果POSIX 并不要求它们线程安全。模块源码对此有专门处理见 Modules/pwdmodule.c#L66-L69 的注释与pwd_db_mutex互斥量在回退路径中先用PyMutex_Lock(pwd_db_mutex)加锁再调用非线程安全的getpwuid/getpwnam在错误返回或成功构造struct_passwd之后立即解锁getpwall()的遍历同样全程持有该互斥量把setpwent()/getpwent()/endpwent()三次调用包裹为一个不可分割的临界区避免流式枚举状态被并发破坏。多解释器与自由线程支持pwd模块还声明了较新的模块槽位Modules/pwdmodule.c#L374-L380支持多解释器下的每解释器 GILPy_MOD_PER_INTERPRETER_GIL_SUPPORTED并在自由线程构建free-threaded buildPy_MOD_GIL_NOT_USED下不使用 GIL。每个解释器通过pwdmodulestate持有自己独立的StructPwdType类型对象其文档字符串、字段与清理由模块级traverse/clear/free钩子统一管理。CPython 还专门提供了 Lib/test/test_free_threading/test_pwd.py用 10 个线程并发执行test_pwd的用例来验证并发调用getpwall()、getpwnam()、getpwuid()的健壮性。字符串编解码策略所有字符串字段通过PyUnicode_DecodeFSDefault从文件系统编码解码回 Python 字符串查询名则用PyUnicode_EncodeFSDefault编码见 Modules/pwdmodule.c#L84 与 Modules/pwdmodule.c#L234。这意味着用户名相关的编解码采用与文件系统一致的编码方案现代 Linux 上通常为 UTF-8 surrogateescape用户名中若包含无法按常规规则解码的字节Python 侧可能出现代理字符surrogate——这是标准库内部一贯的文件系统编码语义查询由os.listdir这类 API 得到的结果时应保持一致预期。异常行为速查基于官方文档与 Lib/test/test_pwd.py 中的test_errors可将参数与异常行为归纳如下调用方式结果pwd.getpwuid(uid)且 uid 不存在KeyErrorpwd.getpwuid(uid)且 uid 超出uid_t范围如2**128KeyErrorpwd.getpwuid(3.14)等非整数TypeErrorpwd.getpwnam(name)且 name 不存在KeyErrorpwd.getpwnam()空字符串KeyErrorpwd.getpwnam(broot)传bytesTypeErrorpwd.getpwnam(a\x00b)含内嵌空字节ValueErrorpwd.getpwnam(未定义字符)无法编码UnicodeEncodeErrorpwd.getpwnam()/pwd.getpwuid()缺少参数或参数过多TypeError这里有一个细节值得注意负数 uid 在多数系统上是“合法范围但查无此用户”因此通常会得到KeyError但个别平台如 Cygwin 会把-1映射为UnknownUser行为不同Lib/test/test_pwd.py#L106-L110 便特意排除了 Cygwin 再做断言。实战示例1. 获取“当前用户”的完整信息最经典的用法是结合os.getuid()查询当前进程所属用户import os import pwd entry pwd.getpwuid(os.getuid()) print(f用户: {entry.pw_name}) print(fUID/GID: {entry.pw_uid}/{entry.pw_gid}) print(f主目录: {entry.pw_dir}) print(f登录Shell: {entry.pw_shell}) print(f真实姓名(GECOS): {entry.pw_gecos})2. 按名字解析用户实现比expanduser更细的控制import pwd def home_of(username: str) - str | None: try: return pwd.getpwnam(username).pw_dir except KeyError: return None # 系统中不存在该用户 print(home_of(root)) # 通常是 /root print(home_of(nobody)) # 通常是 /nonexistent 或类似占位目录3. 遍历系统中的全部账号统计可登录用户注意对pw_gecos与pw_shell做空值保护并跳过nologin/false类 shellimport pwd loginable [] for u in pwd.getpwall(): shell u.pw_shell or if nologin in shell or shell.endswith(/false): continue loginable.append((u.pw_name, u.pw_uid, u.pw_dir)) for name, uid, home in sorted(loginable, keylambda t: t[1]): print(f{uid:6} {name:12} {home})需要明确的是getpwall()只能枚举本地系统当前可见的数据库条目在接入 LDAP/NIS 的大型环境中它不保证返回全部账户因此不要用它做权限相关的安全决策只能作为辅助性的用户枚举手段。典型误区小结别指望pw_passwd里有可用哈希shadow 系统下它多半是x或*判断密码状态请使用平台相关的 shadow 接口标准库中有废弃与替代的演进历史相关说明见 Doc/library/removed.rst不要自行解析该字段。getpwnam不收bytes与许多“路径式”API 不同这里传bytes直接报TypeError。别假定 uid 唯一同一 uid 可对应多条记录反向查找时应做集合归并测试中也是先按 uid 聚合成列表再校验。pw_gecos等字符串字段可能为None虽然文档说“除 uid/gid 外均为字符串”源码与测试都确认了空指针映射为None的现实解引用前请判空。它不是跨平台模块仅限 Unix 类系统Windows、WASI、iOS 上不可用在缺少对应系统调用的受限环境configure 阶段ac_cv_func_getpwuid/ac_cv_func_getpwuid_r均不满足时根本不会构建。相关模块与延伸阅读若要查询组数据库/etc/group请使用结构完全平行的grp模块官方文档 Doc/library/grp.rst 是pwd的 seealso 推荐对象两者的 struct sequence 设计、API 形态getgrgid/getgrnam/getgrall一一对应。涉及用户密码输入不回显的场景可配合 getpass 使用。若想自行查看本模块的完整实现与官方测试可直接阅读 Modules/pwdmodule.c、Lib/test/test_pwd.py 以及并发场景下的 Lib/test/test_free_threading/test_pwd.py相关的构建期探测宏HAVE_GETPWENT、HAVE_GETPWNAM_R、HAVE_GETPWUID_R等声明在 pyconfig.h.in#L674-L685。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考