第10章 时间

程序可能会关注两种时间类型,

  • 真实时间:度量这一时间的起点有二:一个为某个标准点;二为进程生命周期内的某个固定时间点(通常为程序启动)。前者为日历时间,适用于需要对数据库疾苦或文件打上时间戳的程序;后者称之为流逝时间或挂钟(wall clock 时间),主要针对需要周期性操作或定期从外部输入设备进行度量的程序。
  • 进程时间:一个进程所使用的CPU的时间总量,适用于对程序、算法检查的优化。
    大多数计算机体系结构都内置有硬件时钟,使内核得以计算真实时间和进程时间。本章将介绍系统死哦安永对这两种时间的处理,以及在可读时间和机器时间之间转换的库函数。由于可读时间的表现形式与地理位置、语言、文化习俗有关,讨论这一话题自然引出对时区和地区的研究,

10.1 日历时间(Calendar Time)

无论地理位置如何,UNIX系统内部对事件的表示方式是以自Epoch以来的秒数度量的,Epoch亦即(通用协调时间UTC,Y以前也称格林威治时间,或GMT) 的1970年1月1日早晨零点。这也是UNIX系统问世的大致日期,日历时间存储于类型为time_t的变量中,此类型是由SUSv3定义的整数类型
在32位linux系统,time_t是一个有符号整数,可以表示的日期范围从1901年12月13日20时45分52秒至2038年1月19号03:14:07,因此,当前许多32位UNIX系统都面临一个2038年的理论问题,如果执行的计算工作涉及未来日期,那么在2038年之前就会与之遭遇。事实上到了2038年,可能所有的unix系统都早已升级为64位或更多位数的操作系统。这一问题也许会随之而大为缓解。然而,32位嵌入式系统,由于其寿命较之台式机硬件更长,故仍然回收此问题的困扰。此外,对于依然以32位time_t格式保存时间的历史数据和应用程序,这个问题也会依然存在
系统调用gettimeofday(),可于tv指向的缓冲区返回日历时间

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

参数tv指向的是如下数据结构的一个指针:

struct timval{
	time_t tv_sec;  //seconds since 00:00:00 1 Jan 1970
	susconds_t tv_usec; //Addtional microseconds(long int)
}

虽然tv_usec字段能提供微秒级精度,但是其返回值的精确性依赖于架构的具体实现来决定。
gettimeofday()的参数 tz是个历史产物,早期的UNIX实现用其来获取系统的时区信息。目前已遭废弃应始终将其至为NULL.

