UNIX系统的正常运作需要使用大量与系统有关的数据文件,例如,口令文件/etc/passwd和组文件/etc/group就是经常被多个程序频繁使用的两个文件。用户每次登录UNIX系统,以及每次执行ls -l命令时都要使用口令文件。
由于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库读这些文件。但是,对于较大的系统,顺序扫描口令文件很花费时间,我们需要能够以非ASCII文本格式存放这些文件,但仍向使用其他文件格式的应用程序提供接口。对于这些数据文件的可移植接口是本章的主题。本章也包括了系统标识函数、时间和日期函数。
UNIX 系统口令文件(POSIX.1 则将其称为用户数据库)包含了图 6-1 中所示的各字段,这些字段包含在<pwd.h>中定义的passwd结构中。
注意,POSIX.1只指定passwd结构包含的10个字段中的5个。大多数平台至少支持其中7个字段。BSD派生的平台支持全部10个字段。
图6-1 /etc/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
关于这些登录项,请注意下列各点:
•通常有一个用户名为root的登录项,其用户ID是0(超级用户)。
• 加密口令字段包含了一个占位符。较早期的UNIX系统版本中,该字段存放加密口令字。将加密口令字存放在一个人人可读的文件中是一个安全性漏洞,所以现在将加密口令字存放在另一个文件中。在下一节讨论口令字时,我们将详细涉及此问题。
• 口令文件项中的某些字段可能是空。如果加密口令字段为空,这通常就意味着该用户没有口令(不推荐这样做)。squid登录项有一空白字段:注释字段。空白注释字段不产生任何影响。
• shell字段包含了一个可执行程序名,它被用作该用户的登录shell。若该字段为空,则取系统默认值,通常是/bin/sh。注意,squid登录项的该字段为/dev/null。显然,这是一个设备,不是可执行文件,将其用于此处的目的是,阻止任何人以用户squid的名义登录到该系统。
很多服务对于帮助它们得以实施的不同守护进程使用不同的用户ID(见第13章),squid项是为实现squid代理高速缓存服务的进程设置的。
• 为了阻止一个特定用户登录系统,除使用/dev/null外,还有若干种替代方法。常见的一种方法是,将/bin/false 用作登录 shell。它简单地以不成功(非 0)状态终止,该shell将此种终止状态判断为假。另一种常见方法是,用/bin/true禁止一个账户。它所做的一切是以成功(0)状态终止。某些系统提供 nologin 命令,它打印可定制的出错信息,然后以非0状态终止。
• 使用nobody用户名的一个目的是,使任何人都可登录至系统,但其用户ID(65534)和组ID(65534)不提供任何特权。该用户ID和组ID只能访问人人皆可读、写的文件。(假定用户ID 65534和组ID 65534并不拥有任何文件,而实际情况就应如此。)
• 提供 finger(1)命令的某些 UNIX 系统支持注释字段中的附加信息。其中,各部分之间都用逗号分隔:用户姓名、办公室地点、办公室电话号码以及家庭电话号码等。另外,如果注释字段中的用户姓名是一个&,则它被替换为登录名。例如,可以有下列记录:
sar:x:205:105:Steve Rago, SF 5-121, 555-1111, 555-2222:/home/sar:/bin/sh
使用finger命令就可打印Steve Rago的有关信息。
$ finger -p sar
Login: sar Name: Steve Rago
Directory: /home/sar Shell: /bin/sh
Office: SF 5-121, 555-1111 Home Phone: 555-2222
On since Mon Jan 19 03:57 (EST) on ttyv0 (messages off)
No Mail.
即使你所使用的系统并不支持finger命令,这些信息仍可存放在注释字段中,该字段只是一个注释,并不由系统实用程序解释。
某些系统提供了 vipw 命令,允许管理员使用该命令编辑口令文件。vipw 命令串行化地更改口令文件,并且确保它所做的更改与其他相关文件保持一致。系统也常常经由图形用户界面(GUI)提供类似的功能。
POSIX.1定义了两个获取口令文件项的函数。在给出用户登录名或数值用户ID后,这两个函数就能查看相关项。
#include<pwd.h>struct passwd *getpwuid(uid_t uid);struct passwd *getpwnam(const char *name);
两个函数返回值:若成功,返回指针;若出错,返回NULL
getpwuid函数由ls(1)程序使用,它将i节点中的数字用户ID映射为用户登录名。在键入登录名时,getpwnam函数由login(1)程序使用。
这两个函数都返回一个指向passwd结构的指针,该结构已由这两个函数在执行时填入信息。passwd 结构通常是函数内部的静态变量,只要调用任一相关函数,其内容就会被重写。
如果要查看的只是登录名或用户ID,那么这两个POSIX.1函数能满足要求,但是也有些程序要查看整个口令文件。下列3个函数则可用于此种目的。
#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知道什么时间应当打开它所使用的文件(第一次被调用时),但是它并不知道何时关闭这些文件。
实例
图6-2程序给出了getpwnam函数的一个实现。
图6-2 getpwnam函数
在函数开始处调用setpwent是自我保护性的措施,以便确保如果调用者在此之前已经调用getpwent打开了有关文件情况下,反绕有关文件使它们定位到文件开始处。getpwnam和getpwuid完成后不应使有关文件仍处于打开状态,所以应调用endpwent关闭它们。
加密口令是经单向加密算法处理过的用户口令副本。因为此算法是单向的,所以不能从加密口令猜测到原来的口令。
历史上使用的算法总是在64字符集[a-zA-Z0-9./]中产生13个可打印字符(见Morris和Thompson [1979])。某些较新的系统使用其他方法,如MD5或SHA-1算法,对口令加密,产生更长的加密口令字符串。(加密口令的字符越多,这些字符的组合也就越多,于是用各种可能组合来猜测口令的难度就越大。)当我们将单个字符放在加密口令字段中时,可以确保任一加密口令都不会与其相匹配。
对于一个加密口令,找不到一种算法可以将其反变换到明文口令(明文口令是在Password:提示后键入的口令)。但是可以对口令进行猜测,将猜测的口令经单向算法变换成加密形式,然后将其与用户的加密口令相比较。如果用户口令是随机选择的,那么这种方法并不是很有用。但是用户往往以非随机方式选择口令(如配偶的姓名、街名、宠物名等)。一个经常重复的实验是先得到一份口令文件,然后用试探方法猜测口令。(Garfinkel等[2003]的第4章对UNIX口令及口令加密处理方案的历史情况及细节进行了说明。)
为使企图这样做的人难以获得原始资料(加密口令),现在,某些系统将加密口令存放在另一个通常称为阴影口令(shadow password)的文件中。该文件至少要包含用户名和加密口令。与该口令相关的其他信息也可存放在该文件中(图6-3)。
图6-3 /etc/shadow文件中的字段
只有用户登录名和加密口令这两个字段是必须的。其他的字段控制口令更改的频率,或者说口令的衰老以及账户仍然处于活动状态的时间。
阴影口令文件不应是一般用户可以读取的。仅有少数几个程序需要访问加密口令,如login(1)和 passwd(1),这些程序常常是设置用户 ID 为 root 的程序。有了阴影口令后,普通口令文件/etc/passwd可由各用户自由读取。
在Linux 3.2.0和Solaris 10中,与访问口令文件的一组函数相类似,有另一组函数可用于访问阴影口令文件。
#include <shadow.h>
struct spwd *getspnam(const char *name);
struct spwd *getspent(void);
两个函数返回值:若成功,返回指针;若出错,返回NULL
void setspent(void);
void endspent(void);
在FreeBSD 8.0和Mac OS X 10.6.8中,没有阴影口令结构。附加的账户信息存放在口令文件中(见图6-1)。
UNIX组文 件(POSIX.1称其为组数据库)包含了图6-4中所示字段。这些字段包含在<grp.h>中所定义的group结构中。
图6-4 /etc/group文件中的字段
字段gr_mem是一个指针数组,其中每个指针指向一个属于该组的用户名。该数组以null指针结尾。可以用下列两个由POSIX.1定义的函数来查看组名或数值组ID。
#include <grp.h>
struct group *getgrgid(gid_t gid);
struct group *getgrnam(const char *name);
两个函数返回值:若成功,返回指针;若出错,返回NULL
如同对口令文件进行操作的函数一样,这两个函数通常也返回指向一个静态变量的指针,在每次调用时都重写该静态变量。
如果需要搜索整个组文件,则须使用另外几个函数。下列3个函数类似于针对口令文件的3个函数。
#include <grp.h>
struct group *getgrent(void);
返回值:若成功,返回指针;若出错或到达文件尾端,返回NULL
void setgrent(void);
void endgrent(void);
这3个函数不是基本POSIX.1标准的组成部分。Single UNIX Specification的XSI扩展定义了这些函数。所有UNIX系统都提供这3个函数。
setgrent函数打开组文件(如若它尚末被打开)并反绕它。getgrent函数从组文件中读下一个记录,如若该文件尚未打开,则先打开它。endgrent函数关闭组文件。
在UNIX系统中,对组的使用已经做了些更改。在V7中,每个用户任何时候都只属于一个组。当用户登录时,系统就按口令文件记录项中的数值组 ID,赋给他实际组 ID。可以在任何时候执行newgrp(1)以更改组ID。如果newgrp命令执行成功(关于权限规则,请参阅手册),则实际组 ID 就更改为新的组 ID,它将被用于后续的文件访问权限检查。执行不带任何参数的newgrp,则可返回到原来的组。
这种组成员形式一直维持到1983年左右。此时,4.2BSD引入了附属组ID(supplementary group ID)的概念。我们不仅可以属于口令文件记录项中组ID所对应的组,也可属于多至16个另外的组。文件访问权限检查相应被修改为:不仅将进程的有效组ID与文件的组ID相比较,而且也将所有附属组ID与文件的组ID进行比较。
附属组 ID 是 POSIX.1 要求的特性。(在较早的 POSIX.1 版本中,该特性是可选的。)常量NGROUPS_MAX(见图2-11)规定了附属组ID的数量,其常用值是16(见图2-15)。
使用附属组 ID 的优点是不必再显式地经常更改组。一个用户会参与多个项目,因此也就要同时属于多个组,此类情况是常有的。
为了获取和设置附属组ID,提供了下列3个函数。
#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
在这3个函数中,POSIX.1只说明了getgroups。因为setgroups和initgroups是特权操作,所以它们并非POSIX.1的组成部分。但是,本书说明的所有4种平台都支持这3个函数。在Mac OS X 10.6.8中,basegid 被声明为int类型。
getgroups将进程所属用户的各附属组ID填写到数组grouplist中,填写入该数组的附属组ID数最多为gidsetsize个。实际填写到数组中的附属组ID数由函数返回。
作为一种特殊情况,如若gidsetsize为0,则函数只返回附属组ID数,而对数组grouplist则不做修改。(这使调用者可以确定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。
只有少数几个程序调用initgroups,例如login(1)程序在用户登录时调用该函数。
我们已讨论了Linux和Solaris支持的阴影口令文件。FreeBSD和Mac OS X则以不同方式存储加密口令字。图6-5总结了本书涉及的4种平台如何存储用户和组信息。
图6-5 账户实现的区别
在FreeBSD中,阴影口令文件是/etc/master.passwd。可以使用特殊命令编辑该文件,它会从阴影口令文件产生/etc/passwd 的一个副本。另外,也产生该文件的散列副本。/etc/pwd.db是/etc/passwd的散列副本,/etc/spwd.db是/etc/master.passwd的散列版本。这些为大型安装的系统提供了更好的性能。
但是,Mac OS X只在单用户模式下使用/etc/passwd和/etc/master.passwd(在维护系统时,单用户模式通常意味着不能提供任何系统服务)。在正常运行期间的多用户模式,目录服务守护进程提供对用户和组账户信息的访问。
虽然Linux和Solaris支持类似的阴影口令接口,但两者之间存在某些细微的差别。例如,图6-3中所示的整数字段在Solaris中定义为int类型,而在Linux中则定义为long int。另一个差别是账户-不活动字段:Solaris将其定义为自用户上次登录后到下次账户自动失效之间的天数,而Linux则将其定义为达到最大口令年龄尚余天数。
在很多系统中,用户和组数据库是用网络信息服务(Network Information Service,NIS)实现的。这使管理人员可编辑数据库的主副本,然后将它自动分发到组织中的所有服务器上。客户端系统联系服务器以查看用户和组的有关信息。NIS+和轻量级目录访问协议(Lightweight Directory Access Protocol,LDAP)提供了类似功能。很多系统通过配置文件/etc/nsswitch.conf控制用于管理每一类信息的方法。
至此仅讨论了两个系统数据文件——口令文件和组文件。在日常操作中,UNIX系统还使用很多其他文件。例如,BSD网络软件有一个记录各网络服务器所提供服务的数据文件(/etc/services),有一个记录协议信息的数据文件(/etc/protocols),还有一个则是记录网络信息的数据文件(/etc/networks)。幸运的是,对于这些数据文件的接口都与上述对口令文件和组文件的相似。
一般情况下,对于每个数据文件至少有3个函数。
(1)get函数:读下一个记录,如果需要,还会打开该文件。此种函数通常返回指向一个结构的指针。当已达到文件尾端时返回空指针。大多数get函数返回指向一个静态存储类结构的指针,如果要保存其内容,则需复制它。
(2)set 函数:打开相应数据文件(如果尚末打开),然后反绕该文件。如果希望在相应文件起始处开始处理,则调用此函数。
(3)end函数:关闭相应数据文件。如前所述,在结束了对相应数据文件的读、写操作后,总应调用此函数以关闭所有相关文件。
另外,如果数据文件支持某种形式的键搜索,则也提供搜索具有指定键的记录的例程。例如,对于口令文件,提供了两个按键进行搜索的程序:getpwnam 寻找具有指定用户名的记录;getpwuid寻找具有指定用户ID的记录。
图6-6中列出了一些这样的例程,这些都是UNIX常用的。在图中列出了针对口令文件和组文件的函数,这些已在前面说明过。图中也列出了一些与网络有关的函数。对于图中列出的所有数据文件都有get、set和end函数。
图6-6 访问系统数据文件的一些例程
在 Solaris 中,图 6-6 中的最后 4 个数据文件都是符号链接,它们都链接到目录/etc/inet下的同名文件上。大多数UNIX系统实现都有类似于图中所列的附加函数,但是这些附加函数都旨在处理系统管理文件,专用于各个实现。
大多数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文件并打印所选择的记录。
大多数UNIX版本仍提供utmp和wtmp文件,但正如所期望的,其中的信息量却增加了。V7中写入的20字节的结构在SVR2中已扩充为36字节,而在SVR4中,utmp结构已扩充为多于350字节。
在Solaris中,这些记录的详细格式请参见手册页utmpx(4)。Solaris 10中这两个文件都在目录/var/adm中。Solaris提供了很多函数(见getutx(3))读或写这两个文件。
在FreeBSD 8.0和Linux 3.2.0中,登录记录的格式请参见手册页utmp(5)。这两个文件的路径名是/var/run/utmp和/var/log/wtmp。在Mac OS X 10.6.8中,utmp和wtmp文件不存在。在Mac OS X 10.5中,wtmp文件中的信息可以从系统登录工具中获得,utmpx文件包含了活动的登录会话的信息。
POSIX.1定义了uname函数,它返回与主机和操作系统有关的信息。
#include <sys/utsname.h>
int uname(struct utsname *name);
返回值:若成功,返回非负值;若出错,返回-1
通过该函数的参数向其传递一个 utsname 结构的地址,然后该函数填写此结构。POSIX.1只定义了该结构中最少需提供的字段(它们都是字符数组),而每个数组的长度则由实现确定。某些实现在该结构中提供了另外一些字段。
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字节结尾。本书讨论的4种平台支持的最大名字长度(包含终止null字节)列于图6-7中。utsname结构中的信息通常可用uname(1)命令打印。
POSIX.1警告nodename元素可能并不适用于在通信网络上引用主机。此函数来自于System V,在早期,nodename元素适用于在UUCP网络上引用主机。
还要认识到,在此结构中并没有给出有关POSIX.1版本的信息。应当使用2.6节中所说明的_POSIX_VERSION获得该信息。
最后,此函数只给出了一种获取该结构中信息的方法,至于如何初始化这些信息,POSIX.1没有给出任何说明。
历史上,BSD派生的系统提供gethostname函数,它只返回主机名,该名字通常就是TCP/IP网络上主机的名字。
#include <unistd.h>
int gethostname(char *name, i n t namelen);
返回值:若成功,返回0;若出错,返回-1
namelen参数指定name缓冲区长度,如若提供足够的空间,则通过name返回的字符串以null字节结尾。如若没有提供足够的空间,则没有说明通过name返回的字符串是否以null结尾。
现在,gethostname函数已在POSIX.1中定义,它指定最大主机名长度是HOST_NAME_MAX。图6-7中总结列出了本书讨论的4种实现支持的最大名字长度。
图6-7 系统标识名限制
如果宿主机联接到TCP/IP网络中,则此主机名通常是该主机的完整域名。
hostname(1)命令可用来获取和设置主机名。(超级用户用一个类似的函数 sethostname来设置主机名。)主机名通常在系统自举时设置,它由/etc/rc或init取自一个启动文件。
由UNIX内核提供的基本时间服务是计算自协调世界时(Coordinated Universal Time,UTC)公元1970年1月1日00:00:00这一特定时间以来经过的秒数。1.10节中曾提及这种秒数是以数据类型time_t表示的,我们称它们为日历时间。日历时间包括时间和日期。UNIX在这方面与其他操作系统的区别是:(a)以协调统一时间而非本地时间计时;(b)可自动进行转换,如变换到夏令时;(c)将时间和日期作为一个量值保存。
time函数返回当前时间和日期。
#include <time.h>
time_t time(time_t *calptr);
返回值:若成功,返回时间值;若出错,返回-1
时间值作为函数值返回。如果参数非空,则时间值也存放在由calptr指向的单元内。
POSXI.1的实时扩展增加了对多个系统时钟的支持。在Single UNIX Specification V4中,控制这些时钟的接口从可选组被移至基本组。时钟通过clockid_t类型进行标识。图6-8给出了标准值。
图6-8 时钟类型标识符
clock_gettime函数可用于获取指定时钟的时间,返回的时间在4.2节介绍的timespec结构中,它把时间表示为秒和纳秒。
#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函数得到更高精度的时间值。
#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函数。
#include <sys/time.h>
int clock_settime(clockid_t clock_id, const struct timespec *tsp);
返回值:若成功,返回0;若出错,返回-1
我们需要适当的特权来更改时钟值,但是有些时钟是不能修改的。
历史上,在System V派生的系统实现中,调用stime(2)函数来设置系统时间,而在BSD派生的系统中调用settimeofday(2)设置系统时间。
SUSv4指定gettimeofday函数现在已弃用。然而,一些程序仍然使用这个函数,因为与time函数相比,gettimeofday提供了更高的精度(可到微秒级)。
#include <sys/time.h>
int gettimeofday(struct timeval *restrict tp, void *restrict tzp);
返回值:总是返回0
tzp的唯一合法值是NULL,其他值将产生不确定的结果。某些平台支持用tzp说明时区,但这完全依实现而定,Single UNIX Specification对此并没有定义。
gettimeofday函数以距特定时间(1970年1月1日00 : 00 : 00)的秒数的方式将当前时间存放在tp指向的timeval结构中,而该结构将当前时间表示为秒和微秒。
一旦取得这种从上述特定时间经过的秒数的整型时间值后,通常要调用函数将其转换为分解的时间结构,然后调用另一个函数生成人们可读的时间和日期。图6-9说明了各种时间函数之间的关系。(图中以虚线表示的3个函数localtime、mktime和strftime都受到环境变量TZ的影响,我们将在本节的最后部分对其进行说明。点划线表示了如何从时间相关的结构获得日历时间。)
两个函数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 */
};
秒可以超过59的理由是可以表示润秒。注意,除了月日字段,其他字段的值都以0开始。如果夏令时生效,则夏令时标志值为正;如果为非夏令时时间,则该标志值为0;如果此信息不可用,则其值为负。
Single UNIX Specification的以前版本允许双润秒,于是,tm_sec值的有效范围是0~61。
UTC的正式定义不允许双润秒,所以,现在tm_sec值的有效范围定义为0~60。191
图6-9 各个时间函数之间的关系
#include <time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);
两个函数的返回值:指向分解的tm结构的指针;若出错,返回NULL
localtime和gmtime之间的区别是:localtime将日历时间转换成本地时间(考虑到本地时区和夏令时标志),而 gmtime 则将日历时间转换成协调统一时间的年、月、日、时、分、秒、周日分解结构。
函数mktime以本地时间的年、月、日等作为参数,将其变换成time_t值。
#include <time.h>
time_t mktime(struct tm *tmptr);
返回值:若成功,返回日历时间;若出错,返回-1
函数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);
两个函数的返回值:若有空间,返回存入数组的字符数;否则,返回0
两个较早的函数——asctime和ctime能用于产生一个26字节的可打印的字符串,类似于date(1)命令默认的输出。然而,这些函数现在已经被标记为弃用,因为它们易受到缓冲区溢出问题的影响。
strftime_l允许调用者将区域指定为参数,除此之外,strftime和strftime_l函数是相同的。strftime使用通过TZ环境变量指定的区域。
tmptr参数是要格式化的时间值,由一个指向分解时间值tm结构的指针说明。格式化结果存放在一个长度为maxsize个字符的buf数组中,如果buf长度足以存放格式化结果及一个null终止符,则该函数返回在buf中存放的字符数(不包括null终止符);否则该函数返回0。
format参数控制时间值的格式。如同printf函数一样,转换说明的形式是百分号之后跟一个特定字符。format中的其他字符则按原样输出。两个连续的百分号在输出中产生一个百分号。与printf函数的不同之处是,每个转换说明产生一个不同的定长输出字符串,在format字符串中没有字段宽度修饰符。图6-10中列出了37种ISO C规定的转换说明。
图6-10 strftime的转换说明
图中第3列的数据来自于在Mac OS X中执行strftime函数所得的结果,它对应的时间和日期是:Thu Jan 19 21:24:52 EST 2012。
图 6-10中的大多数格式说明的意义很明显。需要略做解释的是%U、%V和%W。%U是相应日期在该年中所属周数,包含该年中第一个星期日的周是第一周。%W 也是相应日期在该年中所属的周数,不同的是包含第一个星期一的周为第一周。%V 说明符则与上述两者有较大区别。如果包含了1月1日的那一周包含了新一年的4天或更多天,那么该周是一年中的第一周;否则该周被认为是上一年的最后一周。在这两种情况下,周一都被视作每周的第一天。
同printf一样,strftime对某些转换说明支持修饰符。可以使用E和O修饰符产生本地支持的另一种格式。
某些系统对strftime的format字符串提供另一些非标准的扩充支持。
实例
图6-11演示了如何使用本章中讨论的多个时间函数。特别演示了如何使用strftime打印包含当前日期和时间的字符串。
图6-11 使用strftime函数
回顾图6-9中的不同时间函数的关系。在以人们可读的格式打印时间之前,需要获取时间并将其转换成分解的时间结构。图6-11程序的输出如下:
$ ./a.out
buffer length 16 is too small
time and date: 11:12:35 PM, Thu Jan 19, 2012
strptime函数是strftime的反过来版本,把字符串时间转换成分解时间。
#include <time.h>
char *strptime(const char *restrict buf, const char *restrict format,
struct tm *restrict tmptr);
返回值:指向上次解析的字符的下一个字符的指针;否则,返回NULL
format参数给出了buf参数指向的缓冲区内的字符串的格式。虽然与strftime函数的说明稍有不同,但格式说明是类似的。strptime函数转换说明符列在图6-12中。
图6-12 strptime函数的转换说明
我们曾在前面提及,图6-9中以虚线表示的3个函数受到环境变量TZ的影响。这3个函数是localtime、mktime和strftime。如果定义了TZ,则这些函数将使用其值代替系统默认时区。如果 TZ定义为空串(即TZ=),则使用协调统一时间UTC。TZ的值常常类似于TZ=EST5EDT,但是 POSIX.1 允许更详细的说明。有关 TZ 变量的详细情况,请参阅 Single UNIX Specification [Open Group 2010]中的环境变量章节。
关于TZ环境变量的更多信息可参见手册页tzset(3)。
所有UNIX系统都使用口令文件和组文件。我们说明了读这些文件的各种函数。本章也介绍了阴影口令,它可以增加系统的安全性。附属组ID提供了一个用户同时可以参加多个组的方法。我们还介绍了大多数系统所提供的访问其他与系统有关数据文件的类似函数。我们讨论了几个POSIX.1的系统标识函数,应用程序使用它们以标识它在何种系统上运行。最后,说明了ISO C和Single UNIX Specification提供的与时间和日期有关的一些函数。
6.1 如果系统使用阴影文件,那么如何取得加密口令?
6.2 假设你有超级用户权限,并且系统使用了阴影口令,重新考虑上一道习题。
6.3 编写一程序,它调用uname并输出utsname结构中的所有字段,将该输出与uname(1)命令的输出结果进行比较。
6.4 计算可由time_t数据类型表示的最近时间。如果超出了这一时间将会如何?
6.5 编写一程序,获取当前时间,并使用 strftime 将输出结果转换为类似于 date(1)命令的默认输出。将环境变量TZ设置为不同值,观察输出结果。