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

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

前言

定时器(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 天 -- 定时器

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

android 启动页倒计时页面编写

一、需求和技术 1、实现5,4,3,2,1启动页倒计时 2、倒计时实现使用CountDownTimer 二、activity代码 public class OpenActivity extends AppCompatActivity {private Button in;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanc…...

Win10和11 git/Android Studio遇到filename too long问题的解决

1、打开windows长文件、长路径支持&#xff1a; 可以参考这篇文章&#xff1a; 修改注册表方法&#xff1a; 使用Admin登陆machine&#xff0c;在run中输入regedit并回车&#xff1b; 找到路径 ’Computer -> HKEY_LOCAL_MACHINE -> SYSTEM -> CurrentControlSet -&g…...

OpenCV相机标定与3D重建(43)用于计算矫正和重映射的变换函数initUndistortRectifyMap()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算畸变矫正和校正变换映射。 该函数计算联合的畸变矫正和校正变换&#xff0c;并以 remap 所需的地图形式表示结果。矫正后的图像看起来像是原…...

运行npm install 时,卡在sill idealTree buildDeps没有反应

运行npm install 时&#xff0c;卡在sill idealTree buildDeps没有反应 原因&#xff1a; 淘宝镜像源的域名早已经过期&#xff0c;所以我们需要绑定新的镜像源。 2021 年&#xff0c;淘宝就发布了消息称&#xff0c;npm 淘宝镜像已经从 registry.npm.taobao.org 切换到了 re…...

与 Oracle Dataguard 相关的进程及作用分析

与 Oracle Dataguard 相关的进程及作用分析 目录 与 Oracle Dataguard 相关的进程及作用分析与 Oracle Dataguard 相关的进程及作用分析一、主库的进程1、LGWR 进程2、ARCH进程3、LNS 进程 二、备库的进程1、RFS 进程2、ARCH3、MRP&#xff08;Managed Recovery Process&#x…...

如何在Windows上编译OpenCV4.7.0

前言 ​ 参考&#xff1a;Win10 下编译 OpenCV 4.7.0详细全过程&#xff0c;包含xfeatures2d 这里在其基础上还出现了一些问题&#xff0c;仅供参考。 正文 一、环境 1、win10 2、cmake-gui 3、opencv4.7.0 4、VS2019 二、编译过程 1、下载需要的文件&#xff1a; 通…...

get和post有什么区别

GET和POST是HTTP协议中两种常用的请求方法&#xff0c;它们在用途、参数传递方式、缓存处理、安全性等方面存在显著差异。 以下是对GET和POST区别的详细讲解&#xff0c;并给出示例演示。 一、GET和POST的区别 用途 GET&#xff1a;主要用于获取信息&#xff0c;即进行查询操…...

STM32之CAN通讯(十一)

STM32F407 系列文章 - CAN通讯&#xff08;十一&#xff09; 目录 前言 一、CAN 二、CAN驱动电路 三、CAN软件设计 1.CAN状态初始化 2.头文件相关定义 3.接收中断服务函数 4.用户层使用 1.用户层相关定义 2.发送数据 3.接收数据 1.查询方式处理 2.中断方式处理 3…...

23.行号没有了怎么办 滚动条没有了怎么办 C#例子

新建了一个C#项目&#xff0c;发现行号没有了。 想把行号调出来&#xff0c;打开项目&#xff0c;选择工具>选项> 如下图&#xff0c;在文本编辑器的C#里有一个行号&#xff0c;打开就可以了 滚动条在这里&#xff1a;...

QT 下拉菜单设置参数 起始端口/结束端口/线程数量 端口扫描4

上篇文章QT实现 端口扫描暂停和继续功能 3-CSDN博客 双击 添加对话框类 界面设计 由于主体代码已经写完&#xff0c;只需要更改参数的获取即可 获取起始端口结束端口的输入 槽函数 给主界面类添加调用对话框类的功能 实现功能&#xff1a;点击菜单项可以弹出对话框窗体 增加槽…...

spark——RDD算子集合

目录 算子转换算子示例mapflatMapReduceByKeyfilterdistinctglomgroupBygroupByKeySortBysortByKeyunion交集intersection和差集subtractjoinpartitionBymapPartitionsample 行动算子示例ForeachPartitionForeachSaveAsTextFileCountByKeyReducefoldfirst、take、counttop、tak…...

【ArcGIS Pro二次开发实例教程】(1):图层的前置、后置

一、简介 此工具要实现的功能是&#xff1a;将内容框中当前选定的图层移到最顶层或最底层。 主要技术要点包括&#xff1a; 1、Config.daml文件设置&#xff08;UI设置&#xff09; 2、按钮的图片和位置设置 3、当前选定图层的获取 4、图层在内容列表中位置的获取和移动 …...

idea 运行 docker-compose 文件问题

我idea 连接远程docker 然后本地运行compose 文件出了问题 C:\Program Files\Docker\Docker\resources\bin\docker.exe compose -f D:\aproject\group-buy-market\suger_group_buy\docs\dev-ops\docker-compose-environment.yml -p dev-ops up -d正在准备 Docker SSH 环境…ti…...

深度学习中的正则化方法

最近看到了正则化的内容&#xff0c;发现自己对正则化的理解已经忘得差不多了&#xff0c;这里在整理一下&#xff0c;方便以后查阅。 深度学习中的正则化方法 1. L2 正则化&#xff08;L2 Regularization&#xff09;2. L1 正则化&#xff08;L1 Regularization&#xff09;3.…...

LInux单机安装Redis

1. 安装gee工具包 由于Redis是基于c语言编写的所以安装的时候需要先安装gee以及gcc的依赖,yum云用不了可以看一下这个 linux 替换yum源镜像_更换yum镜像源-CSDN博客 yum install -y gcc tcl 2. 添加redis的压缩包 3. 上传到Linux 上传到 /usr/local/src 目录、这个目录一般用于…...

kafka使用以及基于zookeeper集群搭建集群环境

一、环境介绍 zookeeper下载地址&#xff1a;https://zookeeper.apache.org/releases.html kafka下载地址&#xff1a;https://kafka.apache.org/downloads 192.168.142.129 apache-zookeeper-3.8.4-bin.tar.gz kafka_2.13-3.6.0.tgz 192.168.142.130 apache-zookee…...

深入理解 pytest_runtest_makereport:如何在 pytest 中自定义测试报告

pytest_runtest_makereport 是 pytest 系统中的一个钩子函数&#xff0c;它允许我们在测试执行时获取测试的报告信息。通过这个钩子&#xff0c;我们可以在测试运行时&#xff08;无论是成功、失败还是跳过&#xff09;对测试结果进一步处理&#xff0c;比如记录日志、添加自定…...

嵌入式技术之Linux(Ubuntu) 一

一、Linux入门 1.硬件和操作系统以及用户的关系 一个传感器&#xff0c;获得数据后&#xff0c;需要向服务器发送数据。传感器传数据给上位机。 上位机需要一个程序来接收数据&#xff0c;那么这个上位机是什么机器&#xff1f; 我们的笔记本电脑就可以当成上位机。 两个手…...

VB.NET CRC32 校验

在 VB.NET 中实现 CRC32 校验并在校验失败时退出程序&#xff0c;你可以按照以下步骤进行&#xff1a; ‌实现 CRC32 计算函数‌&#xff1a;首先&#xff0c;你需要一个函数来计算给定数据的 CRC32 值。 ‌比较计算的 CRC32 值‌&#xff1a;然后&#xff0c;你需要将计算出的…...

iOS - 弱引用表(Weak Reference Table)

1. 基本数据结构 // 弱引用表的基本结构 struct weak_table_t {weak_entry_t *weak_entries; // 保存所有的弱引用对象size_t num_entries; // 当前存储的弱引用数量uintptr_t mask; // 哈希表大小掩码uintptr_t max_hash_displacement; /…...

Taro地图组件和小程序定位

在 Taro 中使用腾讯地图 1.首先在项目配置文件 project.config.json 中添加权限&#xff1a; {"permission": {"scope.userLocation": {"desc": "你的位置信息将用于小程序位置接口的效果展示"}} }2.在 app.config.ts 中配置&#x…...

汇编实现函数调用

x86_64 通过将函数参数存放在栈中的方式来实现参数传递。 # PURPOSE: Program to illustrate how functions work # This program will compute the value of # 2^3 5^2 ## Everything in the main program is stored in registers, # so the data section…...

C#—Task异步的常用方法及TaskFactory工厂类详解

Task异步的常用方法 C# 中的 Task 类是 System.Threading.Tasks 命名空间的一部分&#xff0c;用于表示异步操作。 以下是一些常用的 Task 类方法&#xff1a; 一、Task.Run(Action action): 此静态方法用于在后台运行一个新任务&#xff0c;并返回与该任务关联的 Task 实例…...

JAVA | 通过自定义注解与AOP防止接口重复提交

关注&#xff1a;CodingTechWork 引言 在Web应用开发中&#xff0c;特别是在处理表单提交或API调用时&#xff0c;可能会遇到用户因网络延迟、按钮多次点击等原因导致的重复提交问题。为了解决这一问题&#xff0c;通常的做法是在前端禁用提交按钮&#xff0c;或者在后端使用唯…...

从零手写实现redis(四)添加监听器

1、删除监听器 /*** 删除监听器接口** author binbin.hou* since 0.0.6* param <K> key* param <V> value*/ public interface ICacheRemoveListener<K,V> {/*** 监听* param context 上下文* since 0.0.6*/void listen(final ICacheRemoveListenerContext&…...

Spring Boot项目中使用单一动态SQL方法可能带来的问题

1. 查询计划缓存的影响 深入分析 数据库系统通常会对常量SQL语句进行编译并缓存其执行计划以提高性能。对于动态生成的SQL语句&#xff0c;由于每次构建的SQL字符串可能不同&#xff0c;这会导致查询计划无法被有效利用&#xff0c;从而需要重新解析、优化和编译&#xff0c;…...

51单片机——中断(重点)

学习51单片机的重点及难点主要有中断、定时器、串口等内容&#xff0c;这部分内容一定要认真掌握&#xff0c;这部分没有学好就不能说学会了51单片机 1、中断系统 1.1 概念 中断是为使单片机具有对外部或内部随机发生的事件实时处理而设置的&#xff0c;中断功能的存在&#…...

MySQL insert or update方式性能比较

MySQL中&#xff0c;有如下两种方式&#xff0c;哪种方式比较好&#xff1f; 1、先使用enterprise_id字段查询数据表&#xff0c;如果表中存在记录&#xff0c;则更新记录&#xff1b;如果不存在&#xff0c;则插入记录&#xff1b; 2、使用“INSERT INTO XXX ON DUPLICATE K…...

Linux下常用命令

本文以笔记的形式记录Linux下常用命令。 注1&#xff1a;限于研究水平&#xff0c;阐述难免不当&#xff0c;欢迎批评指正。 注2&#xff1a;文章内容会不定期更新。 一、Ubuntu 添加账号 useradd -m -s /bin/bash -d /home/newuser newuser:newuser passwd newuser 二、 Ce…...

计算机网络、嵌入式等常见问题简答

1.嵌入式系统中经常要用到无限循环&#xff0c;如何用C编写死循环 答&#xff1a;while(1){}或者for(;;) 2.程序的局部变量存在于哪里&#xff0c;全局变量存在于哪里&#xff0c;动态申请数据存在于哪里。 答&#xff1a;程序的局部变量存在于栈区&#xff1b;全局变量存在…...

嵌入式中QT实现文本与线程控制方法

第一:利用QT进行文件读写实现 利用QT进行读写文本的时候进行读写,读取MP3歌词的文本,对这个文件进行读写操作。 实例代码,利用Qfile,对文件进行读写。 //读取对应文件文件,头文件的实现。 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #incl…...

141.环形链表 142.环形链表II

141.环形链表 & 142.环形链表II 141.环形链表 思路&#xff1a;快慢指针 or 哈希表 快慢指针代码&#xff1a; class Solution { public:bool hasCycle(ListNode *head) {if(headnullptr||head->nextnullptr)return false;ListNode *fasthead->next; //不能设置成…...

计算机网络掩码、最小地址、最大地址计算、IP地址个数

一、必备知识 1.无分类地址IPV4地址网络前缀主机号 2.每个IPV4地址由32位二进制数组成 3. /15这个地址表示网络前缀有15位&#xff0c;那么主机号32-1517位。 4.IP地址的个数&#xff1a;2**n (n表示主机号的位数) 5.可用&#xff08;可分配&#xff09;IP地址个数&#x…...

第3章——HTTP报文内的HTTP信息

第3章——HTTP报文内的HTTP信息 HTTP报文 ​ 用于HTTP协议交互的信息被称为HTTP报文&#xff0c;分为请求报文和响应报文。分为Head&#xff0c;Body 结构&#xff1a; 请求行&#xff1a;包含用于请求的方法&#xff0c;请求URI和HTTP版本。 状态行&#xff1a;包含表明响应…...

Minio-Linux-安装

文章目录 1.Linux安装1.下载源码包2.上传到/usr/local/minio1.进入目录2.上传 3.开放执行权限4.创建minio文件存储目录及日志目录5.编写启动的shell脚本1.脚本编写2.赋予执行权限 6.启动&#xff01;1.执行run脚本2.查看日志3.开放9001和9000端口1.服务器2.安全组3.访问&#x…...

面试高阶问题:对称加密与非对称加密的原理及其应用场景

目录 第一章 对称加密原理及算法实现 第二章 非对称加密原理及算法实现 第三章 对称加密与非对称加密的应用场景 第四章 对称加密与非对称加密的应用实例 第五章 对称加密与非对称加密的对比分析 第一章 对称加密原理及算法实现 1.1 对称加密的原理 对称加密,又称私钥加密…...

报错 - decord 在 macOS Silicon 安装失败

问题&#xff1a;在 macOS M2 上 pip 安装 decord 出错&#xff1a; ERROR: Could not find a version that satisfies the requirement decord (from versions: none) ERROR: No matching distribution found for decord使用 decord 源码编译&#xff0c;make 也会出很多问题 …...

英伟达 RTX 5090 显卡赋能医疗大模型:变革、挑战与展望

一、英伟达 RTX 5090 与 RTX 4090 技术参数对比 1.1 核心架构与制程工艺 在探讨英伟达 RTX 4090 与 RTX 5090 的差异时&#xff0c;核心架构与制程工艺无疑是最为关键的基础要素&#xff0c;它们从根本上决定了两款显卡的性能上限与应用潜力。 1.1.1 核心架构差异 RTX 4090…...

PyCharm简单调试

本文简单讲述一下PyCharm中经常用到的调试操作。 示例代码如下&#xff1a; for i in range(10):print("hello", i)if i > 2:print("ok!")在代码前面打上断点&#xff0c;如下图所示&#xff1a; 单机调试按钮Debug 单机Resume Program按钮&#xf…...

快速入门Spring Cloud Alibaba,轻松玩转微服务

​ 1 快速入门Spring Cloud Alibaba&#xff0c;轻松玩转微服务 1.1 架构 架构图&#xff1a; 1.2 项目结构 1.2.1 系统框架版本 版本适配查看&#xff1a;https://sca.aliyun.com/docs/2023/overview/version-explain/ Spring Boot Version &#xff1a;3.2.4 Spring Clo…...

浅尝Appium自动化框架

浅尝Appium自动化框架 Appium自动化框架介绍Appium原理Appium使用安装平台驱动 Appium自动化框架介绍 Appium 是一个开源的自动化测试框架&#xff0c;最初设计用于移动应用的测试&#xff0c;但现在它也扩展了对桌面端应用的支持。Appium 使得自动化测试变得更加简单&#xf…...

poi-tl+kkviewfile实现生成pdf业务报告

需求背景&#xff0c;需要把ai生成的一些业务数据&#xff0c;生成一份pdf报告 需求分析 简单来说&#xff0c;就是json生成pdf的方案。 直接生成pdf。适合一些pdf样式简单的场景&#xff0c;一般就是纯文本按序渲染&#xff0c;或者是纯表格。如果需要一些复杂的排布&#x…...

python导入模块失败

运行下面代码模块&#xff0c;出现报错&#xff0c;导入模块失败 import torch from layers.Embed import DataEmbedding from layers.Conv_Blocks import Inception_Block_V1 将你自己的目录添加到 sys.path&#xff0c;假设你的目录位置是D://winhzq//桌面//pydemo//…...

Vulkan 学习(12)---- Vulkan pipeline 创建

目录 Vulkan 渲染管线顶点输入阶段输入装配阶段顶点着色器阶段细分控制、评估着色器阶段(可选)几何着色器阶段(可选)图元装配阶段光栅化阶段片段着色器片段测试阶段混合阶段 Vulkan 渲染管线 渲染管线可以看作是一条生产流水线&#xff0c;定义了从输入顶点到输出图像的所有步…...

BloombergGPT: A Large Language Model for Finance——面向金融领域的大语言模型

这篇文章介绍了BloombergGPT&#xff0c;一个专门为金融领域设计的大语言模型&#xff08;LLM&#xff09;。以下是文章的主要内容总结&#xff1a; 背景与动机&#xff1a; 大语言模型&#xff08;如GPT-3&#xff09;在多个任务上表现出色&#xff0c;但尚未有针对金融领域的…...

来说数据库

什么是数据库&#xff1f; 是部署在操作系统上&#xff0c;把数据按一定的数据模型组织、永久存储&#xff0c;并可以被用户共享的软件系统。 其实数据库&#xff0c;可以理解为&#xff0c;把数据都存成文件&#xff0c;有很多的文件和很多的目录&#xff0c;不好管理&#xf…...

教程:从pycharm基于anaconda构建机器学习环境并运行第一个 Python 文件

1. 安装 PyCharm 访问 PyCharm 官方网站&#xff1a;https://www.jetbrains.com/pycharm/。下载社区版&#xff08;免费&#xff09;或专业版&#xff08;收费&#xff0c;提供更多功能&#xff09;。按照操作系统的安装指导安装 PyCharm。安装后打开 PyCharm&#xff0c;并根…...

嵌入式驱动开发详解11(INPUT子系统)

文章目录 前言input子系统简介主要结构体API函数input子系统驱动框架上报事件后续设备树配置方式参考文献 前言 按键、鼠标、键盘、触摸屏等都属于输入(input)设备&#xff0c;Linux 内核为此专门做了一个叫做 input 子系统的框架来处理输入事件。输入设备本质上还是字符设备&…...

动态规划解决目标和问题

代码随想录链接:代码随想录 思路: 可以将数组分为两部分&#xff0c;其中一部分记作left&#xff0c;其中数字的符号全为,而另外一部分记作right&#xff0c;其中数字的符号全为-。这里全为-的意思不是真正的符号为-&#xff0c;而表示这一堆数字在计算时取值为负 因此有如下…...