ngx_vslprintf 声明
ngx_vslprintf的声明在
ngx_string.h 中:
u_char *ngx_vslprintf(u_char *buf, u_char *last, const char *fmt, va_list args);
ngx_vslprintf 实现
ngx_string.c 中 ngx_vslprintf 函数的定义
u_char *
ngx_vslprintf(u_char *buf, u_char *last, const char *fmt, va_list args)
{u_char *p, zero;int d;double f;size_t slen;int64_t i64;uint64_t ui64, frac;ngx_msec_t ms;ngx_uint_t width, sign, hex, max_width, frac_width, scale, n;ngx_str_t *v;ngx_variable_value_t *vv;while (*fmt && buf < last) {/** "buf < last" means that we could copy at least one character:* the plain character, "%%", "%c", and minus without the checking*/if (*fmt == '%') {i64 = 0;ui64 = 0;zero = (u_char) ((*++fmt == '0') ? '0' : ' ');width = 0;sign = 1;hex = 0;max_width = 0;frac_width = 0;slen = (size_t) -1;while (*fmt >= '0' && *fmt <= '9') {width = width * 10 + (*fmt++ - '0');}for ( ;; ) {switch (*fmt) {case 'u':sign = 0;fmt++;continue;case 'm':max_width = 1;fmt++;continue;case 'X':hex = 2;sign = 0;fmt++;continue;case 'x':hex = 1;sign = 0;fmt++;continue;case '.':fmt++;while (*fmt >= '0' && *fmt <= '9') {frac_width = frac_width * 10 + (*fmt++ - '0');}break;case '*':slen = va_arg(args, size_t);fmt++;continue;default:break;}break;}switch (*fmt) {case 'V':v = va_arg(args, ngx_str_t *);buf = ngx_sprintf_str(buf, last, v->data, v->len, hex);fmt++;continue;case 'v':vv = va_arg(args, ngx_variable_value_t *);buf = ngx_sprintf_str(buf, last, vv->data, vv->len, hex);fmt++;continue;case 's':p = va_arg(args, u_char *);buf = ngx_sprintf_str(buf, last, p, slen, hex);fmt++;continue;case 'O':i64 = (int64_t) va_arg(args, off_t);sign = 1;break;case 'P':i64 = (int64_t) va_arg(args, ngx_pid_t);sign = 1;break;case 'T':i64 = (int64_t) va_arg(args, time_t);sign = 1;break;case 'M':ms = (ngx_msec_t) va_arg(args, ngx_msec_t);if ((ngx_msec_int_t) ms == -1) {sign = 1;i64 = -1;} else {sign = 0;ui64 = (uint64_t) ms;}break;case 'z':if (sign) {i64 = (int64_t) va_arg(args, ssize_t);} else {ui64 = (uint64_t) va_arg(args, size_t);}break;case 'i':if (sign) {i64 = (int64_t) va_arg(args, ngx_int_t);} else {ui64 = (uint64_t) va_arg(args, ngx_uint_t);}if (max_width) {width = NGX_INT_T_LEN;}break;case 'd':if (sign) {i64 = (int64_t) va_arg(args, int);} else {ui64 = (uint64_t) va_arg(args, u_int);}break;case 'l':if (sign) {i64 = (int64_t) va_arg(args, long);} else {ui64 = (uint64_t) va_arg(args, u_long);}break;case 'D':if (sign) {i64 = (int64_t) va_arg(args, int32_t);} else {ui64 = (uint64_t) va_arg(args, uint32_t);}break;case 'L':if (sign) {i64 = va_arg(args, int64_t);} else {ui64 = va_arg(args, uint64_t);}break;case 'A':if (sign) {i64 = (int64_t) va_arg(args, ngx_atomic_int_t);} else {ui64 = (uint64_t) va_arg(args, ngx_atomic_uint_t);}if (max_width) {width = NGX_ATOMIC_T_LEN;}break;case 'f':f = va_arg(args, double);if (f < 0) {*buf++ = '-';f = -f;}ui64 = (int64_t) f;frac = 0;if (frac_width) {scale = 1;for (n = frac_width; n; n--) {scale *= 10;}frac = (uint64_t) ((f - (double) ui64) * scale + 0.5);if (frac == scale) {ui64++;frac = 0;}}buf = ngx_sprintf_num(buf, last, ui64, zero, 0, width);if (frac_width) {if (buf < last) {*buf++ = '.';}buf = ngx_sprintf_num(buf, last, frac, '0', 0, frac_width);}fmt++;continue;#if !(NGX_WIN32)case 'r':i64 = (int64_t) va_arg(args, rlim_t);sign = 1;break;
#endifcase 'p':ui64 = (uintptr_t) va_arg(args, void *);hex = 2;sign = 0;zero = '0';width = 2 * sizeof(void *);break;case 'c':d = va_arg(args, int);*buf++ = (u_char) (d & 0xff);fmt++;continue;case 'Z':*buf++ = '\0';fmt++;continue;case 'N':
#if (NGX_WIN32)*buf++ = CR;if (buf < last) {*buf++ = LF;}
#else*buf++ = LF;
#endiffmt++;continue;case '%':*buf++ = '%';fmt++;continue;default:*buf++ = *fmt++;continue;}if (sign) {if (i64 < 0) {*buf++ = '-';ui64 = (uint64_t) -i64;} else {ui64 = (uint64_t) i64;}}buf = ngx_sprintf_num(buf, last, ui64, zero, hex, width);fmt++;} else {*buf++ = *fmt++;}}return buf;
}
作用:
这个函数是 Nginx 源码中的一个关键函数,用于格式化字符串。
它类似于标准 C 语言中的 sprintf
函数,但更加灵活和强大,能够处理多种数据类型,并且可以控制输出的格式。
它在 Nginx 中用于生成日志信息、错误信息等,是 Nginx 内部进行字符串处理的重要工具。
它的主要功能是根据提供的格式字符串和参数,生成格式化的字符串,并将其存储在指定的缓冲区中。
函数参数:
u_char *buf
:目标缓冲区的起始地址,用于存储格式化后的字符串。
u_char *last
:目标缓冲区的结束地址,用于限制输出的长度,防止缓冲区溢出。
const char *fmt
:格式字符串,定义了如何格式化输入的参数。
va_list args
:可变参数列表,包含需要格式化的数据。
返回值:
函数的返回值是 u_char *
类型,它指向目标缓冲区 buf
的当前写入位置。
这个返回值非常重要,它是缓冲区中下一个可以写入的字符位置
这使得函数可以被多次调用,每次从上次写入的位置继续写入,从而实现高效的字符串拼接。
在函数的整个执行过程中,buf
不断地向后移动,每次写入一个字符后,buf
的值就会增加 1。
当函数处理完所有的格式化指令后,buf
指向的是缓冲区中最后一个写入的字符的下一个位置。
主要逻辑:
循环处理格式字符串:
函数通过一个 while
循环逐个字符处理格式字符串 fmt
,直到遇到字符串结束符 \0
或者缓冲区满(buf < last
)
处理普通字符:
如果当前字符不是 %
,则直接将其复制到目标缓冲区 buf
中。
处理格式化指令:
如果当前字符是 %
,则进入复杂的格式化处理逻辑:
解析宽度和填充字符:
通过循环读取数字字符,解析出格式化宽度 width
,并确定填充字符('0'
或 ' '
)。
解析格式化选项:通过 switch
语句解析格式化选项,如 'u'
(无符号整数)、'm'
(最大宽度)、'X'
(十六进制大写)、'x'
(十六进制小写)、'.'
(小数点精度)等。
处理具体格式化类型:
根据格式化类型(如 'V'
、'v'
、's'
、'i'
、'd'
、'f'
等),从可变参数列表 args
中取出对应的值,并调用相应的函数(如 ngx_sprintf_str
、ngx_sprintf_num
)进行格式化处理。
特殊处理:
对于浮点数 'f'
,会先处理符号位,然后分别处理整数部分和小数部分,并控制小数部分的精度。
其他特殊字符:
如 'c'
(字符)、'Z'
(字符串结束符)、'N'
(换行符)等,也会进行特殊处理。
关键函数调用:
ngx_sprintf_str
:
用于格式化字符串
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_sprintf_str 函数-CSDN博客
ngx_sprintf_num
:
用于格式化数字。
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_sprintf_num 函数-CSDN博客
详解:
u_char *p, zero;int d;double f;size_t slen;int64_t i64;uint64_t ui64, frac;ngx_msec_t ms;ngx_uint_t width, sign, hex, max_width, frac_width, scale, n;ngx_str_t *v;ngx_variable_value_t *vv;
u_char *p
:
这是一个指向 u_char
类型的指针,在函数中,它会被用来临时存储或操作字符串。
u_char zero;
这是一个 u_char
类型的变量,用于存储一个字符。在函数中,它会被用来表示填充字符(如 '0'
或 ' '
),具体取决于格式化要求。
int d;
这是一个整数类型的变量,用于存储临时的整数值。在函数中,它可能会被用来处理字符类型的数据(如 ASCII 值)。
double f;
这是一个双精度浮点数类型的变量,用于处理浮点数格式化。在函数中,它会被用来存储从可变参数列表中取出的浮点数值。
size_t slen;
这是一个无符号整数类型,通常用于表示长度或大小。在函数中,它会被用来存储字符串的最大长度或其他相关的长度信息。
int64_t i64;
这是一个 64 位有符号整数类型的变量,用于处理有符号整数格式化。在函数中,它会被用来存储从可变参数列表中取出的有符号整数值。
uint64_t ui64;
这是一个 64 位无符号整数类型的变量,用于处理无符号整数格式化。在函数中,它会被用来存储从可变参数列表中取出的无符号整数值。
uint64_t frac;
这也是一个 64 位无符号整数类型的变量,用于处理浮点数的小数部分。在函数中,它会被用来存储浮点数的小数部分的值。
ngx_msec_t ms;
这是一个 Nginx 自定义的类型,通常用于表示毫秒时间。在函数中,它会被用来处理与时间相关的格式化。
ngx_msec_t
这个类型的定义在哪里?
在 os/unix/ngx_time.h 中:
typedef ngx_rbtree_key_t ngx_msec_t;
ngx_core.h 中引入了 ngx_time.h
#include <ngx_time.h>
ngx_msec_t 的本质是 ngx_rbtree_key_t 类型
ngx_rbtree_key_t 类型的定义是什么呢?
它的定义在 ngx_rbtree.h 中:
typedef ngx_uint_t ngx_rbtree_key_t;
所以它的本质是 ngx_uint_t 类型
在 ngx_core.h 中引入了 ngx_rbtree.h
#include <ngx_rbtree.h>
ngx_uint_t 的定义是什么?
gx_uint_t 的定义是 ngx_config.h 中
typedef uintptr_t ngx_uint_t;
intptr_t
是一种整数类型,它保证可以容纳指针的值,帮助我们安全地在指针和整数之间进行转换在不同的系统和编译器中,指针的大小可能不同(比如 32 位系统和 64 位系统)
intptr_t
确保在这些系统上都能正确工作。可以通过引入 #include <unistd.h>来使用 intptr_t 类型
uintptr_t 是 intptr_t 的无符号形式
ngx_uint_t width;
这是一个无符号整数类型的变量,用于存储格式化宽度。
ngx_uint_t sign;
这是一个无符号整数类型的变量,用于存储符号标志。在函数中,它会被用来表示是否需要显示符号(如正负号)。
ngx_uint_t hex;
这是一个无符号整数类型的变量,用于存储十六进制标志。在函数中,它会被用来表示是否需要以十六进制格式输出。
ngx_uint_t max_width;
这是一个无符号整数类型的变量,用于存储最大宽度。在函数中,它会被用来限制输出的最大宽度。
ngx_uint_t frac_width;
这是一个无符号整数类型的变量,用于存储小数部分的宽度。在函数中,它会被用来控制浮点数小数部分的精度。
ngx_uint_t scale;
这是一个无符号整数类型的变量,用于存储缩放因子。在函数中,它会被用来计算浮点数小数部分的缩放值。
ngx_uint_t n;
这是一个无符号整数类型的变量,用于临时存储计数或其他整数值。在函数中,它会被用来进行循环计数或其他计算。
ngx_str_t *v;
这是一个指向 ngx_str_t
类型的指针。ngx_str_t
是 Nginx 自定义的结构体,通常用于表示字符串。
在函数中,它会被用来从可变参数列表中取出字符串类型的参数。
ngx_str_t
声明在 ngx_string.h 中:
typedef struct {size_t len;u_char *data; } ngx_str_t;
ngx_variable_value_t *vv;
这是一个指向 ngx_variable_value_t
类型的指针。ngx_variable_value_t
是 Nginx 自定义的结构体,通常用于表示变量的值。
在函数中,它会被用来从可变参数列表中取出变量类型的参数。
ngx_variable_value_t
类型的 定义在 ngx_string.h:
typedef struct {unsigned len:28;unsigned valid:1;unsigned no_cacheable:1;unsigned not_found:1;unsigned escape:1;u_char *data; } ngx_variable_value_t;
while (*fmt && buf < last) {
逐个解析格式化字符串(fmt
)中的字符,并根据格式说明符将参数数据写入目标缓冲区(buf
)
*fmt
:检查格式化字符串是否尚未结束(\0
)。
buf < last
:确保目标缓冲区仍有空间(last
指向缓冲区末尾的下一个位置,因此buf
必须严格小于last
才允许写入)。
if (*fmt == '%') {
当格式化字符串(fmt
)遇到%
字符时,表示进入一个格式说明符的解析过程,
该过程负责提取参数、应用格式规则(如宽度、精度、符号等),并将数据安全写入缓冲区。
i64 = 0;ui64 = 0;zero = (u_char) ((*++fmt == '0') ? '0' : ' ');width = 0;sign = 1;hex = 0;max_width = 0;frac_width = 0;slen = (size_t) -1;
这段代码的作用是为处理 %
后面的格式化指令做准备,初始化一些关键变量,确保后续的处理逻辑能够正确执行
初始化变量:
i64 = 0;
初始化一个 64 位有符号整数变量 i64
,用于存储后续可能的有符号整数。
ui64 = 0;
初始化一个 64 位无符号整数变量 ui64
,用于存储后续可能的无符号整数。
zero = (u_char) ((*++fmt == '0') ? '0' : ' ');
检查格式化字符串的下一个字符是否是 '0'
。 如果是 '0'
,则将 zero
设置为 '0'
,表示后续的数字格式化会用零填充;否则设置为 ' '
,表示用空格填充。fmt
指针会向前移动一位。
width=0;
初始化宽度变量 width
,用于存储格式化指令中指定的宽度(例如 %5d
中的 5
)。
sign = 1;
初始化符号标志 sign
,默认值为 1
,表示后续的数字是有符的。 如果后续遇到 u
或其他无符号修饰符,会将 sign
设置为 0
。
hex = 0;
初始化十六进制标志 hex
,默认值为 0
,表示后续的数字不是十六进制格式。 如果后续遇到 x
或 X
,会将 hex
设置为 1
或 2
。
max_width = 0;
初始化最大宽度变量 max_width
,用于存储格式化指令中可能的最大宽度限制。
frac_width = 0;
初始化小数部分宽度变量 frac_width
,用于存储浮点数格式化指令中指定的小数部分宽度(例如 %.2f
中的 2
)。
slen = (size_t) -1;
初始化字符串长度变量 slen
,默认值为 (size_t) -1
,表示字符串长度未指定。如果后续遇到 *
,会从 va_list args
中取出实际的长度值。
while (*fmt >= '0' && *fmt <= '9') {width = width * 10 + (*fmt++ - '0');}
这段代码的作用是解析格式说明符中的宽度字段(例如`%10d`中的`10`),并将其转换为整数值存储在`width`变量中。width
用于控制后续格式化输出的宽度
for ( ;; ) {
无限循环:通过for ( ;; )
实现,内部通过break
退出。
逐字符处理:每次循环处理一个字符(*fmt
),通过switch-case
分支处理不同修饰符。
通过continue
继续解析更多修饰符,或通过break
退出循环
switch (*fmt) {
switch (*fmt)
:根据当前字符 *fmt
的值,选择执行不同的代码分支。
*fmt
是当前格式化字符串中的字符。
case 'u':sign = 0;fmt++;continue;
这段代码是一个 case
分支,用于处理格式化字符串中的 u
修饰符。
u
修饰符表示后续的整数是无符号的。
代码通过设置 sign
变量为 0
来标记这一点,并将 fmt
指针向前移动一位,然后继续处理下一个字符。
case 'm':max_width = 1;fmt++;continue;
处理格式化字符串中的 m
修饰符
当前字符 *fmt
是 'm'
。这意味着格式化指令中有一个最大宽度的限制。
把 max_width
设置为 1
。
max_width
是一个标志变量,用来标记是否有限制最大宽度的要求。
当 max_width
被设置为 1
,后续的代码就知道在格式化输出时需要考虑最大宽度的限制
为什么要这样做: 某些数据类型(如整数)的最大可能值需要固定的字符宽度。例如,32位整数的最大值是 2147483647
,需要 10 个字符,加上负号可能需要 11 个字符。max_width = 1
告诉程序后续要自动计算这个最大宽度,而不是用用户指定的值。
把 fmt
指针向前移动一位,准备处理下一个字符
continue;
跳过当前循环的剩余部分,直接进入下一次循环,这意味着在处理完 'm'
修饰符后,代码不会执行 switch
语句后面的代码,而是直接继续处理下一个字符
为什么需要跳过:因为 m
可能只是格式说明符中的一个修饰符,后面可能还有其他字符需要处理(比如 %mlx
中的 l
和 x
)。continue
会让程序继续解析后面的字符。
case 'X':hex = 2;sign = 0;fmt++;continue;
处理格式化字符串中的 X
修饰符
在格式化字符串中,%X
表示以大写十六进制格式输出一个数字。
这段代码的作用是告诉程序接下来我们要以大写十六进制格式输出一个数字
hex
是一个标志变量,用来标记当前的格式化指令是否要求以十六进制格式输出。
在这里,hex = 2
表示要求以大写十六进制格式输出。如果 hex
是 1
,则表示小写十六进制;如果是 0
,则表示不是十六进制格式
sign = 0
表示当前的数字是无符号的。因为十六进制通常用于表示无符号数字,所以这里把 sign
设置为 0
fmt++
,代码跳过当前的 'X'
字符,准备处理下一个字符
continue;
这行代码的作用是跳过当前循环的剩余部分,直接进入下一次循环
case 'x':hex = 1;sign = 0;fmt++;continue;
与上一段代码类似
case '.':fmt++;while (*fmt >= '0' && *fmt <= '9') {frac_width = frac_width * 10 + (*fmt++ - '0');}break;
处理格式化字符串中的小数点(.
)和它后面跟着的数字。
这些数字表示小数部分的宽度,比如在 %5.2f
中,2
表示小数部分显示两位。
fmt++
,代码跳过当前的 '.'
字符,准备处理小数点后面的数字
while (*fmt >= '0' && *fmt <= '9')
这是一个循环,用来检查当前字符是否是一个数字
*fmt >= '0' && *fmt <= '9'
:这个条件检查当前字符是否在 '0'
到 '9'
之间,也就是是否是一个数字
如果是数字,循环继续执行;如果不是数字,循环结束
frac_width = frac_width * 10 + (*fmt++ - '0');
这行代码的作用是把小数点后面的数字解析出来,并存储到 frac_width
中。
frac_width * 10
:把当前的 frac_width
值乘以 10,相当于把数字左移一位。
比如,如果 frac_width
是 2
,乘以 10 后变成 20
(*fmt++ - '0')
:把当前字符转换成对应的数字。比如,字符 '3'
的 ASCII 值是 51,'0'
的 ASCII 值是 48,所以 '3' - '0'
的结果是 3
。
fmt++
:把 fmt
指针向前移动一位,继续处理下一个字符。
break;
退出 switch
语句,然后会退出 for 循环
case '*':slen = va_arg(args, size_t);fmt++;continue;
处理格式化字符串中的 *
修饰符
*
表示格式化指令中的宽度或精度由一个变量指定,而不是直接写在格式化字符串中。代码会从 va_list args
中取出这个变量,并将其存储到 slen
中
switch (*fmt) {case 'V':v = va_arg(args, ngx_str_t *);buf = ngx_sprintf_str(buf, last, v->data, v->len, hex);fmt++;continue;
进入下一个 switch
前面的 switch
:处理的是格式化指令中的修饰符,比如宽度、精度、是否是无符号数等。它的目的是解析这些修饰符,并设置相应的标志变量。
这个 switch
:在解析完所有修饰符后,用来处理数据类型的转换
处理格式化字符串中的 V
修饰符。V
表示需要插入一个字符串,这个字符串的地址存储在参数列表 args
中。代码会从 args
中取出这个字符串的地址,并将其内容格式化到输出缓冲区 buf
中
v = va_arg(args, ngx_str_t *);
这行代码从参数列表 args
中取出一个 ngx_str_t
类型的指针,并将其存储到变量 v
中。
ngx_str_t
是一个结构,通常包含两个字段:
data
:指向字符串内容的指针。
len
:字符串的长度。
buf = ngx_sprintf_str(buf, last, v->data, v->len, hex);
这行代码调用 ngx_sprintf_str
函数,将字符串 v->data
格式化到输出缓冲区 buf
中。
ngx_sprintf_str
是一个函数,用于将字符串格式化到缓冲区中
它的参数包括:
buf
:当前的输出缓冲区指针。
last
:缓冲区的末尾指针,用于防止缓冲区溢出。
v->data
:要插入的字符串内容。
v->len
:要插入的字符串长度。
hex
:一个标志,表示是否以十六进制格式输出
调用 ngx_sprintf_str
后,buf
指针会移动到刚刚写入的字符串的末尾,准备写入下一个内容。
case 'v':vv = va_arg(args, ngx_variable_value_t *);buf = ngx_sprintf_str(buf, last, vv->data, vv->len, hex);fmt++;continue;
处理格式化字符串中的 v
修饰符。
v
表示需要插入一个字符串,这个字符串的地址存储在参数列表 args
中。
代码会从 args
中取出这个字符串的地址,并将其内容格式化到输出缓冲区 buf
中。
vv = va_arg(args, ngx_variable_value_t *);
这行代码从参数列表 args
中取出一个 ngx_variable_value_t
类型的指针,并将其存储到变量 vv
中。
ngx_variable_value_t
是一个结构体
data
:指向字符串内容的指针。
len
:字符串的长度。
buf = ngx_sprintf_str(buf, last, vv->data, vv->len, hex);
这行代码调用 ngx_sprintf_str
函数,将字符串 vv->data
格式化到输出缓冲区 buf
中。
case 's':p = va_arg(args, u_char *);buf = ngx_sprintf_str(buf, last, p, slen, hex);fmt++;continue;
处理格式化字符串中的 s
修饰符。
s
表示需要插入一个普通的字符串,这个字符串的地址存储在参数列表 args
中。
代码会从 args
中取出这个字符串的地址,并将其内容格式化到输出缓冲区 buf
中。
p = va_arg(args, u_char *);
这行代码从参数列表 args
中取出一个 u_char *
类型的指针,并将其存储到变量 p
中。
buf = ngx_sprintf_str(buf, last, p, slen, hex);
这行代码调用 ngx_sprintf_str
函数,将字符串 p
格式化到输出缓冲区 buf
中
case 'O':i64 = (int64_t) va_arg(args, off_t);sign = 1;break;
%O
是 Nginx 自定义的格式符,专用于处理 off_t
类型的数据
off_t
通常表示文件偏移量(比如读写文件时的位置)
off_t
off_t 是一个用于表示文件偏移量的类型。它是一个有符号整数类型。在 C 语言中,文件偏移量是指从文件的开头到文件的某个特定位置之间的字节数。例如,如果一个文件的大小为 1024 字节,那么偏移量可以是 0(文件开头)、512(文件中间位置)等。
它的具体大小(如是 32 位还是 64 位)取决于系统架构和编译器。在 32 位系统上,off_t 通常是 32 位的,而在 64 位系统上,off_t 通常是 64 位的。这是因为 64 位系统可以支持更大的文件大小,需要更大的偏移量范围来表示文件中的位置。
off_t 类型通常定义在 <sys/types.h> 头文件中
i64 = (int64_t) va_arg(args, off_t);
从可变参数列表(args
)中提取一个 off_t
类型的参数
off_t
的类型可能因系统不同而变化(例如在 32 位系统可能是 32 位,64 位系统是 64 位)。
sign = 1;
需要处理正负号(比如负数前面加 -
)
off_t
可以是负数(比如返回错误时的 -1
),所以必须保留符号信息
break;
跳出 switch (*fmt)
的整个选择结构
执行完这行后,代码会继续执行 switch
之后的公共逻辑 (ngx_sprintf_num
:用于数字转换为字符串)
case 'P':i64 = (int64_t) va_arg(args, ngx_pid_t);sign = 1;break;
%P
是 Nginx 自定义的格式符,专门用于打印进程ID(PID)
用 ngx_pid_t
而不是直接 int
?
pid_t
类型在不同系统中可能不同(如在 32 位系统是 int
,64 位系统可能是 long
)
Nginx 通过 ngx_pid_t
封装了这一差异,确保代码可移植
将 ngx_pid_t
强制转换为 int64_t
(固定 64 位有符号整数),统一后续处理逻辑
ngx_pid_t
ngx_process.h 中可以找到 ngx_pid_t 的定义,它本质上是 pid_t
ngx_core.h 引入了 ngx_process.h
pid_t
是一个用来表示进程ID的类型。它是一个整数类型,专门用来存储进程的唯一标识符
pid_t
的声明在<sys/types.h>
头文件中
进程ID不可能是负数吧?这里为什么还要用 sign=1
?
某些系统 API 可能返回 -1
表示错误(例如 fork()
失败)
case 'T':i64 = (int64_t) va_arg(args, time_t);sign = 1;break;
%T
是 Nginx 自定义的格式符,用于处理 time_t
类型的时间戳。
time_t
是什么?
它是 C 标准库中表示时间的类型,通常是从 1970-01-01 开始的秒数(Unix 时间戳)。
在 32 位系统可能是 int32_t
(最大到 2038 年),64 位系统是 int64_t
time_t
要使用
time_t
,需要在代码中引入头文件<time.h>
time_t
的具体类型由编译器和系统决定,可能是long
、long long
或其他整数类型。例如,在32位系统中可能是32位整数,而在64位系统中可能是64位整数。
为什么要把 time_t
转成 int64_t
?
time_t
的大小可能因系统不同而变化(32 位或 64 位)
转换为 int64_t
确保统一处理,避免溢出(例如 2038 年后 32 位时间戳会溢出)
case 'M':ms = (ngx_msec_t) va_arg(args, ngx_msec_t);if ((ngx_msec_int_t) ms == -1) {sign = 1;i64 = -1;} else {sign = 0;ui64 = (uint64_t) ms;}break;
%M
是 Nginx 自定义的格式符,用于处理 ngx_msec_t
类型的毫秒时间。
在 Nginx 中,-1
表示「无限时间」或「无效值」
if ((ngx_msec_int_t) ms == -1)
ngx_msec_int_t
是 ngx_msec_t
对应的有符号类型
ngx_msec_t
被定义为无符号类型,直接判断 ms == -1
会永远为假
通过强制转换为有符号类型,可以正确检测 -1
的特殊值
ngx_msec_int_t
是
ngx_msec_t 的有符号形式
sign = 1; i64 = -1;
当检测到 ms
是 -1
时,表示需要输出 -1
设置 sign = 1
:告诉后续代码这是有符号数,需要处理负号
i64 = -1
:将值固定为 -1
(后续会转换为字符串 "-1"
)
else { sign = 0; ui64 = (uint64_t) ms; }
当 ms
不是 -1
时,表示合法的毫秒数
sign = 0
:标记为无符号数(数值非负)
ui64 = ms
:将毫秒值存入无符号变量
case 'z':if (sign) {i64 = (int64_t) va_arg(args, ssize_t);} else {ui64 = (uint64_t) va_arg(args, size_t);}break;
%z
是 Nginx 自定义的格式符,专门处理 size_t
和 ssize_t
类型的数据
size_t
和ssize_t
size_t 是一个无符号整数类型,
通常用于表示对象的大小或数组的索引。
ssize_t 是有符号的,因此可以表示负数,通常用于错误处理,比如返回-1表示错误。
可以通过引入 <sys/types.h> 使用这2个类型
使用
size_t
而非固定类型(如int
或long
)能提高代码在不同平台间的兼容性。
case 'i':if (sign) {i64 = (int64_t) va_arg(args, ngx_int_t);} else {ui64 = (uint64_t) va_arg(args, ngx_uint_t);}if (max_width) {width = NGX_INT_T_LEN;}break;
%i
是 Nginx 自定义的格式符,用于处理其内部整数类型 ngx_int_t
和 ngx_uint_t
ngx_int_t
和 ngx_uint_t
是跨平台类型,可能是 int32_t
或 int64_t
,具体取决于编译配置。
if (max_width)
max_width
由格式字符串中的修饰符设置(例如 %10i
中的 10
是宽度,而 %mi
可能设置 max_width=1
)。
%mi
中的 m
表示使用最大宽度(即 max_width=1
)
NGX_INT_T_LEN
Nginx 定义的常量,表示 ngx_int_t
类型的最大字符长度
例如,如果 ngx_int_t
是 64 位,其最大值为 -9223372036854775808
(20 字符),则 NGX_INT_T_LEN
可能是 20
当 max_width
启用时,强制将宽度设为该类型可能的最大长度
NGX_INT_T_LEN
NGX_INT_T_LEN 的定义在
ngx_config.h#if (NGX_PTR_SIZE == 4) #define NGX_INT_T_LEN NGX_INT32_LEN #define NGX_MAX_INT_T_VALUE 2147483647#else #define NGX_INT_T_LEN NGX_INT64_LEN #define NGX_MAX_INT_T_VALUE 9223372036854775807 #endif
它具体的定义取决于 NGX_PTR_SIZE
NGX_PTR_SIZE 定义在 objs/ngx_auto_config.h 中
#ifndef NGX_PTR_SIZE #define NGX_PTR_SIZE 8 #endif
所以 #if (NGX_PTR_SIZE == 4) 这个条件不成立
所以
NGX_INT_T_LEN 的定义是:
#define NGX_INT_T_LEN NGX_INT64_LEN
NGX_INT64_LEN 的定义是什么?在哪里?
在 ngx_config.h 中:
#define NGX_INT64_LEN (sizeof("-9223372036854775808") - 1)
"-9223372036854775808":这是一个字符串,表示64位整数的最小值。64位整数的范围是从-9223372036854775808到9223372036854775807,这个字符串的长度就是64位整数的最大长度。
-1:因为字符串的长度包括了结尾的空字符\0,所以减去1,得到实际的数字长度。
case 'd':if (sign) {i64 = (int64_t) va_arg(args, int);} else {ui64 = (uint64_t) va_arg(args, u_int);}break;
%d
主要处理有符号整数(如 int
),但 Nginx 扩展了它的能力
通过 sign
标志,允许 %d
同时兼容有符号和无符号整数的打印
case 'l':if (sign) {i64 = (int64_t) va_arg(args, long);} else {ui64 = (uint64_t) va_arg(args, u_long);}break;
%l
是 Nginx 自定义的格式符,用于处理 long
和 unsigned long
类型的数据。
例如:
-
%ul
→ 无符号(sign=0
) -
%l
默认有符号(sign=1
)
i64 = (int64_t) va_arg(args, long);
为什么要转成 int64_t
?
跨平台一致性:
long
的大小因系统而异(32 位系统通常是 4 字节,64 位系统是 8 字节)转换后统一处理
case 'D':if (sign) {i64 = (int64_t) va_arg(args, int32_t);} else {ui64 = (uint64_t) va_arg(args, uint32_t);}break;
%D
是 Nginx 自定义的格式符,专门处理精确 32 位的整数(无论系统是 32 位还是 64 位)
-
典型场景:
-
处理网络协议中的固定长度字段(如 IP 地址的 32 位表示)
-
与外部系统交互时确保数据宽度一致(如二进制文件格式)
-
为什么要转成 int64_t
?
-
如果直接操作
int32_t
,负数的符号位在 64 位系统中可能错误扩展(例如-1
在 32 位是0xFFFFFFFF
,转为 64 位可能变成0xFFFFFFFFFFFFFFFF
)。 -
转换为
int64_t
会保留原始值的符号和大小(例如-1
→-1
)。 -
后续代码只需处理
int64_t
类型,无需考虑原始数据类型,统一处理
case 'L':if (sign) {i64 = va_arg(args, int64_t);} else {ui64 = va_arg(args, uint64_t);}break;
%L
是 Nginx 自定义的格式符,专门处理 64 位整数
case 'A':if (sign) {i64 = (int64_t) va_arg(args, ngx_atomic_int_t);} else {ui64 = (uint64_t) va_arg(args, ngx_atomic_uint_t);}if (max_width) {width = NGX_ATOMIC_T_LEN;}break;
%A
是 Nginx 自定义的格式符,专门用于处理原子整数类型,原子操作专用
-
用途场景:
-
打印多线程环境下的原子计数器(如请求计数、连接数)。
-
调试原子变量的值(例如检查锁的状态)。
-
ngx_atomic_int_t 和 ngx_atomic_uint_t
定义在 ngx_atomic.h
#if (NGX_HAVE_LIBATOMIC)#define AO_REQUIRE_CAS #include <atomic_ops.h>#define NGX_HAVE_ATOMIC_OPS 1typedef long ngx_atomic_int_t; typedef AO_t ngx_atomic_uint_t; typedef volatile ngx_atomic_uint_t ngx_atomic_t;
#if (NGX_HAVE_LIBATOMIC)
这个条件编译指令检查是否定义了
NGX_HAVE_LIBATOMIC
宏。只有在定义了NGX_HAVE_LIBATOMIC这个宏的时候,下面的代码才会被包含进来
这个宏通常在Nginx的配置阶段生成,表示当前系统是否安装了
libatomic
库。如果有的话,Nginx就会使用这个库提供的原子操作函数。
libatomic
是一个 跨平台原子操作库,其核心作用是为不同硬件架构提供统一的原子操作接口,Nginx用它来实现原子操作的可移植性。
<atomic_ops.h>
是libatomic
库的头文件,包含平台无关的原子操作函数和类型定义(如AO_t
)
AO_t
是atomic_ops
库定义的无符号原子类型,其长度和内存对齐由库自动适配当前平台。将
AO_t
重定义为ngx_atomic_uint_t
,是为了隐藏实现细节,提高代码可移植性。如果未来更换原子操作库,只需修改此处类型定义AO_t通常是一个无符号整型,其具体位数取决于平台,比如在32位系统上是32位,64位系统上是64位,这样能够保证原子操作的效率。
为什么需要原子类型?
线程安全:
这些类型保证在多线程/多进程环境下,读写操作是原子的(不会被中断)。
NGX_ATOMIC_T_LEN
是原子类型的最大字符长度(例如 32 位原子数最大是
-2147483648
,长度是 11)NGX_ATOMIC_T_LEN 定义
在 os/unix/ngx_atomic.h :
#if (NGX_PTR_SIZE == 8) #define NGX_ATOMIC_T_LEN (sizeof("-9223372036854775808") - 1) #else #define NGX_ATOMIC_T_LEN (sizeof("-2147483648") - 1) #endif
同
NGX_INT_T_LEN 一样是 (sizeof("-9223372036854775808") - 1)
case 'f':f = va_arg(args, double); // 提取浮点数if (f < 0) { // 处理负号*buf++ = '-';f = -f;}ui64 = (int64_t) f; // 取整数部分frac = 0; // 小数部分初始化if (frac_width) { // 如果有小数位数要求scale = 1;for (n = frac_width; n; n--) { // 计算10的N次方(如保留3位小数,scale=1000)scale *= 10;}// 计算小数部分并四舍五入frac = (uint64_t) ((f - (double) ui64) * scale + 0.5);// 处理进位(如0.9995保留3位会进位到1.000)if (frac == scale) {ui64++;frac = 0;}}// 写入整数部分buf = ngx_sprintf_num(buf, last, ui64, zero, 0, width);if (frac_width) { // 写入小数部分if (buf < last) {*buf++ = '.';}buf = ngx_sprintf_num(buf, last, frac, '0', 0, frac_width);}fmt++;continue;
1. 提取浮点数
f = va_arg(args, double);
作用:从参数列表中提取一个双精度浮点数(如 3.1415
)
2. 处理负号
if (f < 0) {*buf++ = '-'; // 写入负号f = -f; // 转为正数处理
}
如果是负数,先在缓冲区写 -
将 f
转为正数,简化后续处理
3. 分离整数和小数部分
ui64 = (int64_t) f; // 取整数部分(如123.456 → 123)
frac = 0; // 初始化小数部分为0
(int64_t) f
会直接截断小数(例如 3.999
→ 3
)
4. 处理小数位数
if (frac_width) {// 1. 计算缩放因子(如保留3位 → scale=1000)scale = 1;for (n = frac_width; n; n--) {scale *= 10;}// 2. 计算四舍五入后的小数值frac = (uint64_t) ((f - ui64) * scale + 0.5);// 3. 处理进位(如0.9995 → 1.000)if (frac == scale) {ui64++; // 整数部分加1frac = 0; // 小数清零}
}
frac_width
是格式字符串中指定的保留小数位数(如 %.3f
→ frac_width=3
)
-
示例:
对3.1415926
保留3位小数:-
scale = 1000
-
小数部分
0.1415926
→0.1415926 * 1000 = 141.5926
→ 加上0.5
后四舍五入 →142
。 -
最终
frac=142
→ 输出3.142
。
-
5. 写入整数部分
buf = ngx_sprintf_num(buf, last, ui64, zero, 0, width);
6. 写入小数部分
if (frac_width) {if (buf < last) { // 检查缓冲区是否还有空间*buf++ = '.'; // 写入小数点}// 将小数部分按指定位数写入(如frac=142 → 在小数点后写入"142")buf = ngx_sprintf_num(buf, last, frac, '0', 0, frac_width);
}
7. 收尾处理
fmt++; // 移动到格式字符串的下一个字符
continue; // 跳过循环剩余代码
#if !(NGX_WIN32)
case 'r':i64 = (int64_t) va_arg(args, rlim_t);sign = 1;break;
#endif
第1行:#if !(NGX_WIN32)
作用:这是条件编译指令,意思是“如果当前不是Windows系统”。
Nginx需要跨平台兼容,但rlim_t
这个类型是Unix/Linux系统中特有的(用于表示系统的资源限制值,如ulimit
)。Windows没有这个类型和对应的系统调用,所以通过条件编译屏蔽了Windows平台的这段代码,避免编译错误。
rlim_t
rlim_t
用于表示Unix/Linux系统中 进程资源限制的数值,典型场景包括:
- 定义进程可创建文件的最大大小(
RLIMIT_FSIZE
)- 限制进程栈内存大小(
RLIMIT_STACK
)- 控制最大打开文件数(
RLIMIT_NOFILE
)- 管理CPU时间配额(
RLIMIT_CPU
)使用
rlim_t
及相关函数时,必须引入<sys/resource.h>
头文件底层类型:在POSIX标准中,
rlim_t
通常是 无符号64位整数,但在不同系统可能不同
第2行:case 'r':
作用:匹配格式符%r
。
当Nginx的格式化字符串中遇到%r
时,会进入这个分支处理对应的参数。
第3行:i64 = (int64_t) va_arg(args, rlim_t);
从可变参数列表args
中提取一个rlim_t
类型的值,并强制转换为int64_t
(64位有符号整数)。
1️⃣ rlim_t
是Unix系统中表示资源限制的类型(例如通过getrlimit()
获取进程的文件描述符数量限制)。
2️⃣ 强制转换是为了后续统一处理数值格式(比如处理负数),因为rlim_t
可能是long
或long long
类型,与平台相关,转成固定宽度的int64_t
保证兼容性。
第4行:sign = 1;
作用:设置符号标记为需要处理正负号。
资源限制值可能是负数或无意义(例如表示“无限制”),设置sign=1
后,后续代码会检查i64
是否为负,如果是负数,会在输出时添加负号-
。
第5行:break;
作用:跳出switch
语句
第6行:#endif
作用:结束条件编译块。
与开头的#if !(NGX_WIN32)
配对,确保Windows平台下完全忽略这段代码。
case 'p':ui64 = (uintptr_t) va_arg(args, void *);hex = 2;sign = 0;zero = '0';width = 2 * sizeof(void *);break;
第1行:case 'p':
作用:匹配格式符%p
,用于输出指针地址
Nginx会通过这个分支处理指针的格式化。
第2行:ui64 = (uintptr_t) va_arg(args, void *);
作用:
1️⃣ va_arg(args, void *)
:从可变参数列表args
中提取一个指针
2️⃣ (uintptr_t)
:将指针转换为无符号整数类型uintptr_t
(保证能完整存储指针值)。
第3行:hex = 2;
作用:标记后续以大写十六进制格式输出。
在代码中,hex=1
表示小写十六进制(如1a3f
),hex=2
表示大写(如1A3F
)。指针地址通常用大写,例如Linux内核和调试工具(如GDB)的默认行为。
第4行:sign = 0;
指针地址永远是非负数(内存地址从0x0
开始),不需要处理正负号
第5行:zero = '0';
作用:设置填充字符为0
。
统一指针地址的显示风格,例如0x00007ffeeb4a8d20
(用0
填充左侧空白),而不是空格填充(如0x 7ffeeb4a8d20
)。
第6行:width = 2 * sizeof(void *);
作用:计算指针地址的完整十六进制位数。
1️⃣ sizeof(void *)
:获取指针类型的字节数(32位系统为4字节,64位系统为8字节)。
2️⃣ 2 *
:每个字节用2个十六进制字符表示
示例:
-
32位系统:
4字节 × 2 = 8字符
→ 地址格式化为0x00000000
-
64位系统:
8字节 × 2 = 16字符
→ 地址格式化为0x0000000000000000
case 'c':d = va_arg(args, int);*buf++ = (u_char) (d & 0xff);fmt++;continue;
第1行:case 'c':
作用:匹配格式符%c
,用于输出单个字符。
第2行:d = va_arg(args, int);
作用:从可变参数列表args
中提取一个int
类型的参数。
1️⃣ 在C语言中,char
类型参数传递给可变参数函数时会被提升为int
(例如字符'A'
实际以整数65
传递)。
2️⃣ 提取为int
是为了兼容这种隐式类型提升,确保能正确处理所有字符值(包括char
和int
参数)。
第3行:*buf++ = (u_char) (d & 0xff);
作用:将整数d
转换为1个字节的字符,写入缓冲区。
1️⃣ d & 0xff
:取d
的最低8位(即一个字节),屏蔽高位数据(例如传入0x1234
会被截断为0x34
)。
-
为什么要屏蔽高位?
-
确保即使传入的
int
值超出char
范围(如256
),也只保留有效字节(类似(char)d
但更安全)。
2️⃣(u_char)
:将结果转为无符号字符(避免符号扩展问题,例如char
可能被当作负数)。
3️⃣*buf++
:写入缓冲区并移动指针
-
第4行:fmt++;
作用:移动格式字符串指针到下一个字符。
当前格式符%c
已经处理完毕,需要继续解析后续字符
第5行:continue;
作用:跳过switch
语句的剩余代码,直接回到while
循环开头。
与break
的区别:
-
break
:退出整个switch
,继续执行switch
之后的代码(例如后续的数值格式化逻辑)。 -
continue
:直接回到while (*fmt && buf < last)
循环的开头,立即处理下一个格式字符。
为什么这里用continue
?
因为%c
的代码逻辑已经完全结束(不需要执行switch
后的公共数值处理代码),而其他格式符(如%d
、%x
)可能需要继续执行公共代码。
case 'Z':*buf++ = '\0';fmt++;continue;
第1行:case 'Z':
作用:匹配格式符%Z
,用于在缓冲区中写入一个空字符(\0
)。
第2行:*buf++ = '\0';
作用:
1️⃣ *buf = '\0'
:在当前缓冲区位置写入空字符(字符串终止符)。
第3行:fmt++;
作用:移动格式字符串指针到下一个字符。
当前格式符%Z
已经处理完毕,继续解析后续字符
第4行:continue;
作用:跳过switch
语句的剩余代码,直接回到while
循环开头。
%Z
仅插入空字符,不需要执行后续的数值格式化逻辑(如处理符号、十六进制转换等),因此用continue
直接进入下一轮循环。
case 'N':
#if (NGX_WIN32)*buf++ = CR;if (buf < last) {*buf++ = LF;}
#else*buf++ = LF;
#endiffmt++;continue;
第1行:case 'N':
作用:匹配格式符%N
,用于输出换行符(类似\n
)。
Nginx用%N
统一跨平台换行符的差异,类似C语言的\n
,但实际行为因平台而异。
第2行:#if (NGX_WIN32)
作用:判断当前是否为Windows系统。
Windows和Unix系统的换行符不同:
-
Windows:换行符是
CRLF
(Carriage Return + Line Feed,即\r\n
)。 -
Unix/Linux/macOS:换行符是
LF
(\n
)。
Windows分支代码:
第3行:*buf++ = CR;
作用:写入CR
(Carriage Return,ASCII码0x0D
,即\r
)。
这是Windows换行符的第一个字符。
第4行:if (buf < last) {
作用:检查缓冲区是否还有空间。
写入CR
后缓冲区指针buf
已移动,需确认剩余空间是否至少1字节。
第5行:*buf++ = LF;
作用:写入LF
(Line Feed,ASCII码0x0A
,即\n
)。
完成Windows换行符\r\n
的第二个字符。若缓冲区已满(buf == last
),则跳过此操作。
非Windows分支代码:
第7行:*buf++ = LF;
作用:直接写入LF
(对应Unix的\n
)。
无需CR
,单字符即可表示换行。
公共逻辑:
第9行:fmt++;
作用:移动格式字符串指针到下一个字符。
第10行:continue;
作用:跳过switch
后续代码,直接处理下一个格式字符。
总结:这段代码通过%N
实现了跨平台换行符的自动适配:
-
Windows →
\r\n
-
Unix →
\n
CR、 LF
定义在 ngx_core.h:
#define LF (u_char) '\n' #define CR (u_char) '\r' #define CRLF "\r\n"
case '%':*buf++ = '%';fmt++;continue;default:*buf++ = *fmt++;continue;
case '%'
(%%
转义)
代码作用:当格式字符串中出现连续两个%
(即%%
)时,输出单个%
。
逐行解释:
*buf++ = '%';
-
向缓冲区写入一个
%
字符(转义输出)。 -
直接进入下一轮循环,避免执行其他无关逻辑。
default
(未识别的格式符)
代码作用:当遇到未知的格式符(例如%k
)时,原样输出该字符。
跳过后续处理,直接处理下一个字符。
if (sign) {if (i64 < 0) {*buf++ = '-';ui64 = (uint64_t) -i64;} else {ui64 = (uint64_t) i64;}
}
第1行:if (sign)
作用:检查是否需要处理数值的符号位。
-
sign=1
:表示当前处理的数值是有符号类型 -
sign=0
:表示数值是无符号类型,直接跳过符号处理。
第2行:if (i64 < 0)
作用:判断数值是否为负数。
如果i64
是负数(例如-123
),需要输出负号-
并取其绝对值,否则直接使用原值
第3行:*buf++ = '-';
作用:向缓冲区写入负号-
。
第4行:ui64 = (uint64_t) -i64;
作用:将负数的绝对值转换为无符号整数。
-i64
:对负数取反
(uint64_t)
:转换为无符号类型,以便后续统一处理数值格式化
第5-6行:else { ui64 = (uint64_t) i64; }
作用:如果数值非负,直接将其转换为无符号整数。
buf = ngx_sprintf_num(buf, last, ui64, zero, hex, width);
fmt++;
第1行:buf = ngx_sprintf_num(...)
作用:调用函数将数值ui64
格式化为字符串,并写入缓冲区。
参数解析:
1️⃣ buf
:当前缓冲区写入位置(函数返回后更新到新位置)。
2️⃣ last
:缓冲区末尾地址(防止溢出)。
3️⃣ ui64
:待格式化的无符号数值(绝对值和进制转换已完成)。
4️⃣ zero
:填充字符('0'
或' '
),用于宽度不足时补位。
5️⃣ hex
:进制模式(0
=十进制,1
=小写十六进制,2
=大写十六进制)。
6️⃣ width
:最小输出宽度(若实际位数不足,左侧填充zero
字符)。
第2行:fmt++
作用:移动格式字符串指针到下一字符。
当前格式符已处理完毕,fmt
指向格式符后的字符
通过fmt++
跳到下一个待处理字符,继续主循环
else {*buf++ = *fmt++;
}
将格式字符串当前字符(如a
、b
、c
)复制到缓冲区
同时移动buf
和fmt
指针到下一位置
用于处理普通字符(非%
的字符),例如格式字符串"hello"
会原样写入缓冲区