当前位置: 首页 > news >正文

30天开发操作系统 第 12 天 -- 定时器 v1.0

前言

定时器(Timer)对于操作系统非常重要。它在原理上却很简单,只是每隔一段时间(比如0.01秒)就发送一个中断信号给CPU。幸亏有了定时器,CPU才不用辛苦地去计量时间。……如果没有定时器会怎么样呢?让我们想象一下吧。
假如CPU看不到定时器而仍想计量时间的话,就只能牢记每一条指令的执行时间了。比如,往寄存器写人常数的MOV指令是1个时钟周期(Clock):加法计算的ADD指令原则上是1个时钟周期,但根据条件不同可能是2个时钟周期……等等。CPU不仅要牢记这些内容,然后还要据此调查一下调用这些函数所需的时间,比如,调用这个函数需要150个时钟周期,调用那个函数因参数不同需要106到587个时钟周期等。
而这里的“时钟周期”又不是一个固定值。比如CPU主频是100MHz的话,一个时钟周期是10 纳秒;但主频如果是200MHz,1个时钟周期就是5纳秒。既然CPU有各种主频,那么1个时钟周期的时间也就各不相同。(大家这下理解cpu的大小核了吧)
这样做可以勉强通过程序对时间进行管理,实现每隔一定时间进行一次某种处理,比如让钟表(程序)的秒针动起来。如果程序中时间计算出错了,那么做出的钟表不是快就是慢,没法使用。
如果没有定时器,还会出现别的麻烦,即不能使用HLT指令。完成这个指令所需的时钟周期不是个固定值。这样,一旦执行HLT指令,程序就不知道时间了。不能执行HLT指令,就意味着要浪费很多电能。所以只能二选一,要么放弃时间的计量,要么选择浪费电能。左右为难,实在糟糕透顶。
打个比方说,如果大家没有手表还想知道时间,那该怎么办呢?当然,不准看太阳,也不准看星星。那就只能根据肚子的饥饿程度,或者烧一壶开水所用的时间等方法来判断了。
总之只能是一边干点儿什么,一边计算时间,而且决不能睡觉!一睡觉就没法计时了……就类似这种情况。
然而实际上,由于有定时器中断,所以不用担心会发生这样的悲剧。程序只需要以自己的步
调处理自己的问题就行了。至于到底经过了多长时间,只要在中断处理程序中数一数定时器中断发生的次数就可以了。就算CPU处于HLT状态,也可以通过中断来唤醒。根本就没必要让程序自己去记忆时间。CPU也就可以安心地去睡觉了(HT)。这样,大家还可以省点电费(笑)。
所以说定时器非常重要。管理定时器是操作系统的重大任务之一,所以在我们的操作系统中也要使用定时器。

一、定时器的设定和使用

1.定时器使用前的设定

要在电脑中管理定时器,只需对PIT进行设定就可以了。PIT是“ProgrammableInterval Timer的缩写,翻译过来就是“可编程的间隔型定时器”。我们可以通过设定PIT,让定时器每隔多少秒就产生一次中断。因为在电脑中PIT连接着IRQ(imteruptrequest,参考第6天)的0号,所以只要设定了PIT就可以设定IRQ0的中断间隔。……在旧机种上PIT是作为一个独立的芯片安装在主板上的,而现在已经和PIC(programmableinterruptcontroller,参考第6天)一样被集成到别的芯片里了。
前几天我们学习PIC时曾经非常辛苦,从现在开始,我们又要重温那种感觉了。大家可不要想:“怎么又学这个?”刚开始学习PIC时,陌生的东西比较多,学起来很费力。这次就不会那么辛苦了。
电脑里的定时器用的是8254芯片(或其替代品 ),那就查一下这个芯片吧。
IRQ0的中断周期变更:
■ AL=0x34:OUT(0x43,AL);
■ AL=中断周期的低8位;OUT(0x40,AL);
■ AL=中断周期的高8位;OUT(0x40,AL);
■ 到这里告一段落。
■ 如果指定中断周期为0,会被看作是指定为65536。实际的中断产生的频率是单位时间时钟周期数(即主频)/设定的数值。比如设定值如果是1000,那么中断产生的频率就是1.19318KHz。设定值是10000的话,中断产生频率就是119.318Hz。再比如设定值是11932的话,中断产生的频率大约就是100Hz了,即每10ms发生一次中断。
我们不清楚其中的详细原理,只知道只要执行3次OUT指令设定就完成了。将中断周期设定为11932的话,中断频率好像就是100Hz,也就是说1秒钟会发生100次中断。那么我们就设定成这个值吧。把11932换算成十六进制数就是0x2e9c,下面是我们编写的函数initpit。

#define PIT_CTRL	0x0043
#define PIT_CNT0	0x0040void init_pit(void)
{io_out8(PIT_CTRL, 0x34);io_out8(PIT_CNT0, 0x9c);io_out8(PIT_CNT0, 0x2e);return;
}
void HariMain(void)
{...init_gdtidt();init_pic();io_sti(); /* IDT/PIC的初始化已经结束,所以解除CPU的中断禁止 */fifo8_init(&keyfifo, 32, keybuf);fifo8_init(&mousefifo, 128, mousebuf);init_pit(); /* 这里 */...
}

这样的话IRQ0就会在1秒钟内发生100次中断了。
下面我们来编写IRO0发生时所调用的中断处理程序。它几乎和键盘中断处理程序一样,大家还记得吗:

void inthandler20(int *esp)
{io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收完了的信息通知给PIC *//* 暂时什么也不做 */return;
}

我们把 init_pit 和 inthandler20 放到了新创建的文件夹 timer.c
下面是naskfunc.nas新增的:

_asm_inthandler20:PUSH	ESPUSH	DSPUSHADMOV		EAX,ESPPUSH	EAXMOV		AX,SSMOV		DS,AXMOV		ES,AXCALL	_inthandler20POP		EAXPOPADPOP		DSPOP		ESIRETD

大家还记得下一步是要干什么吗?
为了把这个中断处理程序注册到IDT,inlt gdtidt函数中也要加上几行。这也和键盘处理的时候差不多哦:

/* IDT的设定 */set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);

