kk Blog —— 通用基础

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

Linux内存管理

http://blog.csdn.net/myarrow/article/details/8624687

http://blog.csdn.net/myarrow/article/details/8682819

1. Linux物理内存三级架构

对于内存管理,Linux采用了与具体体系架构不相关的设计模型,实现了良好的可伸缩性。它主要由内存节点node、内存区域zone和物理页框page三级架构组成。

内存节点node

内存节点node是计算机系统中对物理内存的一种描述方法,一个总线主设备访问位于同一个节点中的任意内存单元所花的代价相同,而访问任意两个不同节点中的内存单元所花的代价不同。在一致存储结构(Uniform Memory Architecture,简称UMA)计算机系统中只有一个节点,而在非一致性存储结构(NUMA)计算机系统中有多个节点。Linux内核中使用数据结构pg_data_t来表示内存节点node。如常用的ARM架构为UMA架构。

内存区域zone

内存区域位于同一个内存节点之内,由于各种原因它们的用途和使用方法并不一样。如基于IA32体系结构的个人计算机系统中,由于历史原因使得ISA设备只能使用最低16MB来进行DMA传输。又如,由于Linux内核采用

• 物理页框page

2. Linux虚拟内存三级页表

Linux虚拟内存三级管理由以下三级组成:
• PGD: Page Global Directory (页目录)
• PMD: Page Middle Directory (页目录)
• PTE: Page Table Entry (页表项)

每一级有以下三个关键描述宏:
• SHIFT
• SIZE
• MASK

如页的对应描述为:

1
2
3
4
/* PAGE_SHIFT determines the page size  asm/page.h */  
#define PAGE_SHIFT      12  
#define PAGE_SIZE       (_AC(1,UL) << PAGE_SHIFT)  
#define PAGE_MASK       (~(PAGE_SIZE-1))  

数据结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* asm/page.h */  
typedef unsigned long pteval_t;  

typedef pteval_t pte_t;  
typedef unsigned long pmd_t;  
typedef unsigned long pgd_t[2];  
typedef unsigned long pgprot_t;  

#define pte_val(x)      (x)  
#define pmd_val(x)      (x)  
#define pgd_val(x)  ((x)[0])  
#define pgprot_val(x)   (x)  

#define __pte(x)        (x)  
#define __pmd(x)        (x)  
#define __pgprot(x)     (x)  
2.1 Page Directory (PGD and PMD)

每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd_t数组。其定义见<asm/page.h>。 进程的pgd_t数据见 task_struct -> mm_struct -> pgd_t * pgd;

ARM架构的PGD和PMD的定义如下<arch/arm/include/asm/pgtable.h>:

1
2
3
4
5
6
7
8
9
10
11
#define PTRS_PER_PTE  512    // PTE中可包含的指针<u32>数 (21-12=9bit)  
#define PTRS_PER_PMD  1  
#define PTRS_PER_PGD  2048   // PGD中可包含的指针<u32>数 (32-21=11bit)</p><p>#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)  
#define PTE_HWTABLE_OFF  (PTE_HWTABLE_PTRS * sizeof(pte_t))  
#define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))
/*  
 * PMD_SHIFT determines the size of the area a second-level page table can map  
 * PGDIR_SHIFT determines what a third-level page table entry can map  
 */  
#define PMD_SHIFT  21  
#define PGDIR_SHIFT  21

虚拟地址SHIFT宏图:

虚拟地址MASK和SIZE宏图:

2.2 Page Table Entry

PTEs, PMDs和PGDs分别由pte_t, pmd_t 和pgd_t来描述。为了存储保护位,pgprot_t被定义,它拥有相关的flags并经常被存储在page table entry低位(lower bits),其具体的存储方式依赖于CPU架构。

每个pte_t指向一个物理页的地址,并且所有的地址都是页对齐的。因此在32位地址中有PAGE_SHIFT(12)位是空闲的,它可以为PTE的状态位。

PTE的保护和状态位如下图所示:

2.3 如何通过3级页表访问物理内存

为了通过PGD、PMD和PTE访问物理内存,其相关宏在asm/pgtable.h中定义。

• pgd_offset

根据当前虚拟地址和当前进程的mm_struct获取pgd项的宏定义如下:

1
2
3
4
5
6
7
/* to find an entry in a page-table-directory */  
#define pgd_index(addr)     ((addr) >> PGDIR_SHIFT)  //获得在pgd表中的索引  

#define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr)) //获得pmd表的起始地址  

