日志打印相关代码
1. printf函数的实现原理
1. int printf(const char *format, ...); format是固定参数,是参数1
… 是可变参数
2. 相关宏
(1)va_list 就是一个char*指针, 即:typedef char * va_list; (2)va_start宏 ,#define va_start(list,param1) (list = (va_list)¶m1+ sizeof(param1)) 获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是固定参数)
(3)va_arg宏,#define va_arg(list,mode) ((mode *) (list += sizeof(mode)))[-1] 获取可变参数的当前参数的值,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型)
(4)va_end宏 va_end(list) ( list = (va_list)0 )
清空va_list可变参数列表,防止出现野指针
说白了 就是通过固定参数的地址,根据可变参数的大小,来寻找可变参数的地址
printf实现原理基本如下:(ngx_log_stderr、ngx_log_error_core函数就是这样实现的)
1. 定义一个数组str(最后用于write输出)
2. 遍历固定参数所指向的字符串 (vsprintf函数中来实现的)
遇到普通文本就直接拷贝到数组str中;
遇到%d这些,就利用那几个宏去可变参数中找出来然后转换成文本形式拷贝到str中。
3. 利用write将str数组输出即可。(如果想添加额外的信息,就自己在str数组中添加即可)
printf源码(参考Linux1.0内核源码)
3 static int printf(const char *fmt, ...)
4 {
5 va_list args;
6 int i;
7 va_start(args, fmt);
8 write(1,printbuf,i=vsprintf(printbuf, fmt, args)); //vsprintf源码此处就省略,感兴趣的自己去网上搜搜
9 va_end(args);
10 return i;
11 }
2. ngx_log_stderr函数
往屏幕中进行错误输出,使用方式如下:void ngx_log_stderr(int err, const char *fmt, ...)
ngx_log_stderr(0, "invalid option: \"%s\"", argv[0]); //nginx: invalid option: "./nginx"
ngx_log_stderr(0, "invalid option: %10d", 21); //nginx: invalid option: 21 ---21前面有8个空格
ngx_log_stderr(0, "invalid option: %.6f", 21.378); //nginx: invalid option: 21.378000 ---%.这种只跟f配合有效,往末尾填充0
ngx_log_stderr(0, "invalid option: %.6f", 12.999); //nginx: invalid option: 12.999000
ngx_log_stderr(0, "invalid option: %.2f", 12.999); //nginx: invalid option: 13.00
ngx_log_stderr(0, "invalid option: %xd", 1678); //nginx: invalid option: 68E
ngx_log_stderr(0, "invalid option: %Xd", 1678); //nginx: invalid option: 68E
ngx_log_stderr(15, "invalid option: %s , %d", "testInfo",326); //nginx: invalid option: testInfo , 326
ngx_log_stderr(0, "invalid option: %d", 1678);
3. ngx_log_error_core函数
类似这样的输出,前面增加了很多提示信息,
2019/03/12 22:54:18 [stderr] 4112: nginx: 当前收消息队列/发消息队列大小分别为(0/0),丢弃的待发送数据包数量为0。
4. 源码实现
ngx_log.h
1 #ifndef NGX_LOG_H
2 #define NGX_LOG_H
3 #include<stddef.h>
4 #include <stdarg.h>
5
6 #define NGX_MAX_ERROR_STR 2048 //显示的错误信息最大数组长度
7 //简单功能函数--------------------
8 //类似memcpy,但常规memcpy返回的是指向目标dst的指针,而这个ngx_cpymem返回的是目标【拷贝数据后】的终点位置,连续复制多段数据时方便
9 #define ngx_cpymem(dst, src, n) (((u_char *) memcpy(dst, src, n)) + (n)) //注意#define写法,n这里用()包着,防止出现什么错误
10 #define ngx_min(val1, val2) ((val1 > val2) ? (val2) : (val1)) //比较大小,返回小值,注意,参数都用()包着
11
12 //数字相关--------------------
13 #define NGX_MAX_UINT32_VALUE (uint32_t) 0xffffffff //最大的32位无符号数:十进制是4294967295
14 #define NGX_INT64_LEN (sizeof("-9223372036854775808") - 1)
15
16 //日志相关--------------------
17 //我们把日志一共分成八个等级【级别从高到低,数字最小的级别最高,数字大的级别最低】,以方便管理、显示、过滤等等
18 #define NGX_LOG_STDERR 0 //控制台错误【stderr】:最高级别日志,日志的内容写入log参数指定的文件,同时也尝试直接将日志输出到标准错误设备比如控制台屏幕
19 #define NGX_LOG_EMERG 1 //紧急 【emerg】
20 #define NGX_LOG_ALERT 2 //警戒 【alert】
21 #define NGX_LOG_CRIT 3 //严重 【crit】
22 #define NGX_LOG_ERR 4 //错误 【error】:属于常用级别
23 #define NGX_LOG_WARN 5 //警告 【warn】:属于常用级别
24 #define NGX_LOG_NOTICE 6 //注意 【notice】
25 #define NGX_LOG_INFO 7 //信息 【info】
26 #define NGX_LOG_DEBUG 8 //调试 【debug】:最低级别
27
28 //#define NGX_ERROR_LOG_PATH "logs/error1.log" //定义日志存放的路径和文件名
29 #define NGX_ERROR_LOG_PATH "error.log" //定义日志存放的路径和文件名
30
31
32 typedef unsigned char u_char;
33 typedef unsigned int u_int;
34
35 //和日志,打印输出有关
36 void ngx_log_init();
37 void ngx_log_stderr(int err, const char *fmt, ...);
38 void ngx_log_error_core(int level, int err, const char *fmt, ...);
39 u_char *ngx_log_errno(u_char *buf, u_char *last, int err);
40 u_char *ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...);
41 u_char *ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...);
42 u_char *ngx_vslprintf(u_char *buf, u_char *last,const char *fmt,va_list args);
43
44 #endif // NGX_LOG_H
ngx_log.cpp
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <stdint.h> //uintptr_t
5 #include <stdarg.h> //va_start....
6 #include <unistd.h> //STDERR_FILENO等
7 #include <sys/time.h> //gettimeofday
8 #include <time.h> //localtime_r
9 #include <fcntl.h> //open
10 #include <errno.h> //errno
11 #include <ngx_log.h>
12
13 //#include "ngx_global.h"
14 //#include "ngx_macro.h"
15 //#include "ngx_func.h"
16 #include "ngx_c_conf.h"
17
18 short ngx_pid;
19
20 //和运行日志相关
21 typedef struct
22 {
23 int log_level; //日志级别 或者日志类型,ngx_macro.h里分0-8共9个级别
24 int fd; //日志文件描述符
25
26 }ngx_log_t;
27
28
29 //全局量---------------------
30 //错误等级,和ngx_macro.h里定义的日志等级宏是一一对应关系
31 static char err_levels[][20] =
32 {
33 {"stderr"}, //0:控制台错误
34 {"emerg"}, //1:紧急
35 {"alert"}, //2:警戒
36 {"crit"}, //3:严重
37 {"error"}, //4:错误
38 {"warn"}, //5:警告
39 {"notice"}, //6:注意
40 {"info"}, //7:信息
41 {"debug"} //8:调试
42 };
43
44 ngx_log_t ngx_log;
45
46
47 //----------------------------------------------------------------------------------------------------------------------
48 //描述:通过可变参数组合出字符串【支持...省略号形参】,自动往字符串最末尾增加换行符【所以调用者不用加\n】, 往标准错误上输出这个字符串;
49 // 如果err不为0,表示有错误,会将该错误编号以及对应的错误信息一并放到组合出的字符串中一起显示;
50
51 //《c++从入门到精通》里老师讲解过,比较典型的C语言中的写法,就是这种va_start,va_end
52 //fmt:通过这第一个普通参数来寻址后续的所有可变参数的类型及其值
53 //调用格式比如:ngx_log_stderr(0, "invalid option: \"%s\",%d", "testinfo",123);
54 /*
55 ngx_log_stderr(0, "invalid option: \"%s\"", argv[0]); //nginx: invalid option: "./nginx"
56 ngx_log_stderr(0, "invalid option: %10d", 21); //nginx: invalid option: 21 ---21前面有8个空格
57 ngx_log_stderr(0, "invalid option: %.6f", 21.378); //nginx: invalid option: 21.378000 ---%.这种只跟f配合有效,往末尾填充0
58 ngx_log_stderr(0, "invalid option: %.6f", 12.999); //nginx: invalid option: 12.999000
59 ngx_log_stderr(0, "invalid option: %.2f", 12.999); //nginx: invalid option: 13.00
60 ngx_log_stderr(0, "invalid option: %xd", 1678); //nginx: invalid option: 68E
61 ngx_log_stderr(0, "invalid option: %Xd", 1678); //nginx: invalid option: 68E
62 ngx_log_stderr(15, "invalid option: %s , %d", "testInfo",326); //nginx: invalid option: testInfo , 326
63 ngx_log_stderr(0, "invalid option: %d", 1678);
64 */
65 void ngx_log_stderr(int err, const char *fmt, ...)
66 {
67 //typedef char * va_list; args就是一个char*类型的指针
68 va_list args; //创建一个va_list类型变量
69 u_char errstr[NGX_MAX_ERROR_STR+1]; //2048 -- ************ +1是我自己填的,感谢官方写法有点小瑕疵,所以动手调整一下
70 u_char *p, *last;
71
72 memset(errstr,0,sizeof(errstr)); //我个人加的,这块有必要加,至少在va_end处理之前有必要,否则字符串没有结束标记不行的;***************************
73
74 last = errstr + NGX_MAX_ERROR_STR; //last指向整个buffer最后去了【指向最后一个有效位置的后面也就是非有效位】,作为一个标记,防止输出内容超过这么长,
75 //其实我认为这有问题,所以我才在上边errstr[NGX_MAX_ERROR_STR+1]; 给加了1
76 //比如你定义 char tmp[2]; 你如果last = tmp+2,那么last实际指向了tmp[2],而tmp[2]在使用中是无效的
77
78 p = ngx_cpymem(errstr, "nginx: ", 7); //p指向"nginx: "之后
79
80 //让args指向可变参数的第一项
81 va_start(args, fmt); //使args指向起始的参数 args = ¶m1+sizeof(param1)
82
83 p = ngx_vslprintf(p,last,fmt,args); //组合出这个字符串保存在errstr里
84 va_end(args); //释放args
85
86 if (err) //如果错误代码不是0,表示有错误发生
87 {
88 //错误代码和错误信息也要显示出来
89 p = ngx_log_errno(p, last, err);
90 }
91
92 //若位置不够,那换行也要硬插入到末尾,哪怕覆盖到其他内容
93 if (p >= (last - 1))
94 {
95 p = (last - 1) - 1; //把尾部空格留出来,这里感觉nginx处理的似乎就不对
96 //我觉得,last-1,才是最后 一个而有效的内存,而这个位置要保存\0,所以我认为再减1,这个位置,才适合保存\n
97 }
98 *p++ = '\n'; //增加个换行符
99
100 //往标准错误【一般是屏幕】输出信息
101 write(STDERR_FILENO,errstr,p - errstr); //三章七节讲过,这个叫标准错误,一般指屏幕
102
103 if(ngx_log.fd > STDERR_FILENO) //如果这是个有效的日志文件,本条件肯定成立,此时也才有意义将这个信息写到日志文件
104 {
105 //因为上边已经把err信息显示出来了,所以这里就不要显示了,否则显示重复了
106 err = 0; //不要再次把错误信息弄到字符串里,否则字符串里重复了
107 p--;*p = 0; //把原来末尾的\n干掉,因为到ngx_log_err_core中还会加这个\n
108 ngx_log_error_core(NGX_LOG_STDERR,err,(const char *)errstr);
109 }
110 return;
111 }
112
113 //----------------------------------------------------------------------------------------------------------------------
114 //描述:给一段内存,一个错误编号,我要组合出一个字符串,形如: (错误编号: 错误原因),放到给的这段内存中去
115 // 这个函数我改造的比较多,和原始的nginx代码多有不同
116 //buf:是个内存,要往这里保存数据
117 //last:放的数据不要超过这里
118 //err:错误编号,我们是要取得这个错误编号对应的错误字符串,保存到buffer中
119 u_char *ngx_log_errno(u_char *buf, u_char *last, int err)
120 {
121 //以下代码是我自己改造,感觉作者的代码有些瑕疵
122 char *perrorinfo = strerror(err); //根据资料不会返回NULL;
123 size_t len = strlen(perrorinfo);
124
125 //然后我还要插入一些字符串: (%d:)
126 char leftstr[10] = {0};
127 sprintf(leftstr," (%d: ",err);
128 size_t leftlen = strlen(leftstr);
129
130 char rightstr[] = ") ";
131 size_t rightlen = strlen(rightstr);
132
133 size_t extralen = leftlen + rightlen; //左右的额外宽度
134 if ((buf + len + extralen) < last)
135 {
136 //保证整个我装得下,我就装,否则我全部抛弃 ,nginx的做法是 如果位置不够,就硬留出50个位置【哪怕覆盖掉以往的有效内容】,也要硬往后边塞,这样当然也可以;
137 buf = ngx_cpymem(buf, leftstr, leftlen);
138 buf = ngx_cpymem(buf, perrorinfo, len);
139 buf = ngx_cpymem(buf, rightstr, rightlen);
140 }
141 return buf;
142 }
143
144 //----------------------------------------------------------------------------------------------------------------------
145 //往日志文件中写日志,代码中有自动加换行符,所以调用时字符串不用刻意加\n;
146 // 日过定向为标准错误,则直接往屏幕上写日志【比如日志文件打不开,则会直接定位到标准错误,此时日志就打印到屏幕上,参考ngx_log_init()】
147 //level:一个等级数字,我们把日志分成一些等级,以方便管理、显示、过滤等等,如果这个等级数字比配置文件中的等级数字"LogLevel"大,那么该条信息不被写到日志文件中
148 //err:是个错误代码,如果不是0,就应该转换成显示对应的错误信息,一起写到日志文件中,
149 //ngx_log_error_core(5,8,"这个XXX工作的有问题,显示的结果是=%s","YYYY");
150 void ngx_log_error_core(int level, int err, const char *fmt, ...)
151 {
152 u_char *last;
153 u_char errstr[NGX_MAX_ERROR_STR+1]; //这个+1也是我放入进来的,本函数可以参考ngx_log_stderr()函数的写法;
154
155 memset(errstr,0,sizeof(errstr));
156 last = errstr + NGX_MAX_ERROR_STR;
157
158 struct timeval tv;
159 struct tm tm;
160 time_t sec; //秒
161 u_char *p; //指向当前要拷贝数据到其中的内存位置
162 va_list args;
163
164 memset(&tv,0,sizeof(struct timeval));
165 memset(&tm,0,sizeof(struct tm));
166
167 gettimeofday(&tv, NULL); //获取当前时间,返回自1970-01-01 00:00:00到现在经历的秒数【第二个参数是时区,一般不关心】
168
169 sec = tv.tv_sec; //秒
170 localtime_r(&sec, &tm); //把参数1的time_t转换为本地时间,保存到参数2中去,带_r的是线程安全的版本,尽量使用
171 tm.tm_mon++; //月份要调整下正常
172 tm.tm_year += 1900; //年份要调整下才正常
173
174 u_char strcurrtime[40]={0}; //先组合出一个当前时间字符串,格式形如:2019/01/08 19:57:11
175 ngx_slprintf(strcurrtime,
176 (u_char *)-1, //若用一个u_char *接一个 (u_char *)-1,则 得到的结果是 0xffffffff....,这个值足够大
177 "%4d/%02d/%02d %02d:%02d:%02d", //格式是 年/月/日 时:分:秒
178 tm.tm_year, tm.tm_mon,
179 tm.tm_mday, tm.tm_hour,
180 tm.tm_min, tm.tm_sec);
181 p = ngx_cpymem(errstr,strcurrtime,strlen((const char *)strcurrtime)); //日期增加进来,得到形如: 2019/01/08 20:26:07
182 p = ngx_slprintf(p, last, " [%s] ", err_levels[level]); //日志级别增加进来,得到形如: 2019/01/08 20:26:07 [crit]
183 p = ngx_slprintf(p, last, "%P: ",ngx_pid); //支持%P格式,进程id增加进来,得到形如: 2019/01/08 20:50:15 [crit] 2037:
184
185 va_start(args, fmt); //使args指向起始的参数
186 p = ngx_vslprintf(p, last, fmt, args); //把fmt和args参数弄进去,组合出来这个字符串
187 va_end(args); //释放args
188
189 if (err) //如果错误代码不是0,表示有错误发生
190 {
191 //错误代码和错误信息也要显示出来
192 p = ngx_log_errno(p, last, err);
193 }
194 //若位置不够,那换行也要硬插入到末尾,哪怕覆盖到其他内容
195 if (p >= (last - 1))
196 {
197 p = (last - 1) - 1; //把尾部空格留出来,这里感觉nginx处理的似乎就不对
198 //我觉得,last-1,才是最后 一个而有效的内存,而这个位置要保存\0,所以我认为再减1,这个位置,才适合保存\n
199 }
200 *p++ = '\n'; //增加个换行符
201
202 //这么写代码是图方便:随时可以把流程弄到while后边去;大家可以借鉴一下这种写法
203 ssize_t n;
204 while(1)
205 {
206 if (level > ngx_log.log_level)
207 {
208 //要打印的这个日志的等级太落后(等级数字太大,比配置文件中的数字大)
209 //这种日志就不打印了
210 break;
211 }
212 //磁盘是否满了的判断,先算了吧,还是由管理员保证这个事情吧;
213
214 //写日志文件
215 n = write(ngx_log.fd,errstr,p - errstr); //文件写入成功后,如果中途
216 if (n == -1)
217 {
218 //写失败有问题
219 if(errno == ENOSPC) //写失败,且原因是磁盘没空间了
220 {
221 //磁盘没空间了
222 //没空间还写个毛线啊
223 //先do nothing吧;
224 }
225 else
226 {
227 //这是有其他错误,那么我考虑把这个错误显示到标准错误设备吧;
228 if(ngx_log.fd != STDERR_FILENO) //当前是定位到文件的,则条件成立
229 {
230 n = write(STDERR_FILENO,errstr,p - errstr);
231 }
232 }
233 }
234 break;
235 } //end while
236 return;
237 }
238
239 //----------------------------------------------------------------------------------------------------------------------
240 //描述:日志初始化,就是把日志文件打开 ,注意这里边涉及到释放的问题,如何解决?
241 void ngx_log_init()
242 {
243 u_char *plogname = NULL;
244 size_t nlen;
245
246 //从配置文件中读取和日志相关的配置信息
247 CConfig *p_config = CConfig::GetInstance();
248 plogname = (u_char *)p_config->GetString("Log");
249 if(plogname == NULL)
250 {
251 //没读到,就要给个缺省的路径文件名了
252 plogname = (u_char *) NGX_ERROR_LOG_PATH; //"logs/error.log" ,logs目录需要提前建立出来
253 }
254 ngx_log.log_level = p_config->GetIntDefault("LogLevel",NGX_LOG_NOTICE);//缺省日志等级为6【注意】 ,如果读失败,就给缺省日志等级
255 //nlen = strlen((const char *)plogname);
256
257 //只写打开|追加到末尾|文件不存在则创建【这个需要跟第三参数指定文件访问权限】
258 //mode = 0644:文件访问权限, 6: 110 , 4: 100: 【用户:读写, 用户所在组:读,其他:读】 老师在第三章第一节介绍过
259 //ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT|O_DIRECT,0644); //绕过内和缓冲区,write()成功则写磁盘必然成功,但效率可能会比较低;
260 ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT,0644);
261 if (ngx_log.fd == -1) //如果有错误,则直接定位到 标准错误上去
262 {
263 ngx_log_stderr(errno,"[alert] could not open error log file: open() \"%s\" failed", plogname);
264 ngx_log.fd = STDERR_FILENO; //直接定位到标准错误去了
265 }
266 return;
267 }
ngx_printf.cpp
1 //和打印格式相关的函数放这里
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <string.h>
10 #include <stdarg.h>
11 #include <stdint.h> //类型相关头文件
12
13 #include <ngx_log.h>
14
15 typedef long long pid_t;
16 //只用于本文件的一些函数声明就放在本文件中
17 static u_char *ngx_sprintf_num(u_char *buf, u_char *last, uint64_t ui64,u_char zero, uintptr_t hexadecimal, uintptr_t width);
18
19 //----------------------------------------------------------------------------------------------------------------------
20 //对于 nginx 自定义的数据结构进行标准格式化输出,就像 printf,vprintf 一样,我们顺道学习写这类函数到底内部是怎么实现的
21 //该函数只不过相当于针对ngx_vslprintf()函数包装了一下,所以,直接研究ngx_vslprintf()即可
22 u_char *ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...)
23 {
24 va_list args;
25 u_char *p;
26
27 va_start(args, fmt); //使args指向起始的参数
28 p = ngx_vslprintf(buf, last, fmt, args);
29 va_end(args); //释放args
30 return p;
31 }
32
33 //----------------------------------------------------------------------------------------------------------------------
34 //和上边的ngx_snprintf非常类似
35 u_char * ngx_snprintf(u_char *buf, size_t max, const char *fmt, ...) //类printf()格式化函数,比较安全,max指明了缓冲区结束位置
36 {
37 u_char *p;
38 va_list args;
39
40 va_start(args, fmt);
41 p = ngx_vslprintf(buf, buf + max, fmt, args);
42 va_end(args);
43 return p;
44 }
45
46 //----------------------------------------------------------------------------------------------------------------------
47 //对于 nginx 自定义的数据结构进行标准格式化输出,就像 printf,vprintf 一样,我们顺道学习写这类函数到底内部是怎么实现的
48 //例如,给进来一个 "abc = %d",13 ,最终buf里得到的应该是 abc=13 这种结果
49 //buf:往这里放数据
50 //last:放的数据不要超过这里
51 //fmt:以这个为首的一系列可变参数
52 //支持的格式: %d【%Xd/%xd】:数字, %s:字符串 %f:浮点, %P:pid_t
53 //对于:ngx_log_stderr(0, "invalid option: \"%s\",%d", "testinfo",123);
54 //fmt = "invalid option: \"%s\",%d"
55 //args = "testinfo",123
56 u_char *ngx_vslprintf(u_char *buf, u_char *last,const char *fmt,va_list args)
57 {
58 //比如说你要调用ngx_log_stderr(0, "invalid option: \"%s\"", argv[i]);,那么这里的fmt就应该是: invalid option: "%s"
59 //printf("fmt = %s\n",fmt);
60
61 u_char zero;
62
63 uintptr_t width,sign,hex,frac_width,scale,n; //临时用到的一些变量
64
65 int64_t i64; //保存%d对应的可变参
66 uint64_t ui64; //保存%ud对应的可变参,临时作为%f可变参的整数部分也是可以的
67 u_char *p; //保存%s对应的可变参
68 double f; //保存%f对应的可变参
69 uint64_t frac; //%f可变参数,根据%.2f等,取得小数部分的2位后的内容;
70
71
72 while (*fmt && buf < last) //每次处理一个字符,处理的是 "invalid option: \"%s\",%d" 中的字符
73 {
74 if (*fmt == '%') //%开头的一般都是需要被可变参数 取代的
75 {
76 //-----------------变量初始化工作开始-----------------
77 //++fmt是先加后用,也就是fmt先往后走一个字节位置,然后再判断该位置的内容
78 zero = (u_char) ((*++fmt == '0') ? '0' : ' '); //判断%后边接的是否是个'0',如果是zero = '0',否则zero = ' ',一般比如你想显示10位,而实际数字7位,前头填充三个字符,就是这里的zero用于填充
79 //ngx_log_stderr(0, "数字是%010d", 12);
80
81 width = 0; //格式字符% 后边如果是个数字,这个数字最终会弄到width里边来 ,这东西目前只对数字格式有效,比如%d,%f这种
82 sign = 1; //显示的是否是有符号数,这里给1,表示是有符号数,除非你 用%u,这个u表示无符号数
83 hex = 0; //是否以16进制形式显示(比如显示一些地址),0:不是,1:是,并以小写字母显示a-f,2:是,并以大写字母显示A-F
84 frac_width = 0; //小数点后位数字,一般需要和%.10f配合使用,这里10就是frac_width;
85 i64 = 0; //一般用%d对应的可变参中的实际数字,会保存在这里
86 ui64 = 0; //一般用%ud对应的可变参中的实际数字,会保存在这里
87
88 //-----------------变量初始化工作结束-----------------
89
90 //这个while就是判断%后边是否是个数字,如果是个数字,就把这个数字取出来,比如%16,最终这个循环就能够把16取出来弄到width里边去
91 //%16d 这里最终width = 16;
92 while (*fmt >= '0' && *fmt <= '9') //如果%后边接的字符是 '0' --'9'之间的内容 ,比如 %16这种;
93 {
94 //第一次 :width = 1; 第二次 width = 16,所以整个width = 16;
95 width = width * 10 + (*fmt++ - '0');
96 }
97
98 for ( ;; ) //一些特殊的格式,我们做一些特殊的标记【给一些变量特殊值等等】
99 {
100 switch (*fmt) //处理一些%之后的特殊字符
101 {
102 case 'u': //%u,这个u表示无符号
103 sign = 0; //标记这是个无符号数
104 fmt++; //往后走一个字符
105 continue; //回到for继续判断
106
107 case 'X': //%X,X表示十六进制,并且十六进制中的A-F以大写字母显示,不要单独使用,一般是%Xd
108 hex = 2; //标记以大写字母显示十六进制中的A-F
109 sign = 0;
110 fmt++;
111 continue;
112 case 'x': //%x,x表示十六进制,并且十六进制中的a-f以小写字母显示,不要单独使用,一般是%xd
113 hex = 1; //标记以小写字母显示十六进制中的a-f
114 sign = 0;
115 fmt++;
116 continue;
117
118 case '.': //其后边必须跟个数字,必须与%f配合使用,形如 %.10f:表示转换浮点数时小数部分的位数,比如%.10f表示转换浮点数时,小数点后必须保证10位数字,不足10位则用0来填补;
119 fmt++; //往后走一个字符,后边这个字符肯定是0-9之间,因为%.要求接个数字先
120 while(*fmt >= '0' && *fmt <= '9') //如果是数字,一直循环,这个循环最终就能把诸如%.10f中的10提取出来
121 {
122 frac_width = frac_width * 10 + (*fmt++ - '0');
123 } //end while(*fmt >= '0' && *fmt <= '9')
124 break;
125
126 default:
127 break;
128 } //end switch (*fmt)
129 break;
130 } //end for ( ;; )
131
132 switch (*fmt)
133 {
134 case '%': //只有%%时才会遇到这个情形,本意是打印一个%,所以
135 *buf++ = '%';
136 fmt++;
137 continue;
138
139 case 'd': //显示整型数据,如果和u配合使用,也就是%ud,则是显示无符号整型数据
140 if (sign) //如果是有符号数
141 {
142 i64 = (int64_t) va_arg(args, int); //va_arg():遍历可变参数,var_arg的第二个参数表示遍历的这个可变的参数的类型
143 }
144 else //如何是和 %ud配合使用,则本条件就成立
145 {
146 ui64 = (uint64_t) va_arg(args, u_int);
147 }
148 break; //这break掉,直接跳道switch后边的代码去执行,这种凡是break的,都不做fmt++; *********************【switch后仍旧需要进一步处理】
149
150 case 'i': //转换ngx_int_t型数据,如果用%ui,则转换的数据类型是ngx_uint_t
151 if (sign)
152 {
153 i64 = (int64_t) va_arg(args, intptr_t);
154 }
155 else
156 {
157 ui64 = (uint64_t) va_arg(args, uintptr_t);
158 }
159
160 //if (max_width)
161 //{
162 // width = NGX_INT_T_LEN;
163 //}
164
165 break;
166
167 case 'L': //转换int64j型数据,如果用%uL,则转换的数据类型是uint64 t
168 if (sign)
169 {
170 i64 = va_arg(args, int64_t);
171 }
172 else
173 {
174 ui64 = va_arg(args, uint64_t);
175 }
176 break;
177
178 case 'p':
179 ui64 = (uintptr_t) va_arg(args, void *);
180 hex = 2; //标记以大写字母显示十六进制中的A-F
181 sign = 0; //标记这是个无符号数
182 zero = '0'; //前边0填充
183 width = 2 * sizeof(void *);
184 break;
185
186 case 's': //一般用于显示字符串
187 p = va_arg(args, u_char *); //va_arg():遍历可变参数,var_arg的第二个参数表示遍历的这个可变的参数的类型
188
189 while (*p && buf < last) //没遇到字符串结束标记,并且buf值够装得下这个参数
190 {
191 *buf++ = *p++; //那就装,比如 "%s" , "abcdefg",那abcdefg都被装进来
192 }
193
194 fmt++;
195 continue; //重新从while开始执行
196
197 case 'P': //转换一个pid_t类型
198 i64 = (int64_t) va_arg(args, pid_t);
199 sign = 1;
200 break;
201
202 case 'f': //一般 用于显示double类型数据,如果要显示小数部分,则要形如 %.5f
203 f = va_arg(args, double); //va_arg():遍历可变参数,var_arg的第二个参数表示遍历的这个可变的参数的类型
204 if (f < 0) //负数的处理
205 {
206 *buf++ = '-'; //单独搞个负号出来
207 f = -f; //那这里f应该是正数了!
208 }
209 //走到这里保证f肯定 >= 0【不为负数】
210 ui64 = (int64_t) f; //正整数部分给到ui64里
211 frac = 0;
212
213 //如果要求小数点后显示多少位小数
214 if (frac_width) //如果是%d.2f,那么frac_width就会是这里的2
215 {
216 scale = 1; //缩放从1开始
217 for (n = frac_width; n; n--)
218 {
219 scale *= 10; //这可能溢出哦
220 }
221
222 //把小数部分取出来 ,比如如果是格式 %.2f ,对应的参数是12.537
223 // (uint64_t) ((12.537 - (double) 12) * 100 + 0.5);
224 //= (uint64_t) (0.537 * 100 + 0.5) = (uint64_t) (53.7 + 0.5) = (uint64_t) (54.2) = 54
225 frac = (uint64_t) ((f - (double) ui64) * scale + 0.5); //取得保留的那些小数位数,【比如 %.2f ,对应的参数是12.537,取得的就是小数点后的2位四舍五入,也就是54】
226 //如果是"%.6f", 21.378,那么这里frac = 378000
227
228 if (frac == scale) //进位,比如 %.2f ,对应的参数是12.999,那么 = (uint64_t) (0.999 * 100 + 0.5) = (uint64_t) (99.9 + 0.5) = (uint64_t) (100.4) = 100
229 //而此时scale == 100,两者正好相等
230 {
231 ui64++; //正整数部分进位
232 frac = 0; //小数部分归0
233 }
234 } //end if (frac_width)
235
236 //正整数部分,先显示出来
237 buf = ngx_sprintf_num(buf, last, ui64, zero, 0, width); //把一个数字 比如“1234567”弄到buffer中显示
238
239 if (frac_width) //指定了显示多少位小数
240 {
241 if (buf < last)
242 {
243 *buf++ = '.'; //因为指定显示多少位小数,先把小数点增加进来
244 }
245 buf = ngx_sprintf_num(buf, last, frac, '0', 0, frac_width); //frac这里是小数部分,显示出来,不够的,前边填充'0'字符
246 }
247 fmt++;
248 continue; //重新从while开始执行
249
250 //..................................
251 //................其他格式符,逐步完善
252 //..................................
253
254 default:
255 *buf++ = *fmt++; //往下移动一个字符
256 continue; //注意这里不break,而是continue;而这个continue其实是continue到外层的while去了,也就是流程重新从while开头开始执行;
257 } //end switch (*fmt)
258
259 //显示%d的,会走下来,其他走下来的格式日后逐步完善......
260
261 //统一把显示的数字都保存到 ui64 里去;
262 if (sign) //显示的是有符号数
263 {
264 if (i64 < 0) //这可能是和%d格式对应的要显示的数字
265 {
266 *buf++ = '-'; //小于0,自然要把负号先显示出来
267 ui64 = (uint64_t) -i64; //变成无符号数(正数)
268 }
269 else //显示正数
270 {
271 ui64 = (uint64_t) i64;
272 }
273 } //end if (sign)
274
275 //把一个数字 比如“1234567”弄到buffer中显示,如果是要求10位,则前边会填充3个空格比如“ 1234567”
276 //注意第5个参数hex,是否以16进制显示,比如如果你是想以16进制显示一个数字则可以%Xd或者%xd,此时hex = 2或者1
277 buf = ngx_sprintf_num(buf, last, ui64, zero, hex, width);
278 fmt++;
279 }
280 else //当成正常字符,源【fmt】拷贝到目标【buf】里
281 {
282 //用fmt当前指向的字符赋给buf当前指向的位置,然后buf往前走一个字符位置,fmt当前走一个字符位置
283 *buf++ = *fmt++; //*和++优先级相同,结合性从右到左,所以先求的是buf++以及fmt++,但++是先用后加;
284 } //end if (*fmt == '%')
285 } //end while (*fmt && buf < last)
286
287 return buf;
288 }
289
290 //----------------------------------------------------------------------------------------------------------------------
291 //以一个指定的宽度把一个数字显示在buf对应的内存中, 如果实际显示的数字位数 比指定的宽度要小 ,比如指定显示10位,而你实际要显示的只有“1234567”,那结果可能是会显示“ 1234567”
292 //当然如果你不指定宽度【参数width=0】,则按实际宽度显示
293 //你给进来一个%Xd之类的,还能以十六进制数字格式显示出来
294 //buf:往这里放数据
295 //last:放的数据不要超过这里
296 //ui64:显示的数字
297 //zero:显示内容时,格式字符%后边接的是否是个'0',如果是zero = '0',否则zero = ' ' 【一般显示的数字位数不足要求的,则用这个字符填充】,比如要显示10位,而实际只有7位,则后边填充3个这个字符;
298 //hexadecimal:是否显示成十六进制数字 0:不
299 //width:显示内容时,格式化字符%后接的如果是个数字比如%16,那么width=16,所以这个是希望显示的宽度值【如果实际显示的内容不够,则后头用0填充】
300 static u_char * ngx_sprintf_num(u_char *buf, u_char *last, uint64_t ui64, u_char zero, uintptr_t hexadecimal, uintptr_t width)
301 {
302 //temp[21]
303 u_char *p, temp[NGX_INT64_LEN + 1]; //#define NGX_INT64_LEN (sizeof("-9223372036854775808") - 1) = 20 ,注意这里是sizeof是包括末尾的\0,不是strlen;
304 size_t len;
305 uint32_t ui32;
306
307 static u_char hex[] = "0123456789abcdef"; //跟把一个10进制数显示成16进制有关,换句话说和 %xd格式符有关,显示的16进制数中a-f小写
308 static u_char HEX[] = "0123456789ABCDEF"; //跟把一个10进制数显示成16进制有关,换句话说和 %Xd格式符有关,显示的16进制数中A-F大写
309
310 p = temp + NGX_INT64_LEN; //NGX_INT64_LEN = 20,所以 p指向的是temp[20]那个位置,也就是数组最后一个元素位置
311
312 if (hexadecimal == 0)
313 {
314 if (ui64 <= (uint64_t) NGX_MAX_UINT32_VALUE) //NGX_MAX_UINT32_VALUE :最大的32位无符号数:十进制是4294967295
315 {
316 ui32 = (uint32_t) ui64; //能保存下
317 do //这个循环能够把诸如 7654321这个数字保存成:temp[13]=7,temp[14]=6,temp[15]=5,temp[16]=4,temp[17]=3,temp[18]=2,temp[19]=1
318 //而且的包括temp[0..12]以及temp[20]都是不确定的值
319 {
320 *--p = (u_char) (ui32 % 10 + '0'); //把屁股后边这个数字拿出来往数组里装,并且是倒着装:屁股后的也往数组下标大的位置装;
321 }
322 while (ui32 /= 10); //每次缩小10倍等于去掉屁股后边这个数字
323 }
324 else
325 {
326 do
327 {
328 *--p = (u_char) (ui64 % 10 + '0');
329 } while (ui64 /= 10); //每次缩小10倍等于去掉屁股后边这个数字
330 }
331 }
332 else if (hexadecimal == 1) //如果显示一个十六进制数字,格式符为:%xd,则这个条件成立,要以16进制数字形式显示出来这个十进制数,a-f小写
333 {
334 //比如我显示一个1,234,567【十进制数】,他对应的二进制数实际是 12 D687 ,那怎么显示出这个12D687来呢?
335 do
336 {
337 //0xf就是二进制的1111,大家都学习过位运算,ui64 & 0xf,就等于把 一个数的最末尾的4个二进制位拿出来;
338 //ui64 & 0xf 其实就能分别得到 这个16进制数也就是 7,8,6,D,2,1这个数字,转成 (uint32_t) ,然后以这个为hex的下标,找到这几个数字的对应的能够显示的字符;
339 *--p = hex[(uint32_t) (ui64 & 0xf)];
340 } while (ui64 >>= 4); //ui64 >>= 4 ---> ui64 = ui64 >> 4 ,而ui64 >> 4是啥,实际上就是右移4位,就是除以16,因为右移4位就等于移动了1111;
341 //相当于把该16进制数的最末尾一位干掉,原来是 12 D687, >> 4后是 12 D68,如此反复,最终肯定有=0时导致while不成立退出循环
342 //比如 1234567 / 16 = 77160(0x12D68)
343 // 77160 / 16 = 4822(0x12D6)
344 }
345 else // hexadecimal == 2 //如果显示一个十六进制数字,格式符为:%Xd,则这个条件成立,要以16进制数字形式显示出来这个十进制数,A-F大写
346 {
347 //参考else if (hexadecimal == 1),非常类似
348 do
349 {
350 *--p = HEX[(uint32_t) (ui64 & 0xf)];
351 } while (ui64 >>= 4);
352 }
353
354 len = (temp + NGX_INT64_LEN) - p; //得到这个数字的宽度,比如 “7654321”这个数字 ,len = 7
355
356 while (len++ < width && buf < last) //如果你希望显示的宽度是10个宽度【%12f】,而实际想显示的是7654321,只有7个宽度,那么这里要填充5个0进去到末尾,凑够要求的宽度
357 {
358 *buf++ = zero; //填充0进去到buffer中(往末尾增加),比如你用格式
359 //ngx_log_stderr(0, "invalid option: %10d\n", 21);
360 //显示的结果是:nginx: invalid option: 21 ---21前面有8个空格,这8个弄个,就是在这里添加进去的;
361 }
362
363 len = (temp + NGX_INT64_LEN) - p; //还原这个len,也就是要显示的数字的实际宽度【因为上边这个while循环改变了len的值】
364 //现在还没把实际的数字比如“7654321”往buf里拷贝呢,要准备拷贝
365
366 //如下这个等号是我加的【我认为应该加等号】,nginx源码里并没有加;***********************************************
367 if((buf + len) >= last) //发现如果往buf里拷贝“7654321”后,会导致buf不够长【剩余的空间不够拷贝整个数字】
368 {
369 len = last - buf; //剩余的buf有多少我就拷贝多少
370 }
371
372 return ngx_cpymem(buf, p, len); //把最新buf返回去;
373 }