kk Blog —— 通用基础

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

SMP、NUMA体系结构

http://www.cnblogs.com/yubo/archive/2010/04/23/1718810.html

blog.csdn.net/skymanwu/article/details/7551670

从系统架构来看,目前的商用服务器大体可以分为三类,即对称多处理器结构 (SMP : Symmetric Multi-Processor) ,非一致存储访问结构 (NUMA : Non-Uniform Memory Access) ,以及海量并行处理结构 (MPP : Massive Parallel Processing) 。它们的特征分别描述如下:

1. SMP(Symmetric Multi-Processor)

SMP (Symmetric Multi Processing),对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。Access to RAM is serialized; this and cache coherency issues causes performance to lag slightly behind the number of additional processors in the system.

所谓对称多处理器结构,是指服务器中多个 CPU 对称工作,无主次或从属关系。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。

SMP 服务器的主要特征是共享,系统中所有资源 (CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成 SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着 CPU 数量的增加,内存访问冲突将迅速增加,最终会造成 CPU 资源的浪费,使 CPU 性能的有效性大大降低。实验证明, SMP 服务器 CPU 利用率最好的情况是 2 至 4 个 CPU 。

2. NUMA(Non-Uniform Memory Access)

由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA 就是这种努力下的结果之一。利用 NUMA 技术,可以把几十个 CPU( 甚至上百个 CPU) 组合在一个服务器内。其 CPU 模块结构如图 2 所示:


图 2.NUMA 服务器 CPU 模块结构

NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。

利用 NUMA 技术,可以较好地解决原来 SMP 系统的扩展问题,在一个物理服务器内可以支持上百个 CPU 。比较典型的 NUMA 服务器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。

但 NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。如 HP 公司发布 Superdome 服务器时,曾公布了它与 HP 其它 UNIX 服务器的相对性能值,结果发现, 64 路 CPU 的 Superdome (NUMA 结构 ) 的相对性能值是 20 ,而 8 路 N4000( 共享的 SMP 结构 ) 的相对性能值是 6.3 。从这个结果可以看到, 8 倍数量的 CPU 换来的只是 3 倍性能的提升。


用户态到内核态切换

http://www.cnblogs.com/justcxtoworld/p/3155741.html

本文将主要研究在X86体系下Linux系统中用户态到内核态切换条件,及切换过程中内核栈和任务状态段TSS在中断机制/任务切换中的作用及相关寄存器的变化。

一、用户态到内核态切换途径:

1:系统调用 2:中断   3:异常

对应代码,在3.3内核中,可以在/arch/x86/kernel/entry_32.S文件中查看。

二、内核栈

内核栈:Linux中每个进程有两个栈,分别用于用户态和内核态的进程执行,其中的内核栈就是用于内核态的堆栈,它和进程的task_struct结构,更具体的是thread_info结构一起放在两个连续的页框大小的空间内。

在内核源代码中使用C语言定义了一个联合结构方便地表示一个进程的thread_info和内核栈:

此结构在3.3内核版本中的定义在include/linux/sched.h文件的第2106行:

1
2
3
4
2016  union thread_union {
2017          struct thread_info thread_info;
2018          unsigned long stack[THREAD_SIZE/sizeof(long)];
2019     };        

其中thread_info结构的定义如下:

3.3内核 /arch/x86/include/asm/thread_info.h文件第26行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 26   struct thread_info {
 27         struct task_struct      *task;          /* main task structure */
 28         struct exec_domain      *exec_domain;   /* execution domain */
 29         __u32                   flags;          /* low level flags */
 30         __u32                   status;         /* thread synchronous flags */
 31         __u32                   cpu;            /* current CPU */
 32         int                     preempt_count;  /* 0 => preemptable,
 33                                                    <0 => BUG */
 34         mm_segment_t            addr_limit;
 35         struct restart_block    restart_block;
 36         void __user             *sysenter_return;
 37 #ifdef CONFIG_X86_32
 38         unsigned long           previous_esp;   /* ESP of the previous stack in
 39                                                    case of nested (IRQ) stacks
 40                                                 */
 41         __u8                    supervisor_stack[0];
 42 #endif
 43         unsigned int            sig_on_uaccess_error:1;
 44         unsigned int            uaccess_err:1;  /* uaccess failed */
 45 };

它们的结构图大致如下:

esp寄存器是CPU栈指针,存放内核栈栈顶地址。在X86体系中,栈开始于末端,并朝内存区开始的方向增长。从用户态刚切换到内核态时,进程的内核栈总是空的,此时esp指向这个栈的顶端。

在X86中调用int指令型系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。所以在进入内核态之前要保存进程的上下文,中断结束后恢复进程上下文,那靠的就是内核栈。

这里有个细节问题,就是要想在内核栈保存用户态的esp,eip等寄存器的值,首先得知道内核栈的栈指针,那在进入内核态之前,通过什么才能获得内核栈的栈指针呢?答案是:TSS

三、TSS

X86体系结构中包括了一个特殊的段类型:任务状态段(TSS),用它来存放硬件上下文。TSS反映了CPU上的当前进程的特权级。

linux为每一个cpu提供一个tss段,并且在tr寄存器中保存该段。

在从用户态切换到内核态时,可以通过获取TSS段中的esp0来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的cs,esp,eip等上下文。

注:linux中之所以为每一个cpu提供一个tss段,而不是为每个进程提供一个tss段,主要原因是tr寄存器永远指向它,在任务切换的适合不必切换tr寄存器,从而减小开销。

下面我们看下在X86体系中Linux内核对TSS的具体实现:

内核代码中TSS结构的定义:

3.3内核中:/arch/x86/include/asm/processor.h文件的第248行处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
248   struct tss_struct {
249         /*
250          * The hardware state:
251          */
252         struct x86_hw_tss       x86_tss;
253 
254         /*
255          * The extra 1 is there because the CPU will access an
256          * additional byte beyond the end of the IO permission
257          * bitmap. The extra byte must be all 1 bits, and must
258          * be within the limit.
259          */
260         unsigned long           io_bitmap[IO_BITMAP_LONGS + 1];
261 
262         /*
263          * .. and then another 0x100 bytes for the emergency kernel stack:
264          */
265         unsigned long           stack[64];
266 
267 } ____cacheline_aligned;    

其中主要的内容是:
硬件状态结构: x86_hw_tss
IO权位图:     io_bitmap
备用内核栈:    stack

其中硬件状态结构:其中在32位X86系统中x86_hw_tss的具体定义如下:

/arch/x86/include/asm/processor.h文件中第190行处:

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
190#ifdef CONFIG_X86_32
191 /* This is the TSS defined by the hardware. */
192 struct x86_hw_tss {
193         unsigned short          back_link, __blh;
194         unsigned long           sp0;              //当前进程的内核栈顶指针
195         unsigned short          ss0, __ss0h;       //当前进程的内核栈段描述符
196         unsigned long           sp1;
197         /* ss1 caches MSR_IA32_SYSENTER_CS: */
198         unsigned short          ss1, __ss1h;
199         unsigned long           sp2;
200         unsigned short          ss2, __ss2h;
201         unsigned long           __cr3;
202         unsigned long           ip;
203         unsigned long           flags;
204         unsigned long           ax;
205         unsigned long           cx;
206         unsigned long           dx;
207         unsigned long           bx;
208         unsigned long           sp;            //当前进程用户态栈顶指针
209         unsigned long           bp;
210         unsigned long           si;
211         unsigned long           di;
212         unsigned short          es, __esh;
213         unsigned short          cs, __csh;
214         unsigned short          ss, __ssh;
215         unsigned short          ds, __dsh;
216         unsigned short          fs, __fsh;
217         unsigned short          gs, __gsh;
218         unsigned short          ldt, __ldth;
219         unsigned short          trace;
220         unsigned short          io_bitmap_base;
221 
222 } __attribute__((packed));

linux的tss段中只使用esp0和iomap等字段,并且不用它的其他字段来保存寄存器,在一个用户进程被中断进入内核态的时候,从tss中的硬件状态结构中取出esp0(即内核栈栈顶指针),然后切到esp0,其它的寄存器则保存在esp0指的内核栈上而不保存在tss中。

每个CPU定义一个TSS段的具体实现代码:

3.3内核中/arch/x86/kernel/init_task.c第35行:

1
2
3
4
5
6
7
8
 35  * per-CPU TSS segments. Threads are completely 'soft' on Linux,
 36  * no more per-task TSS's. The TSS size is kept cacheline-aligned
 37  * so they are allowed to end up in the .data..cacheline_aligned
 38  * section. Since TSS's are completely CPU-local, we want them
 39  * on exact cacheline boundaries, to eliminate cacheline ping-pong.
 40  */

 41 DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;

INIT_TSS的定义如下:

3.3内核中 /arch/x86/include/asm/processor.h文件的第879行:

1
2
3
4
5
6
7
8
9
879 #define INIT_TSS  {                                                       \
880         .x86_tss = {                                                      \
881                 .sp0            = sizeof(init_stack) + (long)&init_stack, \
882                 .ss0            = __KERNEL_DS,                            \
883                 .ss1            = __KERNEL_CS,                            \
884                 .io_bitmap_base = INVALID_IO_BITMAP_OFFSET,               \
885          },                                                               \
886         .io_bitmap              = { [0 ... IO_BITMAP_LONGS] = ~0 },       \
887 }

