APUE 06 - 系统数据文件和信息

  UNIX 系统的正常运作需要使用大量与系统有关的数据文件,如口令文件和组文件。

口令文件

  UNIX 系统口令文件 (POSIX 称为用户数据库) 包含以下字段,这些字段被包含在 <pwd.h> 中定义的 passwd 结构中:

  由于历史原因,口令文件是 /etc/passwd,而且是一个 ASCII 文件。每一行代表一个记录,每个记录包含以上字段,字段之间用冒号分隔,如在 Linux 中:

root:x:0:0:root:/root:/bin/bash
squid:x:23:23::/var/spool/squid:/dev/null
nobody:x:65534:65534:Nobody:/home:/bin/sh
sar:x:205:105:Stephen Rago:/home/sar:/bin/bash

  关于这些登录项:

  • 通常有一个用户名为 root 的登录项,其用户 ID 是 0。
  • 加密口令字包含一个占位符 x,较早的 UNIX 系统中,该字段用于存放加密口令字。但为了安全,现在的系统将加密口令字放在另一个文件中。
  • 口令文件项中的某些字段可能是空。如果加密口令字为空,这就意味着该用户没有任何口令 (不推荐这样做)。但是,注释字段为空白没有任何影响。
  • shell 字段包含一个可执行文件名,它被用作该用户的登录 shell。如果该字段为空,则使用系统默认值。注意,用户 squid 的登录 shell 是一个设备 /dev/null 而不是可执行文件。这将阻止任何人以 squid 的名义登录到系统中。
  • 想要阻止一个特定用户登录到系统,除了使用 /dev/null 之外,还可以将 /bin/false 和 /bin/true 用作登录 shell。前者简单的以非 0 状态终止,后者以 0 状态终止。
  • 使用 nobody 作为登录名,任何人都可以登录到系统。但是其用户 ID 65534 和 组 ID 65534 没有任何特权,该用户只能访问人人皆可读,写的文件。

  finger(1) 命令可以查看用户注释字段中的附加信息:

finger -p sar

  POSIX 定义了两个获取口令文件项的函数,可以通过登录名或者用户 ID 查看相关项:

#include <pwd.h>

struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);

/* Both return: pointer if OK, NULL on error */

  ls(1) 程序使用了 getpwuid 函数,它将 i-node 中的数字 ID 映射为用户的登录名。在键入登录名时,login(1) 程序会使用 getpwnam 函数。

  这两个函数都返回一个指向 passwd 结构的指针。其指向的 passwd 结构通常是一个静态变量,只要调用任一相关函数,其内容就会被重写。

  以上两个函数适用于已知 UID 或者 UNAME 的情况,下面三个函数用于未知情况下查询:

#include <pwd.h>

struct passwd *getpwent(void);
/* Returns: pointer if OK, NULL on error or end of file */

void setpwent(void);
void endpwent(void);

  函数 getpwent 返回口令文件中的下一个记录项,每次调用它时都会重写共享的 passwd 结构 (第一次调用时,它会自动打开要使用的口令文件)。函数 setpwent 会使程序定位到口令文件的开始处。使用 getpwent 函数后,一定要调用 endpwent 函数关闭这些文件。

  下面是 getpwnam 函数的一个实现:

#include <pwd.h>
#include <stddef.h>
#include <string.h>
struct passwd *getpwnam(const char *name)
{
    struct passwd *ptr;
    setpwent();
    while ((ptr = getpwent()) != NULL)
        if (strcmp(name, ptr->pw_name) == 0)
            break;
    /* found a match */
    endpwent();
    return (ptr);
    /* ptr is NULL if no match found */
}

