第6章:系统数据文件和信息(System Data Files and Information)
一、导读
1.1 本章学习目标
第6章介绍UNIX系统的数据文件和信息系统。UNIX系统将用户信息、组信息、系统配置等存储在特定的数据文件中(如/etc/passwd、/etc/shadow、/etc/group),并提供了一系列C库函数来访问这些信息。理解这些系统数据文件的结构和访问方式,对于编写需要用户认证、权限检查、系统信息获取的程序至关重要。
本章的核心学习目标包括:
- 掌握密码文件的结构:
/etc/passwd和/etc/shadow存储了哪些信息?各字段的含义是什么? - 理解影子口令机制:为什么现代UNIX系统将密码从
passwd文件分离到shadow文件? - 学会访问组信息:
/etc/group文件的结构,以及如何查询用户所属的组 - 掌握系统信息获取函数:
uname、gettimeofday、times等函数的使用 - 理解NSS(Name Service Switch):系统如何通过不同的数据源(文件、LDAP、NIS)解析用户信息
- 学会获取主机和网络信息:主机名、域名、网络配置等信息的获取方式
1.2 为什么本章如此重要
在实际的系统编程中,经常需要获取用户信息、检查组权限、获取系统配置。例如:
- 编写需要用户认证的程序(如FTP服务器、Web应用)
- 实现权限检查(如文件服务器、数据库系统)
- 编写系统监控工具(如
top、ps、df) - 实现日志系统(需要记录用户名、主机名等信息)
如果不了解系统数据文件的结构和访问方式,就无法正确地实现这些功能。此外,安全性也是一个重要考量——错误的用户认证实现可能导致安全漏洞。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
/etc/passwd文件中为什么密码字段是x?实际的密码存储在哪里?
为什么getpwnam返回的指针指向静态内存?这对多线程程序有什么影响?
一个用户可以属于多个组吗?如何查询用户的所有组?
uname返回的信息中,sysname、nodename、release、version、machine分别代表什么?
为什么获取时间要用gettimeofday而不是time?两者的精度有什么区别?
NSS是什么?它如何影响getpwnam等函数的行为?
二、核心概念详解
2.1 密码文件
2.1.1 /etc/passwd
密码文件/etc/passwd存储系统用户的基本信息,每行一个用户记录,字段用冒号分隔:
username:password:UID:GID:GECOS:directory:shell各字段含义:
| 字段 | 含义 | 示例 |
|---|---|---|
| username | 登录名 | alice |
| password | 密码占位符(现代系统为x) | x |
| UID | 用户ID | 1000 |
| GID | 主组ID | 1000 |
| GECOS | 用户全名和联系信息 | Alice Smith,,, |
| directory | 主目录 | /home/alice |
| shell | 登录shell | /bin/bash |
# 查看密码文件
cat /etc/passwd
# root:x:0:0:root:/root:/bin/bash
# alice:x:1000:1000:Alice Smith:/home/alice:/bin/bash注意:/etc/passwd对所有用户可读(因为很多程序需要查询用户信息),所以密码不能存储在这里。现代系统将密码存储在/etc/shadow中,passwd文件中的密码字段为x。
2.1.2 /etc/shadow
影子口令文件/etc/shadow存储加密后的密码和密码策略信息,只有root可读:
username:encrypted_password:last_change:min_age:max_age:warn:inactive:expire:reserved各字段含义:
| 字段 | 含义 |
|---|---|
| username | 登录名 |
| encrypted_password | 加密后的密码(使用bcrypt/sha256/sha512) |
| last_change | 密码最后修改日期(从1970-01-01起的天数) |
| min_age | 密码最小使用天数 |
| max_age | 密码最大使用天数 |
| warn | 密码过期前警告天数 |
| inactive | 密码过期后账号禁用天数 |
| expire | 账号过期日期 |
# 查看影子文件(需要root权限)
sudo cat /etc/shadow
# root:$6$xyz...:18000:0:99999:7:::
# alice:$6$abc...:18500:0:99999:7:::2.1.3 访问密码文件的函数
#include <pwd.h>
#include <sys/types.h>
struct passwd *getpwnam(const char *name); // 按用户名查询
struct passwd *getpwuid(uid_t uid); // 按UID查询
// 遍历密码文件
void setpwent(void); // rewind到开头
struct passwd *getpwent(void); // 获取下一条记录
void endpwent(void); // 关闭文件#include <pwd.h>
#include <stdio.h>
#include <sys/types.h>
int main() {
// 按用户名查询
struct passwd *pw = getpwnam("alice");
if (pw) {
printf("Name: %s\n", pw->pw_name);
printf("UID: %d\n", pw->pw_uid);
printf("Home: %s\n", pw->pw_dir);
printf("Shell: %s\n", pw->pw_shell);
}
// 按UID查询
pw = getpwuid(0);
if (pw) {
printf("UID 0 is: %s\n", pw->pw_name);
}
return 0;
}重要注意事项:getpwnam和getpwuid返回的指针指向静态内存,每次调用都会覆盖之前的结果。如果需要保存结果,必须复制:
struct passwd *pw = getpwnam("alice");
// 错误:pw指向静态内存,下次调用getpwnam会被覆盖
// 正确:复制需要的字段
char *name = strdup(pw->pw_name);
char *home = strdup(pw->pw_dir);多线程安全版本:getpwnam_r等_r后缀的函数是可重入的,适合多线程程序。
struct passwd pwbuf;
char buf[1024];
struct passwd *result;
getpwnam_r("alice", &pwbuf, buf, sizeof(buf), &result);
if (result != NULL) {
printf("Found: %s\n", result->pw_name);
}2.1.4 遍历所有用户
#include <pwd.h>
#include <stdio.h>
int main() {
struct passwd *pw;
setpwent(); // 回到文件开头
while ((pw = getpwent()) != NULL) {
if (pw->pw_uid >= 1000 && pw->pw_uid < 65534) {
printf("%-15s UID=%-6d Home=%-20s Shell=%s\n",
pw->pw_name, pw->pw_uid, pw->pw_dir, pw->pw_shell);
}
}
endpwent(); // 关闭文件
return 0;
}2.2 组文件
2.2.1 /etc/group
组文件/etc/group定义用户组及其成员:
group_name:password:GID:user_listcat /etc/group
# root:x:0:
# sudo:x:27:alice,bob
# developers:x:1001:alice,bob,charlie2.2.2 访问组信息的函数
#include <grp.h>
#include <sys/types.h>
struct group *getgrnam(const char *name); // 按组名查询
struct group *getgrgid(gid_t gid); // 按GID查询
// 遍历组文件
void setgrent(void);
struct group *getgrent(void);
void endgrent(void);#include <grp.h>
#include <stdio.h>
int main() {
struct group *gr = getgrnam("sudo");
if (gr) {
printf("Group: %s, GID: %d\n", gr->gr_name, gr->gr_gid);
printf("Members: ");
for (int i = 0; gr->gr_mem[i] != NULL; i++) {
printf("%s ", gr->gr_mem[i]);
}
printf("\n");
}
return 0;
}2.2.3 用户的补充组
一个用户可以属于多个组。主组由/etc/passwd中的GID字段指定,补充组由/etc/group中的成员列表指定。
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <grp.h>
int main() {
// 获取所有组ID
gid_t groups[64];
int ngroups = 64;
getgroups(ngroups, groups);
printf("User belongs to %d groups:\n", ngroups);
for (int i = 0; i < ngroups; i++) {
struct group *gr = getgrgid(groups[i]);
if (gr) {
printf(" %s (GID: %d)\n", gr->gr_name, groups[i]);
}
}
return 0;
}2.3 系统标识
2.3.1 uname
uname获取系统信息:
#include <sys/utsname.h>
int uname(struct utsname *buf);
struct utsname {
char sysname[]; // 操作系统名称(如"Linux")
char nodename[]; // 网络节点名(主机名)
char release[]; // 操作系统发行版(如"5.15.0")
char version[]; // 内核版本
char machine[]; // 硬件架构(如"x86_64")
};#include <stdio.h>
#include <sys/utsname.h>
int main() {
struct utsname info;
if (uname(&info) == -1) {
perror("uname");
return 1;
}
printf("System: %s\n", info.sysname);
printf("Node: %s\n", info.nodename);
printf("Release: %s\n", info.release);
printf("Version: %s\n", info.version);
printf("Machine: %s\n", info.machine);
return 0;
}2.3.2 主机名和域名
#include <unistd.h>
int gethostname(char *name, size_t len);
int getdomainname(char *name, size_t len);#include <stdio.h>
#include <unistd.h>
int main() {
char hostname[256];
gethostname(hostname, sizeof(hostname));
printf("Hostname: %s\n", hostname);
char domain[256];
getdomainname(domain, sizeof(domain));
printf("Domain: %s\n", domain);
return 0;
}2.4 时间和日期
2.4.1 time和gettimeofday
#include <time.h>
#include <sys/time.h>
time_t time(time_t *calptr);
int gettimeofday(struct timeval *tv, struct timezone *tz);time返回秒级精度的时间,gettimeofday返回微秒级精度:
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
int main() {
// time:秒级精度
time_t now = time(NULL);
printf("Time: %s", ctime(&now));
// gettimeofday:微秒级精度
struct timeval tv;
gettimeofday(&tv, NULL);
printf("Time: %ld.%06ld seconds since epoch\n",
tv.tv_sec, tv.tv_usec);
// 测量代码执行时间
struct timeval start, end;
gettimeofday(&start, NULL);
// ... 执行一些操作 ...
for (volatile int i = 0; i < 1000000; i++);
gettimeofday(&end, NULL);
double elapsed = (end.tv_sec - start.tv_sec) +
(end.tv_usec - start.tv_usec) / 1000000.0;
printf("Elapsed: %.6f seconds\n", elapsed);
return 0;
}2.4.2 时间转换函数
#include <time.h>
struct tm *gmtime(const time_t *calptr); // UTC时间
struct tm *localtime(const time_t *calptr); // 本地时间
time_t mktime(struct tm *tmp); // tm转换为time_t
char *asctime(const struct tm *tmp); // tm转换为字符串
char *ctime(const time_t *calptr); // time_t转换为字符串
size_t strftime(char *buf, size_t maxsize,
const char *format, const struct tm *tmp);#include <stdio.h>
#include <time.h>
int main() {
time_t now = time(NULL);
struct tm *local = localtime(&now);
// 使用strftime格式化时间
char buf[256];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
printf("Local time: %s\n", buf);
strftime(buf, sizeof(buf), "%A, %B %d, %Y", local);
printf("Date: %s\n", buf);
// 解析时间字符串
struct tm tm = {0};
strptime("2024-01-15 14:30:00", "%Y-%m-%d %H:%M:%S", &tm);
time_t t = mktime(&tm);
printf("Parsed: %s", ctime(&t));
return 0;
}2.5 系统资源限制
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlp);
int setrlimit(int resource, const struct rlimit *rlp);
struct rlimit {
rlim_t rlim_cur; // 软限制(当前生效)
rlim_t rlim_max; // 硬限制(软限制的上限)
};常用的资源限制:
| 资源 | 含义 |
|---|---|
| RLIMIT_CPU | CPU时间(秒) |
| RLIMIT_FSIZE | 文件大小(字节) |
| RLIMIT_DATA | 数据段大小 |
| RLIMIT_STACK | 栈大小 |
| RLIMIT_CORE | core dump大小 |
| RLIMIT_NOFILE | 打开的文件描述符数 |
| RLIMIT_NPROC | 进程数 |
#include <stdio.h>
#include <sys/resource.h>
void print_limit(const char *name, int resource) {
struct rlimit rl;
getrlimit(resource, &rl);
printf("%-20s cur=%-10ld max=%-10ld\n",
name, rl.rlim_cur, rl.rlim_max);
}
int main() {
print_limit("RLIMIT_CPU", RLIMIT_CPU);
print_limit("RLIMIT_FSIZE", RLIMIT_FSIZE);
print_limit("RLIMIT_DATA", RLIMIT_DATA);
print_limit("RLIMIT_STACK", RLIMIT_STACK);
print_limit("RLIMIT_CORE", RLIMIT_CORE);
print_limit("RLIMIT_NOFILE", RLIMIT_NOFILE);
print_limit("RLIMIT_NPROC", RLIMIT_NPROC);
return 0;
}三、实战案例
3.1 用户信息查询工具
#include <stdio.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
void print_user_info(const char *username) {
struct passwd *pw = getpwnam(username);
if (pw == NULL) {
fprintf(stderr, "User '%s' not found\n", username);
return;
}
printf("Username: %s\n", pw->pw_name);
printf("UID: %d\n", pw->pw_uid);
printf("GID: %d\n", pw->pw_gid);
printf("Home: %s\n", pw->pw_dir);
printf("Shell: %s\n", pw->pw_shell);
if (pw->pw_gecos && strlen(pw->pw_gecos) > 0) {
printf("GECOS: %s\n", pw->pw_gecos);
}
// 查询主组
struct group *gr = getgrgid(pw->pw_gid);
if (gr) {
printf("Primary group: %s\n", gr->gr_name);
}
// 查询所有补充组
gid_t groups[64];
int ngroups = 64;
getgrouplist(username, pw->pw_gid, groups, &ngroups);
printf("All groups: ");
for (int i = 0; i < ngroups; i++) {
gr = getgrgid(groups[i]);
if (gr) {
printf("%s ", gr->gr_name);
}
}
printf("\n");
}
int main(int argc, char *argv[]) {
if (argc == 1) {
// 查询当前用户
uid_t uid = getuid();
struct passwd *pw = getpwuid(uid);
if (pw) {
print_user_info(pw->pw_name);
}
} else {
for (int i = 1; i < argc; i++) {
print_user_info(argv[i]);
if (i < argc - 1) printf("\n");
}
}
return 0;
}3.2 系统信息收集工具
#include <stdio.h>
#include <sys/utsname.h>
#include <sys/sysinfo.h>
#include <unistd.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
void print_system_info() {
// 系统标识
struct utsname uts;
uname(&uts);
printf("=== System Information ===\n");
printf("OS: %s %s (%s)\n", uts.sysname, uts.release, uts.machine);
printf("Hostname: %s\n", uts.nodename);
// 系统运行信息
struct sysinfo si;
sysinfo(&si);
printf("Uptime: %ld seconds\n", si.uptime);
printf("Processes: %d\n", si.procs);
printf("Total RAM: %lu MB\n", si.totalram / 1024 / 1024);
printf("Free RAM: %lu MB\n", si.freeram / 1024 / 1024);
// 当前用户
uid_t uid = getuid();
struct passwd *pw = getpwuid(uid);
printf("\n=== Current User ===\n");
printf("User: %s (UID: %d)\n", pw->pw_name, uid);
printf("Home: %s\n", pw->pw_dir);
printf("Shell: %s\n", pw->pw_shell);
// 当前时间
time_t now = time(NULL);
printf("\n=== Current Time ===\n");
printf("%s", ctime(&now));
}
int main() {
print_system_info();
return 0;
}3.3 资源限制监控
#include <stdio.h>
#include <sys/resource.h>
#include <sys/time.h>
struct limit_info {
int resource;
const char *name;
const char *unit;
};
int main() {
struct limit_info limits[] = {
{RLIMIT_CPU, "CPU time", "seconds"},
{RLIMIT_FSIZE, "File size", "bytes"},
{RLIMIT_DATA, "Data segment", "bytes"},
{RLIMIT_STACK, "Stack size", "bytes"},
{RLIMIT_CORE, "Core dump", "bytes"},
{RLIMIT_NOFILE, "Open files", "descriptors"},
{RLIMIT_NPROC, "Processes", "processes"},
};
int n = sizeof(limits) / sizeof(limits[0]);
printf("%-20s %-15s %-15s %s\n", "Resource", "Soft Limit", "Hard Limit", "Unit");
printf("%-20s %-15s %-15s %s\n", "--------", "----------", "----------", "----");
for (int i = 0; i < n; i++) {
struct rlimit rl;
if (getrlimit(limits[i].resource, &rl) == 0) {
char soft[32], hard[32];
if (rl.rlim_cur == RLIM_INFINITY)
snprintf(soft, sizeof(soft), "unlimited");
else
snprintf(soft, sizeof(soft), "%ld", rl.rlim_cur);
if (rl.rlim_max == RLIM_INFINITY)
snprintf(hard, sizeof(hard), "unlimited");
else
snprintf(hard, sizeof(hard), "%ld", rl.rlim_max);
printf("%-20s %-15s %-15s %s\n",
limits[i].name, soft, hard, limits[i].unit);
}
}
return 0;
}四、本章小结
第6章介绍了UNIX系统的数据文件和信息系统。核心知识点包括:
密码文件:/etc/passwd存储用户基本信息(登录名、UID、GID、主目录、shell),/etc/shadow存储加密密码和密码策略。
组文件:/etc/group定义用户组及成员。用户可以属于多个组(一个主组+多个补充组)。
访问函数:getpwnam/getpwuid查询用户信息,getgrnam/getgrgid查询组信息。这些函数返回指向静态内存的指针,多线程程序应使用_r版本。
系统标识:uname获取系统信息(OS名称、主机名、内核版本、硬件架构)。gethostname/getdomainname获取主机名和域名。
时间函数:time返回秒级时间,gettimeofday返回微秒级时间。strftime格式化时间输出。
资源限制:getrlimit/setrlimit获取和设置进程的资源限制(CPU时间、文件大小、打开文件数等)。
五、思考与练习
思考题
为什么/etc/passwd文件需要对所有用户可读?如果它不可读,会影响哪些程序?
getpwnam返回的指针指向静态内存,这意味着什么?在多线程程序中应该如何处理?
为什么现代系统将密码从passwd文件分离到shadow文件?这种设计解决了什么安全问题?
软限制和硬限制有什么区别?普通用户可以修改软限制吗?可以修改硬限制吗?
编程练习
编写一个程序,列出系统中所有UID大于等于1000的普通用户及其信息。
实现一个whoami命令的简化版,显示当前用户的详细信息(用户名、UID、主组、所有补充组)。
编写一个程序,获取并显示系统的资源限制,对比ulimit -a的输出。
实现一个简单的用户认证程序:提示输入用户名和密码,验证密码是否正确(读取/etc/shadow并比较加密后的密码)。