【nginx流程分析之时间和并发控制】
继承上一篇 nginx流程分析,首先进行分析nginx的ngx_time_init,然后先看在代码中的位置
好的我们接下来慢慢分析
ngx_time_init
我们先来看看这个具体方法,
这个方法前面都是比较好理解的,分别初始化ngx_cached_err_log_time,ngx_cached_http_time,ngx_cached_http_log_time,ngx_cached_http_log_iso8601和ngx_cached_syslog_time的长度,当然这些也是这些日志的格式。
然后就是初始化了ngx_cached_time,取的是cached_time的第一个地址,然后看一下cached_time 的定义,
然后NGX_TIME_SLOTS是64,说明cached_time是一个大小为64的数组,类型为ngx_time_t。
然后我们接下来看ngx_time_update这个方法
ngx_time_update
接下来我们来看这个方法,然后因为这个方法比较长我们一段一段的来看,先看一部分:
首先是定义了几个变量,这些都是时间的一些变量,然后就是ngx_trylock这个方法。我们来看一下。
开始加锁 ngx_trylock
加锁,其实不严谨,因为是原子操作,ngx_trylock,这个其实就是c中的原子操作 ,我们来看一下实现:
然后再看看ngx_atomic_cmp_set这个方法的实现,
可以看出来一个原子操作,关键字atomic,然后我们再看一下ngx_trylock(&ngx_time_lock))中
ngx_time_lock这个变量,
可以看到 关键字volatile进行了修饰,这个说明对于ngx_time_lock这个变量,强制系统每次都是内存中读取最新的值,还不会去寄存器中或者cpu缓存中去读取,同时不会受到编译器优化的影响,关于c语言的volatile,推荐文章C语言再学习 – 关键字volatile ,其实会有代码说明。
综上所述,ngx_trylock方法同时配合原子操作和volatile,判断是否有其他线程在操作这个方法。
ngx_gettimeofday
接下来继续看,就是ngx_gettimeofday,点进去看一下具体的实现:
可以看到ngx_gettimeofday这个c语言中自带的gettimeofday方法,这个方法的具体作用就是获取当前的时间存到了类型为timeval 名称为 tv的结构体中,然后我们来看一下timeval这个结构体定义:
struct timeval {
long tv_sec; // 秒数
long tv_usec; //微秒数
}
就是把当前时间存在了这个结构体中,然后接下来,就是把当前的时间的秒存在了sec这个变量中,然后通过将微秒数除以一千得到了毫秒数。
ngx_monotonic_time
然后就是ngx_monotonic_time这个方法,首先还是来看一下定义
static ngx_msec_t
ngx_monotonic_time(time_t sec, ngx_uint_t msec)
{
#if (NGX_HAVE_CLOCK_MONOTONIC)
struct timespec ts;
#if defined(CLOCK_MONOTONIC_FAST)
clock_gettime(CLOCK_MONOTONIC_FAST, &ts);
#elif defined(CLOCK_MONOTONIC_COARSE)
clock_gettime(CLOCK_MONOTONIC_COARSE, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
sec = ts.tv_sec;
msec = ts.tv_nsec / 1000000;
#endif
return (ngx_msec_t) sec * 1000 + msec;
}
然后还是来分析一下,我们把宏去掉其实就是下面的代码
static ngx_msec_t
ngx_monotonic_time(time_t sec, ngx_uint_t msec)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
sec = ts.tv_sec;
msec = ts.tv_nsec / 1000000;
return (ngx_msec_t) sec * 1000 + msec;
}
然后我们看一下clock_gettime(CLOCK_MONOTONIC, &ts)这个方法,这个方法其实就是获取当前系统启动这一刻起开始计时的时间,并且不受到用户设置的影响,其实作用和cat /proc/uptime一样,看一下我们当前系统,
然后将值存到了
struct timespec
{
time_t tv_sec; /* 秒*/
long tv_nsec; /* 纳秒*/
};
然后就是将tv_nsec除以1000000,得到了当前毫秒数加上秒数,然后再进行了返回并且存到了ngx_current_msec这个变量中。
初始化 slot
然后就是初始化slot,然后一下代码
if (slot == NGX_TIME_SLOTS - 1) {
slot = 0;
} else {
slot++;
}
tp = &cached_time[slot];
tp->sec = sec;
tp->msec = msec;
可以看到,首先是判断slot是不是已经和当前最大值一样了,如果不是那么就进行+1操作,这个也就是意味着每次都会对slot其实是不断更新的一个过程。然后就是把缓存的时间存到了cached_time这个数组里面。
ngx_gmtime
首先看一下这个方法具体操作:
虽然nginx没有给出注释,我们也可以看出,是获取时间然后存到了gmt,然后gmt这个类型是ngx_tm_t,然后我们看一下ngx_tm_t的类型定义,
struct tm {
int tm_sec; /* seconds after the minute [0-60] */
int tm_min; /* minutes after the hour [0-59] */
int tm_hour; /* hours since midnight [0-23] */
int tm_mday; /* day of the month [1-31] */
int tm_mon; /* months since January [0-11] */
int tm_year; /* years since 1900 */
int tm_wday; /* days since Sunday [0-6] */
int tm_yday; /* days since January 1 [0-365] */
int tm_isdst; /* Daylight Savings Time flag */
long tm_gmtoff; /* offset from UTC in seconds */
char *tm_zone; /* timezone abbreviation */
};
这个结构体注释的还是很好的,可以看出来,分别给出了时分秒,日期,月,年等等。
当然我来看看一下这个方法
void
ngx_gmtime(time_t t, ngx_tm_t *tp)
{
ngx_int_t yday;
ngx_uint_t sec, min, hour, mday, mon, year, wday, days, leap;
/* the calculation is valid for positive time_t only */
if (t < 0) {
t = 0;
}
days = t / 86400;
sec = t % 86400;
/*
* no more than 4 year digits supported,
* truncate to December 31, 9999, 23:59:59
*/
if (days > 2932896) {
days = 2932896;
sec = 86399;
}
/* January 1, 1970 was Thursday */
wday = (4 + days) % 7;
hour = sec / 3600;
sec %= 3600;
min = sec / 60;
sec %= 60;
/*
* the algorithm based on Gauss' formula,
* see src/core/ngx_parse_time.c
*/
/* days since March 1, 1 BC */
days = days - (31 + 28) + 719527;
/*
* The "days" should be adjusted to 1 only, however, some March 1st's go
* to previous year, so we adjust them to 2. This causes also shift of the
* last February days to next year, but we catch the case when "yday"
* becomes negative.
*/
year = (days + 2) * 400 / (365 * 400 + 100 - 4 + 1);
yday = days - (365 * year + year / 4 - year / 100 + year / 400);
if (yday < 0) {
leap = (year % 4 == 0) && (year % 100 || (year % 400 == 0));
yday = 365 + leap + yday;
year--;
}
/*
* The empirical formula that maps "yday" to month.
* There are at least 10 variants, some of them are:
* mon = (yday + 31) * 15 / 459
* mon = (yday + 31) * 17 / 520
* mon = (yday + 31) * 20 / 612
*/
mon = (yday + 31) * 10 / 306;
/* the Gauss' formula that evaluates days before the month */
mday = yday - (367 * mon / 12 - 30) + 1;
if (yday >= 306) {
year++;
mon -= 10;
/*
* there is no "yday" in Win32 SYSTEMTIME
*
* yday -= 306;
*/
} else {
mon += 2;
/*
* there is no "yday" in Win32 SYSTEMTIME
*
* yday += 31 + 28 + leap;
*/
}
tp->ngx_tm_sec = (ngx_tm_sec_t) sec;
tp->ngx_tm_min = (ngx_tm_min_t) min;
tp->ngx_tm_hour = (ngx_tm_hour_t) hour;
tp->ngx_tm_mday = (ngx_tm_mday_t) mday;
tp->ngx_tm_mon = (ngx_tm_mon_t) mon;
tp->ngx_tm_year = (ngx_tm_year_t) year;
tp->ngx_tm_wday = (ngx_tm_wday_t) wday;
}
当然这个方法有点长,我们看核心就好了,最后就是把时分秒等等参数塞到了tp这个结构体中。
将时间写入
上面时间分析完成,接下来就是将时间写入,我们先看一下代码:
我们先看一下cached_http_time这个结构体,
static u_char cached_http_time[NGX_TIME_SLOTS]
[sizeof("Mon, 28 Sep 1970 06:00:00 GMT")];
可以看出来这个是一个二维数组,并且值是u_char的类型。这个二维数组,第一维是64,第二位是sizeof(“Mon, 28 Sep 1970 06:00:00 GMT”)就是30,然后是一个字节,其实就是这个二位数组就等于是一维数组,然后第二维是连续的30个字节的内存。
然后看一下ngx_sprintf这个方法
u_char * ngx_cdecl
ngx_sprintf(u_char *buf, const char *fmt, ...)
{
u_char *p;
va_list args;
va_start(args, fmt);
p = ngx_vslprintf(buf, (void *) -1, fmt, args);
va_end(args);
return p;
}
然后ngx_vslprintf其实和c语言中的vsnprintf的使用是一样的,但是估计写nginx的大佬觉得vsnprintf性能不够好,或者为了定制化的需求,自己单独写了一下。因为比较多我截了个图,简单说一下,有兴趣的同学可以了解一下vsnprintf哈:
其实一个一个判断,进行填充。我们看一下p0的输出这个值:Sat, 05 Mar 2022 13:05:50 GMT
其实和上面的Mon, 28 Sep 1970 06:00:00 GMT,类型是一样的,不得不感慨,nginx真的一个字节都要省。
然后我们看一下接下来的p1,p2,p3,p4的操作
其实道理是一样的,这里就是重复描述了。
ngx_memory_barrier
可以看到这里面出现了ngx_memory_barrier,然后看一下定义是OSMemoryBarrier().这里面解释一下,OSMemoryBarrier这个从名称上面可以看出来内存屏障的意思,接触过go和java的gc原理的同学会有一些熟悉,其实就是保证前面的指令编辑器在编译的时候,不会乱序执行,因为在编译器在编译代码的时候,往往不会按照程序员写的逻辑去执行,往往会有一些优化,在单线程的逻辑中可能没问题,但是在多线程的过程可能就有问题了。而内存屏障往往和多线程中,原子操作结合在一起使用。
内存屏障是一种屏障和指令类,可以让CPU或编译器强制将barrier之前和之后的内存操作分开。CPU采用了一些可能导致乱序执行的性能优化。在单个线程的执行中,内存操作的顺序一般是悄无声息的,但是在并发编程和设备驱动程序中就可能出现一些不可预知的行为,除非我们小心地去控制。排序约束的特性是依赖于硬件的,并由架构的内存顺序模型来定义。一些架构定义了多种barrier来执行不同的顺序约束。
概括一下,OSMemoryBarrier()函数就是用来设置内存屏障,然后保证cpu每次不对读取cpu的缓存,每次都会从内存中读取,并且,保证OSMemoryBarrier()前面和后面的代码在编译器在编译的时候,是真正分开的,不会乱序执行
赋值
然后再看看,最后的实现:
其实这里就是进行了赋值,没有什么特殊的
释放锁 ngx_unlock
我们来看一下实现
#define ngx_unlock(lock) *(lock) = 0
其实就是把lock这个变量赋值成0,这样别的进程就可以调用了,至此,分析结束了。