HY:内核禁止抢占,并不会妨碍进程调度,所以自旋锁(禁止抢占)保护的代码不能睡眠,否则会进行内核进程调度,可能会造成死锁。

内核抢占:在系统调用到内核态时,也可以发生进程调度。

https://blog.csdn.net/gatieme/article/details/51872618

schedule函数调用的时机:

/*
 * __schedule() is the main scheduler function.
 *
 * The main means of driving the scheduler and thus entering this function are:
 *
 *   1. Explicit blocking: mutex, semaphore, waitqueue, etc.
 *
 *   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
 *      paths. For example, see arch/x86/entry_64.S.
 *
 *      To drive preemption between tasks, the scheduler sets the flag in timer
 *      interrupt handler scheduler_tick().在可抢占时,在时钟中断中设置TIF_NEED_RESCHED 。
 *
 *   3. Wakeups don't really cause entry into schedule(). They add a
 *      task to the run-queue and that's it.Wakeups 并不会引起调度,只是将就绪任务装载到run-queue。
 *
 *      Now, if the new task added to the run-queue preempts the current
 *      task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
 *      called on the nearest possible occasion:
 *
 *       - If the kernel is preemptible (CONFIG_PREEMPT=y):
 *
 *         - in syscall or exception context, at the next outmost
 *           preempt_enable(). (this might be as soon as the wake_up()'s
 *           spin_unlock()!)
 *
 *         - in IRQ context, return from interrupt-handler to
 *           preemptible context
 *
 *       - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
 *         then at the next:
 *
 *          - cond_resched() call
 *          - explicit schedule() call
 *          - return from syscall or exception to user-space
 *          - return from interrupt-handler to user-space
 */

/** __schedule() is the main scheduler function.** The main means of driving the scheduler and thus entering this function are:**   1. Explicit blocking: mutex, semaphore, waitqueue, etc.**   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return*      paths. For example, see arch/x86/entry_64.S.**      To drive preemption between tasks, the scheduler sets the flag in timer*      interrupt handler scheduler_tick().**   3. Wakeups don't really cause entry into schedule(). They add a*      task to the run-queue and that's it.**      Now, if the new task added to the run-queue preempts the current*      task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets*      called on the nearest possible occasion:**       - If the kernel is preemptible (CONFIG_PREEMPT=y):**         - in syscall or exception context, at the next outmost*           preempt_enable(). (this might be as soon as the wake_up()'s*           spin_unlock()!)**         - in IRQ context, return from interrupt-handler to*           preemptible context**       - If the kernel is not preemptible (CONFIG_PREEMPT is not set)*         then at the next:**          - cond_resched() call*          - explicit schedule() call*          - return from syscall or exception to user-space*          - return from interrupt-handler to user-space*/

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

抢占式内核与半抢占式内核的不同

Linux2.4只实现了“有条件抢占式”的调度。它的缺点在于:当进程在内核态时,调度的时机有局限。就是只能在xxx的前夕。例如:当外部来一中断,中断程序过程完后,需要一个用户进程B对此进行进一步的处理(响应IP包数据)。此时进程A正在使用系统调用进入了内核态。那么等到A从系统调用返回之际,内核进行调度,B才有可能运行。假设A的系统调用占用了CPU的时间为T。这个T大于用户要求的响应时间。那这个系统就不够实时。

为了提高linux的实时性。在linux2.6中引入了“Kernel preemption”(内核抢占调度模式)。并很好的解决了这个问题。一句话就是抢占式内核可以在进程处于内核态时,进行抢占。

当然抢占式内核在以下几种情况下不可抢占:

1.当内核运行中断处理程序和异常处理程序时,在linux内核中进程不能抢占中断,在中断例程中不允许进行调度。进程调度函数schedule会对此作出判断,如果是在中断中调用,会打印出出错信息。

2.当进程在内核态运行临界区的代码时,不可抢占。这些临界区被自旋锁spin_lock保护了起来。【但是当进程使用spin_lock时,自己被锁住并自旋时,这时可以调度。】

3. 内核正在进行bottom half(中断的底半部)处理时,不可抢占。【不太懂】

4.内核正在执行调度程序Scheduler时,不可抢占。

5.内核正在对每一个CPU“私有”数据结构操作(per CPU date structures)时,不可抢占。在SMP中,对于Per-cpu数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了。

抢占式内核什么时候,什么位置调用schedule函数?

当中断发生,并完成中断处理时,在返回之前被中断的进程时,可以根据需要进行调度。

抢占式内核为每一个进程的task_struct结构引入了preempt_count变量,称为内核抢占锁。每当进程进入以上五种状态时,preempt_count加1.表示不可抢占。当退出以上五种状态时,preempt_count减1. 每次进行抢占式调度时,先判断preempt_count与0大小,preempt_count<0,表示可抢占。preempt_count>0表示不可抢占。

一系统抢占式的调度器函数:preempt_schedule;preempt_schedule_irq。它们都是调用schedule来完成调度的。

实时操作系统与抢占式内核的关系

