UNIX环境高级编程 第6章 系统数据文件和信息

时间:2022-12-18 12:00:25

UNIX系统的正常运作需要用到大量与系统有关的数据文件,例如系统用户账号、用户密码、用户组等文件。出于历史原因,这些数据文件都是ASCII文本文件,并且使用标准I/O库函数来读取。

口令文件

/etc/passwd文件是UNIX安全的关键文件之一。该文件用于用户登录时校验用户的口令,文件中每行的一般格式为:

用户名:x:用户ID:用户组ID:说明信息:个人主目录:SHELL

对于第二项x来说是密码,但由于安全原因密码已经被移至其他文件,因此使用x来代替。

UNIX系统提供了两个用于获取passwd文件中条目的函数,在给出用户名或者用户ID之后,这两个函数即可查看相关信息。其头文件及函数原型如下:

#include <pwd.h>

struct passwd* getpwuid(uid_t __uid);
struct passwd* getpwnam(const char *__name);

函数成功是返回相应指针,出错返回NULL。第一个函数使用用户UID,第二个函数使用用户名。返回的指针指向一个静态变量,因此只要再调用任一相关的函数就会改写指针所指向的变量内容。

上面两个函数一次只能查看一个,而且必须提供用户名或者用户UID,如果想要查看所有账户信息或者事先不了解用户名及用户UID,则可以通过下面几个函数来查看。其头文件及函数原型如下:

#include <pwd.h>

struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);

getpwent( )函数用于读取passwd文件,setpwent( )用于设置从passwd文件头部开始读,endpwent( )用于关闭文件。

阴影口令

阴影口令文件/etc/shadow用于存储加密后的密码,该密码加密方式是单向不可逆。shadow文件普通用户不允许读取,访问shadow文件的一组函数与访问passwd文件函数类似。其头文件及函数原型如下:

#include <shadow.h>

struct spwd *getspnam(const char *__name);
struct spwd *getspent(void);
void setspent(void);
void endspent(void);

函数成功是返回相应指针,出错返回NULL。

组文件

组文件/etc/group包含了系统用户组相关的信息,UNIX系统提供了两个函数来查看。其头文件及函数原型如下:

#include <grp.h>

struct group *getgrgid (gid_t __gid);
struct group *getgrnam (const char *__name);

函数成功是返回相应指针,出错返回NULL。

如果需要搜索整个组文件,则类似passwd文件函数。其头文件及函数原型如下:

#include <grp.h>

struct group *getgrent(void);
extern void setgrent(void);
extern void endgrent(void);

附属组ID

现有的UNIX都支持一个用户加入多个组,对于除了创建用户时产生的自带用户组之外,用户额外加入的其他组则为附属组。为了获取和设置附加组ID,UNIX提供了3个函数。其头文件及函数原型如下:

#include <unistd.h>
#include <grp.h> int getgroups (int __size, gid_t __list[]);
int setgroups (size_t __n, const gid_t *__groups);
int initgroups (const char *__user, gid_t __group);

登录账户记录

UNIX系统还提供了一些事件簿记功能,该功能可以查看当前登录到系统的各个用户,还能跟踪登录注销等事件。UNIX中利用一个二进制结构来填写记录。登录时,login程序将信息写入到utmp文件和wtmp文件中。注销时,init进程将utmp文件中的记录移除,并添加一个新记录到wtmp文件中。

系统标识

UNIX系统提供了一个函数用于返回与主机和操作系统有关的信息。其头文件及函数原型如下:

#include <sys/utsname.h>

int uname(struct utsname *__name);

函数成功时返回非负值,失败返回-1。

时间和日期例程

正如第一章所述,UNIX为运行在它之上的程序提供各种服务,其中就包括提供时间这一服务。UNIX系统提供的时间服务是计算自协调世界时1970年1月1日以来所经过的秒数,该秒数使用time_t类型表示。在UNIX系统中,可以使用time( )函数来返回当前日期和时间。其头文件及函数原型如下:

#include <time.h>

time_t time (time_t *__timer);

函数成功返回时间值,失败返回-1。如果__timer指针非空,那么__timer指针所指的对象也被设置为相应的值。

time( )函数只能用来获取当前时间,UNIX系统还提供了一个函数用于获取指定时钟开始之后的秒数。其头文件及函数原型如下:

#include <time.h>

int clock_gettime (clockid_t __clock_id, struct timespec *__tp);

函数成功时返回0,失败返回-1。该函数可以用来计时,比如从当前程序开始执行开始到程序停止执行为止统计总计花费了多少时间。