/* to find an entry in a kernel page-table-directory */  
#define pgd_offset_k(addr)  pgd_offset(&init_mm, addr)  

• pmd_offset

根据通过pgd_offset获取的pgd 项和虚拟地址,获取相关的pmd项(即pte表的起始地址)

1
2
/* Find an entry in the second-level page table.. */  
#define pmd_offset(dir, addr)   ((pmd_t *)(dir))   //即为pgd项的值  

• pte_offset

根据通过pmd_offset获取的pmd项和虚拟地址,获取相关的pte项(即物理页的起始地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef CONFIG_HIGHPTE  
#define __pte_map(pmd)      pmd_page_vaddr(*(pmd))  
#define __pte_unmap(pte)    do { } while (0)  
#else  
#define __pte_map(pmd)      (pte_t *)kmap_atomic(pmd_page(*(pmd)))  
#define __pte_unmap(pte)    kunmap_atomic(pte)  
#endif  

#define pte_index(addr)     (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))  

#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))  

#define pte_offset_map(pmd,addr)    (__pte_map(pmd) + pte_index(addr))  
#define pte_unmap(pte)          __pte_unmap(pte)  

#define pte_pfn(pte)        (pte_val(pte) >> PAGE_SHIFT)  
#define pfn_pte(pfn,prot)   __pte(__pfn_to_phys(pfn) | pgprot_val(prot))  

#define pte_page(pte)       pfn_to_page(pte_pfn(pte))  
#define mk_pte(page,prot)   pfn_pte(page_to_pfn(page), prot)  

#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)  
#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)  

其示意图如下图所示:

2.4 根据虚拟地址获取物理页的示例代码

根据虚拟地址获取物理页的示例代码详见<mm/memory.c中的函数follow_page>。

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
147
148
149
150
151
152
153
154
155
156
/** 
 * follow_page - look up a page descriptor from a user-virtual address 
 * @vma: vm_area_struct mapping @address 
 * @address: virtual address to look up 
 * @flags: flags modifying lookup behaviour 
 * 
 * @flags can have FOLL_ flags set, defined in <linux/mm.h> 
 * 
 * Returns the mapped (struct page *), %NULL if no mapping exists, or 
 * an error pointer if there is a mapping to something not represented 
 * by a page descriptor (see also vm_normal_page()). 
 */  
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,  
		    unsigned int flags)  
{  
	pgd_t *pgd;  
	pud_t *pud;  
	pmd_t *pmd;  
	pte_t *ptep, pte;  
	spinlock_t *ptl;  
	struct page *page;  
	struct mm_struct *mm = vma->vm_mm;  

	page = follow_huge_addr(mm, address, flags & FOLL_WRITE);  
	if (!IS_ERR(page)) {  
		BUG_ON(flags & FOLL_GET);  
		goto out;  
	}  

	page = NULL;  
	pgd = pgd_offset(mm, address);  
	if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))  
		goto no_page_table;  

	pud = pud_offset(pgd, address);  
	if (pud_none(*pud))  
		goto no_page_table;  
	if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {  
		BUG_ON(flags & FOLL_GET);  
		page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);  
		goto out;  
	}  
	if (unlikely(pud_bad(*pud)))  
		goto no_page_table;  

	pmd = pmd_offset(pud, address);  
	if (pmd_none(*pmd))  
		goto no_page_table;  
	if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {  
		BUG_ON(flags & FOLL_GET);  
		page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);  
		goto out;  
	}  
	if (pmd_trans_huge(*pmd)) {  
		if (flags & FOLL_SPLIT) {  
		    split_huge_page_pmd(mm, pmd);  
		    goto split_fallthrough;  
		}  
		spin_lock(&mm->page_table_lock);  
		if (likely(pmd_trans_huge(*pmd))) {  
		    if (unlikely(pmd_trans_splitting(*pmd))) {  
		        spin_unlock(&mm->page_table_lock);  
		        wait_split_huge_page(vma->anon_vma, pmd);  
		    } else {  
		        page = follow_trans_huge_pmd(mm, address,  
		                         pmd, flags);  
		        spin_unlock(&mm->page_table_lock);  
		        goto out;  
		    }  
		} else  
		    spin_unlock(&mm->page_table_lock);  
		/* fall through */  
	}  