到这里准备工作就完成了。也不知能不能正常运行。正常的话,嗯,应该什么都不发生。
下面我们执行“make run”。哦,什么也没发生。太好了!但这样有点不过瘾,还是在中断处理程序中做点什么吧!

2.计量时间

那我们让它干点什么呢?……我们就让它执行下面这段程序吧:
bookpack.h

struct TIMERCTL {unsigned int count;
};
#include "bootpack.h"#define PIT_CTRL	0x0043
#define PIT_CNT0	0x0040struct TIMERCTL timerctl;void init_pit(void)
{io_out8(PIT_CTRL, 0x34);io_out8(PIT_CNT0, 0x9c);io_out8(PIT_CNT0, 0x2e);timerctl.count = 0; /* 这里 */return;
}void inthandler20(int *esp)
{io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收完了的信息通知给PIC */timerctl.count++; /* 这里 */return;
}

大家瞅瞅,没几行新增代码。程序所做的处理是:首先定义了struct TIMERCTL结构体。然后,在结构体内定义了一个计数变量 count。初始化PIT时,将这个计数变量设置为0。每次发生定时器中断时,计数变量就以1递增。
也就是说,即使这个计数变量在HariMain中不进行加算,每1秒钟它也会自动增加100。
为了确认,我们把数值显示出来吧。

		...for (;;) {sprintf(s, "%010d", timerctl.count);boxfill8(buf_win, 160, COL8_C6C6C6, 40, 28, 119, 43);putfonts8_asc(buf_win, 160, 40, 28, COL8_000000, s);sheet_refresh(sht_win, 40, 28, 120, 44);...

这样的话,数字应该是以每秒钟100的速度增加。而且不论哪个机种增加速度都是一样的。
即使CPU的速度不同,增加速度也应该是一样的。我们先做做看吧。执行“make run"。……正常运行了,还算顺利。
在这里插入图片描述
就能知道从启动开始时间过去了多少秒。如果往方便面里倒入开水的同时利用这个方法,就能测量是否到3分钟(=180秒)了。哦,终于向着有实用价值的操作系统迈出了第一步。

现在,从启动开始经过了多少秒这一类问题,我们就可以很轻松地判断了。另外,我们还可以计量处理所花费的时间。具体做法是,处理前看一下时间并把它存放到一个变量里,处理结束之后再看一下时间,然后只要用减法算出时间差,就能得到答案了,比如“这个处理耗时13.56秒”等。我们甚至可以据此编制基准测试程序(benchmark program)。
这里大家稍稍回想一下,现在已经能够显示出窗口,又能使用鼠标,又能计量时间,还能进行内存管理,已经实现了很多功能。有了这些功能,只要对它们进行各种组合,就能做很多事情。
我们言归正传,继续说定时器吧。操作系统的定时器经常被用于这样一种情形:“喂,操作系统,过了10秒钟以后通知我一声,我要干什么什么”。当然,不一定非要是10秒,也可以是1秒或30分钟。我们把这样的功能叫做“超时”(timeout)。下面就来实现这个功能吧。
首先往结构体struct TIMERCTL里添加一些代码,以便记录有关超时的信息。

struct TIMERCTL {unsigned int count;unsigned int timeout;struct FIFO8 *fifo;unsigned char data;
};

以上结构体中的timeout用来记录离超时还有多长时间。一旦这个剩余时间达到0,程序就往FIFO缓冲区里发送数据。定时器就是通过这种方法通知HariMain时间到了。至于为什么要使用FIFO缓冲区,也说不上个所以然,只是觉得这个方法简单,因为使用FIFO缓冲区来通知的话,可以比照键盘和鼠标,利用同样的方法来处理。
下面我们来修改函数吧。

#include "bootpack.h"#define PIT_CTRL	0x0043
#define PIT_CNT0	0x0040struct TIMERCTL timerctl;void init_pit(void)
{io_out8(PIT_CTRL, 0x34);io_out8(PIT_CNT0, 0x9c);io_out8(PIT_CNT0, 0x2e);timerctl.count = 0;timerctl.timeout = 0;return;
}void inthandler20(int *esp)
{io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收结束的信息通知给PIC */timerctl.count++;if (timerctl.timeout > 0) { /* 如果已经设定了超时 */timerctl.timeout--;if (timerctl.timeout == 0) {fifo8_put(timerctl.fifo, timerctl.data);}}return;
}void settimer(unsigned int timeout, struct FIFO8 *fifo, unsigned char data)
{int eflags;eflags = io_load_eflags();io_cli();timerctl.timeout = timeout;timerctl.fifo = fifo;timerctl.data = data;io_store_eflags(eflags);return;
}

希望大家注意的是,我们在inthandler20函数里实现了超时功能。每次发生中断时就把timeout减1,减到0时,就向fifo发送数据。
在settimer函数里,如果设定还没有完全结束IRQ0的中断就进来的话,会引起混乱,所以我们先禁止中断,然后完成设定,最后再把中断状态复原。
这在HariMain中如何实现呢?我们来尝试这样做:


void HariMain(void)
{struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;struct FIFO8 timerfifo;char s[40], keybuf[32], mousebuf[128], timerbuf[8];...fifo8_init(&timerfifo, 8, timerbuf);settimer(1000, &timerfifo, 1);...for (;;) {...io_cli();if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo) == 0) {io_sti();} else {if (fifo8_status(&keyfifo) != 0) {...} else if (fifo8_status(&mousefifo) != 0) {i = fifo8_get(&mousefifo);io_sti();if (mouse_decode(&mdec, i) != 0) {...} else if (fifo8_status(&timerfifo) != 0) {i = fifo8_get(&timerfifo); /* 读入 */io_sti();putfonts8_asc(buf_back, binfo->scrnx, 0, 64, COL8_FFFFFF, "10[sec]");sheet_refresh(sht_back, 0, 64, 56, 80);}}}
}

程序很简单,我们在其中设定10秒钟以后向timerfifo写人“1”这个数据,而timerffo接收到数据时,就会在屏幕上显示“10[sec]”。
我们执行一下“make run”,看,显示出来了!
在这里插入图片描述

2.设定多个计时器