实时操作系统要求就是对来自外部的请求,要求有及时的处理。及时到什么程度就是实时操作系统呢?这个没有一个明确的定义,因为用户对响应时间的要求各不相同。

我们可以说当在同样的硬件条件下,Linux2.4的实时性不高,或是不如linux2.6的实时性高。那么提高系统的实时性的方法有很多,提高CPU速度,增加CPU核。优化操作系统等。那么 linux在提高系统实时性的重要贡献就是引入了“内核抢占调度模式”。那么我们也可以说linux很好的支持了实时性。
————————————————
版权声明:本文为CSDN博主「鹏鹏~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36321889/article/details/100137700

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

抢占的时机

1.用户态的抢占时机

当该进程进行系统调用从内核态返回到用户态的时候,判断如果该进程有TIF_NEED_RESCHED标签,则进行抢占。

2.内核态的抢占时机

对内核态的执行中,被抢占的时机一般发生在preempt_enable()中。

preempt_disable()关闭抢占

在内核态的执行中,有的操作是不能被中断的,所有在进行这些操作之前,总是先调用preempt_disable()关闭抢占,当再次打开的时候,也就是调用preempt_enable()的时候

就是一次内核态代码被抢占的机会。

在内核态也会遇到中断的情况,当中断返回的时候,返回的仍然是内核态度。这个时候也是一个执行抢占的时机。

大家可以看看这张图理解

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

自旋锁是SMP架构中的一种low-level的同步机制。
     当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。对于自选锁需要注意:

  • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
  • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起,最近刚解决了一个内核中的问题就是由于持有自旋锁时sleep了,然后导致所有的核全部挂起(是一个8核的CPU))

使用任何锁需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:

  1. 建立锁所需要的资源
  2. 当线程被阻塞时锁所需要的资源

Pthreads提供的与Spin Lock锁操作相关的API主要有:

int pthread_spin_destroy(pthread_spinlock_t *); int pthread_spin_init(pthread_spinlock_t *, int); int pthread_spin_lock(pthread_spinlock_t *); int pthread_spin_trylock(pthread_spinlock_t *); int pthread_spin_unlock(pthread_spinlock_t *);

1)初始化自旋锁

pthread_spin_init用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。pshared的取值及其含义:

  • PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。
  • PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。

2)获得一个自旋锁

pthread_spin_lock用来获取(锁定)指定的自旋锁. 如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁.否则该函数在获得自旋锁之前不会返回。如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。

3)尝试获取一个自旋锁

pthread_spin_trylock会尝试获取指定的自旋锁,如果无法获取则理解返回失败。

4)释放(解锁)一个自旋锁

pthread_spin_unlock用于释放指定的自旋锁。

5)销毁一个自旋锁

pthread_spin_destroy 用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)在调用该函数之后如果没有调用 pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。如果调用该函数时自旋锁正在被使用或者自旋锁未被初 始化则结果是未定义的。

互斥量和自旋锁的区别:

Pthreads提供的Mutex锁操作相关的API主要有:

pthread_mutex_lock (pthread_mutex_t *mutex); pthread_mutex_trylock (pthread_mutex_t *mutex); pthread_mutex_unlock (pthread_mutex_t *mutex);

Pthreads提供的与Spin Lock锁操作相关的API主要有:

pthread_spin_lock (pthread_spinlock_t *lock); pthread_spin_trylock (pthread_spinlock_t *lock); pthread_spin_unlock (pthread_spinlock_t *lock);

从实现原理上来讲,Mutex属于sleep-waiting类型的 锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过 pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
       如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果 没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。
        对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
        对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用 时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
        因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

自旋锁与linux内核进程调度关系

  如果临界区可能包含引起睡眠的代码则不能使用自旋锁,否则可能引起死锁。

  那么为什么信号量保护的代码可以睡眠而自旋锁就不能呢?

  先看下自旋锁的实现方法吧,自旋锁的基本形式如下:

  spin_lock(&mr_lock);   //临界区   spin_unlock(&mr_lock);   跟踪一下spin_lock(&mr_lock)的实现   #define spin_lock(lock) _spin_lock(lock)   #define _spin_lock(lock) __LOCK(lock)   #define __LOCK(lock) \   do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

注意到“preempt_disable()”,这个调用的功能是“关抢占”(在spin_unlock中会重新开启抢占功能)。从 中可以看出,使用自旋锁保护的区域是工作在非抢占的状态;即使获取不到锁,在“自旋”状态也是禁止抢占的。了解到这,我想咱们应该能够理解为何自旋锁保护 的代码不能睡眠了。试想一下,如果在自旋锁保护的代码中间睡眠,此时发生进程调度,则可能另外一个进程会再次调用spinlock保护的这段代码。而我们 现在知道了即使在获取不到锁的“自旋”状态,也是禁止抢占的,而“自旋”又是动态的,不会再睡眠了,也就是说在这个处理器上不会再有进程调度发生了,那么 死锁自然就发生了。

  咱们可以总结下自旋锁的特点:

  ● 单处理器非抢占内核下:自旋锁会在编译时被忽略;

  ● 单处理器抢占内核下:自旋锁仅仅当作一个设置内核抢占的开关;

  ● 多处理器下:此时才能完全发挥出自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。

