IO多路复用-select的使用详解【C语言】

1.多进程/线程并发和IO多路复用的对比

IO多路转接也称为IO多路复用,它是一种网络通信的手段(机制),通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了。通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有:select、poll、epoll。

下面先对多线程/多进程并发和IO多路转接的并发处理流程进行对比(服务器端):

多线程/多进程并发

  1. 主线程/父进程

    • 调用accept()阻塞等待客户端连接请求。
    • 当有新的连接请求到达时,解除阻塞,建立连接并创建一个新的子线程或子进程来处理该连接。
  2. 子线程/子进程

    • 与客户端进行通信,使用read() / recv()来接收数据,如果没有数据则阻塞,直到数据到达。
    • 使用write() / send()来发送数据,如果写缓冲区满了则阻塞,直到可以写入数据。
  3. 优点

    • 简单直观,编程模型类似于顺序执行。
    • 可以利用多核处理器的优势,实现真正的并行处理。
  4. 缺点

    • 每个连接需要一个单独的线程或进程,创建和销毁线程/进程的开销较大。
    • 系统资源消耗较高,特别是在面对大量连接时,上下文切换开销增加。

IO多路复用并发

  1. IO多路复用

    • 使用selectpollepoll等函数委托操作系统监视多个文件描述符(包括监听和通信的文件描述符)。
    • 调用这些函数会阻塞进程,直到有文件描述符准备好IO操作(连接请求到达或有数据可读写)。
  2. 监听文件描述符

    • 一旦有新的连接请求到达,不会阻塞程序,因为select等函数已经通知监听文件描述符就绪。
    • 可以立即调用accept()接受连接,而不用等待。
  3. 通信文件描述符

    • 一旦有数据可读或可写,对应的通信操作不会阻塞,可以直接进行读写操作。
    • 不同于多线程/多进程模型,避免了为每个连接创建线程或进程的开销,大大减少了系统资源消耗。
  4. 优点

    • 系统开销小,不必频繁创建和销毁线程/进程。
    • 可以处理大量连接而不受资源限制的影响,效率较高。
  5. 缺点

    • 编程复杂度较高,因为需要管理和维护多个文件描述符的状态。
    • 在某些情况下,如文件描述符数量非常大时,性能可能略逊于epoll等更高级的IO复用机制。

2.select的使用

在IO多路复用中,select是最基础和最古老的一种实现方式之一。select函数允许程序同时监视多个文件描述符(sockets、stdin、stdout等),并在其中任何一个文件描述符准备好进行IO操作(如读或写)时通知程序。

程序猿通过调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态:

  • 读缓冲区:检测里边有没有数据,如果有数据该缓冲区对应的文件描述符就绪
  • 写缓冲区:检测写缓冲区是否可以写(有没有容量),如果有容量可以写,缓冲区对应的文件描述符就绪
  • 读写异常:检测读写缓冲区是否有异常,如果有该缓冲区对应的文件描述符就绪

委托检测的文件描述符被遍历检测完毕之后,已就绪的这些满足条件的文件描述符会通过select()的参数分3个集合传出,程序猿得到这几个集合之后就可以分情况依次处理了。

2.1select函数

select函数是一个用于IO多路复用的系统调用,允许程序监视多个文件描述符(sockets、stdin、stdout等),并在其中任何一个文件描述符准备好进行IO操作(如读或写)时通知程序。