在上一节做的超时功能,超时结束后如果再设定1000的话,那我们就可以让它每10秒显示或是让它一闪一灭地显示。另外,间隔不仅限于10秒,我们还可以设定得更长一些或更短一次,些。比如设定为0.5秒的间隔可以用于文字输人时的光标闪烁。
开发操作系统时,超时功能非常方便,所以在很多地方都可以使用它。比如可以让电子时钟每隔1秒重新显示一次;演奏音乐时,可以用它计量音符的长短;也可以让它以0.1秒1次的频率来监视没有中断功能的装置;另外,还可以用它实现光标的闪烁功能。
为了简单地实现这些功能,我们要准备很多能够设定超时的定时器。
首先把struct TIMERCTL修改成下面这样。

#define MAX_TIMER		500
struct TIMER {unsigned int timeout, flags;struct FIFO8 *fifo;unsigned char data;
};
struct TIMERCTL {unsigned int count;struct TIMER timer[MAX_TIMER];
};

这样超时定时器最多就可以设定为500个了,fags则用于记录各个定时器的状态。
继续修改对应的函数:

#include "bootpack.h"#define PIT_CTRL	0x0043
#define PIT_CNT0	0x0040struct TIMERCTL timerctl;#define TIMER_FLAGS_ALLOC		1	/* 已配置状态 */
#define TIMER_FLAGS_USING		2	/* 定时器运行中 */void init_pit(void)
{int i;io_out8(PIT_CTRL, 0x34);io_out8(PIT_CNT0, 0x9c);io_out8(PIT_CNT0, 0x2e);timerctl.count = 0;for (i = 0; i < MAX_TIMER; i++) {timerctl.timer[i].flags = 0; /* 未使用 */}return;
}struct TIMER *timer_alloc(void)
{int i;for (i = 0; i < MAX_TIMER; i++) {if (timerctl.timer[i].flags == 0) {timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;return &timerctl.timer[i];}}return 0; /* 没找到 */
}void timer_free(struct TIMER *timer)
{timer->flags = 0; /* 未使用 */return;
}void timer_init(struct TIMER *timer, struct FIFO8 *fifo, unsigned char data)
{timer->fifo = fifo;timer->data = data;return;
}void timer_settime(struct TIMER *timer, unsigned int timeout)
{timer->timeout = timeout;timer->flags = TIMER_FLAGS_USING;return;
}void inthandler20(int *esp)
{int i;io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收结束的信息通知给PIC */timerctl.count++;for (i = 0; i < MAX_TIMER; i++) {if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {timerctl.timer[i].timeout--;if (timerctl.timer[i].timeout == 0) {timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);}}}return;
}

程序稍微有些长,不是很难,只要前面的程序大家都明白了,这里应该也没什么困难。
最后来看HariMain函数。我们不一定都设定为10秒,也尝试一下设为3秒吧。另外,我们还要编写类似光标闪烁那样的程序。

void HariMain(void)
{struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;struct FIFO8 timerfifo, timerfifo2, timerfifo3;char s[40], keybuf[32], mousebuf[128], timerbuf[8], timerbuf2[8], timerbuf3[8];struct TIMER *timer, *timer2, *timer3;...fifo8_init(&timerfifo, 8, timerbuf);timer = timer_alloc();timer_init(timer, &timerfifo, 1);timer_settime(timer, 1000);fifo8_init(&timerfifo2, 8, timerbuf2);timer2 = timer_alloc();timer_init(timer2, &timerfifo2, 1);timer_settime(timer2, 300);fifo8_init(&timerfifo3, 8, timerbuf3);timer3 = timer_alloc();timer_init(timer3, &timerfifo3, 1);timer_settime(timer3, 50);...for (;;) {...io_cli();if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) + fifo8_status(&timerfifo)+ fifo8_status(&timerfifo2) + fifo8_status(&timerfifo3) == 0) {io_sti();} else {if (fifo8_status(&keyfifo) != 0) {...} else if (fifo8_status(&mousefifo) != 0) {i = fifo8_get(&mousefifo);io_sti();if (mouse_decode(&mdec, i) != 0) {} else if (fifo8_status(&timerfifo) != 0) {i = fifo8_get(&timerfifo); /* 读入 */io_sti();putfonts8_asc(buf_back, binfo->scrnx, 0, 64, COL8_FFFFFF, "10[sec]");sheet_refresh(sht_back, 0, 64, 56, 80);} else if (fifo8_status(&timerfifo2) != 0) {i = fifo8_get(&timerfifo2); /* 读入 */io_sti();putfonts8_asc(buf_back, binfo->scrnx, 0, 80, COL8_FFFFFF, "3[sec]");sheet_refresh(sht_back, 0, 80, 48, 96);} else if (fifo8_status(&timerfifo3) != 0) {/* 模拟光标 */i = fifo8_get(&timerfifo3);io_sti();if (i != 0) {timer_init(timer3, &timerfifo3, 0); /* 设置 0 */boxfill8(buf_back, binfo->scrnx, COL8_FFFFFF, 8, 96, 15, 111);} else {timer_init(timer3, &timerfifo3, 1); /* 设置 1 */boxfill8(buf_back, binfo->scrnx, COL8_008484, 8, 96, 15, 111);}timer_settime(timer3, 50);sheet_refresh(sht_back, 8, 96, 16, 112);}}}
}

下面就是期盼已久的“make run”了。我们执行一下,看看我们的成就:
在这里插入图片描述

二、加快中断处理

1.0

现在我们可以自由使用多个定时器了,从数量上说,已经足够了。但仔细看一下大家会发现,inthandler20还有很大问题:中断处理本来应该在很短的时间内完成,可利用inthandler20时却花费了很长时间。这就妨碍了其他中断处理的执行,使得操作系统反应很迟钝。
如果检査inthandler20,能发现每次进行定时器中断处理的时候,都会对所有活动中的定时器
进行“timerctl.timer[i].timeout–;”处理。也就是说,CPU要完成从内存中读取变量值,减去1,然后又往内存中写人的操作。本来谁也不会注意到这种细微之处,但由于我们想在中断处理程序中尽可能减少哪怕是一点点工作量,所以才会注意到这里。
问题找到了,那该怎么修改才好呢?我们看看下面这样行不行:

