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 表。其中,basegid 是 username 在口令文件中对应的组 ID,这个组 ID 也会被设置到附属组 ID 表中。
只有少数几个程序调用 initgroups 函数,入 login(1) 在用户登录时会调用它。
其他数据文件
除了口令文件和组文件外,UNIX 系统还使用很多其他文件。一般情况下,对每个数据文件至少有 3 个函数:
- get 函数
读下一个记录,如果需要,还会打开该文件。此函数通常返回一个指向某种结构的指针,当到达文件尾端时返回空指针。大多数 get 函数返回一个指向静态变量的指针,如果要保存它,则需要复制它。
- set 函数
打开相应文件 (如果尚未打开),然后反绕该文件。如果需要从文件的起始位置开始处理,则调用它。
- 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:太复杂了,暂时没看。