kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

Linux内核获取当前进程指针

一、内存数据表示:

我们在教材或阅读中,经常需要直观的用图示来展示数据在内存中的分布,那么数据是如何在内存中组织的呢?不同的机器有不同的表示法,我们以最常见的Intel X86系列计算机为例来说明这个问题。

如上图示内存示意图:内存低址在上。内存高址在下,内存单位为16bit。对于基于intel i386架构的计算机,系统采用小端字节序来存放数据,所谓小端字节序是指低序字节低地址,高序字节高地址(内存地址增大方向),大端字节序反之,给定系统所用的字节序称为主机字节序;CPU也以小端字节序形式读取数据,如上图所示,如果变量num是16位的short短整类型,则CPU从内存中读出的num=0x1234;如果num是32位的int类型,则CPU从内存中读出的是num=0x56781234,其中num地址是0x12345678,即&num=0x12345678

二、linux内核获取进程任务结构的指针

明白了系统内存数据表示,我们现在来看看linux内核是如何获取当前进程的任务结构指针的,以下代码均参照linux内核2.4.0的源码。
在include\asm-i386\ current.h中

1
2
3
4
5
6
7
8
9
10
11
#ifndef _I386_CURRENT_H
#define _I386_CURRENT_H
struct task_struct;
static inline struct task_struct * get_current(void)
{
	struct task_struct *current;
	__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));
	return current;
}
#define current get_current()
#endif /* !(_I386_CURRENT_H) */

每个进程都有一个task_struct任务结构,和一片用于系统空间堆栈的存储空间,他们在物理内存空间中也是联系在一起的,当给进程申请task_struct任务结构空间时,系统将连同系统的堆栈空间一起分配,如下图为某个进程切换时刻的内存图:

下面针对代码实现来分析一下系统如何通过一系列操作获取进程在内核中的任务结构指针的: 由于linux内核分配进程任务结构空间时,是以8KB(2个页面空间,即21*4KB,linux对物理内存空间和虚拟内存空间管理时,均规定其页面单位的尺寸为4KB)为单位来分配的,所以内存应用地址是8KB(213)的整数倍,即指针地址的低13位全为0,所以根据小端字节序,分配内存返回地址应该是指向struct task_struct结构,如图中的0xc2342000地址所指,至于为何采用代码中的做法而不是直接将此指针保存在全局变量中以供应用,内核是从其自身的效率方面来考虑的,我们在此只针对代码解释: 根据上图,此刻内存esp内容必定在0xc2342000和0xc2344000之间的一个数值,我们假设取0xc2343ffe(即堆栈压栈EIP、返回地址、内部数据等相关数据了,地址值要减小;只要符合0xc2342xxx 、0xc2343xxx的地址指针都是正确的),来通过代码运算看是否current的指针是0xc2342000。

1
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));

语句的意思是将ESP的内容与8191UL的反码按位进行与操作,之后再把结果赋值给current,其中8191UL=8192-1=213-1,计算过程如下:

1
2
3
4
5
6
7
8192UL=2^13 0000 0000 0000 0000 0010 0000 0000 0000
8191UL 0000 0000 0000 0000 0001 1111 1111 1111
~8191UL(反码) 1111 1111 1111 1111 1110 0000 0000 0000
0xc2343ffe 1100 0010 0011 0100 0011 1111 1111 1110 
andl结果: 1100 0010 0011 0100 0010 0000 0000 0000
|| (对照着看)
0x c 2 3 4 2 0 0 0

所以按位与操作之后的结果位0xc2342000,正好是struct task_struct结构的地址指针.通过观察可知,只要符合0xc2342xxx 、0xc2343xxx的地址指针经过相同的计算,都可以得到内核进程任务结构的指针。
另外,在进入中断或系统调用时所引用的宏操作(include\asm-i386\ hw_irq.h):

1
2
3
#define GET_CURRENT \
	"movl %esp, %ebx\n\t" \
	"andl $-8192, %ebx\n\t"