#include <sys/select.h>
struct timeval {time_t      tv_sec;         /* seconds */suseconds_t tv_usec;        /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明nfds:监视的所有文件描述符中最大的文件描述符加1。通常可以通过计算出最大的文件描述符值加1来设置这个参数,例如 (fd + 1),其中 fd 是最大的文件描述符。内核需要线性遍历这些集合中的文件描述
符,这个值是循环结束的条件readfds:要监视读操作的文件描述符集合。传入传出参数,读集合一般情况下都是需要检测的,这样
才知道通过哪个文件描述符接收数据writefds:要监视写操作的文件描述符集合。传入传出参数,如果不需要使用这个参数可以指定为NULLexceptfds:要监视异常情况的文件描述符集合(一般设置为 NULL,表示不关心异常情况)。传入传出
参数,如果不需要使用这个参数可以指定为NULLtimeout:超时时间,用于设置select函数的阻塞时间。它可以指定程序阻塞的最长时间。如果设置为 NULL,select将一直阻塞,直到有事件发生;如果设置为指向具有零时间的 timeval 结构的指针,则
select将立即返回;如果 timeout 不为 NULL,并且 select 在指定时间内没有检测到事件,则返回超时。
返回值说明-1:如果 select 函数调用失败,通常是由于参数错误或者系统调用中断(如信号中断)。0:如果 select 超时,即在指定的超时时间内没有文件描述符准备好进行IO操作。大于 0:返回已经准备好进行IO操作的文件描述符的数量。在返回时,fd_set 集合中会被修改,标识
出哪些文件描述符已经准备好。

注意事项和细节

  • 文件描述符集合的操作

    • 在调用 select 之前,需要使用 FD_ZEROFD_SET 等函数来准备好 readfdswritefdsexceptfds 这些文件描述符集合。
  • 文件描述符数量限制

    • 某些系统对单个进程能够监视的文件描述符数量有限制,可以通过 getrlimit() 函数查询当前进程的文件描述符数量限制。
  • 超时设置

    • 如果不需要超时等待,可以将 timeout 参数设置为 NULL,此时 select 函数将一直阻塞,直到有文件描述符准备好。
  • 并发性和效率

    • select 函数的效率可能会随着文件描述符数量的增加而下降,因为它在内核中需要线性扫描整个文件描述符集合。
  • 可移植性

    • select 函数几乎在所有主流操作系统上都有实现,因此具有较好的跨平台性。

另外初始化fd_set类型的参数还需要使用相关的一些列操作函数,具体如下:

1. void FD_CLR(int fd, fd_set *set);
功能:从文件描述符集合中删除指定的文件描述符。
参数:
fd:要从集合中删除的文件描述符。
set:要操作的文件描述符集合指针。
作用:将文件描述符集合 set 中的 fd 对应的标志位清零,即将其从集合中移除。
2. int FD_ISSET(int fd, fd_set *set);
功能:检查文件描述符集合中特定文件描述符的状态。
参数:
fd:要检查的文件描述符。
set:要操作的文件描述符集合指针。
返回值:
如果文件描述符 fd 在 set 集合中被设置(即处于就绪状态),返回非零值;
否则返回0。
作用:通常用于在调用 select 函数后,检查哪些文件描述符已经准备好进行IO操作。
3. void FD_SET(int fd, fd_set *set);
功能:向文件描述符集合中添加指定的文件描述符。
参数:
fd:要添加的文件描述符。
set:要操作的文件描述符集合指针。
作用:将文件描述符集合 set 中的 fd 对应的标志位设置为1,表示将该文件描述符加入到监视列表中。
4. void FD_ZERO(fd_set *set);
功能:清空文件描述符集合。
参数:
set:要操作的文件描述符集合指针。
作用:将文件描述符集合 set 中的所有标志位清零,即清除所有文件描述符的状态,使集合为空集。

2.2细节说明

在select()函数中第2、3、4个参数都是fd_set类型,它表示一个文件描述符的集合,类似于信号集 sigset_t,这个类型的数据有128个字节,也就是1024个标志位,和内核中文件描述符表中的文件描述符个数是一样的。

sizeof(fd_set) = 128 字节 * 8 = 1024 bit      // int [32]

这并不是巧合,而是故意为之。这块内存中的每一个bit 和 文件描述符表中的每一个文件描述符是一一对应的关系,这样就可以使用最小的存储空间将要表达的意思描述出来了。

下图中的fd_set中存储了要委托内核检测读缓冲区的文件描述符集合。

