Project2 进程调度
实验要求
在MINIX3中实现 Earlist-Deadline-First 近似实时调度功能:
- 提供设置进程执行期限的系统调度
chrt(long deadline)
,用于将调用该系统调用的进程设为实时进程,其执行的期限为:从调用处开始deadline秒。例如:
1 |
|
chrt
的定义:
int chrt(long deadline);
deadline 是最后期限值(秒),返回值1表示成功,返回值0表示该调用出错
- 在内核进程表中需要增加一个条目,用于表示进程的实时属性;修改相关代码,新增一个系统调用chrt,用于设置其进程表中的实时属性。
- 修改proc.c和proc.h中相关的调度代码,实现最早deadline的用户进程相对于其它用户进程具有更高的优先级,从而被优先调度运行。
- 在用户程序中,可以在不同位置调用多次chrt系统调用,在未到deadline之前,调用chrt将会改变该程序的deadline。
- 未调用chrt的程序将以普通的用户进程(非实时进程)在系统中运行
实现过程
增加系统调用chrt
MINIX3中的系统调用结构分成三个层次:应用层,服务层,内核层。在这三层中分别进行代码修改,实现系统调用chrt的信息传递。从应用层用_syscall
将信息传递到服务层,在服务层用_kernel_call
将信息传递到内核层,在内核层对进程结构体增加deadline成员。
应用层
需要添加的系统调用chrt可以定义在unistd头文件中,并在libc中添加chrt函数体实现。
- 在
/usr/src/include/unistd.h
中添加chrt函数定义
1 | //... |
这里我犯了一个很愚蠢的错误,一开始我写的是 int char(long deadline)
,虽然kernel编译成功但是在编译test.c 的时候却始终找不到chrt 函数,经过修改后排除错误
- 在
/usr/src/minix/lib/libc/sys/chrt.c
中添加chrt函数实现。可用alarm函数实现超时强制终止。参照该文件夹下fork.c文件,在实现中通过_syscall
(调用号)向系统服务传递。例如:
1 | pid_t fork(void){ |
chrt.c
如下图所示:
1 |
|
timeval
使用来获得时间的
timezone
是用来获得时区的
gettimeofday
用来获取当前的时间的函数,包含在头文件 #include <sys/time.h>
中
_syscall
函数可以用来查找系统调用。PM_PROC_NR 将会用来存放PM_CHRT的系统调用号,
- 在
/usr/src/minix/lib/libc/sys
中Makefile.inc
文件添加chrt.c条目(添加C文件后,需在同目录下的Makefile/Makefile.inc中添加条目)
服务层:
需要向MINIX系统的进程管理服务中注册chrt,使得chrt服务可以向应用层提供。
- 在
/usr/src/minix/servers/pm/proto.h
中添加do_chrt
函数定义。
因为 do_chrt
的功能是调用 sys_chrt()
函数,并不需要传入参数,所以这里写void
- 在
/usr/src/minix/servers/pm/chrt.c
中添加chrt
函数实现,调用sys_chrt()
1 |
|
- 在
/usr/src/minix/include/minix/callnr.h
中定义PM_CHRT
编号。用来帮助应用层的系统调用能找到
我一开始没有定义 PM_CHRT 的编号,导致 make build 的时候总是报告称error: use of undeclared identifier 'PM_CHRT'
- 在
/usr/src/minix/servers/pm/Makefile
中添加chrt.c
条目。
- 在
/usr/src/minix/servers/pm/table.c
中调用映射表。
- 在
/usr/src/minix/include/minix/syslib.h
中添加sys_chrt()
定义。
- 在
/usr/src/minix/lib/libsys/sys_chrt.c
中添加sys_chrt ()
实现。可参照该文件夹下的sys_fork
文件,在实现中通过_kernel_call
(调用号)向内核传递。例如:
1 | int sys_fork(parent, child, child_endpoint, flags, msgaddr){ |
1 |
|
- 在
/usr/src/minix/lib/libsys
中的Makefile
中添加sys_chrt.c
条目。
内核层
在MINIX内核中实现进程调度功能,此处可以直接修改内核信息,例如进程的截至时间。
- 在
/usr/src/minix/kernel/system.h
中添加do_chrt
函数定义。
- 在
/usr/src/minix/kernel/config.h
中添加USE_CHRT
踩坑:这一步虽然在实验说明中没有写,但是我们要加上去。否则在编译 测试文件的时候会报错
- 在
/usr/src/minix/kernel/system/do_chrt.c
中添加do_chrt
函数实现。参考该文件下的do_fork
文件,修改调用者进程信息。例如:
1 | pid_t fork(void){ |
do_chrt.c
1 |
|
- 在
/usr/src/minix/kernel/system/
中Makefile.inc
文件添加do_chrt.c
条目。
- 在
/usr/src/minix/include/minix/com.h
中定义SYS_CHRT
编号。方便Kernel_Call找到SYS_CHRT函数。
- 在
/usr/src/minix/kernel/system.c
中添加SYS_CHRT
编号到do_chrt
的映射。
- 在
/usr/src/minix/commands/service/parse.c
的system_tab
中添加名称编号对。
踩坑:这个文件中有两个struct,一个都是 VM_****
,一个都是 SYS_****
,不要放错,我因为这个调试了很久
Minix3中的进程调度
MINIX3使用一种多级调度算法。进程优先级数字越小,优先级越高,根据优先级不同分成了16个可运行进程队列。每个队列内部采用时间片轮转调度,找到最高非空优先级队列,选取队列首部可运行的进程,当用完了时间片,则移到当前队列的队尾(详见教材P124)。
将EDF添加到多级调度算法中,可控制入队实现实时调度(也可有其他新颖方式,得分更高)。入队是将当前剩余时间(终止时间-运行时间)大于0的进程添加到某个优先级队列,即设置进程优先级(需要选择合适的优先级否则执行效果不理想)。
在该队列内部将时间片轮转调度改成剩余时间最少优先调度,即将剩余时间最小的进程移到队列首部。
进程调度模块位于
/usr/src/minix/kernel/
下的proc.h
和proc.c
,修改影响进程调度顺序的部分。proc.h
struct proc 维护每个进程的信息,用于调度决策。添加deadline成员。
利用xcode 可以很清楚得看见 proc 这个结构的内部成员:
1
2
3
4
5
6static struct proc * pick_proc(void)
{
//...
long deadline;
//...
}proc.c
switch_to_user() 选择进程进行切换。
enqueue_head() 按优先级将进程加入列队首。实验中需要将实时进程的优先级设置成合适的优先级。这里我设置成了5。也就是说,当我给进程设置了 deadline 之后,这个实时进程的优先级就会变成5。然后多级调度算法就会发现这个较高的优先级并执行入队操作:
1
2
3if(rp->deadline > 0) {
rp->p_priority = 5;
}
enqueue() 按优先级将进程加入列队尾。同上。
pick_proc() 从队列中返回一个可调度的进程。遍历设置的优先级队列,返回剩余时间最小并可运行的进程。
1 | static struct proc * pick_proc(void) |
实验结果
测试用例
测试用例来说明这个函数更直观:
1 |
|
输出
编译完成之后,我们把测试用的 test.c
文件放到 文件夹下,然后 clang test.c -o test
编译文件 ./test
运行可执行文件,结果如下图所示:
显然这个答案并不是正确的,但是起码在kernel中添加了chrt 函数。第二天我调试了四五个小时(主要时间花在编译上面),将内核层多次删改后再次尝试,这次成功了:
在测试中,在main
函数中fork
三个子进程(P1, P2, P3),并为每个子进程设置id。
P1
和P2
为实时进程,deadline
分别设为20s和15s。
三个子进程会打印出子进程id和循环次数。
第0s时:优先级P2 > P1 > P3
;
第5s 时:P1设置deadline为5s,P1调用chrt(5);
第5s后:优先级P1 > P2 > P3
;
第10s时:P3设置deadline为3s,P3调用chrt(3);
第10s后:优先级P3 > P2
;
n 打印输出信息,观察子进程执行顺序是否正确.
注意事项
- MINIX的不同服务模块和内核都是运行在不同进程中,只能使用基于消息的进程间系统调用/内核调用,不能使用直接调用普通C函数。
- 添加调用编号,需要修改取值范围限制。
- 以源码为准(博客等资料版本落后)。
- 善用source insight高级功能(调用关系,全局搜索)。
- 善用git diff 检查代码修改。修改涉及文件较多,git diff可直观看到修改内容,避免引入无意的错误。
- 善用FileZilla功能。连接虚拟机,拉取需修改的文件,修改后上传到虚拟机。
编译方法
cd /usr/src
make build
#首次编译,或者修改了头文件,Makefile时使用,时间较长。Make build MKUPDATE=yes
#增量式编译,适用于少量C源代码修改时使用。reboot
#重启,默认情况下自动选择latest kernel(新生成的kernel),需要原始版本时手工选择。
总结
这次实验要求我们修改的东西很多,我的解决步骤是:
应用层$\longrightarrow$服务层$\longrightarrow$内核层$\longrightarrow$修改进程调度算法
在每一part做完之后,都会编译一下kernal,如果没有报错,说明修改的代码没有问题
找到效率高的工作流很重要。我的工作软件如下:
Typora 负责步骤查看和报告撰写
Terminus 负责用ssh连接虚拟机与命令操作
Transmit3 负责用ftp连接虚拟机的文件系统,配合xcode 修改代码
Chrome、Acrobat 用于博客查看与资料查阅