其原理与上述描述也是一致的。

linux内存分配

关于虚拟内存有三点需要注意:

1、4G的进程地址空间被人为的分为两个部分–用户空间与内核空间。用户空间从0到3G(0xc0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。例外情况只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
2、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程变化,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。
3、每个进程的用户空间都是完全独立、互不相干的。

一、4G地址空间解析图

上图展示了整个进程地址空间的分布,其中4G的地址空间分为两部分,在用户空间内,对应了内存分布的五个段:数据段、代码段、BSS段、堆、栈。在上篇文章中有详细的介绍。

二、虚拟地址空间分配及其与物理内存对应图

这个图示内核用户空间的划分,图中最重要的就是高端内存的映射
其中kmalloc和vmalloc函数申请的空间对应着不同的区域,同时又不同的含义。

三、物理内存分配图

这张图中页解释了三者的不同关系,和上篇文章中的内容有相似之处。

伙伴算法:

一种物理内存分配和回收的方法,物理内存所有空闲页都记录在BUDDY链表中。首选,系统建立一个链表,链表中的每个元素代表一类大小的物理内存,分别为2的0次方、1次方、2次方,个页大小,对应4K、8K、16K的内存,没一类大小的内存又有一个链表,表示目前可以分配的物理内存。例如现在仅存需要分配8K的物理内存,系统首先从8K那个链表中查询有无可分配的内存,若有直接分配;否则查找16K大小的链表,若有,首先将16K一分为二,将其中一个分配给进程,另一个插入8K的链表中,若无,继续查找32K,若有,首先把32K一分为二,其中一个16K大小的内存插入16K链表中,然后另一个16K继续一分为二,将其中一个插入8K的链表中,另一个分配给进程……..以此类推。当内存释放时,查看相邻内存有无空闲,若存在两个联系的8K的空闲内存,直接合并成一个16K的内存,插入16K链表中。(伙伴算法用于物理内存分配方案)

SLAB算法:

是一种对伙伴算的一种补充,对于用户进程的内存分配,伙伴算法已经够好了,但对于内核进程,还需要存在一类很小的数据(字节大小,比如进程描述符、虚拟内存描述符等),若每次给几个字节的数据分配一个4KB的页,实在太浪费,于是就有了SLBA算法,SLAB算法其实就是把一个页用力劈成一小块一小块,然后再分配。

Windows XP中硬盘安装ubuntu

1、ubuntu-8.04-desktop-i386.iso 安装镜像
2、grub for dos

安装前的准备工作

1、把ubuntu-8.04-desktop-i386.iso放到win系统根目录下,假设是C盘。
2、用winrar 打开ubuntu-8.04-desktop-i386.iso,提取casper目录内的initrd.gz和vmlinuz两个文件到C根目录下[只是两个文件]。
3、解压缩ubuntu-8.04-desktop-i386.iso的casper目录也解压到C根目录下[整个目录]。
4、打开grub for dos,只取两个文件即可:grldr和menu.lst 将它们同样也放入C根目录下[只是两个文件]。
5、编辑menu.lst文件,在最后加上如下内容:[其他不需要修改]

1
2
3
4
title Install Ubuntu
root (hd0,0)
kernel /vmlinuz boot=casper iso-scan/filename=/ubuntu-8.04-desktop-i386.iso
initrd /initrd.gz

6、编辑 c:\boot.ini 去掉该文件的隐含系统只读属性
windows 下,开始->运行->cmd , 后输入 attrib -r -h -s c:\boot.ini 或者直接右键点击boot.ini文件,把只读去掉
用记事本打开 boot.ini
把 timeout=0 改成 timeout=5
在最后一行添加 C:\grldr=“ubuntu-8.04-desktop-i386” 保存退出即可!
7、重启计算机,在启动菜单位置,选择ubuntu-8.04-desktop-i386,然后选择最下面一个选项:Install Ubuntu就可以进入安装过程了

内核抢占实现机制分析

1 内核抢占概述

2.6新的可抢占式内核是指内核抢占,即当进程位于内核空间时,有一个更高优先级的任务出现时,如果当前内核允许抢占,则可以将当前任务挂起,执行优先级更高的进程。

在2.5.4版本之前,Linux内核是不可抢占的,高优先级的进程不能中止正在内核中运行的低优先级的进程而抢占CPU运行。进程一旦处于核心态(例如 用户进程执行系统调用),则除非进程自愿放弃CPU,否则该进程将一直运行下去,直至完成或退出内核。与此相反,一个可抢占的Linux内核可以让 Linux内核如同用户空间一样允许被抢占。当一个高优先级的进程到达时,不管当前进程处于用户态还是核心态,如果当前允许抢占,可抢占内核的Linux 都会调度高优先级的进程运行。

2 用户抢占

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

简而言之,用户抢占在以下情况时产生: 从系统调返回用户空间。
从中断处理程序返回用户空间。

3 不可抢占内核的特点

在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度—内核中的各任务是协作方式调度的,不具备抢占性。当然,运行于内核态 的进程可以主动放弃CPU,比如,在系统调用服务例程中,由于内核代码由于等待资源而放弃CPU,这种情况叫做计划性进程切换(planned process switch)。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止, 在单CPU情况下,这样的设定大大简化了内核的同步和保护机制。可以分两步对此加以分析:
首先,不考虑进程在内核中自愿放弃CPU的情况(也即在内核中不发生进程的切换)。一个进程一旦进入内核就将一直运行下去,直到完成或退出内核。在其没有 完成或退出内核之前,不会有另外一个进程进入内核,即进程在内核中的执行是串行的,不可能有多个进程同时在内核中运行,这样内核代码设计时就不用考虑多个 进程同时执行所带来的并发问题。Linux的内核开发人员就不用考虑复杂的进程并发执行互斥访问临界资源的问题。当进程在访问、修改内核的数据结构时就不 需要加锁来防止多个进程同时进入临界区。这时只需再考虑一下中断的情况,若有中断处理例程也有可能访问进程正在访问的数据结构,那么进程只要在进入临界区 前先进行关中断操作,退出临界区时进行开中断操作就可以了。
再考虑一下进程自愿放弃CPU的情况。因为对CPU的放弃是自愿的、主动的,也就意味着进程在内核中的切换是预先知道的,不会出现在不知道的情况下发生进 程的切换。这样就只需在发生进程切换的地方考虑一下多个进程同时执行所可能带来的并发问题,而不必在整个内核范围内都要考虑进程并发执行问题。

4 为什么需要内核抢占?

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

这对于那些要求高实时响应的系统是不能接受的。而可抢占的内核不仅对Linux的实时应用至关重要,而且能解决Linux对多媒体(video, audio)等要求低延迟的应用支持不够好的缺陷。

由于可抢占内核的重要性,在Linux2.5.4版本发布时,可抢占被并入内核,同SMP一样作为内核的一项标准可选配置。

5 什么情况不允许内核抢占

有几种情况Linux内核不应该被抢占,除此之外Linux内核在任意一点都可被抢占。这几种情况是:
? 内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。
? 内核正在进行中断上下文的Bottom Half(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。
? 内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系 统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占,否则由于抢占将导致其他CPU长期不能获得锁而死等。
? 内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。
? 内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的 per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到 其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占。

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

从中断返回内核空间的时候,内核会检查need_resched和preempt_count的值。如果need resched被设置,并且preempt count为0的话,这说明可能有一个更为重要的任务需要执行并且可以安全地抢占,此时,调度程序就会被调用。如果preempt-count不为0,则 说明内核现在处干不可抢占状态,不能进行重新调度。这时,就会像通常那样直接从中断返回当前执行进程。如果当前进程持有的所有的锁都被释放了,那么 preempt count就会重新为0。此时,释放锁的代码会检查need_ resched是否被设置。如果是的话,就会调用调度程序。

6 内核抢占时机

在2.6版的内核中,内核引入了抢占能力;现在,只要重新调度是安全的,那么内核就可以在任何时间抢占正在执行的任务。
那么,什么时候重新调度才是安全的呢?只要premptcount为0,内核就可以进行抢占。通常锁和中断是非抢占区域的标志。由于内核是支持SMP的,所以,如果没有持有锁,那么正在执行的代码就是可重新导人的,也就是可以抢占的。
如果内核中的进程被阻塞了,或它显式地调用了schedule(),内核抢占也会显式地发生。这种形式的内核抢占从来都是受支持的(实际上是主动让出 CPU),因为根本无需额外的逻辑来保证内核可以安全地被抢占。如果代码显式的调用了schedule(),那么它应该清楚自己是可以安全地被抢占的。

内核抢占可能发生在:
当从中断处理程序正在执行,且返回内核空间之前。
当内核代码再一次具有可抢占性的时候,如解锁及使能软中断(local_bh_enable)等。
如果内核中的任务显式的调用schedule()
如果内核中的任务阻塞(这同样也会导致调用schedule())

7 如何支持抢占内核

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

我们说可抢占Linux内核在内核的任一点可被抢占,主要就是因为在任意一点中断都有可能发生,每当中断发生,Linux可抢占内核在处理完中断返回时都 会进行内核的可抢占判断。若内核当前所处状态允许被抢占,内核都会重新进行调度选取高优先级的进程运行。这一点是与非可抢占的内核不一样的。在非可抢占的 Linux内核中,从硬件中断返回时,只有当前被中断进程是用户态进程时才会重新调度,若当前被中断进程是核心态进程,则不进行调度,而是恢复被中断的进 程继续运行。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/////////////////////////////////////////////////////////////////////////
/linux+v2.6.19/kernel/spinlock.c
 320void __lockfunc _spin_unlock(spinlock_t *lock)
 321{
 322        spin_release(&lock->dep_map, 1, _RET_IP_);
 323        _raw_spin_unlock(lock);
 324        preempt_enable();
 325}
 326EXPORT_SYMBOL(_spin_unlock);
 178void __lockfunc _spin_lock(spinlock_t *lock)
 179{
 180        preempt_disable();
 181        spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
 182        _raw_spin_lock(lock);
 183}
 184
 185EXPORT_SYMBOL(_spin_lock);
/////////////////////////////////////////////////////////////////////////
  29#define preempt_disable() /
  30do { /
  31        inc_preempt_count(); /
  32        barrier(); /
  33} while (0)
  34
  35#define preempt_enable_no_resched() /
  36do { /
  37        barrier(); /
  38        dec_preempt_count(); /
  39} while (0)
  40
  41#define preempt_check_resched() /
  42do { /
  43        if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) /
  44                preempt_schedule(); /
  45} while (0)
  46
  47#define preempt_enable() /
  48do { /
  49        preempt_enable_no_resched(); /
  50        barrier(); /
  51        preempt_check_resched(); /
  52} while (0)
  53

另外一种可抢占内核实现方案是在内核代码段中插入抢占点(preemption point)的方案。在这一方案中,首先要找出内核中产生长延迟的代码段,然后在这一内核代码段的适当位置插入抢占点,使得系统不必等到这段代码执行完就 可重新调度。这样对于需要快速响应的事件,系统就可以尽快地将服务进程调度到CPU运行。抢占点实际上是对进程调度函数的调用,代码如下:
if (current->need_ resched) schedule();
通常这样的代码段是一个循环体,插入抢占点的方案就是在这一循环体中不断检测need_ resched的值,在必要的时候调用schedule()令当前进程强行放弃CPU

8 何时需要重新调度

内核必须知道在什么时候调用schedule()。如果仅靠用户程序代码显式地调用schedule(),它们可能就会永远地执行下去。相反,内核提供了 一个need_resched标志来表明是否需要重新执行一次调度。当某个进程耗尽它的时间片时,scheduler tick()就会设置这个标志;当一个优先级高的进程进入可执行状态的时候,try_to_wake_up也会设置这个标志。
set tsk_need_resched:设置指定进程中的need resched标志
clear tsk need_resched:清除指定进程中的need resched标志
need_resched():检查need
resched标志的值;如果被设置就返回真,否则返回假信号量、等到队列、completion等机制唤醒时都是基于waitqueue的,而waitqueue的唤醒函数为default_wake_function,其调用try_to_wake_up将进程更改为可运行状态并置待调度标志。

在返回用户空间以及从中断返回的时候,内核也会检查need_resched标志。如果已被设置,内核会在继续执行之前调用调度程序。
每个进程都包含一个need_resched标志,这是因为访问进程描述符内的数值要比访问一个全局变量快(因为current宏速度很快并且描述符通常 都在高速缓存中)。在2.2以前的内核版本中,该标志曾经是一个全局变量。2.2到2.4版内核中它在task_struct中。而在2.6版中,它被移 到thread_info结构体里,用一个特别的标志变量中的一位来表示。可见,内核开发者总是在不断改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/linux+v2.6.19/include/linux/sched.h
1503static inline void set_tsk_need_resched(struct task_struct *tsk)
1504{
1505        set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
1506}
1507
1508static inline void clear_tsk_need_resched(struct task_struct *tsk)
1509{
1510        clear_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
1511}
1512
1513static inline int signal_pending(struct task_struct *p)
1514{
1515        return unlikely(test_tsk_thread_flag(p,TIF_SIGPENDING));
1516}
1517 
1518static inline int need_resched(void)
1519{
1520        return unlikely(test_thread_flag(TIF_NEED_RESCHED));
1521}
///////////////////////////////////////////////////////////////////////////////
/linux+v2.6.19/kernel/sched.c
 991/*
 992 * resched_task - mark a task 'to be rescheduled now'.
 993 *
 994 * On UP this means the setting of the need_resched flag, on SMP it
 995 * might also involve a cross-CPU call to trigger the scheduler on
 996 * the target CPU.
 997 */
 998#ifdef CONFIG_SMP
 999
1000#ifndef tsk_is_polling
1001#define tsk_is_polling(t) test_tsk_thread_flag(t, TIF_POLLING_NRFLAG)
1002#endif
1003
1004static void resched_task(struct task_struct *p)
1005{
1006        int cpu;
1007
1008        assert_spin_locked(&task_rq(p)->lock);
1009
1010        if (unlikely(test_tsk_thread_flag(p, TIF_NEED_RESCHED)))
1011                return;
1012
1013        set_tsk_thread_flag(p, TIF_NEED_RESCHED);
1014
1015        cpu = task_cpu(p);
1016        if (cpu == smp_processor_id())
1017                return;
1018
1019        /* NEED_RESCHED must be visible before we test polling */
1020        smp_mb();
1021        if (!tsk_is_polling(p))
1022                smp_send_reschedule(cpu);
1023}
1024#else
1025static inline void resched_task(struct task_struct *p)
1026{
1027        assert_spin_locked(&task_rq(p)->lock);
1028        set_tsk_need_resched(p);
1029}
1030#endif
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
1366/***
1367 * try_to_wake_up - wake up a thread
1368 * @p: the to-be-woken-up thread
1369 * @state: the mask of task states that can be woken
1370 * @sync: do a synchronous wakeup?
1371 *
1372 * Put it on the run-queue if it's not already there. The "current"
1373 * thread is always on the run-queue (except when the actual
1374 * re-schedule is in progress), and as such you're allowed to do
1375 * the simpler "current->state = TASK_RUNNING" to mark yourself
1376 * runnable without the overhead of this.
1377 *
1378 * returns failure only if the task is already active.
1379 */
1380static int try_to_wake_up(struct task_struct *p, unsigned int state, int sync)
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
1538int fastcall wake_up_process(struct task_struct *p)
1539{
1540        return try_to_wake_up(p, TASK_STOPPED | TASK_TRACED |
1541                                 TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE, 0);
1542}
1543EXPORT_SYMBOL(wake_up_process);
1545int fastcall wake_up_state(struct task_struct *p, unsigned int state)
1546{
1547        return try_to_wake_up(p, state, 0);
1548}
1616/*
1617 * wake_up_new_task - wake up a newly created task for the first time.
1618 *
1619 * This function will do some initial scheduler statistics housekeeping
1620 * that must be done for every newly created context, then puts the task
1621 * on the runqueue and wakes it.
1622 */
1623void fastcall wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
3571/*
3572 * The core wakeup function.  Non-exclusive wakeups (nr_exclusive == 0) just
3573 * wake everything up.  If it's an exclusive wakeup (nr_exclusive == small +ve
3574 * number) then we wake all the non-exclusive tasks and one exclusive task.
3575 *
3576 * There are circumstances in which we can try to wake a task which has already
3577 * started to run but is not in state TASK_RUNNING.  try_to_wake_up() returns
3578 * zero in this (rare) case, and we handle it by continuing to scan the queue.
3579 */
3580static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
3581                             int nr_exclusive, int sync, void *key)
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
3595/**
3596 * __wake_up - wake up threads blocked on a waitqueue.
3597 * @q: the waitqueue
3598 * @mode: which threads
3599 * @nr_exclusive: how many wake-one or wake-many threads to wake up
3600 * @key: is directly passed to the wakeup function
3601 */
3602void fastcall __wake_up(wait_queue_head_t *q, unsigned int mode,
3603                        int nr_exclusive, void *key)
3604{
3605        unsigned long flags;
3606
3607        spin_lock_irqsave(&q->lock, flags);
3608        __wake_up_common(q, mode, nr_exclusive, 0, key);
3609        spin_unlock_irqrestore(&q->lock, flags);
3610}
3611EXPORT_SYMBOL(__wake_up);
3564int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
3565                          void *key)
3566{
3567        return try_to_wake_up(curr->private, mode, sync);
3568}
3569EXPORT_SYMBOL(default_wake_function);
3652void fastcall complete(struct completion *x)
3653{
3654        unsigned long flags;
3655
3656        spin_lock_irqsave(&x->wait.lock, flags);
3657        x->done++;
3658        __wake_up_common(&x->wait, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE,
3659                         1, 0, NULL);
3660        spin_unlock_irqrestore(&x->wait.lock, flags);
3661}
3662EXPORT_SYMBOL(complete);