  • 如果集合中的标志位为0代表不检测这个文件描述符状态
  • 如果集合中的标志位为1代表检测这个文件描述符状态

内核在遍历这个读集合的过程中,如果被检测的文件描述符对应的读缓冲区中没有数据,内核将修改这个文件描述符在读集合fd_set中对应的标志位,改为0,如果有数据那么这个标志位的值不变,还是1。

当select()函数解除阻塞之后,被内核修改过的读集合通过参数传出,此时集合中只要标志位的值为1,那么它对应的文件描述符肯定是就绪的,我们就可以基于这个文件描述符和客户端建立新连接或者通信了。

2.3select处理流程

  • 初始化准备

    • 创建监听的套接字 lfd,并设置为非阻塞模式(可选但推荐,可以通过 fcntl 函数实现)。
    • 绑定套接字到本地的IP和端口,使用 bind() 函数。
    • 开始监听连接请求,使用 listen() 函数。
  • 文件描述符集合准备

    • 创建一个 fd_set 结构体对象,用于存储所有需要监视的文件描述符。
    • 使用 FD_ZERO() 初始化该结构体,清空所有文件描述符的状态。
    • 使用 FD_SET() 将监听套接字 lfd 添加到监视集合中。
  • 循环处理事件

    • 进入主循环,周期性地调用 select() 函数来等待文件描述符的就绪事件。
  • 调用 select() 函数

    • 使用 select() 函数阻塞等待事件发生。一旦有文件描述符就绪(可以读或写),select() 函数将返回。
  • 处理就绪事件

    • 使用 FD_ISSET() 函数遍历文件描述符集合,检查哪些文件描述符已经就绪。
    • 如果监听套接字 lfd 就绪,表示有新的客户端连接请求到来:
      • 使用 accept() 函数接受新连接,并创建新的通信套接字 cfd
      • 将新的通信套接字 cfd 添加到文件描述符集合中,使用 FD_SET()
      • 如果 cfd 大于当前的 max_fd,更新 max_fd
      • 输出连接的客户端的 IP、端口和文件描述符信息。
    • 如果是其他通信套接字 cfd 就绪,表示有数据到达或可以发送:
      • 调用相应的读取和写入函数处理客户端的数据收发操作。
      • 如果读取返回0,表示客户端关闭连接,应该调用 close() 关闭套接字,并使用 FD_CLR() 将其从文件描述符集合中移除。
      • 如果写入遇到缓冲区满的情况,可以选择继续尝试写入或者延迟处理。
  • 错误处理和异常情况

    • 在调用 select()accept()read()write() 等函数时,需要适时处理可能的错误情况和异常。
    • 对于非阻塞套接字,当 accept() 返回 EAGAINEWOULDBLOCK 错误时,应适当延迟重试或放入队列等待处理。
  • 循环迭代

