0 00:00:00,000 --> 00:00:06,600 1 00:00:06,640 --> 00:00:07,800 好 那我们来看一下 2 00:00:07,840 --> 00:00:10,600 这个底层支撑是怎么来完成的 3 00:00:10,640 --> 00:00:12,040 第一个是定时器 4 00:00:12,080 --> 00:00:14,400 定时器主要是用来干什么呢 5 00:00:14,440 --> 00:00:16,920 让进程睡眠 进入等待状态 6 00:00:16,960 --> 00:00:19,160 为此我们有一个do_sleep函数 7 00:00:19,200 --> 00:00:20,920 这个函数就可以让进程睡眠 8 00:00:20,960 --> 00:00:22,000 并进入等待状态 9 00:00:22,040 --> 00:00:23,280 它做什么事情呢 10 00:00:23,320 --> 00:00:24,600 添加一个timer 11 00:00:24,640 --> 00:00:25,680 我们称之为定时器 12 00:00:25,720 --> 00:00:27,640 这个细节后面会讲到 13 00:00:27,680 --> 00:00:28,840 一旦添加timer之后呢 14 00:00:28,880 --> 00:00:31,240 我们会让这个进程进入等待状态 15 00:00:31,280 --> 00:00:33,000 完成schedule 这样它就切换出去 16 00:00:33,040 --> 00:00:36,760 然后让其它新的进程去执行 17 00:00:36,800 --> 00:00:39,240 紧接着呢 我们的时钟 18 00:00:39,280 --> 00:00:40,880 会产生时钟中断 19 00:00:40,920 --> 00:00:42,280 产生时钟中断之后呢 20 00:00:42,320 --> 00:00:44,080 我们会遍历这个timer 的list 21 00:00:44,120 --> 00:00:44,960 这是一个timer_list 22 00:00:45,000 --> 00:00:46,040 挂了很多的timer 23 00:00:46,080 --> 00:00:47,040 我们刚才添加的timer 24 00:00:47,080 --> 00:00:49,320 会添加到这个timer_list里面去 25 00:00:49,360 --> 00:00:52,640 然后看看是否有哪一个定时器 26 00:00:52,680 --> 00:00:55,200 这里面会有个时间 一个限制 27 00:00:55,240 --> 00:00:56,640 这个时间限制是否到期 28 00:00:56,680 --> 00:01:00,480 如果到期之后呢 它就会唤醒进程A 29 00:01:00,520 --> 00:01:02,560 使得我们进程A 30 00:01:02,600 --> 00:01:06,440 从刚才的等待状态变到就绪状态 31 00:01:06,480 --> 00:01:09,000 这就是定时器的大致的工作过程 32 00:01:09,040 --> 00:01:11,800 我们看细节 33 00:01:11,840 --> 00:01:14,120 定时器的数据结构的定义 34 00:01:14,160 --> 00:01:15,400 它里面包含什么呢 35 00:01:15,440 --> 00:01:16,680 什么时候到期 36 00:01:16,720 --> 00:01:18,800 什么时候这个时钟就结束了 37 00:01:18,840 --> 00:01:20,440 然后哪一个进程 38 00:01:20,480 --> 00:01:24,040 当前是跟这个timer绑定在一起的 39 00:01:24,080 --> 00:01:26,920 然后这个timer连到list里面 40 00:01:26,960 --> 00:01:28,840 它这个link 双向链表 41 00:01:28,880 --> 00:01:31,080 这是一个list_entry 42 00:01:31,120 --> 00:01:32,360 那么这个结构就形成了 43 00:01:32,400 --> 00:01:36,600 我们说定时器一个数据结构 44 00:01:36,640 --> 00:01:38,400 那么 timer_list呢 45 00:01:38,440 --> 00:01:39,840 是把所有的timer挂在一起 46 00:01:39,880 --> 00:01:42,400 便于我们这个时钟中断 47 00:01:42,440 --> 00:01:45,480 这个处理例程呢能够对此进行判断 48 00:01:45,520 --> 00:01:50,000 看哪个timer到期了 49 00:01:50,040 --> 00:01:52,720 然后我们再看它这个执行过程 50 00:01:52,760 --> 00:01:55,600 站在进程或者线程角度来看 51 00:01:55,640 --> 00:01:56,640 进程或者线程 52 00:01:56,680 --> 00:01:57,760 它只要调用以下几个函数 53 00:01:57,800 --> 00:02:00,520 比如说sys_sleep等等 54 00:02:00,560 --> 00:02:01,840 最终会调到do_sleep 55 00:02:01,880 --> 00:02:03,040 我们刚才说的 56 00:02:03,080 --> 00:02:04,640 就是内核提供的一个函数 57 00:02:04,680 --> 00:02:07,200 那么do_sleep呢会创建一个timer 58 00:02:07,240 --> 00:02:08,160 就 add_timer 59 00:02:08,200 --> 00:02:10,000 然后给timer做初始化 60 00:02:10,040 --> 00:02:14,520 并让当前这个进程进入等待状态 61 00:02:14,560 --> 00:02:15,920 完成一次schedule 62 00:02:15,960 --> 00:02:17,720 切换到新的一个进程去执行 63 00:02:17,760 --> 00:02:21,080 这就是说 当一个进程 64 00:02:21,120 --> 00:02:22,400 如果说它想睡眠 65 00:02:22,440 --> 00:02:24,280 它要做的一个大致的过程 66 00:02:24,320 --> 00:02:27,360 是这么一回事 67 00:02:27,400 --> 00:02:30,200 那如果说时钟中断产生之后呢 68 00:02:30,240 --> 00:02:31,520 我们可以看到 69 00:02:31,560 --> 00:02:32,720 只要时钟中断一产生 70 00:02:32,760 --> 00:02:34,800 我们的中断服务例程 71 00:02:34,840 --> 00:02:36,200 就会接受这个处理 72 00:02:36,240 --> 00:02:37,560 比如说Trap_dispatch 73 00:02:37,600 --> 00:02:38,880 在我们这个trap.C函数里面 74 00:02:38,920 --> 00:02:41,600 就会进一步去查询这个timer 75 00:02:41,640 --> 00:02:43,680 所对应那个timer_list 76 00:02:43,720 --> 00:02:45,240 总的timer_list里面 77 00:02:45,280 --> 00:02:47,360 是否有哪个timer到期了 78 00:02:47,400 --> 00:02:49,320 如果有哪个timer到期了 79 00:02:49,360 --> 00:02:51,120 它们会把跟这个timer相关的 80 00:02:51,160 --> 00:02:53,720 那个进程给唤醒 wake up_proc 81 00:02:53,760 --> 00:02:56,080 这是我们在进程管理之后 82 00:02:56,120 --> 00:02:58,000 用到的一个很常见的函数 83 00:02:58,040 --> 00:02:59,240 它会把它这个状态 84 00:02:59,280 --> 00:03:02,560 从等待状态变到就绪状态 85 00:03:02,600 --> 00:03:05,240 然后把这个timer给delete掉 86 00:03:05,280 --> 00:03:08,520 从这个list里取出来 没有用了 87 00:03:08,560 --> 00:03:10,920 同时再调了一个 88 00:03:10,960 --> 00:03:12,760 sched_class_proc_tick 89 00:03:12,800 --> 00:03:14,520 大家想想 这个里面还有印象吗 90 00:03:14,560 --> 00:03:16,920 在lab6里面这个函数很重要 91 00:03:16,960 --> 00:03:18,640 它完成了什么呢 92 00:03:18,680 --> 00:03:22,960 它完成了跟时间相关的参数的调整 93 00:03:23,000 --> 00:03:24,720 比如说我们现在说 有一个时间片 94 00:03:24,760 --> 00:03:26,640 那么每一个timer 95 00:03:26,680 --> 00:03:28,920 有可能导致时间片的递减 96 00:03:28,960 --> 00:03:31,840 从而可以使得其它进程有机会 97 00:03:31,880 --> 00:03:33,920 在某个进程用完时间片之后呢 98 00:03:33,960 --> 00:03:35,800 有机会被调用去执行 99 00:03:35,840 --> 00:03:38,640 那这就是说在定时器里面 100 00:03:38,680 --> 00:03:43,760 这个timer_list里很重要一个处理过程 101 00:03:43,800 --> 00:03:46,280 那么第二个这个底层支撑机制呢 102 00:03:46,320 --> 00:03:47,600 是屏蔽中断 103 00:03:47,640 --> 00:03:50,120 我们为什么屏蔽中断 大家想一想 104 00:03:50,160 --> 00:03:51,360 屏蔽中断最主要的目标 105 00:03:51,400 --> 00:03:52,840 是要保证互斥性 106 00:03:52,880 --> 00:03:54,600 在这个屏蔽中断之后呢 107 00:03:54,640 --> 00:03:57,000 我们就不可能对它进行调度了 108 00:03:57,040 --> 00:03:59,000 这对调度器产生的影响 109 00:03:59,040 --> 00:04:00,640 这是我们说这个 110 00:04:00,680 --> 00:04:02,320 屏蔽中断最重要一个目标 111 00:04:02,360 --> 00:04:03,840 屏蔽中断完成了一个 112 00:04:03,880 --> 00:04:05,000 互斥的一个保护 113 00:04:05,040 --> 00:04:08,040 使得当前在屏蔽中断状态下的 114 00:04:08,080 --> 00:04:09,040 这个内核线程呢 115 00:04:09,080 --> 00:04:12,160 它不会被调度或者被打断 116 00:04:12,200 --> 00:04:13,600 那怎么来屏蔽中断 117 00:04:13,640 --> 00:04:15,000 那么这个和我们硬件相关 118 00:04:15,040 --> 00:04:16,800 我们有个Eflags寄存器 119 00:04:16,840 --> 00:04:18,000 这个寄存器里面有一个bit 120 00:04:18,040 --> 00:04:18,800 很重要的一个bit 121 00:04:18,840 --> 00:04:21,280 叫做 Interrupt Enable Flag 122 00:04:21,320 --> 00:04:23,640 那么这个flag那么如果自乘1 123 00:04:23,680 --> 00:04:25,320 表明当前是允许中断的 124 00:04:25,360 --> 00:04:27,440 如果自乘0表明是什么呢 125 00:04:27,480 --> 00:04:29,840 表明是当前不允许中断 126 00:04:29,880 --> 00:04:31,240 所以我们专门有两条指令 127 00:04:31,280 --> 00:04:33,680 COI和STI 这两条指令呢 128 00:04:33,720 --> 00:04:37,360 能够完成屏蔽中断和使能中断 129 00:04:37,400 --> 00:04:41,960 也就说对这个flag产生的影响 130 00:04:42,000 --> 00:04:45,040 我们的ucore利用了这种机制 131 00:04:45,080 --> 00:04:47,000 通过这两条指令来实现 132 00:04:47,040 --> 00:04:50,280 一个临界区一个代码的一个保护 133 00:04:50,320 --> 00:04:53,160 使得在这里面执行的代码呢 134 00:04:53,200 --> 00:04:56,840 它不会被我们操作系统所打断 135 00:04:56,880 --> 00:04:59,400 在这里面Local_intr_save 136 00:04:59,440 --> 00:05:00,440 对应的就是什么呢 137 00:05:00,480 --> 00:05:02,920 对应这里面的COI指令 138 00:05:02,960 --> 00:05:04,520 而Local_intr_restore呢 139 00:05:04,560 --> 00:05:06,280 对应的是STI指令 140 00:05:06,320 --> 00:05:11,720 它们除了完成对这个中断使能flag 141 00:05:11,760 --> 00:05:13,720 的一个enable和disable之外呢 142 00:05:13,760 --> 00:05:16,680 还对当前这个标记寄存器 143 00:05:16,720 --> 00:05:18,120 做了一定的保存和恢复 144 00:05:18,160 --> 00:05:21,960 这是它们完成的大致的一个功能 145 00:05:22,000 --> 00:05:24,080 那么第三个这个底层支撑呢 146 00:05:24,120 --> 00:05:25,320 是等待队列 147 00:05:25,360 --> 00:05:26,600 为什么要有等待队列 148 00:05:26,640 --> 00:05:30,320 它和我们刚才说的时钟睡眠的队列 149 00:05:30,360 --> 00:05:32,080 就timer_list是类似的 150 00:05:32,120 --> 00:05:33,720 它主要是由于某一些资源 151 00:05:33,760 --> 00:05:34,800 得不到满足之后呢 152 00:05:34,840 --> 00:05:35,840 它也需要等待 153 00:05:35,880 --> 00:05:37,320 所以呢通过这个等待队列 154 00:05:37,360 --> 00:05:41,520 使得进程可以进入等待状态 155 00:05:41,560 --> 00:05:43,520 当某个事件 或者某个资源 156 00:05:43,560 --> 00:05:44,720 得到满足之后呢 157 00:05:44,760 --> 00:05:46,680 它可以从等待当中恢复出来 158 00:05:46,720 --> 00:05:48,680 为此 我们设计了一个 159 00:05:48,720 --> 00:05:50,240 等待项和等待队列 160 00:05:50,280 --> 00:05:51,720 这两个关键的数据结构 161 00:05:51,760 --> 00:05:52,960 我们可以看一看 162 00:05:53,000 --> 00:05:54,680 这里面等待项包含什么呢 163 00:05:54,720 --> 00:05:57,280 第一个proc 就是当前哪一个进程 164 00:05:57,320 --> 00:06:00,120 和这个等待项是对应的 165 00:06:00,160 --> 00:06:02,200 第二个呢 它的这个wakeup_flag 166 00:06:02,240 --> 00:06:04,120 就是它被唤醒 或者它等待 167 00:06:04,160 --> 00:06:05,680 到底等待什么原因 168 00:06:05,720 --> 00:06:06,800 有可能有几种 169 00:06:06,840 --> 00:06:07,920 我们前面说到的 比如说 170 00:06:07,960 --> 00:06:10,880 由于信号量没有得到满足 它会等待 171 00:06:10,920 --> 00:06:13,240 由于子进程还在运行 172 00:06:13,280 --> 00:06:14,880 它需要等待子进程结束 173 00:06:14,920 --> 00:06:16,160 那么它也需要等待 174 00:06:16,200 --> 00:06:18,920 那么这就是等待的一些flag 175 00:06:18,960 --> 00:06:20,680 第三个呢有一个等待队列 176 00:06:20,720 --> 00:06:23,360 也就说你这个等待的项 177 00:06:23,400 --> 00:06:26,000 会挂到这个等待队列里面去 178 00:06:26,040 --> 00:06:28,840 第四是wait_link 那么就是说 179 00:06:28,880 --> 00:06:30,600 它在这里面就是完成了 180 00:06:30,640 --> 00:06:32,960 对这个等待队列里面链表的 181 00:06:33,000 --> 00:06:34,080 一个建立过程 182 00:06:34,120 --> 00:06:35,200 那么这几项合在一起 183 00:06:35,240 --> 00:06:37,920 形成了我们说的wait这个等待项 184 00:06:37,960 --> 00:06:40,320 等待列表呢 其实就是一个 185 00:06:40,360 --> 00:06:42,560 等待项形成的双向链表 186 00:06:42,600 --> 00:06:47,640 用的是我们通常用到list_entry 187 00:06:47,680 --> 00:06:49,840 我们看它的操作 188 00:06:49,880 --> 00:06:51,480 如果说 现在的进程 189 00:06:51,520 --> 00:06:53,120 想进入一个等待状态 190 00:06:53,160 --> 00:06:54,400 它会做什么事情呢 191 00:06:54,440 --> 00:06:55,520 这里面举的例子 192 00:06:55,560 --> 00:06:59,760 是以信号量的P操作为例 193 00:06:59,800 --> 00:07:01,440 就是对应函数叫down函数 194 00:07:01,480 --> 00:07:04,320 来做这个等待一个过程 195 00:07:04,360 --> 00:07:06,680 可以看着 这里面很多其它一些 196 00:07:06,720 --> 00:07:09,760 高层函数呢会调这个down函数 197 00:07:09,800 --> 00:07:10,880 这个down函数最终会 198 00:07:10,920 --> 00:07:12,160 调一个很关键的函数 199 00:07:12,200 --> 00:07:13,720 让进程进入等待队列 200 00:07:13,760 --> 00:07:16,080 一个函数叫wait_corrent_set 201 00:07:16,120 --> 00:07:17,760 那么这个函数呢 202 00:07:17,800 --> 00:07:18,920 会完成什么事情 203 00:07:18,960 --> 00:07:21,480 它会产生一个等待项 204 00:07:21,520 --> 00:07:23,480 然后对这个等待项进行初始化 205 00:07:23,520 --> 00:07:24,880 叫wait_init 206 00:07:24,920 --> 00:07:26,960 同时把这个等待项呢 207 00:07:27,000 --> 00:07:28,480 挂到这个等待队列里面去 208 00:07:28,520 --> 00:07:30,600 叫wait_queue_add 209 00:07:30,640 --> 00:07:32,120 最后还会把这个进程 210 00:07:32,160 --> 00:07:35,880 从运行状态变到等待状态 211 00:07:35,920 --> 00:07:38,200 这里面跟以前讲的不太一样 212 00:07:38,240 --> 00:07:40,920 以前经常是运行态变到就绪态 213 00:07:40,960 --> 00:07:41,920 这里面说的是从 214 00:07:41,960 --> 00:07:43,560 运行状态变到等待状态 215 00:07:43,600 --> 00:07:45,000 同时完成schedule 216 00:07:45,040 --> 00:07:48,040 这是说让进程进入等待队列里面 217 00:07:48,080 --> 00:07:51,040 跟等待项和等待队列相关的 218 00:07:51,080 --> 00:07:53,880 一系列的操作 219 00:07:53,920 --> 00:07:55,320 第二个也会唤醒 220 00:07:55,360 --> 00:07:58,080 我们说 当一个进程 221 00:07:58,120 --> 00:07:59,560 它需要的资源得到满足之后 222 00:07:59,600 --> 00:08:00,840 它会被唤醒 223 00:08:00,880 --> 00:08:02,200 那么唤醒最主要的一个函数 224 00:08:02,240 --> 00:08:03,600 是wakeup_wait 225 00:08:03,640 --> 00:08:04,960 跟这个等待队列相关 226 00:08:05,000 --> 00:08:06,320 它也是一样 可以看着 227 00:08:06,360 --> 00:08:08,480 由于一系列的一些高层函数呢 228 00:08:08,520 --> 00:08:12,360 会调 比如说信号量这个V操作 229 00:08:12,400 --> 00:08:13,880 这里面对应是up函数 230 00:08:13,920 --> 00:08:14,600 这个up函数呢 231 00:08:14,640 --> 00:08:16,240 会进一步调wakeup_wait 232 00:08:16,280 --> 00:08:18,120 就是唤醒当前等待在 233 00:08:18,160 --> 00:08:20,320 这个信号量上这个进程 234 00:08:20,360 --> 00:08:22,120 那么wakeup_wait呢 235 00:08:22,160 --> 00:08:23,920 它首先会把那个等待项 236 00:08:23,960 --> 00:08:25,600 从等待队列里面取出来 237 00:08:25,640 --> 00:08:27,640 并进一步调wakeup_proc 238 00:08:27,680 --> 00:08:30,960 完成对这个进程的唤醒工作 239 00:08:31,000 --> 00:08:32,200 所谓wakeup_proc 240 00:08:32,240 --> 00:08:33,760 很明显就是跟我们前面讲的 241 00:08:33,800 --> 00:08:34,960 进程管理相关 242 00:08:35,000 --> 00:08:37,120 它会把这个进程的状态 243 00:08:37,160 --> 00:08:39,640 从等待状态变到就绪态 244 00:08:39,680 --> 00:08:42,000 然后呢就进一步去执行了 245 00:08:42,040 --> 00:08:43,920 那么既然进到就绪态呢 246 00:08:43,960 --> 00:08:46,480 那就意味着它可以参与到这个 247 00:08:46,520 --> 00:08:48,600 调度器的调度管理中来 248 00:08:48,640 --> 00:08:48,680