返回

getpwuid内存分配详解:静态区?libc内部缓冲区!

Linux

理解 getpwuid 的内存分配

getpwuid 是一个在 Unix-like 系统中用于通过用户 ID (UID) 获取用户信息(如用户名)的函数。 此函数返回一个指向 struct passwd 结构体的指针,其中包含用户的详细信息。关键在于理解这个结构体及其包含的字符串字段的内存从何而来。

静态区域的误解

常见的一种误解是 getpwuid 返回的内存来自于程序的静态区域。正如手动页面所指,函数返回的指针可以指向一个“静态区域”。但此处的静态区域指的不是程序加载时位于数据段(包括初始化数据和未初始化数据段)的那些区域。etext, edataend 标志着这些区域的边界。程序实例代码输出了这些标志位置,明确的证实了这一点。getpwuid 使用的并不是这些。

实际内存来源:库内部缓冲区

实际情况下,getpwuid 返回的 struct passwd 以及其中的 pw_name 等字符串指针,都指向 libc 库内部分配的静态 缓冲区。这个缓冲区的生命周期与 libc 库的生命周期相同,它并非为每次调用都重新分配的,这是它被称之为 “static area” 的原因,但此处的 static 是相对于 C语言 的static关键词(局部静态变量)。由于这块缓冲区由 libc 管理,而非应用程序,因此我们不能用 free 来释放它。这也解释了手册页中的警告:“不要将返回的指针传递给 free(3)

此缓冲区的目的在于效率。 如果 getpwuid每次都为结果分配堆内存,然后期望用户稍后释放,就会引入复杂性和开销,而大多数时候并不需要。 使用静态缓冲区能更有效地完成获取用户信息这个任务。这个静态缓冲区的内部实现通常使用 static 修饰的变量。由于多个用户空间程序会并发调用 getpwuid, 现代libc库一般会在调用内部同步原语后使用线程局部变量的特性,避免出现数据竞争。

长度如何确定

关键的第二个问题是,程序如何确定用户信息的长度? 答案在于,用户信息存储在 /etc/passwd 文件中。 此文件存储了系统中所有用户的信息,包含固定的字段格式,如用户名,密码(通常以"x"占据),用户 ID,组 ID,用户注释,home 目录,登陆 shell。 libc 中的函数读取这个文件,解析用户数据并把信息复制到库内部预先分配的固定大小缓冲区中。

需要注意,固定大小的缓冲区大小有一个上限。如果系统用户名或者其他用户信息长度超过了此限制,可能会发生缓冲区溢出或者数据截断的状况。这是使用系统用户管理命令,创建和编辑用户时的注意事项,应当尽量保持用户名,用户注释,登陆shell等用户字符串的长度在合理范围内。

pwdpw_name 地址差异

程序输出的 pwd 地址,指向一个类型为 struct passwd 的结构体,其内存是 libc 分配的。此结构体中包含了多个字段,其中一个字段 pw_name 指向了存储用户名的字符串。这两个指针的值,由于数据和结构体所在内存地址的不同而出现差异。 仔细观察,pwd 地址 0x7f2b3c7aba60 落在了 /usr/lib/x86_64-linux-gnu/libc.so.6 所映射内存范围 7f2b3c58a000-7f2b3c7a6000 内, 这表明 pwd 结构体本身在 libc 内。而 pw_name 指针的 0x5646b174e2a0 地址在程序实例的堆上,意味着用户字符串的数据拷贝落在了堆区(这只是 libc 的一个优化),具体实现方式随系统的库不同。 libc 并非返回一个位于 libc 的内存区域,包含完整信息的大结构体。 它是在 libc 内存区域内维护一个 struct passwd 结构体。为了避免 struct passwd 占用大量内存(由于用户注释字段过长的情况),仅保存结构体和指向用户数据的指针。 用户数据本身的数据拷贝可以在 libc 内或者由 libc 使用的库实现,具体看操作系统库的实现策略。

解决方案

尽管不能直接 free 内存,有些策略仍可以优化性能或者减少安全风险:

  • 拷贝字符串: 若需要在程序的生命周期中长时间保存用户信息, 应该复制返回的用户名等字符串到自己管理的内存中 (比如使用 strdup )。 如下例所示:
 char *my_copy = strdup(pwd->pw_name);
 if (my_copy == NULL) {
      perror("strdup failed");
      // handle error condition
 } else {
   // use my_copy
     free(my_copy); // 之后不要忘记释放
 }
 ```
 此操作能保证用户信息的副本不依赖 `libc` 的缓冲区,不会由于其他 `getpwuid` 或者 `getpwnam` 等函数的调用而被覆写。 
*   **避免长期持有:**  不要在长期循环或持续运行的任务中长时间保存 `struct passwd*`。  一旦用户信息用完,将其舍弃,让 `getpwuid` 的静态缓存被下一个用户复用, 减少内存的使用。 
*   **使用缓存:**  如果应用程序频繁查找相同用户信息, 使用缓存策略 (比如通过用户ID到结构体拷贝的哈希表) 以避免多次调用 `getpwuid`,可以有效的提升应用程序的整体性能。

### 总结

`getpwuid`  内存的分配是 `libc` 库内部处理的。 关键要理解这些信息来源于库的静态区域,不能使用 `free` 进行回收。为了保障程序的稳定运行和避免内存相关问题, 务必拷贝所需字符串,及时舍弃返回结构体指针,或利用缓存机制减少对 `getpwuid` 的直接调用。正确理解和应用这些知识有助于开发者编写健壮且安全的应用。