SerenityOS groups 命令深度解析:用户组成员查询的工作原理与源码实现
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
groups是 SerenityOS 中用于查询用户组成员关系的命令行工具,本文基于其 man 手册页 groups.md,结合 groups.cpp 工具源码、Account 账户库与 LibC 的 grp 实现,完整讲解该命令的用法、输出格式、数据文件来源,以及从/etc/passwd、/etc/group到 GID 名称解析的完整实现链路。读完后你可以准确理解 SerenityOS 的组管理机制,并能基于源码追踪任何账户/组查询工具的内部逻辑。
命令概览:用法、参数与示例
根据 man 手册页的定义,groups的完整用法如下:
$ groups [username...]- 无参数:列出当前进程(即当前用户)的组归属;
- 带
username参数:逐个列出指定用户的组归属,支持一次传入多个用户名。
手册页给出的标准示例覆盖了三种典型场景:
# 列出当前用户的组归属 $ groups # 列出单个用户的组归属 $ groups nona # 同时列出多个用户的组归属 $ groups nona anon root这里以 SerenityOS 镜像自带的账户为例:发行版内置了root、anon、nona等账户(见下文/etc/passwd实例),因此groups nona这类命令可以直接在系统里复现。
输出格式的细节:无参数与有参数行为不同
手册页没有说明输出格式的细节,这需要看 groups.cpp 的源码:
if (usernames.is_empty()) { auto account = TRY(Core::Account::from_uid(geteuid(), Core::Account::Read::PasswdOnly)); print_account_gids(account); } for (auto const& username : usernames) { auto result = Core::Account::from_name(username, Core::Account::Read::PasswdOnly); if (result.is_error()) { warnln("{} '{}'", result.error(), username); continue; } out("{} : ", username); print_account_gids(result.value()); }由此可以确认三条实际行为:
- 无参数时输出不带前缀,直接打印组名列表(以有效 UID
geteuid()查询); - 指定用户名时,每行以
username :作为前缀,后跟该用户的组名列表; - 某个用户名不存在时不中断,而是向 stderr 打印错误(如
No such user 'xxx')后continue继续处理后续用户名。
组名列表本身的生成逻辑在 print_account_gids:
static void print_account_gids(Core::Account const& account) { auto* gr = getgrgid(account.gid()); if (!gr) { outln(); return; } out("{}", gr->gr_name); for (auto& gid : account.extra_gids()) { gr = getgrgid(gid); out(" {}", gr->gr_name); } outln(); }即:先打印主组(/etc/passwd中记录的pw_gid对应的组名),再依次打印每个附加组(extra_gids,通过 GID 反查组名),组名之间以空格分隔。若主组在/etc/group中查不到,则输出空行。
数据来源:/etc/passwd 与 /etc/group
groups启动时的 unveil 声明 精确暴露了它的全部数据依赖:
TRY(Core::System::unveil("/etc/passwd", "r")); TRY(Core::System::unveil("/etc/group", "r")); TRY(Core::System::unveil(nullptr, nullptr)); TRY(Core::System::pledge("stdio rpath"));工具只被授予/etc/passwd和/etc/group两个文件的读权限,随后关闭 unveil 命名空间并 pledgestdio rpath——这是 SerenityOS 的进程沙箱机制,保证groups只能读这两个文件并使用标准 I/O 与已打开的路径。
/etc/passwd:主组的来源
SerenityOS 镜像中的 passwd 文件 实例如下:
root::0:0:root:/root:/bin/Shell lookup:!:10:10:LookupServer,,,:/:/bin/false window:!:13:13:WindowServer,,,:/:/bin/false sshd:!:19:19:OpenSSH privsep,,,:/:/bin/false anon:!:100:100:Anonymous,,,:/home/anon:/bin/Shell nona:!:200:100:Nona,,,:/home/nona:/bin/Shell字段格式为用户名:密码:UID:GID:GECOS:主目录:Shell,其中第 4 个字段(如nona的100)就是主 GID,print_account_gids输出的第一个组名即来自它。
/etc/group:附加组成员的来源
group 文件 实例如下:
root:x:0: wheel:x:1:anon tty:x:2: phys:x:3:window,anon audio:x:4:anon utmp:x:5: lookup:x:10:anon window:x:13:anon users:x:100:anon字段格式为组名:密码:GID:成员列表,成员以逗号分隔。对照/etc/passwd可以推断:执行groups nona时,nona的主 GID 是 100(users组),但它并未出现在任何组的成员列表中,因此输出只有一行nona : users;而执行groups anon时,anon的主组是users(GID 100),同时它作为成员出现在wheel、phys、audio、lookup、window各组,因此会输出anon : users wheel phys audio lookup window。
LibC 的 group 解析实现
上述文件由 grp.cpp 实现的标准 API 解析。核心函数 getgrgid 采用线性扫描方式:
struct group* getgrgid(gid_t gid) { setgrent(); ScopeGuard guard = [] { endgrent(); }; while (auto* gr = getgrent()) { if (gr->gr_gid == gid) return gr; } return nullptr; }即每次调用都setgrent()从文件头开始,逐行读取直到 GID 匹配。底层的 parse_grpdb_entry 负责把一行拆成 4 个字段:将:和换行替换为\0后按空字符切分,字段数不为 4 即判定为畸形条目并告警;成员字段再按逗号切分,成员指针表被复制进调用者提供的缓冲区尾部。getgrent使用一个静态 1024 字节缓冲区,行过长时返回ERANGE。
Core::Account:附加组 GID 的推导逻辑
groups并不直接读文件,而是通过 Core::Account 库完成“用户名 → 账户 → 组”的查询。指定用户名路径调用Account::from_name(username, Read::PasswdOnly),它内部经Core::System::getpwnam定位/etc/passwd条目,再以PasswdOnly模式跳过/etc/shadow的读取——因为查组成员不需要密码散列。
附加组 GID 的推导发生在 get_extra_gids:
static Vector<gid_t> get_extra_gids(passwd const& pwd) { StringView username { pwd.pw_name, strlen(pwd.pw_name) }; Vector<gid_t> extra_gids; setgrent(); for (auto* group = getgrent(); group; group = getgrent()) { if (group->gr_gid == pwd.pw_gid) continue; for (size_t i = 0; group->gr_mem[i]; ++i) { if (username == group->gr_mem[i]) { extra_gids.append(group->gr_gid); break; } } } endgrent(); return extra_gids; }算法一目了然:遍历/etc/group的每一行,跳过与主 GID 相同的组,若该组成员列表包含目标用户名,则把该组 GID 记为附加组,最终按文件中出现顺序收集。这也解释了前文groups anon的输出顺序与/etc/group的行序一致。
值得注意的是,Account类区分两种读取模式(见 Account.h):Read::All会额外读取 shadow 条目以获取密码散列,Read::PasswdOnly则只依赖/etc/passwd。groups选用后者,与其 unveil 声明严格对应——它根本没有被授予/etc/shadow的读权限。
与账户管理工具的协作关系
groups只负责“查询”,组的增删改由 man8 下的配套工具完成,手册页的 See Also 也指向它们:
- groupadd(8):创建新组;
- groupdel(8):删除组。
这些管理命令的底层写入路径是 Account::sync():它通过generate_group_file()重新生成整个/etc/group内容(在成员列表中加入或移除用户名),先写入/etc/group.XXXXXX临时文件再rename原子替换,避免写坏账户数据库。也就是说,groupadd/groupdel/usermod改完后,groups立刻能查询到最新状态,因为两者共享同一套Account/grp数据源。
此外,/etc/group成员列表的消费方不止groups:LibC 的 initgroups 在用户登录时会扫描同样的成员表,把所有含该用户的组 GID(外加传入的extra_gid)交给setgroups,从而决定进程的补充组权限;Account::login() 则执行setgroups+setgid+setuid完成登录态切换。groups查询到的结果,本质上就是登录时进程实际会获得的组集合的“只读视图”。
小结
groups无参数查当前用户、有参数查指定用户,多用户批量查询时单个失败不影响其余用户;- 主组来自
/etc/passwd的 GID 字段,附加组来自/etc/group成员列表中对该用户名的枚举; - 实现链路为:
groups.cpp→Core::Account::from_name/from_uid→getpwnam/getpwuid+get_extra_gids(遍历/etc/group)→getgrgid(线性扫描反查组名); - unveil/pledge 沙箱严格限制了该工具的访问面,仅
stdio rpath; - 组数据由
Account::sync()原子重写,与groupadd/groupdel/usermod等管理命令构成完整的账户管理体系。
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考