    • 回到第3步,继续等待和处理下一轮的事件。

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>#define MAX_CLIENTS 10
#define PORT 9999int main() {int lfd, cfd, maxfd, activity, i, valread;int client_socket[MAX_CLIENTS] = {0};fd_set readfds, rdtemp;struct sockaddr_in addr, cliaddr;socklen_t cliLen = sizeof(cliaddr);char buffer[1024];// 创建监听的套接字if ((lfd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置服务器地址结构addr.sin_family = AF_INET;addr.sin_port = htons(PORT);addr.sin_addr.s_addr = INADDR_ANY;// 绑定套接字到本地IP和端口if (bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 开始监听连接if (listen(lfd, 128) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Waiting for connections on port %d...\n", PORT);// 初始化文件描述符集合FD_ZERO(&readfds);FD_SET(lfd, &readfds);  // 将监听套接字加入集合maxfd = lfd;            // 初始时最大的文件描述符是监听套接字while (1) {// 阻塞等待文件描述符就绪rdtemp = readfds;activity = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);if (activity < 0 && errno != EINTR) {perror("select error");}// 处理新连接if (FD_ISSET(lfd, &rdtemp)) {if ((cfd = accept(lfd, (struct sockaddr *)&cliaddr, &cliLen)) < 0) {perror("accept failed");exit(EXIT_FAILURE);}printf("New connection: socket fd is %d, IP is : %s, port : %d\n",cfd, inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));// 将新的通信套接字加入集合for (i = 0; i < MAX_CLIENTS; i++) {if (client_socket[i] == 0) {client_socket[i] = cfd;break;}}// 更新最大文件描述符if (cfd > maxfd) {maxfd = cfd;}FD_SET(cfd, &readfds);  // 将新连接的套接字加入集合}// 处理客户端消息for (i = 0; i < MAX_CLIENTS; i++) {cfd = client_socket[i];if (FD_ISSET(cfd, &rdtemp)) {valread = read(cfd, buffer, sizeof(buffer));if (valread == 0) {// 客户端关闭连接getpeername(cfd, (struct sockaddr *)&cliaddr, &cliLen);printf("Host disconnected, IP %s, port %d, fd %d\n",inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), cfd);close(cfd);FD_CLR(cfd, &readfds);  // 从集合中移除套接字client_socket[i] = 0;} else {// 回显客户端消息buffer[valread] = '\0';printf("Client %s, fd %d: %s\n", inet_ntoa(cliaddr.sin_addr), cfd, buffer);// 发送回显数据给客户端send(cfd, buffer, strlen(buffer), 0);}}}}return 0;
}

在上面的代码中,创建了两个fd_set变量,用于保存要检测的读集合:

// 初始化检测的读集合
fd_set rdset;        //要检测的位置集合
fd_set rdtemp;       //这些检测位置集合中有变化的的集合,就是有就绪状态的文件描述符

rdset用于保存要检测的原始数据,这个变量不能作为参数传递给select函数,因为在函数内部这个变量中的值会被内核修改,函数调用完毕返回之后,里边就不是原始数据了,大部分情况下是值为1的标志位变少了,不可能每一轮检测,所有的文件描述符都是就行的状态。因此需要通过rdtemp变量将原始数据传递给内核,select() 调用完毕之后再将内核数据传出,这两个变量的功能是不一样的。

客户端代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>int main()
{// 1. 创建用于通信的套接字int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd == -1){perror("socket");exit(0);}// 2. 连接服务器struct sockaddr_in addr;addr.sin_family = AF_INET;     // ipv4addr.sin_port = htons(9999);   // 服务器监听的端口, 字节序应该是网络字节序inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));if(ret == -1){perror("connect");exit(0);}// 通信while(1){// 读数据char recvBuf[1024];// 写数据// sprintf(recvBuf, "data: %d\n", i++);fgets(recvBuf, sizeof(recvBuf), stdin);write(fd, recvBuf, strlen(recvBuf)+1);// 如果客户端没有发送数据, 默认阻塞read(fd, recvBuf, sizeof(recvBuf));printf("recv buf: %s\n", recvBuf);sleep(1);}// 释放资源close(fd); return 0;
}