其中init_stack是宏定义,指向内核栈:

1
61 #define init_stack              (init_thread_union.stack)

这里可以看到分别把内核栈栈顶指针、内核代码段、内核数据段赋值给TSS中的相应项。从而进程从用户态切换到内核态时,可以从TSS段中获取内核栈栈顶指针,进而保存进程上下文到内核栈中。

总结、有了上面的一些准备,现总结在进程从用户态到内核态切换过程中,Linux主要做的事:

1:读取tr寄存器,访问TSS段
2:从TSS段中的sp0获取进程内核栈的栈顶指针
3:由控制单元在内核栈中保存当前eflags,cs,ss,eip,esp寄存器的值。
4:由SAVE_ALL保存其寄存器的值到内核栈
5:把内核代码选择符写入CS寄存器,内核栈指针写入ESP寄存器,把内核入口点的线性地址写入EIP寄存器

此时,CPU已经切换到内核态,根据EIP中的值开始执行内核入口点的第一条指令。

进程切换过程分析

http://blog.csdn.net/nkguohao/article/details/9187381

参考《深入理解Linux内核(第三版)》

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换,任务切换或上下文切换。下面几节描述在Linux中进行进程切换的主要内容。

硬件上下文

尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此要恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。

进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存在TSS段,而剩余部分存放在内核态的堆栈中。

在下面的描述中,我们假定用prev局部变量表示切换出的进程的描述符,next表示切换进的进程的描述符。因此,我们把进程切换定义为这样的行为:保存prev硬件上下文,用next硬件上下文代替prev。因为进程切换经常发生,因此减少和装入硬件上下文所花费的时间是非常重要的。

早期的Linux版本利用80x86体系结构所提供的硬件支持,并通过far jmp指令跳到进程TSS描述符的选择符来执行进程切换。当执行这条指令时,CPU通过自动保存原来的硬件上下文,装入新的硬件上下文来执行硬件上下文切换。但是基于以下原因,Linux2.6使用软件执行进程切换:

通过一组mov指令逐步执行切换,这样能较好地控制所装入数据的合法性,尤其是,这使检查ds和es段寄存器的值成为可能,这些值有可能被恶意用户伪造。当用单独的farjmp指令时,不可能进行这类检查。

旧方法和新方法所需时间大致相同。然而,尽管当前的切换代码还有改进的余地,却不能对硬件上下文切换进行优化。

进程切换只发生在内核态。在执行进程切换之前,用户态进程所使用的所有寄存器内容已保存在内核态堆栈上,这也包括ss和esp这对寄存器的内容。

任务状态段

80x86体系结构包括一个特殊的段类型,叫任务状态段(Task State Segment, TSS)来存放硬件上下文。尽管Linux并不使用硬件上下文切换,但是强制它为系统中每个不同的CPU创建一个TSS。这样做的两个主要理由为:
当80x86的一个CPU从用户态切换到内核态时,它就从TSS中获取内核态堆栈的地址。
当用户态进程试图通过in或out指令访问一个I/O端口时,CPU需要访问存放在TSS中的I/O许可图以检查该进程是否有访问端口的权力。

更确切地说,当进程在用户态下执行in或out指令时,控制单元执行下列操作:
它检查eflags寄存器中的2位IOPL字段。如果该字段值为3,控制单元就执行I/O指令。否则,执行下一个检查。
访问tr寄存器以确定当前的TSS和相应的I/O许可权位图。
检查I/O指令中指定的I/O端口在I/O许可权位图中对应的位。如果该位清0,这条I/O指令就执行,否则控制单元产生一个”Generalprotetion”异常。

tss_struct结构描述TSS的格式。正如第二章(《深入理解Linux内核(第三版)》)所提到的,init_tss数组为系统上每个不同的CPU存放一个TSS。在每次进程切换时,内核都更新TSS的某些字段以便相应的CPU控制单元可以安全地检索到它需要的信息。因此,TSS反映了CPU上的当前进程的特权级,但不必为没有在运行的进程保留TSS。

每个TSS有它自己8字节的任务状态段描述符。这个描述符包括指向TSS起始地址的32位Base字段,20位Limit字段。TSSD的S标志被清0,以表示相应的TSS是系统段的事实。

Type字段置为11或9以表示这个段实际上是TSS。在Intel的原始设计中,系统中的每个进程都应当指向自己的TSS;Type字段的第二个有效位叫做Busy位;如果进程正由CPU执行,则该位置为1,否则置为0。在Linux的设计中,每个CPU只有一个TSS,因此,Busy位总置为1。

由linux创建的TSSD存放在全局描述符表中。GDT的基地址存放在每个CPU的gdtr寄存器中。每个CPU的tr寄存器包含相应TSS的TSSD选择符,也包括了两个隐藏了非编程字段;TSSD的Base字段和Limit字段。这样,处理器就能直接对TSS寻址而不用从GDT中检索TSS的地址。

Thread字段

在每次进程切换时,被替换进程的硬件上下文必须保存在别处。不能像Intel原始设计那样把它保存在TSS中,因为Linux为每个处理器而不是为每个进程使用TSS。

因此,每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。随后我们会看到,这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如exa、ebx等等这些通用寄存器,它们的值保留在内核堆栈中。

执行进程切换

进程切换可能只发生在精心定义的点:schedule()函数(《深入理解Linux内核(第三版)》第七章有详细讨论)。这里,我们仅关注内核如何执行一个进程切换。

从本质上说,每个进程切换由两步组成:
切换页全局目录以安装一个新的地址空间;将在第九章(《深入理解Linux内核(第三版)》)描述这一步。
切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。

我们又一次假定prev指向被替换进程的描述符,而next指向被激活进程的描述符。prev和next是schedule()函数的局部变量。

switch_to宏

进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的例程之一,要理解它到低做了些什么我们必须下些功夫。

首先,该宏有三个参数,它们是prev,next和last。你可能很容易猜到prev和next的作用:它们仅是局部变量prev和next的占位符,即它们是输入参数,分别表示被替换进程和新进程描述符的地址在内存中的位置。

那第三个参数last呢?在任何进程切换中,涉及到三个进程而不是两个。假设内核决定暂停进程A而激活里程B。在schedule()函数中,prev指向A的描述符而next指向B的描述符。switch_to宏一但使A暂停,A的执行流就冻结。

随后,当内核想再次此激活A,就必须暂停另一个进程C,于是就要用prev指向C而next指向A来执行另一个swithch_to宏。当A恢复它的执行流时,就会找到它原来的内核栈,于是prev局部变量还是指向A的描述符而next指向B的描述符。此时,代表进程A执行的内核就失去了对C的任何引用。但是,事实表明这个引用对于完成进程切换是很有用的。

switch_to宏的最后一个参数是输出参数,它表示宏把进程C的描述符地址写在内存的什么位置了。在进程切换之前,宏把第一个输入参数prev表示的变量的内容存入CPU的eax寄存器。在完成进程切换,A已经恢复执行时,宏把CPU的eax寄存器的内容写入由第三个输出参数——-last所指示的A在内存中的位置。因为CPU寄存器不会在切换点发生变化,所以C的描述符地址也存在内存的这个位置。在schedule()执行过程中,参数last指向A的局部变量prev,所以prev被C的地址覆盖。

