转载请注明出处: http://blog.csdn.net/luotuo44/article/details/39374759
本文主要涉及Linux时间类型、时间函数以及Linux提供的睡眠函数。
时间类型和对应的函数:
time_t:
最不陌生的时间类型恐怕是time_t这个类型了吧。它出现在C语言的标准库。但ISO C中并没有规定time_t是什么类型、范围以及精度,不过在POSIX中一般是被实现为有符号的整型。
time_t的单位是秒。函数time()的返回值就是一个time_t类型,表示从1970-01-01 00:00:00 UTC (即Epoch时间)到现在有多少秒。要注意的是,该函数返回的是一个UTC时间,不是我们平常使用的北京时间。可以用函数localtime把这个UTC时间转换成当地时间。我们可以用这个函数把UTC时间转换成北京时间。localtime的返回值是一个struct tm结构体指针。定义如下:
struct tm { int tm_sec; /* 秒 [0-60] 有闰秒*/ int tm_min; /* 分钟 [0-59] */ int tm_hour; /* 小时 [0-23) */ int tm_mday; /* 日 [1-31) */ int tm_mon; /*月 [0-11) */ int tm_year; /* 年 其值等于所在年份- 1900 */ int tm_wday; /* 星期 ]0-6] 星期天为0 */ int tm_yday; /* 今天是今年的第几天 [0-365] 1月1日是第0天*/ int tm_isdst; /* 夏令时标志位*/ };
注意各个成员的范围,不要搞错了。此外,对于秒,是有闰秒的。所以范围是[0, 60]。
可以看到,相对于一个time_t,这个struct tm结构体的成员还是很有用的。因为一个给定time_t,人们根本就不知道它表示的是哪个时间,但转换成struct tm结构体后就能一眼看出是什么时间。
如果并不想把时间转换成本地时间,而仅仅转换成struct tm,那么可以使用函数gmtime,它同样是返回一个struct tm指针。因为localtime和gmtime都是返回一个指针,所以它们都不是线程安全的。对于地,有localtime_r和gmtime_r。这个两个函数是线程安全的。如果像把一个struct tm时间转换成time_t可以使用mktime函数。有时也想把用这个时间类型转换成一个字符串方便输出,标准C语言也提供了这些函数。下面给出这些函数的声明。
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);
一般来说,这些函数都是在time.h头文件中。但对于线程安全的那几个函数,有些编译器还要另外#include<pthread.h>。
clock_t:
它的实际类型是有符号整型,它一般是用来表示一个进程占用的CPU时间。函数clock()返回就一个clock_t类型值,表示进程从启动到调用clock函数,使用了多少CPU时间。将返回值除以CLOCKS_PER_SEC,就能得到以秒为单位的时间。clock函数声明在time.h头文件中。
struct timeval:
相对来说,time_t的粒度还是比较大,单位是秒。
Linux还提供了下面另外一个精确度高一些类型strut timeval。
struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds 微秒*/ };
成员tv_usec表示的是微秒,1秒 = 1 000毫秒 = 1 000 000微秒。
一般是在函数gettimeofday函数中使用到这个时间类型。
#include<syt/time.h> int gettimeofday(struct timeval *tv, struct timezone *tz);//第二个参数一般为NULL
像time函数语言,它也返回一个从1970-01-01 00:00:00 UTC(即Epoch时间)到现在流逝的时间,当然这个时间也就是系统时间,为UTC时间不是当地时间。也可以把struct timeval当作现在的时间,将成员tv_sec用上面的localtime转换即可。
Linux还定义了一系列用于struct timeval的操作函数。比如相加减,清空,比较。
#include <sys/time.h> void timeradd(struct timeval *a, struct timeval *b, struct timeval *res);//res = a + b void timersub(struct timeval *a, struct timeval *b, struct timeval *res);//res = a - b void timerclear(struct timeval *tvp); int timerisset(struct timeval *tvp); int timercmp(struct timeval *a, struct timeval *b, CMP);
参数CMP就是我们平常用的比较符号<、>=、!=等等。
有一点要注意的是,因为gettimeofday获取的是系统的时间。因为用户是可以手动修改这个系统时间的。所以gettimeofday获取的时间可能是错误的时间。
如果程序想在两个不同的时刻分别获取系统,然后计算时间差。那么gettimeofday函数不是最佳选择。原因就是用户可以修改系统时间。此时应该使用monotonic时间。monotonic时间是boot启动后到现在的时间,没人能修改它。这个monotonic时间下面会讲解。
struct timespec:
struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds 纳秒*/ };
它能提供的时间精确度是纳秒。当然,实际上硬件是不一定支持这个精确度的。 有三个与之相关的函数。
//glibc2.17之前的版本在链接时需加上-lrt #include <time.h> int clock_getres(clockid_t clk_id, struct timespec *res); int clock_gettime(clockid_t clk_id, struct timespec *tp); int clock_settime(clockid_t clk_id, const struct timespec *tp);
可以用ls-al /lib/libc.so.6或者ldd –version命令查看glibc的版本,uname -r命令查看Linux内核版本。
参数clk_id表示时钟类型,Linux提供下面这些时钟类型。
- CLOCK_REALTIME: 系统范围的实时时钟。系统范围是指系统的所有用户所有程序都使用。实时时钟是指系统现在的时间(就像Windows右下角的那个时间),这和gettimeofday函数获取的系统时间是相同的。系统时间是可以修改的,所以可以使用clock_settime(CLOCK_REALTIME)修改系统的时间
- CLOCK_REALTIME_COARSE: 一个更快但时间粒度更大(即时间精确度没那么高)的CLOCK_REALTIME。这个函数是Linux系统特有的,在2.6.32内核版本中首次出现
- CLOCK_MONOTONIC:monotonic时间。monotonic是单调的意思,也就是说它只会单调递增,不能被人手动修改。POSIX只是说明它是一个单调时间,并没有指定从什么时候开始算起的单调时间。所以有些系统取了Epoch时间,而有些系统(比如Linux)就取boot启动的时间。但也并不影响它的本身意义
- CLOCK_MONOTONIC_COARSE:一个更快但时间粒度更大(即时间精确度没那么高)的CLOCK_MONOTONIC。这个函数是Linux系统特有的,在2.6.32内核版本中首次出现
- CLOCK_BOOTTIME: 同CLOCK_MONOTONIC一样,只是当系统被挂起时,一样会计时(CLOCK_MONOTONIC不会)
- CLOCK_PROCESS_CPUTIME_ID: 进程使用了多少CPU时间。该进程的所有线程所使用的CPU时间都会被统计进来
- CLOCK_THREAD_CPUTIME_ID: 线程使用了多少CPU时间
clock_getres函数是用来获取对应时钟类型能够提供的时间精确度,res参数保存其精确度。在设置的时候,设置的时间值也应该是这个精确度的倍数。后面的休眠函数有些也会使用到上面这些时钟类型,休眠的时间也应该是精确度的倍数,否则由休眠函数截断取倍数。比如nanosleep函数。
函数clock_gettime是用来获取对应时钟的时间。函数clock_settime则是用来设置对应时钟的时间。有些时钟是不能被设置的。
休眠函数:
Linux提供了不同时间精确度的休眠函数。这些函数有些是在Linux高版本才出现的,有些是非线程安全的。下面就详细说明每一个休眠函数。
sleep:
#include <unistd.h> unsigned int sleep(unsigned int seconds);
从sleep的参数名称可以看到,该函数的休眠时间单位是秒。由于这个休眠函数可能会被外界的信号所打断,所以其有一个返回值,用来指明余下的休眠时间。如果没有被信号打断,而休眠了参数锁指定的时间,那么将返回0。
由于sleep函数可能是利用信号SIGALRM实现的,所以不应该在使用sleep函数的同时使用alarm函数。其实在《UNIX环境高级编程》里面,也看到书中有一个例子是怎么实现sleep函数,其中用到的方法就是alarm。
明显,如果sleep是用SIGALRM实现的话,那么就肯定不是线程安全的。
要注意的是,在Windows中提供了一个Sleep函数(S是大写的),它的时间单位是毫秒。
usleep:
#include <unistd.h> int usleep(useconds_t usec);//微秒
usleep休眠函数的时间粒度比较小,是微秒,足够了。并且该函数是线程安全的。
当这个休眠函数不被打断地休眠了指定的时间,将返回0。如果被信号打断了,将返回-1,并且将errno设置为EINTR。如果指定的参数不小于1000000也将返回-1,它认为休眠时间太长了,此时errno被设置为EINVAL。
如果要使用这个函数,那么glibc的版本要不小于2.12。可以用命令ls -al /lib/libc.so.6查看glibc的版本。
nanosleep:
#include <time.h> int nanosleep(const struct timespec *req, struct timespec *rem);//纳秒
nanosleep函数能够提供纳秒级的休眠时间。这是相当不错的。参数req指明要休眠的时间。如果成功休眠了指定的时间,将返回0。
如果休眠被一个信号给打断了,那么将返回-1,errno被赋值为EINTR。此时如果参数rem不为NULL的话,rem将被赋值成余下的休眠时间。有了rem,即使被信号打断,还是可以继续休眠,直到一开始指定的休眠时间。虽然这种方法理论上是可行,不过实际上可能出现时间漂移。比如说,你要休眠30ms,过了10ms后,被一个信号给打断了。此时rem将得到20ms的剩余值。如果CPU执行信号处理函数用了10ms,接着再休眠rem指明的时间。这样就出现时间漂移,一共休眠了40ms。可以用clock_nanosleep避免这个问题,因为它可以使用绝对时间。
req的tv_nsec 成员的值范围得是0到999999999。如果是其他值,该函数直接返回-1,errno被设置为EINVAL。
POSIX.1规定,nanosleep是使用CLOCK_REALTIME时钟来检测时间的(即检测是否超时)。然而Linux却是使用CLOCK_MONOTONIC时钟。看上去Linux的做法是明智的,因为前面已经说过用户是可以通过调用clock_settime函数修改CLOCK_REALTIME时钟的。不过,其实POSIX.1也规定了修改CLOCK_REALTIME时钟并不会影响到nanosleep函数的使用。明显要做到这一点,代码会复杂一些。使用CLOCK_MONOTONIC时钟就不用考虑这个问题。
clock_nanosleep:
//glibc2.17之前的版本在链接时需加上-lrt #include <time.h> int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *request, struct timespec *remain);
参数clock_id指明该函数使用哪个时钟检测有没有睡够。有CLOCK_REALTIME、 CLOCK_MONOTONIC 、CLOCK_PROCESS_CPUTIME_ID三种可选时钟。上面已经对这些时间进行了讲解这里就不多说了。
参数flags指明用的是不是绝对时间。如果该值为0,则使用的是一个时间段(即休眠的时长)。如果该参数为设置为TIMER_ABSTIME,那么就是使用绝对时间。
参数request指明休眠时间。有参数flags指明这是一个时间段还是绝对时间。
参数remain的作用和前面nanosleep函数的rem参数一样。当flags指明request是一个时间段,并且本函数被信号打断时,remain将得到余下的休眠时间。这个参数可以为NULL。
如果flags指定使用绝对时间,并且request指明的时间小于或者等于当前时间,那么clock_nanosleep函数将马上返回,不会进入休眠。
当clock_nanosleep成功休眠了指定的时间,将返回0。否则返回错误值,不设置errno变量。这点和其他的休眠函数不同。有下面这些错误值。
FAULT: request或者remain指向一个非法地址(即野指针)
EINTR:本函数被信号函数打断了
EINVAL:request的tv_nsec成员的范围不是在0到999999999,即它是一个非法值。当clock_id不是去上面说到的那三个值时,也将返回EINVAL。要注意:clock_nanosleep并不支持CLOCK_THREAD_CPUTIME_ID。
该函数要注意的一些问题:因为该函数可以使用CLOCK_REALTIME时钟并且该时钟可以被手动修改。这可能会影响clock_nanosleep函数。首先,如果flags指明的是一个时间段,那么clock_nanosleep和nanosleep函数一样,没有影响。
如果用户修改了CLOCK_REALTIME时钟的值,必然会对正在利用该时钟进行睡眠的clock_nanosleep产生影响。
该函数的使用,需要Linux内核版本不小于2.6,glibc版本不小于2.1
除了上面正规的休眠函数外,还可以利用其他的函数来实现休眠。
select:
#include <sys/select.h> #include <sys/types.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select函数的最后一个参数就是一个时间值。当然它是一个时间段,不是绝对时间。当我们需要将select用作一个休眠函数时,直接把前面四个参数设置为0即可。
如果休眠了指定的时间,该函数将返回0。如果中途被信号打断,那么将返回-1,errno被设置为EINTR。不同于其他的休眠函数,此时并没有一个可移植的方法知道剩余的休眠时间。虽然部分的Linux会修改timeout参数,得到剩余的休眠时间。
poll:
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
将前面的两个参数设为0,poll就成了一个休眠函数。参数timeout指定的休眠时间也是一个时间段,并且其时间单位是毫秒。
如果休眠了指定的时间,该函数返回0。如果中途被信号打断了,那么返回-1,errno被设置为EINTR。
休眠时间精确度:
由上面列出的一些休眠函数可以知道,Linux提供了各个时间精度的休眠函数:sleep(秒级)、poll(毫秒级)、usleep(微秒级)、nanosleep(纳秒级)。
休眠函数普遍存在的问题:
休眠函数是有一个问题,那就是不能真正准确休眠指定的时间。主要原因有二个。
- 系统提供的时间的精确度不够。比如,系统只能提供10ms的精确度,你却想休眠15ms。此时系统只能把真正休眠时间设置为10ms或者20ms。一般是向上取,即取20ms
- CPU调度问题。虽然线程的休眠时间到了,要唤醒了。但此时CPU还在忙着其他事,没有空来唤醒休眠线程。那么休眠线程真正休眠的时间也将是不准确的
参考:
http://stackoverflow.com/questions/471248/what-is-ultimately-a-time-t-typedef-to
http://man7.org/linux/man-pages/man2/clock_gettime.2.html
http://www.unix.com/man-page/POSIX/3posix/clock_settime/
http://stackoverflow.com/questions/14726401/starting-point-for-clock-monotonic?lq=1
http://man7.org/linux/man-pages/man3/usleep.3.html
http://man7.org/linux/man-pages/man3/sleep.3.html
http://man7.org/linux/man-pages/man2/nanosleep.2.html
http://man7.org/linux/man-pages/man2/clock_nanosleep.2.html
《UNIX网络编程卷一》