我是靠谱客的博主 失眠早晨,最近开发中收集的这篇文章主要介绍10. Linux的时间10.1 日历时间10.2 时间转换函数10.3 Timezones10.4 地区Locales10.5 更新系统时钟10.6 软件时钟(Jiffies)10.7 进程时间,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

在一个程序之内,我们可能对两类时间会很感兴趣:

  • 真实时间Real time:这是一个要么从某个标准点开始测量的时间(日历时间)或者从某一个固定点(进程起始)开始测量的process已经过的时间。获得日历时间对于程序来说非常有用,比如说,对于时间戳数据记录或者文件。对于程序来说测量已经过的时间对于那些采取周期性动作或者在外部输入设备上做一定的时间测量非常有用。
  • 进程时间Process time:被进程所用的CPU时间。测量进程时间对于检查或者优化程序或者算法的表现非常有用。

大多数计算机架构都有一个自己的内建硬件时钟允许内核测量真实和进程时间。这一章,我们会看几个处理这两种类型时间的系统调用还有库函数来在人类可理解的和内部表示的时间之间做转换。因为人类可读的时间形式是依赖于地理位置还有语言文化传统,当讨论这些形式的时候就会引导我们进入时区和位置信息。

10.1 日历时间

不考虑地理位置,UNIX系统内部时间是从Epoch开始以秒计时。Epoch时间也就是1970年1月1日午夜,UTC时间。这个时间是UNIX系统开始工作的时候。日历时间被存储在time_t变量当中,SUSv3中规定是一个整型。

小提示:如果是在使用Linux32位系统的话,那么它的time_t是一个带符号的整型数字,它只能用来表示1901年12月31日20:45:52到2038年1月19日 03:14:07这个时间区间的时间。因此,有很多32位的UNIX系统都会面临到一个2038年问题,但如果他们在2038年之前也许计算一些日期晚于2038年的话,那么它们将会提前遇到这样的问题。但是到2038年的时候也许大多数Linux系统就已经是64位的了。

使用gettimeofday()系统调用就可以得到被bv指针指向的日历时间。

#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
//Returns 0 on success, or –1 on error

而这个timeval是一个结构体类型,它的样子如下:

struct timeval {
 time_t tv_sec; /* Seconds since 00:00:00, 1 Jan 1970 UTC */
 suseconds_t tv_usec; /* Additional microseconds (long int) */
};

虽然tv_usec是一个单位是微秒us的数据,但是它的精确度是由架构所决定的。在现代x86-32系统上,gettimeofday()确实可以提供微秒级准确度信息。

tz参数之于ggettimeofday()只是一个历史遗迹。在早期UNIX实现中,它被用于获取系统上的时区信息。现在这个参数已经不再有用而且一般来讲需要设置成NULL。

time()系统调用返回从Epoch开始所经过的秒数,其实也就是gettimeofday()中返回的tv数组中的tv_sec。

#include <time.h>
time_t time(time_t *timep);
//Returns number of seconds since the Epoch,or (time_t) –1 on error

如果timep参数不是NULL的话,那么从Epoch开始的秒数也就同样会被放在timep所指向的指针变量中。

尽管我们有两种获得该时间的可能性,但是考虑到如果给定的指针是个无效指针的话,这时候就会返回一个错误。为了能够简单安全使用,最好的办法还是设置timep为NULL。

t = time(NULL);

10.2 时间转换函数

下午列出了在time_t时间和别的时间形式之间的转换。这些函数帮助我们解决时区,地理等等复杂问题。

 10.2.1 转换time_t到可读形式

ctime()函数提供了一个简单的方式来转换time_t数据到可读形式上:

#include <time.h>
char *ctime(const time_t *timep);
//Returns pointer to statically allocated string terminated
//by newline and  on success, or NULL on error

给定一个指向time_t值的指针timep,ctime()就会返回一个26字节长的字符串包含了标准化形式的日期和时间,就像这样--Wed Jun 8 14:22:34 2011

该字符串包含了"n"。ctime()函数会在做转换的时候自动考虑上地区时区因素。所返回的字符串也是静态分配的,所以未来对ctime()的调用就会改写该指针所指向的内容。

SUSv3当中任何对ctime(),gmtime(),localtime()或者asctime()的调用都会改写该静态分配的结构。换句话说,这些函数可能都使用者同一个字符串数组以及tm结构,不过这个取决于glibc的版本。如果我们想要保存所收到的信息而不被改写,最好的办法就是将他们在本地保存。

10.2.2 转换time_t和分解时间broken down time

gmtime()和localtime()函数转换一个time_t的值到所谓的broken-down time上。分解时间broken down time是一个放置在静态结构中的,会被以指针形式返回的一个结构体变量。