linux抢占发生的时间

  最后在了解下linux抢占发生的时间,抢占分为用户抢占和内核抢占。

  用户抢占在以下情况下产生:

  ● 从系统调用返回用户空间

  ● 从中断处理程序返回用户空间

  内核抢占会发生在:

  ● 当从中断处理程序返回内核空间的时候,且当时内核具有可抢占性;

  ● 当内核代码再一次具有可抢占性的时候。(如:spin_unlock时)

  ● 如果内核中的任务显式的调用schedule()

  ● 如果内核中的任务阻塞。

  基本的进程调度就是发生在时钟中断后,并且发现进程的时间片已经使用完了,则发生进程抢占。通常我们会利用中断处理程序返回内核空间的时候可以进行内 核抢占这个特性来提高一些I/O操作的实时性,如:当I/O事件发生的是时候,对应的中断处理程序被激活,当它发现有进程在等待这个I/O事件的时候,它 会激活等待进程,并且设置当前正在执行进程的need_resched标志,这样在中断处理程序返回的时候,调度程序被激活,原来在等待I/O事件的进程 (很可能)获得执行权,从而保证了对I/O事件的相对快速响应(毫秒级)。可以看出,在I/O事件发生的时候,I/O事件的处理进程会抢占当前进程,系统 的响应速度与调度时间片的长度无关。

总结:
(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。

(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己 的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。

(3)更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1 用户抢占(hy:叫进程调度比较好)

内核即将返回用户空间的时候,如果need resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。在内核返回用户空间的时候,它知道自己是安全的。所以,内核无论是在从中断处理程序还是在系统调用后返回,都会检查need resched标志。如果它被设置了,那么,内核会选择一个其他(更合适的)进程投入运行。

简而言之,用户抢占在以下情况时产生:

1) 从系统调用返回用户空间。

2) 从中断处理程序返回用户空间。

2 不可抢占内核的特点

在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度——内核中的各任务是协作方式调度的,不具备抢占性。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止。

3. 为什么需要内核抢占?

实现内核的可抢占对Linux具有重要意义。首先,这是将Linux应用于实时系统所必需的。实时系统对响应时间有严格的限定,当一个实时进程被实时设备的硬件中断唤醒后,它应在限定的时间内被调度执行。而Linux不能满足这一要求,因为Linux的内核是不可抢占的,不能确定系统在内核中的停留时间。事实上当内核执行长的系统调用时,实时进程要等到内核中运行的进程退出内核才能被调度,由此产生的响应延迟,在如今的硬件条件下,会长达100ms级。

禁止内核抢占的情况列出如下:

(1)内核执行中断处理例程时不允许内核抢占,中断返回时再执行内核抢占。

(2)当内核执行软中断或tasklet时,禁止内核抢占,软中断返回时再执行内核抢占。

(3)在临界区禁止内核抢占,临界区保护函数通过抢占计数宏控制抢占,计数大于0,表示禁止内核抢占。

4. 如何支持抢占内核

为保证Linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_ count,称为内核抢占锁。这一变量被设置在进程的PCB结构task_struct中。每当内核要进入以上几种状态时,变量preempt_ count就加1,指示内核不允许抢占。每当内核从以上几种状态退出时,变量preempt_ count就减1,同时进行可抢占的判断与调度。

抢占式Linux内核的修改主要有两点:

一是对中断的入口代码和返回代码进行修改。在中断的入口内核抢占锁preempt_count加1,以禁止内核抢占;在中断的返回处,内核抢占锁preempt_count减1,使内核有可能被抢占。

另一基本修改是重新定义了自旋锁、读、写锁,在锁操作时增加了对preempt count变量的操作。在对这些锁进行加锁操作时preemptcount变量加1,以禁止内核抢占;在释放锁时preemptcount变量减1,并在内核的抢占条件满足且需要重新调度时进行抢占调度。

设置调度的时机(hy这是设置调度的时机,调度的时机见6)

内核必须知道在什么时候调用schedule()。如果仅靠用户程序代码显式地调用schedule(),它们可能就会永远地执行下去。相反,内核提供了一个need_resched标志来表明是否需要重新执行一次调度。

1).当前进程用完了它的CPU时间片,update_process_times()重新进行计算时钟中断触发schduler_tick()的主要作用就是每一个tick 进程陷入内核后, 他的时间片就递减1 , 当变为0的时候, 会设置TIF_NEED_RESCHED为1 。

2).当一个进程被唤醒,而且它的优先级比当前进程高 try_to_wake_up(),会设置TIF_NEED_RESCHED为1 。

6. 调度的时机

1) 中断返回内核空间:检查preempt_count是否为0和TIF_NEED_RESCHED是否为1

2) 中断或异常返回到user space:检查TIF_NEED_RESCHED是否为1

3) 显式或者隐式调preemp_enable()函数:检查preempt_count是否为0和TIF_NEED_RESCHED是否为1

