6UNIX环境高级编程(第3版)

系统数据文件和信息

系统信息

passwdshadowgroup

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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用户ID1000
GID主组ID1000
GECOS用户全名和联系信息Alice Smith,,,
directory主目录/home/alice
shell登录shell/bin/bash
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账号过期日期
bash
# 查看影子文件(需要root权限)
sudo cat /etc/shadow
# root:$6$xyz...:18000:0:99999:7:::
# alice:$6$abc...:18500:0:99999:7:::

2.1.3 访问密码文件的函数

c
#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);           // 关闭文件
c
#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返回的指针指向静态内存,每次调用都会覆盖之前的结果。如果需要保存结果,必须复制:

c
struct passwd *pw = getpwnam("alice");
// 错误:pw指向静态内存,下次调用getpwnam会被覆盖
// 正确:复制需要的字段
char *name = strdup(pw->pw_name);
char *home = strdup(pw->pw_dir);

多线程安全版本:getpwnam_r等_r后缀的函数是可重入的,适合多线程程序。

c
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 遍历所有用户

c
#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_list
bash
cat /etc/group
# root:x:0:
# sudo:x:27:alice,bob
# developers:x:1001:alice,bob,charlie

2.2.2 访问组信息的函数

c
#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);
c
#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中的成员列表指定。

c
#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获取系统信息:

c
#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")
};
c
#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 主机名和域名

c
#include <unistd.h>

int gethostname(char *name, size_t len);
int getdomainname(char *name, size_t len);
c
#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

c
#include <time.h>
#include <sys/time.h>

time_t time(time_t *calptr);
int gettimeofday(struct timeval *tv, struct timezone *tz);

time返回秒级精度的时间,gettimeofday返回微秒级精度:

c
#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 时间转换函数

c
#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);
c
#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 系统资源限制

c
#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_CPUCPU时间(秒)
RLIMIT_FSIZE文件大小(字节)
RLIMIT_DATA数据段大小
RLIMIT_STACK栈大小
RLIMIT_COREcore dump大小
RLIMIT_NOFILE打开的文件描述符数
RLIMIT_NPROC进程数
c
#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 用户信息查询工具

c
#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 系统信息收集工具

c
#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 资源限制监控

c
#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并比较加密后的密码)。


6/12系统数据
0%