6.1 口令文件
UNIX 口令文件(POSIX.1 则将其称为用户数据库)包含了表 6-1 中所示的各字段,这些字段包含在 <pwd.h> 中定义的 passwd 结构中。
/etc/passwd 文件中的字段。
由于历史原因,口令文件是 /etc/passwd,而且是一个文本文件。每一行包含表 6-1 中所示的 7 个字段,字段之间用冒号相分隔。
关于这些登录项,注意以下几点:
- 通常有一个登录项,其用户名为 root,其用户 ID 是 0(超级用户)。
- 加密口令字段包含了经单向密码算法处理过的用户口令副本。因为此算法是单向的,所以不能从加密口令猜测到原来的口令。
- 口令文件中的某些字段可能是空。如果密码口令字段为空,这通常就意味着该用户没有口令(不推荐这样做)。
- 支持 finger(1) 命令的某些 UNIX 系统支持注释字段中的附加信息。
POSIX.1 只定义了两个存取口令文件中信息的函数。在给出用户登录名或数值用户 ID 后,这两个函数就能查看相关记录。
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);
// 两个函数返回:若成功则为指针,若出错则为 NULL
有些程序要查看整个口令文件。下列三个函数则可用于此。
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwent(void);
// 返回:若成功则为指针,若出错或到达文件尾端则为 NULL
void setpwent(void);
void endpwent(void);
调用 getpwent 时,它返回口令文件中的下一个记录。如上面所述的两个 POSIX.1 函数一样,它返回一个由它填写好的 password 结构的指针。
函数 setpwent 反绕它所使用的文件,endpwent 则关闭这些文件。在使用 getpwent 查看完口令文件后,一定要调用 endpwent 关闭这些文件。
getpwnam 的一个实现。
#include <sys/types.h>
#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;
}
endpwent();
return(ptr);
}
在程序开始处调用 setpwent 是保护性的措施,以便在调用者在此之前已经调用过 getpwent 的情况下,反绕有关文件使它们定位到文件开始处。getpwnam 和 getpwuid 完成后不应使有关文件仍处于打开状态,所以应调用 endpwent 关闭它们。
6.2 阴影口令
对 UNIX 口令通常使用的加密算法是单向算法。给出一个密码口令,找不到一种算法可以将其反变换到普通文本口令。但是可以对口令进行猜测,将猜测的口令经单向算法变换成加密形成,然后将其与用户的加密口令相比较。如果用户口令是随机选择的,那么这种方法并不是很有用。但是用户往往以非随机方式选择口令(配偶的姓名、街名、宠物名等 )。一个经常重复的试验是先得到一份口令文件,然后用试探方法猜测口令。
为使企图这样做的人难以获得原始资料(加密口令),某些系统将加密口令存放在另一个通常称为阴影口令(shadow password)的文件中。该文件至少要包含用户名和加密口令。与该口令相关的其他信息也可存放在该文件中。
6.3 组文件
UNIX 组文件包含了下表中所示字段。这些字段包含在 <grp.h> 中所定义的 group 结构中。
字段 gr_mem 是一个指针数组,其中的指针各指向一个属于该组的用户名。该数组以 null 结尾。
使用下列两个函数来查看组名或数值组 ID。
#include <sys/types.h>
#include <grp.h>
struct group *getgrgid(gid_t gid);
struct group *getgrnam(const char *name);
// 两个函数返回:若成功则为指针,若出错则为 NULL
如同对口令文件进行操作的函数一样,这两个函数通常也返回指向一个静态变量的指针,在每次调用时都重写该静态变量。
如果需要搜索整个组文件,则须使用另外几个函数。下列三个函数类似于针对口令文件的三个函数。
#include <sys/types.h>
#include <grp.h>
struct group *getgrent(void);
// 返回:若成功则为指针,若出错或到达文件尾端则为 NULL
void setgrent(void);
void endgrent(void);
setgrent 打开组文件(如若它尚未被打开)并反绕它。getgrent 从组文件中读下一个记录,如若该文件尚未打开则先打开它。endgrent 关闭组文件。
6.4 添加组 ID
使用添加组 ID 的优点是不必再显式地经常更改组。一个用户常常会参加多个项目,因此也就要同时属于多个组。
为了存取和设置添加组 ID 提供了下列三个函数:
#include <sys/types.h>
#include <unistd.h>
int getgroups(int gidsetsize, git_t grouplist[]);
// 返回:若成功则为添加的组 ID 数,若出错则为 -1
int setgroups(int ngroups, const gid_t grouplist[]);
int initgroups(const char *username, gid_t basegid);
// 两个函数返回:若成功则为0,若出错则为 -1
getgroups 将进程所属用户的各添加组 ID 填到数组 grouplist 中,填写入该数组的添加组 ID 数最多为 gidsetsize 各。实际填写到数组中的添加组 ID 数由函数返回。如果系统常熟 NGROUPS_MAX 为 0,则返回 0,这并不表示出错。
作为一种特殊情况,如若 gidsetsize 为 0,则函数只返回添加组 ID 数,而对数组 grouplist 则 不作修改。
setgroups 可由超级用户调用以便为调用进程设置添加组 ID 表。grouplist 是组 ID 数组,而 ngroups 说明了数组中的元素数。
6.5 其他数据文件
一般情况下每个数据文件至少有三个函数:
- get 函数:读下一个记录,如果需要还打开该文件。此种函数通常返回指向一个结构的指针。当已达到文件尾端时返回空指针。大多数 get 函数返回指向一个静态存储类结构的指针, 如果要保存其内容,则需复制它。
- set 函数:打开相应数据文件(如果尚末打开),然后反绕该文件。如果希望在相应文件起始处开始处理,则调用此函数。
- end 函数:关闭相应数据文件。正如前述,在结束了对相应数据文件的读、写操作后,总应调用此函数以关闭所有相关文件。
另外,如果数据文件支持某种形式的关键字搜索,则也提供搜索具有指定关键字的记录的例程。例如,对于口令文件提供了两个按关键字进行搜索的程序:getpwnam 寻找具有指定用 户名的记录;getpwuid 寻找具有指定用户 ID 的记录。
下表中列出了一些这样的例程。
6.6 登录会计
大多数 UNIX 系统都提供下列两个数据文件:utmp 文件,它记录当前登录进系统的各个用户;wtmp 文件,它跟踪各个登录和注销事件。
V7 中,包含下列结构的一个二进制记录写进这两个文件中:
struct utmp {
char ut_line[8]; /* tty line: "ttyh0", "ttyd0", "ttyp0", ... */
char ut_name[8]; /* login name */
char ut_time; /* seconds since Epoch */
}
登录时,login 程序填写这样一个结构,然后将其写入到 utmp 文件中,同时也将其添加到 wtmp 文件中。注销时,init 进程将 utmp 文件中像一个的记录擦除(每个字节都填 0),并将一个新纪录添写道 wtmp 文件中。读 wtmp 文件中的该注销记录,其 ut_name 字段清除为 0。在系统再启动时,以及更改系统时间和日期的前后,都在 wtmp 文件中添写特殊的记录项。who(1) 程序读 utmp 文件,并以可读格式打印其内容。后来的 UNIX 版本提供 last(1) 命令,它读 wtmp 文件并打印所选择的记录。
6.7 系统标识
uname 函数返回与主机和操作系统有关的信息。
#include <sys/utsname.h>
int uname(struct utsname *name);
// 返回:若成功则为非负值,若出错则为 -1
通过该函数的参数向其传递一个 utsname 结构的地址,然后该函数填写此结构。
struct utsname {
char sysname[9]; /* name of the operating system */
char nodename[9]; /* name of this node */
char release[9]; /* current release of operating system */
char version[9]; /* current version pf this release */
char machine[9]; /* name of hardware type */
};
utsname 结构中的信息通常可用 uname(1) 命令打印。
gethostname 函数只返回主机名,该名字通常就是 TCP/IP 网络上主机的名字。
#include <unistd.h>
int gethostname(char *name, int namelen);
// 返回:若成功则为 0,若出错则为 -1
6.8 时间和日期例程
由 UNIX 内核提供的基本时间服务是国际标准时间公元 1970 年 1 月 1 日 00:00:00 以来经过的秒数。UNIX 在这方面与其他操作系统的区别是:(a) 以国际标准时间而非本地时间计时;(b) 可自动进行变换,例如变换到夏日制;© 将时间和日期作为一个量值保存。time 函数返回当前时间和日期。
#include <time.h>
time_t time(time_t *calptr);
// 返回:若成功则为时间值,若出错则为 -1
时间值作为函数值返回。如果参数非 null,则时间值也存放在由 calptr 指向的单元内。
一旦取得这种以秒计的很大的时间值后,通常要调用另一个时间函数将其变换为人们可读的时间和日期。下图说明了各种时间函数之间的关系。
两个函数 localtime 和 gmtime 将日历时间变换成以年、月、日、时、分、秒、周日表示的时间,并将这些存放在一个 tm 结构中。
struct tm { /* a broken-down time */
int tm_sec; /* seconds after the minute: [0, 61] */
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; /* month of the year: [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 */
};
秒可以超过 59 的理由是可以表示润秒。注意,除了月日字段,其他字段的值都以 0 开始。如果夏时制生效,则夏时制标志值为正;如果已非夏时制时间则为 0;如果此信息不可用,则为负。
#include <time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);
// 两个函数返回:指向 tm 结构的指针
localtime 和 gmtime 之间的区别是:localtime 将日历时间变换成本地时间(考虑到本地时区和夏时制标志),而 gmtime 则将日历时间变换成国际标准时间的年、月、日、时、分、秒、周日。
函数 mktime 以本地时间的年、月、日等作为参数,将其变换成 time_t 值。
#include <time.h>
time_t mktime(struct tm *tmptr);
// 返回:若成功则为日历时间,若出错则为 -1
asctime 和 ctime 函数产生形式的 26 字节字符串,这与 date(1) 命令的系统默认输出形式类似:
Tue Jan 14 17 : 49 : 03 1992\n\0
#include <time.h>
char *asctime(const struct tm *tmptr);
char *ctime(const time_t *calptr);
// 两个函数返回:指向 null 结尾的字符串
asctime 的参数是指向年、月、日等字符串的指针,而 ctime 的参数则是指向日历时间的指针。
最后一个时间函数是 strftime,它是非常复杂的 printf 类的时间值函数。
#include <time.h>
size_t strftime(char *buf, size_t maxsize, const char *format, const struct tm *tmptr);
// 返回:若有空间,则存入数组的字符数,否则为 0
format 参数控制时间值的格式。如同 printf 函数一样,变换说明的形式是百分号之后跟一个特定字符。下表列出了 21 种 ANSI C 规定的变换说明。
表中第三列的数据来自于在 SVR4,对应于下列时间和日期,执行 strftime 函数所得的结果为:
Tue Jan 14 19 : 40 : 30 MST 1992