#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
//Both return a pointer to a statically allocated broken-down
//time structure on success, or NULL on error

gmtime()转换日历时间到UTC所对应分解时间上(gm这里的意思就代表这格林威治时间)。相反,localtime()会考虑时区和DST设置并返回相应当地的分解时间。Reentrant版本分别是gmtime_r()和localtime_r()。

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 since 1900 */
 int tm_wday; /* Day of the week (Sunday = 0)*/
 int tm_yday; /* Day in the year (0-365; 1 Jan = 0)*/
 int tm_isdst; /* Daylight saving time flag
 > 0: DST is in effect;
 = 0: DST is not effect;
 < 0: DST information not available */
};

tm_sec域可以上到60s而不是仅仅59秒,这是因为这里可能会考虑到闰秒的使用。

如果_BSD_SOURCE被定义的话,那么glibc所定义的tm结构就还会多两个域:

  1. long int tm_gmtoff 包含了和UTC时间之间秒的差值
  2. const char *tm_zone,也就是时区的简写

这些信息并不是SUSv3标准,而且也只在少量UNIX中实现。

mktime()则可以帮助从broken-down时间,也就是local time上转换到time_t值上。调用者将指向分解时间函数结构体的指针发送给mktime,则就可以在返回值里得到对应的time_t值。在这个转换当中,tm_wday和tm_yday会被忽略。

#include <time.h>
time_t mktime(struct tm *timeptr);
//Returns seconds since the Epoch corresponding to timeptr
//on success, or (time_t) –1 on error

mktime()函数也可以修改timeptr指向的结构体。至少来说,它可以确保tm_wday和tm_yday域会被设置到与其它对应输入数据相对应的正确的值上面。

另外mktime()如果发现有一些数据超出范围,它会根据其他信息去改写和调整。

比如,如果输入的tm_sec是123,之后它就会被改写成3,但是同时tm_min会被加2以保证绝对时间没有被改变。当然这个也可以用于当tm_sec为负数的情况。

  • 如果tm_isdst是0,那么这个时间就是标准时间
  • 如果tm_isdst大于0,那么这个时间就是DST
  • 如果tm_isdst小于0,就会尝试确定是否DST会影响该年的时间。

10.2.3 转换分解时间和可读时间

转换分解时间到可读时间

给定一个指向分解时间结构体的指针,asctime()返回一个指向静态分配字符串,其中包含了与ctime()调用返回相同的时间形式。

#include <time.h>
char *asctime(const struct tm *timeptr);
//Returns pointer to statically allocated string terminated by
//newline and  on success, or NULL on error

和ctime()对比,对于asctime()来说本地时区设置并没有什么影响,因为它只是转换一个分解时间,而这个分解时间本身已经是包含时区信息了。

当然我们对asctime()返回的形式没有控制权。可再入的asctime()版本是asctime_r()。

下面的例子给出了asctime()的使用方法,还有所有的这章里面会涉及到的时间转换函数。程序去除现在的日历时间,然后将使用不同的时间转换函数再将他们的结果展示出来。下面是一个运行在慕尼黑的程序的例子,它使用中欧时间CET,早于UTC一个小时。

time/calendar_time.c (from "The Linux Programming Interface")

#include <locale.h>
#include <time.h>
#include <sys/time.h>
#include "tlpi_hdr.h"

#define SECONDS_IN_TROPICAL_YEAR (365.24219 * 24 * 60 * 60)
int
main(int argc, char *argv[])
{
    time_t t;
    struct tm *gmp, *locp;
    struct tm gm, loc;
    struct timeval tv;

    t = time(NULL);
    printf("Seconds since the Epoch (1 Jan 1970): %ld", (long) t);
    printf(" (about %6.3f years)n", t / SECONDS_IN_TROPICAL_YEAR);

    if (gettimeofday(&tv, NULL) == -1)
        errExit("gettimeofday");
    printf("  gettimeofday() returned %ld secs, %ld microsecsn",
            (long) tv.tv_sec, (long) tv.tv_usec);

    gmp = gmtime(&t);
    if (gmp == NULL)
        errExit("gmtime");

    gm = *gmp;          /* Save local copy, since *gmp may be modified
                           by asctime() or gmtime() */
    printf("Broken down by gmtime():n");
    printf("  year=%d mon=%d mday=%d hour=%d min=%d sec=%d ", gm.tm_year,
            gm.tm_mon, gm.tm_mday, gm.tm_hour, gm.tm_min, gm.tm_sec);
    printf("wday=%d yday=%d isdst=%dn", gm.tm_wday, gm.tm_yday, gm.tm_isdst);

    locp = localtime(&t);
    if (locp == NULL)
        errExit("localtime");

    loc = *locp;        /* Save local copy */

    printf("Broken down by localtime():n");
    printf("  year=%d mon=%d mday=%d hour=%d min=%d sec=%d ",
            loc.tm_year, loc.tm_mon, loc.tm_mday,
            loc.tm_hour, loc.tm_min, loc.tm_sec);
    printf("wday=%d yday=%d isdst=%dnn",
            loc.tm_wday, loc.tm_yday, loc.tm_isdst);

    printf("asctime() formats the gmtime() value as: %s", asctime(&gm));
    printf("ctime() formats the time() value as:     %s", ctime(&t));

    printf("mktime() of gmtime() value:    %ld secsn", (long) mktime(&gm));
    printf("mktime() of localtime() value: %ld secsn", (long) mktime(&loc));

    exit(EXIT_SUCCESS);
}

