Unix系统数据文件和信息(六)

本文主体内容来自《UNIX环境高级编程第三版》。

UNIX系统的正常运作需要使用大量与系统有关的数据文件,例如,口令文件/etc/passwd和组文件/etc/group就是经常被多个程序频繁使用的两个文件。用户每次登录UNIX系统,以及每次执行ls -l命令时都要使用口令文件。

由于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库读这些文件。但是,对于较大的系统,顺序扫描口令文件很花费时间,我们需要能够以非ASCII文本格式存放这些文件,但仍向使用其他文件格式的应用程序提供接口。对于这些数据文件的可移植接口是本章的主题。本章也包括了系统标识函数、时间和日期函数。


口令文件

UNIX 系统口令文件(POSIX.1 则将其称为用户数据库)包含了下图中所示的各字段,这些字段包含在 <pwd.h> 中定义的 passwd 结构中。

注意,POSIX.1只指定 passwd 结构包含的10个字段中的5个。大多数平台至少支持其中7个字段。BSD派生的平台支持全部10个字段。

img

由于历史原因,口令文件是 /etc/passwd,而且是一个 ASCII 文件。每一行包含上图所示的各字段,字段之间用冒号分隔。例如,在Linux中,该文件中可能有下列4行:

1
2
3
4
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

POSIX.1定义了两个获取口令文件项的函数。在给出用户登录名或数值用户ID后,这两个函数就能查看相关项。

1
2
3
4
#include<pwd.h>
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);
// 两个函数返回值:若成功,返回指针;若出错,返回NULL

getpwuid 函数由 ls 程序使用,它将i节点中的数字用户ID映射为用户登录名。在键入登录名时,getpwnam 函数由 login 程序使用。

这两个函数都返回一个指向 passwd 结构的指针,该结构已由这两个函数在执行时填入信息。passwd 结构通常是函数内部的静态变量,只要调用任一相关函数,其内容就会被重写。

如果要查看的只是登录名或用户ID,那么这两个POSIX.1函数能满足要求,但是也有些程序要查看整个口令文件。下列3个函数则可用于此种目的。

1
2
3
4
5
#include <pwd.h>
struct passwd *getpwent(void);
// 返回值:若成功,返回指针;若出错或到达文件尾端,返回NULL
void setpwent(void);
void endpwent(void);

基本POSIX.1标准没有定义这3个函数。在Single UNIX Specification中,它们被定义为XSI扩展。因此,可预期所有UNIX实现都将提供这些函数。

调用 getpwent 时,它返回口令文件中的下一个记录项。如同上面所述的两个POSIX.1函数一样,它返回一个由它填写好的 passwd 结构的指针。每次调用此函数时都重写该结构。在第一次调用该函数时,它打开它所使用的各个文件。在使用本函数时,对口令文件中各个记录项的安排顺序并无要求。某些系统采用散列算法对 /etc/passwd文件中各项排序。

函数 setpwent 反绕(重置位置到开头)它所使用的文件,endpwent 则关闭这些文件。在使用 getpwent 查看完口令文件后,一定要调用 endpwent 关闭这些文件。getpwent 知道什么时间应当打开它所使用的文件(第一次被调用时),但是它并不知道何时关闭这些文件。


阴影口令

某些系统将加密口令存放在另一个通常称为阴影口令(shadow password)的文件中。该文件至少要包含用户名和加密口令。与该口令相关的其他信息也可存放在该文件 /etc/shadow 中。

img

只有用户登录名和加密口令这两个字段是必须的。其他的字段控制口令更改的频率,或者说口令的衰老以及账户仍然处于活动状态的时间。

阴影口令文件不应是一般用户可以读取的。仅有少数几个程序需要访问加密口令,如 loginpasswd,这些程序常常是设置用户 ID 为 root 的程序。

在Linux 3.2.0和Solaris 10中,有一组函数可用于访问阴影口令文件。

1
2
3
4
5
6
#include <shadow.h>
struct spwd *getspnam(const char *name);
struct spwd *getspent(void);
// 两个函数返回值:若成功,返回指针;若出错,返回NULL
void setspent(void);
void endspent(void);

组文件

UNIX组文件 /etc/group(POSIX.1称其为组数据库)包含了下图所示字段。这些字段包含在 <grp.h>中所定义的 group 结构中。

img