虽然使用select这种IO多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:

  • 待检测集合(第2、3、4个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
  • 内核对于select传递进来的待检测集合的检测方式是线性的
  • 如果集合内待检测的文件描述符很多,检测效率会比较低
  • 如果集合内待检测的文件描述符相对较少,检测效率会比较高
  • 使用select能够检测的最大文件描述符个数有上限,默认是1024,这是在内核中被写死了的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/47422.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构进阶】二叉搜索树

&#x1f525;个人主页&#xff1a; Forcible Bug Maker &#x1f525;专栏&#xff1a; C || 数据结构 目录 &#x1f308;前言&#x1f525;二叉搜索树&#x1f525; 二叉搜索树的实现Insert&#xff08;插入&#xff09;find&#xff08;查找&#xff09;erase(删除)destro…

分布式锁、Lua脚本、redisson、运行lua脚本优化代码

20240721 一、分布式锁1. 什么是分布式锁2. 分布式锁的实现3. 基于redis的分布式锁4 总结 二、对于lua脚本可以保证事务&#xff0c;要么成功要么失败。1. 在redis中调用lua脚本 三、Redisson1 步骤2. Redisson的总结3. 几种分布式锁的区别 三、优化我们的秒杀1. 我们在创建优惠…

Docker安装笔记

1. Mac安装Docker 1.1 Docker安装包下载 1.1.1 阿里云 对于10.10.3以下的用户 推荐使用 对于10.10.3以上的用户 推荐使用 1.1.2 官网下载 系统和芯片选择适合自己的安装包 1.2 镜像加速 【推荐】阿里镜像 登陆后&#xff0c;左侧菜单选中镜像加速器就可以看到你的专属地…

windows和linux的等保加固测评的经验分享

一头等保加固测评的牛马&#xff0c;需要能做到一下午测评n个服务器 接下来就讲讲如何当一头xxxxxxxxx》严肃的等保测评加固的经验分享&#xff08; 一、window等保 首先你要自己按着教程在虚拟机做过一遍&#xff08;win2012和win2008都做过一遍&#xff0c;大概windows的…

pico+unity3d 射线交互教程

前期配置&#xff1a;环境配置参考教程一&#xff0c;手部模型参考教程二&#xff0c;场景基于上一篇搭建。 最终效果&#xff1a;手部射线&#xff08;初始不可见&#xff09;对准 UI 显示&#xff0c;按下手柄 Trigger 键与可交互 UI&#xff08;如 Button、Toggle、Slider …

论文解读 | ICML2024:突破Transformer上下文学习中的瓶颈

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 作者简介 付靖文&#xff0c;西安交通大学博士生 简介 上下文学习&#xff0c;即从上下文示例中学习&#xff0c;是Transformer一项令人印象深刻的能力。然而&#xff0c;由于学习瓶颈的出现——在训练过程中模…

移动UI:任务中心的作用,该如何设计更合理?

任务中心是移动应用中用于展示和管理用户待办任务、提醒事项、用户福利、打卡签到等内容的功能模块。合理设计任务中心可以提升用户体验和工作效率。 以下是一些设计任务中心的合理建议&#xff1a; 1. 易于查看和管理&#xff1a; 任务中心的设计应该使用户能够快速、直观地…

C# 实现跨进程条件变量

C# 进程通信系列 第一章 共享内存 第二章 条件变量&#xff08;本章&#xff09; 第三章 消息队列 文章目录 C# 进程通信系列前言一、关键实现1、用到的主要对象2、初始化区分创建和打开3、变量放到共享内存4、等待和释放逻辑 二、完整代码三、使用示例1、同步控制2、跨进程控…

202496读书笔记|《飞花令·菊(中国文化·古典诗词品鉴)》——荷尽已无擎雨盖,菊残犹有傲霜枝

202496读书笔记|《飞花令菊&#xff08;中国文化古典诗词品鉴&#xff09;》——荷尽已无擎雨盖&#xff0c;菊残犹有傲霜枝 《飞花令菊&#xff08;中国文化古典诗词品鉴&#xff09;》素心落雪 编著。飞花令得名于唐代诗人韩翃《寒食》中的名句“春城无处不飞花”&#xff0c…

KubeSphere核心实战_kubesphere全功能安装_启用kubesphere的热插拔插件---分布式云原生部署架构搭建037

然后我们开始安装kubesphere,首先进入官网点击kubernetes安装 可以看到对应的,条件说kubernetes要在1.20.x以上,我们的是 1.20.9,然后cpu硬件满足,然后,默认存储类型,上一节我们安装好了 然后就可以开始,去下载两个配置文件可以看到上面的两个配置文件 这两个文件,上面是直接…

跨域问题:预检请求

解决跨域问题之预检请求 预检请求&#xff08;Preflight Request&#xff09;是跨域资源共享&#xff08;CORS&#xff09;中用于安全检查的一种机制。 它是由浏览器自动发起的一个OPTIONS请求&#xff0c;目的是在实际跨域请求之前&#xff0c;询问服务器是否允许这次跨域操…

学习TS -类型

let a:number10;//数值型 let b:string"zhaoya";//字符串型 let c:booleantrue;//布尔类型 let d:10;//字面量 let e:any10;//任意类型&#xff0c;可以给别人赋值&#xff0c;而且不会提示报错 let f:unknown50;//未知类型&#xff0c;他给别人赋值&#xff0c;赋值…

kafka开启kerberos和ACL

作者&#xff1a;恩慈 一、部署kafka-KB包 1&#xff0e;上传软件包 依次点击 部署中心----部署组件----上传软件包 选择需要升级的kafka版本并点击确定 2&#xff0e;部署kafka 依次点击部署中心----部署组件----物理/虚拟机部署----选择集群----下一步 选择手动部署-…

消费金融系统开发回忆录

架构设计图 整个支付链路上的功能 支付系统应该有&#xff1a;账户管理、渠道管理、支付管理、对账管理、清算管理、结算管理 一笔支付订单&#xff0c;在支付系统侧就是要记录清楚&#xff0c;谁发起的、对哪个商品进行支付、通过哪个渠道支付、支付时间、支付结果等…

C++基础入门(二)(函数重载,引用,内联函数,nullptr)

目录 一. 函数重载 1. 概念 2. 实现 (1). 参数类型不同 (2). 参数个数不同 (3). 参数类型顺序不同 3. 注意事项 (1). 返回值不能作为重载的条件 (2). 不能仅按函数返回类型重载 (3). 与缺省参数的问题 二. 引用 1. 概念和定义 2. 引用的特性 (1). 引用在定义时必须…

LDR6020双盲插便携显示器应用

随着USB Type-C接口的普及&#xff0c;越来越多的手机和笔记本电脑都支持通过C接口输出视频。这个小巧而精密的接口&#xff0c;大有把传统的HDMI和DisplayPort接口取而代之的架势。特别是usb4的推出&#xff0c;更是为USB TYPE-C接口一统有线接口形态奠定了基础。 单USB-C接口…

python入门课程Pro(1)--数据结构及判断

数据结构及判断 第1课 复杂的多向选择1.if-elif-else2.if嵌套3.练习题&#xff08;1&#xff09;大招来了&#xff08;2&#xff09;奇数还是偶数&#xff08;3&#xff09;简洁代码 第2课 数据与判断小结1.变量2.格式化输出3.逻辑运算-或与非4.判断条件5.练习题&#xff08;1&…

【手撕数据结构】拿捏双向链表

目录 链表介绍初始化链表销毁链表查找节点打印链表增加节点尾插头插在指定位置之后插入节点 删除节点尾删头删删除指定位置节点 链表判空 链表介绍 前面说到&#xff0c;链表的结构一共有八种&#xff1a;带头单向循环链表、带头单向非循环链表、带头双向循环链表、带头双向非…

【初阶数据结构】5.栈和队列

文章目录 1.栈1.1 概念与结构1.2 栈的实现2.队列2.1 概念与结构2.2 队列的实现3.栈和队列算法题3.1 有效的括号3.2 用队列实现栈3.3 用栈实现队列3.4 设计循环队列 1.栈 1.1 概念与结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操…

C语言宏定义格式化控制台打印

写了个简单的控制台打印代码&#xff0c;有三种打印级别 DEBUG INFO ERROR&#xff0c;支持颜色打印&#xff0c;支持时间打印 在MSVC环境中使用 #include <time.h> #include <string.h> #include <stdio.h>/* log level */ #define LOG_LEVEL_DEBUG (1) #d…