测试的结果

pi@raspberrypi:~/sysprog/learn_tlpi/build $ date
Mon 15 Aug 2022 11:06:07 AM CEST
pi@raspberrypi:~/sysprog/learn_tlpi/build $ ./out
Seconds since the Epoch (1 Jan 1970): 1660554369 (about 52.621 years)
  gettimeofday() returned 1660554369 secs, 907771 microsecs
Broken down by gmtime():
  year=122 mon=7 mday=15 hour=9 min=6 sec=9 wday=1 yday=226 isdst=0
Broken down by localtime():
  year=122 mon=7 mday=15 hour=11 min=6 sec=9 wday=1 yday=226 isdst=1

asctime() formats the gmtime() value as: Mon Aug 15 09:06:09 2022
ctime() formats the time() value as:     Mon Aug 15 11:06:09 2022
mktime() of gmtime() value:    1660550769 secs
mktime() of localtime() value: 1660554369 secs

这里面比较有意思的需要稍微转换下思维的就是分解时间。其中year根据提示,是从1900年开始计算,所以2022年对应着的就是year = 122。现在是8月,但是mon显示7,因为它是从0计数到11,也就意味着7 = 8月。

strftime()函数则提供者一些更精确的控制办法当我们转换一个分解时间到可读时间的时候。给定一个timeptr指针指向分解时间结构体,strftime()就会再outstr中放置相应的以""结尾中间内容为date+time的字符串。

#include <time.h>
size_t strftime(char *outstr, size_t maxsize, const char *format,
 const struct tm *timeptr);
//Returns number of bytes placed in outstr (excluding
//terminating null byte) on success, or 0 on error

该字符串outstr输出以format所决定的内容形式。maxsize表明outstr中所预留的最大空间。不同于ctime()和asctime()输出,strftime()不包含"n"在末尾。

再成功调用之后,strftime()会返回outstr实际被写入数据的字节数,当然这里不包含""在内。如果返回数据的总长度超过给定maxsize的话,那么它会返回一个错误,并用0表示。

format我们后面还会碰到很多,到时候再做讲解。

#include "curr_time.h"
char *currTime(const char *format);
//Returns pointer to statically allocated string, or NULL on error

上面是一个书里提供的一个利用strftime()来输出现在时间的例程。

 例程:
time/curr_time.c (from "The Linux Programming Interface")

#include <time.h>
#include "curr_time.h"          /* Declares function defined here */

#define BUF_SIZE 1000
/* Return a string containing the current time formatted according to
   the specification in 'format' (see strftime(3) for specifiers).
   If 'format' is NULL, we use "%c" as a specifier (which gives the'
   date and time as for ctime(3), but without the trailing newline).
   Returns NULL on error. */

char *
currTime(const char *format)
{
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}

time/curr_time.h (from "The Linux Programming Interface")

/* curr_time.h

   Header file for curr_time.c.
*/
#ifndef CURR_TIME_H
#define CURR_TIME_H             /* Prevent accidental double inclusion */

char *currTime(const char *fmt);

#endif

转换可读形式到分解时间

strptime()是strftime()的反形式,它转换date+time到可分解时间形式上。

#define _XOPEN_SOURCE
#include <time.h>
char *strptime(const char *str, const char *format, struct tm *timeptr);
//Returns pointer to next unprocessed character in
//str on success, or NULL on error

strptime()函数使用了format形式来解析date+time字符串(从str字符串里),并且将被转换的分解时间放在timeptr所指向的结构体中。

一旦成功,strptime()就会返回一个指向下一个未被处理的str字符指针(当字符串里包含更多需要处理的信息的时候这是非常有用的)。如果完整的format字符串没有被匹配的话,strptime()会返回NULL来表示错误。