图3-7显示了进程A,B,C内核堆栈的内容以及eax寄存器的内容。必须注意的是:图中显示的是在被eax寄存器的内容覆盖以前的prev局部变量的值。

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
#define switch_to(prev, next, last)                 \
32do {                                  \
33  /*                              \
34   * Context-switching clobbers all registers, so we clobber  \
35   * them explicitly, via unused output variables.        \
36   * (EAX and EBP is not listed because EBP is saved/restored \
37   * explicitly for wchan access and EAX is the return value of   \
38   * __switch_to())                       \
39   */                             \
40  unsigned long ebx, ecx, edx, esi, edi;              \
41                                  \
42  asm volatile("pushfl\n\t"       /* save    flags */ \
43           "pushl %%ebp\n\t"      /* save    EBP   */ \
44           "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */ \
45           "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \
46           "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */ \
47           "pushl %[next_ip]\n\t" /* restore EIP   */ \
48           __switch_canary                    \
49           "jmp __switch_to\n"    /* regparm call  */ \
50           "1:\t"                     \
51           "popl %%ebp\n\t"       /* restore EBP   */ \
52           "popfl\n"          /* restore flags */ \
53                                  \
54           /* output parameters */                \
55           : [prev_sp] "=m" (prev->thread.sp),     \
56             [prev_ip] "=m" (prev->thread.ip),     \
57             "=a" (last),                 \
58                                  \
59             /* clobbered output registers: */        \
60             "=b" (ebx), "=c" (ecx), "=d" (edx),      \
61             "=S" (esi), "=D" (edi)               \
62                                      \
63             __switch_canary_oparam               \
64                                  \
65             /* input parameters: */              \
66           : [next_sp]  "m" (next->thread.sp),     \
67             [next_ip]  "m" (next->thread.ip),     \
68                                      \
69             /* regparm parameters for __switch_to(): */  \
70             [prev]     "a" (prev),               \
71             [next]     "d" (next)                \
72                                  \
73             __switch_canary_iparam               \
74                                  \
75           : /* reloaded segment registers */         \
76          "memory");                  \
77} while (0)
78

由于switch_to宏采用扩展的内联汇编语言编码,所以可读性比较差:实际上这段代码通过特殊位置记数法使用寄存器,而实际使用的通用寄存器由编译器自由选择。我们将采用标准汇编语言而不是麻烦的内联汇编语言来描述switch_to宏在80x86微处理器上所完成的典型工作。

在eax和edx寄存器中分别保存prev和next的值。

1
2
movl prev ,%eax
movl next ,%edx

把eflags和ebp寄存器的内容保存在prev内核栈中。必須保存它们的原因是编译器认为在switch_to结束之前它们的值应当保持不变。

1
2
pushf1
push %ebp

把esp的内容保存到prev->thread.esp中以使该字段指向prev内核栈的栈顶:

1
movl %esp, 484(%eax)

把next->thread.esp装入esp.此时,内核开始在next的内核栈上操作,因此这条指令实际上完成了从prev到next的切换。由于进程描述符的地址和内核栈的地址紧挨着,所以改变内核栈意味着改变进程。

1
movl 484(%edx),%esp

把标记为1的地址存入prev->thread.eip。当被替换的进程重新恢复执行时,进程执行被标记为1的那条指令:

1
movl $lf, 480(%eax)

宏把next->thread.eip的值压入next的内核栈。

1
push1 480(%edx)

跳到__switch_to() 函数

1
jmp __switch_to

这里被进程B替换的进程A再次获得CPU;它执行一些保存eflags和ebp寄存器内容的指令,这两条指令的第一条指令被标记为1。

拷贝eax寄存器的内容到switch_to宏的第三个参数lash标识的内存区域中:

1
movl  %eax, last

正如以前讨论的,eax寄存器指向刚被替换的进程描述符。

__switch_to()函数

__switch_to()函数执行大多数开始于switch_to()宏的进程切换。这个函数作用于prev_p和next_p参数,这两个参数表示前一个进程和新进程。这个函数的调用不同于一般函数的调用,因为__switch_to()从eax和edx取参数prev_p和next_p,而不像大多数函数一样从栈中取参数。为了强迫函数从寄存器取它的参数,内核利用__attribute__和regparm关键字,这两个关键字是C语言非标准的扩展名,由gcc编译程序实现。在include/asm-i386/system.h头文件中,__switch_to()函数的声明如下:

1
__switch_to(structtask_struct *prev_p,struct tast_struct *next_p)__attribute_(regparm(2));

函数执行的步骤如下:
1、执行由__unlazy_fpu()宏产生的代码,以有选择地保存prev_p进程的FPU、MMX及XMM寄存器的内容。

1
__unlazy_fpu(prev_p);

2、执行smp_processor_id()宏获得本地(local)CPU的下标,即执行代码的CPU。该宏从当前进程的thread_info结构的cpu字段获得下标将它保存到cpu局部变量。

3、把next_p->thread.esp0装入对应于本地CPU的TSS的esp0字段;将在通过sysenter指令发生系统调用一节看到,以后任何由sysenter汇编指令产生的从用户态到内核态的特权级转换将把这个地址拷贝到esp寄存器中:

1
init_tss[cpu].esp0= next_p->thread.esp0;

4、把next_p进程使用的线程局部存储段装入本地CPU的全局描述符表;三个段选择符保存在进程描述符内的tls_array数组中

1
2
3
cpu_gdt_table[cpu][6]= next_p->thread.tls_array[0];
cpu_gdt_table[cpu][7]= next_p->thread.tls_array[1];
cpu_gdt_table[cpu][8]= next_p->thread.tls_array[2];

5、把fs和gs段寄存器的内容分别存放在prev_p->thread.fs和prev_p->thread.gs中,对应的汇编语言指令是:

1
2
movl%fs,40(%esi)
movl%gs,44(%esi)

6、如果fs或gs段寄存器已经被prev_p或next_p进程中的任意一个使用,则将next_p进程的thread_struct描述符中保存的值装入这些寄存器中。这一步在逻辑上补充了前一步中执行的操作。主要的汇编语言指令如下:

1
2
movl40(%ebx),%fs
movl44(%edb),%gs

7、ebx寄存器指向next_p->thread结构。代码实际上更复杂,因为当它检测到一个无效的段寄存器值时,CPU可能产生一个异常。

8、用next_p->thread.debugreg数组的内容装载dr0,…,dr7中的6个调试寄存器。只有在next_p被挂起时正在使用调试寄存器,这种操作才能进行。这些寄存器不需要被保存,因为只有当一个调试器想要监控prev时prev_p->thread.debugreg才会修改。

1
2
3
4
5
6
7
if(next_p->thread.debugreg[7]){
loaddebug(&next_p->thread,0);
loaddebug(&next_p->thread,1);
loaddebug(&next_p->thread,2);
loaddebug(&next_p->thread,3);
loaddebug(&next_p->thread,6);
loaddebug(&next_p->thread,7);

8、如果必要,更新TSS中的I/O位图。当next_p或prev_p有其自己的定制I/O权限位图时必须这么做:

1
2
if(prev_p->thread.io_bitmap_ptr|| next_p->thread.io_bitmap_ptr )
handle_io_bitmap(&next_p->thread,&init_tss[cpu]);

因为进程很修改I/O权限位图,所以该位图在“懒”模式中被处理;当且仅当一个进程在当前时间片内实际访问I/O端口时,真实位图才被拷贝到本地CPU的TSS中。进程的定制I/O权限位图被保存在thread_info结构的io_bitmap_ptr字段指向的缓冲区中。handle_io_bitmap()函数为next_p进程设置本地CPU使用的TSS的in_bitmap字段如下:
(a)如果next_p进程不拥有自己的I/O权限位图,则TSS的io_bitmap字段被设为0x8000.
(b) 如果next_p进程拥有自己的I/O权限位图,则TSS的io_bitmap字段被设为0x9000。

TSS的io_bitmap字段应当包含一个在TSS中的偏移量,其中存放实际位图。无论何时用户态进程试图访问一个I/O端口,0x8000和0x9000指向TSS界限之外并将因此引起”Generalprotection”异常。do_general_protection()异常处理程序将检查保存在io_bitmap字段的值:如果是0x8000,函数发送一个SIGSEGV信号给用户态进程;如果是0x9000,函数把进程位图拷贝拷贝到本地CPU的TSS中,把io_bitmap字段为实际位图的偏移(104),并强制再一次执行有缺陷的汇编指令。

9、终止。 __switch_to()函数通过使用下列声明结束:

1
return prev_p;

由编译器产生的相应汇编语言指令是:

1
2
movl %edl,%eax
ret

prev_p参数被拷贝到eax,因为缺省情况下任何C函数的返回值被传递给eax寄存器。注意eax的值因此在调用__switch_to()的过程中被保护起来;这非常重要,因为调用switch_to宏时会假定eax总是用来存放被替换的进程描述符的地址。

汇编语言指令ret把栈顶保存的返回地址装入eip程序计数器。不过,通过简单地跳转到__switch_to()函数来调用该函数。因此,ret汇编指令在栈中找到标号为1的指令的地址,其中标号为1的地址是由switch_to()宏推入栈中的。如果因为next_p第一次执行而以前从未被挂起,__switch_to()就找到ret_from_fork()函数的起始地址。

SystemTap---嵌入C代码

  • 访问参数的值是以STAP_ARG_+参数名的形式,返回值STAP_RETVALUE=xxx,这种方式是最新版本的SystemTap中的方式。1.7及更早的版本是通过THIS->+参数名的方式, 返回值THIS->__returnval=xxx

http://www.4byte.cn/learning/53860.html

SystemTap支持guru模式,通过-g选项来以这种模式执行SystemTap脚本。在guru模式下,嵌入的C代码在“%{“和“%}"标记之间,这些代码会原封不动地放到生成的模块中。嵌入的C代码不仅可以作为函数体,还可以出现在SystemTap描述中(例如函数等),示例如下:

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
%{
	#include <linux/in.h>
	#include <linux/ip.h>
%} /* <-- top level */

function read_iphdr:long(skb:long)
%{
	struct iphdr *iph = ip_hdr((struct sk_buff *)STAP_ARG_skb);
	STAP_RETVALUE = (long)iph;
%}

/* Determines whether an IP packet is TCP, based on the iphdr: */
function is_tcp_packet:long(iphdr)
{
	protocol = @cast(iphdr, "iphdr")->protocol
	return (protocol == %{ IPPROTO_TCP %}) /* <-- expression */
}

probe begin {
	printf("SystemTap start!\n");
}

probe kernel.function("ip_local_deliver") {
	iph = read_iphdr(pointer_arg(1));
	printf("tcp packet ? %s\n", is_tcp_packet(iph) ? "yes" : "no");
}

在这里read_iphdr函数就是使用嵌入的C代码作为函数体,is_tcp_packet中是作为systemtap辅助函数中的一部分。

在使用嵌入C代码作为函数体的函数中,访问参数的值是以STAP_ARG_+参数名的形式,这种方式是最新版本的SystemTap中的方式。1.7及更早的版本是通过THIS->+参数名的方式。CentOS6.4中的SystemTap版本是1.8,所以你如果在SystemTap脚本中仍然使用老的访问方式会报错。同样,最新的设置返回值的方式是STAP_RETVALUE,1.7及更早的版本是THIS->__retvalue。

由于在guru模式下,SystemTap对嵌入的C代码没有做任何的处理,所以如果在C代码中出现异常的访问或者其他错误,就会导致内核crash。不过SystemTap提供了kread宏来安全地访问指针,如下所示:

1
2
3
4
struct net_device *dev;
char *name;
dev = kread(&(skb->dev));
name = kread(&(dev->name));

还有一点要特别注意,所有的SystemTap函数和probe都是在关闭中断下执行,所以在所有嵌入的C代码中都不能睡眠!

SystemTap Beginner

http://blog.csdn.net/kafeiflynn/article/details/6429976

SystemTap

应用:

对管理员,SystemTap可用于监控系统性能,找出系统瓶颈,而对于开发者,可以查看他们的程序运行时在linux系统内核内部的运行情况。主要用于查看内核空间事件信息,对用户空间事件的探测,目前正加紧改进。

安装

1、SystemTap的安装及使用需要针对正在使用的内核安装相应的kernel-devel、kernel-debuginfo和kernel-debuginfo-common包,以插入探针。
2、安装SystemTap和SystemTap-runtime包
3、使用如下命令测试一下:

1
stap -v -e 'probe vfs.read {printf("read performed/n"); exit()}'

为目标机产生SystemTap instrumentation:

这样就可以在一台机器上为多种内核产生SystemTap instrumentation,而且目标机上只安装SystemTap-runtime即可。

操作如下:
1.在目标机上安装systemtap-runtime RPM包;
2.使用uname –r查看目标机内核;
3.在host system上安装SystemTap;
4.在host system上安装目标机内核及相关RPMs
5.在host name上运行命令:

1
stap -r kernel_version script -m module_name

6.把新产生的模块拷贝到目标机,并运行如下命令:

1
staprun module_name.ko

注意:host system和目标机架构及操作系统版本必须一致。

运行SystemTap脚本

运行stap和staprun需要被授以权限,一般用户需要运行SystemTap,则需要被加入到以下用户组的一个:
1、stapdev:用stap编译SystemTap脚本成内核模块,并加载进内核;
2、stapusr:仅能运行staprun加载/lib/modules/kernel_version/systemtap/目录下模块。

SystemTap Flight Recorder模式

该模式允许长时间运行SystemTap脚本,但仅focus on 最近的输出,有2个变种:in-memory和file模式,两种情况下SystemTap都作为后台进程运行。

In-memory模式:
1
stap -F iotime.stp

一旦脚本启动后,你可以看到以下输出信息以辅助命令重新连到运行中的脚本:

1
2
Disconnecting from systemtap module.
To reconnect, type "staprun -A stap_5dd0073edcb1f13f7565d8c343063e68_19556"

当感兴趣的事件发生时,可以重新连接到运行中的脚本,并在内存Buffer中输出最近的数据并持续输出:

1
staprun -A stap_5dd0073edcb1f13f7565d8c343063e68_19556

内存Buffer默认1MB,可以使用-S选项,例如-S2指定为2MB

1
2
File Flight Recorder
stap -F -o /tmp/pfaults.log -S 1,2  pfaults.stp

命令结果输出到/tmp/pfaults.log.[0-9],每个文件1MB,并且仅保存最近的两个文件,-S指定了第一个参数:每个输出文件大小1MB,第二个参数:仅保留最近的两个文件,systemtap在pfaults.log后面加.[0-9]后缀。

该命令的输出是systemtap脚本进程ID,使用如下命令可以终止systemtap脚本

1
kill -s SIGTERM 7590

运行

1
2
3
ls –sh /tmp/pfaults.log.*

1020K /tmp/pfaults.log.5    44K /tmp/pfaults.log.6

SystemTap如何工作

SystemTap的基本工作原理就是:event/handler,运行systemtap脚本产生的加载模块时刻监控事件的发生,一旦发生,内核就调用相关的handler处理。

一运行一个SystemTap脚本就会产生一个SystemTap session:
1.SystemTap检查脚本以及所使用的相关tapset库;
2.SystemTap将脚本转换成C语言文件,并运行C语言编译器编译之创建一个内核模块;
3.SystemTap加载该模块,从而使用所有探针(events和handlers);
4.事件发生时,执行相关handlers
5.一旦SystemTap session停止,则探针被禁止,该内核模块被卸载。

探针:event及其handler,一个SystemTap脚本可以包含多个探针。

SystemTap脚本以.stp为扩展名,其基本格式如下所示:

1
probe event {statements}

允许一个探针内多个event,以,隔开,任一个event发生时,都会执行statements,各个语句之间不需要特殊的结束符号标记。而且可以在一个statements block中包含其他的statements block。

函数编写:

1
2
3
function function_name(arguments) {statements}

probe event {function_name(arguments)}

SystemTap Event

可大致划分为synchronous和asynchronous。

同步事件:

执行到定位到内核代码中的特定位置时触发event

1.syscall.system_call
系统调用入口和exit处:syscall.system_call和syscall.system_call.return,比如对于close系统调用:syscall.close和syscall.close.return

2.vfs.file_operation
vfs.file_operation和vfs.file_operation.return

3.kernel.function(“function”)
如:kernel.function(“sys_open”)和kernel.function(“sys_open”).return

可使用*来代表wildcards:

1
2
probe kernel.function("*@net/socket.c") { }
probe kernel.function("*@net/socket.c").return { }

代表了net/socket.c中所有函数的入口和exit口。

4.kernel.trace(“tracepoint”)
2.6.30及newer为内核中的特定事件定义了instrumentation,入kernel.trace(“kfree_skb”)代表内核中每次网络buffer被释放掉时的event。

5.module(“module”).function(“function”)

1
2
probe module("ext3").function("*") { }
probe module("ext3").function("*").return { }

系统内核模块多存放在/lib/modules/kernel_version

Asynchronous Events

不绑定到内核的特定指令或位置处。包括:
1、begin:SystemTap session开始时触发,当SystemTap脚本开始运行时触发;
2、end :SystemTap session终止时触发;
3、timer事件:

1
2
3
4
probe timer.s(4)
{
	printf("hello world/n")
}

• timer.ms(milliseconds)
• timer.us(microseconds)
• timer.ns(nanoseconds)
• timer.hz(hertz)
• timer.jiffies(jiffies)

可查看man stapprobes来查看其它支持的events

SystemTap Handler/Body

支持的函数:
1、 printf (“format string/n”, arguments),%s:字符串,%d数字,以 , 隔开;
2、 tid():当前线程ID;
3、 uid():当前用户ID;
4、 cpu():当前CPU号;
5、 gettimeofday_s():自从Epoch开始的秒数;
6、 ctime()将从Unix Epoch开始的秒数转换成date;
7、 pp():描述当前被处理的探针点的字符串;
8、 thread_indent():

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
probe kernel.function("*@net/socket.c")
{
	printf ("%s -> %s/n", thread_indent(1), probefunc())
}

probe kernel.function("*@net/socket.c").return
{
	printf ("%s <- %s/n", thread_indent(-1), probefunc())
}

0 ftp(7223): -> sys_socketcall
1159 ftp(7223):  -> sys_socket
2173 ftp(7223):   -> __sock_create
2286 ftp(7223):    -> sock_alloc_inode
2737 ftp(7223):    <- sock_alloc_inode
3349 ftp(7223):    -> sock_alloc
3389 ftp(7223):    <- sock_alloc
3417 ftp(7223):   <- __sock_create
4117 ftp(7223):   -> sock_create
4160 ftp(7223):   <- sock_create
4301 ftp(7223):   -> sock_map_fd
4644 ftp(7223):    -> sock_map_file
4699 ftp(7223):    <- sock_map_file
4715 ftp(7223):   <- sock_map_fd
4732 ftp(7223):  <- sys_socket
4775 ftp(7223): <- sys_socketcall

函数thread_indent()只有1个参数:代表对线程的”indentation counter”的增减数,即系统调用显示的步数,返回字符串(自从第一次调用thread_indent()以来的描述:进程名(进程ID))

9、name
标记系统调用的名字,仅用于syscall.system_call中。

10、target()
与stap script -x process ID or stap script -c command联合使用,如果想在脚本中获得进程ID或命令可以如此做

1
2
3
4
probe syscall.* {
	if (pid() == target())
		printf("%s/n", name)
}

SystemTap Handler构造

变量

1、不必事先声明,直接使用即可,由SystemTap自动判断其属于string还是integer,整数则默认为0,默认在probe中声明的是local变量
2、在各个probe之间共享的变量使用global声明

1
2
3
4
5
6
7
8
9
10
global count_jiffies, count_ms
probe timer.jiffies(100) { count_jiffies ++ }
probe timer.ms(100) { count_ms ++ }
probe timer.ms(12345)
{
	hz=(1000*count_jiffies) / count_ms
	printf ("jiffies:ms ratio %d:%d => CONFIG_HZ=%d/n",
		count_jiffies, count_ms, hz)
	exit()
}
Target变量

Probe event可以映射到代码的实际位置,如kernel.function(“function”)、kernel.statement(“statement”),这允许使用target变量来记录代码中指定位置处可视变量的值。

运行如下命令:可以显示指定vfs_read处可视target变量

1
stap -L 'kernel.function("vfs_read")'

显示

1
2
3
kernel.function("vfs_read@fs/read_write.c:277") $file:struct file* $buf:char* $count:size_t

$pos:loff_t*

每个target变量以$开头:变量类型。如果是结构体类型,则SystemTap可以使用->来查看其成员。对基本类型,integer或string,SystemTap有函数可以直接读取address处的值,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 好像有时对于小于8位的函数,会取出8为长度的值
kernel_char(address)
Obtain the character at address from kernel memory.

kernel_short(address)
Obtain the short at address from kernel memory.

kernel_int(address)
Obtain the int at address from kernel memory.

kernel_long(address)
Obtain the long at address from kernel memory

kernel_string(address)
Obtain the string at address from kernel memory.

kernel_string_n(address, n)
Obtain the string at address from the kernel memory and limits the string to n bytes.
打印target变量
1
2
3
4
5
6
7
$$vars:类似sprintf("parm1=%x ... parmN=%x var1=%x ... varN=%x", parm1, ..., parmN, var1, ..., varN),目的是打印probe点处的每个变量;

$$locals:$$vars子集,仅打印local变量;

$$parms:$$vars子集,仅包含函数参数;

$$return:仅在return probes存在,类似sprintf("return=%x", $return),如果没有返回值,则是空串

例子如下:

1
stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms); exit(); }'

函数vfs_read有4个参数:file、buf、count和pos,输出如下:

1
file=0xffff8800b40d4c80 buf=0x7fff634403e0 count=0x2004 pos=0xffff8800af96df48

如果你想知道数据结构里面的成员信息,可以在”$$params”后面加一个”$”,如下所示:

1
stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms$); exit(); }'