阴影口令

  加密口令是经过单向加密算法处理过的用户口令副本,因为算法是单向的,所以不能从加密口令猜测到原来的口令。对于一个加密口令,没有办法将其反变换到明文口令。

  常见的破解明文口令的方法是穷举法,即根据加密口令不断实验。为了增加破解难度,某些系统将加密口令存放在另一个称为阴影口令 (shadow password) 的文件中,这将阻止一般用户获取原始资料。阴影口令文件至少要包含用户名和加密口令:

  阴影口令不应是一般用户可以读取的,仅有少数几个程序需要访问加密口令:login(1) 和 passwd(1)。这些程序通常都是设置用户 ID 位为 root 的可执行文件。有了阴影口令文件,普通口令文件 /etc/passwd 就可以被任何用户自由读取。

  在 Linux 中,有一组用于访问阴影口令文件的函数:

#include <shadow.h>

struct spwd *getspnam(const char *name);
struct spwd *getspent(void);

/* Both return: pointer if OK, NULL on error */

void setspent(void);
void endspent(void);

组文件

  UNIX 组文件 (POSIX 称为组数据库) 包含下列字段,这些字段被包含在 <grp.h> 中的 group 结构中:

  字段 gr_mem 是一个指针数组,其中每个指针指向一个该组中的用户名字符串,该数组以 NULL 指针结尾。

  POSIX 定义了以下两个函数查看组名和组 ID:

#include <grp.h>

struct group *getgrgid(gid_t gid);
struct group *getgrnam(const char *name);

/* Both return: pointer if OK, NULL on error */

  类似操作口令文件的函数,这两个函数也返回一个指向静态变量的 group 结构指针。并且,每次调用相关的函数都将重写该结构。

  如果要搜索整个组文件,需要使用另外 3 个函数:

#include <grp.h>

struct group *getgrent(void);
/* Returns: pointer if OK, NULL on error or end of file */

void setgrent(void);
void endgrent(void);

  这 3 个函数类似之前搜索口令文件的函数:setgrent 打开组文件 (如果它尚未被打开) 并反绕到开始处。getgrent 从组文件中获取下一条记录 (如果组文件未打开则先打开它)。engrent 关闭组文件。

附属组 ID

  在早期的 UNIX 系统中,一个用户在任何时候都只属于一个组。当用户登录时,系统就取其口令文件项中记录的组 ID 作为它的实际组 ID。可以执行 newgrp(1) 更改组 ID,如果 newgrp 执行成功,则实际组 ID 就更改为新的组 ID,它将被用户后续的权限检查。执行不带任何参数的 newgrp 即可返回原来的组。

  从 BSD 4.2 开始,附属组 ID 的概念被引入。现在用户不仅可以属于口令文件中对应的一个组,也可以属于多至 16 个另外的组。文件访问权限检查相应被修改为:不仅将进程的有效组 ID 与对应文件的组 ID 比较,而且也将所有附属组 ID 与文件的组 ID 相比较。

  使用附属组 ID 的优点是不必再显式的来回更改组。以下 3 个函数用于获取和设置附属组 ID:

#include <unistd.h>
int getgroups(int gidsetsize, gid_t grouplist[]);
/* Returns: number of supplementary group IDs if OK, −1 on error */

#include <grp.h> /* on Linux */
#include <unistd.h> /* on FreeBSD, Mac OS X, and Solaris */
int setgroups(int ngroups, const gid_t grouplist[]);

#include <grp.h> /* on Linux and Solaris */
#include <unistd.h> /* on FreeBSD and Mac OS X */
int initgroups(const char *username, gid_t basegid);

/* Both return: 0 if OK, −1 on error */

  getgroups 将进程所属用户的各个附属组 ID 填写到数组 grouplist 中,并且需要传递填写的最大数量 gidsetsiz。实际填写的附属组 ID 数将作为返回值返回。

  特殊的,如果 gidsetsize 为 0,则函数只返回附属组 ID 数。

  setgroups 是特权函数,超级用户可以调用它来为调用进程设置附属组 ID 表。其中,grouplist 是组 ID 数组,而 ngroups 说明数组中的元素数。

  通常,只有 initgroups 函数调用 setgroups,initgroups 读整个组文件,然后对 username 确定其组成员关系,最后它调用 setgroups 来为该用户初始化附属组 ID 表。其中,basegidusername 在口令文件中对应的组 ID,这个组 ID 也会被设置到附属组 ID 表中。

  只有少数几个程序调用 initgroups 函数,入 login(1) 在用户登录时会调用它。