字段 gr_mem 是一个指针数组,其中每个指针指向一个属于该组的用户名。该数组以 null 指针结尾。可以用下列两个由POSIX.1定义的函数来查看组名或数值组ID。

1
2
3
4
#include <grp.h>
struct group *getgrgid(gid_t gid);
struct group *getgrnam(const char *name);
// 两个函数返回值:若成功,返回指针;若出错,返回NULL

如同对口令文件进行操作的函数一样,这两个函数通常也返回指向一个静态变量的指针,在每次调用时都重写该静态变量。

如果需要搜索整个组文件,则须使用另外几个函数。

1
2
3
4
5
#include <grp.h>
struct group *getgrent(void);
// 返回值:若成功,返回指针;若出错或到达文件尾端,返回NULL
void setgrent(void);
void endgrent(void);

setgrent 函数打开组文件(如若它尚末被打开)并反绕它。getgrent 函数从组文件中读下一个记录,如若该文件尚未打开,则先打开它。endgrent 函数关闭组文件。


附属组ID

4.2BSD引入了附属组ID(supplementary group ID)的概念。我们不仅可以属于口令文件记录项中组ID所对应的组,也可属于多至16个另外的组。文件访问权限检查相应被修改为:不仅将进程的有效组ID与文件的组ID相比较,而且也将所有附属组ID与文件的组ID进行比较。

附属组 ID 是 POSIX.1 要求的特性。常量 NGROUPS_MAX 规定了附属组ID的数量,其常用值是16。

使用附属组 ID 的优点是不必再显式地经常更改组。一个用户会参与多个项目,因此也就要同时属于多个组,此类情况是常有的。

为了获取和设置附属组ID,提供了下列3个函数。

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
int getgroups(int gidsetsize, gid_t grouplist[]);
// 返回值:若成功,返回附属组ID数量;若出错,返回-1

#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);
// 两个函数的返回值:若成功,返回0;若出错,返回-1

getgroups 将进程所属用户的各附属组ID填写到数组 grouplist 中,填写入该数组的附属组ID数最多为 gidsetsize 个。实际填写到数组中的附属组ID数由函数返回。

作为一种特殊情况,如若 gidsetsize 为0,则函数只返回附属组ID数,而对数组 grouplist 则不做修改。

setgroups 可由超级用户调用以便为调用进程设置附属组ID表。grouplist 是组ID数组,而 ngroups 说明了数组中的元素数。ngroups 的值不能大于NGROUPS_MAX

通常,只有 initgroups 函数调用 setgroupsinitgroups 读整个组文件(用前面说明的函数 getgrentsetgrentendgrent),然后对 username 确定其组的成员关系。然后,它调用 setgroups,以便为该用户初始化附属组ID表。因为initgroups要调用 setgroups,所以只有超级用户才能调用 initgroups。除了在组文件中找到 username 是成员的所有组, initgroups 也在附属组ID表中包括了 basegidbasegidusername 在口令文件中的组ID。

只有少数几个程序调用 initgroups,例如 login 程序在用户登录时调用该函数。


其他数据文件

在日常操作中,UNIX系统还使用很多其他文件。例如,BSD网络软件有一个记录各网络服务器所提供服务的数据文件(/etc/services),有一个记录协议信息的数据文件(/etc/protocols),还有一个则是记录网络信息的数据文件(/etc/networks)。幸运的是,对于这些数据文件的接口都与上述对口令文件和组文件的相似。

一般情况下,对于每个数据文件至少有3个函数。

(1)get函数:读下一个记录,如果需要,还会打开该文件。此种函数通常返回指向一个结构的指针。当已达到文件尾端时返回空指针。大多数get函数返回指向一个静态存储类结构的指针,如果要保存其内容,则需复制它。

(2)set 函数:打开相应数据文件(如果尚末打开),然后反绕该文件。如果希望在相应文件起始处开始处理,则调用此函数。

(3)end函数:关闭相应数据文件。如前所述,在结束了对相应数据文件的读、写操作后,总应调用此函数以关闭所有相关文件。

另外,如果数据文件支持某种形式的键搜索,则也提供搜索具有指定键的记录的例程。例如,对于口令文件,提供了两个按键进行搜索的程序:getpwnam寻找具有指定用户名的记录;getpwuid 寻找具有指定用户ID的记录。

下图列出了一些这样的例程,这些都是UNIX常用的。对于图中列出的所有数据文件都有get、set和end函数。

img


登录账户记录

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

