第六章〓系统数据文件和信息? 6?1〓引言? 有很多操作需要使用一些与系统有关的数据文件,例如,口令字文件/etc/passwc l和组文件 /etc/group就是经常由多种程序使用的两个文件。用户每次登录入Unix系统时,以 及每次执 行ls-l命令时都要使用口令字文件。? 由于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库读这些文件 。但是, 对于较大的系统,顺序扫描口令字文件变得很花费时间,我们想以非ASCII文本格 式存放这 些文件,但仍向应用程序提供一个可以处理任何一种文件格式的界面。对于这些数 据文件的 可移植界面是本章的主题。本章也包括了系统标识函数、时间和日期函数。? 6?2〓口令字文件? Unix口令字文件(POSIX?1则将其称为用户数据库)包含了图6?1中所示的各字段, 这些字段 包含在中定义的passwd结构中。? 注意,POSIX?1只指定passwd结构中7个字段中的5个。另外2个元素由SVR4和4?3 +BSD支持 。???P146? 图6?1〓/etc/passwd文件中的字段? 由于历史原因,口令字文件是/etc/passwd,而且是一个文本文件。每一行包含图 6?1中所 示的7个字段,字段之间用冒号相分隔。例如,该文件中可能有下列三行:? root:jheVopR58x9Fx:0:1:The superuser:/:/bin/sh? nobody:*:65534:65534::/:? stevens:3hKVD8R58r9Fx:224:20:Richard Stevens:/home/stevens:/bin/ksh? 关于这些记录项请注意下列各点:? ·通常有一个记录项,其用户名为root。此记录项的用户ID是O(超级用户)。? ·密码口令字字段包含了经单向密码单法处理过的用户口令字副本。因为此算法是 单问的, 所以我们不能从密码口令字猜测到原来的口令字。当前使用的算法(见Morris和Th ompson〔 1979〕)总是产生13个可打印字符(在64字符集〔a-zA-Z0-9?1〕中)。因为对 用户名no b ody的记录的密码口令字段只包含一个字符(*),所以密码口令字决不会与此值相匹 配。此用 户名可用于网络服务器,这些服务器允许我们登录到一个系统,但其用户ID和组I D(65534) ,并不提供优先权。以此用户ID和组ID我们可存取的文件只是大家都可读、写的文 件。(这 假定用户ID65534和组ID65534并不拥有任何文件。)在本节稍后部分我们将讨论对 口令字文 件最近所作的更改(阴影口令字)。? ·在口令字文件记录中的某些字段可能是空。如果密码口令字段为空,这通常就意 味着该用 户没有口令字。(不希望这样做。)nobody记录有两个空白字段:注释字段和初始s hell字段 。空白注释字段不产生任何影响。空白shell字段则表示取系统默认值,这通常是 /bin/sh。 ? ·支持finger(1)命令的某些Unix系统支持在注释字段中的附加信息。其中,各部 分之间都 用逗号分隔:用户姓名,办公室地点,办公室电话号码,家庭电话号码。另外,如 果在注释 字段中的用户姓名是一个&,则它被代换为登录名。例如,可以有下列记录:??
stevens:3hKVD8R58r9Fx:224:20:Richard &,B232,555-1111,555-2222:/home/ste vens:/bin /ksh? 即使你所使用的系统并不支持finger命令,这些信息仍可存放在注释字段中,因为 该字段只 是一个注释,并不由系统公用程序解释。? POXIX?1只定义了两个存取口令字文件中信息的函数。在给出用户登录名或数值用 户ID后, 这两个函数就能查看相关记录。? #include ? include ? struct passwd *getpwuid(uid 迹茫模*常病絫 ?uid?);? struct passwd *getpwnaw(const char *?name?);? Both return:pointer if OK,NULL on error? 两个函数反回:看成功为指针,出错为NULL? getpwuid由ls(1)程序使用,以便将包含在一个i-node中的数值用户ID映照为用户 登录名。g etpwnaw在我们键入登录名时由login(1)程序使用。? 这两个函数都返回一个指向passwd结构的指针,该结构已由这两个函数在执行时填 入了所需 的信息。此结构通常是在相关函数内的静态变量,只要调用相关函数,其内容就会 被重写。 ? 如果我们要查看的只是一个登录名或一个用户ID,那么这两个POSIX?1函数是能满 足要求的 ,但是也有些程序要查看整个口令字文件。下列三个函数则可用于此种目的。? #include ? #include ? struct passwd *getpwent(void);? Returns:pointer if OK,NULL on error or end of file? 返回:看成功为指针,出错或文件尾为NULL?? void setpwent(void);? void endpwent(void);? POXIX?1没有定义这三个函数,但它们受到SVR4和4?3+BSD的支持。? 调用getpwent时,它返回口令字文件中的下一个记录。如同上面所述的两个POSIX ?1函数一 样,它返回一个由它填写好的passwd结构的指针。每次调用此函数时都重写该结构 。在第一 次调用该函数时,它打开它所使用的各个文件。在使用本函数时,对口令字文件中 各个记录 安排的顺序并无要求。? 函数setpwent反绕它所使用的文件,endpwent则关闭这些文件。在使用getpwent查 看完了口 令字文件后,一定要调用endpwent关闭这些文件。getpwent知道什么时间它应当打 开它所使 用的文件(第一次被调用时),但是它并不能知道何时关闭这些文件。? 实例? 程序6?1示出了函数getpwnam的一个实现。? 程序6?1〓getpwent函数? P148? 在程序开始处调用setpwent是保护性的措施,以便在调用者在此之前已经调用过g etpwent的 情况下,反绕有关文件使它们定位到文件开始处。getpwnaw和getpwuid完成后不应 使有关文 件仍处于打开状态,所以应调用endpwent关闭它们。? 6?3〓阴影口令字(Shadow Passwords)? 在上面一节我们曾提及,对Unix口令字通常使用的密码算法是单向算法。给出一个 密码口令 字,我们找不到一种算法可以将其反变换到普通文本口令字。(普通文本口令字是 在Passwor d:提示后键入的口令字。)但是我们可以对口令字进行猜测,将猜测的口令字经单 向算法变 换成密码形成,然后将其与用户的密码口令字相比较。如果用户口令字是随机选择 的,那么 这种方法并不是很有用的。但是用户往往以非随机方式选择口令字(对偶的姓名、 街名、宠 物名等)。一个经常重复的试验是先得到一份口令字文件,然后用试探方法猜测口 令字。(Ga rfinkel和Spaftord〔1991〕的第二章对Unix口令字及口令字密码处理方案的 历史情况 及细节进行了说明。)? 为使企图这样做的人难以获得原始资料(密码口令字),某些系统将密码口令字存放 在另一个 通常称为阴影口令字文件中。该文件至少要包含用户名和密码口令字。与该口令字 相关的其 它信息也可存放在该文件中。例如,具有阴影口令字的系统经常要求用户在一定时 间间隔后 选择一个新口令字。这被称之为口令字时效,要选择新口令字的时间间隔长度经常 也存放在 阴影口令字文件中。? 在SVR4中,阴影口令文件是/etc/shadow。在4?3+BSD中,密码口令字存放在/etc /master? passwd中。? 阴影口令字文件不应是一般用户可以读取的。仅有少数几个程序需要存取密码口令 字文件, 例如login(1)和passwd(1),这些程序常常是设置-用户-ID为root。有了阴影口令 字后,普 通口令字文件/etc/passwd可由各用户自由读取。? 6?4〓组文件? Unix组文件(POSIX?1称其为组数据库)包含了图6?2中所示字段。这些字段包含在 中所定义的group结构中。?? P149??? 图6?2〓/etc/group文件中的字段? POSIX?1只定义了其中3个字段。另一个字段gr 迹茫模*常病絧asswd则由SVR4和 4?3+BSD 支持。? 字段gr-mem是一个指针数组,其中的指针各指向一个属于该组的用户名。该数组以 空指针(n ull)结尾。? 我们可以用下列两个由posix?1定义的函数来查看组名或数值组I。? #include ? #include ? struct group *getgrgid(gid 迹茫模*常病絫 ?gid?);? struct group *getgrnam(const char *?name?);? Both return:pointer if OK,NULL on error两个函数返回:若成功为指针,出错 为NULL? 如同对口令字文件进行操作的函数一样,这两个函数通常也返回指向一个静态变量 的指针, 在每次调用时都重写该静态变量。? 如果需要搜索整个组文件,则须使用另外几个函数。下列三个函数类似于针对口令 字文件的 三个函数。? #include ? #include ? struct group *getgrent(void);? Returns:Pointer if OK,NULL on error or end of file? 返回:若成功为指针,出错或文件尾为NULL? void setgrent(void);? void endgrent(void);? 这三个函数由SVR4和4?3+BSD提供POSIX?1并末定义这些函数。? setgrent打开组文件(如若它尚末被打开)并反绕它。getgrent从组文件中读下一个 记录,如 若该文件尚末打开则先打开它。endgrent关闭组文件。? 6?5〓添加组ID? 在Unix中,组的使用已经作了些更改。在Version7中,每个用户任何时候都只属于 一个组。 当用户登录时,系统就按口令字文件中与其相关记录中的数字组ID,赋给他实际组 ID。我们 可以在任何时候执行newgrp(1)以更改组ID。如果newgrp命令执行成功(关于许可权 规则,请 参阅手册页。),则我们的实际组ID就更改为新的组ID,它将被用于后续的文件存 取许可权 检查。执行不带任何参数的newgrp,则可返回到原来的组。? 这种组的成员关系一直维持到1983年左右。此时,4?2BSD引入了添加组ID的概念 。我们不 仅属于我们的口令字记录中组ID所对应的组,也可属于多至16个另外的一些组。文 件存取权 检查相应修改为:不仅将进程的有效组ID与文件的组ID相比较,而且也将所有添加 组ID与文 件的组ID进行比较。? 添加组ID是POSIX?1的可选特性。常数NGROUPS 迹茫模*常病組AX(图2?7)规定了 添加组ID 的数量,其常用值是16。如果不支持添加组ID,则此常数值为O。? SVR4和4?3+BSD都支持添加组ID。? FIPS 151-1要求支持添加组ID,并要求NGROUP 迹茫模*常病組AX至少是8。? 使用添加组ID的优点是我们不必再显式地经常更改组。一个用户常常会参加多个项 目组,因 此也就要同时属于多个组。? 为了存取和设置添加组ID提供了下列三个函数:? POSIX?1只说明了getgroups。因为setgroups和initgroups是特权操作,所以Pos ix?1没有 说明它们。但是,SVR4和4?3+BSD支持所有这三个函数。? getgroups将进程所属用户的各添加组ID填写到数组bgrouplist中,填写入该数组 的添加组I D数最多为gidsetsize个。实际填写到数组中的添加组ID数由函数返回。如果系统 常数NGROU P 迹茫模*常病組AX为0,则返回0,这并不表示出错。? #include ? #include ? int getgroups(int ?gidsetsize?,gid 迹茫模*常病絫 ?grouplist?[]); ? Returns:number of supplementary group IDs if OK,-1 on error? int setgroups(int? ngroups,?const gid 迹茫模*常病絫 ?grouplist?[] );? int initgroups(const char *?username,?gid 迹茫模*常病絫? basegid);? ? 两个函数返回:若成功为0,出错为-1? Both return:0 if OK,-1 on error? 作为一种特殊情况,如若giclsetsize为0,则函数只返回添加组ID数,而对数组g rouplist 则不作修改。(这使调用者可以确定grouplist数组的长度,以便进行分配。)? setgroups可由超级用户调用以便为调用进程设置添加组ID表。grouplist,是组I D数组,而 ngroups说明了数组中的元素数。? 通常,只有initgroups函数调用setgroups,initgroups读整个组文件(用函数getg rent,setg rent和endgrent),然后对username确定其组的成员关系。然后,它调用setgroup s,以便为 该用户初始化添加组ID表。因为initgroups调用setgroups,所以只有超级用户才能 调用init groups。除了在组文件中找到username是成员的组,initgroups也在添加组ID表中 包括了ba segid。basegid是username在口令字文件中的组ID。? initgroups只有少数几个程序调用,例如login(1)程序在用户登录时调用该函数。 ? 6?6〓其它数据文件? 至此我们讨论了两个系统数据文件-口令字文件和组文件。在日常事务操作中,Un ix系统还 使用很多其它文件。例如,BSD网络软件有一个记录各网络服务器所提供的服务的 数据文件( /etc/services),有一个记录协议信息是数据文件(/etc/protocols),还有一个则 是记录网 络信息的数据文件(/etc/networks)。幸运的是,对于这些数据文件的界面都与上 述对口令 字文件和组文件的相似。? 一般原理是,每个数据文件至少有三个函数:? 1? get函数;读下一个记录,如果需要还打开该文件。此种函数通常返回指向一 个结构的 指针。当已达到文件尾端时返回空指针。大多数get函数返回指向一个静态存储类 结构的指 针,如果要保存其内容,则需复制它。? 2? set函数:打开相应数据文件(如果尚末打开),然后反绕该文件。如果希望在 相应文件 起始处开始处理,则调用此函数。? 3? end函数:关闭相应数据文件。正如前述,在结束了对相应数据文件的读、写 操作后, 总应调用此函数以关闭所有相关文件。? 另外,如果数据文件支持某种形式的关键字搜寻,则也提供搜寻具有指定关键字的 记录的例 程。例如,对于口令字文件提供了两个按关键字进行搜寻的程序:getpwnam寻找具 有指定用 户名的记录;getpwuid寻找具有指定用户ID的记录。? 图6?3中列出了一些这样的例程,这些都是SVR4和4?3+BSD支持的。在图中列出了 针对口令 字文件和组文件的函数,这些已在上面说明过。图中也列出了一些与网络有关的函 数。对图 中列出的所有数据文件都有get、set和end函数。? 在SVR4中,图6?3中最后四个数据文件都是符号连接,连接到目录/e6tc/inet下的 同名文件 上。? SVR4和4?3+BSD都有类似于图中所列的附加函数,但是这些附加函数都处理系统管 理文件, 专用于各个实现。? P153??? 图6?3〓存取系统数据文件的一些例程?
6?7〓登录会计? 大多数Unix系统都提供下列两个数据文件:utmp文件,它记录当前登录进系统的各 个用户; wtmp文件,它跟踪各个登录和注销事件。? 在Version7中,一个包含下列结构的二进制记录写入这两个文件中:? struct utmp {? char ut 迹茫模*常病絣ine[8]; /* tty 行:"ttyh0","ttyd0","ttyp0",… * /? char ut 迹茫模*常病絥ame[8]; /*登录名 */? long ut 迹茫模*常病絫ime; /*公元秒数 */? };? 在登录时,login程序填写这样的一个结构,然后将其写入到utmp文件中,同时也 将其添写 到wtmp文件中。在注销时,init进程在utmp文件中的相应记录擦除(每个字节都填 以0),并 将一个新记录添写到wtmp文件中。读wtmp文件中的该注销记录,其ut 迹茫模*常?nbsp; 〗name字 段清除为0。在 系统再启动时,以及更改系统时间和日期的前后,都在wtmp文件中添写特殊的记录 项。who( 1)程序读utmp文件,并以可读格式打印其内容。后来的Unix版本提供last(1)命令 ,它读wtm p文件并打印所选择的记录。? 大多数Unix版本仍提供utmp和wtmp文件,但在这些文件中的信息量却增加了。ver sion 7中2 0字节的结构在SVR2中已扩充为36字节,而在SVR4中,utmp结构已扩充为350字节。 ? SVR4中,这些记录的详细格式请参见手册页utmp(4)和utmpx(4)。在SVR4中,这两 个文件都 在目录/var/adm中。SVR4提供了很多函数(见getut(3)和getutx(3))读、写这两个 文件。? 4?3+BSD中,登录记录的格式请参见手册页utmp(5)。这两个文件的路径名是/var /run/utmp 和/var/log/wtmp。? 6?8〓系统标识? POSIX?1定义了uname函数,它返回与宿主机和操作系统有关的信息。? #include ? int uname(struct utsname *?name?;? Returns:nonnegative value if OK,-1 on error 返回:若成功为非负值,出错为 -1? 通过该函数的参数向其传递一个utsname结构的地址,然后该函数填写此结构。PO SIX?1只 定义了该结构中最少需要的字段(它们都是字符数组),而每个数组的长度则由实现 确定。某 些实现在该结构中提供了另外一些字段。在历史上,系统V为每个数组分配9个字节 ,其中有 1个字节用于字符串结束符(null字符)。? 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 of this release */此发行版的当前版 ? char machine[9]; /*name of hardware type */硬件类型名? };? 在utsname结构中的信息通常可用uname(1)命令打印。? POSIX?1警告:nodename元素可能并不适用于在一通信网络上引用宿主机。此函数 来自于系 统V,在较早时期,nodename元素适用于在UUCP网络上引用主机。? 也应理解在此结构中并没有给出有关POSIX?1版本的信息。这应当使用2?5?2节 中所说明 的 迹茫模*常病絇OSIX 迹茫模*常病絍ERSION以获得该信息。最后,此函数给出 了一种存 取该结构中信息的方法,至于 如何初始化这些信息,POSIX?1没有作任何说明。大多数系统V版本在构造系统核 时通过编 译将这些信息存放在系统核中。? 贝克莱类的版本提供gethostname函数,它只返回宿主机名。这通常就是在TCP/IP 网上该宿 主机的名字。?? #include ? int gethostname(char *?name,?int ?namelen);?? Returns:0 if OK,-1 on error返回:若成功为0,出错为-1? 通过name返回的字符串以null符结尾(除非没有提供足够的空间)。>中的常数 MAXHOSTNAMELEN规定了此名字的最大长度(通常是64字节)。如果该宿主机联到TCP /IP网,则 此宿主机名通常是该宿主机的完整的域名。? hostname(1)命令可用来存取和设置宿主机名。(超级用户用一个类似的函数setho stname来 设置宿主机名。)宿主机名通常在系统自举时设置,它由/etc/rc取自一个启动文件 。? 虽然此函数是贝克莱所特有的,但是,SVR4作为BSD兼容性软件包的一部分提供ge tbostname 和setbostname函数,以及hostname命令。SVR4也将MAXHOSTNAMELEN扩充为256字节 。? 6?9〓时间和日期例程? 由Unix系统核提供的基本时间服务是国际标准时公元1970?1?1 00:00:00以来 经过的秒 数。在1?10节中曾提及这种秒数是以数据类型time-t表示的。我们称它们为日历 时间。日 历时间包括时间和日期。Unix在这方面与其它操作系统的区别是(a)以国际标准时 而非本地 时间计时,(b)可自动进行转换,例如变换到夏日制,(c)将时间和日期作为一个量 值保存。 time函数返回当前时间和日期。? #include
| | |