1. 口令文件 /etc/passwd
用户每次登录都要使用口令文件,该文件中的登录项由以下几项(可能有其他更多的项)构成,不同项之间用冒号:
分隔
用户名:加密口令:数值用户id:数值组id:注释字段:初始工作目录:初始shell
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
- 通常有一个用户名为root的登录项,其用户ID是0(超级用户)
- 加密口令项包含一个占位符。将加密口令存放在一个人人可读的文件中是一个安全性漏洞,所以现在将加密口令存放在另一个文件中
- 口令中的某些字段可能为空,如果注释字段为空不产生任何影响。
1.1 通过数值用户id或用户名查找口令文件中的指定登录项
struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
getpwnam返回/etc/passwd中与name匹配的用户名登录项
getpwuid返回/etc/passwd中与uid匹配的数值用户id登录项
两个函数都返回一个指向passwd结构体的指针,该结构体对象通常是函数内部的静态变量,只要调用这两个函数,其内容就被重写。因此调用后应该将该对象内容保存出来,以免后续调用更改该对象内容。
示例:
struct passwd * info = getpwuid(0);
cout << "用户名:"<< info->pw_name << "加密字段:"<< info->pw_passwd
<< "数值用户id:"<< info->pw_uid << "数值组id:"<< info->pw_gid
<< "注释字段:"<< info->pw_gecos << "初始工作目录:"<< info->pw_dir
<< "初始shell:" << info->pw_shell << endl;
info = getpwnam("root");
cout << "用户名:"<< info->pw_name << "加密字段:"<< info->pw_passwd
<< "数值用户id:"<< info->pw_uid << "数值组id:"<< info->pw_gid
<< "注释字段:"<< info->pw_gecos << "初始工作目录:"<< info->pw_dir
<< "初始shell:" << info->pw_shell << endl;
//输出:
//用户名:root加密字段:x数值用户id:0数值组id:0注释字段:root初始工作目录:/root初始shell:/bin/bash
//用户名:root加密字段:x数值用户id:0数值组id:0注释字段:root初始工作目录:/root初始shell:/bin/bash
1.2 查看口令文件中的所有登录项
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
调用getpwent返回口令文件中的下一个记录项。同样的,该函数返回的passwd指针指向的对象是该函数内部的一个静态变量,即每次调用该函数都重写该对象。在第一次调用getpwent函数时,它会打开口令文件。
setpwent函数将getpwent的读写指针移动到口令文件开头。
endpwent关闭口令文件。在使用getpwent查看完口令文件后,一定要调用endpwent函数关闭口令文件。因为getpwent函数知道什么时候打开文件(第一次调用时),但是不知道何时关闭文件。
示例:
struct passwd* info;
setpwent();
while((info = getpwent()) != nullptr) {
cout << "用户名:"<< info->pw_name << "加密字段:"<< info->pw_passwd
<< "数值用户id:"<< info->pw_uid << "数值组id:"<< info->pw_gid
<< "注释字段:"<< info->pw_gecos << "初始工作目录:"<< info->pw_dir
<< "初始shell:" << info->pw_shell << endl;
}
endpwent();
2. 阴影口令 shadow password
加密口令是经单向加密算法处理过的用户口令副本。因为算法是单向的,因此不能从加密口令猜测原来的口令(明文口令,即Password:后面用户键入的口令)
为了防止可以随意的获取加密口令,某些系统(如linux)将加密口令存放在另一个称为阴影口令的文件/etc/shadow中。该文件至少要包含用户名和加密口令。与该口令相关的其他信息也可以存放在该文件中(见后面的spwd结构体成员)。
阴影口令文件不是一般用户可以访问的。只有超级用户权限(有效用户ID为0)的进程可以访问。
2.1 访问阴影口令文件
struct spwd *getspnam(const char *name);
struct spwd *getspent(void);
void setspent(void);
void endspent(void);
struct spwd {
char *sp_namp; /* 用户登录名 */
char *sp_pwdp; /* 加密口令(加密后的明文口令) */
long sp_lstchg; /* 上次更改口令以来经过的时间 */
long sp_min; /* 经过多少天后允许更改 */
long sp_max; /* 要求更改尚余天数 */
long sp_warn; /* 超期警告天数 */
long sp_inact; /* 账户不活动之前尚余天数 */
long sp_expire; /* 账户超期天数 */
unsigned long sp_flag; /* 保留 */
};
这组api与访问口令文件的使用方式类似。
示例:
struct spwd* info;
setspent();
while((info = getspent()) != nullptr) {
cout << "用户名:"<< info->sp_namp << endl << "加密字段:"<< info->sp_pwdp << endl;
}
endspent();
/* 输出
用户名:root
加密字段:$6$4GIXxGIn$taBUn7Dqf7FfvvxMdZAEO3fvUxUdVlXSDq40i1mP6Mil.GxVQsdipJYIPAWtA.aii1NHOh5lH6Y.JB.ZQe88v1
用户名:daemon
加密字段:*
...
*/
3. 组文件
组文件/etc/group包含了用户组id与组名的映射关系,与组相关的一些其他信息也保存在该文件中(见下面的group结构体)
与之前的api类似,可以通过以下函数访问/etc/group组文件
struct group *getgrnam(const char *name);//根据组名返回指定组信息
struct group *getgrgid(gid_t gid);//根据组id返回指定组信息
//搜索整个组文件
struct group *getgrent(void);//返回组文件中下一个记录
void setgrent(void);
void endgrent(void);
struct group {
char *gr_name; /* 组名 */
char *gr_passwd; /* 组加密口令 */
gid_t gr_gid; /* 数值组id */
char **gr_mem; /* 指向各用户名指针数组 */
};
需要注意,gr_mem是一个指针数组,其中每个指针指向一个属于该组的用户名。该数组以nullptr结尾。这个成员可以用来查询附属组
4. 附属组id
一个用户可以属于至多16个另外的组。文件访问权限检查:不仅将进程的有效组ID与文件组ID比较,而且也将所有附属组ID与文件组ID比较。
使用附属组id的优点:不必再显式的经常更改组,一个用户会参与多个项目,因此也就要同时属于多个组。
用于获取和设置附属组,使用以下函数
int getgroups(int size, gid_t list[]);//成功返回附属组数量,出错返回-1
将进程所属附属组id写到数组list中,最多写size个。返回实际写入的附属组id个数。如果size为0,则直接返回附属组id个数,不对list参数进行写入。
int setgroups(size_t size, const gid_t *list);
只能由超级用户调用(root),设置调用进程的附属组id。参数list是一个数组,元素个数是size,用以保存要设置的附属组id。
5. 其他数据文件
除了口令文件、阴影口令文件、组文件以外,UNIX操作系统还使用其他数据文件。如记录各网络服务器所提供的服务的数据文件(/etc/services),记录协议信息的数据文件(/etc/protocols),记录网络信息的数据文件(/etc/networks)。访问这些数据文件的接口都很类似。
通常对于访问这些数据文件,需要三个步骤(对应三个api函数)
- get函数:读下一条记录,如果需要,会打开该文件。这种函数通常返回一个结构体指针,如果读到末尾返回null。需要注意这种函数返回的指针指向的对象是函数中的静态变量,因此再次调用会覆盖该对象内容。因此在调用后要复制它
- set函数:如果尚未打开则打开数据文件,并将读写指针设置为该文件初始位置
- end函数:关闭数据文件。
除此之外,一些访问数据文件api还提供根据键进行搜索,返回指定记录项。如getpwnam根据用户名查找口令文件中的登录项,getpwuid根据数值用户id查找口令文件中的登录项。
6. 系统标识
可以通过uname函数,返回与主机和操作系统有关的信息
int uname(struct utsname *buf);
struct utsname {
char sysname[]; /* 操作系统名字 (e.g., "Linux") */
char nodename[]; /* Name within "some implementation-defined
network" */
char release[]; /* Operating system release (e.g., "2.6.28") */
char version[]; /* Operating system version */
char machine[]; /* Hardware identifier */
};
该函数将主机与操作系统信息保存在输入参数结构体指针中。
也可以通过gethostname函数获取主机名,该函数将主机名保存在参数指定的缓冲区中,该函数返回的主机名是TCP/IP网络上主机的名字
int gethostname(char *name, size_t len);
超级用户可以通过sethostname设置主机名
int sethostname(const char *name, size_t len);
示例:
struct utsname info;
uname(&info);
cout << "操作系统名:" << info.sysname << endl;
char str[100];
gethostname(str,100);
cout << "主机名:" << str << endl;
//输出
//操作系统名:Linux
//主机名:wudi-huaweiyun
7. 在C程序中获取环境变量
7.1 获取指定环境变量
char *getenv(const char *name);
示例:
cout << getenv("PATH") << endl;//输出PATH环境变量
7.2 获取所有环境变量
int main(int argc, char* argv[], char**envp) {
for(char** env = envp ; *env != nullptr ; env++) {
cout << *env << endl;
}
}
8. 时间和日期
8.1 time函数
使用time_t类型表示自1970年开始所经理的秒数,称之为日历时间。
通过time函数返回当前日历时间
time_t time(time_t *tloc);
时间值作为返回值返回。如果参数不为NULL,也将时间值保存在参数指向的time_t中。
8.2 clock_gettime函数
可以通过clock_gettime函数检索clk_id指定的时钟时间,时间保存在timespec结构体中(它把时间表示为秒和纳秒)
int clock_gettime(clockid_t clk_id, struct timespec *tp);
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
参数clk_id:
-
CLOCK_REALTIME:系统实时时间,即从1970年开始的时间
-
CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
-
CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码的CPU时间
-
CLOCK_THREAD_CPUTIME_ID:本线程到当前代码的CPU时间
8.3 clock_settime函数
可以设置clk_id指定的时钟时间,但是有些时钟时间不能修改
int clock_settime(clockid_t clk_id, const struct timespec *tp);
8.4 gettimeofday函数
获取微秒级的自1970年来的时间值。注意第二个参数必须是NULL
int gettimeofday(struct timeval *tv, struct timezone *tz);
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
8.5 localtime/gmtime/mktime函数
获取到日历时间(自1970年来的时间)后,需要将其转换为人们刻度的时间和日期等具体信息。具体转换关系如下图所示
其中localtime、mktime和strftime函数都受到环境变量TZ的影响。
TZ环境变量:用于设置时区。它由各种时间函数用于计算相对于全球标准时间 (UTC)(以前称为格林威治标准时间 (GMT))的时间
struct tm *localtime(const time_t *timep);
struct tm *gmtime(const time_t *timep);
这两个函数都将日历时间(输入参数)转换成分解的时间,并将结果存放在tm结构体中
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */
int tm_mon; /* Month (0-11) */
int tm_year; /* Year - 1900 */
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */
};
localtime和gmtime的区别:
localtime将日历时间转换成本地时间(根据时区TZ环境变量),而gmtime将日历时间转换为格林威治(GMT)时间的函数
mktime以本地时间的tm结构体为参数,转换为time_t值,该函数受时区TZ环境变量影响
time_t mktime(struct tm *tm);
8.5 strftime和strptime函数
strftime将tm结构体表示的本地时间进行格式化转换为字符串并存储在指定缓冲区中(输入参数),该函数受时区TZ环境变量影响
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
其中format函数表示输出的格式,转换方式是%后跟一个特殊字符
示例:
int main(int argc, char* argv[], char**envp) {
time_t t;
time(&t);
auto tm = localtime(&t);
char str[1024];
strftime(str,1024,"年:%Y 月:%B 日:%d 时:%H 分:%M 秒:%S\n",tm);
cout << str;
}
//输出:
//年:2021 月:October 日:24 时:18 分:56 秒:08
strptime函数是strftime的反过来版本,把字符串按照格式转换为时间tm
char *strptime(const char *s, const char *format, struct tm *tm);
format格式转换说明:
8.6 ctime函数
根据参数返回一个表示当地时间的字符串。
返回的字符串格式如下: Www Mmm dd hh:mm:ss yyyy 其中,Www 表示星期几,Mmm 是以字母表示的月份,dd 表示一月中的第几天,hh:mm:ss 表示时间,yyyy 表示年份。
char *ctime(const time_t *timep);//输出例子:Mon Aug 13 08:23:14 2012