void inthandler20(int *esp)
{int i;io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收结束的信息通知给PIC */timerctl.count++;for (i = 0; i < MAX_TIMER; i++) {if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {if (timerctl.timer[i].timeout <= timerctl.count) {timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);}}}return;
}

我们改变了程序中变量timer[i].timeout的含义。它指的不再是“所剩时间”,而是“设定时刻”
了。因为现在的时刻计数到timerctl.count中去了,所以就拿它和timer[i].timeout进行比较,如果相同或是超过了,就通过往FIFO缓冲区里传送数据来通知HariMain。大家现在再看一看,我们一直担心的减法计算没有了。这样一改,程序的速度应该能稍微变快一些了。
下面我们也要相应地修改timer_settime函数。

void timer_settime(struct TIMER *timer, unsigned int timeout)
{timer->timeout = timeout + timerctl.count;timer->flags = TIMER_FLAGS_USING;return;
}

timer_settime函数中所指定的时间,是“从现在开始多少多少秒以后”的意思,所以用这个
时间加上现在的时刻,就可以计算出中断的预定时刻。程序中对这个时刻进行了记录。别的地方就不用改了。

到底这样做行不行呢,我们执行一下“make run”。好哇,进行得很顺利。虽然还没能切身
感到速度变快了多少,不过先自我满足一下吧。
同时也正是因为变成了这种方式,在我们这个操作系统中,启动以后经过42949673
秒后,count就是0xfffffffff了,比这个值再大就不能设定了。这么多秒是几天呢?……嗯,请稍等(用计算器算一下)……大约是497天。也就是大约一年就要重新启动一次操作系统,让count归0。
这里大家可能又会有怨言了“哎呀,还需要重新起动,这样的操作系统真是麻烦”。事实上
本人也是这么想的(笑)。怎么办才好呢。回到上一节的做法,好不好呢?可是回到上
一节的做法,速度又有些慢。……既不希望速度慢,又不想重新启动 – 为了满足这种奢望,我们设计成一年调整一次时刻的程序也许比较好。

int t0=timerctl.count;/*所有时刻都要减去这个值*/
io_cli();/*在时刻调整时禁止定时器中断 */
timerctl.count-=t0;
for(i=0;i<MAX TIMER;i++){if(timerctl.timeri],flags ==TIMER FLAGS USING){timerctl.timer[i].timeout =t;}
}
io_sti();

也许以上方法并非最好,但我们不轻言放弃而去想办法解决,这种心境是最重要的。只要努
力,我们肯定还能找到别的好办法。

2.0

我们再来改善一下吧。

代码如下(示例):

void inthandler20(int *esp)
{int i;io_out8(PIC0_OCW2, 0x60);timerctl.count++;for (i = 0; i < MAX_TIMER; i++) {if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {if (timerctl.timer[i].timeout <= timerctl.count) {timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);}}}return;
}

如果看一下 harib09e的inthandler20,大家会发现每次中断都要执行500次(=MAX TIMER的
次数)if语句,很浪费时间。由于1秒钟就要发生100次中断,这个if语句1秒钟就要执行5万次。
尽管如此,这两个if语句都为真,而其中的fags值得以更改,或者是6fo8put函数能够执行的频
率,最多也就是每0.5秒1次,即每秒2次左右。其余的49998次if语句都是在做无用功,基本没什么意义。

我们来变通一下思考方式,如果是人在进行着这样的定时器管理,会怎么做呢?定时器加在
一起最多有500个。其中有3秒钟以后超时的,有50秒钟以后超时的,也有0.3秒钟以后超时的,还有一天以后超时的。这种情况下,我们首先会关注哪一个?应该是0.3秒钟以后的那个吧。0.3秒钟的结束后,下次是3秒钟以后的。也就是没必要把500个都看完,只要看到“下一个”的时刻就可以了。因此,我们追加一个变量timerctl.next,让它记住下一个时刻。

struct TIMERCTL {unsigned int count, next;struct TIMER timer[MAX_TIMER];
};
void inthandler20(int *esp)
{int i;io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收结束的信息通知给PIC */timerctl.count++;if (timerctl.next > timerctl.count) {return; /* 还不到下一个时刻,所以结束 */}timerctl.next = 0xffffffff;for (i = 0; i < MAX_TIMER; i++) {if (timerctl.timer[i].flags == TIMER_FLAGS_USING) {if (timerctl.timer[i].timeout <= timerctl.count) {/* 超时 */timerctl.timer[i].flags = TIMER_FLAGS_ALLOC;fifo8_put(timerctl.timer[i].fifo, timerctl.timer[i].data);} else {/* 还没有超时的话,这里会找到离下一次超时最近的*/if (timerctl.next > timerctl.timer[i].timeout) {timerctl.next = timerctl.timer[i].timeout;}}}}return;
}

虽然程序变长了,但要做的处理却减少了。在大多数情况下,第一个if语句的return都会执行,
中断处理就到此结束了。当到达下一个时刻时,使用之前那种方法检查是否超时。超时的话,就写人到FIFO中;还没超时的话就调查是否将其设定为下一个时刻(未超时时刻中,最小的时刻是下一个时刻)。
如果用这样的方法,就能大大减少没有意义的if语句的执行次数,速度也应该快多了。
由于使用了next,所以其他地方也要修改一下。

void init_pit(void)
{int i;io_out8(PIT_CTRL, 0x34);io_out8(PIT_CNT0, 0x9c);io_out8(PIT_CNT0, 0x2e);timerctl.count = 0;timerctl.next = 0xffffffff; /* 因为最初没有正在运行的定时器 */for (i = 0; i < MAX_TIMER; i++) {timerctl.timer[i].flags = 0; /* 没有使用 */}return;
}void timer_settime(struct TIMER *timer, unsigned int timeout)
{timer->timeout = timeout + timerctl.count;timer->flags = TIMER_FLAGS_USING;if (timerctl.next > timer->timeout) {/* 更新下一次的时刻 */timerctl.next = timer->timeout;}return;
}

这样就好了。现在我们来确认是否能正常运行。“makerun”。……和以前一样,虽然仍不能
切身地感受到速度变快了,但还是自我满足一下吧。

3.0