4) 使能软中断:检查preempt_count是否为0和TIF_NEED_RESCHED是否为1

5) 自己主动schedule()

Question:

FIFO实时进程在运行时候,能加载一个新程序吗,cup一直被该实时进程占据 谁来加载这个新进程。

如果可以加载 ,而且这个新进程优先级比原来的高,那如何抢占原来的实时进程(时钟中断?)

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Linux进程调度与抢占

https://www.cnblogs.com/hellokitty2/p/10741600.html

一、linux内核抢占介绍

1.抢占发生的必要条件

a.preempt_count抢占计数必须为0,不为0说明其它地方调用了禁止抢占的函数,比如spin_lock系列函数。
b.中断必须是使能的状态,因为抢占动作要依赖中断

preempt_schedule()具体源码实现参考如下:

asmlinkage __visible void __sched notrace preempt_schedule(void)
{/** If there is a non-zero preempt_count or interrupts are disabled,* we do not want to preempt the current task. Just return..*//*preempt_disable()会增加preempt_count的计数*/if (likely(!preemptible()))return;preempt_schedule_common();
}
#define preemptible()    (preempt_count() == 0 && !irqs_disabled())
------------------------------------------------------------------------------------
#ifdef CONFIG_PREEMPT
/** this is the entry point to schedule() from in-kernel preemption* off of preempt_enable. Kernel preemptions off return from interrupt* occur there and call schedule directly.*/
asmlinkage void __sched notrace preempt_schedule(void)
{struct thread_info *ti = current_thread_info();/** If there is a non-zero preempt_count or interrupts are disabled,* we do not want to preempt the current task. Just return..*/if (likely(ti->preempt_count || irqs_disabled()))return;#ifdef CONFIG_PREEMPT_LAZY/** Check for lazy preemption*/if (ti->preempt_lazy_count && !test_thread_flag(TIF_NEED_RESCHED))return;
#endifdo {add_preempt_count_notrace(PREEMPT_ACTIVE);/** The add/subtract must not be traced by the function* tracer. But we still want to account for the* preempt off latency tracer. Since the _notrace versions* of add/subtract skip the accounting for latency tracer* we must force it manually.*/start_critical_timings();__schedule();stop_critical_timings();sub_preempt_count_notrace(PREEMPT_ACTIVE);/** Check again in case we missed a preemption opportunity* between schedule and now.*/barrier();} while (need_resched());
}
EXPORT_SYMBOL(preempt_schedule);

2.spin_lock系列函数

a.spin_lock()会调用preempt_disable函数关闭抢占.
b.spin_lock_irq()会调用spin_lock()函数和local_irq_disable()函数(关闭中断)
c.spin_lock_irqsave()会调用spin_lock()函数和local_irq_save()函数(关闭中断,同时保存cpu对中断的屏蔽状态,使用spin_lock_irqsave在于你不期望在离开临界区后,改变中断的开启,关闭状态!进入临界区是关闭的,离开后它同样应该是关闭的!)

spin_lock():

