ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

curl 内部时间管理机制解析:从 curlx_now() 到 Curl_pgrs_now() 的演进与实现

curl 内部时间管理机制解析:从 curlx_now() 到 Curl_pgrs_now() 的演进与实现 curl 内部时间管理机制解析从 curlx_now() 到 Curl_pgrs_now() 的演进与实现【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl导读本文基于 curl 仓库内部设计文档 docs/internals/TIME-KEEPING.md系统剖析 libcurl 如何在传输过程中获取与维护当前时间超时判定、低速检测、速率限制、事件时间线timeline记录都依赖一套统一的时间来源。全文围绕curlx_now()与Curl_pgrs_now()两个核心接口还原从循环内取一次时间并向下传递指针的旧方案到时间戳就地存放、按需获取新方案的演进原因与源码级实现。读完本文你将掌握 curl 内部时间戳的存放位置、获取路径、跨平台单调时钟实现以及多句柄multi handle状态机中时间是如何被消费的。一、传输为什么需要当前时间数据传输过程天然与时间强耦合libcurl 在以下场景中必须读取当前时间超时控制CURLOPT_TIMEOUT总超时、CURLOPT_CONNECTTIMEOUT连接超时、CURLOPT_LOW_SPEED_TIME等选项需要把剩余时间与流逝的时间做减法进度与速率统计计算下载/上传速度bytes/sec决定进度回调触发频率低速检测low-speed limit连续一段时间低于速度阈值即判定CURLE_OPERATION_TIMEDOUT速率限制rate limitingCURLOPT_MAX_RECV_SPEED_LARGE/CURLOPT_MAX_SEND_SPEED_LARGE需要按时间窗发放流量令牌事件时间线记录为curl_easy_getinfo()提供CURLINFO_NAMELOOKUP_TIME、CURLINFO_CONNECT_TIME、CURLINFO_APPCONNECT_TIME、CURLINFO_STARTTRANSFER_TIME等统计数据。正如文档开篇所言Transfers need the current time to handle timeouts and keep a record of events.传输需要当前时间来处理超时并记录事件。二、统一时间入口curlx_now() 与单调时钟当前时间获取函数是curlx_now()在大多数平台上它基于单调时钟monotonic clock。这意味着返回的时间戳只增不减时间永远向前推进但它并不是真实世界的墙上时钟——CURLINFO_*_TIME等统计数据是相对差值因此不需要与墙上时钟对齐。struct curltime的结构定义在 lib/curlx/timeval.hstruct curltime { time_t tv_sec; /* seconds */ int tv_usec; /* microseconds */ };curlx_now()的实现在 lib/curlx/timeval.c它只是对内部函数curlx_pnow()的封装struct curltime curlx_now(void) { struct curltime now; curlx_pnow(now); return now; }2.1 跨平台实现矩阵curlx_pnow()通过条件编译在不同平台选择最优时间源其完整优先级如下对应 lib/curlx/timeval.c平台 / 编译宏时间源特性_WIN32QueryPerformanceCounter()高精度性能计数器使用前必须先调用curlx_now_init()取得QueryPerformanceFrequency()频率timeval.cHAVE_CLOCK_GETTIME_MONOTONIC_RAWclock_gettime(CLOCK_MONOTONIC_RAW, ...)最纯净的单调时钟不受 NTP 调整影响HAVE_CLOCK_GETTIME_MONOTONICclock_gettime(CLOCK_MONOTONIC, ...)标准单调时钟HAVE_MACH_ABSOLUTE_TIMEmacOSmach_absolute_time()需通过mach_timebase_info()换算纳秒timeval.cHAVE_GETTIMEOFDAYgettimeofday()兜底方案不保证单调可能因时钟漂移或时间同步发生前后跳变源码注释明确说明了这一风险其他time(NULL)最简陋兜底仅秒级精度值得注意的两处细节CLOCK_MONOTONIC_RAW优先于CLOCK_MONOTONIC当两者都可用时curl 优先尝试 RAW 版本失败后才回退到普通单调时钟timeval.c运行时回退即使配置阶段检测到单调时钟可用运行时仍可能调用失败例如 Apple SDK 将clock_gettime声明为弱符号老版本系统上调用会崩溃因此代码中用__builtin_available()做运行时检查失败后回退到gettimeofday()timeval.c。2.2 配套时间差工具除了取时间本身lib/curlx/timeval.h 还提供了一组时间差计算工具全部要求第一个参数newer比第二个参数older更新否则会得到负的时间差curlx_timediff_ms()/curlx_ptimediff_ms()毫秒级差值过大差值会被钳制到TIMEDIFF_T_MAX/TIMEDIFF_T_MINcurlx_timediff_ceil_ms()毫秒级差值并向上取整curlx_timediff_us()/curlx_ptimediff_us()微秒级差值curlx_us_to_ceil_ms()微秒向上取整转毫秒。curlx_ptimediff_ms的实现还带有unittest: 1323注释标记timeval.c说明该函数由单元测试覆盖读者可在tests/unit中检索对应测试。三、历史方案Initial Approach循环入口取一次时间并向下传指针文档中详细记录了旧方案的来龙去脉这对理解新方案至关重要。3.1 旧方案的做法多句柄的事件处理循环loop processing在循环开头调用一次curlx_now()然后把struct curltime now的指针一路传递给所有可能涉及时间的函数试图让它们共用同一个循环内时间快照从而节省重复调用系统时钟的开销。3.2 为什么被废弃旧方案存在两个致命问题API 污染把now指针作为参数层层下传会pollute the internal APIs——内部函数的签名被时间参数绑架任何深层的函数都不得不接受一个与自身职责无关的指针时间倒流jumps in time有些函数继续自行调用curlx_now()获取实时时间而另一些函数使用的是循环开头传入的过去的时间戳指针。两者混用导致同一次传输经历的时间线出现跳跃因果倒置——在快速机器上通常不明显但在慢速机器或 CI 环境下会引发偶发且令人恼火的测试失败。尤其严重的是当 curl 增加了传输时间线必须按序排列的断言后问题被放大。所谓时间线即事件发生的先后顺序queue - namelookup - connect - appconnect - ...排队 → 域名解析 → TCP 连接 → TLS/应用层连接 → ……如果某处用了过期的now指针而另一处用了新鲜的curlx_now()这条时间线的单调性就可能被破坏从而触发断言失败。这正是旧方案被推翻的直接原因。四、修订方案Revised Approach时间戳就地存放、按需获取新策略在文档中被总结为三条铁律并已在当前源码中完整落地Keep a now timestamp in the multi handle. Keep a fallback now timestamp in the easy handle.Always useCurl_pgrs_now(data)to get the current time of a transfer.Do not usecurlx_now()directly for transfer handling (exceptions apply for loops).4.1 时间戳存放在哪里从当前源码看now 时间戳实际存放于每个 easy 句柄的进度结构体中。lib/urldata.h 中struct Progress的第一个字段即是struct Progress { struct curltime now; /* current time of processing */ struct curltime start; /* when transfer was initialized, set once */ ... };文档描述的设计意图是在 multi 层维护统一时间戳、在 easy 层保存 fallback落地实现中now与start都挂在Curl_easy的progress成员上multi 状态机通过data句柄统一访问多传输之间天然共享同一套时间管理代码路径。4.2 统一获取入口Curl_pgrs_now(data)所有传输处理代码必须通过Curl_pgrs_now(data)获取当前时间。它的实现非常简洁lib/progress.cconst struct curltime *Curl_pgrs_now(struct Curl_easy *data) { curlx_pnow(data-progress.now); return data-progress.now; }关键点它调用curlx_pnow()就地更新data-progress.now然后返回指向该字段的const指针。这就实现了文档所说的三个优点无需向下传struct curltime或过时时间戳的指针——每个函数需要时直接Curl_pgrs_now(data)即可无需提前计算精确的 now——延迟到真正使用的那一刻才刷新避免取早了导致的过期时间const指针优于结构体传值而所有传输共享同一内存位置更新则更优——同一份内存被反复就地刷新调用方永远读到最新值且避免了结构体的值拷贝开销。该接口的声明位于 lib/progress.h。4.3 唯一例外循环内部文档明确指出例外exceptions apply for loops循环本身仍可直接使用curlx_now()。典型例子在 easy 句柄的事件驱动循环中lib/easy.c 用curlx_now()记录 poll 开始时间用于计算本轮 poll 实际等待了多久从而在下一次超时值中扣减timediff_t spent_ms curlx_timediff_ms(curlx_now(), start); if(spent_ms 0) { if(spent_ms ev-ms) ev-ms 0; else ev-ms - (long)spent_ms; }这类测量循环自身耗时的场景与单个传输的时间线无关属于合理使用curlx_now()的例外。4.4 注意事项Caveat文档给出的唯一警告是不要把Curl_pgrs_now(data)返回的指针保存到超出当前代码调用范围的地方。因为该指针指向data-progress.now这块会被反复就地刷新的内存任何持久的引用都会读到不断变化的最新值而非快照如果需要保留历史时刻必须把struct curltime按值拷贝走例如data-progress.start、state.keeps_speed等都是按值保存的时间点。五、源码级消费路径时间在 multi 状态机中的流向Curl_pgrs_now()在 libcurl 各模块中被广泛使用仅 lib/progress.c、lib/multi.c、lib/transfer.c 就有数十处调用Curl_pgrs_now还出现在lib/http.c、lib/ftp.c、lib/smtp.c、lib/connect.c、lib/vtls/openssl.c等 30 余个文件中。以下梳理几条典型的消费链路。5.1 事件时间线Curl_pgrsTime 与 timerid 枚举时间线timeline的里程碑由timerid枚举定义lib/progress.htypedef enum { TIMER_NONE, TIMER_STARTOP, /* 传输开始 */ TIMER_STARTSINGLE, /* 单个请求开始可能进入排队 */ TIMER_POSTQUEUE, /* 出队后立即 */ TIMER_NAMELOOKUP, /* 域名解析 */ TIMER_CONNECT, /* 连接 */ TIMER_APPCONNECT, /* 应用层连接TLS 等 */ TIMER_PRETRANSFER, TIMER_STARTTRANSFER, TIMER_POSTRANSFER, TIMER_REDIRECT, TIMER_LAST /* must be last */ } timerid;这与文档中提及的queue - namelookup - connect - appconnect时间线一一对应。时间戳记录分为两个接口lib/progress.cCurl_pgrsTimeWas(data, timer, timestamp)把给定的时间戳按标签记录并计算与progress.start的差值累加到对应统计项Curl_pgrsTime(data, timer)内部就是Curl_pgrsTimeWas(data, timer, *Curl_pgrs_now(data))——先取最新时间再记录该函数带有unittest: 1399注释标记。其中TIMER_STARTTRANSFER还有一个防重复逻辑startransfer_added标志保证除非发生重定向否则t_starttransfer只记录第一次progress.c避免多次触发覆盖掉真实的首个字节到达时间。5.2 状态机初始化与超时注入在 multi 状态机进入MSTATE_SETUP时lib/multi.cmultistate_setup()用最新时间完成两件事const struct curltime *pnow Curl_pgrs_now(data); Curl_pgrsTimeWas(data, TIMER_STARTSINGLE, *pnow); if(data-set.timeout) Curl_expire_set(data, EXPIRE_TIMEOUT,>pnow Curl_pgrs_now(data); if(CURL_REQ_WANT_IO(data)) { if(Curl_timeleft_now_ms(data, pnow) 0) { failf(data, Operation timed out after ... milliseconds with ... bytes received); result CURLE_OPERATION_TIMEDOUT; goto out; } }正是因为每次循环迭代都从同一内存位置刷新时间旧方案中部分函数用过期时间、部分用实时时间导致超时判断失准的问题在这里被彻底消除。5.4 低速限制检测低速检测函数pgrs_speedcheck()lib/progress.c体现了按值保存历史时刻的正确姿势当瞬时速度低于low_speed_limit时它把*pnow按值拷入data-state.keeps_speed记录开始变慢的时刻之后每次检测用curlx_ptimediff_ms(pnow, data-state.keeps_speed)计算已持续的低速时长一旦超过low_speed_time即返回CURLE_OPERATION_TIMEDOUT。这里keeps_speed是值拷贝的时间点而pnow是每次刷新的最新时间——正是不要存储指针、需要历史点就拷值原则的活教材。5.5 速率限制rate limitingmulti 层的mspeed_check()lib/multi.c也遵循同一模式先Curl_pgrs_now(data)取当前时间再交给Curl_rlimit_wait_ms()计算距离下一个流量令牌发放还需等待多久随后用Curl_expire_set(data, EXPIRE_TOOFAST, ..., pnow)注册下一次唤醒。整个速率限制子系统相关文档见 docs/internals/RATELIMITS.md都建立在这套统一时间基准之上。5.6 传输开始与进度统计Curl_pgrsStart()lib/progress.c在传输初始化时把progress.start置为当前时间并清零所有统计项Curl_pgrsDone()progress.c则在传输结束时执行最终强制的进度更新。Curl_pgrsUpdate()、progress_calc()等也都统一从Curl_pgrs_now(data)取时间计算速率与剩余时间。六、对开发者的启示与最佳实践综合文档与源码这套时间管理机制沉淀出几条可迁移到其他项目的最佳实践循环内集中取时按需刷新不要在每层函数里随意调用系统时钟而是让当前时间集中存放于句柄对象中统一入口此处为Curl_pgrs_now()就地刷新不要传递时间戳指针函数签名不应被now参数污染谁需要时间谁自己取取到的一定是最新值需要历史时刻就按值拷贝struct curltime是 16 字节左右的小结构按值保存毫无压力切勿保存指向可刷新内存的指针用单调时钟而非墙上时钟只要目的是测间隔超时、速率、耗时统计单调时钟就能规避 NTP 调整、闰秒、手动改系统时间导致的跳变只有需要展示给用户的绝对时刻才用墙上时钟为时间线单调性加断言文档提到的时间线顺序断言queue → namelookup → connect → appconnect是发现时间倒流类 bug 的高性价比手段。七、总结curl 的内部时间管理经历了从循环开头取一次时间、指针向下传递到时间戳就地存放于struct Progress.now、通过Curl_pgrs_now(data)统一按需刷新的演进。这一设计消除了过时时间戳引发的时间跳跃问题保证了 multi 状态机中超时、低速检测、速率限制与时间线统计的一致性。核心代码集中在 lib/curlx/timeval.c跨平台单调时钟与 lib/progress.cCurl_pgrs_now/Curl_pgrsTime/Curl_pgrsTimeWas与进度统计设计初衷与演进理由则完整记录在 docs/internals/TIME-KEEPING.md 中。理解这套机制不仅有助于阅读 libcurl 源码也为自研网络库的时间管理设计提供了现成的、经过生产验证的范本。【免费下载链接】curlA command line tool and library for transferring data with URL syntax, supporting DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET, TFTP, WS and WSS. libcurl offers a myriad of powerful features项目地址: https://gitcode.com/GitHub_Trending/cu/curl创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表