format所给定的形式与scanf一致。它包含了以下字符类型:

  • 转换以%开始
  • 空白字符
  • 非空白字符

转换的方式和strftime()类似。不同点在于这里的转换形式会更自由一些,比如%a和%A在这里没有区别,%d和%e也可以用于读取一个月的日期,他们可以是以0也可以不是以0开始的数字。另外,May,MAY等价。%%用于匹配输入字符串的里的百分号。

下面的例子给出了一些对strptime()和strftime()的使用。该程序可以从命令行参数当中取得日期和时间,并将它们转换到分解时间上并展示,然后再用反作用函数strftime()转换回去。该程序会有三个参数,前两个是必输入。第一个参数是一个包含了日期和时间的字符串。第二个参数是用于再strptime()中解析第一个参数用的format。第三个参数是可选参数是用于反向转换strftime()的。如果忽略这个参数的话,那么一个缺省转换形式将会被使用。

程序:

time/strtime.c (from "The Linux Programming Interface")

/* strtime.c

   Demonstrate the use of strptime() and strftime().

   Calls strptime() using the given "format" to process the "input-date+time".
   The conversion is then reversed by calling strftime() with the given
   "out-format" (or a default format if this argument is omitted).
*/
#if ! defined(__sun)
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE
#endif
#endif
#include <time.h>
#include <locale.h>
#include "tlpi_hdr.h"

