系统数据文件和信息
口令文件/etc/passwd
和组文件/etc/group
经常被多个进程频繁使用。用户每次登录Linux和使用ls
命令都会访问口令文件。
除了直接访问文件之外,系统通过一些接口来对外提供信息,比如系统标识函数、时间和日期函数。
口令
口令文件
Unix口令文件/etc/passwd
会包含下图所示的各字段,这些字段定义包含在pwd.h
中定义的passwd
结构体中。
说明 | struct passwd 成员 | POSIX.1 |
---|---|---|
用户名 | char *pw_name | * |
加密口令 | char *pw_passwd | |
用户ID | uid_t pw_uid | * |
组ID | gid_t pw_gid | * |
注释字段 | char *pw_gecos | |
初始工作目录 | char *pw_dir | * |
初始shell | char *pw_shell | * |
用户访问类 | char *pw_class | |
下次更改口令时间 | time_t pw_change | |
账户有效时间 | time_t pw_expire |
Linux glibc包含了前7个字段,POSIX.1指定必须包含5个字段。
/* A record in the user database. */
struct passwd
{
char *pw_name; /* Username. */
char *pw_passwd; /* Hashed passphrase, if shadow database
not in use (see shadow.h). */
__uid_t pw_uid; /* User ID. */
__gid_t pw_gid; /* Group ID. */
char *pw_gecos; /* Real name. */
char *pw_dir; /* Home directory. */
char *pw_shell; /* Shell program. */
};
/etc/passwd
文件结构如下所示:
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
blduan:x:1000:1000:blduan:/home/blduan:/bin/bash
test:x:1001:1001::/home/test:/bin/bash
sshd:x:127:65534::/run/sshd:/usr/sbin/nologin
内容解析:
- 通常有一个用户名为
root
的登录项,用户ID=0。 - 加密口令字段包含了一个占位符x。早期Unix中此处是加密口令,处于安全考虑,已经将加密口令单独放在文件
/etc/shadow
中。 - 口令文件项中某些字段可能为空,
test
和sshd
用户的注释字段为空。 shell
字段包含了一个可执行程序名,它是被用作该用户的登录shell
。若该字段为空吗,则取系统默认值,通常为/bin/bash
。使用/usr/sbin/nologin
是为了不允许用户登录,并使用该程序打印指定错误。nobody
用户名的一个目的是,是任何人都可以登录该系统,但其用户ID(65534)和组ID(65534)不提供任何特权。该用户ID和组ID只能访问人人皆可读写的文件。可以用来
Ubuntu提供了vipw
命令,可以使用该命令编辑/etc/passwd
,使用该命令的好处是可以确保它做的更改与其他相关文件保持一致。
POSIX.1定义了两个获取口令文件项的函数。在给出用户登录名或用户ID后,可以通过这两个函数查看其它项。
#include <pwd.h>
struct passwd* getpwuid(uid_t uid);
struct passwd* getpwnam(const char* name);
/* 成功返回passwd结构体指针,失败返回NULL */
getpwuid
函数有ls
使用,参数为i节点中的用户ID。getpwnam
函数有login
程序调用。
passwd
通常为函数内部的静态变量,只要调用任一相关函数,其内容就会被重写。
#include <pwd.h>
#include <stdio.h>
int main()
{
struct passwd* pPWD = NULL;
if ((pPWD = getpwnam("blduan")) != NULL)
{
printf("pw_name=%s\n", pPWD->pw_name);
printf("pw_passwd=%s\n", pPWD->pw_passwd);
printf("pw_uid=%d\n", pPWD->pw_uid);
printf("pw_gid=%d\n", pPWD->pw_gid);
printf("pw_gecos=%s\n", pPWD->pw_gecos);
printf("pw_dir=%s\n", pPWD->pw_dir);
printf("pw_shell=%s\n", pPWD->pw_shell);
}
if ((pPWD =getpwuid(1001)) != NULL)
{
printf("pw_name=%s\n", pPWD->pw_name);
printf("pw_passwd=%s\n", pPWD->pw_passwd);
printf("pw_uid=%d\n", pPWD->pw_uid);
printf("pw_gid=%d\n", pPWD->pw_gid);
printf("pw_gecos=%s\n", pPWD->pw_gecos);
printf("pw_dir=%s\n", pPWD->pw_dir);
printf("pw_shell=%s\n", pPWD->pw_shell);
}
}
执行结果如下:
$ cat /etc/passwd | grep 'blduan'
blduan:x:1000:1000:blduan:/home/blduan:/bin/bash
$ cat /etc/passwd | grep '1001'
test:x:1001:1001::/home/test:/bin/bash
$ ./passwd_info_by_user_or_id
pw_name=blduan
pw_passwd=x
pw_uid=1000
pw_gid=1000
pw_gecos=blduan
pw_dir=/home/blduan
pw_shell=/bin/bash
pw_name=test
pw_passwd=x
pw_uid=1001
pw_gid=1001
pw_gecos=
pw_dir=/home/test
pw_shell=/bin/bash
如果要查看整个口令文件,可以使用以下的函数:
#include <pwd.h>
struct passwd* getpwent(void);
/* 成功返回指针,失败或到达文件为返回NULL */
void setpwent(void);
void endpwent(void);
调用getpwent
函数时,它返回口令文件的下一个记录项。
函数setpwent
用来将getpwent
的读写地址指向口令文件开头,endpwent
则关闭口令文件。
getpwuid
和getpwnam
函数调用完之后也不应该使文件处于打开状态,应调用endpwent
关闭文件。
#include <pwd.h>
#include <stdio.h>
#include <string.h>
int main()
{
struct passwd* pPWD;
setpwent();
while ((pPWD = getpwent()) != NULL)
{
printf("pw_name=%s,", pPWD->pw_name);
printf("pw_passwd=%s,", pPWD->pw_passwd);
printf("pw_uid=%d,", pPWD->pw_uid);
printf("pw_gid=%d,", pPWD->pw_gid);
printf("pw_gecos=%s,", pPWD->pw_gecos);
printf("pw_dir=%s\n", pPWD->pw_dir);
}
endpwent();
}
执行结果如下所示:
$ ./passwd_info_traverse
pw_name=root,pw_passwd=x,pw_uid=0,pw_gid=0,pw_gecos=root,pw_dir=/root
pw_name=nobody,pw_passwd=x,pw_uid=65534,pw_gid=65534,pw_gecos=nobody,pw_dir=/nonexistent
pw_name=test,pw_passwd=x,pw_uid=1001,pw_gid=1001,pw_gecos=,pw_dir=/home/test
pw_name=sshd,pw_passwd=x,pw_uid=127,pw_gid=65534,pw_gecos=,pw_dir=/run/sshd
阴影口令
加密口令是经过单向加密算法处理过的用户口令副本。加密算法有MD5、SHA-1、SHA-256、SHA-512等。
大部分系统将加密口令存放在另一个称为阴影口令的文件中,该文件中至少要包含用户名和加密口令。加密口令通常包含以下字段:
/* A record in the shadow database. */
struct spwd
{
char *sp_namp; /* Login name. */
char *sp_pwdp; /* Hashed passphrase. */
long int sp_lstchg; /* Date of last change. */
long int sp_min; /* Minimum number of days between changes. */
long int sp_max; /* Maximum number of days between changes. */
long int sp_warn; /* Number of days to warn user to change
the password. */
long int sp_inact; /* Number of days the account may be
inactive. */
long int sp_expire; /* Number of days since 1970-01-01 until
account expires. */
unsigned long int sp_flag; /* Reserved. */
};
阴影口令文件不应是普通用户可以读取的。仅有少数几个程序需要访问加密口令,如login
和passwd
,这些程序通常设置用户为root。
有了阴影口令文件之后,普通口令文件/etc/passwd
可由各用户自由读取。
与访问口令文件类似,有一组函数可用于阴影口令文件访问:
#include <shadow.h>
struct spwd* getspnam(const char* name);
struct spwd* getspent(void);
void setspent(void);
void endspent(void);
下面是该函数的使用范例:
#include <shadow.h>
#include <stdio.h>
int main()
{
struct spwd* pSPWD = NULL;
if ((pSPWD = getspnam("blduan")) != NULL)
{
printf("sp_namp=%s, sp_pwdp=%s, sp_lstchg=%ld, sp_min=%ld, sp_max=%ld, "
"sp_warn=%ld, sp_inact=%ld, sp_expire=%ld, sp_flag=%uld\n",
pSPWD->sp_namp, pSPWD->sp_pwdp, pSPWD->sp_lstchg, pSPWD->sp_min,
pSPWD->sp_max, pSPWD->sp_warn, pSPWD->sp_inact, pSPWD->sp_expire,
pSPWD->sp_flag);
}
else
perror("getspnam failed");
setspent();
while ((pSPWD = getspent()) != NULL)
printf("sp_namp=%s, sp_pwdp=%s, sp_lstchg=%ld, sp_min=%ld, sp_max=%ld, "
"sp_warn=%ld, sp_inact=%ld, sp_expire=%ld, sp_flag=%uld\n",
pSPWD->sp_namp, pSPWD->sp_pwdp, pSPWD->sp_lstchg, pSPWD->sp_min,
pSPWD->sp_max, pSPWD->sp_warn, pSPWD->sp_inact, pSPWD->sp_expire,
pSPWD->sp_flag);
endspent();
return 0;
}
执行结果如下:
$ sudo ./shadow_passwd_info
sp_namp=blduan, sp_pwdp=$1$jbKtX7HQ$LIC5WYIkWKu7Li/YXRHAR., sp_lstchg=19035, sp_min=0, sp_max=99999, sp_warn=7, sp_inact=-1, sp_expire=-1, sp_flag=4294967295ld
sp_namp=root, sp_pwdp=!, sp_lstchg=19035, sp_min=0, sp_max=99999, sp_warn=7, sp_inact=-1, sp_expire=-1, sp_flag=4294967295ld
sp_namp=nobody, sp_pwdp=*, sp_lstchg=18858, sp_min=0, sp_max=99999, sp_warn=7, sp_inact=-1, sp_expire=-1, sp_flag=4294967295ld
sp_namp=blduan, sp_pwdp=$1$jbKtX7HQ$LIC5WYIkWKu7Li/YXRHAR., sp_lstchg=19035, sp_min=0, sp_max=99999, sp_warn=7, sp_inact=-1, sp_expire=-1, sp_flag=4294967295ld
sp_namp=test, sp_pwdp=$6$/yhfXl20IXzYXbTI$HmJbYMd1uqXJQOut7W3J/g5h3wM5s8lbo5HJaBsUZ3AOK6/kreFdVFwam7II6Qh0L7BlD0kZOIcSx7TvxIVxL., sp_lstchg=19058, sp_min=0, sp_max=99999, sp_warn=7, sp_inact=-1, sp_expire=-1, sp_flag=4294967295ld
组
Linux用户能够属于两种类型的组,分别是主组(Primary or login group)和附属组(Secondary or supplementary group)。
主组主要用来在用户创建文件时来设置文件所属组的。主组名称通常和用户名一致,并且用户只能属于一个主组。
附属组主要用来给多个用户授予某种权限,用户可以属于0个或多个附属组。
用户的主组信息保存在口令文件/etc/passwd
中,用户的附属组信息保存在组文件/etc/group
中。
groups
命令可以罗列出当前用户的所有组信息。第一个是用户的主组。
Unix组文件主要包含4个字段,这些字段定义在grp.h
头文件中,如下所示:
/* The group structure. */
struct group
{
char *gr_name; /* Group name. */
char *gr_passwd; /* Password. */
__gid_t gr_gid; /* Group ID. */
char **gr_mem; /* Member list. */
};
gr_mem
是一个指针数组,每个指针指向属于该组的用户名。该数组以null指针结尾。
可以通过组名或组ID来获取组信息:
#include <grp.h>
struct group* getgrgid(gid_t gid);
struct group* getgrnam(const char* name);
/* 成功返回指针,失败返回NULL */
上面两个函数和口令文件操作类似,都返回的是静态变量的指针,每次调用都会重写。
遍历整个组文件,可以使用下面3个函数:
#include <grp.h>
struct group* getgrent(void);
void setgrent(void);
void endgrent(void);
setgrent
函数打开组文件,并将读位置置为文件开头。
getgrent
函数从组文件读取下一项,如果未打开,则先打开文件。
endgrent
函数关闭组文件。
下面是获取组信息的实例:
#include <grp.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
struct group* pGroup = NULL;
if ((pGroup = getgrnam("sambashare")) != NULL)
{
printf("gr_name=%s, gr_passwd=%s, gr_gid=%d, gr_mem=", pGroup->gr_name,
pGroup->gr_passwd, pGroup->gr_gid);
int i = 0;
while (pGroup->gr_mem[i] != NULL)
{
printf("%s,", pGroup->gr_mem[i++]);
}
printf("\n");
endgrent();
}
if ((pGroup = getgrgid(getgid())) != NULL)
{
printf("gr_name=%s, gr_passwd=%s, gr_gid=%d, gr_mem=", pGroup->gr_name,
pGroup->gr_passwd, pGroup->gr_gid);
int i = 0;
while (pGroup->gr_mem[i] != NULL)
{
printf("%s,", pGroup->gr_mem[i++]);
}
printf("\n");
endgrent();
}
setgrent();
while ((pGroup = getgrent()) != NULL)
{
printf("gr_name=%s, gr_passwd=%s, gr_gid=%d, gr_mem=", pGroup->gr_name,
pGroup->gr_passwd, pGroup->gr_gid);
int i = 0;
while (pGroup->gr_mem[i] != NULL)
{
printf("%s,", pGroup->gr_mem[i++]);
}
printf("\n");
}
endgrent();
}
执行结果如下所示:(节选部分结果)
$ ./grp_info
gr_name=sambashare, gr_passwd=x, gr_gid=133, gr_mem=blduan,
gr_name=blduan, gr_passwd=x, gr_gid=1000, gr_mem=
gr_name=root, gr_passwd=x, gr_gid=0, gr_mem=
gr_name=blduan, gr_passwd=x, gr_gid=1000, gr_mem=
gr_name=sambashare, gr_passwd=x, gr_gid=133, gr_mem=blduan,
gr_name=systemd-coredump, gr_passwd=x, gr_gid=999, gr_mem=
gr_name=test, gr_passwd=x, gr_gid=1001, gr_mem=
附属组
自从1983年引入附属组的概念之后,文件访问权限检查就被修改为:不仅将进程的有效组ID和文件的组ID相比较,也将所有附属组ID与文件的组ID进行比较。
使用附属组的优点在于不用再显式地经常更改组。
下面几个函数用于获取和设置附属组:
#include <unistd.h>
int getgroups(int gidsize, gid_t grouplist[]);
/* 成功返回附属组ID数量,失败返回-1 */
#include <grp.h>
#include <unistd.h>
int setgroups(int ngroups, const gid_t grouplist[]);
int initgroups(const char* username, gid_t basegid);
/* 成功返回0, 失败返回-1 */
getgroups
函数将进程所属用户的各附属组ID填写到数组grouplist
中,填写个数最多是gidsize
。实际填写到数组中的附属组ID个数由函数返回。如果gidsize
为0,函数只返回附属组ID个数,对grouplist
不做修改。
getgroups
函数相比getgrnam
和getgrgid
函数的区别在于,前者是获取当前进程所属用户的附属组ID,后者是根据组名或组ID获取组自身的信息。
setgroups
函数可有超级用户调用,以便为进程设置附属组ID表。grouplist
是组ID数组,ngroups
指的是组ID个数。ngroups
不能超过NGROUPS_MAX
。
通常只有initgroups
函数调用setgroups
,initgroups
函数使用setgrent, getgrent, endgrent
函数读取整个组文件,然后获取username
的所有附属组,最后调用setgroups
为用户初始化附属组ID表。
由于initgroups
函数要调用setgroups
,所以必须的超级用户权限(除了在组文件在找username
的附属组)。
initgroups
也在附属组ID表中包含了basegid
。basegid
是username
的主组ID,保存在口令文件中。
下面是这几个函数的测试用例:
#include <grp.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("Get supplentary groups size limits is %ld\n",
sysconf(_SC_NGROUPS_MAX));
gid_t groupList[32];
int supplentaryGrpNums = -1;
printf("The effective group id of current proccess is %ld\n", getegid());
/* getgroups函数有时会返回进程的有效组ID */
if ((supplentaryGrpNums =
getgroups(sizeof(groupList) / sizeof(gid_t), groupList)) > 0)
{
printf("Current process owner's supplentary group ids are ");
for (int i = 0; i < supplentaryGrpNums; i++)
{
printf("%ld,", groupList[i]);
}
printf("\n");
}
}
/*
Get supplentary groups size limits is 65536
The effective group id of current proccess is 1000
Current process owner's supplentary group ids are 4,24,27,30,46,120,132,133,1000,
*/
其他数据文件
Unix系统还使用了其他数据文件,例如记录网络服务器提供服务的数据文件/etc/services
,记录协议信息的数据文件/etc/protocols
,记录网络信息的数据文件/etc/networks
。
访问这些数据文件的接口和口令文件、阴影文件以及组文件的接口类似。
一般情况下,对每个数据文件至少有3个函数。
- get函数:读取文件的下一个记录,如果文件没有打开,则会先打开文件。此函数通常返回一个指向结构的指针(结构保存在静态区)。到达文件尾返回空指针。
- set函数:打开相应数据文件,重置文件位置为文件开头。
- end函数:关闭相应数据文件。
除了这3个函数之外,数据文件也会支持某种形式的键搜索。例如口令文件的getpwuid
和getpwnam
等。
说明 | 数据文件 | 头文件 | 结构 | 附加的键搜索函数 |
---|---|---|---|---|
口令 | /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, getaddinfo |
网络 | /etc/networks | <netdb.h> | netent | getnetbyname, getnetbyaddr |
协议 | /etc/protocols | <netdb.h> | protoent | getprotobyname, getprotobynumber |
服务 | /etc/services | <netdb.h> | servent | getservbyname, getservbyport |
登录账户
大多数Unix系统都提供两个数文件:
/var/run/utmp
文件记录当前登录到系统的各个用户。
/var/log/wtmp
文件跟踪各个登录和注销事件。
上面两个文件保存的是多条struct utmp
结构体的记录,是二进制文件,不允许其他用户写。
/* Values for ut_type field, below */
#define EMPTY 0 /* Record does not contain valid info
(formerly known as UT_UNKNOWN on Linux) */
#define RUN_LVL 1 /* Change in system run-level (see
init(8)) */
#define BOOT_TIME 2 /* Time of system boot (in ut_tv) */
#define NEW_TIME 3 /* Time after system clock change
(in ut_tv) */
#define OLD_TIME 4 /* Time before system clock change
(in ut_tv) */
#define INIT_PROCESS 5 /* Process spawned by init(8) */
#define LOGIN_PROCESS 6 /* Session leader process for user login */
#define USER_PROCESS 7 /* Normal process */
#define DEAD_PROCESS 8 /* Terminated process */
#define ACCOUNTING 9 /* Not implemented */
#define UT_LINESIZE 32
#define UT_NAMESIZE 32
#define UT_HOSTSIZE 256
struct exit_status { /* Type for ut_exit, below */
short int e_termination; /* Process termination status */
short int e_exit; /* Process exit status */
};
/* The structure describing an entry in the user accounting database. */
struct utmp {
short ut_type; /* Type of record */
pid_t ut_pid; /* PID of login process */
char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */
char ut_id[4]; /* Terminal name suffix,
or inittab(5) ID */
char ut_user[UT_NAMESIZE]; /* Username */
char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or
kernel version for run-level
messages */
struct exit_status ut_exit; /* Exit status of a process
marked as DEAD_PROCESS; not
used by Linux init (1 */
/* The ut_session and ut_tv fields must be the same size when
compiled 32- and 64-bit. This allows data files and shared
memory to be shared between 32- and 64-bit applications. */
#if __WORDSIZE == 64 && defined __WORDSIZE_COMPAT32
int32_t ut_session; /* Session ID (getsid(2)),
used for windowing */
struct {
int32_t tv_sec; /* Seconds */
int32_t tv_usec; /* Microseconds */
} ut_tv; /* Time entry was made */
#else
long ut_session; /* Session ID */
struct timeval ut_tv; /* Time entry was made */
#endif
int32_t ut_addr_v6[4]; /* Internet address of remote
host; IPv4 address uses
just ut_addr_v6[0] */
char __unused[20]; /* Reserved for future use */
};
登录时,login
进程会生成utmp
结构,并将内容先后写到/var/run/utmp
和/var/log/wtmp
文件中。注销时,init
进程会从/var/run/utmp
文件中删除相应记录,并添加新纪录到/var/log/wtmp
文件中。
who
命令可以读取/var/run/utmp
文件,last
命令可以读取/var/log/wtmp
文件。
下面是一个who命令自实现版本:
系统标识
POSIX.1定义了uname
函数,返回与主机和操作系统相关的信息。
#include <sys/utsname.h>
int uname(struct utsname* name);
/* 成功返回0 失败返回-1 */
函数填写填入的utsname
结构。POSIX.1定义该结构必须的字段,字段的长度有具体实现定义。下面是glibc中的实现:
/* Length of the entries in `struct utsname' is 65. */
#define _UTSNAME_LENGTH 65
#ifndef _UTSNAME_SYSNAME_LENGTH
# define _UTSNAME_SYSNAME_LENGTH _UTSNAME_LENGTH
#endif
#ifndef _UTSNAME_NODENAME_LENGTH
# define _UTSNAME_NODENAME_LENGTH _UTSNAME_LENGTH
#endif
#ifndef _UTSNAME_RELEASE_LENGTH
# define _UTSNAME_RELEASE_LENGTH _UTSNAME_LENGTH
#endif
#ifndef _UTSNAME_VERSION_LENGTH
# define _UTSNAME_VERSION_LENGTH _UTSNAME_LENGTH
#endif
#ifndef _UTSNAME_MACHINE_LENGTH
# define _UTSNAME_MACHINE_LENGTH _UTSNAME_LENGTH
#endif
/* Structure describing the system and machine. */
struct utsname
{
/* Name of the implementation of the operating system. */
char sysname[_UTSNAME_SYSNAME_LENGTH];
/* Name of this node on the network. */
char nodename[_UTSNAME_NODENAME_LENGTH];
/* Current release level of this implementation. */
char release[_UTSNAME_RELEASE_LENGTH];
/* Current version level of this release. */
char version[_UTSNAME_VERSION_LENGTH];
/* Name of the hardware type the system is running on. */
char machine[_UTSNAME_MACHINE_LENGTH];
#if _UTSNAME_DOMAIN_LENGTH - 0
/* Name of the domain of this node on the network. */
# ifdef __USE_GNU
char domainname[_UTSNAME_DOMAIN_LENGTH];
# else
char __domainname[_UTSNAME_DOMAIN_LENGTH];
# endif
#endif
};
其中nodename
用于早期的UUCP网络,并不适用于现在的TCP网络。在使用该结构之前需要使用sysconf
和_SC_VERSION
判断POSIX的版本号。
为了获取网络主机名,可以使用gethostname
函数代替。
#include <unistd.h>
int gethostname(char* name, int namelen);
/*成功返回0 失败返回-1*/
POSIX.1规定最大主机名长度为HOST_NAME_MAX。
下面是uname函数的用法:
#include <stdio.h>
#include <sys/utsname.h>
#include <unistd.h>
int main()
{
if (sysconf(_SC_VERSION) > 200112L)
{
struct utsname utsnameInfo;
if (uname(&utsnameInfo) != 0)
perror("uname error:");
printf("sysname=%s\n", utsnameInfo.sysname);
printf("nodename=%s\n", utsnameInfo.nodename);
printf("release=%s\n", utsnameInfo.release);
printf("version=%s\n", utsnameInfo.version);
printf("machine=%s\n", utsnameInfo.machine);
char hostname[256];
if (gethostname(hostname, sizeof(hostname)) != 0)
perror("gethostname error");
printf("hostname=%s\n", hostname);
}
}
/*
sysname=Linux
nodename=ubuntu
release=5.13.0-39-generic
version=#44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022
machine=x86_64
hostname=ubuntu
*/
时间
秒:铯-133原子在不受干扰的情况下,原子跃迁频率很稳定,因此将其跃迁9 192 631 770个周期所用的时间定义为一秒。
GMT:也称格林威治时间,天文时,GMT。是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。时区概念就指的是以本初子午线为基准的。
UTC:协调世界时,现在世界标准时间。指的是原子时和天文时协调之后的时间,由于地球气候变化、地球自转的影响,会导致天文时比原子时慢,每当两者相差0.9s时,就会将UTC协调时变慢1s,这就是闰秒的由来。具体数值是自1970年1月1日00:00:00以来经过的秒数。
时区:从格林威治本初子午线起,经度每向东或者向西间隔15°,就划分一个时区,在这个区域内,大家使用同样的标准时间。
Unix系统采用UTC而非本地时间作为系统时间,并且将时间和日期作为一个量值来保存。
结构体
保存时间的几个数据结构time_t, timespec, timeval
,具有不同的时间精度,从高到低分别是:
time_t
等同于long int
,因此在32位机器上是32byte,64位机器上是64bits。
time_val
的时间精度为微秒。gettimeofday
和settimeofday
可用来获取该结构。
struct timeval{
time_t tv_sec; // 秒
long tv_usec; // 微秒
}
timespec
的时间精度为纳秒。系统始终获取时间精度高,clock_gettime
会将获取到的时间信息保存在timespec
结构中。
struct timespac{
time_t tv_sec;
long tv_nsec;
}
获取和设置
常规获取时间的函数有:
#include <time.h>
time_t time(time_t* calptr);
/* 成功返回时间值,失败返回-1 */
#include <sys/time.h>
int gettimeofday(struct timeval* tp, void* tzp);
/* 返回值总是0 */
int clock_gettime(clockid_t clock_id, struct timespec *tsp);
int clock_getres(clockid_t clock_id, struct timespec *tsp);
int clock_settime(clockid_t clock_id, const struct timespec *tsp);
/* 成功返回0, 失败返回-1 */
上面的函数分别获取时间精度从低到高的时间值。
其中clock_id
的取值主要有CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID
。
clock_gettime
函数用于获取指定时钟的时间,返回的时间保存在timespec
结构体中。clock_getres
获取时间精度,默认为1纳秒。
gettimeofday
函数以距1970年1月1日00:00:00的秒数将当前时间存放在tp
结构体中。其中tsp
参数总是NULL。(建议不要使用该函数,无法通过返回值判断函数执行结果)。
下面是三中函数的使用范例:
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
int main()
{
printf("sizeof(time_t)=%lu\n", sizeof(time_t));
time_t tNow = time(NULL);
printf("tNow=%lu\n", tNow);
/* struct timeval tVal; */
struct timespec timeSP;
clock_gettime(CLOCK_REALTIME, &timeSP);
printf("tv_sec=%lu, tv_nsec=%ld\n", timeSP.tv_sec, timeSP.tv_nsec);
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &timeSP);
printf("tv_sec=%lu, tv_nsec=%ld\n", timeSP.tv_sec, timeSP.tv_nsec);
struct timespec timeRes;
clock_getres(CLOCK_REALTIME, &timeRes);
printf("tv_sec=%lu, tv_nsec=%ld\n", timeRes.tv_sec, timeRes.tv_nsec);
struct timeval timeV;
gettimeofday(&timeV, NULL);
printf("tv_sec=%lu, tv_usec=%ld\n", timeV.tv_sec, timeV.tv_usec);
}
执行结果如下:
$ ./time_info
sizeof(time_t)=8
tNow=1650464876
tv_sec=1650464876, tv_nsec=715839889
tv_sec=0, tv_nsec=750579
tv_sec=0, tv_nsec=1
tv_sec=1650464876, tv_usec=715847
时区和格式化
时间除了上面的采用3中结构体表示经过的秒数之外,还有经过分解的时间结构如下所示:
#include <time.h>
struct tm{
int tm_sec; /* 0-60 瑞秒*/
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon
int tm_year; /* 当前年份-1900 */
int tm_wday; /* 每周的日期,取值0-6 */
int tm_yday; /* 0-365 */
int tm_isdst;
}
char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
time_t mktime(struct tm *tm);
size_t strftime(char *s, size_t max, const char *format,
const struct tm *tm);
asctime
函数将tm
指向的时间结构体转换为字符串格式的时间。时区和tm
表示的值对应。
ctime
函数将time_t
表示的秒数转换为字符串形式的本地时间,和系统时区有关。
gmtime
函数将time_t
表示的秒数转换为tm
格式的UTC时间,时区默认为0.
localtime
函数将time_t
表示的秒数转换为tm
格式的本地时间。
mktime
以本地时间的tm
结构形式转换为time_t
值。
strftime
函数将tm
结构体表示的时间转为format
指定的格式。
格式说明如下:
格式 | 说明 | 示例 |
---|---|---|
%F | YYYY-MM-DD | 2022-04-23 |
%T | %H:%M:%S | 10:57:23 |
下面是这些函数的使用示例:
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
int main()
{
time_t tNow;
time(&tNow);
printf("%lu\n", tNow);
struct tm gmt;
struct tm lt;
gmtime_r(&tNow, &gmt);
localtime_r(&tNow, <);
printf("%d--%d\n", gmt.tm_hour, lt.tm_hour);
char buf[256] = {0};
asctime_r(&gmt, buf);
printf("asctime gmt=%s\n", buf);
asctime_r(<, buf);
printf("asctime lt=%s\n", buf);
ctime_r(&tNow, buf);
printf("ctime=%s\n", buf);
time_t tVal = mktime(<);
printf("%lu\n", tVal);
strftime(buf, sizeof(buf), "%F %T", <);
printf("%s\n", buf);
}
执行结果如下所示:
$ ./time_format
1650682356
2--10
asctime gmt=Sat Apr 23 02:52:36 2022
asctime lt=Sat Apr 23 10:52:36 2022
ctime=Sat Apr 23 10:52:36 2022
1650682356
2022-04-23 10:58:34
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/%E7%B3%BB%E7%BB%9F%E6%95%B0%E6%8D%AE%E6%96%87%E4%BB%B6%E5%92%8C%E4%BF%A1%E6%81%AF/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。