/*include/linux/spinlock.h*/
static __always_inline void spin_lock(spinlock_t *lock)
{raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock)    _raw_spin_lock(lock)/*kernel/locking/spinlock.c*/
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{__raw_spin_lock(lock);
}/*include/linux/spinlock_api_smp.h*/
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{preempt_disable(); /*调用禁止抢占函数*/spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

spin_unlock():

/*kernel/locking/spinlock.c*/
void __lockfunc _raw_spin_unlock(raw_spinlock_t *lock)
{__raw_spin_unlock(lock);
}/*include/linux/spinlock_api_smp.h*/
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{spin_release(&lock->dep_map, 1, _RET_IP_);do_raw_spin_unlock(lock);preempt_enable();
}

preempt_enable():

/*include/linux/preempt.h*/
#define preempt_enable() \
do { \barrier(); \if (unlikely(preempt_count_dec_and_test())) \/*这里提供了一个抢占点__preempt_schedule(),其它高优先级的进程可直接抢占*/\__preempt_schedule(); \
} while (0)

由上可知,spin_unlock()系列函数可以直接触发内核抢占,因为它里面提供可抢占点。

3.preempt_disable()和local_irq_disable()的区别

由抢占发生的必要条件可知两个函数都可以关闭抢占。区别不在于关抢占和关中断函数上,而是在对应的开抢占和开中断的函数上,也就是
preempt_enable()函数local_irq_enable()函数。preempt_enable()会是能抢占并提供抢占点,而local_irq_enable()仅仅是开中断(是能抢占),
并没有提供抢占点。

4.抢占点可能是:时钟tick中断处理返回、中断返回、软中断结束、yield()(进程调用它放弃CPU)等等多种情况。

5.注意spin_lock系列函数关闭了抢占,但是并没有关闭调度!(spin_lock保护的代码中不能出现睡眠,而且要尽量短,否则可能会死锁)

6.原子上下文中不可睡眠,可以打开内核中的CONFIG_DEBUG_ATOMIC_SLEEP选项,运行时一旦检测出在原子上下文中可能睡眠就会打印栈回溯信息。

7.进程的优先级使用nice值表示。

二、进程调度

1.目前4.14.35内核中只有下列sched_class:

fair_sched_class: .next = idle_sched_class
rt_sched_class  : .next = fair_sched_class
dl_sched_class  : .next = rt_sched_class
idle_sched_class: .next = NULL
stop_sched_class: .next = dl_sched_class

所有的调度类构成一个单链表:

stop_sched_class --> dl_sched_class --> rt_sched_class --> fair_sched_class --> idle_sched_class --> NULL
#ifdef CONFIG_SMP
#define sched_class_highest (&stop_sched_class)
#else
#define sched_class_highest (&dl_sched_class)
#endif
#define for_each_class(class)   for (class = sched_class_highest; class; class = class->next)

SCHED_NORMAL:普通的分时进程,使用的fair_sched_class调度类

SCHED_FIFO:先进先出的实时进程,使用的是rt_sched_class调度类。
当调用程序把CPU分配给进程的时候,它把该进程描述符保留在运行队列链表的当前位置。使用此调度策略的进程一旦使用CPU则一直运行。如果没有其他可运行的更高优先级实时进程,进程就会继续使用CPU,想用多久就用多久,即使还有其他具有相同优先级的实时进程处于可运行状态。

http://blog.chinaunix.net/uid-24774106-id-3379478.html

对于实时进程而言,高优先级的进程存在,低优先级的进程是轮不上的,没机会跑在CPU上,所谓实时进程的调度策略,指的是相同优先级之间的调度策略。如果是FIFO实时进程在占用CPU,除非出现以下事情,否则FIFO一条道跑到黑。a)FIFO进程良心发现,调用了系统调用sched_yield 自愿让出CPUb) 更高优先级的进程横空出世,抢占FIFO进程的CPU。有些人觉得很奇怪,怎么FIFO占着CPU,为啥还能有更高优先级的进程出现呢。别忘记,我们是多核多CPU ,如果其他CPU上出现了一个比FIFO优先级高的进程,可能会push到FIFO进程所在的CPU上。c)FIFO进程停止(TASK_STOPPED or TASK_TRACED状态)或者被杀死(EXIT_ZOMBIE or EXIT_DEAD状态)d) FIFO进程执行了阻塞调用并进入睡眠(TASK_INTERRUPTIBLE OR TASK_UNINTERRUPTIBLE)。

SCHED_RR:时间片轮转的实时进程,使用的rt_sched_class调度类。
当调度程序把CPU分配给进程的时候,它把该进程的描述符放在运行队列链表的末尾。这种策略保证对所有具有相同优先级的SCHED_RR实时进程
进行公平分配CPU时间。

SCHED_BATCH:是SCHED_NORMAL的分化版本,使用的fair_shed_class调度类。
采用分时策略,根据动态优先级,分配CPU资源。在有实时进程的时候,实时进程优先调度。但针对吞吐量优化,除了不能抢占外与常规进程一
样,允许任务运行更长时间,更好使用高速缓存,适合于成批处理的工作。

SCHED_IDLE:优先级最低,在系统空闲时运行,使用的是idle_sched_class调度类,给0号进程使用。

SCHED_DEADLINE:新支持的实时进程调度策略,使用的是dl_sched_class调度类。
针对突发型计算,并且对延迟和完成时间敏感的任务使用,基于EDF(earliest deadline first)。

2.调度类struct sched class

struct sched_class {const struct sched_class *next;void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);void (*yield_task) (struct rq *rq);bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);/** It is the responsibility of the pick_next_task() method that will* return the next task to call put_prev_task() on the @prev task or* something equivalent.** May return RETRY_TASK when it finds a higher prio class has runnable tasks.*/struct task_struct * (*pick_next_task) (struct rq *rq, struct task_struct *prev, struct rq_flags *rf);void (*put_prev_task) (struct rq *rq, struct task_struct *p);#ifdef CONFIG_SMPint  (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);void (*migrate_task_rq)(struct task_struct *p);void (*task_woken)(struct rq *this_rq, struct task_struct *task);void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask);void (*rq_online)(struct rq *rq);void (*rq_offline)(struct rq *rq);
#endifvoid (*set_curr_task) (struct rq *rq);void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);void (*task_fork) (struct task_struct *p);void (*task_dead) (struct task_struct *p);/** The switched_from() call is allowed to drop rq->lock, therefore we* cannot assume the switched_from/switched_to pair is serliazed by* rq->lock. They are however serialized by p->pi_lock.*/void (*switched_from) (struct rq *this_rq, struct task_struct *task);void (*switched_to) (struct rq *this_rq, struct task_struct *task);void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio);unsigned int (*get_rr_interval) (struct rq *rq, struct task_struct *task);void (*update_curr) (struct rq *rq);#define TASK_SET_GROUP  0
#define TASK_MOVE_GROUP    1#ifdef CONFIG_FAIR_GROUP_SCHEDvoid (*task_change_group) (struct task_struct *p, int type);
#endif
};

