Ubuntu 下 nginx-1.24.0 源码分析 - ngx_time_update函数-CSDN博客
定义在 src\core\ngx_times.c
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_monotonic_time(sec, msec);tp = &cached_time[slot];if (tp->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;#elsengx_localtime(sec, &tm);cached_gmtoff = ngx_timezone(tm.ngx_tm_isdst);tp->gmtoff = cached_gmtoff;#endifp1 = &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);
}
if (!ngx_trylock(&ngx_time_lock)) {return;}
尝试获取一个互斥锁,失败时直接返回
ngx_trylock是一个非阻塞的锁尝试函数如果
ngx_time_lock已被其他线程持有,ngx_trylock会立即返回失败,而不是阻塞等待Ubuntu 下 nginx-1.24.0 源码分析 - ngx_trylock函数-CSDN博客
ngx_gettimeofday(&tv);sec = tv.tv_sec;msec = tv.tv_usec / 1000;
ngx_gettimeofday 函数
定义在 src/os/unix/ngx_time.h 中:#define ngx_gettimeofday(tp) (void) gettimeofday(tp, NULL);
struct timeval *tv是一个指向
timeval结构体的指针,用于存储当前时间
struct timezone *tz是一个指向
timezone结构体的指针,用于存储时区信息
timezone结构体定义如下struct timezone {int tz_minuteswest; // 西侧相对于 UTC 的分钟差int tz_dsttime; // 夏令时修正类型(已废弃,通常设为 NULL) };
在现代程序中,
tz参数通常设置为NULL,因为时区信息可以通过其他更可靠的方式获取(例如通过环境变量或系统调用)返回值
- 如果调用成功,
gettimeofday返回0。- 如果调用失败,返回
-1,并设置errno来指示错误原因
获取秒(
tv_sec)和微秒(tv_usec)
sec = tv.tv_sec;从
struct timeval结构体中提取出秒数部分,赋值给变量sec
msec = tv.tv_usec / 1000;从
struct timeval结构体中提取出微秒部分,并将其转换为毫秒,赋值给变量msec
此时
sec=1743410554
msec=267
ngx_current_msec = ngx_monotonic_time(sec, msec);
ngx_current_msec 是 Nginx 内部维护的一个全局变量,用于存储当前的时间值(以毫秒为单位)
ngx_monotonic_time 是 Nginx 提供的一个函数,用于将时间值(秒和毫秒)转换为单调递增的时间值(monotonic time)
这行代码的意图是将当前的时间值(sec 和 msec)转换为单调时间值,并更新到 ngx_current_msec 中
单调时间值通常表示自某个固定起点(例如系统启动时间)以来经过的时间,而不是绝对的日历时间
不会因为系统时钟调整(如 NTP 时间同步、人为更改系统时间等)而回退或突然变小
ngx_monotonic_time-CSDN博客
tp = &cached_time[slot];
获取对时间缓存数组
cached_time中特定位置的引用,并将其赋值给指针tp
此时
slot=0
if (tp->sec == sec) {tp->msec = msec;ngx_unlock(&ngx_time_lock);return;}
检查缓存的时间(
tp->sec)是否与当前获取的时间(sec)一致,是否仍处于同一秒内此时
tp->sec=0 (还未设置)
sec=1743425782条件不成立
if (slot == NGX_TIME_SLOTS - 1) {slot = 0;} else {slot++;}
检查当前 slot 是否已经到达缓存数组 cached_time 的最后一个位置
NGX_TIME_SLOTS 是数组的大小,因此最后一个索引是 NGX_TIME_SLOTS - 1
如果当前 slot 已经到达数组末尾,则将 slot 重置为 0,表示循环到数组的起始位置。
如果当前 slot 尚未到达数组末尾,则将 slot 增加 1,继续遍历数组。
此时
slot=0
NGX_TIME_SLOTS=64进入 else
执行后
slot=1
tp = &cached_time[slot];tp->sec = sec;tp->msec = msec;
获取下一个元素,更新它的时间
ngx_gmtime(sec, &gmt);
将秒数时间值(sec)转换为人类可读的 GMT 时间格式
sec(一个 time_t 类型的时间值,表示自 1970 年 1 月 1 日 00:00:00 UTC 以来的秒数)转换为 GMT 时间,并将结果存储在 gmt 结构体中。
ngx_gmtime 用于将时间值转换为 GMT 时间。
ngx_gmtime-CSDN博客
p0 = &cached_http_time[slot][0];
获取当前时间槽(slot)对应的 HTTP 时间缓存字符串的起始地址
cached_http_time 是一个二维数组,每个 slot 对应一个预分配的字符数组,用于存储格式化后的 HTTP 时间字符串。
slot 是一个循环索引,通过轮换槽位实现无锁更新:写入新时间到下一个槽位后,原子切换全局指针,避免读写竞争。
为格式化的时间字符串(如 HTTP 报文头中的时间信息)准备一个存储空间。
通过直接操作缓冲区地址,避免频繁的内存分配和释放,提高效率
此时
slot=1
p0 所指向的还是空串
(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);
ngx_sprintf函数将格式化的时间字符串存储到p0指向的缓冲区中格式:"%s, %02d %s %4d %02d:%02d:%02d GMT"
分解:
%s:星期缩写(如"Mon")
%02d:两位数的日期(不足补零)
%s:月份缩写(如"Jan")
%4d:四位数的年份
%02d:%02d:%02d:时:分:秒(两位数格式)
GMT:固定时区标识
week[gmt.ngx_tm_wday]获取星期几的缩写
gmt.ngx_tm_wday:0-6(0=Sunday)
week[]:预定义的星期字符串数组src\core\ngx_times.c 中:
static char *week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
months[gmt.ngx_tm_mon - 1]
获取月份缩写gmt.ngx_tm_mon:1-12
months[]:预定义的月份字符串数组(如["Jan", "Feb", ...])
-1:调整数组索引(从0开始)
src\core\ngx_times.c 中:
static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
ngx_sprintf-CSDN博客
p0=Wed, 02 Apr 2025 15:23:19 GMT
#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;#elsengx_localtime(sec, &tm);cached_gmtoff = ngx_timezone(tm.ngx_tm_isdst);tp->gmtoff = cached_gmtoff;#endif
NGX_HAVE_GETTIMEZONE 未定义
NGX_HAVE_GMTOFF=1
ngx_localtime(sec, &tm);cached_gmtoff = (ngx_int_t) (tm.ngx_tm_gmtoff / 60);tp->gmtoff = cached_gmtoff;这部分代码成立
ngx_localtime(sec, &tm);
将秒级时间戳转换为本地时间结构体
输入 sec 是自 Epoch 的秒数(UTC 时间)
输出 tm 是包含本地时间的结构体(含时区信息)
ngx_localtime
ngx_localtime-CSDN博客
cached_gmtoff = (ngx_int_t)(tm.ngx_tm_gmtoff / 60);
计算并缓存时区偏移(以分钟为单位)
ngx_tm_gmtoff 成员是以秒为单位的时区偏移
除以 60 转换为分钟
强制转换为 ngx_int_t
tp->gmtoff = cached_gmtoff;
将时区偏移值存储到时间结构体
tp 指向当前时间槽的缓存时间结构体
gmtoff 字段用于存储时区偏移
代码段整体逻辑:
通过 ngx_localtime 获取本地时间(含时区信息)
从 tm 结构体提取秒级时区偏移,转换为分钟单位
将计算结果缓存到全局变量和当前时间结构体
p1 = &cached_err_log_time[slot][0];
将指针 p1 指向二维数组 cached_err_log_time 中当前时间槽(slot)对应的字符串缓存的首地址
cached_err_log_time 是一个预分配的二维数组,用于存储不同时间槽的错误日志时间字符串。
slot 是当前时间槽的索引
[slot][0] 表示取第 slot 个时间槽的字符串首地址
cached_err_log_time是一个缓存变量,用于存储格式化后的错误日志时间字符串。它的主要作用是优化错误日志记录的性能 ,避免每次记录错误日志时都重新格式化时间字符串。
确保不同线程或请求之间不会相互干扰
(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);
将时间数据格式化为字符串并写入 p1 指向的内存
格式字符串 "%4d/%02d/%02d %02d:%02d:%02d":
%4d:4 位 年份(如 2023)
/%02d:2 位 月份(不足补零,如 03 表示三月)
/%02d:2 位 日期(不足补零,如 05)
%02d:%02d:%02d:2 位 小时、分钟、秒(不足补零)
时间字段:
tm.ngx_tm_year:四位数的年份(如 2023)。
tm.ngx_tm_mon:月份
tm.ngx_tm_mday:日期
tm.ngx_tm_hour:小时
tm.ngx_tm_min:分钟
tm.ngx_tm_sec:秒
输出示例:2023/10/05 15:30:45
意图:
生成一个易读的本地时间字符串,用于错误日志的时间戳。
缓存此字符串,避免每次记录日志时重复格式化时间,提升性能。
p1=2025/04/03 13:12:55
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));
获取当前时间槽(slot)对应的缓存位置
cached_http_log_time 是一个二维数组,每个槽(slot)存储一个时间字符
p2 指向当前槽的首地址,准备写入新的时间字符串
将格式化后的时间字符串写入 p2 指向的缓存
%02d:两位数的日期(不足补零,如 02)。
/%s/:缩写的月份(如 Oct)。
%d:四位数的年份(如 2023)。
%02d:%02d:%02d:两位数的时、分、秒(如 14:05:09)。
%c%02i%02i:时区符号(+或-)和两位数的时区偏移(如 +0800)
tm.ngx_tm_mday:当前日期(1-31)。
months[tm.ngx_tm_mon - 1]:月份缩写。
months 是预定义的月份缩写数组(如 Jan, Feb 等)。
时间部分tm.ngx_tm_year:四位数的年份(如 2023)。
tm.ngx_tm_hour:小时(0-23)。
tm.ngx_tm_min:分钟(0-59)。
tm.ngx_tm_sec:秒(0-59)。
时区部分tp->gmtoff < 0 ? '-' : '+':时区符号(负偏移用 -,正偏移用 +)。
ngx_abs(tp->gmtoff / 60):时区偏移的小时部分(绝对值)。
ngx_abs(tp->gmtoff % 60):时区偏移的分钟部分(绝对值)。
tp->gmtoff 以分钟为单位的时区偏移
看一下对
%c%02i%02i
的处理
ngx_vslprintf
ngx_vslprintf-CSDN博客
p2=03/Apr/2025:14:53:47 +0800
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));
p3 = &cached_http_log_iso8601[slot][0];
cached_http_log_iso8601 是一个二维数组,每个槽位(slot)存储一个预生成的 ISO 8601 时间字符串。
slot 表示当前活跃的缓存槽位,通过轮换槽位避免多线程/异步环境下的读写冲突。
p3 指向当前槽位的起始地址,后续代码将时间字符串写入此处
(void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d%c%02i:%02i", ...);
作用:将当前时间和时区信息格式化为 ISO 8601 字符串,并写入 p3 指向的缓存。
参数解析:
格式字符串:"%4d-%02d-%02dT%02d:%02d:%02d%c%02i:%02i"
%4d:4 位 年份(如 2023)。
%02d:2 位 月份(如 01 表示 1 月)。
%02d:2 位 日期(如 05)。
T:字面量字符,分隔日期和时间。
%02d:2 位 小时(00-23)。
%02d:2 位 分钟(00-59)。
%02d:2 位 秒(00-59)。
%c:时区符号(+ 或 -)。
%02i:%02i:时区偏移的小时和分钟部分(如 +08:00)。
实际参数:
tm.ngx_tm_year:年份(如 2023)。
tm.ngx_tm_mon:月份(1-12,直接使用无需调整)。
tm.ngx_tm_mday:日期(1-31)。
tm.ngx_tm_hour:小时(0-23)。
tm.ngx_tm_min:分钟(0-59)。
tm.ngx_tm_sec:秒(0-59)。
tp->gmtoff < 0 ? '-' : '+':时区符号(负值为 -,正值为 +)。
ngx_abs(tp->gmtoff / 60):时区偏移的小时部分(绝对值)。
ngx_abs(tp->gmtoff % 60):时区偏移的分钟部分(绝对值)。
p3=2025-04-03T14:53:47+08:00
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);
p4=Apr 3 14:53:47
ngx_memory_barrier();
ngx_memory_barrier() 是内存屏障(Memory Barrier),用于解决多线程环境下的 内存可见性 和 指令重排序 问题:
内存可见性:确保当前线程对内存的修改对其他线程立即可见。
指令重排序:防止编译器或 CPU 将屏障前后的指令乱序执行。
在 Nginx 这种高并发场景中,时间更新可能被多个线程(如 Worker 进程)竞争访问,内存屏障能确保全局时间变量的更新是原子且一致的。ngx_memory_barrier-CSDN博客
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_unlock 函数src/os/unix/ngx_atomic.h
#define ngx_unlock(lock) *(lock) = 0将 lock 指向的内存值赋 0,释放之前获取的互斥锁
