CPythonpwd模块完全指南:Unix 密码数据库的结构、API 与源码级实现
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
pwd是 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 object)pwd.struct_passwd。它的字段与 C 语言struct passwd(见<pwd.h>)的成员一一对应。官方文档用下面这张表定义了字段布局:
| 索引 | 属性(Attribute) | 含义(Meaning) |
|---|---|---|
| 0 | pw_name | 登录名(Login name) |
| 1 | pw_passwd | 可选加密密码(Optional encrypted password) |
| 2 | pw_uid | 数值用户 ID(Numerical user ID) |
| 3 | pw_gid | 数值组 ID(Numerical group ID) |
| 4 | pw_gecos | 用户名或注释字段(User name or comment field) |
| 5 | pw_dir | 用户主目录(User home directory) |
| 6 | pw_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 与主组 ID,Python 侧以整数呈现。构建时通过_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:注释字段,可能是None
GECOS 字段通常存放真实姓名或补充注释(历史上源自通用电气综合操作系统)。文档将其描述为字符串,但从源码的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:主目录与登录 Shell
pw_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_name='root', pw_passwd='x', pw_uid=0, pw_gid=0, pw_gecos='root', 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_name='root', pw_passwd='x', pw_uid=0, ...)- 参数必须是
str:传入bytes(如b'root')会抛出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_SIZE(1024 字节); - 用
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):支持多解释器下的每解释器 GIL(Py_MOD_PER_INTERPRETER_GIL_SUPPORTED),并在自由线程构建(free-threaded build,Py_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 不存在 | KeyError |
pwd.getpwuid(uid)且 uid 超出uid_t范围(如2**128) | KeyError |
pwd.getpwuid(3.14)等非整数 | TypeError |
pwd.getpwnam(name)且 name 不存在 | KeyError |
pwd.getpwnam("")空字符串 | KeyError |
pwd.getpwnam(b"root")传bytes | TypeError |
pwd.getpwnam("a\x00b")含内嵌空字节 | ValueError |
pwd.getpwnam(未定义字符)无法编码 | UnicodeEncodeError |
pwd.getpwnam()/pwd.getpwuid()缺少参数或参数过多 | TypeError |
这里有一个细节值得注意:负数 uid 在多数系统上是“合法范围但查无此用户”,因此通常会得到KeyError,但个别平台(如 Cygwin 会把-1映射为Unknown+User)行为不同,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(f"UID/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类 shell:
import 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, key=lambda 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),仅供参考