9 参考资料

请解释抢占式内核与非抢占式内核的区别联系,http://oldlinux.org/oldlinux/viewthread.php?tid=3024

抢占式内核中的锁问题,http://hi.baidu.com/juventus/blog/item/a71c8701960454d2277fb5f0.html

http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=610932&page=

http://linux.chinaunix.net/bbs/viewthread.php?tid=912039

Linux kernel design and development

Linux抢占式内核就是由Robert Love修改实现的。在他的书中有如下描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
User Preemption
User preemption occurs when the kernel is about to return to user-space, 
need_resched is set, and therefore, the scheduler is invoked. 
If the kernel is returning to user-space, it knows it is in a safe quiescent state. 
In other words, if it is safe to continue executing the current task, 
it is also safe to pick a new task to execute. Consequently, 
whenever the kernel is preparing to return to user-space either on return from 
an interrupt or after a system call, the value of need_resched is checked. 
If it is set, the scheduler is invoked to select a new (more fit) process to execute. 
Both the return paths for return from interrupt and return from system call 
are architecture dependent and typically implemented in assembly in entry.S 
(which, aside from kernel entry code, also contains kernel exit code).
In short, user preemption can occur
When returning to user-space from a system call
When returning to user-space from an interrupt handler
Kernel Preemption

The Linux kernel, unlike most other Unix variants and many other operating systems, 
is a fully preemptive kernel. In non-preemptive kernels, kernel code runs until completion. 
That is, the scheduler is not capable of rescheduling a task while it is in the kernel. 
kernel code is scheduled cooperatively, not preemptively. 