到了harib09f的时候,中断处理程序的平均处理时间已经大大缩短了。这真是太好了。可是,
现在有一个问题,那就是到达next时刻和没到next时刻的定时器中断,它们的处理时间差别很大。
这样的程序结构不好。因为平常运行一直都很快的程序,会偶尔由于中断处理拖得太长,而搞得像是主程序要停了似的。更确切一点,这样有时会让人觉得“不知为什么,鼠标偶尔会反应迟钝,很卡。”
因此,我们要让到达next时刻的定时器中断的处理时间再缩短一些。,怎么办呢?模仿
sheet.c的做法怎么样呢?我们来试试看。
在sheet.c的结构体struct SHTCTL中,除了sheet0[]以外,我们还定义了*sheets[]。它里面存
放的是按某种顺序排好的图层地址。有了这个变量,按顺序描绘图层就简单了。这次我们在stuct TIMERCTL中也定义一个变量,其中存放按某种顺序排好的定时器地址。

struct TIMERCTL {unsigned int count, next, using;struct TIMER *timers[MAX_TIMER];struct TIMER timers0[MAX_TIMER];
};

变量using相当于struct SHTCTL中的top,它用于记录现在的定时器中有几个处于活动中。
改进后的inthandler20函数如下:

void inthandler20(int *esp)
{int i, j;io_out8(PIC0_OCW2, 0x60);	/* 把IRQ-00信号接收结束的信息通知给PIC */timerctl.count++;if (timerctl.next > timerctl.count) {return;}for (i = 0; i < timerctl.using; i++) {/* timers的定时器都处于动作中,所以不确认flags */if (timerctl.timers[i]->timeout > timerctl.count) {break;}/* 超时 */timerctl.timers[i]->flags = TIMER_FLAGS_ALLOC;fifo8_put(timerctl.timers[i]->fifo, timerctl.timers[i]->data);}/* 正好有i个定时器超时了。其余的进行移位。 */timerctl.using -= i;for (j = 0; j < timerctl.using; j++) {timerctl.timers[j] = timerctl.timers[i + j];}if (timerctl.using > 0) {timerctl.next = timerctl.timers[0]->timeout;} else {timerctl.next = 0xffffffff;}return;
}

这样,即使是在超时的情况下,也不用查找下一个next时刻,或者查找有没有别的定时器超
时了,真不错。如果有很多的定时器都处于正在执行的状态,我们会担心定时器因移位而变慢,这放在以后再改进吧。
由于timerctl中的变量名改变了,所以其他地方也要随之修改。

void init_pit(void)
{int i;io_out8(PIT_CTRL, 0x34);io_out8(PIT_CNT0, 0x9c);io_out8(PIT_CNT0, 0x2e);timerctl.count = 0;timerctl.next = 0xffffffff; /* 因为最初没有正在运行的定时器 */timerctl.using = 0;for (i = 0; i < MAX_TIMER; i++) {timerctl.timers0[i].flags = 0; /* 未使用 */}return;
}struct TIMER *timer_alloc(void)
{int i;for (i = 0; i < MAX_TIMER; i++) {if (timerctl.timers0[i].flags == 0) {timerctl.timers0[i].flags = TIMER_FLAGS_ALLOC;return &timerctl.timers0[i];}}return 0; /* 没找到 */
}

这两个函数比较简单,只是稍稍修改了一下变量名。
在timer_settime函数中,必须将timer注册到timers中去,而且要注册到正确的位置。如果在注册时发生中断的话可就麻烦了,所以我们要事先关闭中断。

void timer_settime(struct TIMER *timer, unsigned int timeout)
{int e, i, j;timer->timeout = timeout + timerctl.count;timer->flags = TIMER_FLAGS_USING;e = io_load_eflags();io_cli();/* 搜索注册位置 */for (i = 0; i < timerctl.using; i++) {if (timerctl.timers[i]->timeout >= timer->timeout) {break;}}/* i号之后全部后移一位 */for (j = timerctl.using; j > i; j--) {timerctl.timers[j] = timerctl.timers[j - 1];}timerctl.using++;/* 插入到空位上 */timerctl.timers[i] = timer;timerctl.next = timerctl.timers[0]->timeout;io_store_eflags(e);return;
}

这样做看来不错。虽然中断处理程序速度已经提高了,但在设定定时器期间,我们关闭了中
这多少有些令人遗憾。不过就算对此不满意,也不要随便更改哦。
从某种程度上来讲,这也是无法避免的事。如果在设定时,多下点工夫整理一下,到达中断
时刻时就能轻松一些了。反之,如果在设定时偷点懒,那么到达中断时刻时就要吃点苦头了。总之,要么提前做好准备,要么临时抱佛脚。究竟哪种做法好呢,要根据情况而定。

总结