next: 指向下一个调度类,用于在函数pick_next_task、check_preempt_curr、set_rq_online、set_rq_offline中遍历整个调度类,根据调度
类的优先级选择调度类。
优先级为: stop_sched_class-->dl_sched_class-->rt_sched_class-->fair_sched_class-->idle_sched_class
enqueue_task: 将任务加入到调度类中
dequeue_task: 将任务从调度类中移除
yield_task/yield_to_task: 主动放弃CPU
check_preempt_curr: 检查当前进程是否可被强占
pick_next_task: 从调度类中选出下一个要运行的进程
put_prev_task: 将进程放回到调度类中
select_task_rq: 为进程选择一个合适的cpu的运行队列
migrate_task_rq: 迁移到另外的cpu运行队列
pre_schedule: 调度以前调用
post_schedule: 通知调度器完成切换
task_woken: 用于进程唤醒
set_cpus_allowed: 修改进程cpu亲和力
affinityrq_online: 启动运行队列
rq_offline:关闭运行队列
set_curr_task: 当进程改变调度类或者进程组时被调用
task_tick: 将会引起进程切换,驱动运行running强占,由time_tick调用
task_fork: 进程创建时调用,不同调度策略的进程初始化不一样
task_dead: 进程结束时调用
switched_from/switched_to:进程改变调度器时使用
prio_changed: 改变进程优先级.

3.调度的触发

调度的触发主要有两种方式:

(1)一种是本地定时中断触发调用scheduler_tick()函数,然后使用当前运行进程的调度类中的task_tick.
(2)另外一种则是主动调用schedule().
不管是哪一种最终都会调用到__schedule函数,该函数调用pick_netx_task,通过(rq->nr_running==rq->cfs.h_nr_running)判断出如果当前运行队列中的进程都在cfs调度器中,则直接调用cfs的调度类(内核代码里面这一判断使用了likely说明大部分情况都是满足该条件的)。如果运行队列不都在cfs中,则通过优先级stop_sched_class-->dl_sched_class-->rt_sched_class-->fair_sched_class-->idle_sched_class遍历选出下一个需要运行的进程,然后进程任务切换。

4.发生调度的时机

处于TASK_RUNNING状态的进程才会被进程调度器选择,其他状态不会进入调度器,系统发生调度的时机如下:
a.调用cond_resched()时
b.显式调用schedule()时
c.从中断上下文返回时
当内核开启抢占时,会多出几个调度时机:
d.在系统调用中或者中断下文中调用preemt_enable()时

5.__schedule()实现
TODO:分析它

6.CFS(Completely Fair Scheduler)调度

该部分代码位于linux/kernel/sched/fair.c中,定义了const struct sched_classfair_sched_class,这个是CFS的调度类定义的对象。其中
基本包含了CFS调度的所有实现。

CFS实现三个调度策略:
SCHED_NORMAL:这个调度策略是被常规任务使用
SCHED_BATCH:这个策略不像常规的任务那样频繁的抢占,以牺牲交互性为代价下,因而允许任务运行更长的时间以更好的利用缓存,这种策略
适合批处理。
SCHED_IDLE:这是nice值甚至比19还弱,但是为了避免陷入优先级导致问题,这个问题将会死锁这个调度器,因而这不是一个真正空闲定时调
度器。

CFS调度类fair_sched_class:
enqueue_task():当任务进入runnable状态,这个回调将把这个任务的调度实体(entity)放入红黑树并且增加nr_running变量的值。
dequeue_task():当任务不再是runnable状态,这个回调将会把这个任务的调度实体从红黑树中取出,并且减少nr_running变量的值。
yield_task():除非compat_yield sysctl是打开的,这个回调函数基本上就是一个dequeue后跟一个enqueue,这那种情况下,他将任务的调度
实体放入红黑树的最右端
check_preempt_curr():这个回调函数是检查一个任务进入runnable状态是否应该抢占当前运行的任务。
pick_next_task():这个回调函数选出下一个最合适运行的任务。
set_curr_task():当任务改变他的调度类或者改变他的任务组,将调用该回调函数。
task_tick():这个回调函数大多数是被time tick调用。它可能引起进程切换,这就驱动了运行时抢占。

