kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

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中的值开始执行内核入口点的第一条指令。