静态网页模板素材,重庆百度seo排名优化软件,企业形象设计考试,android手机版下载概要 在Linux中#xff0c;仅等待CPU时间的进程称为就绪进程#xff0c;它们被放置在一个运行队列中#xff0c;一个就绪进程的状 态标志位为 TASK_RUNNING。一旦一个运行中的进程时间片用完#xff0c; Linux 内核的调度器会剥夺这个进程对CPU的控制权#xff0c;并且从运… 概要 在Linux中仅等待CPU时间的进程称为就绪进程它们被放置在一个运行队列中一个就绪进程的状 态标志位为 TASK_RUNNING。一旦一个运行中的进程时间片用完 Linux 内核的调度器会剥夺这个进程对CPU的控制权并且从运行队列中选择一个合适的进程投入运行。
当然一个进程也可以主动释放CPU的控制权。函数 schedule() 是一个调度函数它可以被一个进程主动调用从而调度其它进程占用 CPU。一旦这个主动放弃 CPU 的进程被重新调度占用 CPU那么它将从上次停止执行的位置开始执行也就是说它将从调用 schedule() 的下一行代码处开始执行。
有时候进程需要等待直到某个特定的事件发生例如设备初始化完成、I/O 操作完成或定时器到时等。在这种情况下进程则必须从运行队列移出加入到一个等待队列中这个时候进程就进入了睡眠状态。
Linux 中的进程睡眠状态有两种 一种是可中断的睡眠状态其状态标志位TASK_INTERRUPTIBLE。 另一种是不可中断 的睡眠状态其状态标志位为TASK_UNINTERRUPTIBLE。
可中断的睡眠状态的进程会睡眠直到某个条件变为真比如说产生一个硬件中断、释放 进程正在等待的系统资源或是传递一个信号都可以是唤醒进程的条件。不可中断睡眠状态与可中断睡眠状态类似但是它有一个例外那就是把信号传递到这种睡眠 状态的进程不能改变它的状态也就是说它不响应信号的唤醒。不可中断睡眠状态一般较少用到但在一些特定情况下这种状态还是很有用的比如说进程必须等 待不能被中断直到某个特定的事件发生。
在现代的 Linux 操作系统中进程一般都是用调用 schedule() 的方法进入睡眠状态的下面的代码演示了如何让正在运行的进程进入睡眠状态。
sleeping_task current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
func1();
/* Rest of the code ... */在第一个语句中程序存储了一份进程结构指针 sleeping_taskcurrent 是一个宏它指向正在执行的进程结构。
set_current_state() 将该进程的状态从执行状态 TASK_RUNNING 变成睡眠状态 TASK_INTERRUPTIBLE。如果 schedule() 是被一个状态为 TASK_RUNNING 的进程调度那么 schedule() 将调度另外一个进程占用CPU。
如果 schedule() 是被一个状态为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的进程调度那么还有一个附加的步骤将被执行当前执行的进程在另外一个进程被调度之前会被从运行队列中移出这将导致正在运行的那个进程进入睡眠因为它已经不在运行队列中了。
我们可以使用下面的这个函数将刚才那个进入睡眠的进程唤醒。
wake_up_process(sleeping_task);在调用了 wake_up_process() 以后这个睡眠进程的状态会被设置为 TASK_RUNNING而且调度器会把它加入到运行队列中去。当然这个进程只有在下次被调度器调度到的时候才能真正地投入运行。
无效唤醒
几乎在所有的情况下进程都会在检查了某些条件之后发现条件不满足才进入睡眠。可是有的时候进程却会在判定条件为真后开始睡眠如果这样的话进程就会无限期地休眠下去这就是所谓的无效唤醒问题。
在操作系统中当多个进程都企图对共享数据进行某种处理而 最后的结果又取决于进程运行的顺序时就会发生竞争条件这是操作系统中一个典型的问题无效唤醒恰恰就是由于竞争条件导致的。
设想有两个进程A 和BA 进程正在处理一个链表它需要检查这个链表是否为空如果不空就对链表里面的数据进行一些操作同时B进程也在往这个链表添加节点。当这个链表是空的时候由于无数据可操作这时A进程就进入睡眠当B进程向链表里面添加了节点之后它就唤醒A 进程其代码如下
A进程:
1 spin_lock(list_lock);
2 if (list_empty(list_head)) {
3 spin_unlock(list_lock);
4 set_current_state(TASK_INTERRUPTIBLE);
5 schedule();
6 spin_lock(list_lock);
7 }
8
9 /* Rest of the code ... */
10 spin_unlock(list_lock);B进程:
100 spin_lock(list_lock);
101 list_add_tail(list_head, new_node);
102 spin_unlock(list_lock);
103 wake_up_process(processa_task);这里会出现一个问题假如当A进程执行到第3行后第4行前的时候B进程被另外一个处理器调度投入运行。在这个时间片内B进程执行完了它所有的指令因此它试图唤醒A进程而此时的A进程还没有进入睡眠所以唤醒操作无效。
在这之后A 进程继续执行它会错误地认为这个时候链表仍然是空的于是将自己的状态设置为 TASK_INTERRUPTIBLE 然后调用 schedule() 进入睡 眠。由于错过了B进程唤醒它将会无限期的睡眠下去这就是无效唤醒问题因为即使链表中有数据需要处理A 进程也还是睡眠了。
避免无效唤醒
如何避免无效唤醒问题呢
我们发现无效唤醒主要发生在检查条件之后和进程状态被设置为睡眠状态之前本来B进程的 wake_up_process() 提供了一次将A进程状态置为 TASK_RUNNING 的机会可惜这个时候A进程的状态仍然是 TASK_RUNNING所以 wake_up_process() 将A进程状态从睡眠状态转变为运行状态的努力 没有起到预期的作用。
要解决这个问题必须使用一种保障机制使得判断链表为空和设置进程状态为睡眠状态成为一个不可分割的步骤才行也就是必须消除竞争条 件产生的根源这样在这之后出现的 wake_up_process() 就可以起到唤醒状态是睡眠状态的进程的作用了。
找到了原因后重新设计一下A进程的代码结构就可以避免上面例子中的无效唤醒问题了。
A进程:
1 set_current_state(TASK_INTERRUPTIBLE);
2 spin_lock(list_lock);
3 if (list_empty(list_head)) {
4 spin_unlock(list_lock);
5 schedule();
6 spin_lock(list_lock);
7 }
8 set_current_state(TASK_RUNNING);
9
10 /* Rest of the code ... */
11 spin_unlock(list_lock);可以看到这段代码在测试条件之前就将当前执行进程状态转设置成 TASK_INTERRUPTIBLE 了并且在链表不为空的情况下又将自己置为 TASK_RUNNING 状态。
这样一来如果B进程在A进程进程检查了链表为空以后调用 wake_up_process()那么A进程的状态就会自动由原来 TASK_INTERRUPTIBLE 变成 TASK_RUNNING此后即使进程又调用了 schedule()由于它现在的状态是 TASK_RUNNING所以仍然不会被从运行队列中移出因而不会错误的进入睡眠当然也就避免了无效唤醒问题。
Linux内核的例子
在Linux操作系统中内核的稳定性至关重要为了避免在Linux操作系统内核中出现无效唤醒问题Linux内核在需要进程睡眠的时候应该使用类似如下的操作
/* q 是我们希望睡眠的等待队列 */
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(q, wait);
set_current_state(TASK_INTERRUPTIBLE);
/* condition 是等待的条件 */
while (!condition) {schedule();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(q, wait);上面的操作使得进程通过下面的一系列步骤安全地将自己加入到一个等待队列中进行睡眠首先调用 DECLARE_WAITQUEUE() 创建一个等待队列的项然后调用 add_wait_queue() 把自己加入到等待队列中并且将进程的状态设置为 TASK_INTERRUPTIBLE 或者 TASK_INTERRUPTIBLE。
然后循环检查条件是否为真如果是的话就没有必要睡眠如果条件不为真就调用 schedule()。当进程检查的条件满足后进程又将自己设置为 TASK_RUNNING 并调用 remove_wait_queue() 将自己移出等待队列。
从上面可以看到Linux的内核代码维护者也是在进程检查条件之前就设置进程的状态为睡眠状态然后才循环检查条件。如果在进程开始睡眠之前条件就已经达成了那么循环会退出并用 set_current_state() 将自己的状态设置为就绪这样同样保证了进程不会存在错误的进入睡眠的倾向当然也就不会导致出现无效唤醒问题。
下面让我们用 Linux 内核中的实例来看看其是如何避免无效睡眠的这段代码出自 Linux2.6 的内核 (/kernel/sched.c):
/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {schedule();set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;上面的这些代码属于迁移服务线程 migration_thread这个线程不断地检查 kthread_should_stop()直到 kthread_should_stop() 返回 1 它才可以退出循环也就是说只要 kthread_should_stop() 返回 0 该进程就会一直睡眠。
从代码中我们可以看出检查 kthread_should_stop() 确实是在进程的状态被置为 TASK_INTERRUPTIBLE 后才开始执行的。因此如果在条件检查之后但是在 schedule() 之前有其他进程试图唤醒它那么该进程的唤醒操作不会失效。
小结
通过上面的讨论可以发现在 Linux 中避免进程的无效唤醒的关键是在进程检查条件之前就将进程的状态置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE并且如果检查的条件满足的话就应该将其状态重新设置为 TASK_RUNNING。
这样无论进程等待的条件是否满足进程都不会因为被移出就绪队列而错误地进入睡眠状态从而避免了无效唤醒问题。
-End-