Kernel code runs until it finishes (returns to user-space) or explicitly blocks. 
In the 2.6 kernel, however, the Linux kernel became preemptive: 
It is now possible to preempt a task at any point, so long as the kernel is in a state in which it is safe to reschedule.

So when is it safe to reschedule? The kernel is capable of preempting a task 
running in the kernel so long as it does not hold a lock. That is, locks are used as 
markers of regions of non-preemptibility. Because the kernel is SMP-safe, 
if a lock is not held, the current code is reentrant and capable of being preempted.

The first change in supporting kernel preemption was the addition of a preemption counter, 
preempt_count, to each process's thread_info. This counter begins at zero and increments 
once for each lock that is acquired and decrements once for each lock that is released. 
When the counter is zero, the kernel is preemptible. Upon return from interrupt, 
if returning to kernel-space, the kernel checks the values of need_resched and preempt_count. 

If need_resched is set and preempt_count is zero, then a more important task is runnable and 
it is safe to preempt. Thus, the scheduler is invoked. If preempt_count is nonzero, 
a lock is held and it is unsafe to reschedule. In that case, the interrupt returns 
as usual to the currently executing task. When all the locks that the current task is holding are released, 
preempt_count returns to zero. At that time, the unlock code checks whether need_resched is set. 
If so, the scheduler is invoked. Enabling and disabling kernel preemption is sometimes 
required in kernel code and is discussed in Chapter 9
.
Kernel preemption can also occur explicitly, when a task in the kernel blocks or explicitly calls schedule(). 
This form of kernel preemption has always been supported because no additional logic is 
required to ensure that the kernel is in a state that is safe to preempt. 
It is assumed that the code that explicitly calls schedule() knows it is safe to reschedule.