1
2
3
4
5
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 程序读取 utmp 文件,并以可读格式打印其内容。后来的UNIX版本提供 last命令,它读 wtmp 文件并打印所选择的记录。


系统标识

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

1
2
3
#include <sys/utsname.h>
int uname(struct utsname *name);
// 返回值:若成功,返回非负值;若出错,返回-1

通过该函数的参数向其传递一个 utsname 结构的地址,然后该函数填写此结构。POSIX.1只定义了该结构中最少需提供的字段(它们都是字符数组),而每个数组的长度则由实现确定。某些实现在该结构中提供了另外一些字段。

1
2
3
4
5
6
7
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字节结尾。


时间和日期例程

由UNIX内核提供的基本时间服务是计算自协调世界时(Coordinated Universal Time,UTC)公元1970年1月1日00:00:00这一特定时间以来经过的秒数。这种秒数是以数据类型 time_t 表示的,称它们为日历时间。日历时间包括时间和日期。UNIX在这方面与其他操作系统的区别是:(a)以协调统一时间而非本地时间计时;(b)可自动进行转换,如变换到夏令时;(c)将时间和日期作为一个量值保存。

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

1
2
3
#include <time.h>
time_t time(time_t *calptr);
// 返回值:若成功,返回时间值;若出错,返回-1

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

clock_gettime 函数可用于获取指定时钟的时间,返回的时间在 timespec 结构中,它把时间表示为秒和纳秒。

1
2
3
#include <sys/time.h>
int clock_gettime(clockid_t clock_id, struct timespec *tsp);
// 返回值:若成功,返回0;若出错,返回-1

当时钟ID设置为 CLOCK_REALTIME 时,clock_gettime 函数提供了与time函数类似的功能,不过在系统支持高精度时间值的情况下,clock_gettime 可能比 time 函数得到更高精度的时间值。

1
2
3
#include <sys/time.h>
int clock_getres(clockid_t clock_id, struct timespec *tsp);
// 返回值:若成功,返回0;若出错,返回-1

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

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

1
2
3
#include <sys/time.h>
int clock_settime(clockid_t clock_id, const struct timespec *tsp);
// 返回值:若成功,返回0;若出错,返回-1

一旦取得这种从上述特定时间经过的秒数的整型时间值后,通常要调用函数将其转换为分解的时间结构,然后调用另一个函数生成人们可读的时间和日期。下图说明了各种时间函数之间的关系。

img

两个函数 localtimegmtime 将日历时间转换成分解的时间,并将这些存放在一个tm结构中。

1
2
3
4
5
6
7
8
9
10
11
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 */
};
1
2
3
4
#include <time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);
// 两个函数的返回值:指向分解的tm结构的指针;若出错,返回NULL

localtimegmtime 之间的区别是:localtime 将日历时间转换成本地时间(考虑到本地时区和夏令时标志),而 gmtime 则将日历时间转换成协调统一时间的年、月、日、时、分、秒、周日分解结构。

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

1
2
3
#include <time.h>
time_t mktime(struct tm *tmptr);
// 返回值:若成功,返回日历时间;若出错,返回-1

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

1
2
3
4
#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);
// 两个函数的返回值:若有空间,返回存入数组的字符数;否则,返回0

strftime_l 允许调用者将区域指定为参数,除此之外,strftimestrftime_l 函数是相同的。

tmptr 参数是要格式化的时间值,由一个指向分解时间值 tm 结构的指针说明。格式化结果存放在一个长度为 maxsize 个字符的buf数组中,如果buf长度足以存放格式化结果及一个null终止符,则该函数返回在buf中存放的字符数(不包括null终止符);否则该函数返回0。

format参数控制时间值的格式。如同printf函数一样,转换说明的形式是百分号之后跟一个特定字符。format中的其他字符则按原样输出。两个连续的百分号在输出中产生一个百分号。下图列出了37种ISO C规定的转换说明。

img

strptime 函数是 strftime 的反过来版本,把字符串时间转换成分解时间。

1
2
3
#include <time.h>
char *strptime(const char *restrict buf, const char *restrict format, struct tm *restrict tmptr);
// 返回值:指向上次解析的字符的下一个字符的指针;否则,返回NULL

format 参数给出了buf参数指向的缓冲区内的字符串的格式。虽然与 strftime 函数的说明稍有不同,但格式说明是类似的。strptime函数转换说明符列在下图。

img