Linux唤醒抢占----Linux进程的管理与调度(二十三)
1. 唤醒抢占
当在try_to_wake_up/wake_up_process和wake_up_new_task中唤醒进程时, 内核使用全局check_preempt_curr看看是否进程可以抢占当前进程可以抢占当前运行的进程. 请注意该过程不涉及核心调度器.
每个调度器类都因应该实现一个check_preempt_curr函数, 在全局check_preempt_curr中会调用进程其所属调度器类check_preempt_curr进行抢占检查, 对于完全公平调度器CFS处理的进程, 则对应由check_preempt_wakeup函数执行该策略.
新唤醒的进程不必一定由完全公平调度器处理, 如果新进程是一个实时进程, 则会立即请求调度, 因为实时进程优先极高, 实时进程总会抢占CFS进程.
2 Linux进程的睡眠
在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 ... */
3 linux进程的唤醒
当在try_to_wake_up
/wake_up_process
和wake_up_new_task
中唤醒进程时, 内核使用全局check_preempt_curr看看是否进程可以抢占当前进程可以抢占当前运行的进程. 请注意该过程不涉及核心调度器.
3.1 wake_up_process
我们可以使用wake_up_process
将刚才那个进入睡眠的进程唤醒, 该函数定义在kernel/sched/core.c, line 2043.
int wake_up_process(struct task_struct *p)
{
return try_to_wake_up(p, TASK_NORMAL, 0);
}
在调用了wake_up_process以后, 这个睡眠进程的状态会被设置为TASK_RUNNING,而且调度器会把它加入到运行队列中去. 当然, 这个进程只有在下次被调度器调度到的时候才能真正地投入运行.
3.2 try_to_wake_up
try_to_wake_up函数通过把进程状态设置为TASK_RUNNING, 并把该进程插入本地CPU运行队列rq来达到唤醒睡眠和停止的进程的目的.
例如: 调用该函数唤醒等待队列中的进程, 或恢复执行等待信号的进程.
static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
该函数接受的参数有: 被唤醒进程的描述符指针(p), 可以被唤醒的进程状态掩码(state), 一个标志wake_flags,用来禁止被唤醒的进程抢占本地CPU上正在运行的进程.
try_to_wake_up函数定义在kernel/sched/core.c, line 1906
3.3 wake_up_new_task
void wake_up_new_task(struct task_struct *p)
该函数定义在kernel/sched/core.c, line 2421
之前进入睡眠状态的可以通过try_to_wake_up和wake_up_process完成唤醒, 而我们fork新创建的进程在完成自己的创建工作后, 可以通过wake_up_new_task完成唤醒工作, 参见Linux下进程的创建过程分析(_do_fork/do_fork详解)–Linux进程的管理与调度(八)
使用fork创建进程的时候, 内核会调用_do_fork(早期内核对应do_fork)函数完成内核的创建, 其中在进程的信息创建完毕后, 就可以使用wake_up_new_task将进程唤醒并添加到就绪队列中等待调度. 代码参见kernel/fork.c, line 1755
3.4 check_preempt_curr
wake_up_new_task中唤醒进程时, 内核使用全局check_preempt_curr看看是否进程可以抢占当前进程可以抢占当前运行的进程.
check_preempt_curr(rq, p, WF_FORK);
函数定义在[kernel/sched/core.c, line 905](函数定义在kernel/sched/core.c, line 905)
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
const struct sched_class *class;
if (p->sched_class == rq->curr->sched_class)
{
rq->curr->sched_class->check_preempt_curr(rq, p, flags);
}
else
{
for_each_class(class) {
if (class == rq->curr->sched_class)
break;
if (class == p->sched_class) {
resched_curr(rq);
break;
}
}
}
/*
* A queue event has occurred, and we're going to schedule. In
* this case, we can save a useless back to back clock update.
*/
if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))
rq_clock_skip_update(rq, true);
}
4 无效唤醒
4.1 无效唤醒的概念
几乎在所有的情况下, 进程都会在检查了某些条件之后, 发现条件不满足才进入睡眠. 可是有的时候进程却会在判定条件为真后开始睡眠, 如果这样的话进程就会无限期地休眠下去, 这就是所谓的无效唤醒问题.
在操作系统中, 当多个进程都企图对共享数据进行某种处理, 而最后的结果又取决于进程运行的顺序时, 就会发生竞争条件, 这是操作系统中一个典型的问题, 无效唤醒恰恰就是由于竞争条件导致的.
设想有两个进程A 和B, A 进程正在处理一个链表, 它需要检查这个链表是否为空, 如果不空就对链表里面的数据进行一些操作, 同时B进程也在往这个链表添加节点. 当这个链表是空的时候, 由于无数据可操作, 这时A进程就进入睡眠, 当B进程向链表里面添加了节点之后它就唤醒A进程, 其代码如下:
A进程:
spin_lock(&list_lock);
if(list_empty(&list_head))
{
spin_unlock(&list_lock);
set_current_state(TASK_INTERRUPTIBLE);
schedule();
spin_lock(&list_lock);
}
/* Rest of the code ... */
spin_unlock(&list_lock);
}
B进程:
spin_lock(&list_lock);
list_add_tail(&list_head, new_node);
spin_unlock(&list_lock);
wake_up_process(A);
在这之后, A进程继续执行, 它会错误地认为这个时候链表仍然是空的, 于是将自己的状态设置为TASK_INTERRUPTIBLE然后调用schedule()进入睡眠. 由于错过了B进程唤醒, 它将会无限期的睡眠下去, 这就是无效唤醒问题, 因为即使链表中有数据需要处理, A进程也还是睡眠了.
4.2 无效唤醒的原因
如何避免无效唤醒问题呢?
我们发现无效唤醒主要发生在检查条件之后和进程状态被设置为睡眠状态之前, 本来B进程的wake_up_process提供了一次将A进程状态置为TASK_RUNNING的机会,可惜这个时候A进程的状态仍然是TASK_RUNNING,所以wake_up_process将A进程状态从睡眠状态转变为运行状态的努力没有起到预期的作用.
4.3 避免无效抢占
要解决这个问题, 必须使用一种保障机制使得判断链表为空和设置进程状态为睡眠状态成为一个不可分割的步骤才行, 也就是必须消除竞争条件产生的根源, 这样在这之后出现的wake_up_process就可以起到唤醒状态是睡眠状态的进程的作用了.
找到了原因后, 重新设计一下A进程的代码结构, 就可以避免上面例子中的无效唤醒问题了.
A进程
set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
spin_unlock(&list_lock);
schedule();
spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* Rest of the code ... */
spin_unlock(&list_lock);
可以看到,这段代码在测试条件之前就将当前执行进程状态转设置成TASK_INTERRUPTIBLE了, 并且在链表不为空的情况下又将自己置为TASK_RUNNING状态.
这样一来如果B进程在A进程进程检查了链表为空以后调用wake_up_process, 那么A进程的状态就会自动由原来TASK_INTERRUPTIBLE变成TASK_RUNNING, 此后即使进程又调用了schedule, 由于它现在的状态是TASK_RUNNING, 所以仍然不会被从运行队列中移出, 因而不会错误的进入睡眠,当然也就避免了无效唤醒问题.
5 Linux内核的例子
5.1 一个最基本的例子
在Linux操作系统中, 内核的稳定性至关重要, 为了避免在Linux操作系统内核中出现无效唤醒问题, Linux内核在需要进程睡眠的时候应该使用类似如下的操作:
/* ‘q’是我们希望睡眠的等待队列 */
DECLARE_WAITQUEUE(wait,current);
add_wait_queue(q, &wait);
set_current_state(TASK_INTERRUPTIBLE);
/* 或TASK_INTERRUPTIBLE */
while(!condition) /* ‘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将自己的状态设置为就绪, 这样同样保证了进程不会存在错误的进入睡眠的倾向, 当然也就不会导致出现无效唤醒问题.
内核中有很多地方使用了避免无效唤醒的时候, 最普遍的地方是内核线程的, 因为内核线程的主要功能是辅助内核完成一定的工作的, 大多数情况下他们处于睡眠态, 当内核发现有任务要做的时候, 才会唤醒它们.
5.2 2号进程的例子-避免无效抢占
下面让我们用linux内核中的实例来看看Linux 内核是如何避免无效睡眠的, 我还记得2号进程吧, 它的主要工作就是接手内核线程kthread的创建, 其工作流程函数是kthreadd代码在kernel/kthread.c, kthreadd函数, line L514
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
/* ==do_something start== */
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
spin_unlock(&kthread_create_lock);
create_kthread(create);
/* ==do_something end == */
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
5.2 kthread_worker_fn
kthread_worker/kthread_work是一种内核工作的更好的管理方式, 可以多个内核线程在同一个worker上工作, 共同完成work的工作, 有点像线程池的工作方式.
内核提供了kthread_worker_fn函数一般作为 kthread_create或者 kthread_run函数的 threadfn 参数运行, 可以将多个内核线程附加的同一个worker上面,即将同一个worker结构传给kthread_run 或者kthread_create当作threadfn的参数就可以了.
其kthread_worker_fn函数作为worker的主函数框架, 也包含了避免无效唤醒的代码, kernel/kthread.c, kthread_worker_fn函数, line573, 如下所示
int kthread_worker_fn(void *worker_ptr)
{
/* ......*/
set_current_state(TASK_INTERRUPTIBLE); /* mb paired w/ kthread_stop */
if (kthread_should_stop()) {
__set_current_state(TASK_RUNNING);
spin_lock_irq(&worker->lock);
worker->task = NULL;
spin_unlock_irq(&worker->lock);
return 0;
}
/* ......*/
}
此外内核的__kthread_parkme函数中也包含了类似的代码
6 总结
通过上面的讨论, 可以发现在Linux 中避免进程的无效唤醒的关键是
- 在进程检查条件之前就将进程的状态置为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
- 并且如果检查的条件满足的话就应该将其状态重新设置为TASK_RUNNING.
这样无论进程等待的条件是否满足, 进程都不会因为被移出就绪队列而错误地进入睡眠状态, 从而避免了无效唤醒问题.
set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
spin_unlock(&list_lock);
schedule();
spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* Rest of the code ... */
spin_unlock(&list_lock);
Linux唤醒抢占----Linux进程的管理与调度(二十三)的更多相关文章
- Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)
1 前景回顾 1.1 Linux的调度器组成 2个调度器 可以用两种方法来激活调度 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测 ...
- Linux CFS调度器之负荷权重load_weight--Linux进程的管理与调度(二十五)
1. 负荷权重 1.1 负荷权重结构struct load_weight 负荷权重用struct load_weight数据结构来表示, 保存着进程权重值weight.其定义在/include/lin ...
- Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)
1 非抢占式和可抢占式内核 为了简化问题,我使用嵌入式实时系统uC/OS作为例子 首先要指出的是,uC/OS只有内核态,没有用户态,这和Linux不一样 多任务系统中, 内核负责管理各个任务, 或者说 ...
- Linux CFS调度器之pick_next_task_fair选择下一个被调度的进程--Linux进程的管理与调度(二十八)
1. CFS如何选择最合适的进程 每个调度器类sched_class都必须提供一个pick_next_task函数用以在就绪队列中选择一个最优的进程来等待调度, 而我们的CFS调度器类中, 选择下一个 ...
- Linux CFS调度器之队列操作--Linux进程的管理与调度(二十七)
1. CFS进程入队和出队 完全公平调度器CFS中有两个函数可用来增删队列的成员:enqueue_task_fair和dequeue_task_fair分别用来向CFS就绪队列中添加或者删除进程 2 ...
- Linux CFS调度器之虚拟时钟vruntime与调度延迟--Linux进程的管理与调度(二十六)
1 虚拟运行时间(今日内容提醒) 1.1 虚拟运行时间的引入 CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度. 具体实现时,CFS通过每个进程的虚拟运行时间(vrun ...
- Linux进程优先级的处理--Linux进程的管理与调度(二十二)
1. linux优先级的表示 1.1 优先级的内核表示 linux优先级概述 在用户空间通过nice命令设置进程的静态优先级, 这在内部会调用nice系统调用, 进程的nice值在-20~+19之间. ...
- Linux CFS调度器之task_tick_fair处理周期性调度器--Linux进程的管理与调度(二十九)
1. CFS如何处理周期性调度器 周期性调度器的工作由scheduler_tick函数完成(定义在kernel/sched/core.c, line 2910), 在scheduler_tick中周期 ...
- ELF文件的加载过程(load_elf_binary函数详解)--Linux进程的管理与调度(十三)
加载和动态链接 从编译/链接和运行的角度看,应用程序和库程序的连接有两种方式. 一种是固定的.静态的连接,就是把需要用到的库函数的目标代码(二进制)代码从程序库中抽取出来,链接进应用软件的目标映像中: ...
随机推荐
- 敏感词汇过滤DFA算法
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Tex ...
- Python机器学习笔记 使用sklearn做特征工程和数据挖掘
特征处理是特征工程的核心部分,特征工程是数据分析中最耗时间和精力的一部分工作,它不像算法和模型那样式确定的步骤,更多的是工程上的经验和权衡,因此没有统一的方法,但是sklearn提供了较为完整的特征处 ...
- Python机器学习笔记 K-近邻算法
K近邻(KNN,k-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一. 所谓K最近邻,就是K个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表.KNN算法的 ...
- DNS工作原理
一.简述dns DNS(domain name system)域名系统或者(domain named system)区域名称服务,分为正向与反向域名解析,适用C/S,端口路53/udp,53/tcp, ...
- MONGODB(二)——索引操作
一.1.插入10w条数据> for(var i = 0;i<100000;i++){... var rand = parseInt(i*Math.random());... db.pers ...
- ASP.NET Core中使用Graylog记录日志
以下基于.NET Core 2.1 定义GrayLog日志记录中间件: 中间件代码: public class GrayLogMiddleware { private readonly Request ...
- EChart中使用地图方式总结(转载)
EChart中使用地图方式总结 2018年02月06日 22:18:57 来源:https://blog.csdn.net/shaxiaozilove/article/details/79274772 ...
- [nodejs] nodejs开发个人博客(四)数据模型
数据库模型 /model/db.js 数据库操作类,完成链接数据库和数据库的增删查改 查询表 /*查询*/ select:function(tableName,callback,where,field ...
- 【开源程序(C++)】获取bing图片并自动设置为电脑桌面背景
众所周知,bing搜索网站首页每日会更新一张图片,张张漂亮(额,也有一些不合我口味的),特别适合用来做电脑壁纸. 我们想要将bing网站背景图片设置为电脑桌面背景的通常做法是: 上网,搜索bing 找 ...
- 如何实现JavaScript的Map和Filter函数?
译者按: 鲁迅曾经说过,学习JavaScript最好方式莫过于敲代码了! 原文: Master Map & Filter, Javascript’s Most Powerful Array F ...