其他数据文件

  除了口令文件和组文件外,UNIX 系统还使用很多其他文件。一般情况下,对每个数据文件至少有 3 个函数:

  1. get 函数

  读下一个记录,如果需要,还会打开该文件。此函数通常返回一个指向某种结构的指针,当到达文件尾端时返回空指针。大多数 get 函数返回一个指向静态变量的指针,如果要保存它,则需要复制它。

  1. set 函数

  打开相应文件 (如果尚未打开),然后反绕该文件。如果需要从文件的起始位置开始处理,则调用它。

  1. end 函数

  关闭相应数据文件。


  如果数据文件支持某种形式的键搜索,则也提供搜素具有指定键的函数。例如,针对口令文件,有两个按键搜索的函数:getpwnam 和 getpwuid。

  下面是一些这样的例程:

登录账户记录

  大多数 UNIX 系统都提供下列两个数据文件:utmp 文件记录当前登录到系统的各个用户。wtmp 文件跟踪各个登录和注销事件。在 V7 中,每次写入这两个文件中的是一个结构的二进制记录:

struct utmp {
    char ut_line[8]; /* tty line: "ttyh0", "ttyd0", "ttyp0", ... */
    char ut_name[8]; /* login name */
    long ut_time;
    /* seconds since Epoch */
};

  登录时,login 程序填写此类型结构,然后将其写入到 utmp 文件中,同时也将其添写到 wtmp 文件中。注销时,init 进程将 utmp 文件中的相应记录擦除 (用 NULL 填充每个字段),并将一个新的记录添写到 wtmp 文件中。在 wtmp 文件的注销记录中,ut_name 字段清除为 0。在系统再启动时,以及更改系统事件和日期的前后,都在 wtmp 文件中追加写特殊的记录项。

  who(1) 程序读取 utmp 文件,并以可读格式打印其内容。后来的 UNIX 版本提供 last(1) 命令,它读 wtmp 文件并打印所选择的记录。

系统标识

  POSIX 定义了 uname 函数,它返回与主机和操作系统有关的信息:

#include <sys/utsname.h>
int uname(struct utsname *name);

/* Returns: non-negative value if OK, −1 on error */

  调用该函数需传递一个 utsname 结构的地址,该函数会填写此结构。POSIX 只定义了该结构中最少需提供的字段,每个字段的长度是由实现确定的:

struct utsname {
    char sysname[]; /* name of the operating system */
    char nodename[];/* name of this node */
    char release[]; /* current release of operating system */
    char version[]; /* current version of this release */
    char machine[]; /* name of hardware type */
};

  每个字段都以 NULL 字符结尾,utsname 结构中的信息可用 uname(1) 命令打印。

  历史上,BSD 派生的系统提供 gethostname 函数,它只返回主机名,该名字通常就是 TCP/IP 网络上主机的名字。现在,gethostname 已经在 POSIX 中定义。

#include <unistd.h>

int gethostname(char *name, int namelen);

/* Returns: 0 if OK, −1 on error */

  参数 namelen 指定 name 缓冲区的长度,如果缓冲区空间足够大,则通过 name 返回的字符串是以 NULL 结尾的,否则无法确定返回的字符串是否以 NULL 结尾。

  如果宿主机连接到 TCP/IP 网络中,则此主机名通常是该主机的完整域名。

  hostname(1) 命令可用来获取和设置主机名,超级用户可以使用函数 sethostname 设置主机名。

时间和日期

  UNIX 内核提供的基本时间服务是计算从协调世界时 (UTC) 这以特定时间以来所经过的秒数。这种秒数以数据类型 time_t 表示,它又被称为日历时间,日历时间包括时间和日期。

  time 函数返回当前时间和日期:

#include <time.h>