/** 一个调度实体(红黑树的一个节点),其包含一组或一个指定的进程,包含一个自己的运行队列,* 一个父亲指针,一个指向需要调度的队列.*/
struct sched_entity {/* For load-balancing: */struct load_weight        load; /*权重,在数组prio_to_weight[]包含优先级转权重的数值*/struct rb_node            run_node; /*实体在红黑树对应的节点信息*/struct list_head        group_node; /*实体所在的进程组*/unsigned int            on_rq;  /*实体是否处于红黑树运行队列中*/u64                exec_start; /*开始运行时间*/u64                sum_exec_runtime;  /*总运行时间*//*虚拟运行时间,在时间中断或者任务状态发生改变时会更新.其会不停的增长,增长速度与load权重成反比,load越高,增长速度越慢,就越可能处于红黑树最左边被调度。每次时钟中断都会修改其值,具体见calc_delta_fair()函数*/u64                vruntime;/*进程在切换进cpu时的sum_exec_runtime值*/u64                prev_sum_exec_runtime;/*此调度实体中进程移到其他cpu组的数量*/u64                nr_migrations;struct sched_statistics        statistics;#ifdef CONFIG_FAIR_GROUP_SCHEDint                depth;/*父亲调度实体指针,如果是进程则指向其运行队列的调度实体,如果是进程组则指向其上一个进程组的调度实体,在set_task_rq函数中设置。*/struct sched_entity        *parent;/* rq on which this entity is (to be) queued: */struct cfs_rq            *cfs_rq; /*实体所处红黑树运行队列*//* rq "owned" by this entity/group: */struct cfs_rq            *my_q; /*实体的红黑树运行队列,如果为NULL表明其是一个进程,若非NULL表明其是调度组*/
#endif#ifdef CONFIG_SMP/** Per entity load average tracking.** Put into separate cache line so it does not* collide with read-mostly values above.*/struct sched_avg        avg ____cacheline_aligned_in_smp;
#endif
};

load
指定了权重, 决定了各个实体占队列总负荷的比重, 计算负荷权重是调度器的一项重任, 因为CFS所需的虚拟时钟的速度最终依赖于负荷, 权
重通过优先级转换而成,是vruntime计算的关键
run_node
调度实体在红黑树对应的结点信息, 使得调度实体可以在红黑树上排序
sum_exec_runtime
记录程序运行所消耗的CPU时间, 以用于完全公平调度器CFS
on_rq
调度实体是否在就绪队列上接受检查, 表明是否处于CFS红黑树运行队列中,需要明确一个观点就是,CFS运行队列里面包含有一个红黑树,但
这个红黑树并不是CFS运行队列的全部,因为红黑树仅仅是用于选择出下一个调度程序的算法。很简单的一个例子,普通程序运行时,其并不在
红黑树中,但是还是处于CFS运行队列中,其on_rq为真。只有准备退出、即将睡眠等待和转为实时进程的进程其CFS运行队列的on_rq为假。
vruntime
虚拟运行时间,调度的关键,其计算公式:一次调度间隔的虚拟运行时间 = 实际运行时间 * (NICE_0_LOAD / 权重)。可以看出跟实际运行时
间和权重有关,红黑树就是以此作为排序的标准,优先级越高的进程在运行时其vruntime增长的越慢,其可运行时间相对就长,而且也越有可
能处于红黑树的最左结点,调度器每次都选择最左边的结点为下一个调度进程。注意其值为单调递增,在每个调度器的时钟中断时当前进程的
虚拟运行时间都会累加。单纯的说就是进程们都在比谁的vruntime最小,最小的将被调度。
cfs_rq
此调度实体所处于的CFS运行队列
my_q
如果此调度实体代表的是一个进程组,那么此调度实体就包含有一个自己的CFS运行队列,其CFS运行队列中存放的是此进程组中的进程,这些
进程就不会在其他CFS运行队列的红黑树中被包含(包括顶层红黑树也不会包含他们,他们只属于这个进程组的红黑树)。
sum_exec_runtime
跟踪运行时间是由update_curr不断累积完成的。内核中许多地方都会调用该函数, 例如, 新进程加入就绪队列时, 或者周期性调度器中. 每次
调用时, 会计算当前时间和exec_start之间的差值, exec_start则更新到当前时间. 差值则被加到sum_exec_runtime.
在进程执行期间虚拟时钟上流逝的时间数量由vruntime统计。
在进程被撤销时, 其当前sum_exec_runtime值保存到prev_sum_exec_runtime, 此后, 进程抢占的时候需要用到该数据, 但是注意, 在prev_sum_exec_runtime
中保存了sum_exec_runtime的值, 而sum_exec_runtime并不会被重置, 而是持续单调增长。

每一个进程的task_struct中都嵌入了sched_entry对象,所以进程是可调度的实体,但是可调度的实体不一定是进程,也可能是进程组。

7.CFS调度总结:

Tcik中断,主要会更新调度信息,然后调整当前进程在红黑树中的位置。调整完成以后如果当前进程不再是最左边的叶子,就标记为Need_resched
标志,中断返回时就会调用scheduler()完成切换、否则当前进程继续占用CPU。从这里可以看出CFS抛弃了传统时间片概念。Tick中断只需要更新红黑树。

红黑树键值即为vruntime,该值通过调用update_curr函数进行更新。这个值为64位的变量,会一直递增,__enqueue_entity中会将vruntime作为键值将
要入队的实体插入到红黑树中。__pick_first_entity会将红黑树中最左侧即vruntime最小的实体取出。

优秀文章:

Linux 2.6 Completely Fair Scheduler 内幕: https://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html

https://blog.csdn.net/zhoutaopower/article/details/86290196