如果提供了tz参数,那么将返回一个人timezone结构体,其内容为上次调用settimeofday()时
传入的tz参数(已废弃)值,该结构包含两个字段tz_minuteswest和tz_dsttime.
tz_minuteswest字段表示欲将本时区时间转换为UTC时间所必须增加的分钟数,如为负值,
则表示此时区位于UTC时区以东(例如如为欧洲中部时间,会提前UTC一小时,
则将此字段设置为-60.tz_dsttime字段内为一个常量,意在表示这个时区是否强制施行夏令时
(DST)值,正由于夏令时制度无法用一个简单算法加以表达,所以tz参数乙遭放弃

time 系统调用返回自Epoch以来的秒数(和函数 gettimeofday()所返回的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所指向的位置
由于time()会以两种方式返回相同的 值,而使用时唯一可能出错的地方就是赋予timep一个无效的地址(EFAULT),因此往往采用如下调用,
t = time(NULL)

之所以存在两个本质上目的相同的系统调用(time()gettimeofday())自有其历史原因。
早期的unix实现提供了time,4.3BSD又补充了更为精确的gettimeofday()系统调用。这时再将time()作为系统调用就显得多余,可以将其实现为一个调用gettimeofday()的库函数。

10.2时间转换函数

下图所示为用于time_t值和其他时间格式之间相互转换的函数,其中包括打印输出。这下函数屏蔽了因时区,夏令时(DST)制和本地化等问题给转换所带来的种种复杂性
获取和使用日历时间的函数

10.2.1将time_t转换为可打印格式

为了将time_t转换为可打印格式,ctime()函数提供了一个简单方法。

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

把一个指向time_t的指针作为timep参数传入函数ctime(),将返回一个长达26字节的字符串,内含标准格式的日期和时间,如下图所示:
Wed Jun 8 14:22:34 2022
该字符串包含换行符和终止符各一。ctime()函数在进行转换时,会自动对本时区和DST设置加以考虑(10.3节将解释这些设置的确定过程),返回的字符串经由静态分配,下一次ctime()调用将其覆盖。
SUSv3 规定,调用ctime(),gmtime(),localtime()或asctime()中的任意函数,都可能会覆盖由其他函数返回,且经静态分配的数据结构,换言之,这些函数可以共享返回的字符串组和tm结构体

10.2.2 time_t和分解时间之间的转换

函数gmtime()和localtime()可将一time_t值转换为一个所谓的分解时间。分解时间被置于一个由静态分配的结构中,其地址则作为函数结果返回。

#include <time.h>
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
	Both Return a pointer to a pointer ao a statically allocated 
	broken_down time structure on success or NULL on error

函数gmtime()能够将日历时间转换为一个对应于UTC的分解时间(字母GM原语格林威志标准时间)。相比之下,函数localtime()需要考虑时区和夏令时设置,返回对应于系统本地时间的一个分解时间。
在这些函数所返回的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 Moth(1-32)*/
	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 ihe year (0-365; 1 Jan=0)(0-60)*/
	int tm_isdst ;  /** Daylight saving time flag
								>0: DST is in effect
								=0:DST is not effect
								<0: DST information not available*/
};

将字段tm_sec的上限设置为60(而非59)以考虑闰秒,偶尔会用其将人类日历调整至精确的的天文年。
函数mktime 将一个本地时区的分解时间翻译为time_t值,并将其作为函数结果返回。调用者将分解时间置于一个tm结构,再以timeptr指向该结构,这一转换会忽略输入tm中的tm_wday和tm_yday字段。

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

函数mktime()可能会修改timeptr所指向的结构体,至少会确保对tm_wday和tm_yday字段值的设置,会与其他输入字段值能对应起来。
此外,mktime()不要求tm结构体的其他字段受到签署范围的限制。任何一个字段超出范围,mktime()都会将其调整回有效范围之内,并适当调整其他字段。所有这些调整,均发生于mktime更新tm_wday和tm_yday字段并计算返回值time_t之前。

例如,如果输入字段tm_sec的值为123,那么再返回时此字段的值将为3,且tm_min字段值会在其之前值的基础上加上2,(如果这一改动造成tm_min溢出,那么将调整tm_min的值,并且递增tm_hour字段,以此类推)这些调整甚至适用于字段负值。例如,指定tm_sec为-1即意味着前一分钟的第59秒,此功能允许一份及时间来计算日期和时间,非常有用。
mktime()在进行转换时会对时区进行设置。此外,DST设置的使用与否,取决于输入字段的tm_isdstd的值

  • 若tm_isdst为0,则将这一时间视为标准时间(即,忽略夏令时,即使实际上每年的这一时刻处于夏令时时段)
  • 若tm_isdst大于0,则将这一时间视为夏令时(即,夏令时生效,即使每年的此时不处于夏令时阶段)
  • 若tm_isdst小于0,则试图判断DST在每年的这一时间是否生效。这往往是众望所归的设置。
    (无论tm_isdst的初始值设置如何)在转换完成时,如果针对给定的时间,DST生效,mktime()回见tm_isdst字段设置为正值,若DST未生效,则将tm_isdst置为0。

分解时间和打印格式之间的转换

从分解时间转换为可打印格式

在参数tm中提供一个 指向分解时间结构体的指针,asctime()则会返回一指针,指向精油静态分配的字符串,内含时间,格式与ctime()相同。

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

相比于ctime,本地时区设置对asctime()没有影响,因为其转换的是一个分解时间,改时间要么已经通过localtime()做了本地化处理,要么早已由gmtime()转换成了UTC。
如同ctime一样,asctime() 也无法控制其生成字符串的格式。

#include  <locale.h>
#include <time.h>
#include <sys/time.h>
#include <stdio.h>

#define SECONDS_IN_TROPICAL_YEAR (365.24219*24*60*60)

int main()
{
    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)
    {
        perror("gettimeofday");
    }
     printf("gettimeofday() return %ld secs, %ld microsecs\n",(long) tv.tv_sec,(long)tv.tv_usec);

     gmp = gmtime(&t);
     if(gmp == NULL)
     {
        perror("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);
locp=localtime(&t);
if(locp == NULL)
{
     perror("localtime");
}
     loc=*locp;  //save local copy
     #if 1
     printf("Broken down by gmtime():");
     
     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=%d\n\n",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 secs\n",(long)mktime(&gm));
       printf("mktime() of localtime() value:    %ld secs\n",(long)mktime(&loc));
#endif
       return 0;
}

当把一个分解时间转换成打印格式时,函数==strstime()==可以提供更为精确的控制,令timeptr指向分解时间,strftime()将以null结尾,由日期和时间组成的相应字符置于outstr所指向的缓冲区中。

#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的最大长度,不同于cime()和asctime(),strftime()不会在字符串的结尾包括换行符(除非format中有定义换行符)。
如果成功,strftime()返回outstr所指向缓冲区的字节长度,且不包括终止空字节,如果结果字符串的总长度,含终止空字节,超过了maxsize参数,那么strftime()会返回0以表示出错,且此时无法确定outstr的内容。
strftime()的format参数是一字符串他,与赋予printf的参数类似。冠以百分号(%)的字符序列时对转换的定义,函数会将百分号后的说明符字符一一替换为日期和时间的组成部分。这是一套相当丰富的转换说明符,下表中所列的是其中一个子集,除非特别注明,所有这些转换说明符都符合SUSv3标准。
==%U和%W说明符都生成一年中的周数。%U的周数按照一下方法计算,含有星期日的第一周编号为1,此周的前一周编号为0,如果星期天恰巧是当前的第一天,那么就没有第0周,==当年的最后一天属于第53周。%W的周数编号以同样的方式来计算,只不过计算对象时周一而不是周日。
将打印格式时间转换为分解时间
函数strptime()时身体strftime()的逆向函数,将包含日期和时间的字符串转换成一分解时间。

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

函数strptime()按照参数format内的格式要求,对由日期和时间组成的字符串str加以解析,并将转换后的分解时间置于指针timeptr所指向的结构体中。
如果成功,strptime()返回一指针,指向str中下一个未经处理的字符。(如果字符串中还包含有需要应用程序处理的额外信息,这一特性就能排上用场。)如果无法匹配整个格式字符串,strptime()返回NULL,以示出现错误。
strptime()的格式规范类似于scanf(3),包含以下类型的字符。

  • 转换字符串冠以一个百分号(%)字符。
  • 如包含空格字符,则意味着其可匹配零个或多个空格。
  • (%之外的)非空格字符必须和输入字符串中的相同字符严格匹配。
    转换说明类似于之前为strftime()给出的内容(表10-1)。主要区别在于,此处的说明符更为通用。例如,不拘于星期名称的全称或简称,%a和%A都可接受,而且%d和%e均可用于读取月中的个位天数,无论数字前面是否有0.此外不区分大小写,例如May和MAY是相同的月份名称。使用字符串%%来匹配输入字符串中的百分号字符。
    glibc在实现strptime()时,并不修改tm结构体中那些未获format说明符初始化的字段。这也意味着可以根据多个字符串,例如,一个日期字符串和一个时间字符串,发起多次strptime()调用,来创建一个他们结构体。SUSv3虽然允许这一行为,但并不强制要求实现,因此在其他UNIX实现上不能对其有所依赖。要保证应用的可移植性,就必须确保,要么str和format中所含输入信息足已设置最终tm结构的所有字段,要么在调用strtime()之前对tm结构体已经做了适当的初始化处理。在大多数情况下,用memset()把整个结构体置为0也就足够了,但要留心,在glibc和许多其他时间转换函数的实现中,m_day字段值为0,意为上月的最后一天,最后还要注意,strptime()从不设置他们结构体的tm_isdst字段。

程序清单10-3演示了strptime和strftime()的用法。该程序从命令行参数中接收日期和时间,然后用strptime()将其转换为一个分解时间,截止使用strftime()执行逆向转换并显示结果。该程序至多接收三个参数,其中前两个为必须提供。第一个参数时包含日期和时间的字符串。第二个参数指定了strptime()在解析第一个参数时所采用的格式。可选的第三个参数是格式字符串,用于strftime()的逆向转换。如果省略此参数,将使用一个默认的格式字符串。

$ ./strtime "9:39:46pm Feb 2011" "%I:%M:%S%p %d %b %Y"
calendar time (seconds since Epoth):1296592786
strftime() yields 21:39:46 Tuesday,01 February 2011 CET

//以下用法与之类似,只不过这词为strtime()明确指定了格式

$ strtime "9:39:46pm 1 Feb 2011" "%I:%M:%S%p %d %b %Y" "%F %T"
calender time(seconds since Epoth):1296592786
strftime() yields:2011-02-01 21:39:46
#define _XOPEN_SOURCE
#include <time.h>
#include <locale.h>

#define SBUF_SIZE 1000

int main(int argc,char*argv[])
{
    struct tm tm;
    char sbuf[SBUF_SIZE];
    char *ofmat;

    if(srgc <3 || strcmp(argv[1],"-help")==0)
        printf("%s, inputr-date time in-format [out-format]\n" arg[0]);
    if(setlocale(LC_ALL,"") ==NULL)
        perror("setlocale");   /*Use locale settings in conversions*/
    memset(&tm,0,sizeof(struct tm));   /* Initialize 'tm'*/
    if(strptime(argv[1],argv[2],&tm) == NULL)
        perror("strptime");

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

     ofmat = (argc > 3) ?argv[3] :"%H:%M %A, %d %B %Y %Z";

     if(strftime(sbuf,SBUF_SIZE,ofmt,&tm) ==0)
        perror("strftime returned 0");
    printf("strftime() yields:%s\n",sbuf);

    return 0;
}
                   程序清单10-3:获取好和转换日历时间

10.3 时区

不同的国家(有些甚至时同一国家内的不同地区)使用不同的时区和夏时制,对于要输入和输出时间的程序来说,必须对系统所处的时区和夏时制加以考虑。所幸的时,所有这些细节都已经由C语言函数库包办了。
时区定义
时区信息往往是既浩繁又多变的。处于这一原因,系统没有直接编码与程序会函数库中,而是以标准格式保存于文件中,并加以维护。
这些文件位于目录==/usr/share/zoneinfo==中。该目录下的每个文件都包含了一个特定国家或地区内制度的相关信息,且往往根据所描述的时区加以命名,诸如EST(美国东部标准时间)、CET(欧洲中部时间)、UTC、Turkey和Iran.此外可以利用子目录树对相关时区进行有层次的分组。例如,Pacfic目录就可能包含文件Aucland、Port_Moresby和Galagagos。在程序中指定的时区,实际上时值该目录下木易时区的相对路径名。
系统的本地时间由时区文件/etc/localtime定义,通常连接到/user/share/zoneinfo下的一个文件
为程序指定时区
为运行中的程序制定一个时区,需要将TZ环境变量设置为由一冒号(:)和时区名称组成的字符串,其中时区名称定义于 /usr/share/zoneinfo中。设置时区会自动影响到函数ctime()、localtime()、mktime()和strftime()。
为了获取当前的时区设置,上述函数都会调用tzset(3),对下面3个全局变量进行了初始化:

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

函数tztest()会首先检查环境变量环境变量TZ.如果尚未设置该变量,那么就采用/etc/localtime中定义的默认时区来初始化时区。如果TZ环境变量的值为空,或违法与时区文件名相匹配,那么就采用UTC。还可以将TZDIR环境变量(非标准的GNU扩展)设置为搜索时区信息的目录名称,以替代默认的 /usr/share/zoneinfo目录。
可以通过运行程序清单10-4中的程序来观察TZ变量的影响力。第一次运行输出的时相应系统的默认时区(欧洲中部时间CET)。在第二次运行时由于指定的时区为 New Zealand,其在每年此时已进入夏令时,时区要比CET提前12个小时。

在这里插入代码片
$ ./show_time
ctime() of time() vlue is:Tue Feb 1 10:25:56 2011
asctime() of local time vlue is:Tue Feb 1 10:25:56 2011
strftime() of local time vlue is:Tuesday 01 Feb 2011 , 10:25:56 CET

$tz=":Pacific/Auckland" ./show_time
ctime() of time() vlue is:Tue Feb 1 22:26:19 2011
asctime() of local time vlue is:Tue Feb 1 22:26:19 2011
strftime() of local time vlue is:Tuesday 01 Feb 2011 , 22:26:19 CET

程序清单10-4:演示时区和地区的效果 time/show_time.c

#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <locale.h>

#define BUF_SIZE 100
int main(int argc,char *argv[])
{
    time_t t;
    struct tm  *loc;
    char buf[BUF_SIZE];
    
    if(setlocale(LC_ALL,"") == NULL)
    {
        printf("setlocale err\n");  //Use locale settings in conversions
        return -1;
    }

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

    loc = localtime(&t);
    if(loc == NULL)
    {
        printf("localtime err \n");
        return -1;
    }

    printf("asctime() of local time is: %s",asctime(loc));
    if(strftime(buf,BUF_SIZE,"%a,%D %B %Y,%H:%M:%S %Z",loc) == 0)
    {
        perror("strftime returned 0");
    }

    return 0;
}

SUSv3 为设置TZ环境变量定义了两个通用方法。如前所述,可将TZ设置为由冒号外加字符串组成的字符序列,其中的字符串泳衣标识时区,并随系统的实现的不同而不同,通常为失去描述文件的路径名。(在采用这种形式时,Linux和其他一些UNIX实现允许将冒号省略,但SUSv3并未规范这一行为。为了保证代码的可移植性,应当始终包含冒号。)
设置TZ 的另一种方法在SUSv3中有完整的定义。使用此方法,可以使用如下形式的字符串赋给TZ:

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

为了方便阅读,在上面的这行字符串中加入了空格,但实际上任何空格都不应出现在TZ中。方括号([])用来表示可选项
std 和 dst部分时用以标识标准和DST时区名称的字符串。例如,CET和CEST分别为欧洲中部时间和欧中中部夏令时间。各中情况的offset分别表示欲转换为UTC,需叠加在本地时间上的征服调整值。最后四部分则提供了一个规则,描述何时从标准时间变更为夏令时。
可以多种格式指定date,其中之一时Mm.n.d意即:m(1~~12)月中,第n(1~5,每个月的最后d天总为第5周)周,星期d(0=星期一,6=星期天)。如果省略time,则无论你何种情况下军默认为02:00:00(上午两点)。
一下将TZ定义为Central Europe,该时区的标准时间比UTC提前1个小时,且DST始于3月的最后一个星期日,直至10月的最后一个星期日结束,提前UTC2小时。

 	TZ="CET-1:00:00CDST-2:00:00,m3.5.0,M10.5.0"
 	此处省略了对DST转换时间的指定,因为默认其发生与02:00:00。显然较之于如下的Linux专有格式,
 	上述的形式的确缺乏可读性:
TZ=":Europe/Berlin"

10.4 地区(locale)

世界各地在使用数千种语言,其中在计算机系统上经常使用的占了相当比例。此外,在显示诸如数字、货币金额、日期和时间之类的信息时,不同国家的习俗也不同。例如,大多数欧洲国家使用逗号,而非小数点来分割实数的整数和小数部分,大多数国家的日期的书写格式也与美国所采用的MM/DD/YY格式不同。SUSv3对local的定义为:用户环境中依赖于语言和文话习俗的一个子集。
理想情况下意欲在多个地理区位运行的任何程序都应该处理地区概念,以期以用户的语言和格式来显示和输入信息。这也构成了相当复杂的课题–国际化。在理想情况下,程序只要经过一次编写,则不论运行于何处,总会自动以正确方式来执行I/O操作,也就是说,完成本地化任务。尽管存在各种支持工具。程序国际化的工作仍然耗时不菲。诸如glibc之类的程序也提供有工具,来帮助程序支持国际化。

经常将术语internationlization写为I18N,意即:I加上18个字母再加N.这种形式既便于书写,有避免了单词本身在英语和美语之前拼写方式不同的问题。

地区定义
和时区信息一样,地区信息同样时既浩繁且多变的。处于这一原因,与其要求各个程序和函数库来存储地区信息,还不如由系统按标准格式将地区信息存储与文件中,并加以维护。
地区信息维护于/usr/share/local(在一些发行版本中为/usr/lib/local)之下的目录层次结构中。该目录下的每个子目录都包含一特定地区的信息。这些目录的命名约定如下:

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

language 是双字母的ISO语言代码,territory是双字母的ISO国家代码。codeset表示字符编码集。modifier则提供了一种方法,用以区分多个地区目录下labguage、terriory和codeset均相同的情况下。de_DE.utf-8@euro是完整的目录名称的例子之一,代表地区如下:德语,德国,UTF-8字符编码,并采用欧元作为货币单位。
正如命名格式中的方括号所示,可以将地区目录中的响应部分省略。通常情况下,命名只包括语言和国家。因此,en_US是(说英语的)美国的地区目录,而fr_CH则是法语区的地区目录。

当在程序中指定要使用的地区时,实际上是指定了/usr/share/locale 下的某个子目录名称。如果程序指定地区不与任何子目录名称相匹配,那么C语言函数库将按如下顺序将各部分从执行地区(locale)中剥离,以寻求匹配:

  • codeset

  • normalized codeset

  • territory

  • modifier
    标准化字符集(bormalized codeset)是一个特定版本字符编码集的名称,提出了所有非字母、非数字的字符,且将所有字母转换为小写,最终字符串前冠以ISO三个字符,在于排除字符集名称因大小写和标点符号(例如,额外的连接符)而发生的变化。
    这里是剥离过程的一个例子,假设为一程序指定的地区为fr_CH.utf-8.但并不村子啊以该名称命名的地区目录,那么入伏哦fr_CH目录存在,则与之匹配。如果fr_CH目录也不存在,那么将采用fr地区目录,万一fr目录也不存在,那么简而言之,setLocale()函数将会报错。
    每个地区子目录中包括有标准的一套文件,指定了此地区的约定设置,如表10-2所示。关于表中的信息,还需要注意以下几点。

  • 文件LC_COLLATE定义了一套规则,描述了如何在一字符集内对字符排序(例如alphabetical "按字母顺序排列的"字符集顺序)。这些规则将决定函数strcoll(3)和strxfrm(3)的动作。即便是同属于拉丁语系的语言,其遵循的排序规则也不相同。例如,一些欧洲语言由额外字母,在某些情况下排在字母Z之后。另外还有特殊情况,西班牙语的双字母序列||,排序位于字母1之后。

  • 目录LC_MESSAGES是程序显式信息迈向国际化测步骤之一。要实现更为全面的程序信息国际化,可以惨老消息目录或是GNU的gettext API。

表10-2 特定于地区的子目录内容
表10-2特定于地区的目录子内容系统中实际定义的尔地区可能会各有不同。除了必须定义一个名为POSIX(与C统一,后者的存在由于历史原因)的标准地区之外,SUSv3没有对此做出任何要求。POSIX折射出UNIX系统的历史渊源。因之,系统建立与ASCII字符集之上,使用英文“yes/no”来响应。该地区的货币和数字格式处于未定义状态。

为程序设置地区
函数setlocale()既可以设置也可查询程序的当前地区。

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

category参数选择设置或查询地区的哪一部分,他仅能使用表10-2中的地区类别的常量名称。因此,它可以设置地区的时间显式格式是德国,而地区的货币符号是美元,或者,更常见的是,我们可以利用LC_ALL来指定我们要设置的地区的所有部分的值。
使用setLocale()设置地区有两种不同的方法。locale参数可能是一个字符串,指定系统上已定义的一个地区(例如,/usr/lib/locale 中的子目录的名称),如de-DE或en_US。另外,地区可能呗指定为空字符串,这意味着从环境变量取得地区的设置

setlocale(LC_ALL,"");

我们必须这样调用才能使程序使用环境变量的地区。如果调用被省略,这些环境变量将不会对程序生效。
当程序调用了setlocale(LC_ALL,“”),我们能够使用一系列环境变量来控制地区的各部分内容,环境变量的名称也对应于表10-2中列出的类型:LC_CTYPE、LC_COLLATE、LC_MONETARY、LC_NUMERIC、LC_TIME、LC_MESSAGES。另外,我们可以使用LC_ALL或LANG环境变量指定整个地区的设置。如果设置了多个先前的环境变量,那么LC_ALL会覆盖所有其他的LC_*环境变量,同时LANG的优先级最低。因此通常使用LANG为地区所有内容设置默认值,然后用单独的LC_*变量,设置地区的各个方便内容来覆盖默认值。
最后,setlocale()返回一个指针指向标识这一类地区设置的字符串,通常使静态分配的。如果我们仅需要查看地区的设置而不需要改变它,那么我们可以指定locale参数为NULL.
地区这只影响众多GNU/Linux实用程序,以及glibc的许多函数功能,其中有函数stfftime()和strptime,当我们在不同的地区运行程序清单10-4,strftime返回的结果如下:
在这里插入图片描述在这里插入图片描述

10.5更新系统时钟

两个更新系统时钟的接口:settimeofday()和adjtime().这些接口都很少被应用程序使用,因为系统时间通常是由工具软件维护,如网络事件协议(Network Time Protocol)守护进程,并且他们需要调用者已被授权(CAP_SYS_TIME)。
系统调用settimeofday()是gettimeofday()的逆向操作。他将tv指向timeval结构体里的秒数和微秒数,设置到系统的日历时间。

#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

和函数gettimeofday()一样tz参数已经被废弃,这个参数应该始终被指定为NULL。
tv.tv_usec字段的微秒精度并不意味着我们以微秒精度来设置系统时钟,因为时钟的精度可能会低于微秒。
虽然SUSv3没有定义settimeofday(),但他在其他unix实现这种被广泛使用。

Linux 还提供了stime()系统调用来设置系统时钟。settimeofday()和stime()之间的区别是,后者调用允许使用秒的精度来标识新的日历时间。和函数time()与gettimeofday()相同,stime()和gettimeofday()的并存是由历史原因造成的:拥有更高精度的后一个函数,是由4.3BSD添加的。

settimeofday()调用所造成的那种系统时间的变化,可能会对依赖于系统时钟单调递增的应用造成有害的影响(例如make(1))数据库系统使用的时间戳或包含时间戳的日志文件。处于这个原因,当对事件做微小调整时(几秒钟误差),通常是推荐使用库函数adjtime(),他将系统时钟逐步调整到正确的事件。

#define _BSD_SOURCE
#include <sys/time.h>

int adjtime(struct timeval *delta,struct timeval *olddelta)
							Returns 0 on success or -1 on error

delta参数指向timeval结构体,指定需要改变事件的秒和微秒数。如果这个值是正数,那么每秒系统时间都会额外拨快一点点,直到增加完所需系统时间。如果delta值为负时,始终会以类似的方式减慢。

Linux/x86-32以没2000秒变化1秒(或每天43.2秒)的频率调整时钟。

在adjtime()函数执行的时间里,它可能无法完成始终调整,剩余未经调整的事件存放在oldelta指向的timeval结构体内。如果我们不关心这个值,我们可以指定olddelta为NULL.相反,如果我们只关心当前未完成时间校正的信息,而并不想改变它,他们可以指定delta为NULL。

adjtime()在Linux上,基于更通用和复杂的特定于Linux的系统调用adjtimex()来完成功能。这个系统调用也同时被网络事件协议(NTP)守护进程调用。

10.6 软件时钟 (jiffies)

在本书中所描述的时间相关的各种系统调用的精度时受限于系统软件时钟(software clock)的分辨率,他的度量单位被称为jiffies。jiffes的大小时定义在内核源代码的常量HZ.这是内核按照round-robin的分时调度算法(35.1节)分配CPU进程的单位。
在2.4或以上版本的Linux/x86-32内核中,软件时钟速度是100赫兹,也就是说,一个jiffy是10毫秒。
子Linux面世以来 ,由于CPU速度已大大增加,Linux/x86-32 2.6.0内核的软件时钟速度已提高到1000赫兹。更高的软件时钟速率意味着定时器可以有更高的操作精度和时间可以拥有更高的测量精度。然而,这并非可以任意提高时钟分辨率,因为每个时钟中断会消耗少量的CPU时间,这部分时间CPU无法执行任何操作。
经过内核开发人员之间的讨论,最终导致软件时钟频率成为一个可配置的内核的选项(包括处理器类型和特性,定时器的频率)。自2.6.13内核,时钟频率可以设置到100、250(默认)或1000赫兹,对应的jiffy分别为10、4、1毫秒。自内核2.6.20,增加了一个频率:300赫兹,它可以被两种城建的视频帧速率25帧每秒(PAL)和30帧每秒(NTSC)整除。

10.7进程时间

进程时间时进程穿件后使用的CPU时间数量。处于记录的目的,内核把CPU时间分成以下两部分。

  • 用户CPU时间是在用户模式下执行所花费的时间数量。有时也成为虚拟时间,这对于程序来说,是他已经得到CPU的时间。
  • 系统CPU时间是在内核模式中执行所花费的时间数量。这是内核用于执行系统调用或代表程序执行的其他任务(例如,服务页错误)的时间

有时候,进程时间指处理过程中所消耗的总CPU时间。
当我们运行一个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
		"arbitry" time in past on sucess,or (clock_t)-1 on error

buf指向的TMS结构体有下列格式:

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

tms结构体的前两个字段返回调用进程目前位置使用的用户和系统组件的CPU时间。最后两个字段返回的信息是:父进程(比如times()的调用者)执行了系统调用wait()的所有已经终止的子进程使用的CPU时间。
数据类型clock_t是用时钟计时单元(clock tick)为单位度量时间的整型值,习惯于计算tms结构体的4个字段。我们可以调用sysconf(_SC_CLK_TCK)来获得每秒包含的时钟计时单元数,然后用这个数字除以clock_t转换为秒(sysconf 见11.2节)

在大多数Linux硬件架构,sysconf(_SC_CLK_TCK)返回100.与此对应的内核常量时USER_HZ。然而USER_HZ在其他几个架构下可以被定义超过100,如Alpha

如果成功,times()返回自过去的任一点流逝的以时钟计时单元为单位的真实的时间。SUSv3特别未定义这点是什么,只是说,浙江是在调用进程的生命周期内的一个固定点。因此,这个返回值唯一的用法时通过计算一对times()调用返回的值的差,来计算进程执行消耗的时间。然而,即使这种用法,times()的返回值任然是不可靠的,因为他可能会溢出clock_t的有效范围,这时times()的返回值将再次从0开始计算(也就是说,一个稍后的times()的调用返回的数值可能会低于一个更早的times()调用)。可靠的测量经过时间的方法时使用函数gettimofday()。
在Linux 上,我们可以指定buf参数为NULL.。在这种情况下,times()只是简单地返回一个函数结果。然而这是没有意义的。SUSv3并未定义buf可以使用NULL,因此许多其他UNIX实现需要这个参数必须为一个非NULL值。
函数clock()提供了一个简单的接口用于取得进程时间。它返回一个值描述了调用进程使用的总的CPU时间(包括用户和系统)。

#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

time()的返回值计量单位是CLOCKS_PER_SEC,所以我们必须除以这个值来获得进程所使用的CPU时间数。在POSIX.1,CLOCK_PER_SEC是常量10000,无论底层软件时钟(10.6)的分辨率是多少。clock()的精度最终仍受限于软件时钟的分辨率。

虽然clock()和time()返回相公的数据类型clock_t,这两个接口使用的测量单位却并不相同。这是历史原因造成了clock_t定义的冲突,一个是POSIX.1标准,而另一个时C变成语言标准。

即使CLOCKS_PER_SEC是常量10000,SUSv3注明,这个常量在不兼容XSI(non-XSI-conformant)的系统上可以为整型变量,所以,我们不能简单地把它作为一个编译时常量(即,我们不能够使用#ifdef预处理表达式)。他可能会被定义为一个长整数(即1000000L),我们总是将这个常量转换为long,因此我们可以简单地用printf()把它打印输出(见3.6.2节)
SUSv3描述clock()应该返回“进程所使用的处理器时间”时有不同的的解释。在一些UNIX的实现中,clock()返回的时间包含所有等待子进程使用的CPU时间。而在Linux上他不包括。
示例程序
在程序清单10-5中的程序演示了如何使用本书中描述的功能。函数displayProcessTimes()首先打印由调用者提供的信息,然后使用clock()和times()来获得和显示进程时间。主程序首先调用函数displayProcessTimes(),然后执行一个循环,通过重复调用getpid()消耗一些CPU时间,再次调用displayProcessTimes()来查看这个循环会消耗多少CPU时间。当我们使用这个程序调用getpid()十万次,这就是我们看到的:
在这里插入图片描述

程序清单10-5 获取进程CPU时间 process_time.c

#include <sys/times.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void   displayProcessTimes(const char *msg) /*Display 'msg' and process times*/
{
    struct tms t;
    clock_t clockTime;
    static long clockTicks = 0;
    if(msg !=NULL)
    {
        printf("%s\n",msg);
    }

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

    clockTime = clock();
    if(clockTime == -1)
    {
        perror("clock:");
        return;
    }

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

    if(times(&t) == -1)
    {
        perror("times:");
        return;
    }

    printf("    times() yields:user CPU = %.2f;system CPU:%.2f\n",
                (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) = %ld\n\n",
        (long)CLOCKS_PER_SEC,sysconf(_SC_CLK_TCK));

    displayProcessTimes("At program start:\n");

    numCalls = (argc>1)?atoi(argv[1]):100000000;

    for(j = 0;j<numCalls;j++)
    {
        (void)getppid();
    }

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

    exit(EXIT_SUCCESS);
}

10.8总结

真实时间对应于时间定义的每一天。当真实时间通过一些标准点计算的时候,我们称它为日历时间。和经过的时间相对,他是度量一个进程生命周期的一些点(通常是开始)。
进程时间是由一个进程使用的CPU时间量,并划分为用户时间和系统时间。
多种系统调用允许我们获取和设置系统时钟值(即日历时间,以秒为单位从Epoch计算),以及一些列的库函数能够完成从日历时间到其他格式之间的转换,包括分解时间和具有可读性字符串。描述这种转换把我们引入了地区和国际化的讨论。

使用和现实时间和日期是许多应用程序的一个重要组成部分,我们会在23章介绍更多的时间的度量(定时器)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值