第六章 系统数据文件和信息
UNIX系统的正常运行需要使用大量的与系统有关的数据文件,例如,口令文件/etc/passwd和组文件/etc/group就是经常由多种程序使用的两个文件。用户每次登录UNIX系统,以及每次执行ls -l命令时都要使用口令文件。
由于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库读这些文件。但是,对于较大的系统,顺序扫描口令文件非常耗时,我们需要能够以非ASCII文本格式存放这些文件,但仍向应用程序提供可以处理任何一种文件格式的接口。针对这些数据文件的可移植接口是本章的主题。本章还介绍了系统标识函数,时间和日期函数。
#include<pwd.h>
//getpwuid和getpwnam用于从系统的密码数据库(通常是/etc/passwd文件)中获取用户信息。它们分别通过用户ID和用户名进行查找,并返回一个指向struct passwd结构的指针。
struct passwd*getpwuid(uid_t uid);
uid:是用户的ID
返回值:返回指向struct passwd结构的指针。如果没有找到对应的用户,返回NULL
struct passwd *getpwnam(const char *name);
name:是用户名的字符串
返回值:返回指向struct passwd结构的指针。如果没有找到对应的用户,返回NULL。
struct passwd {
char *pw_name; /* 用户的登录名 */
char *pw_passwd; /* 用户的加密密码 */
uid_t pw_uid; /* 用户ID */
gid_t pw_gid; /* 用户的组ID */
char *pw_gecos; /* 用户的真实姓名或描述 */
char *pw_dir; /* 用户的主目录 */
char *pw_shell; /* 用户的登录Shell */
};
//以下是一个实例代码,演示如何使用getpwuid和getpwnam函数
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
uid_t uid = getuid(); // 获取当前用户ID
struct passwd *pw = getpwuid(uid);
if (pw) {
printf("用户名: %s\n", pw->pw_name);
printf("用户ID: %d\n", pw->pw_uid);
printf("组ID: %d\n", pw->pw_gid);
printf("真实姓名: %s\n", pw->pw_gecos);
printf("主目录: %s\n", pw->pw_dir);
printf("登录Shell: %s\n", pw->pw_shell);
} else {
perror("getpwuid");
}
const char *username = "root";
pw = getpwnam(username);
if (pw) {
printf("用户名: %s\n", pw->pw_name);
printf("用户ID: %d\n", pw->pw_uid);
printf("组ID: %d\n", pw->pw_gid);
printf("真实姓名: %s\n", pw->pw_gecos);
printf("主目录: %s\n", pw->pw_dir);
printf("登录Shell: %s\n", pw->pw_shell);
} else {
perror("getpwnam");
}
return 0;
}
如果要查看的只是登录名或用户ID,那么getpwuid和getpwnam这两个函数能满足要求,但是也有些程序要查看整个口令文件。下列三个函数则可用于此种目的:
#include<pwd.h>
//getpwent,setpwent和endpwent用于遍历和管理系统密码数据库(通常是/etc/passwd文件)
struct passwd *getpwent(void);
功能:读取并返回密码文件中的下一条记录。每次调用getpwent会返回文件中的下一条记录,直到文件结束。
返回值:返回指向struct passwd结构的指针。如果读取到文件末尾或发生错误,返回NULL。
void setpwent(void);
功能:重新定位到密码文件的开头。
void endpwent(void);
功能:关闭密码文件。调用endpwent后,任何对getpwent的调用都将重新打开文件。
//以下是一个示例代码,演示如何使用getpwent,setpwent,endpwent
#include <pwd.h>
#include <stdio.h>
int main() {
struct passwd *pw;
// 重新定位到密码文件的开头
setpwent();
// 遍历密码文件中的所有条目
while ((pw = getpwent()) != NULL) {
printf("用户名: %s\n", pw->pw_name);
printf("用户ID: %d\n", pw->pw_uid);
printf("组ID: %d\n", pw->pw_gid);
printf("真实姓名: %s\n", pw->pw_gecos);
printf("主目录: %s\n", pw->pw_dir);
printf("登录Shell: %s\n\n", pw->pw_shell);
}
// 关闭密码文件
endpwent();
return 0;
}
程序清单6-1 getpwnam函数的一个实现
#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);
}
阴影口令
加密口令是经单项加密算法处理过的用户口令副本。因为此算法是单向的,所以不能从加密口令猜测到原来的口令。
历史上使用的算法总是从64字符集中产生13个可打印字符。某些较新的系统使用MD5算法对口令加密,为每个加密口令产生31个字符。(加密口令的字符越多,这些字符的组合也就越多,于是用各种可能组合来猜测口令的难度就越大。)当我们将一个字符放在加密口令字段中时。可以确保任一加密口令都不会与其相匹配。
在早期的Unix系统中,用户密码被存储在/etc/passwd文件中,该文件同时包含其他用户信息。在这种方法中,用户的密码通常以加密的形式存储在/etc/passwd文件的第二个字段。然而,由于/etc/passwd文件需要对系统中的所有用户刻度,以便执行各种操作(如获取用户名,主目录等),这意味着任何用户都可以读取加密的密码字段。
为了解决上述安全问题,阴影口令机制将用户的加密密码从/etc/shadow文件中移除,并将其存储在一个只有特权用户(通常是root)才能读取的文件中(通常是/etc/shadow)。这样以来,普通用户无法访问加密的密码,增强了系统的安全性。
阴影口令存放在/etc/shadow中。
以下几个函数可以用于访问阴影口令文件:
#include<shadow.h>
//getspnam,getspent,setspent和endspent是用于读取和管理阴影密码文件/etc/shadow的函数。
struct spwd *getspnam(const char *name);
功能:根据用户名查找阴影密码记录
参数:name是用户名的字符串
返回值:返回指向struct spwd结构的指针。如果没有找到对应的用户,返回NULL
struct spwd *getspent(void);
功能:读取并返回阴影密码文件中的下一条记录。每次调用getspent会返回文件中的下一条记录,直到文件结束。
返回值:返回指向struct spwd结构的指针。如果读取到文件末尾或发生错误,返回NULL
void setspent(void);
功能:重新定位到阴影密码文件的开头。
void endspent(void);
功能:关闭阴影密码文件
struct spwd {
char *sp_namp; /* 用户名 */
char *sp_pwdp; /* 加密密码 */
long sp_lstchg; /* 最近一次更改密码的日期(从1970-01-01起的天数) */
long sp_min; /* 密码最短使用期限(天) */
long sp_max; /* 密码最长使用期限(天) */
long sp_warn; /* 密码过期前的警告天数 */
long sp_inact; /* 密码失效后的宽限期(天) */
long sp_expire; /* 账号失效日期(从1970-01-01起的天数) */
unsigned long sp_flag; /* 保留字段 */
};
//以下演示如何使用getspnam,getspent,setspent,endspent
//在root下运行
#include <shadow.h>
#include <stdio.h>
int main() {
struct spwd *sp;
const char *username = "root";
// 根据用户名查找阴影密码记录
sp = getspnam(username);
if (sp) {
printf("用户名: %s\n", sp->sp_namp);
printf("加密密码: %s\n", sp->sp_pwdp);
printf("最近一次更改密码的日期: %ld\n", sp->sp_lstchg);
printf("密码最短使用期限: %ld\n", sp->sp_min);
printf("密码最长使用期限: %ld\n", sp->sp_max);
printf("密码过期前的警告天数: %ld\n", sp->sp_warn);
printf("密码失效后的宽限期: %ld\n", sp->sp_inact);
printf("账号失效日期: %ld\n", sp->sp_expire);
} else {
perror("getspnam");
}
// 重新定位到阴影密码文件的开头
setspent();
// 遍历阴影密码文件中的所有条目
while ((sp = getspent()) != NULL) {
printf("用户名: %s\n", sp->sp_namp);
printf("加密密码: %s\n", sp->sp_pwdp);
}
// 关闭阴影密码文件
endspent();
return 0;
}
组文件
可以用下列两个函数来查看组名或数值组ID:
#include<grp.h>
//getgrgid和getgrnam是用于获取组信息的函数。这些函数在操作系统中用于查找和管理与组相关的信息。
struct group *getgrgid(gid_t gid);
功能:根据组ID查找组信息
参数:gid是组ID
返回值:返回指向struct group结构的指针。如果没有找到对应的组,返回NULL
struct group *getrnam(const char *name);
功能:根据组名查找组信息
参数:name是组名的字符串
返回值:返回指向struct group结构的指针。如果没有找到对应的组,返回NULL
struct group {
char *gr_name; /* 组名 */
char *gr_passwd; /* 组密码(通常未使用) */
gid_t gr_gid; /* 组ID */
char **gr_mem; /* 组成员(用户名) */
};
//使用示例
#include <grp.h>
#include <stdio.h>
int main() {
struct group *grp;
gid_t gid = 1000; // 替换为实际的组ID
const char *groupname = "staff"; // 替换为实际的组名
// 根据组ID查找组信息
grp = getgrgid(gid);
if (grp) {
printf("组名: %s\n", grp->gr_name);
printf("组ID: %d\n", grp->gr_gid);
printf("组成员: ");
for (char **member = grp->gr_mem; *member != NULL; member++) {
printf("%s ", *member);
}
printf("\n");
} else {
perror("getgrgid");
}
// 根据组名查找组信息
grp = getgrnam(groupname);
if (grp) {
printf("组名: %s\n", grp->gr_name);
printf("组ID: %d\n", grp->gr_gid);
printf("组成员: ");
for (char **member = grp->gr_mem; *member != NULL; member++) {
printf("%s ", *member);
}
printf("\n");
} else {
perror("getgrnam");
}
return 0;
}
如果需要搜索整个组文件,则需使用另外几个函数,下面三个函数类似于针对口令文件的三个函数:
#include<grp.h>
//用于遍历和管理组数据库文件(通常是/etc/group)
struct group *getgrent(void);
void setgrent(void);
void endgrent(void);
附加组ID
我们不仅可以属于口令文件记录项中组ID所对应的组,也可属于多达16个另外的组。文件访问权限检查相应被修改为:不仅将进程的有效组ID与文件的组ID相比较,而且也将所有附加组ID与文件的组ID进行比较。
使用附加组ID的有点事不必再显示地经常更改组。一个用户会参加多个项目,因此也就要同属于多个组。此类情况是经常有的。
为了获取和设置附加组ID,提供了下列三个函数:
#include<unistd.h>
//用于在系统中获取当前进程的附加组ID列表。它主要用户系统编程和脚本编写,以了解一个用户属于哪些组。
int getgroups(int size,gid_t list[]);
size:指定list数组的大小。如果size为0,函数返回需要的组ID数。
list:一个数组,用于存储当前进程的附加组ID
返回值:
成功时,返回实际存储在list中的组ID数。如果size为0,返回需要的组ID数
失败时,返回-1,并设置errno以指示错误类型
#include<grp.h>
#include<unistd.h>
//用于设置当前进程的附加组ID列表。允许程序更改当前进程的附加组,这在权限管理和用户模拟等场景中非常有用
int setgroups(size_t size,const gid_t *list);
size:指定list数组中组ID的数量
list:一个数组,包含要设置的组ID
返回值:
成功,返回0
失败,返回-1,设置errno
#include<grp.h>
#include<unistd.h>
//用于初始化用户所属的组列表。他会将用户添加到指定的组以及该用户在系统中所属的所有其他组。这个函数常用于设置进程的组列表以匹配指定用户的组列表,尤其在改变用户上下文或模拟用户时非常有用
int initgroups(const char *user,gid_t group);
user:用户名
group:主要组ID
返回值:
成功,返回0
失败,返回-1,设置errno
登录账户记录
大多数UNIX系统都提供下列两个数据文件:utmp文件,它记录当前登录进系统的各个用户;wtmp文件,它跟踪各个登录和注销事件。
系统标识
uname函数,它返回与当前主机和操作系统有关的信息:
#include<sys/utsname.h>
//用于获取系统和内核的相关信息。通过调用uname函数,程序可以获取操作系统名称,主机名,内核版本等信息
int uname(struct utsname *buf);
buf:指向utsname结构体的指针,该结构体将包含系统信息。
返回值:
成功:返回0
失败:返回-1,设置errno
struct utsname {
char sysname[]; // 操作系统名称
char nodename[]; // 网络节点名称
char release[]; // 操作系统发布级别
char version[]; // 操作系统版本
char machine[]; // 硬件架构名称
};
//示例
#include <stdio.h>
#include <sys/utsname.h>
int main() {
struct utsname buffer;
if (uname(&buffer) != 0) {
perror("uname");
return 1;
}
printf("System name: %s\n", buffer.sysname);
printf("Node name: %s\n", buffer.nodename);
printf("Release: %s\n", buffer.release);
printf("Version: %s\n", buffer.version);
printf("Machine: %s\n", buffer.machine);
return 0;
}
BSD派生的系统提供了gethostname函数,它只返回主机名,该名字通常就是TCP/IP网络上主机的名字。
#include<unistd.h>
//用户获取当前主机的名称(主机名)。主机名通常用于标识网络中的计算机,这对于网络通信和管理非常重要
int gethostname(char *name,size_t len);
name:指向一个字符数组的指针,用于存储主机名
len:name数组的长度(即缓冲区大小)
返回值:
成功:返回0
失败:返回-1,设置errno
如果将主机连接到TCP/IP网络中,则此主机名通常是该主机的完全限定域名。
时间和日期例程
由UNIX内核提供的基本时间服务是计算自国际标准时间公园1970年1月1日00:00:00以来经过的秒数。
time函数返回当前的时间和日期:
#include<time.h>
//用于获取当前的时间
time_t time(time_t *tloc);
tloc:指向time_t类型的指针。如果不为NULL,当前时间也将存储在tloc指向的变量中。
返回值:
成功:返回当前的时间(1970年到现在的秒数)
失败:返回((time_t)-1)
与time函数相比,gettimeofday提供了更高的分辨率(最高位微秒级)。这对某些应用很重要:
#include<sys/time.h>
//用于获取当前时间,提供秒数和微妙数
int gettimeofday(struct timeval *tv,struct timezone *tz);
tv:指向timeval结构体的指针,用于存储当前时间
tz:指向timezone结构体的指针,用于存储时区信息(一般传递NULL,因为时区信息已废弃)。
返回值:
成功:返回0
失败:返回-1,设置errno
struct timeval
{
long tv_sec;//秒
long tv_usec;//微妙
}
两个localtime和gmtime将日历时间转换成年,月,日,时,分,秒,周日表示的时间,并将这些时间存放在一个tm结构中
struct tm {
int tm_sec; // 秒,范围从0到59
int tm_min; // 分,范围从0到59
int tm_hour; // 小时,范围从0到23
int tm_mday; // 一个月中的第几天,范围从1到31
int tm_mon; // 月,范围从0到11(0表示一月)
int tm_year; // 自1900年以来的年数
int tm_wday; // 一周中的第几天,范围从0到6(0表示星期日)
int tm_yday; // 一年中的第几天,范围从0到365
int tm_isdst; // 夏令时标志,正值表示夏令时,0表示非夏令时,负值表示信息不可用
};
#include<time.h>
将时间戳转换为国际标准时间
struct tm *gmtime(const time_t *timep);
timep:指向time_t类型的指针。
返回值:返回指向tm结构体的指针,如果失败,返回NULL
将时间戳转换为本地时间表示的结构体
struct tm *localtime(const time_t *timep);
将tm结构表示的本地时间转换为时间戳
time_t mktime(struct tm *timeptr);
asctime和ctime函数产生大家都熟悉的26字节的字符串
用于将时间结构转换为一个格式化的字符串表示。
char *asctime(const struct tm *timeptr);
用于将时间戳转换为格式化的字符串表示
char *ctime(const time_t *timep);
strftime函数用于根据格式字符串将tm结构表示的时间转换为指定格式的字符串。这是一个非常灵活和强大的函数,可以用于生成几乎任何形式的时间和日期字符串
size_t strftime(char *s,size_t max,const char *format,const struct tm *tm);
s:指向输出字符串的缓冲区
max:缓冲区的最大长度
format:格式字符串,指定输出的日期和时间格式。
tm:指向tm结构的指针,表示要格式化的时间
返回值:返回存储在s中的字符数(不包括终止字符)。如果生成的字符串长度超过max,则返回0,表示转换失败。
小结
所有UNIX系统都使用口令文件和组文件。我们说明了读这些文件的各种函数。本章也介绍了阴影口令,它可以增加系统的安全性。附加组ID提供了一个用户同时可以参加多个组的方法。我们还介绍了大多数系统所提供的存取其他与系统有关数据文件的类似函数。我们讨论了几个系统标识函数,应用程序使用它们以标识它在何种系统上运行。
习题
6.1如果系统使用阴影文件,那么如何取得加密口令?
普通用户无法获取,只有超级用户才能读取阴影文件。
6.2假设你有超级用户权限,并且系统使用了阴影口令,重新考虑上一道习题。
#include <stdio.h>
#include <shadow.h>
#include <errno.h>
#include <unistd.h>
int main() {
const char *username = "your_username"; // 替换为目标用户名
struct spwd *spwd_entry;
// 获取阴影文件中的密码条目
spwd_entry = getspnam(username);
if (spwd_entry == NULL) {
perror("getspnam");
return 1;
}
// 打印加密密码
printf("Encrypted password for %s: %s\n", username, spwd_entry->sp_pwdp);
return 0;
}
6.3编写一个程序,它调用uname并输出utsname结构中的所有字段,将该输出与uname命令的输出结果作比较。
#include <stdio.h>
#include <sys/utsname.h>
#include <stdlib.h>
int main() {
struct utsname uname_data;
// 调用 uname 函数获取系统信息
if (uname(&uname_data) != 0) {
perror("uname");
return 1;
}
// 输出 utsname 结构中的所有字段
printf("sysname: %s\n", uname_data.sysname);
printf("nodename: %s\n", uname_data.nodename);
printf("release: %s\n", uname_data.release);
printf("version: %s\n", uname_data.version);
printf("machine: %s\n", uname_data.machine);
return 0;
}
与uname命令的输出结果相比少了三个输出:处理器类型,硬件平台,操作系统。因为UNIX系统并不负责定义硬件及具体实现,UNIX只定义标准规范。
6.4计算可由time_t数据类型表示的最迟时间。如果超出了这一时间将会如何?
time_t被实现为一个32位整数或64位整数。
对于32位有符号整数,最大值位2^31-1,即2147483647
#include <stdio.h>
#include <time.h>
int main() {
// 定义最大的 time_t 值
time_t max_time = 2147483647;
// 将 time_t 值转换为本地时间表示
struct tm *local_time = localtime(&max_time);
// 输出最迟时间
printf("The latest time represented by 32-bit time_t: %s", asctime(local_time));
return 0;
}
运行上述代码,发现32位可以运行到2038年1月19日03:14:07,将会导致溢出问题,这被称为"2038年问题",或"Y2K38"。
6.5编写一个程序,获取当前时间,并使用strftime将输出结果转化为类似于date命令的默认输出。将环境变量TZ这职位不同的值,观察输出结果。
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
void print_current_time() {
time_t now;
struct tm *local_time;
char buffer[80];
// 获取当前时间
time(&now);
// 获取本地时间
local_time = localtime(&now);
// 格式化时间,类似于 date 命令的默认输出
strftime(buffer, sizeof(buffer), "%a %b %d %H:%M:%S %Z %Y", local_time);
// 输出格式化的时间
printf("Current time: %s\n", buffer);
}
int main() {
// 打印当前时间(默认时区)
printf("Default Time Zone:\n");
print_current_time();
// 设置环境变量 TZ 并打印不同时区的时间
setenv("TZ", "America/New_York", 1);
tzset();
printf("\nTime Zone: America/New_York\n");
print_current_time();
setenv("TZ", "Europe/London", 1);
tzset();
printf("\nTime Zone: Europe/London\n");
print_current_time();
setenv("TZ", "Asia/Tokyo", 1);
tzset();
printf("\nTime Zone: Asia/Tokyo\n");
print_current_time();
// 恢复默认时区
unsetenv("TZ");
tzset();
printf("\nRestored Default Time Zone:\n");
print_current_time();
return 0;
}