schedule()函数的调用时机(周期性调度)

今天纠正了一个由来已久的认识错误:一个进程的时间片用完之后,当再次发生时钟中断时内核会调用schedule()来进行调度,把当前的进程上下文切出CPU,并把选定的下一个进程切换进来运行。我一直以为schedule()函数是在时钟中断处理函数中被调用的。其实不是,如果真是这样的话,那么在第一次这样的调度完成之后,时钟中断可能就要被mute掉了,系统从此失去“心跳”。

我之前那样理解是基于这样两点考虑:

  1. 在时钟中断发生时会更新进程的时间片(对于CFS调度器来说,就是更新进程的虚拟运行时间virtual run-time)。 更新完这个时间信息之后,立刻运行schedule()顺理成章,调度就应该在这个时机发生;
  2. 中断发生之后,(以arm架构为例)系统会从IRQ模式迅速切换成SVC模式,并且在此后的中断处理过程中,中断是关闭的,只有在切换回USR模式(其中还经过IRQ模式)时,才回再把中断打开。如果在中断处理函数中调用schedule()并不会带来中断无法再次打开的问题,因为最后总是要切换到USR模式的,那时时钟中断也总是有机会能重新打开的。

但是我没有注意到一个问题,就是ARM中断控制器(VIC)的mask/unmask操作。在进入中断响应函数之前,需要先对相应的中断设计掩码,即把正在处理的这个中断mask掉,在响应完后再把它unmask回来,好让中断能够继续发生。这段代码在kernel/irq/chip.c中(以下是经简化的示例代码):

void handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
 ......
 mask_ack_irq(desc, irq);
 ......
 action_ret = handle_IRQ_event(irq, action);
 ......
 unmask_irq(desc, irq);
 ......
}

注:中断的mask/unmask与enable/disable是两个层次的概念:enable/disable是对所有中断而言,如果disable的话任何中断都不会发生;而mask/unmask是对一个特定的中断而言的,mask之后,指定的中断不会再发生了,但并不影响其它的中断。

所以,基于这种设计,在中断响应过程中,只能更新进程的时间片,却不可以进行调度。如果一旦在上述的handle_IRQ_event()里面调用了schedule()函数,就会立刻切换到其它进程(SVC模式,内核态),接下来的unmask_irq()执行不到,时钟中断就再也没有机会打开。切换到下一个进程之后,因为没有时钟中断,系统也就失去了心跳。

正确的方法是在中断处理快要结束时调用schedule():

在中断处理的汇编代码中(arm架构下主要看__irq_usr),主要的中断处理过程都完成之后,会跑到ret_to_user处准备返回用户模式。这时就会检查进程的thread_info结构中是否置有“_TIF_NEED_RESCHED”标志,如果是的话,说明需要进行进程调度,这时再调用函数schedule()。在这个时间点上,中断控制器中相应的位已经被unmask过,接下来只要开中断即可。

上面提到的汇编代码比较零散,这里就不贴了,都在文件arch/arm/kernel/entry-armv.S和entry-common.S中。

下面是一个简单的C语言EDF调度器示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_TASKS 10 typedef struct { int id; // 任务ID int period; // 周期 int deadline; // 最后期限 int wcet; // 最坏情况执行时间 int remaining_time; // 剩余时间 } Task; Task tasks[MAX_TASKS]; int num_tasks = 0; // 添加任务 void add_task(int period, int deadline, int wcet) { Task task; task.id = num_tasks + 1; task.period = period; task.deadline = deadline; task.wcet = wcet; task.remaining_time = wcet; tasks[num_tasks++] = task; } // EDF调度算法 int schedule() { int time = 0; int i, j; while (time < 1000) { // 模拟1000个时间步 int min_deadline = 0x7fffffff; int selected_task = -1; // 遍历所有任务,找到最早的最后期限 for (i = 0; i < num_tasks; i++) { if (tasks[i].deadline < min_deadline && tasks[i].remaining_time > 0) { min_deadline = tasks[i].deadline; selected_task = i; } } // 如果没有可用的任务,结束调度 if (selected_task == -1) { break; } // 执行选中的任务 tasks[selected_task].remaining_time--; printf("Time=%d, Executing Task %d\n", time, tasks[selected_task].id); // 如果任务已经完成,更新最后期限和剩余时间 if (tasks[selected_task].remaining_time == 0) { tasks[selected_task].deadline += tasks[selected_task].period; tasks[selected_task].remaining_time = tasks[selected_task].wcet; } time++; } return 0; } int main() { add_task(4, 4, 2); add_task(6, 6, 4); add_task(8, 8, 6); schedule(); return 0; } ``` 在这个示例代码中,我们定义了一个Task结构体,包含任务的id、周期、最后期限、最坏情况执行时间和剩余时间等信息。我们使用一个全局数组tasks来存储所有的任务,并通过add_task函数添加任务。 在EDF调度算法中,我们需要遍历所有任务,找到最早的最后期限,并把该任务作为当前要执行的任务。如果没有可用的任务,调度结束。我们使用一个while循环来模拟时间的流逝,每次循环中执行选中的任务,并更新任务的最后期限和剩余时间。如果任务已经完成,我们需要把最后期限更新为下一个周期,并把剩余时间重置为最坏情况执行时间。 在main函数中,我们添加了三个周期分别为4、6、8的任务,并调用schedule函数进行调度。 需要注意的是,这个示例代码并没有考虑实时性约束,只是简单地演示了EDF调度算法的基本原理。在实际应用中,我们需要根据任务的实时性约束来调整调度算法,并确保任务能够按时完成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值