本文分析Linux早期版本-Linux0.11 io阻塞下切换线程的问题。虽然现在Linux都4.X了,但是操作系统灵魂性的、基本性的东西不会轻易改变的。况且分析参天大树早期的小树芽比直接分析参天大树有趣、高效得多。这也是本人从MOOC网哈工大张治军老师的《操作系统》课上领悟到的。
“学习操作系统就是要coding them...”所以选择一个能力范围内的Linux内核版本并coding them, Linux0.11是一个很好的选择。考虑到这是本人在csdn的第一篇博文,所以说了很多。。。下面开始正题吧。
想到写这篇博文的主要动力是因为我在课程讨论区上看到一位同学发出的chat.
https://www.icourse163.org/learn/HIT-1002531008#/learn/forumdetail?pid=1005173138
他提到了几个问题,不外乎关于操作系统进程阻塞、进程切换问题。
对于他提出来一连串问题我总结了两点。
执行序列请求IO会引起中断?
一旦执行序列进入IO阻塞,那如何切换到其它执行序列?
备注:执行序列是一个通俗概念可指线程也可指进程。泛泛的说就是程序执行时在操作系统支持下给用户展示出实体的样子。
对于这两点:我使用代码加图示分析。
加入某应用打开power这个驱动并获取文件描述符power_fd
read(power_fd, &electric, 4); sleep(1); printf("electric:%d\n", electric);
对应到power驱动的read函数为:
/* *用户获取电压 */ static int power_read(struct file *file, char *user, size_t size, loff_t *oppos) { int ret=0; ret = wait_event_interruptible(power_waitq, (0 != g_ElectricOccurred)); if (0 != ret) { printk("Exit WaitComplete function by signal :%d\n", ret); return -EBADRQC; } copy_to_user(user, (const void *)&g_electric, 4); return 0; } static void PowerTimerFunc(unsigned long Data) { static int fre=0; g_electric = get_power_vol(); /*灯指示*/ if(g_electric >150 && g_electric<172)/*低电量*/ { if(fre %3) gpio_set_value(power_io.power_indled, CLOSE_LED); else gpio_set_value(power_io.power_indled, OPEN_LED); fre ++; } mod_timer(&g_Power_Timer, jiffies+HZ/5); g_ElectricOccurred=1; wake_up_interruptible(&power_waitq); }
分析上面应用、驱动代码。
a.应用调read()系统调用(会引发中断进入内核)切换到内核空间执行power_read()然后检查g_ElectricOccurred这个全局标记为0代表没发生,为0就进入wait_event_interruptible()挂入power_waitq等待队列头里睡眠,这函数类似于linux0.11 interruptible_sleep_on函数是可以被中断的。
b.那谁来唤醒它呢?PowerTimerFunc()里的wake_up_interruptible().
那wake_up_interruptible它到底干了什么事,使执行序列进入睡眠呢?我们不去分析Linux3.4 wake_up_interruptible源码,因为这比较复杂。我们分析Linux0.11版本的interruptible_sleep_on。反正wake_up_interruptible 不管怎么调用它还是会调类似于interruptible_sleep_on这种功能的函数的。
void interruptible_sleep_on(struct task_struct **p) { struct task_struct *tmp; if (!p) return; if (current == &(init_task.task)) panic("task[0] trying to sleep"); tmp=*p; *p=current; repeat: current->state = TASK_INTERRUPTIBLE; fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies); //进入阻塞态 schedule(); if (*p && *p != current) { /* 当前执行序列将当前队列头指针所指向的执行序列唤醒 */ (**p).state=0; goto repeat; } *p=tmp; /* 队列头需要改变 */ if (tmp){ fprintk(3, "%ld\t%c\t%ld\n", tmp->pid, 'J', jiffies); //就绪态 tmp->state=0; } }
请考虑下repeat标签及其周边代码。
if (!p) return; if (current == &(init_task.task)) panic("task[0] trying to sleep"); tmp=*p; *p=current; fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies); //进入阻塞态 schedule();
.t0时刻将tmp指向当前队列头
.t1时刻将当前队列头指针指向自己(PCB)
.t2调schedule()调度,schedule会重新评估执行序列的优先级,再决定switch_to()那个执行序列。(switch_to Linux0.11切换函数)
repeat标签的作用是支持一个进程在睡眠时被中断唤醒。当然,它会从等待队列头开始唤醒--->直到将自己唤醒。简单地说它会唤醒,挂在同一个队列中并在它之后睡眠的进程。
想看懂interruptible_sleep_on()需要结合“拉链”思维即弹栈、压栈这个过程。
所以,操作系统就是通过压栈、更新队列头指针、调schedule()实现执行序列的切换。通过弹栈、寻找上一个节点(当前执行序列为队列头)、设置state实现执行序列唤醒,然后重新参与调度。
·如果对上面描述不是很清楚的话:
下面图一用来解密世界上最隐蔽的睡眠队列。sleep_on和interruptible_sleep_on类似,只不过interruptible_sleep_on支持中断唤醒。
void sleep_on(struct task_struct **p) { struct task_struct *tmp; if (!p) return; if (current == &(init_task.task)) panic("task[0] trying to sleep"); tmp = *p; *p = current; current->state = TASK_UNINTERRUPTIBLE; fprintk(3, "%ld\t%c\t%ld\n", current->pid, 'W', jiffies); //进入阻塞态 schedule(); /* 只有wake_up函数操作时才进来这里 */ *p=tmp; /* 队列头需要改变 */ if (tmp) /* 唤醒下一个进程 , 这操作会唤醒所有进程*/ tmp->state=0; }
请考虑tmp局部变量、p指向队列头指针的指针、current当前进程PCB。这三者之间的关系。
图一