在 nginx 中,每个进程各自管理着自己的时间,而对于时间的管理则采用了缓存的方式,由于读取时间比更新时间频繁得多,而时间可能被信号处理函数或不同的线程(如果支持的话)更新,所以需要加锁,此时如果采用同一个变量来表示时间,则读取时间时也需要加锁,为了让读取操作免去加锁,nginx 使用了一个循环数组来缓存时间
变量声明
省略了一些相似的变量,比如描述 http 时间的字符串有好几种格式,所以有好几个数组
typedef struct {
time_t sec;
ngx_uint_t msec;
ngx_int_t gmtoff;
} ngx_time_t;
static ngx_uint_t slot;
static ngx_atomic_t ngx_time_lock;
#if !(NGX_WIN32)
static ngx_int_t cached_gmtoff;
#endif
volatile ngx_msec_t ngx_current_msec;
volatile ngx_time_t *ngx_cached_time;
volatile ngx_str_t ngx_cached_err_log_time;
volatile ngx_str_t ngx_cached_http_time;
static ngx_time_t cached_time[NGX_TIME_SLOTS];
static u_char cached_err_log_time[NGX_TIME_SLOTS][sizeof("1970/09/28 12:00:00")];
static u_char cached_http_time[NGX_TIME_SLOTS][sizeof("Mon, 28 Sep 1970 06:00:00 GMT")];
// 用于获取时间的函数
#define ngx_time() ngx_cached_time->sec
#define ngx_timeofday() (ngx_time_t *) ngx_cached_time
可以看到,用于获取时间的函数访问的是ngx_cached_time
,而cached_time
数组缓存了每次更新的值,slot
则表示最后一次更新时的下标,cached_gmtoff
表示与 GMT 相差的分钟数
更新时间
简单地说,更新时间就修改cached_time
中下一个slot
的值,并将ngx_cached_time
指向这个位置,当然其他描述时间的字符串也需要更新
void
ngx_time_update(void)
{
u_char *p0, *p1, *p2, *p3, *p4;
ngx_tm_t tm, gmt;
time_t sec;
ngx_uint_t msec;
ngx_time_t *tp;
struct timeval tv;
if (!ngx_trylock(&ngx_time_lock)) {
return;
}
ngx_gettimeofday(&tv);
sec = tv.tv_sec;
msec = tv.tv_usec / 1000;
ngx_current_msec = (ngx_msec_t) sec * 1000 + msec;
tp = &cached_time[slot];
if (tp->sec == sec) { // sec 相同
tp->msec = msec;
ngx_unlock(&ngx_time_lock);
return;
}
if (slot == NGX_TIME_SLOTS - 1) {
slot = 0;
} else {
slot++;
}
tp = &cached_time[slot];
tp->sec = sec;
tp->msec = msec;
ngx_gmtime(sec, &gmt);
p0 = &cached_http_time[slot][0];
(void) ngx_sprintf(p0, "%s, %02d %s %4d %02d:%02d:%02d GMT",
week[gmt.ngx_tm_wday], gmt.ngx_tm_mday,
months[gmt.ngx_tm_mon - 1], gmt.ngx_tm_year,
gmt.ngx_tm_hour, gmt.ngx_tm_min, gmt.ngx_tm_sec);
#if (NGX_HAVE_GETTIMEZONE)
tp->gmtoff = ngx_gettimezone();
ngx_gmtime(sec + tp->gmtoff * 60, &tm);
#elif (NGX_HAVE_GMTOFF)
ngx_localtime(sec, &tm);
cached_gmtoff = (ngx_int_t) (tm.ngx_tm_gmtoff / 60);
tp->gmtoff = cached_gmtoff;
#else
ngx_localtime(sec, &tm);
cached_gmtoff = ngx_timezone(tm.ngx_tm_isdst);
tp->gmtoff = cached_gmtoff;
#endif
p1 = &cached_err_log_time[slot][0];
(void) ngx_sprintf(p1, "%4d/%02d/%02d %02d:%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec);
p2 = &cached_http_log_time[slot][0];
(void) ngx_sprintf(p2, "%02d/%s/%d:%02d:%02d:%02d %c%02i%02i",
tm.ngx_tm_mday, months[tm.ngx_tm_mon - 1],
tm.ngx_tm_year, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec,
tp->gmtoff < 0 ? '-' : '+',
ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));
p3 = &cached_http_log_iso8601[slot][0];
(void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d%c%02i:%02i",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec,
tp->gmtoff < 0 ? '-' : '+',
ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));
p4 = &cached_syslog_time[slot][0];
(void) ngx_sprintf(p4, "%s %2d %02d:%02d:%02d",
months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday,
tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec);
ngx_memory_barrier(); // 防止编译器修改执行顺序
ngx_cached_time = tp;
ngx_cached_http_time.data = p0;
ngx_cached_err_log_time.data = p1;
ngx_cached_http_log_time.data = p2;
ngx_cached_http_log_iso8601.data = p3;
ngx_cached_syslog_time.data = p4;
ngx_unlock(&ngx_time_lock);
}
首先判断秒数是否相同,相同的话只需简单更新毫秒的值即可,否则需要将值更新到下一个位置,因为秒数的改变可能引起年月日的变化,也就是需要修改字符串内的时间值,而这些字符串不关心毫秒的值
接下来是关于时区的判断,我的系统中没有定义NGX_HAVE_GETTIMEZONE
这个特性测试宏,所以直接看NGX_HAVE_GMTOFF
,这里调用了ngx_localtime
函数,然后更新了cached_gmtoff
的值,ngx_localtime
的定义如下
void
ngx_localtime(time_t s, ngx_tm_t *tm)
{
#if (NGX_HAVE_LOCALTIME_R)
(void) localtime_r(&s, tm);
#else
ngx_tm_t *t;
t = localtime(&s);
*tm = *t;
#endif
tm->ngx_tm_mon++;
tm->ngx_tm_year += 1900;
}
结果是在 tm
中存储本地时间,也就是带时区偏移的,这里最后还处理了月份和年份的值,可以看下 man 中的描述
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 - 1900 */
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */
};
因为系统中月份的范围是 0 到 11,所以需要递增一次,但是年份为什么需要加上 1900 呢?查了一下资料发现,以前人们为了节省内存而使用两位数表示年份,比如用 60 表示 1960 年,当然,到了 2000 年则引发了著名的千年虫问题
时间更新函数最后更新了几个cached_time
的值,需要注意的是这里调用了之前说过的内存屏障,想象一下,如果编译器为了优化性能而修改了执行顺序,比如改成如下形式
ngx_cached_http_time.data = p0;
p1 = &cached_err_log_time[slot][0];
(void) ngx_sprintf(p1, "%4d/%02d/%02d %02d:%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec);
ngx_cached_err_log_time.data = p1;
由于ngx_cached_http_time
与cached_err_log_time
的赋值语句中间间隔较多语句,可能引发的问题是,两者时间被读取时产生不一致,而将赋值操作全部留到最后则减少了不一致的可能性
信号安全的时间更新函数
void
ngx_time_sigsafe_update(void)
{
u_char *p, *p2;
ngx_tm_t tm;
time_t sec;
ngx_time_t *tp;
struct timeval tv;
if (!ngx_trylock(&ngx_time_lock)) { // 如果信号引发在更新时间时,则这里获取不到锁
return;
}
ngx_gettimeofday(&tv);
sec = tv.tv_sec;
tp = &cached_time[slot];
if (tp->sec == sec) {
ngx_unlock(&ngx_time_lock);
return;
}
if (slot == NGX_TIME_SLOTS - 1) {
slot = 0;
} else {
slot++;
}
tp = &cached_time[slot];
tp->sec = 0;
ngx_gmtime(sec + cached_gmtoff * 60, &tm);
p = &cached_err_log_time[slot][0];
(void) ngx_sprintf(p, "%4d/%02d/%02d %02d:%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec);
p2 = &cached_syslog_time[slot][0];
(void) ngx_sprintf(p2, "%s %2d %02d:%02d:%02d",
months[tm.ngx_tm_mon - 1], tm.ngx_tm_mday,
tm.ngx_tm_hour, tm.ngx_tm_min, tm.ngx_tm_sec);
ngx_memory_barrier();
ngx_cached_err_log_time.data = p;
ngx_cached_syslog_time.data = p2;
ngx_unlock(&ngx_time_lock);
}
大体上和之前那个函数差不多,有两个需要注意的点,一是为了保证下次更新时间时会使用下一个slot
而将tp->sec
的值设置为了 0,二是可这里只更新了两个log
相关的值,原因应该是这个函数只在信号处理函数ngx_signal_handler
里调用,而信号处理函数内涉及较多log
操作
那么,为什么这个函数称为信号安全呢?关键点在于获取年月日时,这里没有调用ngx_localtime
,因为库函数localtime
和他的可重入版本localtime_r
都不是异步信号安全的,那么要怎么才能获取到时区信息呢?这就需要用到之前更新的cached_gmtoff
变量,里面缓存了与 GMT 相差的分钟数,据此可以算出本地时间