口令文件/etc/passwd和组文件/etc/group经常被多个进程频繁使用。用户每次登录Linux和使用ls命令都会访问口令文件。

除了直接访问文件之外,系统通过一些接口来对外提供信息,比如系统标识函数、时间和日期函数。

口令

口令文件

Unix口令文件/etc/passwd会包含下图所示的各字段,这些字段定义包含在pwd.h中定义的passwd结构体中。

说明struct passwd 成员POSIX.1
用户名char *pw_name*
加密口令char *pw_passwd
用户IDuid_t pw_uid*
组IDgid_t pw_gid*
注释字段char *pw_gecos
初始工作目录char *pw_dir*
初始shellchar *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中。
  • 口令文件项中某些字段可能为空,testsshd用户的注释字段为空。
  • 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则关闭口令文件。

getpwuidgetpwnam函数调用完之后也不应该使文件处于打开状态,应调用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.  */
  };

阴影口令文件不应是普通用户可以读取的。仅有少数几个程序需要访问加密口令,如loginpasswd,这些程序通常设置用户为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函数相比getgrnamgetgrgid函数的区别在于,前者是获取当前进程所属用户的附属组ID,后者是根据组名或组ID获取组自身的信息。

setgroups函数可有超级用户调用,以便为进程设置附属组ID表。grouplist是组ID数组,ngroups指的是组ID个数。ngroups不能超过NGROUPS_MAX

通常只有initgroups函数调用setgroupsinitgroups函数使用setgrent, getgrent, endgrent函数读取整个组文件,然后获取username的所有附属组,最后调用setgroups为用户初始化附属组ID表。

由于initgroups函数要调用setgroups,所以必须的超级用户权限(除了在组文件在找username的附属组)。

initgroups也在附属组ID表中包含了basegidbasegidusername的主组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个函数之外,数据文件也会支持某种形式的键搜索。例如口令文件的getpwuidgetpwnam等。

说明数据文件头文件结构附加的键搜索函数
口令/etc/passwd<pwd.h>passwdgetpwnam, getpwuid
/etc/group<grp.h>groupgetgrnam, getgrgid
阴影/etc/shadow<shadow.h>spwdgetspnam
主机/etc/hosts<netdb.h>hostentgetnameinfo, getaddinfo
网络/etc/networks<netdb.h>netentgetnetbyname, getnetbyaddr
协议/etc/protocols<netdb.h>protoentgetprotobyname, getprotobynumber
服务/etc/services<netdb.h>serventgetservbyname, 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的时间精度为微秒。gettimeofdaysettimeofday可用来获取该结构。

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指定的格式。

格式说明如下:

格式说明示例
%FYYYY-MM-DD2022-04-23
%T%H:%M:%S10: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, &lt);

    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(&lt, buf);
    printf("asctime lt=%s\n", buf);
    ctime_r(&tNow, buf);
    printf("ctime=%s\n", buf);

    time_t tVal = mktime(&lt);
    printf("%lu\n", tVal);

    strftime(buf, sizeof(buf), "%F %T", &lt);
    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