split_fallthrough:  
	if (unlikely(pmd_bad(*pmd)))  
		goto no_page_table;  

	ptep = pte_offset_map_lock(mm, pmd, address, &ptl);  

	pte = *ptep;  
	if (!pte_present(pte))  
		goto no_page;  
	if ((flags & FOLL_WRITE) && !pte_write(pte))  
		goto unlock;  

	page = vm_normal_page(vma, address, pte);  
	if (unlikely(!page)) {  
		if ((flags & FOLL_DUMP) ||  
		    !is_zero_pfn(pte_pfn(pte)))  
		    goto bad_page;  
		page = pte_page(pte);  
	}  

	if (flags & FOLL_GET)  
		get_page(page);  
	if (flags & FOLL_TOUCH) {  
		if ((flags & FOLL_WRITE) &&  
		    !pte_dirty(pte) && !PageDirty(page))  
		    set_page_dirty(page);  
		/* 
		 * pte_mkyoung() would be more correct here, but atomic care 
		 * is needed to avoid losing the dirty bit: it is easier to use 
		 * mark_page_accessed(). 
		 */  
		mark_page_accessed(page);  
	}  
	if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {  
		/* 
		 * The preliminary mapping check is mainly to avoid the 
		 * pointless overhead of lock_page on the ZERO_PAGE 
		 * which might bounce very badly if there is contention. 
		 * 
		 * If the page is already locked, we don't need to 
		 * handle it now - vmscan will handle it later if and 
		 * when it attempts to reclaim the page. 
		 */  
		if (page->mapping && trylock_page(page)) {  
		    lru_add_drain();  /* push cached pages to LRU */  
		    /* 
		     * Because we lock page here and migration is 
		     * blocked by the pte's page reference, we need 
		     * only check for file-cache page truncation. 
		     */  
		    if (page->mapping)  
		        mlock_vma_page(page);  
		    unlock_page(page);  
		}  
	}  
unlock:  
	pte_unmap_unlock(ptep, ptl);  
out:  
	return page;  

bad_page:  
	pte_unmap_unlock(ptep, ptl);  
	return ERR_PTR(-EFAULT);  

no_page:  
	pte_unmap_unlock(ptep, ptl);  
	if (!pte_none(pte))  
		return page;  

no_page_table:  
	/* 
	 * When core dumping an enormous anonymous area that nobody 
	 * has touched so far, we don't want to allocate unnecessary pages or 
	 * page tables.  Return error instead of NULL to skip handle_mm_fault, 
	 * then get_dump_page() will return NULL to leave a hole in the dump. 
	 * But we can only make this optimization where a hole would surely 
	 * be zero-filled if handle_mm_fault() actually did handle it. 
	 */  
	if ((flags & FOLL_DUMP) &&  
		(!vma->vm_ops || !vma->vm_ops->fault))  
		return ERR_PTR(-EFAULT);  
	return page;  
}

1. First Fit分配器

First Fit分配器是最基本的内存分配器,它使用bitmap而不是空闲块列表来表示内存。在bitmap中,如果page对应位为1,则表示此page已经被分配,为0则表示此page没有被分配。为了分配小于一个page的内存块,First Fit分配器记录了最后被分配的PFN (Page Frame Number)和分配的结束地址在页内的偏移量。随后小的内存分配被Merge到一起并存储到同一页中。

First Fit分配器不会造成严重的内存碎片,但其效率较低,由于内存经常通过线性地址进行search,而First Fit中的小块内存经常在物理内存的开始处,为了分配大块内存而不得不扫描前面大量的内存。

2. Boot Memory分配器

物理内存分配器如何分配内存来初始化其自己呢?

答案是:通过Boot Memory分配器来实现,而Boot Memory分配器则通过最基本的First Fit分配器来实现。

2.1 Boot Map定义

Boot Map通过数据结构bootmem_data来定义,详见<linux/bootmem.h>,其定义如下所示:

1
2
3
4
5
6
7
8
typedef struct bootmem_data {  
	unsigned long node_boot_start; // 描述的物理内存的起始地址  
	unsigned long node_low_pfn;    // 结束物理地址,即ZONE_NORMAL的结束  
	void *node_bootmem_map;        // 描述“使用或空闲的位图”的地址  
	unsigned long last_offset;     // 最后被分配的页内偏移量,即在llast_pos描述的物理页中,  
		                         // 从last_offset开始,没有被分配   
	unsigned long last_pos;        // 最后被分配的页的PFN  
} bootmem_data_t;  

所有bootmem_data被放于全局变量bdata_list中。

2.2 Boot Memory分配器初始化

每一个CPU架构被要求提供setup_arch函数,它负责获取初始化boot memory分配器的必要参数。不同的CPU架构通过不同的函数来实现,如ARM通过bootmem_init来实现。它负责获取以下参数:

