UNIX环境高级编程-第六章-系统数据文件和信息
1.引言
UNIX系统的正常运作需要适用大量与系统有关的数据文件,例如,口令文件/etc/passwd和组文件/etc/group就是经常被多个程序频繁适用的两个文件。用户每次登录UNIX系统,以及每次执行ls -l命令时都要使用口令文件。
由于历史原因,这些数据文件都是ASCLL文本文件,并且使用标准IO库读这些文件。但是,对于较大的系统,顺序扫描口令文件很花费时间,我们需要能够以非ASCII文本格式存放这些文件,但仍向使用其他文件格式的应用程序提供接口,对于这些数据文件的可移植接口是本章的主题。本章也包括了系统标识函数,时间和日期函数。
2.口令文件
UNIX系统口令文件(POSIX.1则将其称为用户数据库)包含了图6-1中所示的各字段,这些字段包含了在<pwd.h>中定义的passwd结构中。
由于历史原因,口令文件是/etc/passwd,而且是一个ASCII文件。每一行包含图6-1中所示的各字段,字段之间用冒号分隔。例如,在Linux中,该文件中可能有下列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
这些登录项,请注意下列各点:
(1)通常有一个用户名为root的登录项,其用户ID是0(超级用户)
(2)加密口令字段包含了一个占位符。较早期的UNIX系统版本中,该字段存放加密口令字。将加密口令字存放在一个人人刻度的文件中是一个安全性漏洞,所以现在将加密口令字存放在另一个文件中。在下一节讨论口令字时,我们再详细解释
(3)口令文件项中的某些字段可能是空。如果加密口令字段为空,这通常就意味着该用户没有口令。squid登录项有一空白字段:注释字段,空白注释字段不产生任何影响。
(4)shell字段包含了一个可执行程序名,它被用作该用户的登录shell。若该字段为空,则取系统默认值,通常是/bin/sh。注意,squid登录项的该字段为/dev/null。显然,这是一个设备,不是可执行文件,将启用于此处的目的是,阻止任何人以用户squid的名义登录到该系统。
(5)为了阻止一个特定用户登陆系统,除使用/dev/null外,还有若干种替代方法。常见的一种方法是,将/bin/false用作登录shell。它简单地以不成功状态终止,该shell将此种终止状态判断为假。另一种常见方法是,用/bin/true禁止一个账户。它所做的一切是以成功状态终止。某些系统提供nologin命令,它打印可定制的出错信息,然后以非0状态终止。
(6)使用nobody用户名的一个目的是,是任何人都可以登录至系统,但其用户ID(65534)和组ID(65534)不提供任何特权。该用户ID和组ID只能访问人人皆可读,写的文件。(假定用户ID65534和组ID65534并不拥有任何文件)
(7)提供finger命令的某些UNIX系统支持注释字段种的附加信息。其中各部分之间用逗号分隔:用户姓名,办公室地点,办公室电话号码以及家庭电话号码等。另外,如果注释字段中的用户姓名是一个&,则它被替换为登录名。例如,可以有下列记录:
sar:x :205:105:Steve Rago, SF 5-121,
555-1111,555-2222:/home/sar:/bin/sh 使用finger命令就可以打印Steve Rago的有关信息
某些系统提供了vipw命令,允许管理员使用该命令编辑口令文件。vipw命令串行化地更改口令文件,并且确保它所作的更改与其他相关文件保持一致。系统也常常经由图形用户界面提供类似的功能。
POSIX.1定义了两个获取口令文件项的函数。在给出用户登录名或数值用户ID后,这两个函数就能查看相关项。
#include<pwd.h>
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);
getpwuid函数由ls程序使用,它将i节点种的数字用户ID映射为用户登录名。在键入登录名时,getpwnam函数由login程序使用。
这两个函数都返回一个指向passwd结构的指针,该结构已由这两个函数在执行时填入信息。passwd结构通常是函数内部的静态变量,只要调用任一相关函数,其内容就会被重写。
如果要查看的只是登录名或用户ID,那么这两个函数能满足要求,但是也有些程序要查看整个口令文件。下列3个函数可用于此种目的。
#include <pwd.h>
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
调用getpwent时,它返回口令文件中的下一个记录项。如同上面所述的两个POSIX.1函数一样,它返回一个由它填写好的passwd结构的指针。每次调用此函数时都重写该结构。
函数setpwent反绕它所使用的文件,endpwent则关闭这些文件。在使用getpwent查看完口令文件后,一定要调用endpwent关闭这些文件。getpwent知道什么时间应当打开它所使用的文件(第一次被调用时),但是它并不知道何时关闭这些文件。
实例6-2:给出了getpwnam函数的一个实现。
#include <pswd.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关闭它们。
3.阴影口令
加密口令是经单向加密算法处理过的用户口令副本。因为此算法是单向的,所以不能从加密口令猜测到原来的口令。
对于一个加密口令,找不到一种算法可以将其反变换到明文口令。但是可以对口令进行猜测,将猜测的口令经单向算法变换成加密形式,然后将其与用户的加密口令比较。如果用户口令是随机选择的,那么这种方法并不是很有用。但是用户往往以非随机方式选择口令。一个经常重复的实验是先得到一份口令文件,然后试探猜测口令。
为了使企图这样做的人难以获得原始资料,现在,某些系统将加密口令存放在另一个通常称为阴影口令的文件中。该文件至少包含用户名和加密口令。与该口令相关的其他信息也可存放在该文件中(图 6-3)。
只有用户登录名和加密口令这两个字段是必须的。其他的字段控制口令更改的频率,阴影口令文件不应是一般用户可以读取的。仅有少数几个程序需要访问加密口令,如login和passwd,这些程序常常是设置用户ID为root的程序。有了阴影口令后,普通口令文件/etc/passwd可由各用户自由读取。
与访问口令文件的一组函数相类似,有另一组函数而可用于访问阴影口令文件。
#include<shadow.h>
struct spwd *getspnam(cosnt char *name);
struct spwd *getspent(void);
void setspent(void);
void endspent(void);
4.组文件
UNIX组文件包含了图6-4中所示的字段。这些字段包含在<grp.h>中所定义的group结构中。
字段gr_mem是一个指针数组,其中每个指针指向一个属于该组的用户名。该数组以null指针结尾。可以用下列两个由POSIX.1定义的函数来产看组名或数值组ID。
#include <grp.h>
struct group *getgrgid(gid_t gid);
struct group *getgrnam(const char *name);
如同对口令文件进行操作的函数一样,这两个函数通常也返回指向一个静态变量的指针,在每次调用时都重写该静态变量。
如果需要搜索整个组文件,则需使用另外几个函数。下列3个函数类似于针对口令文件的3个函数。
#include <grp.h>
struct group *getgrent(void);
void setgrent(void);
void endgrent(void);
setgrent函数打开组文件并反绕它。getgrent函数从组文件读取下一个记录,如若该文件尚未打开,则先打开它。endgrent函数关闭组我呢见。
5.附属组ID
在UNIX系统中,对组的使用已经作了些修改。在v7中,每个用户任何时候都只属于一个组。当用户登陆时,系统就按口令文件记录项中的数值组ID,赋给他实际组ID。可以在任何时候执行newgrp以更改组ID。如果newgrp命令执行成功,则实际组ID就更改为新的组ID,它将被用于后续的文件访问权限检查。执行不带任何参数的newgrp,则可返回到原来的组。
这种组成员形式一致维持到1983年左右。此时,4.2BSD引入了附属组ID的概念。我们不仅可以属于口令文件记录项中组ID所对应的组,也可属于多至16个另外的组。文件访问权限检查相应被修改为:不仅将进程的有效组ID与文件的组ID相比较,而且也将所有附属组ID与文件组ID进行比较。
使用附属组ID的优点是不必再显示地经常更改组。一个用户会参加多个项目,因此也就要同时属于多个组,此类情况是常有的。
为了获取和设置附属组ID,提供了下列3个函数。
#include <unistd.h>
#include <grp.h>
#include <unistd.h>
int setgroups(int ngroups,const gid_t grouplist[]);
int initgroups(const char *username,gid_t basegid);
int getgroup(int gidsetsize,gid_t grouplist[])
getgroups将进程所属用户的各附属组ID填写到数组grouplist中,填写入该数组的附属组ID数最多为gidsetsize个。实际填写到数组中的附属组ID数由函数返回。
作为一种特殊情况,如若gidsetsize为0,则函数只返回附属组ID数,而对数组grouplist则不做修改。
setgroups可有超级用户调用以便为调用进程设置附属组ID表。grouplist是组ID数组,而ngroups说明了数组中的元素数。ngroups的值不能大于NGROUPS_MAX。
通常,只有initgroups函数调用setgroups,initgroups读整个组文件(用前面说明的函数getgrent,setgrent和endgrent),然后对username确定其组的成员关系。然后它调用setgroups,以便为该用户初始化附属组ID表。因为initgroups要调用setgroups,所以只有超级用户才能能调用initgroups。除了在组文件中找到username是成员的所有组,initgroups也在附属组ID表中包括了basegid。basegid是username在口令文件中的组ID。
6.其他数据文件
至此讨论了两个系统数据文件-口令文件和组文件。
在磁场操作中,UNIX系统还是用很多其他文件。例如BSD网络软件有一个记录各网络服务器所提供服务的数据文件(/etc/services),有一个记录协议信息的数据文件(/etc/protocols),还有一个则是记录网络信息的数据文件(/etc/networks)。幸运的是,对于这些数据文件的接口都与上述对口令文件和组文件的相似。
一般情况下,对于每个数据文件至少有3个函数。
(1)get函数:读下一个记录,如果需要,还会打开该文件。此种函数通常返回指向一个结构的指针。当已到达文件尾端时返回空指针。大多数get函数返回指向一个静态存储类结构的指针,如果要保存其内容,则需要复制它。
(2)set函数:打开相应数据文件(如果尚未打开),然后反绕该文件。如果希望在相应文件起始处开始处理,则调用此函数。
(3)end函数:关闭相应数据文件。如前所述,在结束了对相应数据文件的读,写操作后,总应调用此函数以关闭所有相关文件。
另外,如果数据文件支持某种形式的键搜索,则也提供搜索具有指定键的记录的例程。例如,对于口令文件,提供了两个按键进行搜索的程序:getpwnam和getpwuid用于指定用户的记录。
图6-6列出了一些这样的例程。对于图中列出的所有数据文件都有get,set和end函数。
7.登录账户记录
大多数UNIX系统都提供了下列两个数据文件:utmp文件记录当前登录到系统的各个用户;wtmp文件跟踪各个登录和注销事件。在v7中,每次写入这两个文件中的是包含下列结构的一个二进制记录;
struct utmp {
char ut_line[8];
char ut_name[8];
long ut_time;
}
登陆时,login程序填写此类结构,然后将其写入到utmp文件中,同时也将其添写到wtmp文件中。注销时,init进程将utmp文件中相应的记录擦除,并将一个新纪录添写到wtmp文件中。在wtmp文件的注销记录中,ut_name字段清除为0.在系统再启动时,以及更改系统事件和日期的前后,都在wtmp文件中追加写特殊的记录项。
8.系统标识
POSIX.1定义了uname函数,它返回与主机和操作系统有关的信息。
#include <sys/utsname.h>
int uname(struct utsname *name);
通过该函数的参数向其传递一个utsname结构的地址,然后该函数添写此结构。POSIX.1只定义了该结构中最少需提供的字段,而每个数组的长度则由实现确定。某些实现在该结构中提供了另外一些字段。
struct utsname {
char sysname[];
char nodename[];
char release[];
char version[];
char machine[];
}
每个字符串都以null字节结尾。本书讨论的4中平台支持的最大名字长度列于图6-7中。utsname结构中的信息通常可用uname命令打印。
BSD派生的系统提供gethostname函数,它只返回主机名,该名字通常就是TCP/IP网络上主机的名字。
#include <unistd.h>
int gethostname(char *name,int namelen);
namelen参数指定name缓冲区长度,如若提供足够的空间,则通过name返回的字符串以null字节结尾。如若没有提供足够的空间,则没有说明通过name返回的字符串是否以null结尾。
现在,getihostname函数已在POSIX.1中定义,它指定最大主机名长度是HOST_NAME_MAX。
hostname命令可用来获取和设置主机名。主机名通常在系统自举时设置,它由/etc/rc或init取自一个启动文件。
9.时间和日期例程
由UNIX内核提供的基本时间服务是计算自协调世界时公元1970年1月1日00:00:00这一特定时间以来经过的秒数。1.10节中曾提及这种秒数以数据类型time_t表示的。我们称它们为日历时间。日历时间包括时间和日期。UNIX在这方面与其他操作系统的区别是:
(1)以协调统一时间而非本地时间计时;
(2)可自动进行转换,如变换到夏令时
(3)将时间和日期作为一个量值保存
time函数返回当前时间和日期
#include <time.h>
time_t time(time_t *calptr)
时间值作为函数值返回。如果参数非空,则时间值也存放在由calptr指向的单元内。
POSIX的实时扩展增加了对多个系统时钟的支持。在Single UNIX Specification V4中,控制这些时钟的接口从可选组被移到基本组。时钟通过clockid_t类型进行标识。图6-8给出了标准值。
clock_gettime函数用于获取指定时钟的时间,返回的时间在timespec结构中,它把时间表示为秒和纳秒。
#include <sys/time.h>
int clock_gettime(clockid_t clock_id,struct timespec *tsp);
当前时钟ID设置为CLOCK_REATIME时,clock_gettime函数提供了与time函数类似的功能,不过在系统支持高精度时间值的情况下,clock_gettime可能比time函数得到更高精度的时间值。
#include<sys/time.h>
int clock_getres(clockid clock_id,struct timespec *tsp);
clock_getres函数把参数tsp指向的timespec结构初始化为与clock_id参数对应的时钟精度。例如如果精度为一毫秒,则tv_sec字段就是0,tv_nsec字段就是1000000。
要对特定的时钟设置时间,可以调用clock_settime函数
#include <sys/time.h>
int clock_settime(clockid_t clock_id,const struct timespec *tsp);
我们需要适当的特权来更改时钟值,但是有些时钟是不饿能修改的。
图6-9说明了各种时间函数之间的关系。
两个函数localtime和gmtime将日历时间转换成分解的时间,并将这些存放在一个tm结构中。
struct tm{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
}