Linux高性能服务器编程 | 读书笔记 |9.定时器
9. 定时器
网络程序需要处理定时事件,如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,有效地组织这些定时事件,使其在预期的时间被触发且不影响服务器的主要逻辑,对于服务器的性能有至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,如链表、排序链表、时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。本章主要讨论两种高效的管理定时器的容器:时间轮和时间堆。
定时指在一段时间后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器,即定时机制是定时器得以被处理的原动力。Linux提供三种定时方法:
- socket套接字选项
SO_RCVTIMEO
和SO_SNDTIMEO
; SIGALRM
信号;- I/O复用系统调用的超时参数。
文章目录
- 9. 定时器
- 1.socket 选项 `SO_RCVTIMEO` 和 `SO_SNDTIMEO`
- 2.使用 `SO_SNDTIMEO` 选项设置定时
- setsockopt讲解
- 代码
- 3.SIGALRM 信号
- 1.基于升序链表的定时器
- 2.处理非活动连接
- 处理非活动连接:利用 alarm 函数周期性触发 SIGALRM 信号
- 服务器端
- 客户端
- 效果
- 4.I/O 复用系统调用的超时函数
- 基于 `epoll` 的事件循环与定时机制实现
- `time_t time(time_t *t);`
- 参数
- 返回值
- 6.高性能定时器(较难,需复习)
- 时间轮
- 复杂度分析
- 时间堆
- 复杂度分析
1.socket 选项 SO_RCVTIMEO
和 SO_SNDTIMEO
SO_RCVTIMEO
设置 socket 接收数据超时时间。
SO_SNDTIMEO
设置 socket 发送数据超时时间。
这两个数据仅对与数据接收和发送相关的 socket 系统调用 send
、sendmsg
、recv
、recvmsg
、accept
和 connect
。
在程序中,我们根据上述系统调用的返回值以及 errno
来判断超时时间是否已到,进而决定是否开始处理定时任务。
2.使用 SO_SNDTIMEO
选项设置定时
setsockopt讲解
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
参数
- sockfd:
- 类型:int
- 解释:这是一个整型变量,表示要操作的套接字的文件描述符(file descriptor)。
- level:
- 类型:int
- 解释:这个参数指定了选项所属的协议层次,如SOL_SOCKET(套接字级别)、IPPROTO_IP(IP协议级别)等。
- optname:
- 类型:int
- 解释:这是选项的名称,比如SO_REUSEADDR、SO_TIMEOUT等,是套接字选项库中预定义的一个整数常量。
- optval:
- 类型:const void*
- 解释:这个参数指向一个包含选项值的内存区域。选项值的类型取决于特定的选项,可能是一个整数、字符数组、枚举值等。
- optlen:
- 类型:socklen_t
- 解释:这是一个长度类型的参数,表示optval所指向的内存区域大小。在调用前,应预先根据选项和可能的值设置其正确的大小。
返回值
- 成功:返回0。
- 失败:返回-1(SOCKET_ERROR),并设置errno来表示错误的原因。可能的错误代码包括EBADF(sock不是有效的文件描述词)、EFAULT(optval指向的内存并非有效的进程空间)、EINVAL(在调用setsockopt()时,optlen无效)、ENOPROTOOPT(指定的协议层不能识别选项)、ENOTSOCK(sock描述的不是套接字)等。
常用选项示例
- SO_RCVBUF和SO_SNDBUF:用于设置/读取发送缓冲区和接收缓冲区大小。选项值类型为int,指定新的缓冲区大小。对setsockopt和getsockopt有效。设置缓冲区大小只能在TCP连接建立之前进行。
- SO_REUSEADDR:用于复用socket地址(端口号)。选项值类型为int,0表示不能复用,1表示可以复用,默认值为0。对setsockopt和getsockopt有效。
- SO_KEEPALIVE:用于TCP socket检测/保持网络连接。选项值类型为int,0表示不能发送探测数据段,1表示可以发送,默认值为0。对setsockopt和getsockopt有效。
- TCP_NODELAY:用于禁止TCP协议的Nagle算法,使小数据包(小于最大数据段大小)立即发送,可能会降低TCP协议的效率。
setsockopt函数的作用是设置与指定的套接字关联的选项,从而影响套接字的行为。通过调用setsockopt函数,可以设置这些选项的值,以满足不同的网络编程需求。
代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>/* 超时连接函数 * 这个函数的作用是尝试与指定IP地址和端口的服务器建立连接,* 并设置一个连接超时时间。*/
int timeout_connect(const char *ip, int port, int time) {int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int sockfd = socket(PF_INET, SOCK_STREAM, 0);assert(sockfd >= 0);// SO_RCVTIMEO和SO_SNDTIMEO套接字选项对应的值类型为timeval,这和select函数的超时参数类型相同struct timeval timeout;timeout.tv_sec = time;timeout.tv_usec = 0;socklen_t len = sizeof(timeout);ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);assert(ret != -1);/** 如果连接在指定时间内没有成功建立,connect() 函数将返回 -1,* 并且 errno 会被设置为 EINPROGRESS,表示连接超时。*/printf("Attempting to connect...\n");ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));printf("Connect returned: %d\n", ret);if (ret == -1) {// 超时对应的错误号是EINPROGRESS,此时就可执行定时任务了if (errno == EINPROGRESS) {printf("conencting timeout, process timeout logic\n");return -1;}printf("error occur when connecting to server\n");return -1;}return sockfd;
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);/* 超时时间 10s */int sockfd = timeout_connect(ip, port, 10);if (sockfd < 0) {return 1;}return 0;
}
编译:
g++ -o client connect_timeout.cpp
运行:
./client 192.168.100.100 8080 /* 这是一个无效的IP地址 */
结果:
3.SIGALRM 信号
由alarm
和setitimer
函数设置的实时闹钟一旦超时,就会触发SIGALRM
信号,我们可以使用该信号的信号处理函数来处理定时任务。
1.基于升序链表的定时器
定时器通常至少要包含两个成员:一个超时时间(相对时间或绝对时间)和一个任务回调函数。有时还可能包含回调函数被执行时需要传入的参数,以及是否重启定时器等信息。如果使用链表作为容器来串联所有定时器,则每个定时器还要包含指向下一定时器的指针成员,如果链表是双向的,则每个定时器还需要包含指向前一个定时器的指针成员。
从执行效率来看,添加定时器的时间复杂度是O(n)
,删除定时器的时间复杂度是O(1)
,执行定时任务的时间复杂度平均是O(1)
。
#ifndef LST_TIMER
#include<time.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <libgen.h>#define BUFFER_SIZE 64
class util_timer;//用户数据结构:客户端socket地址,socket文件描述符,读缓存和定时器
struct client_data
{sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];util_timer* timer;
};//定时器类 双向链表节点
class util_timer
{
public:util_timer():prev(NULL),next(NULL){}util_timer* prev;//前一个util_timer* next;//后一个//任务超时时间 使用绝对时间time_t expire;//任务回调函数 时间到了就去调对应的回调函数void (*cb_func)(client_data *);//客户数据client_data * user_data;
};//定时器链表类 升序,双向,有头节点和尾节点
class sort_timer_1st
{
public:sort_timer_1st():head(NULL),tail(NULL){}~sort_timer_1st(){util_timer* tmp=head;while(tmp){head=tmp->next;delete tmp;tmp=head;}}//插入定时器到链表void add_timer(util_timer* timer){if(!timer)return ;if(!head){head=tail=timer;return;}//插到开头if(timer->expire<head->expire){timer->next=head;head->prev=timer;head=timer;return;}//插到中间和结尾add_timer(timer,head);}//当某个任务的定时器时间发生变化的时候需要调整,这里是考虑定时器时间被延长的情况void adjust(util_timer* timer){if(!timer){return;}util_timer* tmp=timer->next;if(!tmp||(timer->expire<tmp->expire)){return;}//头节点被延长,拿出来重新插入if(timer==head){head=head->next;head->prev=NULL;timer->next=NULL;add_timer(timer,head);}//如果不是头节点,那就拿出来插入到它之后的部分else{timer->prev->next=timer->next;timer->next->prev=timer->prev;add_timer(timer,timer->next);}}// 尝试找到定时器Timer从链表里删除 *void del_timer(util_timer* timer){if (!timer){return;}// 下面这个函数用于删除链表中只有一个定时器,即目标定时器 if ((timer == head) && (timer == tail)){delete timer;head = NULL;tail = NULL;return;}// 如果链表中至少有两个定时器,且目标定时器是链表的头节点,将链表的头节点重定位为原// 头节点的下一个节点,然后删除目标定时器 if (timer == head){head = head->next;head->prev = NULL;delete timer;return;}// 如果链表中至少有两个定时器,且目标定时器是链表的尾节点,将链表的尾节点重定位为原// 尾节点的前一个节点,然后删除目标定时器 if (timer == tail){tail = tail->prev;tail->next = NULL;delete timer;return;}// 如果目标定时器位于链表的中间,把它前后的定时器串联起来,然后删除目标定时器 *timer->prev->next = timer->next;timer->next->prev = timer->prev;delete timer;}// SIGALRM信号每次被触发时就执行其信号处理函数(如果使用统一事件源,则是主函数)// 中执行一次tick函数。以处理链表上到期的任务 void tick(){if (!head){return;}printf("timer tick\n");time_t cur = time(NULL); // 获取系统当前的时间 util_timer* tmp = head;// 从头节点开始依次处理每个定时器,直到遇到一个尚未到期的定时器。这是定时器的核心逻辑 while (tmp){// 因为每个定时器都使用绝对时间作为超时时间,所以我们可以判断定时器的超时值和系统当// 前时间,以判断定时器是否到期 if (cur < tmp->expire){break;}// 调用定时器的回调函数,以执行定时任务 tmp->cb_func(tmp->user_data);// 执行完定时器中的定时任务之后,将它从链表中删除,并重置链表头节点 head = tmp->next;if (head){head->prev = NULL;}delete tmp;tmp = head;}}
private://辅助函数void add_timer(util_timer* timer,util_timer* lst_head){util_timer* prev=lst_head;util_timer* tmp=prev->next;//从head开始遍历,找第一个比timer大的while(tmp){if(timer->expire<tmp->expire){prev->next=timer;timer->next=tmp;tmp->prev=timer;timer->prev=prev;break;}prev=tmp;tmp=tmp->next;}//没找到比它大的,就插入到末尾if(!tmp){prev->next=timer;timer->prev=prev;timer->next=NULL;tail=timer;}}util_timer* head;util_timer* tail;
};#endif
核心函数tick相当于一个心博函数,每隔一段固定的时间就执行一次,以检测并处理到期的任务。
判断任务到期的依据是定时器党的expire值小于当前的系统时间。
2.处理非活动连接
现在考虑以上升序定时器链表的实际应用 ——处理非活动连接。服务器进程通常要定期处理非活动连接:给客户端发一个重连请求,或关闭该连接,或者其他。
Linux在内核中提供了对连接是否处于活动状态的定期检查机制,我们可通过socket选项KEEPALIVE
来激活它,不过这样会使得应用程序对连接的管理变得复杂。我们考虑在应用层实现类似KEEPALIVE
的机制,以管理所有长时间处于非活动状态的连接。
处理非活动连接:利用 alarm 函数周期性触发 SIGALRM 信号
如以下代码利用alarm
函数周期性地触发SIGALRM
信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务——关闭非活动的连接。
服务器端
TIMESLOT
定义了定时器的基本时间单位,用于设定定时器回调函数的调用频率。这个值代表了服务器在触发 SIGALRM
信号后多长时间再次触发该信号,从而执行定时任务。在代码示例中,TIMESLOT
被设置为 5 秒,也就是说,基本时间单位是 5 秒。
alarm
函数用于安排信号在未来的某个时间点发送给进程。这个函数是标准库中的一部分,定义在 <unistd.h>
头文件中。当调用 alarm(TIMESLOT);
时,设定了一个闹钟,让操作系统在 TIMESLOT
秒后向当前进程发送一个 SIGALRM
信号。
alarm(TIMESLOT);
在 timer_handler()
函数中调用,意味着每隔 TIMESLOT
秒,操作系统将向进程发送一个 SIGALRM
信号。收到这个信号后,sig_handler
处理函数将被执行,进而调用 timer_handler
函数来处理所有定时任务。
当一个新的客户端连接被接受,服务器为这个连接创建一个定时器,并设置其超时时间:
timer->expire = cur + 3 * TIMESLOT;
cur
是当前时间,3 * TIMESLOT
表示定时器的超时时间是15秒(因为 3 乘以 5秒)。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <libgen.h>#include "lst_timer.h"#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5static int pipefd[2];
static sort_timer_lst timer_lst; /* 用升序链表来管理定时器 */
static int epollfd = 0;/* 将文件描述符设置为非阻塞模式 */
int setnonblocking(int fd) {int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}/* addfd 函数把新的文件描述符添加到 epoll 监听列表中,* 并且设置为非阻塞和边缘触发模式(EPOLLET),以提高事件处理的效率。非阻塞socket+ET比单纯的LT高效*/
void addfd(int epollfd, int fd) {epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); /* 将fd注册到内核事件表epollfd中*/setnonblocking(fd);
}/* sig_handler 用于处理程序接收到的信号。* 它把信号编号发送到 pipefd 管道,这样主循环可以安全地处理信号事件。* 这种使用信号和管道的组合是多线程或多进程环境中处理信号的一种安全模式。
*/
void sig_handler(int sig) {int save_errno = errno;int msg = sig;// 此处还是老bug,没有考虑字节序就发送了int的低地址的1字节 send(pipefd[1], (char *)&msg, 1, 0);//发送编号,编号的长度为1 0表示send没有任何其他的特殊行为就是普通的发送errno = save_errno;
}/* addsig 函数设置信号处理函数,并确保系统调用被中断时能自动重启,避免了部分系统调用失败的问题。 */
void addsig(int sig) {struct sigaction sa;memset(&sa, '\0', sizeof(sa));sa.sa_handler = sig_handler;sa.sa_flags |= SA_RESTART;sigfillset(&sa.sa_mask);assert(sigaction(sig, &sa, NULL) != -1);
}void timer_handler() {// 处理定时任务timer_lst.tick();// 由于alarm函数只会引起一次SIGALRM信号,因此重新定时,以不断触发SIGALRM信号alarm(TIMESLOT);
}/* 定时器回调函数,它删除非活动连接socket上的注册事件,并关闭之 */
void cb_func(client_data *user_data) {epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);assert(user_data);close(user_data->sockfd);printf("close fd %d\n", user_data->sockfd);
}int main(int argc, char *argv[]) {if (argc != 3) {printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5); /* 创建内核事件表 */assert(epollfd != -1);addfd(epollfd, listenfd);ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);assert(ret != -1);setnonblocking(pipefd[1]); /* fd[1]只能用于数据写入。 */addfd(epollfd, pipefd[0]);// 设置信号处理函数addsig(SIGALRM);addsig(SIGTERM);bool stop_server = false;// 直接初始化FD_LIMIT个client_data对象,其数组索引是文件描述符client_data *users = new client_data[FD_LIMIT];bool timeout = false;// 定时alarm(TIMESLOT);while (!stop_server) {int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); /* 它在一段超时时间内等待一组文件描述符上的事件 */if ((number < 0) && (errno != EINTR)) {printf("epoll failure\n");break;}for (int i = 0; i < number; ++i) {int sockfd = events[i].data.fd;// 处理新到的客户连接if (sockfd == listenfd) {struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);addfd(epollfd, connfd);users[connfd].address = client_address;users[connfd].sockfd = connfd;// 创建一个定时器,设置其回调函数和超时时间,然后绑定定时器和用户数据,并将定时器添加到timer_lst中util_timer *timer = new util_timer;timer->user_data = &users[connfd];timer->cb_func = cb_func;time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;users[connfd].timer = timer;timer_lst.add_timer(timer);// 处理信号} else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) {int sig;char signals[1024];ret = recv(pipefd[0], signals, sizeof(signals), 0);if (ret == -1) {// handle the errorcontinue;} else if (ret == 0) {continue;} else {for (int i = 0; i < ret; ++i) {switch (signals[i]) {case SIGALRM:// 先标记为有定时任务,因为定时任务优先级比IO事件低,我们优先处理其他更重要的任务timeout = true;break;case SIGTERM:stop_server = true;break;}}}// 从客户连接上接收到数据} else if (events[i].events & EPOLLIN) {memset(users[sockfd].buf, '\0', BUFFER_SIZE);ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);printf("get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd);util_timer *timer = users[sockfd].timer;if (ret < 0) {// 如果发生读错误,则关闭连接,并移除对应的定时器if (errno != EAGAIN) {cb_func(&users[sockfd]);if (timer) {timer_lst.del_timer(timer);}}} else if (ret == 0) {// 如果对方关闭连接,则我们也关闭连接,并移除对应的定时器cb_func(&users[sockfd]);if (timer) {timer_lst.del_timer(timer);}} else {// 如果客户连接上读到了数据,则调整该连接对应的定时器,以延迟该连接被关闭的时间if (timer) {time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;printf("adjust timer once\n");timer_lst.adjust_timer(timer);}}}}// 最后处理定时事件,因为IO事件的优先级更高,但这样会导致定时任务不能精确按预期的时间执行if (timeout) {timer_handler();timeout = false;}}close(listenfd);close(pipefd[1]);close(pipefd[0]);delete[] users;return 0;
}
整体讲解:
这段代码实现了一个基于 epoll
的网络服务器程序,它具备以下主要功能特点:
- 利用
epoll
实现高效的 I/O 复用,能同时处理多个客户端连接的 I/O 事件,并且采用了非阻塞套接字结合边缘触发(ET)模式来提升性能。 - 通过定时器链表(
sort_timer_lst
)来管理连接的超时时间,实现定时关闭长时间无活动的客户端连接功能,并且能够动态调整定时器时间(比如客户端有数据交互时延长连接的超时时间)。 - 可以处理系统信号,例如
SIGALRM
(用于定时任务触发)和SIGTERM
(用于优雅地终止服务器进程),并在相应信号到来时执行特定的操作。
和10.4统一事件源思想类似,把信号处理函数定义为对管道写数据,写的就是信号的编号,然后通过对管道进行事件监听,一旦监听到管道有读事件,那就是有信号发过来了,recv接收并处理信号,本题中的信号就是定时器信号,5秒一次,到了的话就去处理定时器链表里面的超时事件
同时监听客户的读事件,如果客户在5秒内进行了写事件,那么就要重新设置定时器表示这个客户是活跃的
客户端
为了测试服务器能够按照预定的定时器逻辑关闭非活动连接,我们可以编写一个简单的客户端程序,该程序连接到服务器后不发送任何数据,仅保持连接一定时间后(sleep(20);
)关闭,看服务器是否会在设定的超时时间后自动关闭该连接。
服务端中,定时器的超时时间被设置为3 * TIMESLOT
,即15秒(TIMESLOT
设置为5秒)。定时器的目的是在客户端在指定时间内没有任何活动(例如数据交换)的情况下自动关闭该连接。
SIGALRM
信号每次触发就在其信号处理函数(如果使用统一事件源,则是主函数)中执行一次tick
函数,处理链表上的到期任务,并在终端打印"timer tick"。
void timer_handler() {/* 处理定时任务 */timer_lst.tick();/* 由于alarm函数只会引起一次SIGALRM信号,因此重新定时,以不断触发SIGALRM信号 */alarm(TIMESLOT);
}
在服务器端,在没有收到任何数据的情况下,服务器在定时器到期后关闭了连接的日志消息,例如 “close fd <fd>
” 的输出,表示服务器正确处理了非活动连接。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main(int argc, char *argv[]) {if (argc != 3) {printf("Usage: %s <ip> <port>\n", argv[0]);return 1;}const char *server_ip = argv[1];int server_port = atoi(argv[2]);// 创建 socketint sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0) {perror("Socket creation failed");return 1;}// 服务器地址结构struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port);if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {perror("Invalid address/ Address not supported");return 1;}// 连接到服务器if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("Connection Failed");return 1;}printf("Connected to server, now sleeping...\n");// 保持连接,但不进行任何通信sleep(20); // 保持连接20秒,调整时间以匹配服务器设置的超时时间// 关闭 socketclose(sock);printf("Disconnected from server\n");return 0;
}
效果
4.I/O 复用系统调用的超时函数
Linux下的3组I/O复用系统调用都带有超时参数,因此它们不仅能统一处理信号(通过管道在信号处理函数中通知主进程)和I/O事件,也能统一处理定时事件,但由于I/O复用系统调用可能在超时时间到期前就返回(有I/O事件发生),所以如果我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间,如下代码所示:
#define TIMEOUT 5000int timeout = TIMEOUT;
time_t start = time(NULL);
time_t end = time(NULL);
while (1) {printf("the timeout is now %d mil-seconds\n", timeout);start = time(NULL);int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, timeout);if ((number < 0) && (errno != EINTR)) {printf("epoll failure\n");break;}// 如果epoll_wait函数返回0,说明超时时间到,此时可处理定时任务,并重置定时时间if (number == 0) {// 处理定时任务timeout = TIMEOUT;continue;}// 到此,epoll_wait函数的返回值大于0,end = time(NULL);// 更新timeout的值为减去本次epoll_wait调用的持续时间timeout -= (end - start) * 1000;// 重新计算后的timeout值可能是0,说明本次epoll_wait调用返回时,不仅有文件描述符就绪,且其超时时间也刚好到达// 此时我们要处理定时任务,并充值定时时间if (timeout <= 0) {// 处理定时任务timeout = TIMEOUT;}// handle connections
}
基于 epoll
的事件循环与定时机制实现
这段代码实现了一个基于 epoll
的事件循环,并结合了定时处理的功能,整体功能围绕高效处理 I/O 事件以及定时任务展开,具体如下:
- 定时相关变量初始化
首先定义了一个宏TIMEOUT
表示超时时间(单位为毫秒,不过代码中实际换算有些问题,后续会详细说明),初始化为5000
。然后声明了一个整型变量timeout
并初始化为TIMEOUT
,用于记录剩余的超时时间。同时,通过time(NULL)
函数获取当前时间分别赋值给start
和end
,用于后续计算时间差来跟踪时间流逝情况。 - 循环主体功能
进入一个无限循环(while (1)
),在每次循环中执行以下操作:- 打印剩余超时时间信息:
使用printf
函数打印当前剩余的超时时间(这里代码将timeout
当作毫秒来处理,但实际time(NULL)
获取的是秒数,后面计算时间差时需要进行相应转换才能准确对应毫秒单位,不过暂且按现有逻辑分析),方便查看定时情况。 - 启动
epoll_wait
并处理返回结果:- 调用
epoll_wait
:每次循环都会调用epoll_wait
函数,传入epollfd
(epoll
实例对应的文件描述符)、events
(用于接收就绪事件的数组)、MAX_EVENT_NUMBER
(数组大小,限定了最多能接收的事件数量)以及timeout
(本次等待的超时时间,单位在代码逻辑中期望是毫秒,虽然前面提到换算有问题)。这个函数会阻塞等待文件描述符上有感兴趣的事件发生或者超时。 - 处理
epoll_wait
返回值小于 0 的情况:如果epoll_wait
返回值小于0
,并且错误原因不是因为被信号中断(errno!= EINTR
),则说明epoll
操作出现了真正的错误,此时会打印“epoll failure”
提示信息,并通过break
跳出循环,结束整个事件处理流程。 - 处理
epoll_wait
返回值等于 0 的情况(超时情况):当epoll_wait
返回0
时,意味着在设定的超时时间内没有文件描述符就绪事件发生,也就是超时时间到了。此时,代码会将timeout
重新设置为初始的TIMEOUT
值,用于下一轮循环继续等待事件时使用初始设定的超时时间,然后通过continue
语句直接进入下一轮循环,去再次等待事件或者超时。
- 调用
- 处理
epoll_wait
返回值大于 0 的情况(有文件描述符就绪):- 更新时间记录:当
epoll_wait
返回值大于0
时,表示有文件描述符上的事件就绪了。此时先获取当前时间赋值给end
,然后通过end - start
计算出本次epoll_wait
调用所持续的时间(这里获取的时间是以秒为单位,实际代码逻辑期望换算成毫秒去更新timeout
,存在前面提到的单位换算问题)。 - 更新剩余超时时间:用当前剩余的超时时间
timeout
减去本次epoll_wait
调用持续时间换算后的毫秒数((end - start) * 1000
,这里代码本意是将秒换算成毫秒,但实际time(NULL)
函数获取的是从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间的秒数,所以换算逻辑不准确),以此来更新timeout
的值,反映剩余的超时时间还有多少。 - 处理超时时间耗尽情况:如果更新后的
timeout
值小于等于0
,说明本次epoll_wait
在有文件描述符就绪的同时,超时时间也刚好到达(或者已经超过了设定的超时时间),这时会将timeout
重新设置为初始的TIMEOUT
值,准备下一轮循环继续基于初始超时时间进行事件等待与定时处理,同时也意味着此时需要去处理定时任务(虽然代码中没有明确展示具体的定时任务处理内容,只是做了这个时间重置的相关操作)。
- 更新时间记录:当
- 处理就绪的连接(注释中的
handle connections
部分):
这部分代码没有具体展开,但可以推测在这里会对epoll_wait
返回的就绪文件描述符对应的连接进行相应的读、写等 I/O 操作处理,比如接收客户端发送的数据、向客户端发送响应数据等操作,完成服务器与客户端之间基于网络连接的交互功能。
- 打印剩余超时时间信息:
总体而言,这段代码通过不断循环调用 epoll_wait
函数来同时兼顾 I/O 事件的处理以及定时任务的触发,利用超时时间的计算和更新机制,使得程序既能及时响应文件描述符上的就绪事件,又能按照设定的时间间隔(虽然存在时间单位换算问题)来处理定时任务,在网络服务器等需要同时处理 I/O 和定时逻辑的场景中较为常用。不过代码中的时间计算部分需要修正时间单位换算逻辑,才能准确实现预期的定时功能。
time_t time(time_t *t);
函数原型:
time_t time(time_t *t);
参数
t
: 这是一个指向time_t
类型的指针,用于存储时间值。如果参数是NULL
,函数只返回当前时间。
返回值
- 函数返回一个
time_t
类型的值,它表示从Epoch至今的秒数。如果出现错误,则返回(time_t) -1
。
6.高性能定时器(较难,需复习)
时间轮
基于排序链表的定时器存在一个问题:添加定时器的效率偏低(添加定时器的时间复杂度是**O(n)**
)。下面讨论的时间轮解决了这个问题,一种简单的时间轮如下图:
上图中,时间轮内部的实线指针指向轮子上的一个槽(slot),它以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针所指的槽),每次转动称为一个滴答(tick),一个滴答的时间称为时间轮的槽间隔si(slot interval),它实际上就是心搏时间。上图中的时间轮共有N个槽,因此它转动一周的时间是N * si
。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时事件相差N * si
的整数倍,时间轮正是利用这个关系将定时器散列到不同的链表中。假如现在指针指向槽cs
,我们要添加一个定时事件为ti
的定时器,则该定时器将被插入槽ts
(timer slot)对应的链表中:
ts=(cs+(ti/si))%N
基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插入操作的效率随着定时器数目的增多而降低,而时间轮使用哈希表的思想,将定时器散列到不同的链表上,这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。
对时间轮而言,要想提高定时精度,就要使si
足够小,要提高执行效率,就要求N足够大(N越大,散列冲突的概率就越小)。
以下代码描述了一种简单的时间轮,因为它只有一个轮子,而复杂的时间轮可能有多个轮子,不同的轮子有不同的粒度,相邻的两个轮子,精度高的转一圈,精度低的仅仅往前移动一槽,就像水表一样。
对时间轮而言,如果一共有n个定时器,则添加一个定时器的时间复杂度为O(1)
;删除一个定时器的时间复杂度平均也是O(1)
,但最坏情况下可能所有节点都在一个槽中,此时删除定时器的时间复杂度为O(n)
;执行一个定时器的时间复杂度是O(n)
,实际上执行一个定时器任务的效率要比O(n)
好得多,因为时间轮将所有定时器散列到了不同的链表上,时间轮的槽越多,等价于散列表的入口(entry)越多,从而每条链表上的定时器数量越少。此外,以上代码中只使用了1个时间轮,当使用多个轮子来实现时间轮时,执行一个定时器任务的时间复杂度将接近O(1)
。
// 时间轮定时器,用数组(环)存储每一条定时器链表
// 求hash(超时时间)决定定时器到数组的哪个位置
#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER#include <time.h>
#include <netinet/in.h>
#include <stdio.h>#define BUFFER_SIZE 64
class tw_timer; // 时间轮定时器前置声明// 绑定socket和定时器
struct client_data
{sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];tw_timer *timer;
}class tw_timer
{
public:tw_timer(int rot, int ts):next(NULL), prev(NULL), rotation(rot), time_slot(ts){}public:int rotation; // 记录定时器在时间轮转多少圈才生效int time_slot; // 记录定时器属于时间轮上的哪个slotvoid (*cb_func)(client_data *);client_data *user_data; // 用指针存储对应的用户数据tw_timer *next;tw_timer *prev;
}class time_wheel
{
public:time_wheel() : cur_slot(0){for(int i = 0; i < N; ++i){slots[i] = NULL; // init}}~time_wheel(){for(int i = 0; i < N; ++i){tw_timer *tmp = slots[i];while(tmp){// 重新设置链表头节点slots[i] = tmp->next;delete tmp;tmp = slots[i];}}}// 根据定时值timeout,创建一个定时器,并把它插入到合适的槽中tw_timer* add_timer(int timeout){if(timeout < 0){return NULL;}int ticks = 0;// 根据待插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发// 若待插入定时器的超时值小于时间轮的槽间隔,则将ticks向上取整为1if(timeout < SI){ticks = 1;}else {ticks = timeout / SI;}// 计算转多少圈后被触发int rotation = ticks / N;// 计算待插入的定时器应该被插入哪个槽中int ts = (cur_slot + (ticks % N)) % N;// 创建新的定时器,它在时间轮转动rotation圈之后触发,位于第ts个slot上tw_timer *timer = new tw_timer(rotation, ts);// 插入指定槽中的链表 头// 第ts个slot没有任何定时器(空链表)if(!slots[ts]){printf("add timer, rotation is %d, ts is %d, cur_slot is %d\n", rotation, ts, cur_slot);}else // 链表非空,则头插 {timer->next = slots[ts];slots[ts]->prev = timer;slots[ts] = timer;}return timer;}// 删除目标定时器void del_timer(tw_timer *timer){if(!timer){return;}int ts = timer->time_slot;// slots[ts] 是目标定时器所在头节点// 如果待删定时器就是该头节点,则需要重置第ts个slot的链表头节点if(timer == slots[ts]){slots[ts] = slots[ts]->next;if(slots[ts]){slots[ts]->prev = NULL;}// 如果第ts个slot的链表就剩下一个节点,直接删除delete timer;}else{timer->prev->next = timer->next;if(timer->next){timer->next->prev = timer->prev;}delete timer;}}// 时间轮转动函数,每SI时间之后,cur_slot向前滚动一个slotvoid tick(){// 时间轮上当前槽的头节点tw_timer *tmp = slots[cur_slot];printf("current slot is %d\n", cur_slot);// 遍历当前slot上链表的每个定时器节点,while(tmp){printf("tick the timer once\n"); // 定时器超过1轮,跳过if(tmp->rotation > 0){tmp->rotation--;tmp = tmp->next;}// 否则只要指针到当前slot,里面的所有定时器就都到时了else // 执行定时任务,删除tmp节点{tmp->cb_func(tmp->user_data);// 链表头节点!!if(tmp = slots[cur_slot]){printf("delete header in cur_slot\n");slots[cur_slot] = tmp->next; // 让tmp的下一个节点做头节点delete tmp;if(slots[cur_slot]){slots[cur_slot]->prev = tmp->prev;}tmp = slots[cur_slot]; // tmp为刚刚删除的节点的下一个节点}else // 非头节点{tmp->prev->next = tmp->next;if(tmp->next){tmp->next->prev = tmp->prev;}tw_timer *tmp2 = tmp->next;delete tmp;tmp = tmp2; //}}}// 时间轮转动(指针移动到下一个slot)cur_slot = (++cur_slot) % N;}
private:// 时间轮上slot的数目static const int N = 60;// 指针每1s转动一次,即slot的间隔为1s slot intervalstatic const int SI = 1;// 时间轮,每个存放定时器链表tw_timer* slots[N];int cur_slot; // 指针,指向当前的slot
};
复杂度分析
由于添加一个定时器是链表头插,则时间复杂度为 O ( 1 ) O(1)O(1)
删除一个定时器的时间复杂的也为O ( 1 ) O(1)O(1)
执行一个定时器的复杂度为O ( n ) O(n)O(n).但实际执行一个定时任务效率要比O ( n ) O(n)O(n)好,因为时间轮将所有定时器散列到不同的链表上。
若使用多个轮子实现时间轮,执行一个定时器任务的复杂度可以降到O ( 1 ) O(1)O(1)
时间堆
以上讨论的定时方案都是以固定频率调用心搏函数tick
,并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另一种思路是:将所有定时器中超时时间最小的定时器的超时值作为心搏间隔,这样,一旦心搏函数tick
被调用,超时时间最小的定时器必然到期,我们就可在tick
函数中处理该定时器,然后,再次从剩余定时器中找出超时时间最小的一个,并将这段最小时间设为下一次心搏间隔。
最小堆很适合这种定时方案:
最小堆是每个节点的值都小于或等于其子节点的值的完全二叉树。
由于最小堆其实就是一棵树,所以其实现可以用链表,也可以用数组。用最小堆实现的定时器称为时间堆。对时间堆而言,添加一个定时器的时间复杂度是O(lgn)
;删除一个定时器的时间复杂度是O(1)
;执行一个定时器的时间复杂度是O(1)
。因此,时间堆的效率很高。
最小堆很适合这种定时方案。本文实现最小堆有以下关键特点:
- 根节点值小于其孩子节点的值(递归成立);
- 插入节点是在最后一个节点添加新节点,然后进行上滤保证最小堆特性;
- 删除节点是删除其根节点上的元素,然后把最后一个元素移动到根节点,进行下滤操作保证最小堆特性;
- 将N个元素的数组(普通二叉树)初始化为最小堆,即从二叉树最后一个非叶节点到根节点(第[ ( N − 1 ) / 2 ] [(N-1) / 2][(N−1)/2] ~ 0 个元素)执行下滤操作。
- 本文实现的最小堆底层是用数组进行存储,是一个适配器,联想C++的
priority_queue<int, vector<int>, greater<int>>
// 用最小堆存储定时器,称为时间堆
#ifndef MIN_HEAP
#define MIN_HEAP#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;#define BUFFER_SIZE 64
class heap_timer;// 绑定socket和定时器
struct client_data
{sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];heap_timer *timer;
};// 定时器类
class heap_timer
{
public:heap_timer(int delay){expire = time(NULL) + delay;}public:time_t expire = expire; // 定时器生效的绝对时间void (*cb_func)(client_data *);client_data *user_data;
};// 时间堆类
class time_heap
{
public: // 初始化一个大小为cap的空堆// throw (std::exception) 表示该函数可能抛出std::exception 类型的异常time_heap(int cap) throw (std::exception) : capacity(cap), cur_size(0){array = new heap_timer*[capacity];if(!array){throw std::exception();}else{for(int i = 0; i < capacity; ++i){array[i] = NULL;}}}// 用已有的堆数组初始化堆time_heap(heap_timer **init_array, int size, int capacity) throw (std::exception) : cur_size(size), capacity(capacity) {if(capacity < size){throw std::exception();}array = new heap_timer*[capacity];if(!array){throw std::exception();}for(int i = 0; i < capacity; ++i){array[i] = NULL;}if(size != 0){// 初始化数组for(int i = 0; i < size; ++i){array[i] = init_array[i];}// 最后一个非叶子节点到根节点调堆(下滤)for(int i = (cur_size - 1); i >= 0; --i){percolate_down(i);}}}~time_heap(){for(int i = 0; i < cur_size; ++i){delete array[i];}delete []array;}public:// 堆添加节点,上滤void add_timer(heap_timer *timer) throw (std::exception){if(!timer){return;}if(cur_size >= capacity) // 容量不足,堆指针数组需要扩充一倍{resize();}// 新插入了一个元素,在堆最后插入,然后调堆int hole = cur_size++;int parent = 0;// 上滤操作for(; hole > 0; hole = parent) // hole = parent使得最终结果位置上移{parent = (hole - 1) / 2; // hole节点的父节点计算if(array[parent]->expire <= timer->expire){// 父节点小于插入的节点,满足小根堆要求,直接结束break;}array[hole] = array[parent]; // 父节点节点下移}array[hole] = timer;}void del_timer(heap_timer *timer){if(!timer){return;}// 仅仅将目标定时器的回调函数设置为空,即延迟销毁// 这将节省真正删除该定时器的开销,但易使堆数指针组膨胀timer->cb_func = NULL;}// 获取堆顶部的定时器,expire最小者heap_timer* top() const{if(empty()){return NULL;}return array[0];}// 删除堆顶部的定时器void pop_timer(){if(empty()){return ;}if(array[0]){delete array[0];// 将原来堆顶元素用堆的最后一个元素临时填充,然后下滤array[0] = array[--cur_size];percolate_down(0);}}// 定时处理函数void tick(){heap_timer *tmp = array[0];time_t cur = time(NULL); // 循环遍历堆中每个定时器(堆用数组实现,故数组遍历),处理到期的定时器while(!empty()){if(!tmp){break;}// 如果堆顶定时器没到期,则退出循环,因为堆顶定时器到时时间使最近的,其他更晚if(tmp->expire > cur){break;}if(array[0]->cb_func){array[0]->cb_func(array[0]->user_data);}// 将堆顶元素删除,同时让tmp指向新的堆顶pop_timer();tmp = array[0];}}bool empty() const{return cur_size == 0;}private:// 下面两个函数是被其他成员函数调用,不对外提供// 最小堆的下滤操作,确保数组中以第hole个节点作为根的子树满足最小堆性质void percolate_down(int hole){heap_timer *temp = array[hole];int child = 0;// hole * 2 + 1为hole的左孩子for(; (hole * 2 + 1) <= (cur_size - 1); hole = child) // hole = child是一个下滤的动作{child = hole * 2 + 1; // 左孩子// 要选择expire小的孩子进行比较if((child < (cur_size - 1)) && (array[child + 1]->expire < array[child]->expire)){child++;}if(array[child]->expire < temp->expire) // 下滤{array[hole] = array[child];}else{break;}}array[hole] = temp;}// 将堆数组容量扩大一倍void resize() throw (std::exception){heap_timer **temp = new heap_timer*[2 * capacity];for(int i = 0; i < 2 * capacity; ++i){temp[i] = NULL;}if(!temp){throw std::exception();}capacity = 2 * capacity;// 把原来数组的内容拷贝到新的数组for(int i = 0; i < cur_size; ++i){temp[i] = array[i];}delete []array;array = temp;}
private:heap_timer **array; // 定时器指针数组int capacity;int cur_size;
};#endif
复杂度分析
- 对时间堆而言,添加一个定时器的时间复杂度为O ( l o g n ) O(logn)O(log**n)(由于需要上滤操作)
- 删除一个定时器的时间复杂度为O ( 1 ) O(1)O(1),这是因为只是将目标定时器的回调函数设置为空
- 执行一个定时器的时间复杂度为O ( 1 ) O(1)O(1)
相关文章:
Linux高性能服务器编程 | 读书笔记 |9.定时器
9. 定时器 网络程序需要处理定时事件,如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,有效地组织这些定时事件,使其在预期的时间被触发且不影响服务器的主要逻辑,对于服务器的性能有至关重要的影响。为此&…...
UE5制作伤害浮动数字
效果演示: 首先创建一个控件UI 添加画布和文本 文本设置样式 添加伤害浮动动画,根据自己喜好调整,我设置了缩放和不透明度 添加绑定 转到事件图表,事件构造设置动画 创建actor蓝图类 添加widget 获取位置 设置位移 创建一个被击中…...
双亲委派机制是Java类加载器的一种工作模式
双亲委派机制是Java类加载器的一种工作模式,确保了类加载的一致性和安全性。以下是对双亲委派机制的详细解析: 一、定义与工作原理 双亲委派机制(Parent Delegation Model)要求除了顶层的启动类加载器外,其余的类加载…...
AI智算-k8s部署大语言模型管理工具Ollama
文章目录 简介k8s部署OllamaOpen WebUI访问Open-WebUI 简介 Github:https://github.com/ollama/ollama 官网:https://ollama.com/ API:https://github.com/ollama/ollama/blob/main/docs/api.md Ollama 是一个基于 Go 语言开发的可以本地运…...
百度23届秋招研发岗A卷
百度23届秋招研发岗A卷 2024/12/16 1.下面关于 SparkSQL 中 Catalyst 优化器的说法正确的是(ABC) A.Catalyst 优化器利用高级编程语言功能(例如 Scala 的模式匹配)来构建可扩展的查询优化器 B.Catalyst 包含树和操作树的规则集…...
米哈游大数据面试题及参考答案
怎么判断两个链表是否相交?怎么优化? 判断两个链表是否相交可以采用多种方法。 一种方法是使用双指针。首先分别遍历两个链表,得到两个链表的长度。然后让长链表的指针先走两个链表长度差的步数。之后,同时移动两个链表的指针,每次比较两个指针是否指向相同的节点。如果指…...
Android14 AOSP 允许system分区和vendor分区应用进行AIDL通信
在Android14上,出于种种原因,system分区的应用无法和vendor分区的应用直接通过AIDL的方法进行通信,但是项目的某个功能又需要如此。 好在Binder底层其实是支持的,只是在上层进行了屏蔽。 修改 frameworks/native/libs/binder/Bp…...
llm chat场景下的数据同步
背景 正常的chat/im通常是有单点登录或者利用类似广播的机制做多设备间内容同步的。而且由于长连接的存在,数据同步(想起来)相对简单。而llm的chat在缺失这两个机制的情况下,没见到特别好的做到了数据同步的产品。 llm chat主要两…...
视频去重原理及 Demo 示例
视频去重是一个常见的需求,主要用于视频库或平台管理中,通过判断视频是否相同(或相似)来移除冗余内容。实现视频去重可以通过多种方法,具体选择取决于业务场景和性能要求。 1. 视频去重的原理 1.1 基本原理 视频去重…...
【GIS教程】使用GDAL-Python将tif转为COG并在ArcGIS Js前端加载-附完整代码
目录 一、数据格式 二、COG特点 三、使用GDAL生成COG格式的数据 四、使用ArcGIS Maps SDK for JavaScript加载COG格式数据 一、数据格式 COG(Cloud optimized GeoTIFF)是一种GeoTiff格式的数据。托管在 HTTP 文件服务器上,可以代替geose…...
【ETCD】【源码阅读】深入解析 EtcdServer.applySnapshot方法
今天我们来一步步分析ETCD中applySnapshot函数 一、函数完整代码 函数的完整代码如下: func (s *EtcdServer) applySnapshot(ep *etcdProgress, apply *apply) {if raft.IsEmptySnap(apply.snapshot) {return}applySnapshotInProgress.Inc()lg : s.Logger()lg.In…...
C# 实现 10 位纯数字随机数
本文将介绍如何用 C# 实现一个生成 10 位纯数字随机数的功能。以下是完整的代码示例: using System; using System.Collections.Generic; using System.Linq; using System.Text;namespace RandomTset {class Program{// 使用GUID作为种子来创建随机数生成器static…...
【热力学与工程流体力学】流体静力学实验,雷诺实验,沿程阻力实验,丘里流量计流量系数测定,局部阻力系数的测定,稳态平板法测定材料的导热系数λ
关注作者了解更多 我的其他CSDN专栏 过程控制系统 工程测试技术 虚拟仪器技术 可编程控制器 工业现场总线 数字图像处理 智能控制 传感器技术 嵌入式系统 复变函数与积分变换 单片机原理 线性代数 大学物理 热工与工程流体力学 数字信号处理 光电融合集成电路…...
黑盒白盒测试
任务1 黑盒测试之等价类划分法 【任务需求】 【问题】例:某报表处理系统要求用户输入处理报表的日期,日期限制在2003年1月至2008年12月,即系统只能对该段期间内的报表进行处理,如日期不在此范围内,则显示输入错误信息…...
D99【python 接口自动化学习】- pytest进阶之fixture用法
day99 pytest使用conftest管理fixture 学习日期:20241216 学习目标:pytest基础用法 -- pytest使用conftest管理fixture 学习笔记: fixture(scope"function") conftest.py为固定写法,不可修改名字,使用c…...
RFDiffusion 计算二面角函数解读
th_dih函数来自util.py包,get_dih函数来自kinematics.py包。th_dih函数计算输入向量定义的二面角的余弦值和正弦值,返回一个包含 (cos(ϕ),sin(ϕ)) 的张量。get_dih 函数计算的是传统意义上的二面角。 源代码: def th_dih_v(ab, bc, cd):def th_cross(a, b):a, b = t…...
卓易通:鸿蒙Next系统的蜜糖还是毒药?
哈喽,我是老刘 最近很多人都在问鸿蒙next系统新上线的卓易通和出境易两款应用。 老刘分析了一下这个软件的一些细节,觉得还是蛮有意思的,我觉得可以从使用体验、底层原理和对鸿蒙生态的影响这三个角度来分析一下。 使用体验 性能 看到了一些测…...
Android:展锐T760平台camera PDAF调试
一、平台PDAF流程 目前展锐平台主要支持Shield PD Sensor、Dual PD Sensor 1、Shield PD Sensor Type1相位差和信心度结果直接从Sensor输出,不经过平台算法库。 Type2Sensor端抽取PD信息, 放在一块buffer输出, PDAF算法库算出相位差和信心度。 Type3Sensor端直接输出将带有…...
泷羽Sec学习笔记-zmap搭建炮台
zmap搭建炮台 zmap扫描环境:kali-linux 先更新软件库 sudo apt update 下载zmap sudo apt install zmap 开始扫描(需要root权限) sudo zmap -p 80 -o raw_ips.txt 代码解析: sudo:以超级用户(管理员)权限运行…...
web遇到的安全漏洞
最近项目又在做安全漏扫,记录下遇到的常见的web安全问题 越权 漏洞介绍 攻击者可以在授权状态下,通过修改数据包的参数,操作超出现有权限操作的功能点。举例 修改密码时,可以通过修改名称参数,修改任意用户密码。 任…...
Starfish 因子开发管理平台快速上手:如何完成策略编写与回测
DolphinDB 开发的因子开发管理平台 Starfish 围绕量化投研的因子、策略开发阶段设计,为用户提供了一个从数据管理、因子研究到策略回测的完整解决方案。 因子平台的回测引擎提供了多个关键的事件函数,涵盖策略初始化、每日盘前和盘后回调、逐笔、快照和…...
Oracle 数据库中,UNION ALL创建视图的使用详解
目录 UNION ALL 的特点 UNION ALL 的作用 1. 合并结果集 2. 保留重复行 3. 提高性能 UNION ALL 的使用场景 1. 日志或数据拼接 2. 区分数据来源 3. 解决分区表查询 注意事项 在创建视图中的作用 场景 1:合并多个表的数据到视图 表结构 目标 SQL 实现…...
无名信号量和条件变量
1.使用无名信号量实现春夏秋冬的输出 #include <myhead.h> sem_t sem1,sem2,sem3,sem4; void *fun1() {while(1){sem_wait(&sem1);sleep(1);printf("春\n");sem_post(&sem2);} } void *fun2() {while(1){sem_wait(&sem2);sleep(1);printf("夏…...
之前使用vue-element-admin框架开发的项目无法启动,可能是这个原因
最近运行之前的项目,发现无法正常启动,可能有以下几种情况: 一、版本问题 报错: this[kHandle] new _Hash(algorithm, xofLen); Error: error:0308010C:digital 因为在 node V17 版本发布了 OpenSSL3.0 对算法…...
JDK的配置
目录 第一步,配置JAVA_HOME. 第二步,进入JDK的bin目录,然后复制路径。 第三步,配置CLASSPATH. 第四步,检验是否配置成功 安装好JDK后,配置三个环境变量 第一步,配置JAVA_HOME. 先找到JDK…...
【Linux系列】Linux 系统中查看目录权限
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
开启数字化时代心理服务新篇章:专属线上心理咨询服务小程序
在当今快节奏的社会中,心理健康问题日益受到人们的关注。然而,传统的心理咨询模式往往受限于时间和地点,使得许多人在寻求心理帮助时感到不便。与此同时,心理课程的传播也面临着诸多挑战,如何高效地触达目标客户群体&a…...
[Unity] Text文本首行缩进两个字符
Text文本首行缩进两个字符的方法比较简单。通过代码把"\u3000\u3000"加到文本字符串前面即可。 比如: 效果: 代码: TMPtext1.text "\u3000\u3000" "选择动作类型:";...
探索 OpenTofu:开源基础设施即代码工具
引言 在现代云计算和 DevOps 实践中,基础设施即代码(IaC)已经成为不可或缺的一部分。它使得基础设施的管理更加自动化、可重复和可维护。HashiCorp 的 Terraform 是这一领域的领先工具,但随着时间的推移,开源社区也开始关注其许可证的变更。OpenTofu 作为 Terraform 的一…...
2024首届世界酒中国菜国际地理标志产品美食文化节成功举办篇章
2024首届世界酒中国菜国际地理标志产品美食文化节成功举办,开启美食文化交流新篇章 近日,首届世界酒中国菜国际地理标志产品美食文化节在中国国际地理标志大厦成功举办,这场为期三天的美食文化盛会吸引了来自世界各地的美食爱好者、行业专家…...
宽字节注入
尽管现在呼吁所有的程序都使用unicode编码,所有的网站都使用utf-8编码,来一个统一的国际规范。但仍然有很多,包括国内及国外(特别是非英语国家)的一些cms,仍然使用着自己国家的一套编码,比如gbk…...
H5 scss 移动端的样式适配
在移动端样式的scss文件中,出现了这些变量 env() 与 constant() 设置安全区域,是css里IOS11新增的属性,webkit的css函数,用于设定安全区域与边界的距离,有4个预定义变量: safe-area-inset-left: 安全区域距…...
240004基于Jamva+ssm+maven+mysql的房屋租赁系统的设计与实现
基于ssmmavenmysql的房屋租赁系统的设计与实现 1.项目描述2.运行环境3.项目截图4.源码获取 1.项目描述 该项目在原有的基础上进行了优化,包括新增了注册功能,房屋模糊查询功能,管理员和用户信息管理等功能,以及对网站界面进行了优…...
Word2Vec 模型 PyTorch 实现并复现论文中的数据集
详细注解链接:https://www.orzzz.net/directory/codes/Word2Vec/index.html 欢迎咨询!...
《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(一)
《拉依达的嵌入式\驱动面试宝典》—C/CPP基础篇(一) 你好,我是拉依达。 感谢所有阅读关注我的同学支持,目前博客累计阅读 27w,关注1.5w人。其中博客《最全Linux驱动开发全流程详细解析(持续更新)-CSDN博客》已经是 Lin…...
使用matlab对矩阵进行分块
1. 前言 由于matlab内存限制,导致无法处理较大尺寸的矩阵; 2. 解决思路 读取原始大尺寸矩阵,分块后处理,及时删除中间过程文件,只保留分块处理后的最终结果,最后合并结果文件,减少内存占用。 …...
MongoDB(上)
MongoDB 基础 MongoDB 是什么? MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂…...
超详细的pycharm+anaconda搭建python虚拟环境
(一)pycharm安装 1. 下载 (1)从官网下载 ,一般来说选择社区版就够用了。我这里选择2024.1.6的windows版本Other Versions - PyCharmGet past releases and previous versions of PyCharm.https://www.jetbrains.com/…...
yarn修改缓存位置
查看缓存位置 以下三个命令分别为:bin是yarn存储命令的二进制文件,global存储全局node_modules ,cache存储用下下载缓存,查看本机目前的目录: 查看bin目录命令:yarn global bin 查看global目录命令&…...
单元测试知识总结
我们希望每段代码都是自测试的,每次改动之后,都能自动发现对现有功能的影响。 1 测试要求 在对软件单元进行动态测试之前,应对软件单元的源代码进行静态测试; 应建立测试软件单元的环境,如数据准备、桩模块、模拟器…...
光谱相机
光谱相机是一种能够同时获取目标物体的空间图像信息和光谱信息的成像设备。 1、工作原理 光谱相机通过光学系统将目标物体的光聚焦到探测器上,在探测器前设置分光元件,如光栅、棱镜或滤光片等,将光按不同波长分解成多个光谱通道,…...
账号下的用户列表表格分析
好的,这是您提供的 el-table 组件中所有列的字段信息,以表格形式展示: 列标题 (label)字段属性 (prop)对齐方式 (align)宽度 (width)是否可排序 (sortable)说明IDidcenter100否管理员的唯一标识符头像avatarcenter90否管理员的头像 URL 或路…...
软件开发中 Bug 为什么不能彻底消除
在软件开发中,Bug无法彻底消除的原因主要包括:软件复杂度高、人员认知与沟通受限、需求和环境不断变化、工具与测试覆盖不足、经济与时间成本制约。其中“需求和环境不断变化”尤为关键,因为在实际开发中,业务逻辑随着市场与用户反…...
Flutter 中的 Flexible 与 Expanded 的介绍、区别与使用
在 Flutter 中,布局是构建用户界面的重要部分。Flexible 和 Expanded 是两个常用的布局小部件,它们都用于控制子小部件在父容器中的空间分配。虽然它们有相似之处,但在使用上有一些关键的区别。本文将介绍这两个小部件的基本概念、区别、参数…...
从零开始学习 sg200x 多核开发之 sophpi 编译生成 fip.bin 流程梳理
本文主要介绍 sophpi 编译生成 fip.bin 流程。 1、编译前准备 sophpi 的基本编译流程如下: $ source build/cvisetup.sh $ defconfig sg2002_wevb_riscv64_sd $ clean_all $ build_all $ pack_burn_image注: 需要在 bash 下运行clean_all 非必要可以不…...
通过一个例子学习回溯算法:从方法论到实际应用
回溯算法:从方法论到实际应用 回溯算法(Backtracking)是一种通过穷举法寻找问题所有解的算法,它的核心思想是逐步构建解空间树,在每个步骤中判断当前解是否合法。如果不合法,就“回溯”到上一步࿰…...
google 的guava 学习 基本工具类
Guava 是 Google 开发的一个 Java 核心库,它提供了一系列工具类,用于简化 Java 编程中的常见任务。以下是 Preconditions 和 Verify 两个工具类的使用示例: Preconditions 类 Preconditions 类提供了一组静态方法,用于在代码中插…...
【Linux金典面试题(上)】41道Linux金典面试问题+详细解答,包含基本操作、系统维护、网络配置、脚本编程等问题。
大家好,我是摇光~,用大白话讲解所有你难懂的知识点 之前写了一篇关于 python 的面试题,感觉大家都很需要,所以打算出一个面试专栏。 【数据分析岗】Python金典面试题 这个专栏主要针对面试大数据岗位、数据分析岗位、数据运维等…...
SpringBoot【九】mybatis-plus之自定义sql零基础教学!
一、前言🔥 环境说明:Windows10 Idea2021.3.2 Jdk1.8 SpringBoot 2.3.1.RELEASE mybatis-plus的基本使用,前两期基本讲的差不多,够日常使用,但是有的小伙伴可能就会抱怨了,若是遇到业务逻辑比较复杂的sq…...
CTF 攻防世界 Web: FlatScience write-up
题目名称-FlatScience 网址 index 目录中没有发现提示信息,链接会跳转到论文。 目前没有发现有用信息,尝试目录扫描。 目录扫描 注意到存在 robots.txt 和 login.php。 访问 robots.txt 这里表明还存在 admin.php admin.php 分析 在这里尝试一些 sql…...