Linux内核抢占和进程调度相关推荐

  1. Linux内核抢占实现机制分析【转】

    Linux内核抢占实现机制分析 转自:http://blog.chinaunix.net/uid-24227137-id-3050754.html [摘要]本文详解了Linux内核抢占实现机制.首先介 ...

  2. 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】Linux内核抢占实现机制分析

    Linux内核抢占实现机制分析 Sailor_forever  sailing_9806@163.com 转载请注明 http://blog.csdn.net/sailor_8318/archive/ ...

  3. linux 内核抢占分析

    linux 内核抢占分析 在 Linux 2.6 以后版本的 Linux 内核中,一个内核任务可以被抢占,从而提高系统的实时性.这样做最主要的优势在于,可以极大地增强系统的用户交互性,用户将会觉得鼠标 ...

  4. linux程序不可抢占,Linux内核抢占机制(preempt)

    早期的Linux核心是不可抢占的.它的调度方法是:一个进程可以通过schedule()函数自愿地启动一次调度.非自愿的强制性调度只能发生在 每次从系统调用返回的前夕以及每次从中断或异常处理返回到用户空 ...

  5. linux内核杂记(11)-进程调度(6)

    LINUX实时调度策略 调度策略分类 1.SCHED_FIFO (1)简单的先入先出调度方法 (2)不使用时间片,一旦SCHED_FIFO的进程处于可执行状态,就会继续执行,直到它自己执行完毕或显式释 ...

  6. 【Linux 内核】Linux 内核特性 ( 组织形式 | 进程调度 | 内核线程 | 多平台虚拟内存管理 | 虚拟文件系统 | 内核模块机制 | 定制系统调用 | 网络模块架构 )

    文章目录 一.Linux 内核特性 1.Linux 内核组织形式 2.Linux 进程调度 3.Linux 内核线程 4.Linux 内核多平台虚拟内存管理 5.Linux 虚拟文件系统 6.Linu ...

  7. 【Linux 内核】Linux 内核体系架构 ( 进程调度 | 内存管理 | 中断管理 | 设备管理 | 文件系统 )

    文章目录 一.进程调度 二.内存管理 三.中断管理 四.设备管理 五.文件系统 一.进程调度 进程调度 : 进程 是 系统中 进行 资源分配 的 基本单位 ; 每个进程 在 运行时 , 都 感觉自己占 ...

  8. linux内核杂记(10)-进程调度(5)

    一.用户抢占 1.用户抢占时机 从系统调用返回用户空间 从中断处理程序返回用户空间 2.检查标志 内核返回后检查need_resched标志 如果被设置了,内核会选择一个更适合的进程投入进行 3.返回 ...

  9. Linux内核:了解Linux内核抢占

    目录 无强制抢占 可抢占内核 自愿内核抢占 完全实时抢占 在配置Linux内核时,我们可以设置一些影响系统行为的参数.您可以使用不同的优先级,调度类和抢占模型.了解并选择正确的参数非常重要. 在这篇文 ...

最新文章

  1. 漫画:如何在数组中找到和为 “特定值” 的三个数?
  2. 一文读懂 JAVA 异常处理
  3. python的sorted函数用法_Python sorted函数及用法
  4. Sklearn:sklearn.preprocessing之StandardScaler 的transform()函数和fit_transform()函数清晰讲解及其案例应用
  5. 量子力学问题matlab求解,一个关于量子力学中的matlab的问题
  6. esxi 5.5运行linux拯救模式,启用Esxi 5.5 SSH 功能
  7. 07- HTTP协议详解及Fiddler抓包
  8. visio 2013安装教程
  9. Webshell实现与隐藏探究
  10. cad迷你画图 mac
  11. 图像特征点匹配,SIFT及SURF算法的原理及尽量抛开数学问题的浅析(含示例程序,基于VS2013,OpenCV_2.49)
  12. 颁奖 | 阿D给你发年终奖啦~
  13. html打开网页过场动画_动画演示制作软件(高级版)比PPT强太多了
  14. C#开发微信门户及应用(46)-基于Bootstrap的微信门户应用管理系统功能介绍
  15. Scale 编程基础 A(变量声明、基本数据类型、运算符、文件读写、分支与循环、数据结构)
  16. 规定时间间隔刷新UI
  17. 芝村乡大学生投资理财
  18. 手机可以模拟NFC卡片吗?项目中如何解决不能模拟NFC Tag UID的问题
  19. Vss2005安装配置
  20. 苹果小圆点怎么关闭_原来苹果手机点下屏幕就能截屏,用了3年才发现,没用过学一下...

热门文章

  1. 诺基亚和Digia公司将联合推进Qt发展
  2. Visual Studio Online终于公开上线了
  3. 宁波专业SEO,如何让网站快速排名?
  4. 延时队列(Delayed)实现(支持失败重试机制自定义重试时间)
  5. 区块链协议新方向-图灵奖得主Macali开发Algorand
  6. php+mysql商城
  7. SOLIDWORKS带工程图另存为——使之独立的扩展开发
  8. 京东云:AWS模式+数据云+行业方案,目标互联网+转型
  9. 新版chrome中http无法打开摄像头问题解决
  10. 2V升5V的升压芯片,两款芯片电路图