#define SBUF_SIZE 1000
int
main(int argc, char *argv[])
{
    struct tm tm;
    char sbuf[SBUF_SIZE];
    char *ofmt;

    if (argc < 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s input-date-time in-format [out-format]n", argv[0]);

    if (setlocale(LC_ALL, "") == NULL)
        errExit("setlocale");   /* Use locale settings in conversions */

    memset(&tm, 0, sizeof(struct tm));          /* Initialize 'tm' */
    if (strptime(argv[1], argv[2], &tm) == NULL)
        fatal("strptime");

    tm.tm_isdst = -1;           /* Not set by strptime(); tells mktime()
                                   to determine if DST is in effect */
    printf("calendar time (seconds since Epoch): %ldn", (long) mktime(&tm));

    ofmt = (argc > 3) ? argv[3] : "%H:%M:%S %A, %d %B %Y %Z";
    if (strftime(sbuf, SBUF_SIZE, ofmt, &tm) == 0)
        fatal("strftime returned 0");
    printf("strftime() yields: %sn", sbuf);

    exit(EXIT_SUCCESS);
}

其中setlocale会在10.4章中被讲解。

我们完全仿照书上做了一个例子:

第一个是忽略第三个参数

第二个是带上第三个参数

pi@raspberrypi:~/sysprog/learn_tlpi/build $ ./out  "9:39:46pm 1 Feb 2011" "%I:%M:%S%p %d %b %Y"
calendar time (seconds since Epoch): 1296592786
strftime() yields: 21:39:46 Tuesday, 01 February 2011 CET
pi@raspberrypi:~/sysprog/learn_tlpi/build $ ./out "9:39:46pm 1 Feb 2011" "%I:%M:%S%p %d %b %Y" "%F %T"
calendar time (seconds since Epoch): 1296592786
strftime() yields: 2011-02-01 21:39:46

10.3 Timezones

不同的国家会使用不同的时区和DST域。程序在输入输出时间的时候需要考虑上时区还有系统的DST域。幸运的是这些细节都在C语言库里被处理。

时区定义

时区信息通常是大量的而且容易改变的。处于这个原因,相比于直接将它直接编程的程序或者库里,系统更喜欢将这样的信息维持在一个标准文件当中,也就是在/usr/share/zoneinfo文件夹当中。在这个文件夹当中的每一个文件都包含着在特定国家,特定地区的相关时区信息。这些文件根据所描述写的时区信息被命名,所以我们可以看到一些名字为EST, CET,UTC,Turkey,Iran等等信息。下面是在树莓派里所找到的所有文件:

pi@raspberrypi:/usr/share/zoneinfo $ ls
Africa      Cuba     GMT+0        Japan              Pacific     Turkey
America     EET      GMT-0        Kwajalein          Poland      UCT
Antarctica  Egypt    GMT0         leap-seconds.list  Portugal    Universal
Arctic      Eire     Greenwich    Libya              posix       US
Asia        EST      Hongkong     localtime          posixrules  UTC
Atlantic    EST5EDT  HST          MET                PRC         WET
Australia   Etc      Iceland      Mexico             PST8PDT     W-SU
Brazil      Europe   Indian       MST                right       zone1970.tab
Canada      Factory  Iran         MST7MDT            ROC         zone.tab
CET         GB       iso3166.tab  Navajo             ROK         Zulu
Chile       GB-Eire  Israel       NZ                 Singapore
CST6CDT     GMT      Jamaica      NZ-CHAT            SystemV

为程序确定时区

在运行程序的时候很多时候我们需要确定它相关的时区,所以我们就可以通过设置TZ环境变量来完成这样的事情。对时区的设置会自动影响ctime(),localtime(),mktime()还有strftime()。

为了获得现在时区的设置,每一个这样的函数都是用了tzset(3)来初始化这三个变量:

char *tzname[2]; /* Name of timezone and alternate (DST) timezone */
int daylight; /* Nonzero if there is an alternate (DST) timezone */
long timezone; /* Seconds difference between UTC and local
 standard time */

tzset()函数首先检查TZ环境变量,如果变量没有被设置的话,那么时区就会被初始化为/etc/localtime里的默认时区。如果TZ环境变量是一个不匹配在任何时区文件上的值或者是空字符串的话,那么就使用UTC。TZDIR环境变量(一个非标准GNU扩展)可以被设置成一个文件夹名,这样子程序就会在这个被定义的文件夹中寻找时区文件而不是在标准的/usr/share/zoneinfo当中寻找。

我们可以通过运行下面的程序来观察TZ变量的效果。第一次运行当中,我们可以看到相应于系统默认时区相应的输出。第二次运行我们可以设置时区到新西兰,这样子我们就会早于CET时间12个小时。

例子:time/show_time.c (from "The Linux Programming Interface")

#include <time.h>
#include <locale.h>
#include "tlpi_hdr.h"

#define BUF_SIZE 200
int
main(int argc, char *argv[])
{
    time_t t;
    struct tm *loc;
    char buf[BUF_SIZE];

    if (setlocale(LC_ALL, "") == NULL)
        errExit("setlocale");   /* Use locale settings in conversions */

    t = time(NULL);

    printf("ctime() of time() value is:  %s", ctime(&t));

    loc = localtime(&t);
    if (loc == NULL)
        errExit("localtime");

    printf("asctime() of local time is:  %s", asctime(loc));

    if (strftime(buf, BUF_SIZE, "%A, %d %B %Y, %H:%M:%S %Z", loc) == 0)
        fatal("strftime returned 0");
    printf("strftime() of local time is: %sn", buf);

    exit(EXIT_SUCCESS);
}

在Pacific文件夹中有这些国家和地区可以选择

pi@raspberrypi:/usr/share/zoneinfo/Pacific $ ls
Apia          Enderbury    Guam        Marquesas  Palau         Samoa      Yap
Auckland      Fakaofo      Honolulu    Midway     Pitcairn      Tahiti
Bougainville  Fiji         Johnston    Nauru      Pohnpei       Tarawa
Chatham       Funafuti     Kiritimati  Niue       Ponape        Tongatapu
Chuuk         Galapagos    Kosrae      Norfolk    Port_Moresby  Truk
Easter        Gambier      Kwajalein   Noumea     Rarotonga     Wake
Efate         Guadalcanal  Majuro      Pago_Pago  Saipan        Wallis

 两次运行所得到的结果,第一次默认CET时间,第二次设置为NZST时间,并且TZ只是临时有效

pi@raspberrypi:~/sysprog/learn_tlpi/build $ ./out 
ctime() of time() value is:  Mon Aug 15 18:14:53 2022
asctime() of local time is:  Mon Aug 15 18:14:53 2022
strftime() of local time is: Monday, 15 August 2022, 18:14:53 CEST
pi@raspberrypi:~/sysprog/learn_tlpi/build $ TZ=":Pacific/Auckland" ./out
ctime() of time() value is:  Tue Aug 16 04:15:41 2022
asctime() of local time is:  Tue Aug 16 04:15:41 2022
strftime() of local time is: Tuesday, 16 August 2022, 04:15:41 NZST

至于对TZ的定义,除了上面的这种 TZ=":Pacific/Auckland" 的形式以外,还有一种方式可以使用,它也是完全符合SUSv3的要求,其中[]所包含的内容是可选内容。

std offset [ dst [ offset ][ , start-date [ /time ] , end-date [ /time ]]]

其中日期可以有不同的形式来表示比如Mm.n.d->其中d日期(0=周日,6=周六),n星期(1->5,5代表最后一周),月(1->12)。如果忽略/time的话,它会自动默认02:00:00(2AM)。

下面是一个给CET定义TZ的例子,平时它比UTC早一小时,并且中间涵盖夏令时,从三月的最后一个周日到十月的最后一个周日,比UTC早两小时,这样子我们要写成:

TZ="CET-1:00:00CEST-2:00:00,M3.5.0,M10.5.0"
     ^     ^    ^     ^        ^       ^
     |     |    |     |        |       |
    std offset dst  offset start-date  end-date  

这里std就是CET普通中欧时间,CEST就是dst,中欧夏令时。

当然了它的替换版本更好理解的就是

TZ=":Europe/Berlin"

10.4 地区Locales

全世界有好几千种语言,但只有一部分被使用在计算机系统上面。另外,不同的国家使用不同的方式来显示相同的信息,比如数字,货币,日期,时间。比如说,在大多数欧洲国家,小数点使用的是","而非"."。而且大多数国家对日期时间的书写也与美国完全不同。SUSv3则定义locale作为依赖于语言和文化传统的用户环境子集。

理想上来说,所有设计用于运行超过一个地区的程序都应该能够根据用户喜好的语言和形式输出内容,这也就是所谓的国际化internationalization。理想上来说,我们应该只需要写一次程序,无论他在哪里运行,他都应该能够在输入输出的时候做正确的事情,也就是他会做本地化的事情。国际化程序是一件耗时的工作,尽管在已经有很多工具帮助的前提条件之下。程序库比如glibc也提供了一定的帮助可以做到国际化。

地区定义

就像时区信息一样,地区信息倾向于是量大而且容易改变。出于这样的原因,地区信息也会被一般以标准形式放在系统信息文件当中。

一般地区信息维持在/usr/share/locale当中。每一个子文件夹都包含着一些特定的地区信息。这些文件夹以以下方式命名:

language[_territory[.codeset]][@modifier]

language是由两个字母所组成的ISO标准语言代码,比如CN-中文,territory是两字母所组成的国家代码,codeset是字符编码集,modifier提供了区分多个地区文件夹,他们有同样的语言领土以及字符集。举例 de_DE.utf-8@euro,它代表着德语,德国(瑞士,卢森堡,列支敦士登,奥地利德语均是官方语言),UTF-8字符编码集,采用欧元做为货币。

当我们确定了在程序中要使用的地区之后,我们事实上就会确认/usr/share/locale当中的一个子文件夹的名字。如果没有子文件夹的名字匹配程序中所需要的,那么C库就会从所给定的地区信息中按以下信息挨个查找匹配内容:1,字符集 2,标准字符集 3,领土 4, modifier

标准字符集是一个去除了所有非字母字符的字符集版本,并且所有的字母都被转换成小写,并且所有返回的字符串都带有iso。

以下是截取的树莓派中的部分/usr/share/locale中的子文件夹。这里并没有书上写的完整版,比如德语只分成de和de_CH(瑞士德语差别稍微大一些),中文分成四种。

 zh_CN
bn_IN     en_NZ        hr     kw_GB     nso           szl       zh_Hant
bo        en@quot      ht     kw@kkcor  nv            ta        zh_HK
br        en@shaw      hu     kw@uccor  oc            te        zh_TW

举例一个剥析的过程,如果程序的地区被定义成fr_CH.utf-8,但是没有找到同样的名字的文件夹的时候,那么fr_CH会首先被用于匹配,如果fr_CH也不存在的话,就会查找fr。如果很不幸fr也没有的话,那么setlocale()就会返回一个错误。

在每一个子文件夹当中都有一个描述该地区相对应传统的标准化文件(具体内容可以看下表),其中有两点需要注意:

  • LC_COLLATE文件定义了一套标准描述了字符集中的字符的排序。这套标准据定了strcoll(3)和strxfrm(3)的行为。即使使用拉丁基础的语言也并不会遵循同样的顺序。比如一些欧洲语言有一些额外字符,他们会被列在Z之后,但是对于西班牙语中的一个特殊字符ll,则是直接列在l之后。
  • LC_MESSAGES是一走向国际化显示信息的一步。越来越多的扩展国际化程序信息可以要么使用信息分类要么使用GNU gettext接口来完成。

 为程序设定地区

setlocale()可以用来设定和查询程序当前的地区。

#include <locale.h>
char *setlocale(int category, const char *locale);
//Returns pointer to a (usually statically allocated) string identifying
//the new or current locale on success, or NULL on error

category参数用来选择设置地区的哪一部分进行设置或者查询,也就是上表中所列出来的LC_CTYPE...LC_MESSAGES。也就是说我们是有可能设置成德国的时间但是货币是美元。另外,更平常的是,如果我们使用LC_ALL的话,那么就会对地区的所有设置都采用统一的一个地区。

一般有两种方式用setlocale来设置地区。要么让locale采用任何一个定义在系统当中的字符串(在usr/share/locale),比如de_DE或者en_US,或者locale参数可以为空字符串,意味着地区设置可以从环境变量中获取:

setlocale(LC_ALL, "");

这种情况下,这个调用就是让程序可以从环境变量中获取地区信息。如果没有调用过这个的话,那么环境变量就不会对程序有影响。

当运行一个带setlocale(LC_ALL,"");的程序时,我们可以使用环境变量来控制地区的方方面面特性。另外,我们也可以使用LC_ALL或者LANG环境变量来设置整个地区的设置。所以我们也可以用LANG来给所有分类设置默认地区信息然后使用独立的LC_*变量来设置一些特殊的有别于默认的信息。

最后setlocale()返回一个指向定义了该类别的地区设置的字符串。如果我们仅对发现目前地区设定感兴趣,而不想去修改的话,我们著需要设置locale为NULL。

地区设置对于GNU/Linux中的应用有广泛的影响,尤其是对大多数glibc的函数来说。比如strftime()还有strptime()。如果在调用的过程中设置LANG,就会对执行的程序产生直接的影响,比如:

$ LANG=de_DE ./show_time German locale
ctime() of time() value is: Tue Feb 1 12:23:39 2011
asctime() of local time is: Tue Feb 1 12:23:39 2011
strftime() of local time is: Dienstag, 01 Februar 2011, 12:23:39 CET

$ LANG=de_DE LC_TIME=it_IT ./show_time German and Italian locales
ctime() of time() value is: Tue Feb 1 12:24:03 2011
asctime() of local time is: Tue Feb 1 12:24:03 2011
strftime() of local time is: martedì, 01 febbraio 2011, 12:24:03 CET

$ LC_ALL=fr_FR LC_TIME=en_US ./show_time French and US locales
ctime() of time() value is: Tue Feb 1 12:25:38 2011
asctime() of local time is: Tue Feb 1 12:25:38 2011
strftime() of local time is: mardi, 01 février 2011, 12:25:38 CET

10.5 更新系统时钟

这一部分我们了解两个新的接口来更新系统中时钟:settimeofday()还有adjtime()。这些接口很少用于应用程序(因为系统时间一般都是由比如像Network Time Protocol daemon所维护),而且改变时间也需要特权。

#define _BSD_SOURCE
#include <sys/time.h>
int settimeofday(const struct timeval *tv, const struct timezone *tz);
//Returns 0 on success, or –1 on error
#define _BSD_SOURCE
#include <sys/time.h>
int adjtime(struct timeval *delta, struct timeval *olddelta);
//Returns 0 on success, or –1 on error

因为很少使用,具体的内容就不翻译了。

10.6 软件时钟(Jiffies)

很多和时间相关的系统调用的准确度都被限制在系统的软件时钟上面。软件时钟所测量出来的时间单位叫jiffies。一个jiffy的大小由内核源码中的常数HZ所决定,它也是CPU在round-robin算法下对进程进行调度时使用的时间单位。

在Linux/X86-32系统上,内核小于但包括2.4版本中,软件始终的频率是100HZ,也就是一个jiffy是10ms。但在之后因为CPU的速度的提升等因素下,从内核2.6.0开始,同样在Linux/X86-32体系下,软件时钟频率被提升到1000HZ。更高的软件时钟频率的优势就是时钟可以以更精确的方式计量。但是时钟频率也不可能任意高,因为每一个时钟中断都会消耗一定量的CPU时间,而这些时间CPU不会用来执行任何进程。

当然因为不同的需求会对时钟有不同的要求,从内核2.6.13开始,时钟州旗可以被设置到100, 250, 1000HZ上,他们对应着jiffy值 10, 4, 还有1毫秒。从内核2.6.20开始,我们有进一步的频率可用:300HZ,

10.7 进程时间

进程时间是当进程被创造之后一个进程所使用的总的CPU时间。出于记录的目的,内核将CPU时间分成两个部分:

  • 用户CPU时间是所花在用户模式下的时间。有的时候也被称为virtual time,也就是程序使用CPU的时间
  • 系统CPU时间是所花在内核模式下的时间。当然指的是因为该进程所花在内核上的时间

有的时候我们把total CPU time花在进程上的时间叫做进程时间。

当我们从shell运行一个程序的时候,我们可以使用time(1)命令来获取进程时间值以及运行该程序所真正需要的时间。

在程序内,我么也有times()系统调用来获取时间信息,并其放在buf所指向的结构体当中。

#include <sys/times.h>
clock_t times(struct tms *buf);
//Returns number of clock ticks (sysconf(_SC_CLK_TCK)) since
//“arbitrary” time in past on success, or (clock_t) –1 on error

其中tms结构体如下:

struct tms {
 clock_t tms_utime; /* User CPU time used by caller */
 clock_t tms_stime; /* System CPU time used by caller */ 
 clock_t tms_cutime; /* User CPU time of all (waited for) children */
 clock_t tms_cstime; /* System CPU time of all (waited for) children */
};

clock_t数据类型是一个整型用来以clock ticks为单位测量时间。我们可以调用sysconf(_SC_CLK_TCK)来获得每一秒的clock ticks,然后就可以通过除以这个数来转换成单位为秒的时间。

如果成功调用times()之后,它就会返回从一个过去任意一点开始所经过的clock ticks。SUSv3并没有设定这个任意点是什么,仅仅说它在调用进程的生命周期中是一个常数。因此,出于可移植性的考虑,一般times()的使用都是从想要测量的点使用一次,再到测量结束的点再使用一次,通过对差值的计算从而得到测量中间的时间距离。当然,即使是这样子,times()的返回值也不可信赖,因为它可能会超过clock_t所允许的范围,那么作为溢出错误就会返回一个很小的数值(溢出后从0开始计数)。可信赖的计走过的时间的方式是通过gettimeofday()来实现。

在Linux系统当中,我们通常会将times()中参数buf设置为NULL。在这种情况下,times()仅仅返回一个结果。然而,这不是一个可移植的方法,对NULL的使用并不是SUSv3中所规定,也并不是多数UNIX实现了这样子的方式。

clock()函数提供了一个简单获取进程时间的接口。它返回被该进程使用的CPU总时间(system+user)

#include <time.h>
clock_t clock(void);
//Returns total CPU time used by calling process measured in
//CLOCKS_PER_SEC, or (clock_t) –1 on error

这个时间的返回值是以CLOCKS_PER_SEC为单位,所以我们必须通过除法来获得相应的秒数。CLOCKS_PER_SEC在POSIX.1中定义为一百万(和软件时钟完全不同),但是clock()的精确度也的确被限制在软件时钟的精确度上。

例程

time/process_time.c (from "The Linux Programming Interface")

#include <sys/times.h>
#include <time.h>
#include "tlpi_hdr.h"
static void             /* Display 'msg' and process times */
displayProcessTimes(const char *msg)
{
    struct tms t;
    clock_t clockTime;
    static long clockTicks = 0;

    if (msg != NULL)
        printf("%s", msg);

    if (clockTicks == 0) {      /* Fetch clock ticks on first call */
        clockTicks = sysconf(_SC_CLK_TCK);
        if (clockTicks == -1)
            errExit("sysconf");
    }

    clockTime = clock();
    if (clockTime == -1)
        errExit("clock");

    printf("        clock() returns: %ld clocks-per-sec (%.2f secs)n",
            (long) clockTime, (double) clockTime / CLOCKS_PER_SEC);

    if (times(&t) == -1)
        errExit("times");
    printf("        times() yields: user CPU=%.2f; system CPU: %.2fn",
            (double) t.tms_utime / clockTicks,
            (double) t.tms_stime / clockTicks);
}
int
main(int argc, char *argv[])
{
    int numCalls, j;

    printf("CLOCKS_PER_SEC=%ld  sysconf(_SC_CLK_TCK)=%ldnn",
            (long) CLOCKS_PER_SEC, sysconf(_SC_CLK_TCK));

    displayProcessTimes("At program start:n");

    numCalls = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-calls") : 100000000;
    for (j = 0; j < numCalls; j++)
        (void) getppid();

    displayProcessTimes("After getppid() loop:n");

    exit(EXIT_SUCCESS);
}

根据以上函数的应用,我们会获得如下应用:

pi@raspberrypi:~/sysprog/learn_tlpi/build $ ./out 10000000
CLOCKS_PER_SEC=1000000  sysconf(_SC_CLK_TCK)=100

At program start:
        clock() returns: 5722 clocks-per-sec (0.01 secs)
        times() yields: user CPU=0.00; system CPU: 0.00
After getppid() loop:
        clock() returns: 3360682 clocks-per-sec (3.36 secs)
        times() yields: user CPU=1.22; system CPU: 2.14

最后

以上就是失眠早晨为你收集整理的10. Linux的时间10.1 日历时间10.2 时间转换函数10.3 Timezones10.4 地区Locales10.5 更新系统时钟10.6 软件时钟(Jiffies)10.7 进程时间的全部内容,希望文章能够帮你解决10. Linux的时间10.1 日历时间10.2 时间转换函数10.3 Timezones10.4 地区Locales10.5 更新系统时钟10.6 软件时钟(Jiffies)10.7 进程时间所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部