当我们得到时间之后,这个时间是一个自1970年以来经过的秒数,对人来说它无意义,必须转换为人类可读的时间。转换又分为两步,第一步是转换为年月日时分秒,第二步是格式化输出。如果仅仅用于程序的计算,那么第一步转换已经满足,但为了打印输出让人可读,还必须进行格式化输出。UNIX系统提供了两个函数用于从time_t转换为年月日时分秒的时间结构类型,提供了一个逆向从年月日时分秒的时间结构类型转到time_t的函数,还提供了两个格式化输出的函数。其头文件及函数原型如下:

#include <time.h>

struct tm *gmtime(const time_t *timer);
struct tm *localtime(const time_t *timer);
time_t mktime(struct tm *tp);
size_t strftime(char* s, size_t maxsize, const char* format, const struct tm* tp);
size_t strftime_l(char* s, size_t maxsize, const char* format, const struct tm* tp, locale_t loc);

对于前两个函数,成功返回相应指针,失败返回NULL;对于第三个函数,成功返回时间,失败返回-1;对于最后两个函数,成功返回字符数,失败返回0。

习题

6.1 如果系统使用阴影文件,那么如何取得加密口令?

无法获取,因为shadow文件只允许root用户访问,普通用户无法取得。

6.2 假设你有超级用户权限,并且系统使用了阴影口令,重新考虑上一道习题。

#include <iostream>
#include <shadow.h> int main (int argc, char *argv[])
{
string name;
cout << "input your user name: ";
cin >> name;
struct spwd *ptr = nullptr;
ptr = getspnam(name.c_str()); if (ptr)
{
cout << ptr->sp_pwdp << endl;
} return ;
}

代码编译后,使用root权限运行。

6.3 编写一程序,它调用uname并输出utsname结构中的所有字段,将该输出与uname(1)命令的输出结果进行比较。

#include <sys/utsname.h>
#include <iostream> using namespace std; int main (int argc, char *argv[])
{
utsname p;
if (uname(&p) >= )
{
cout << p.sysname << ' '
<< p.nodename << ' '
<< p.release << ' '
<< p.version << ' '
<< p.machine << endl;
}
return ;
}

与uname(1)命令的输出结果相比少三个输出:处理器类型、硬件平台、操作系统。因为UNIX系统并不负责定义硬件及具体实现,UNIX只定义标准规范。

6.4  计算可由time_t数据类型表示的最近时间。如果超出了这一时间将会如何?

C++11标准规定long类型最少占32位,在我的计算机上,系统使用long int来实现time_t,实际使用64位来表示long类型,因此其取值值范围为 -9223372036854775808~9223372036854775807,由于该值特别大,2900亿年后才会溢出,此时宇宙可能都不存在了。对于某些32位系统或者旧的程序,它们的time_t类型是使用32位int来实现的,而int取值范围为-2147483648~2147483647,我们可以利用localtime( )函数来分解该值,并用strftime( )函数来打印,程序如下:

#include <iostream>
#include <climits>
#include <ctime> int main (int argc, char *argv[])
{
time_t tm_t = INT_MAX;
tm* tmp = nullptr;
char buf[] = {};
if (tmp = localtime(&tm_t))
{
strftime(buf, , "%Y/%m/%e %H:%M:%S \n", tmp);
}
std::cout << buf << std::endl; return ;
}

程序运行结果如下:

UNIX环境高级编程 第6章 系统数据文件和信息

由于我的本地北京时间有8个小时的时差提前,因此真正的溢出时间为2038年1月19日03:14:07。如果到这一天将会溢出,由于int是有符号数,而C和C++标准都对有符号数溢出行为未定义,因此程序的行为无法预计,可能崩溃,可能复位,可能溢出后是个无法估计的值。下面是个动态演示图:

UNIX环境高级编程 第6章 系统数据文件和信息

2038年问题演示:第一行是“time_t”数字的二进制表示;第二行是其十进制表示;第三行是受影响的计算机理解的时间;第四行是实际的时间。

上面图片来自wikipedia.org。

6.5 编写一程序,获取当前时间,并使用strftime将输出结果转换为类似于date(1)命令的默认输出。将环境变量TZ设置为不同值,观察输出结果。

#include <iostream>
#include <climits>
#include <ctime> int main (int argc, char *argv[])
{
time_t tm_t = time(nullptr);
tm* tmp = nullptr;
char buf[] = {};
if (tmp = localtime(&tm_t))
{
strftime(buf, , "%Y年 %m月 %e日 星期%u %H:%M:%S %Z\n", tmp);
}
std::cout << "\n个人程序:";
std::cout << '\n' << buf << std::endl; return ;
}

编译代码后在不同时区执行结果如下:

Africa Abidjan:

UNIX环境高级编程 第6章 系统数据文件和信息

America Adak:

UNIX环境高级编程 第6章 系统数据文件和信息

Asia Shanghai:

UNIX环境高级编程 第6章 系统数据文件和信息