输出如下:

1
file={.f_u={...}, .f_path={...}, .f_op=0xffffffffa06e1d80, .f_lock={...}, .f_count={...}, .f_flags=34818, buf="" count=8196 pos=-131938753921208

仅一个”$”表示,不展开数据结构域成员,如想展开,则需使用”$$”

1
stap -e 'probe kernel.function("vfs_read") {printf("%s/n", parms); exit(); }'

输出受限于最大字符串大小:

1
file={.f_u={.fu_list={.next=0xffff8801336ca0e8, .prev=0xffff88012ded0840}, .fu_rcuhead={.next=0xffff8801336ca0e8
强制类型转换

大多数情况下,SystemTap都可以从debuginfo中获得变量类型,但对于代码中void指针则debuginfo中类型信息不可用,同样probe handler里面的类型信息在function里面也不可用,怎么办呢?

SystemTap函数参数使用long来代替typed pointer,SystemTap的@cast操作可以指出对象正确类型:

1
2
3
4
function task_state:long (task:long)
{
	return @cast(task, "task_struct", "kernel<linux/sched.h>")->state
}

第一个参数是指向对象的指针, 第二个参数是将该对象(参数1)要强制类型转换成的类型,第三个参数指出类型定义的出处,是可选的。

检查Target变量可用性

随着代码运行,变量可能失效,因此需要用@defined来判断该变量是否可用:

1
2
3
4
5
6
7
8
probe vm.pagefault = kernel.function("__handle_mm_fault@mm/memory.c") ?,

kernel.function("handle_mm_fault@mm/memory.c") ?
{
	name = "pagefault"
	write_access = (@defined($flags) ? $flags & FAULT_FLAG_WRITE : $write_access)
	address = $address
}
条件语句
1
2
3
4
if (condition)
	statement1
else
	statement2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global countread, countnonread
probe kernel.function("vfs_read"),kernel.function("vfs_write")
{
	if (probefunc()=="vfs_read")
		countread ++
	else
		countnonread ++
}

probe timer.s(5) { exit() }

probe end
{
	printf("VFS reads total %d/n VFS writes total %d/n", countread, countnonread)
}
循环语句
1
2
3
4
while (condition)
	statement

for (initialization; conditional; increment) statement
比较:
1
==、>=、<=、!=
命令行参数:

使用$标志着希望输入的是integer类型命令行参数,@:string

1
2
probe kernel.function(@1) { }
probe kernel.function(@1).return { }

关联数组

关联数组一般在multiple probes里面处理,所以必须声明为global,不管是在一个还是多个probes里面用,要读取数组成员值,可以:

1
array_name[index_expression]

如下所示:

1
2
3
foo["tom"] = 23
foo["dick"] = 24
foo["harry"] = 25

一个索引可以包含最多9个索引表达式,用 , 隔开:

1
device[pid(),execname(),uid(),ppid(),"W"] = devname
SystemTap的数组操作
赋值:
1
array_name[index_expression] = value

例子:索引和值可以使用handler function:

1
foo[tid()] = gettimeofday_s()

每次触发这个语句,多次后就会构成一个关联数组,如果tid()返回值在foo索引中已有一个,则用新值代替旧值。

读取数组值:
1
delta = gettimeofday_s() - foo[tid()]

如果无法找到指定”索引”对应的值,则数组读返回0(int)或null/empty值(string)

增加关联数组值
1
array_name[index_expression] ++

处理数组的多个成员:

1
2
3
4
5
6
7
8
9
10
11
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	foreach (count in reads)
	printf("%s : %d /n", count, reads[count])
}

这个foreach无序打印所有reads数组值,如果想升序/降序,则需要使用升序(+)、降序(-),也可以限制处理的数组数目:

1
2
3
4
5
probe timer.s(3)
{
	foreach (count in reads- limit 10)
	printf("%s : %d /n", count, reads[count])
}
Clearing/Deleting数组和数组成员
1
2
3
4
5
6
7
8
9
10
11
12
13
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	foreach (count in reads)
	printf("%s : %d /n", count, reads[count])

	delete reads
}

使用delete操作来删除数组成员或整个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
global reads, totalreads
probe vfs.read
{
	reads[execname()] ++
	totalreads[execname()] ++
}

probe timer.s(3)
{
	printf("=======/n")
	foreach (count in reads-)
		printf("%s : %d /n", count, reads[count])
	delete reads
}

probe end
{
	printf("TOTALS/n")
	foreach (total in totalreads-)
	printf("%s : %d /n", total, totalreads[total])
}

在if语句中使用数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	printf("=======/n")
	foreach (count in reads-)
	if (reads[count] >= 1024)
		printf("%s : %dkB /n", count, reads[count]/1024)
	else
		printf("%s : %dB /n", count, reads[count])
}
检查成员

可以检查是否一个指定健是数组键值:

1
if([index_expression] in array_name) statement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
global reads
probe vfs.read
{
	reads[execname()] ++
}

probe timer.s(3)
{
	printf("=======/n")
	foreach (count in reads+)
		printf("%s : %d /n", count, reads[count])
	if(["stapio"] in reads) {
		printf("stapio read detected, exiting/n")
		exit()
	}
}
计算统计集合

统计集合用于收集数值的统计信息,用于计算新值

1
2
3
4
5
global reads
probe vfs.read
{
	reads[execname()] <<< count
}

操作符<<<用于将count返回的值存放在read数组中execname()相关的值中,即一个键值关联多个相关值。

为计算统计信息,使用@extractor(variable/array index expression),extractor可以是如下integer extractor:

1
2
3
4
5
count:@count(writes[execname()])返回存放在writes数组中某单一键值对应的值数目;
sum:@sum(writes[execname()])返回在writes数组中某单一键值对应的值的和
min:最小值
max:最大值
avg:variable/array作为索引的统计集合中数据的平均值
1
2
3
4
5
6
7
8
9
10
11
global reads
probe vfs.read
{
	reads[execname(),pid()] <<< 1
}

probe timer.s(3)
{
	foreach([var1,var2] in reads)
	printf("%s (%d) : %d /n", var1, var2, @count(reads[var1,var2]))
}

Tapsets

Tapsets是脚本库,里面预写好了probes和functions可以被SystemTap脚本调用,tapsets也使用.stp作为后缀,默认位于:/usr/share/systemtap/tapset,但无法直接运行。