Kernel preemption can occur
When an interrupt handler exits, before returning to kernel-space
When kernel code becomes preemptible again
If a task in the kernel explicitly calls schedule()
If a task in the kernel blocks (which results in a call to schedule())

利用kexec快速切换内核

kexec是一个用于在当前系统下快速切换到另一个内核的一种办法,它采用了一定的机制略过了硬件的初始化,所以切换速度会很快。

自2.6.13以后,Linux内核就已经自置了kexec,而Debian采用的内核已经是2.6.26,而且默认就支持kexec,所以在Debian下我们只要安装kexec-tools就行了。

1
2
$ yum install kexec-tools
$ sudo apt-get install kexec-tools

安装好以后,就可以开始加载其他的内核了。
先看看我有哪些内核可以用:

1
2
3
$ ls /boot/vmlinuz-2.6.26-1-*
/boot/vmlinuz-2.6.26-1-amd64         
/boot/vmlinuz-2.6.26-1-vserver-amd64

好多好多,再看看当前的内核

1
2
$ uname -r
2.6.26-1-vserver-amd64

好了,现在我打算切换到2.6.26-1-amd64去:
记得,需要root权限的

1
$ sudo -s

先要用kexec加载它,先看看该追加哪些参数

1
2
3
4
$ cat /boot/grub/menu.lst | grep 2.6.26-1-amd64
title Debian GNU/Linux, kernel 2.6.26-1-amd64
kernel /vmlinuz-2.6.26-1-amd64 root=/dev/sda1 ro
initrd /initrd.img-2.6.26-1-amd64

找到了,对照上面开始用kexec加载了

1
$ kexec -l /boot/vmlinuz-2.6.26-1-amd64 --initrd=/boot/initrd.img-2.6.26-1-amd64 --append="root=/dev/sda1 ro"

加载以后并不直接执行哦,所以我们要执行一下才会切换

1
$ kexec -e

不要紧张,等一下下就好了,起来以后还会提示登录的
看看我的效果:

1
2
$ uname -r
2.6.26-1-amd64

切换到我想要的内核了