1
2
3
4
5
• min_low_pfn: 系统中可获得的最小的PFN,装载kernel image结束之后的第一页,在mm/bootmem.c中定义
• max_low_pfn:低端内存(ZONE_NORMAL)中可获得的最大PFN
• highstart_pfn:高端内存(ZONE_HIGHMEM)的起始PFN
• highend_pfn:高端内存(ZONE_HIGHMEM)的结束PFN
• max_pfn:系统中可获得的最大的PFN, 在mm/bootmem.c中定义

PFN是在物理内存map的偏移量,以page为单位。Kernel可直接访问ZONE_NORMAL,其偏移量为:PAGE_OFFSET。

通过以上5个参数明确了可用物理内存之后,调用init_bootmem->init_bootmem_core来初始化contig_page_data。它主要完成以下两件事:
1) 将把与此node对应pgdat_data_t插入到pgdat_list中
2) 初始化bootmem_data_t的中参数,并分配表示页分配状态的bitmap,其大小为: (end_pfn-start_pfn+7)/8

bitmap的物理地址为:bootmem_data_t->node_boot_start
bitmap的虚拟地直为:bootmem_data_t->node_bootmem_map

2.3 分配内存

• reserve_bootmem:用于预留物理页面。但用于通用的内存分配是低率的,它主要用于各种驱动(如:Video Codec)预留内存。

常用的内存分配函数如下(in UMA架构,我们常的ARM架构为UMA架构):

1
2
3
4
• alloc_bootmem
• alloc_bootmem_low
• alloc_bootmem_pages
• alloc_bootmem_low_pages

其调用关系如下图所示:

2.3.1 __alloc_bootmem

__alloc_bootmem() 需要以下参数:
• pgdat 用于分配内存块的节点,在UMA架构中,它被忽略,因为它总是为:contig_page_data
• size 指定请求分配的内存大小,以字节为单位
• align 请求以多少字节对齐,地于小块内存分配,一般以SMP_CACHE_BYTES对齐,如在X86上,与L1硬件cache对齐
• goal 偏好的分配内存的起始地址,

2.3.2 __alloc_bootmem_core

它从goal指定的地址开始,线性地扫描内存,以寻找可以满足内存分配要求的内存块。它的另外一项功能是决定是否需要把新分配的内存块与以前已经分配的内存块merge到一起。

分配内存常用函数定义如下:

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
#ifdef CONFIG_NO_BOOTMEM  
/* We are using top down, so it is safe to use 0 here */  
#define BOOTMEM_LOW_LIMIT 0  
#else  
#define BOOTMEM_LOW_LIMIT __pa(MAX_DMA_ADDRESS)  
#endif  

#define alloc_bootmem(x) \  
	__alloc_bootmem(x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_align(x, align) \  
	__alloc_bootmem(x, align, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_nopanic(x) \  
	__alloc_bootmem_nopanic(x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages(x) \  
	__alloc_bootmem(x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages_nopanic(x) \  
	__alloc_bootmem_nopanic(x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_node(pgdat, x) \  
	__alloc_bootmem_node(pgdat, x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_node_nopanic(pgdat, x) \  
	__alloc_bootmem_node_nopanic(pgdat, x, SMP_CACHE_BYTES, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages_node(pgdat, x) \  
	__alloc_bootmem_node(pgdat, x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  
#define alloc_bootmem_pages_node_nopanic(pgdat, x) \  
	__alloc_bootmem_node_nopanic(pgdat, x, PAGE_SIZE, BOOTMEM_LOW_LIMIT)  

#define alloc_bootmem_low(x) \  
	__alloc_bootmem_low(x, SMP_CACHE_BYTES, 0)  
#define alloc_bootmem_low_pages(x) \  
	__alloc_bootmem_low(x, PAGE_SIZE, 0)  
#define alloc_bootmem_low_pages_node(pgdat, x) \  
	__alloc_bootmem_low_node(pgdat, x, PAGE_SIZE, 0)  
2.4 释放内存

调用free_bootmem来释放内存。

1
2
3
4
5
6
7
8
9
10
11
void __init free_bootmem(unsigned long addr, unsigned long size)  
{  
	unsigned long start, end;  

	kmemleak_free_part(__va(addr), size);  

	start = PFN_UP(addr);  
	end = PFN_DOWN(addr + size);  

	mark_bootmem(start, end, 0, 0);  
}

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代码中都不能睡眠!