`
现在我们执行“make run”看看吧。希望它能正常运行。会怎么样呢?貌似很顺利,太好了。
关于定时器我们还有想要修改的地方。不过大家肯定已经很困了,我们还是明天再继续吧。
再见!

相关文章:

30天开发操作系统 第 12 天 -- 定时器 v1.0

前言 定时器(Timer)对于操作系统非常重要。它在原理上却很简单&#xff0c;只是每隔一段时间(比如0.01秒)就发送一个中断信号给CPU。幸亏有了定时器&#xff0c;CPU才不用辛苦地去计量时间。……如果没有定时器会怎么样呢?让我们想象一下吧。 假如CPU看不到定时器而仍想计量时…...

RNN之:LSTM 长短期记忆模型-结构-理论详解(Matlab向)

前言 递归&#xff01;循环神经网络Recurrent Neural Network 循环神经网络&#xff08;又称递归神经网络&#xff0c;Recurrent Neural Network&#xff0c;RNN&#xff09;。是一种用于处理序列数据的神经网络结构&#xff0c;具有记忆功能&#xff0c;能够捕捉序列中的时间…...

vue的路由守卫逻辑处理不当导致部署在nginx上无法捕捉后端异步响应消息等问题

近期对前端的路由卫士有了更多的认识。 何为路由守卫&#xff1f;这可能是一种约定俗成的名称。就是VUE中的自定义函数&#xff0c;用来处理路由跳转。 import { createRouter, createWebHashHistory } from "vue-router";const router createRouter({history: cr…...

C++版的飞翔的小鸟

忙了几天&#xff0c;写了个小鸟游戏打发时间。 MINGWOPENGL 作者很懒&#xff0c;具体看readme.txt吧。 好吧&#xff0c;这就是个当网盘用的&#xff0c;没有售后服务的&#xff0c;而且还不怎么会上传文件。。。&#x1f602;&#x1f602; 顺便问下&#xff0c;这破玩意上…...

深入详解自然语言处理(NLP)中的语言模型:BERT、GPT及其他预训练模型的原理与应用

【自然语言处理】——深入详解自然语言处理&#xff08;NLP&#xff09;中的语言模型&#xff1a;BERT、GPT及其他预训练模型的原理与应用 自然语言处理&#xff08;NLP&#xff09;是人工智能&#xff08;AI&#xff09;领域中的重要分支&#xff0c;旨在通过计算机处理和分析…...

EFCore HasDefaultValueSql (续1 ValueGeneratedOnAdd)

前情&#xff1a;EFCore HasDefaultValueSql 小伙伴在使用 HasDefaultValueSql 时&#xff0c;对相关的 ValueGeneratedOnAdd 也有了疑问&#xff1a; ValueGeneratedOnAdd 和 HasDefaultValueSql 是 Entity Framework Core 中用于管理字段默认值的两种不同配置方式&#xff0…...

React PureComponent使用场景

1. 基本概念 1.1 什么是 PureComponent PureComponent 是 React 提供的一个优化类组件&#xff0c;它通过自动实现 shouldComponentUpdate 生命周期方法&#xff0c;对 props 和 state 进行浅比较来决定是否需要重新渲染组件。 1.2 与 Component 的区别 // 普通 Component …...

初学者如何利用AI辅助编程快速搭建软件原型

在当今这个快速发展的科技时代&#xff0c;AI辅助编程已成为软件开发领域的一大助力&#xff0c;尤其在搭建软件原型阶段&#xff0c;更是能帮助开发者快速探索众多想法&#xff0c;创造出新事物。吴恩达老师最近分享了关于搭建简单Web应用原型的一些最佳实践&#xff0c;其中重…...

六、Angular 发送请求/ HttpClient 模块

一、应用 HttpClient 模块 angular/common/http 中的 HttpClient 类基于浏览器提供的 XMLHttpRequest 接口。要想使用 HtpClient 模块&#xff0c;就要先导入 Anqular 的 HttpClientModule。大多数 Web 应用程序都会在根模块 AppModule 中导入它。 编辑 src/app/app.module.ts…...

11_Redis数据类型-Geo地理位置

1.Geo地理位置介绍 1.1 基本概念 Redis中的GEO(Geographic)是一种专门用于处理地理位置信息的功能模块,自Redis 3.2版本引入。它特别适合用于LBS(基于位置的服务)应用,如查找附近的餐馆、用户或兴趣点等。Redis GEO的实现依赖于有序集合(sorted set),并且利用了Geoh…...

电脑每次开机卡到windows图标界面进不去

我遇到的现象是&#xff1a; 可以正常开机&#xff0c;也可以重装系统&#xff0c;主板电池换过&#xff0c;硬盘&#xff0c;内存也没问题&#xff0c;但每次开机都会卡到这个界面不动&#xff0c;也不崩溃&#xff0c;也进不去系统。最后的解决方法&#xff1a;换CPU&#x…...

如何将 DotNetFramework 项目打包成 NuGet 包并发布

如何将 DotNetFramework 项目打包成 NuGet 包并发布 在软件开发过程中&#xff0c;将项目打包成 NuGet 包并发布到 NuGet 库&#xff0c;可以让其他开发者方便地引用和使用你的项目成果。以下是将 WixWPFWizardBA 项目打包成 NuGet 包并发布的详细步骤&#xff1a; 1. 创建 .n…...

7 分布式定时任务调度框架

先简单介绍下分布式定时任务调度框架的使用场景和功能和架构&#xff0c;然后再介绍世面上常见的产品 我们在大型的复杂的系统下&#xff0c;会有大量的跑批&#xff0c;定时任务的功能&#xff0c;如果在独立的子项目中单独去处理这些任务&#xff0c;随着业务的复杂度的提高…...

鸿蒙UI开发——日历选择器

1、概 述 在项目开发中&#xff0c;我们时常会用到日历选择器&#xff0c;效果如下&#xff1a; ArkUI已经为我们提供了组件&#xff0c;我们可以直接使用&#xff0c;下面针对日历组件做简单介绍。 2、CalendarPickerDialog 接口定义如下&#xff1a; // 定义日历选择器弹…...

【python基础——异常BUG】

什么是异常(BUG) 检测到错误,py编译器无法继续执行,反而出现错误提示 如果遇到错误能继续执行,那么就捕获(try) 1.得到异常:try的执行,try内只可以捕获一个异常 2.预案执行:except后面的语句 3.传入异常:except … as uestcprint(uestc) 4.没有异常:else… 5.鉴定完毕,收尾的语…...

【论文复现】量子海洋捕食者算法用于多级图像分割问题

目录 1.摘要2.海洋捕食者算法MPA原理3.改进策略4.结果展示5.参考文献6.代码获取 1.摘要 本文提出了一种基于量子理论的改进海洋捕食者算法&#xff08;QMPA&#xff09;&#xff0c;专门用于解决多级图像分割问题。QMPA算法利用薛定谔波函数中的概率函数来确定任何时刻粒子的位…...

【python自写包模块的标准化方法】

目标: 自写一个包,提供关于字符串和文件的模块 要求对异常可以检测 str_tools.py: def str_reverse(s):""":param s: 传入的字符串:return: 反转后的字符串"""# i -1# j 0# s2 ""# while i > (-len(s)):# s2 s[i]# …...

STL——二叉搜索树

目录 二叉搜索树的概念 ⼆叉搜索树的性能分析 ⼆叉搜索树的插⼊ ⼆叉搜索树的查找 ⼆叉搜索树的删除 中序遍历结果为升序序列 二叉搜索树的概念 ⼆叉搜索树⼜称⼆叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树 • 若它的左⼦树不为空&#…...

2025年XR行业展望:超越虚拟,融合现实

随着科技的飞速发展,扩展现实(XR)技术正逐渐从科幻走进日常生活。2025年,除了备受瞩目的AI百镜大战,XR行业同样充满期待,一系列创新产品和技术进步有望重塑我们对数字世界的体验。 Meta Quest 3S:VR行业的风向标 作为Meta旗下的拳头产品,Quest系列一直是VR市场的领军…...

python异常机制

异常是什么&#xff1f; 软件程序在运行过程中&#xff0c;非常可能遇到刚刚提到的这些问题&#xff0c;我们称之为异常&#xff0c;英文是Exception&#xff0c;意思是例外。遇到这些例外情况&#xff0c;或者交异常&#xff0c;我们怎么让写的程序做出合理的处理&#xff0c…...

JVM与Java体系结构

一、前言: Java语言和JVM简介: Java是目前最为广泛的软件开发平台之一。 JVM:跨语言的平台 随着Java7的正式发布&#xff0c;Java虚拟机的设计者们通过JSR-292规范基本实现在Java虚拟机平台上运行非Java语言编写的程序。 Java虚拟机根本不关心运行在其内部的程序到底是使用何…...

【Python】Python与C的区别

文章目录 语句结束符代码块表示变量声明函数定义注释格式Python的标识符数据输入input()函数数据输出print()函数 语句结束符 C 语言 C 语言中每条语句必须以分号;结束。例如&#xff0c;int a 10;、printf("Hello, World!");。分号是语句的一部分&#xff0c;用于…...

怎么抓取ios 移动app的https请求?

怎么抓取IOS应用程序里面的https&#xff1f; 这个涉及到2个问题 1.电脑怎么抓到IOS手机流量&#xff1f; 2.HTTPS怎么解密&#xff1f; 部分app可以使用代理抓包的方式&#xff0c;但是正式点的app用代理抓包是抓不到的&#xff0c;例如pin检测&#xff0c;证书双向校验等…...

中学综合素质笔记3

第一章职业理念 第三节 教师观 考情提示&#xff1a; 单选题材料分析题 学习要求&#xff1a; 理解、 识记、 运用 &#xff08;一&#xff09;教师职业角色的转变&#xff08;单选材料分析&#xff09; 从教师与学生的关系看——对学生 新课程要求教师应该是学生学习的引…...

U盘加密软件哪个好用?免安装、安全、防复制

U盘作为一种便携式存储设备&#xff0c;因其携带方便、使用灵活而广受欢迎。然而&#xff0c;U盘的易失性也使其成为数据泄露的高风险载体。为了确保U盘中数据的安全性&#xff0c;使用专业的U盘加密软件是必不可少的。 免安装 更方便 《U盘超级加密3000》这款软件下载后直接…...

C++异常

1.C语言的错误处理方式 1.1直接终止程序 利用assert和exit都是直接终止程序。 1.2返回错误码 例如C语言程序的很对接口函数都会将错误信息存储在errno中表示错误。当我们自己设计函数时&#xff0c;函数返回值和返回错误码容易混淆&#xff0c;且如果函数调用栈较深时&…...

银河麒麟v10 arm版 鲲鹏 U盘安装 +解决 安装源 设置基础软件仓库时出错

人人为我&#xff0c;我为人人&#xff0c;分享出来&#xff0c;避免他人踩坑 U盘刻录 出现问题 进入系统安装界面&#xff0c;这里可以看到在“软件”-“安装源”选项上报错了&#xff0c;提示“设置基础软件仓库时出错” 2种解决办法 1. 有网的情况&#xff08;注意自己查地址…...

[network]回顾:集线器(Hub)

集线器&#xff08;Hub&#xff09;的发明是计算机网络发展史上的一个重要里程碑。它最初的设计目的是为了解决局域网&#xff08;LAN&#xff09;中多台计算机共享网络资源的需求。 #mermaid-svg-OAmOmKYGAXoglS5z {font-family:"trebuchet ms",verdana,arial,sans-…...

【Vue.js 组件化】高效组件管理与自动化实践指南

文章目录 摘要引言组件命名规范与组织结构命名规范目录组织 依赖管理工具自动化组件文档生成构建自动引入和文档生成的组件化体系代码结构自动引入组件配置使用 Storybook 展示组件文档自动生成 代码详解QA 环节总结参考资料 摘要 在现代前端开发中&#xff0c;组件化管理是 V…...

打桩机:灾害救援中的 “应急尖兵”,稳固支撑的保障|鼎跃安全

在自然灾害或突发事故中&#xff0c;如地震、泥石流、洪涝灾害、山体滑坡等&#xff0c;地质条件的不稳定可能导致建筑物倒塌、道路损毁、堤坝决口等情况&#xff0c;严重威胁人员和财产安全。 打桩机是一种用于将桩打入地基的重型机械设备&#xff0c;其主要功能是提供支撑力&…...

java1-相对路径与绝对路径

注意注意~开始新部分啦! 开始正式分享java前,先为大家分享一下一个常用的概念---文件的相对路径与绝对路径. 开篇明义: 相对路径是指一个文件或目录相对于当前工作目录的路径。相对路径不包含根目录&#xff0c;而是从当前目录开始计算。 绝对路径是指一个文件或目录从根目录…...

工厂管理中 BOM(物料清单)

工厂管理中 BOM&#xff08;物料清单&#xff09;的一些优点&#xff1a; 1. 提高生产计划准确性 - 准确反映产品所需的物料及数量&#xff0c;为生产计划提供可靠依据&#xff0c;减少因物料估算错误导致的生产延误。 2. 优化成本控制 - 有助于精确计算产品成本&…...

allure报告修改默认语言为中文

1、项目根目录创建.py文件&#xff0c;把代码复制进去 import os from pathlib import Pathdef create_settings_js_file(directory"../pytest_mytt/reports/allures/", filenamesettings.js):# 创建或确认目录存在Path(directory).mkdir(parentsTrue, exist_okTrue…...

浅谈弱电系统RVVP和RVSP电缆的区别(

1、RVVP 1.1RVVP电缆定义&#xff1f; RVVP电缆抗干扰软电缆、屏蔽电缆、信号电缆、控制电缆&#xff08;名字很多&#xff09;&#xff0c;学名&#xff1a;铜芯-聚氯乙烯绝缘-屏蔽聚氯乙烯护套-软电缆。 1.2RVVP执行标准 主要执行标准为JB/T8734.5-2016&#xff0c;部…...

MySQL 入门大全:常用函数

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…...

RV1126+FFMPEG推流项目(1)总体框架讲解

音视频推流项目的讲解 项目介绍 本项目通过 RV1126 采集摄像头和麦克风数据&#xff0c;采用 H.264/H.265 视频编码技术和 AAC 音频编码技术进行压缩和合成复合流&#xff0c;然后推送到流媒体服务器。 项目框图 下图展示了整个项目的总体流程图&#xff0c;核心部分包括&a…...

量子计算遇上人工智能:突破算力瓶颈的关键?

引言&#xff1a;量子计算遇上人工智能——突破算力瓶颈的关键&#xff1f; 在数字化时代的浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;正以前所未有的速度改变着我们的生活&#xff0c;从语音助手到自动驾驶&#xff0c;从医学诊断到金融分析&#xff0c;无不彰显其…...

Kafka消费者如何优雅下线

一、背景 我们在Kafka消费程序中&#xff0c;可能会调用dubbo接口&#xff0c;也可能会使用线程池&#xff0c;连接池等&#xff0c;但是在服务下线的时候&#xff0c;kafka的消费总是会报错。比如dubbo接口就会抛出异常RpcException: The channel is closed. 这说明kafka还在…...

Go语言的网络编程

Go语言的网络编程 Go语言&#xff08;又称Golang&#xff09;是一种由Google开发的开源编程语言&#xff0c;以简洁、高效和并发为主要特点。网络编程是Go语言的一个重要应用领域&#xff0c;其优秀的并发模型和丰富的标准库使得Go成为开发高性能网络应用的理想选择。本篇文章…...

网络安全 | 网络安全法规:GDPR、CCPA与中国网络安全法

网络安全 | 网络安全法规&#xff1a;GDPR、CCPA与中国网络安全法 一、前言二、欧盟《通用数据保护条例》&#xff08;GDPR&#xff09;2.1 背景2.2 主要内容2.3 特点2.4 实施效果与影响 三、美国《加利福尼亚州消费者隐私法案》&#xff08;CCPA&#xff09;3.1 背景3.2 主要内…...

外部获取nVisual所在层级方法

Iframe嵌入nVisual&#xff0c;在iframe渲染完成之后&#xff0c;以后通过增加window.addEventListener()方法监听message事件&#xff0c;来获取nvisual当前的所在层级以及所选中的节点列表以及线缆列表。 nVisualPatrolDiagramIdList 变量是获取nVisual当前所在的层级的ID值…...

ARIMA模型 (AutoRegressive Integrated Moving Average) 算法详解与PyTorch实现

ARIMA模型 (AutoRegressive Integrated Moving Average) 算法详解与PyTorch实现 目录 ARIMA模型 (AutoRegressive Integrated Moving Average) 算法详解与PyTorch实现1. ARIMA模型概述1.1 时间序列预测1.2 ARIMA的优势2. ARIMA的核心技术2.1 自回归 (AR)2.2 差分 (I)2.3 移动平…...

解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域

解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务&#xff08;Spring MVC Springboot&#xff09;同时允许跨域 Tomcat 配置允许跨域Web 项目配置允许跨域Tomcat 同时允许静态文件和 Web 服务跨域 偶尔遇到一个 Tomcat 部署项目跨域问题&#xff0c;因为已经处理…...

【深度学习量化交易13】继续优化改造基于miniQMT的量化交易软件,增加补充数据功能,优化免费下载数据模块体验!

我是Mr.看海&#xff0c;我在尝试用信号处理的知识积累和思考方式做量化交易&#xff0c;应用深度学习和AI实现股票自动交易&#xff0c;目的是实现财务自由~ 目前我正在开发基于miniQMT的量化交易系统——看海量化交易系统。 MiniQMT是一种轻量级的量化交易解决方案&#xff0…...

【并发篇】CompletableFuture学习

CompletableFuture 异步编程 前言 我们异步执行一个任务时&#xff0c;一般是用线程池 Executor 去创建。 如果不需要有返回值&#xff0c;任务实现 Runnable 接口&#xff1b;如果需要有返回值&#xff0c;任务实现 Callable 接口&#xff0c;调用 Executor 的 submit 方法…...

【动手学电机驱动】STM32-MBD(3)Simulink 状态机模型的部署

STM32-MBD&#xff08;1&#xff09;安装 Simulink STM32 硬件支持包 STM32-MBD&#xff08;2&#xff09;Simulink 模型部署入门 STM32-MBD&#xff08;3&#xff09;Simulink 状态机模型的部署 [STM32-MBD&#xff08;4&#xff09;Simulink 状态机实现按键控制] (https://bl…...

springCloudGateWay使用总结

1、什么是网关 功能: ①身份认证、权限验证 ②服务器路由、负载均衡 ③请求限流 2、gateway搭建 2.1、创建一个空项目 2.2、引入依赖 2.3、加配置 3、断言工厂 4、过滤工厂 5、全局过滤器 6、跨域问题...

04、Redis深入数据结构

一、简单动态字符串SDS 无论是Redis中的key还是value&#xff0c;其基础数据类型都是字符串。如&#xff0c;Hash型value的field与value的类型&#xff0c;List型&#xff0c;Set型&#xff0c;ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了…...

zephyr移植到STM32

Zephy如何移植到单片机 1. Window下搭建开发环境1.1 安装Choncolatey1.2 安装相关依赖1.3创建虚拟python环境1.4 安装west1.4.1 使用 pip 安装 west1.4.2 检查 west 安装路径1.4.3 将 Scripts路径添加到环境变量1.4.4 验证安装 1.5 获取zephyr源码和[安装python](https://so.cs…...

Windows使用AutoHotKey解决鼠标键连击现象(解决鼠标连击、单击变双击的故障)

注&#xff1a;罗技鼠标&#xff0c;使用久了之后会出现连击现象&#xff0c;如果刚好过保了&#xff0c;可以考虑使用软件方案解决连击现象&#xff1a; 以下是示例AutoHotKey脚本&#xff0c;实现了调用XButton1用于关闭窗口&#xff08;以及WinW&#xff0c;XButton2也导向…...