time_t time(time_t *calptr);

/* Returns: value of time if OK, −1 on error */

  时间值作为返回值返回,并且如果参数非空,则时间值也存放在 calptr 指向的单元内。

  POSIX 的实时扩展增加了对多个系统时钟的支持。在 SUS v4 中,控制这些时钟的接口从可选组被移至基本组。时钟通过 clockid_t 类型进行标识,下面是标准值:

  clock_gettime 函数用于获取指定时钟的时间,它将结果存放至 timespec 结构中,结果包含秒和纳秒:

#include <sys/time.h>

int clock_gettime(clockid_t clock_id, struct timespec *tsp);

/* Returns: 0 if OK, −1 on error */

  当时钟 ID 为 CLOCK_REALTIME 时,clock_gettime 函数的行为与 time 类似,区别在于:如果系统支持高精度时间值,那么 clock_gettime 可以得到比 time 更高精度的时间值。

#include <sys/time.h>

int clock_getres(clockid_t clock_id, struct timespec *tsp);

/* Returns: 0 if OK, −1 on error */

  clock_getres 函数把参数 tsp 指向的 timespec 结构初始化为与 clock_id 对应的时钟精度。例如:如果精度为 1 毫秒,则 tv_nsec 就是 1 000 000,tv_sec 就是 0。

  要对特定的时钟设置时间,可以调用 clock_settime 函数:

#include <sys/time.h>

int clock_settime(clockid_t clock_id, const struct timespec *tsp);

/* Returns: 0 if OK, −1 on error */

  改变时钟值需要适当的特权,但是有些时钟是不能修改的。

  SUS v4 指定 gettimeofday 已经弃用,但是,与 time 函数相比,gettimeofday 提供了更高的精度 (达到微秒级):

#include <sys/time.h>

int gettimeofday(struct timeval *restrict tp, void *restrict tzp);

/* Returns: 0 always */

  tzp 的唯一合法值是 NULL,其他值都将产生不确定的结果。

  gettimeofday 函数将距离 UTC 以来的秒数存放在 tp 指向的 timeval 结构中,该结构将当前时间表示为秒和微秒。取得这种用整数表示的时间值后,通常要调用函数将其转换为分解的时间结构:localtime 和 gmtime 将日历时间转换为分解的时间,并将结果存放在 tm 结构中:

struct tm {       /* a broken-down time */
    int tm_sec;   /* seconds after the minute: [0 - 60] */
    int tm_min;   /* minutes after the hour: [0 - 59] */
    int tm_hour;  /* hours after midnight: [0 - 23] */
    int tm_mday;  /* day of the month: [1 - 31] */
    int tm_mon;   /* months since January: [0 - 11] */
    int tm_year;  /* years since 1900 */
    int tm_wday;  /* days since Sunday: [0 - 6] */
    int tm_yday;  /* days since January 1: [0 - 365] */
    int tm_isdst; /* daylight saving time flag: <0, 0, >0 */
};

  各个时间函数之间的关系:

#include <time.h>

struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);

/* Both return: pointer to broken-down time, NULL on error */

  localtime 和 gmtime 之间的区别是:localtime 将日历时间准换成本地时间,而 gmtime 将日历时间转换为协调统一时间的年月日时分秒周日分解结构。

  函数 mktime 以本地时间的年月日等作为参数,将其变换为 time_t 的值:

#include <time.h>

time_t mktime(struct tm *tmptr);

/* Returns: calendar time if OK, −1 on error */

  函数 strftime 是一个类似 printf 的时间值函数,它非常复杂,可以用多个参数来定制字符串:

#include <time.h>

size_t strftime(char *restrict buf, size_t maxsize,
                const char *restrict format,
                const struct tm *restrict tmptr);

size_t strftime_l(char *restrict buf, size_t maxsize,
                  const char *restrict format,
                  const struct tm *restrict tmptr, locale_t locale);

/* Both return: number of characters stored in array if room, 0 otherwise */

  TODO:太复杂了,暂时没看。