一、口令文件
UNIX口令文件包含下表中的各个字段,这些字段包含在<pwd.h>头文件中定义的passwd结构体中。
由于历史原因,口令文件是/bin/passwd,而且是一个文本文件,每一行都包括了上表中的七个字段,字段之间用":"分隔,例如一个文件中可能有以下三行:
root:jheVopR58x9Fx:0:1:The superuser:/:/bin/sh
n o b o d y : * : 6 5 5 3 4 : 6 5 5 3 4 : : / :
stevens:3hKVD8R58r9Fx:224:20:Richard Stevens:/home/stevens:/bin/ksh
对于这些登陆项需要注意以下几点:
- 加密口令字段是由单向不可逆算法加密产生的13个可打印字符(在64字符集中[a-zA-Z0-9./])。用nobody用户ID和组ID都是大家都可读写的文件
- 口令文件中某些字段可能是空,如果密码口令为空则说明用户没有口令。nobody用户有两个空字段:注释字段和初始shell字段,空注释字段不产生任何影响,空shell字段表示取系统默认值,一般是/bin/sh。
- 支持finger命令的某些系统支持注释字段中的附加信息,其中各部分之间用逗号分隔:用户姓名,用户地址,用户电话。如果注释字段中的用户姓名是"&",则它被替换为登陆名。例如可以有如下记录:
stevens:3hKVD8R58r9Fx:224:20:Richard &, B232, 555-1111, 555-2222:
/ h o m e / s t e v e n s : / b i n / k s h
POISX.1只定义了两个存取口令文件中信息的函数:参数为用户登录名或者是数值ID:
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);
返回值:成功则为指针,出错为NULL
getpwuid由ls命令使用,用于从i节点中的数值用户ID获取用户登录名。getpwnam在输入登录名时由login程序使用。
可用以下三个函数查看整个口令文件:
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwent(void);
返回值:成功返回指针,出错或到达文件尾端返回NULL
void setpwent(void);
void endpwent(void);
调用getpwent时,返回口令文件中的下一个记录,它返回一个由它填写的passwd结构的指针,每次调用此函数都重写该结构。
setpwdent函数定位文件到开始处,endpwent关闭这些文件。再用getpwent查看完口令文件后一定要用endpwent关闭这些文件。getpwent函数并不知道何时关闭这些文件。
二、阴影文件
有些系统为了不让黑客得到原始加密口令会将加密口令存放在另一个通常被称为阴影口令的文件中(如:/etc/shadow)。该文件至少要包含用户名和加密口令。与该口令相关的信息也可以存放在该文件中。例如有些系统会要求用户在一定时间间隔后修改口令,而这个时间间隔长度就存在阴影口令文件中。
阴影口令文件不应是一般用户可以读取的。仅有少数几个程序需要存取加密口令文件,例如login和passwd。这些程序通常设置-用户-ID为root。有了阴影口令后,普通口令文件/etc/passwd可由各用户自由读取。
三、组文件
UNIX组文件包含了如下字段,这些字段包含在<grp.h>z中所定义的group结构中。
gr_mem是一个指针数组,其中的指针各指向一个属于该组的用户名,该数组以null结尾。
可以由POSIX.1定义的两个函数来查看组名或数值组ID。
#include <sys/types.h>
#include <grp.h>
struct group *getgrgid(gid_t gid);
srruct group *getgrnam(const char *name);
返回值:成功返回group指针,出错NULL
如同对口令文件进行操作的函数一样,这两个函数通常也返回指向一个静态变量的指针,在每次调用时都重写该静态变量。
以下三个函数类似与针对口令文件的函数,用来查看整个组文件:
#include <sys/types.h>
#include <grp.h>
struct group *getgrent(void);
返回值:成功则为指针,出错或到达文件尾端则为NULL
void setgrent(void);
void endgrent(void);
setgrent打开组文件并定位到文件开始,getgrent从组文件读下一条记录,如果该文件未打开则先打开它,endgrent关闭组文件。
四、添加组ID
在UNIX中,对组的使用已经做了一些修改。在V7中一个用户任何时候只能属于一个组,当用户登录时,系统就按照口令文件中用户相关联的组ID赋给他实际组ID。可以在任何时候执行netgrp更改组。如果newgrp命令执行成功则实际组更改为新的组,它将用于后续文件权限检查。执行不带任何权限的newgrp则可返回到原来的组。
这种组的成员关系一直持续到1983年左右,此时4.2BSD引入了添加组ID的概念,我们不仅可以属于口令记录中组ID所对应的组,还可以属于多至16个另外的组。文件权限检查并修改为:不仅将进程的有效组ID与文件的zuID比较,而且也将所有添加组ID与文件的组ID比较
使用添加组ID的一个优势就是不用显式的修改用户组ID。
为了存取和设置添加组ID,通常使用以下三个函数:
#include <sys/types.h>
#include <unistd.h>
int getgroups(int gidsetsize, gid_t grouplist[]);
返回值:成功则为添加的组数量,出错为-1.
int setgroups(int ngroups, const gid_t grouplist[]);
int initgroups(const char *username, gid_t basegid);
两个函数返回值:成功0,出错-1.
getgroups将进程所属用户的各添加组填到数组grouplist中,填入该数组的ID数最多gidsetsize。实际填入的数量由该函数返回。如果系统常数NGROUPS_MAX为0,则返回0,这并不表示错误。
setgroups可由root用户调用为调用进程设置添加组ID表,grouplist是组ID数组,ngroups是数组元素数。
通常只有initgroups调用setgroups,initgroups读整个组文件然后对username确定其组的成员关系。然后它调用setgroups为该用户初始化添加组ID表。因为initgroups调用setgroups,所以只有root用户才能调用initgroups,除了在组文件中找username是成员的组,initgroups也在添加组ID表中包括了basegid。basegid是username在口令文件中的组ID。
五、其他数据文件
除了之上我们学习的口令文件和组文件,UNIX系统还有其他一下数据文件。对于这些数据文件的界面都与上述对口令文件和组文件的相似。
一般情况下数据文件都有三个函数:
- get函数,读下一个记录,这种函数通常返回一个指向静态存储类结构的指针,如果要保存其内容则需要复制它。当到达文件尾端时返回空指针。
- set函数:打开数据文件并将指针移到文件起始位置
- end函数:关闭数据文件。
下表中列出了一些SVR4和4.3+BSD支持的例程。在表中列出了针对口令文件和组文件的函数,也列出了一些与网络有关的函数。
六、登录会计
大多数UNIX系统都提供一下两个数据文件:
- utmp文件,该文件记录当前登录进系统的各个用户;
- wtmp文件,该文件跟踪各个登录和注销事件。
在V7中,包含下列结构的一个二进制记录写入这两个文件中:
struct utmp {
char ut_line[8]; /* tty line: "ttyh0","ttyd1","ttyp0"...... */
char ut_name[8]; /* login name */
long ut_time; /* second since Epoch */
}
登录时,login程序填写这样一个结构然后填入utmp文件中,同时填入到wtmp中。注销时,init进程将utmp文件中相应的记录擦除(每个字节都为0),并将一个新纪录填入到wtmp中。读wtmp中该注销记录,其ut_name清除为0。在系统再启动时,以及更改系统日期和时间的前后,都在wtmp中填写特殊的记录项。who命令读utmp文件,并以可读格式打印其内容。后来的UNIX系统版本提供last命令,它读wtmp文件并打印所选择的记录。
七、系统标识
POSIX.1定义了uname函数,返回与主机和操作系统相关的信息。
#include <sys/utsname.h>
int uname(struct utsname *name);
返回值: 成功为非负值,出错为-1.
通过参数向该函数传一个struct uname结构的地址,然后该函数填写该参数。POSIX.1只定义了该结构至少需要的字段(都是字符数组),每个数组的长度由实现来决定。 历史上,V系统为每个数组分配9个字节,其中最后一个字节是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 operating system */
char machine[9]; /* name of hardware type */
}
utsname结构中的信息通常由uname命令打印。
伯克利类的版本提供了gethostname函数,只返回主机名,该名字通常就是TCP/IP网络上主机的名字。
#include <unistd.h>
int gethostname(char *name, int namelen);
返回值:成功0,出错-1.
通过name返回的字符串以null结束(除非没有提供足够的空间)。<sys/param.h>中的常数MAXHOSTNAMELEN规定了此名字的最大长度(通常是64字节)。
hostname命令可以用来存取和设置主机名(root用户用一个类似的函数sethostname来设置主机名),主机名通常在系统自举(bootstrapping)时设置,它由/etc/rc取自一个启动文件。
八、时间和日期例程
time函数返回当前日期和时间
#include <time.h>
time_t time(time_t *calptr);
返回值:成功为时间值(时间戳),出错为-1.
如果参数不是NULL,则返回的时间值也保存在calptr指向的单元中
一旦取得时间戳后,通常要调用另外一个时间函数将其转换为人们可读的时间和日期。下图说明了各时间函数之间的关系。(图中虚线标注的函数通常受环境变量TZ的影响)
两个函数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 the 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的原因是可以表示润秒。如果夏时制生效,则tm_isdst为正,如果已非夏时制则为0,如果此信息不可用则为负。
#include <time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);
localtime和gmtime的区别在于localtime将时间戳转换为本地时间(考虑到本时区和夏时制标志)。而gmtime转为了国际标准时间。
函数mktime以本地时间的年、月、日、时、分、秒等作为参数将其转为时间戳:
#include <time.h>
time_t mktime(struct tm *tmptr);
asctime和ctime函数产生形式的26字节字符串,这与date命令的系统默认输出形式类似;
Tue Jan 14 10:15:03 1992
#include <time.h>
char *asctime(const struct tm *tmptr);
char *ctime(const time_t *calptr);
返回值:指向null结尾的字符串
最后一个时间函数是strftime,它是非常复杂的printf类的时间值函数
#include <time.h>
size_t strftime(char *buf, size_t maxsize, const char *format, const struct tm *tmptr);
返回值:若有空间,则存入数组的字符数,否则为0.
最后一个参数是要格式化的时间指针。格式化结果存放在一个长度为maxsize个字符的buf数组中,如果buf长度足以存放格式化结果及一个null终止符,则该函数返回在buf中存放的字符数(不包括null终止符),否则该函数返回0。
format参数控制时间值的格式,如同printf函数一样,变换格式说明是百分号后面跟一个特定字符。format中其他自负原样输出。两个连续的百分号在输出中产生一个百分号。与printf函数不同的是,每个变换说明产生一个定长输出字符串,在format字符串中没有字段宽度修饰符。下表列出了21种ANSI C规定的变换说明
表中第三列的数据来自于SVR4,对应于下列日期与时间,执行strftime函数所得的结果为:
Tue Jan 14 10:15:30 MST 1992
表中%U是相应日期在该年中所属周数,包含该年中第一个星期日的周是第一周。%W也是相应日期在该年中所属的周数,不同的是包含第一个星期一的周为第一周。
如果定义了环境变量TZ,则localtime、mktime、ctime、strftime(即时间函数关系图中虚线标准的函数)将使用其值代替系统默认时区。如果定义TZ为空串(即TZ=),则使用国际标准时间。TZ的值常类似于TZ=EST5ETD,但是POSIX.1允许更为详细的说明。