第6章 系统数据文件和信息
UNIX系统的正常运作需要使用大量与系统有关的数据文件。如口令文件
/etc/passwd/
和组文件/etc/group
- 由于历史原因,这些数据文件都是
ASCII
文本文件- 我们可以用标准 IO 库来读取这些文件,但是也可以有专门的 API 来读取这些文件
对于数据文件的可移植界面是本章的主题。也包括了系统标识函数、时间和日期函数。
6.1 口令文件
UNIX口令文件 /etc/passwd/
是一个ASCII
文件,每一行包含很多字段,字段之间用冒号分隔。这些字段包含在<pwd.h>
头文件定义的passwd
,该结构有如下成员:
说明 | struct | passwd成员 | POSIX.1 |
---|---|---|---|
用户名 | char | *pw_name | · |
加密口令 | char | *pw_passwd | |
数值用户ID | uid_t | pw_uid | · |
数值组ID | git_t | pw_gid | · |
注释字段 | char | *pw_gecos | |
初始工作目录 | char | *pw_dir | · |
初始shell(用户程序) | char | *pw_shell | · |
关于这些登录项请注意下列各点:
- 通常有一个登录项,其用户名为root,其用户ID是0(超级用户)
- 加密口令字段包含一个占位符。现在加密口令其实是放在另一个文件中
- 口令文件中某些字段可能为空。
- 如果加密口令字段为空,则说明该用户没有口令
- 如果注释字段为空,则没有任何影响
shell
字段指定了初始shell
- 若它为空,则取系统默认值(通常是
/bin/sh
) - 若它为
/dev/null
,则会阻止任何人以该字段所在行记录中的用户名来登录系统
- 若它为空,则取系统默认值(通常是
- 用户名如果是
nobody
,则任何人都可以使用它登录系统,但是其用户ID和组ID不提供任何特权。该用户ID和组ID只能访问人人皆可读、可写的文件 - 在
Linux
中,没有pw_class,pw_change,pw_expire
字段
6.1.1 getpwuid、getpwnam函数
通过用户登陆名或数值用户ID就能通过两个存取口令文件函数查看相关记录。
#include <sys/types.h>
#include <pwd.h>
struct passwd* getpwuid(uid_t uid);
struct passwd* getpwnam(const char *name);
- 参数:
uid
:用户IDname
:用户名
- 返回值:
- 成功:返回
passwd
结构的指针 - 失败:返回
NULL
- 成功:返回
注意:getpwuid/getpwnam
函数返回的 struct passwd
结构通常是函数内部的静态变量,因此多次调用上面的函数,该静态变量会被覆写。
6.1.2 查看整个口令文件
如果只是查看一个登录名或用户ID,那么上面两个POSIX.1函数能满足要求,但是要查看整个口令文件。下列三个函数则可用于此。
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
getpwent
返回值:- 成功:返回
passwd
结构的指针 - 失败:返回
NULL
- 到达文件尾端:返回
NULL
- 成功:返回
用法:
-
调用
getpwent
时,它返回口令文件中的下一个记录项返回的
struct passwd
结构通常是函数内部的静态变量,因此多次调用getpwent
函数,该静态变量会被覆写 -
在第一次调用
getpwent
函数时,它会打开所使用的各个文件 -
getpwent
对返回的各个记录项顺序并没有要求 -
setpwent
会反绕getpwent
所使用的文件到文件起始处。即当调用setpwent
之后,getpwent
又会从头开始读取记录项 -
endpwent
会关闭getpwent
所使用的文件。在调用getpwent
读取完口令文件后,一定要调用endpwent
关闭这些文件getpwent
知道什么时候应该打开它所使用的文件(第一次被调用时),但是不知道何时应该关闭这些文件。
案例:getpwnam的一个实现
#include <sys/types.h>
#include <pwd.h>
#include <stddef.h>
#include <string.h>
struct passwd* getpwnam(const char* name){
struct passwd *ptr;
if(setpwent() != 1){
// 打开 /etc/passwd 文件失败
return NULL;
}
while((ptr = getpwent()) != NULL) {
if(strcmp(name, ptr->pw_name) == 0)
endpwent(); //关闭 /etc/passwd 文件
return ptr;
}
endpwent();// 关闭 /etc/passwd 文件
return(ptr);// 用户名未找到
}
程序在开始处调用setpwent是保护性措施,以便在调用者在此之前已经调用过getpwent的情况下,反绕有关文件使它们定位到文件开始处。
6.2 阴影口令
加密口令是经单向加密算法处理过的用户口令副本。
因为此算法是单向的,所以不能从加密口令猜测到原始口令
现在的UNIX将加密口令存放在一个称作阴影口令的文件中(即文件/etc/shadow
)。该文件至少应该包含用户名和加密口令。
这些字段包含在<shadow.h>
头文件的struct spwd
结构中。相关的字段如下:
说明 | shadow成员 |
---|---|
用户登陆名 | char* sp_namp |
加密口令 | char* sp_pwdp |
上次更改口令以来经过的时间 | int sp_lstchg |
经过多少天后允许修改口令 | int sp_min |
经过多少天后必须修改口令 | int sp_max |
经过多少天后如果未修改口令则系统发出警告 | int sp_warn |
经过多少天后,该账户是inactive | int sp_inact |
经过多少天后,该账户过期 | int sp_expire |
保留字段 | unsigned int sp_flag |
其中只有用户登录名和加密口令这两个字段是必须的。其他字段都是用于控制口令更改的频率。
注意:
- 阴影口令文件
/etc/shadow
不应该由一般用户读取。- 仅有少数几个程序需要访问加密口令,如
login,passwd
。这些程序通常是设置用户ID为root
的程序 - 普通口令文件
/etc/passwd/
可以任由各用户读取
- 仅有少数几个程序需要访问加密口令,如
6.3 组文件
UNIX 组文件包含的字段定义在<grp.h>
所定义的group
结构中:
说明 | struct group成员 | POSIX.1 |
---|---|---|
组名 | char* gr_name | · |
加密口令 | char* gr_passwd | |
数字组ID | int gr_gid | · |
指向各用户名指针的数组 | char **gr_men | · |
字段gr_men是一个指针数组,其中的指针个指向一个属于该组的用户名。该数组以NULL结尾。
6.2.1 getgrgid、getgrnam函数
查看组名或数值组ID。
#include <sys/types.h>
#include <grp.h>
struct group* getgrgid(git_t gid);
struct group* getgrnam(const char* name);
- 参数:
gid
:组IDname
:组名
- 返回值:
- 成功:返回
group
结构的指针 - 失败:返回
NULL
- 成功:返回
注意:getgrgid/getgrnam
函数返回的 struct group
结构通常是函数内部的静态变量,因此多次调用上面的函数,该静态变量会被覆写。
6.2.2 查看整个组文件
#include <grp.h>
struct group* getgrent(void);
void setgrent(void);
void endgrent(void);
getgrent
返回值:- 成功:返回
group
结构的指针 - 失败:返回
NULL
- 到达文件尾端:返回
NULL
- 成功:返回
用法:
-
调用
getgrent
时,它返回组文件中的下一个记录项返回的
struct group
结构通常是函数内部的静态变量,因此多次调用getgrent
函数,该静态变量会被覆写 -
在第一次调用
getgrent
函数时,它会打开所使用的各个文件 -
getgrent
对返回的各个记录项顺序并没有要求 -
setgrent
会反绕getgrent
所使用的文件到文件起始处。即当调用setgrent
之后,getgrent
又会从头开始读取记录项 -
endgrent
会关闭getgrent
所使用的文件。在调用getgrent
读取完组文件后,一定要调用endgrent
关闭这些文件getgrent
知道什么时候应该打开它所使用的文件(第一次被调用时),但是不知道何时应该关闭这些文件
6.4 添加组ID
优点:不必在显示地经常更改组
存取和设置添加组ID:
#include <sys/types.h>
#include <unistd.h>
int getgroups(int gidsetsize, gid_t grouplist[]);
int setgroups(int ngroups, const gid_t grouplist[]);
int initgroups(const char* username, gid_t basegid);
参数:
-
对于
getgroups
函数:-
gidsetsize
:填入grouplist
数组的附属组ID的最大数量若该值为0,则函数只返回附属组ID数,而不修改
grouplist
数组 -
grouplist
:存放附属组ID的数组
-
-
对于
setgroups
函数:-
ngroups
:grouplist
数组中元素个数数量不能太大,不能超过
NGROUPS_MAX
-
grouplist
:待设置的附属组ID的数组
-
-
对于
initgroups
函数:username
:用户名basegid
:用户的base
组ID(它就是在口令文件中,用户名对于的组ID)
返回值:
- 对于
getgroups
函数:- 成功:返回附属组ID的数量
- 失败:返回 -1
- 对于
setgroups/initgroups
函数:- 成功:返回 0
- 失败:返回 -1
用法:
getgroups
函数将进程所属用户的各附属组ID填写到grouplist
中,填入该数组的附属组ID数最多为gidsetsize
个。实际填写到数组中的附属组ID数由函数返回setgroups
函数可由超级用户调用以便为调用进程设置附属组ID表。- 由于
initgroups
函数会在内部调用setgroups
函数,因此它也必须由超级用户调用
6.5 其他数据文件
除了口令文件和组文件之外,系统中还有很多其他重要的数据文件。UNIX对于这些系统数据文件提供了对应的类似的API。对于每种数据文件,至少有三个函数:
get
函数:读下一个记录。如果需要还会打开该文件。- 此种函数通常返回一个指向某个结构的指针。
- 当已到达文件尾端时,返回空指针
- 大多数
get
函数返回指向一个静态存储类结构的指针,如果需要保存其内容,则需要复制该结构
set
函数:打开相应数据文件(如果尚未打开),然后反绕该文件- 如果希望在相应文件起始处开始处理,则调用该函数
end
函数:关闭相应数据文件。在结束了对相应数据文件的读、写操作后,总应该调用此函数以关闭所有相关文件
另外如果数据文件支持某种形式的键搜索,则也提供搜索具有指定键的记录的函数
下面是各个重要的数据文件:
说明 | 数据文件 | 头文件 | 结构 | 附加的键搜索函数 |
---|---|---|---|---|
口令 | /etc/passwd | <pwd.h> | passwd | getpwnam,getpwuid |
组 | /etc/group | <grp.h> | group | getgrnam,getgrgid |
阴影 | /etc/shadow | <shadow.h> | spwd | getspnam |
主机 | /etc/hosts | <netdb.h> | hostent | getnameinfo,getaddrinfo |
网络 | /etc/networks | <netdb.h> | netent | getnetbyname,getnetbyaddr |
协议 | /etc/protocols | <netdb.h> | protoent | getprotobyname,getprotobynumber |
服务 | /etc/services | <netdb.h> | servent | getservbyname,getservbyport |
6.6 登陆会计
大多数UNIX系统都提供了两个数据文件:
- utmp文件:记录了当前登录到系统的各个用户
- wtmp文件:跟踪各个登录和注销事件
每次写入到这两个文件的是下列结构的一个二进制记录:
struct utmp{
char ut_line[8];//登录地tty
char ut_name[9];//登陆用户名
long ut_time;//自1970.01.01 00:00:00 经过的秒数
}
-
登录时,
login
程序填写此类结构,然后将其写入到utmp
文件中,同时也将其添写到wtmp
文件中 -
注销时,
init
进程将utmp
文件中相应的记录擦除(每个字节都填写null
字节),并将一个新的记录添写到wtmp
文件中 -
在系统重启时,以及更改系统时间和日期的前后,都将在
wtmp
文件中追加写特殊的记录项who
程序会读取utmp
文件;last
程序会读取wtmp
文件
linux
系统中,这两个文件的路径是/var/run/utmp
以及/var/log/wtmp
6.7 系统标识
6.7.1 uname函数
查看主机和操作系统有关的信息:
#include <sys/utsname.h>
int uname(struct utsname *name);
- 参数:
name
:一个utsname
结构的地址,该函数会填写此结构
- 返回值:
- 成功: 返回非负值
- 失败: 返回 -1
POSIX 定义了utsname
结构最少需要提供的字段(全部是字符数组),某些操作系统会在该结构中提供了另外一些字段:
struct utsname {
char sysname[]; //操作系统的名字
char nodename[]; // 节点名字
char release[]; //当前操作系统的 release
char version[]; //该 release 的版本
char machine[]; //硬件类型
}
这些字符串都是以null
结尾。
通常
uname
命令会打印utsname
结构中的信息
6.7.2 gethostname函数
返回主机名。改名字通常就是 TCP/IP
网络上主机的名字:
#include<unistd.h>
int gethostname(char* name, int namelen);
-
参数:
-
name
:放置主机名字符串的缓冲区 -
namelen
:name
缓冲区的长度如果缓冲区够长,则通过
name
返回的字符串以null
结尾;如果缓冲区不够长,则标准没有说通过name
返回的字符串是否以null
结尾
-
-
返回值:
- 成功: 返回 0
- 失败: 返回 -1
hostname
命令可以获取和设置主机名
6.8 时间和日期例程
UNIX在这方面与其他操作系统的区别是:
- 以国际标准时间而非本地时间计时;
- 可自动进行转换,例如变换到夏日制;
- 将时间和日期作为一个量值保存。time函数返回当前时间和日期。
#include <time.h>
time_t time(time_t *calptr);
- 参数:
calptr
:如果该指针不是NULL
,则返回的日历时间也存放在calptr
指向的内存中
- 返回值:
- 成功:返回当前日历时间的值
- 失败:返回 -1
-
gmtime/localtime
函数:将日历时间转换成struct tm
结构:#include<time.h> struct tm* gmtime(const time_t *calptr); struct tm* localtime(const time_t *calptr);
- 参数:
calptr
:指向日历时间的指针 - 返回值:
- 成功:指向
struct tm
结构的指针 - 失败:返回
NULL
- 成功:指向
struct tm{ int tm_sec; //秒数,范围是 [0~60] int tm_min; //分钟数,范围是 [0~59] int tm_hour; //小时数,范围是 [0~23]。午夜12点是 0 int tm_mday; //一个月中的天数,范围是 [1~31] int tm_mon; //月数,范围是 [0~11] ,一月是 0 int tm_year; //年数,范围是 [1900~],如果是16则表示 1916 int tm_wday; //一个星期中的天数,范围是 [0~6] ,周日是0 int tm_yday; //一年中的天数,范围是 [0~365],一月一号是 0 int tm_isdst; //daylight saving time flag }
其中秒可以超过 59 的理由是表示润秒
gmtime/localtime
函数的区别:gmtime
:将日历时间转换成统一协调的年月日时分秒周日分解结构localtime
:将日历时间转换成本地实际(考虑本地市区和夏令时标志),由TZ
环境变量指定
TZ
环境变量影响localtime/mktime/strftime
这三个函数:- 如果定义了
TZ
环境变量:则这些函数将使用TZ
的值代替系统默认时区 - 如果
TZ
定位为空TZ=
,则使用UTC
作为时区
- 参数:
-
mktime
函数:以本地时间的年月日等作为参数,将其变化成time_t
值:#include<time.h> time_t mktime(struct tm*tmptr);
- 参数:
tmptr
:指向struct tm
结构的指针 - 返回值:
- 成功: 返回日历时间
- 失败: 返回 -1
所谓的本地实际的”本地“:由
TZ
环境变量指定 - 参数:
-
strftime/strftime_l
函数:类似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);
-
参数:
buf
:存放格式化后的时间字符串的缓冲区的地址maxsize
:存放格式化后的时间字符串的缓冲区的大小format
:时间的格式化字符串tmptr
:存放时间的struct tm
结构的指针
对于
strftime_l
函数:locale
:指定的区域
-
返回值:
- 成功:返回存入
buf
的字符数 - 失败: 返回 0
- 成功:返回存入
注意:
- 如果
buf
长度足够存放格式化结果以及一个null
终止符,则这两个函数才有可能顺利转换;否则空间不够,这两个函数返回0,表示转换失败 strftime_l
运行调用者将区域指定为参数;而strftime
使用通过TZ
环境变量指定的区域format
参数控制时间值的格式。如同printf
,转换说明的形式是百分号之后跟随一个特定的字符,而format
中的其他字符则按照原样输出:%a
:缩写的周日名,如Thu
%A
:周日名,如Thursday
%b
:缩写的月名:如Jan
%B
:全月名,如January
%c
:日期和时间,如Thu Jan 19 21:24:25 2012
%C
:年的最后两位,范围是(00~99),如20
%d
:月日,范围是 (01~31),如19
%D
日期(MM/DD/YY),如01/19/12
%e
月日(一位数字前加空格)(1~31),如19
%F
:ISO 8601 日期格式 (YYYY-MM-DD),如2012-01-19
%g
:ISO 8601 年的最后2位数(00~99),如12
%G
:ISO 8601 的年,如2012
%h
:与%b
相同,缩写的月名%H
:小时(24小时制)(00~23)%I
:小时(12小时制)(01~12)%j
:年日(001~366),如019
%m
:月(01~12),如01
%M
:分(00~59),如24
%n
:换行符%p
:AM/PM
%r
:本地时间(12小时制),如09:24:52 PM
%R
:与%H:%M
相同%S
:秒(00~60),如52
%t
:水平制表符%T
:同%H:%M:%S
相同,如21:24:52
%u
:ISO 8601 周几(1~7,1为周一)%U
:一年的星期日周数(00~53)%V
:ISO 8601 周数(01~53)%w
:周几:(0~6,周日为0)%W
:一年的星期一周数(00~53)%x
:本地日期,如01/19/12
%X
:本地时间,如21:24:52
%y
:年的最后两位(00~99)%Y
:年,如2012
%z
:ISO 8601 格式的UTC偏移量,如-0500
%Z
:时区名,如EST
%%
:百分号
-
-
strptime
函数:它是strftime
的逆向过程,把时间字符串转换成struct tm
时间#include<time.h> char *strptime(const char*restrict buf,const char*restrict format, struct tm*restrict tmptr);
- 参数:
buf
:存放已经格式化的时间字符串的缓冲区的地址format
:给出了buf
缓冲区中的格式化时间字符串的格式tmptr
:存放时间的struct tm
结构的指针
- 返回值:
- 成功:返回非
NULL
- 失败:返回
NULL
- 成功:返回非
注意:
strptime
的格式化说明与strftime
的几乎相同,但是下列会有区别%a
:缩写或者完整的周日名%A
:同%a
%b
:缩写或者完整的月名%B
:同%b
%n
:任何空白%t
:任何空白
- 参数:
本章小结
在所有UNIX系统上都使用口令文件和组文件。我们说明了各种读这些文件的函数。也介绍了阴影口令,它可以增加系统的安全性。添加组ID正在得到更多应用,它提供了一个用户同时可以参加多个组的方法。还介绍了大多数系统所提供的存取其他与系统有关的数据文件的类似的函数。最后,说明了ANSIC和POSIX.1提供的与时间和日期有关的一些函数。
习题
6.1 如果系统使用阴影文件,如何取得加密口令?
普通用户和普通程序无法直接获取加密口令,获取加密口令通常需要特权和系统管理员的权限,并且必须遵循系统的安全策略。
阴影口令(shadow password)是一种更加安全的方式来存储用户密码,通常将加密密码存储在
/etc/shadow
文件中,而不是/etc/passwd
文件中。这种安全性是通过限制对阴影文件的访问来实现的,通常只有系统管理员或具有特权的用户可以访问该文件。在Unix/Linux系统中,一般的密码验证方法是通过标准的身份验证工具来验证用户身份,而不是获取用户的明文密码。因此,阴影文件的设计目的之一是为了提高密码的安全性,阻止非授权的访问和泄漏。
6.2 假设你有超级用户许可权,并且系统使用了阴影口令,重新考虑上一道习题。
在Unix/Linux系统中,阴影文件通常是 /etc/shadow
,存储了用户的加密密码。只有root用户或具有sudo权限的用户才能访问该文件。因此,作为root用户,你可以通过以下方法来获取加密口令:
-
直接读取
/etc/shadow
文件:使用文本编辑器或命令行工具,你可以打开/etc/shadow
文件并查看其中的内容。这将包括用户的加密密码,通常是一个经过哈希加密的字符串。 -
使用专门的密码破解工具:虽然直接读取
/etc/shadow
文件是一种方式,但密码通常是经过哈希处理的,所以不容易直接解密。但你可以使用专门的密码破解工具,如John the Ripper,来尝试破解加密密码。这通常需要一些计算时间,特别是对于强密码。
代码编译后,使用root权限运行。
#include <iostream>
#include <string>
#include <shadow.h>
int main(int argc, char* argv[]){
std::string name;
std::cout << "input your user name: ";
std::cin >> name;
struct spwd* ptr = nullptr;
ptr = getspnam(name.c_str());
if(ptr){
std::cout << ptr->sp_pwdp << std::endl;
}
}
这个程序会提示用户输入用户名,然后使用 getspnam
函数来获取阴影文件中的密码信息,最后将密码信息输出到标准输出。
6.3 编程调用uname并输出utsname结构中的所有字段,并与uname(1)命令的输出结果作比较。
使用uname函数获取系统信息,并输出utsname结构中的所有字段。
#include <stdio.h>
#include <sys/utsname.h>
int main(){
struct utsname uts;
if(uname(&uts) == -1){
perror("uname");
return 1;
}
printf("System Name: %s\n",uts.sysname);
printf("Node name: %s\n",uts.nodename);
printf("Release: %s\n",uts.release);
printf("Version: %s\n",uts.version);
printf("Machine: %s\n",uts.machine);
return 0;
}
此程序包含以下步骤:
- 包含必要的头文件,包括
<stdio.h>
和<sys/utsname.h>
。 - 声明一个
struct utsname
类型的变量uts
用于存储系统信息。 - 使用
uname(&uts)
函数来获取系统信息并存储在uts
结构中。 - 使用
printf
函数输出utsname
结构中的各个字段,例如系统名称、节点名称、版本等。 - 运行程序并比较输出结果与
uname
命令的输出结果。
与uname(1)命令的输出结果相比少三个输出:处理器类型、硬件平台、操作系统。因为UNIX系统并不负责定义硬件及具体实现,UNIX只定义标准规范。
6.4 编程获取当前时间并使用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[64] = {0};
// 将时间戳转换为本地时间结构
if (tmp = localtime(&tm_t)) {
// 格式化时间字符串
strftime(buf, 63, "%Y年 %m月 %e日 星期%u %H:%M:%S %Z\n", tmp);
}
// 输出格式化后的时间字符串
std::cout << "\n个人程序:";
std::cout << "\n" << buf << std::endl;
return 0;
}
这些注释可以帮助理解代码的各个部分以及其功能。这段代码的主要目的是获取当前时间并将其格式化为包含年份、月份、日期、星期、小时、分钟、秒和时区的字符串,并将其输出到标准输出流。