├── .gitignore ├── 2014-03-12-start.md ├── 2014-03-13-heap.md ├── 2014-03-13-how-kernel-work.md ├── 2014-03-13-parts-of-kernel.md ├── 2014-03-14-lru.md ├── 2014-03-14-what-is-file.md ├── 2014-03-15-red-and-black-tree-with-c-code.md ├── 2014-03-15-task-size.md ├── 2014-03-16-progress-model.md ├── 2014-03-16-reentrant-kernel.md ├── 2014-03-17-consistent-of-kernel.md ├── 2014-03-17-signal.md ├── 2014-03-19-zombie-process.md ├── 2014-03-26-mm-management.md ├── 2014-03-28-process.md ├── 2014-03-30-process-descriptor.md ├── 2014-04-01-process-priority.md ├── 2014-04-02-process-life.md ├── 2014-04-02-process-type-and-namespace.md ├── 2014-04-04-one-process.md ├── 2014-04-09-process-list.md ├── 2014-04-09-process-relationship.md ├── 2014-04-10-process-waiting-link-list.md ├── 2014-04-10-the-kernel-thread.md ├── 2014-04-11-clone-fork-and-vfork.md ├── 2014-04-11-execve-replace-process.md ├── 2014-04-11-group-exit-and-do-exit-a-process.md ├── 2014-04-12-process-switch.md ├── 2014-04-13-memory-address.md ├── 2014-04-13-segment-selector.md ├── 2014-04-14-segment-descriptor.md ├── 2014-04-14-visit-segment-descriptor.md ├── 2014-04-15-segment-in-linux.md ├── 2014-04-15-system-paging-unit.md ├── 2014-04-16-pae.md ├── 2014-04-16-system-hardware-cache-and-tlb.md ├── 2014-04-17-linux-paging.md ├── 2014-04-17-page-and-page-descriptor.md ├── 2014-04-17-physical-memory.md ├── 2014-04-17-thread-page-table-and-kernel-page-table.md ├── 2014-04-18-pglist-data-and-zone.md ├── 2014-04-19-page-and-page-table.md ├── 2014-04-19-page-frame-allocator.md ├── 2014-04-20-highmem.md ├── 2014-04-22-init-mm-management.md ├── 2014-04-25-interrupt.md ├── 2014-04-26-kernel-preemption.md ├── 2014-04-27-per-cpu.md ├── 2014-04-27-timing-measurement.md ├── 2014-04-28-atomic-operations.md ├── 2014-04-28-time-system.md ├── 2014-04-29-memory-barrier.md ├── 2014-04-29-timing-in-linux.md ├── 2014-05-02-buddy-system-struct.md ├── 2014-05-02-memory-fragmentation.md ├── 2014-05-02-spin-lock.md ├── 2014-05-03-init-mm-zone-and-page.md ├── 2014-05-03-irq-and-interrupt.md ├── 2014-05-03-read-and-write-spin-lock.md ├── 2014-05-04-alloc-page.md ├── 2014-05-04-exception.md ├── 2014-05-04-interrupt-descriptor-table.md ├── 2014-05-04-seqlock.md ├── 2014-05-05-loop-interrupt.md ├── 2014-05-05-mm-release.md ├── 2014-05-05-read-copy-update.md ├── 2014-05-05-working-on-exception.md ├── 2014-05-06-kernel-semaphore.md ├── 2014-05-06-vmalloc.md ├── 2014-05-06-working-on-interrupt.md ├── 2014-05-07-io-interrupt-and-data-struct.md ├── 2014-05-07-io-interrupt-dynamic.md ├── 2014-05-07-multi-cpu-interrupt.md ├── 2014-05-07-slab.md ├── 2014-05-07-soft-irq-and-tasklet.md ├── 2014-05-08-kernel-mm-management.md ├── 2014-05-08-soft-irq.md ├── 2014-05-09-how-slab-work.md ├── 2014-05-09-soft-irq-daemon-ksoftirqd.md ├── 2014-05-09-tasklet.md ├── 2014-05-10-slab-structure.md ├── 2014-05-10-wait-queue.md ├── 2014-05-11-completion.md ├── 2014-05-11-init-slab.md ├── 2014-05-11-virtual-filesystem.md ├── 2014-05-12-common-file-model.md ├── 2014-05-12-create-slab.md ├── 2014-05-12-vfs-system-interfaces.md ├── 2014-05-12-work-queue.md ├── 2014-05-13-return-from-interrupt-or-exception.md ├── 2014-05-13-slab-alloc.md ├── 2014-05-13-super-block-object.md ├── 2014-05-14-inode-object.md ├── 2014-05-14-slab-free.md ├── 2014-05-15-file-object.md ├── 2014-05-16-dentry-object.md ├── 2014-05-17-dentry-cache.md ├── 2014-05-18-special-filesystem.md ├── 2014-05-19-filesystem-opts.md ├── README.md └── images ├── APIC.png ├── authors └── guojing.jpg ├── buddy-system.png ├── copy_process.png ├── do_fork.png ├── dram_cache.png ├── execve.png ├── exit.png ├── free_area.png ├── free_area_init_nodes.png ├── free_page.png ├── free_steps.png ├── gdt.png ├── idt.png ├── ioirq.png ├── irq_loop.png ├── link.png ├── linux-paging.png ├── linux-system.png ├── mem.png ├── memory-fragmentation.png ├── mmu.png ├── namespace.png ├── numa.png ├── page.png ├── page_frame.png ├── page_frame_alloc.png ├── paging_unit.png ├── paging_unit_2.png ├── process-pri.png ├── relation.png ├── segment_descriptor.png ├── segment_selector.png ├── segmentation.png ├── slab.png ├── slab2.png ├── slab3.png ├── slab4.png ├── slab5.png ├── slab6.png ├── slab_alloc.png ├── slab_create.png ├── slabinfo.png ├── start_kernel.png ├── task-size.png ├── task_struct.png ├── thread_info.png ├── tree ├── rb_1.png ├── rb_delete.png ├── rb_delete_3.png ├── rb_delete_4.png ├── rb_delete_5.png ├── rb_delete_6.png ├── rb_delete_steps.png ├── rb_insert_1.png ├── rb_insert_2.png ├── rb_insert_3.png ├── rb_insert_4.png ├── rb_insert_5.png ├── rb_insert_steps.png ├── rb_insert_steps_2.png ├── rotate.png ├── search_tree.png ├── search_tree_del_1.png └── search_tree_del_2.png ├── vfs.png ├── vfs2.png ├── visit_segment.png ├── vmalloc.png ├── vmalloc_struct.png └── zonelist.png /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | *.sw[mnpo] 3 | *.un~ 4 | .DS_Store 5 | *.pdf 6 | *.psd 7 | -------------------------------------------------------------------------------- /2014-03-12-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 写在开始 4 | category: 感受 5 | description: 写在开始... 6 | tags: 感受 7 | --- 8 | 为什么我要看[《深入Linux内核架构》](http://book.douban.com/subject/4843567/)和[《深入理解LINUX内核》](http://book.douban.com/subject/1767120/)这两本书?其实说起来很诡异,并没有什么特别的,主要还是因为自己太差,而很多问题去问高手,往往得不到解答。 9 | 10 | 虽然做程序员这一行做了有一段时间了,但很多时候总觉得有些格格不入,想起来看,可能还是因为自己太差,哈哈,虽然这么说,但总觉得可以变得更好。自己一开始主要做微软的项目,是Microsoft的金牌Partner,倒是有很多可以锻炼的地方,后来觉得微软的东西,无论怎么用,都是在使用别人的东西,自己实际上学不到什么,而在微软社区里,只要稍微吹吹牛(倒也不应该这么说),比如安装MSSQL要怎么安装,都可以变成MVP,我自己大学时候也弄过一个MVP,但最终觉得挺没意思的,后来就离开了那个公司。 11 | 12 | 然后我在豆瓣工作,工作了几年觉得开源社区很好,因为很能锻炼人,但是在开源社区里瞎混的越久,越觉得自己垃圾。我觉得倒不是自己真的垃圾,只是说很多习惯很难理解,比如说RTFD[^RTFD],虽然从另一方面来说对于新人的提问,高手们总是神龙见首不见尾,让新人们摸爬滚打,等到新人们成为了老人,怎么的也想享受享受威严的感觉,于是这种风气一直流传下来,就好像当兵里新兵一定会受到欺负一样难以理解。于是在这种情况下,产生了自我怀疑。 13 | 14 | [^RTFD]: Read The Fuck Document 15 | 16 | 当然,大牛们的时间自然是宝贵,这倒无可厚非,另一个难堪的是觉得,每次提问的时候,不仅仅是RTFD,更多时候是觉得他根本就没有理解你想问什么,而觉得深深的无奈。这就好比你看到满大街都在跑汽车,你就很好奇,问汽车是怎么跑起来的,大牛们首先是跟你说RTFD,STFG[^STFG],当然这一点并没有太大的错,因为我觉得如果不加思考的提问,也是很不好的。但对于思考过的人来说,这确实难堪。如果你已经搜索过并提出更稍微难一点的问题,比如发动机是什么原理,其实很多大牛更不愿意和你解释,他们只是回答了你『什么是发动机』或者『如何使用发动机』,有时候,他们自己也只是囫囵吞枣。而对我来说,火花塞的物理作用才是我想得到的答案。 17 | 18 | [^STFG]: Search The Fuck Google 19 | 20 | 就比如之前我问socket的问题,想了解HTTP是怎么实现的,然后问WebSocket是如何实现,大多数人都会让我RTFD,并友好的贴上实现协议。当然,更好的一些人会给js的实现方法[^js],于是觉得他们并没有理解我的问题,直到我自己读到command=0x8就是close的时候,我觉得我才算理解了socket是什么东西。因为我智商低,比如`管道`这种东西,用的地方太多,没有上下文,常常无法理解。 21 | 22 | [^js]: var websocket = New WebSocket(xx),并由浏览器实现。 23 | 24 | 不过也遇到过很多很好的开发,让我对C语言有了很多很新的认识。当然我不是C的开发者,对C的了解仅限于做做算法,但为什么要做算法,一点都不知道。比如红黑树为什么那么重要,谁知道,就算说了文件系统用了红黑树也难以理解(相当多老师在解释树的作用的时候都会这么说),因为这样的理解仅仅是表面的理解。而看过内核调度之后,虽然红黑树还是那个红黑树,但我对它有了更深的了解。 25 | 26 | 我发现,国外很多越厉害的开发者,越愿意和新人交流如何开发。为了满足我自己这样的要求,和无聊的没有意义的没有产出的问题,于是有了这个网站。 27 | 28 | 至于为什么从Linux内核开始看,其实对我来说也挺难的,但很多代码看到最后,都是系统的东西,而中国的教育让我只知其一不知其二,上不了台面,所以打算从基础看起,如果能帮到你,那最好,如果你是高手,也请高抬贵手,谢谢。 29 | 30 | 对于这样一篇读书笔记,随着时间的关系,可能会有很多问题,毕竟是第一次读的笔记,欢迎各种PR,有各种typo也请一并PR。 31 | 32 | 或者联系我 soundbbg at gmail。 33 | 34 | [Github](https://github.com/GuoJing/linux-kernel-architecture) -------------------------------------------------------------------------------- /2014-03-13-heap.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 堆 4 | category: 数据结构 5 | description: Heap... 6 | tags: heap 堆 数据结构 7 | --- 8 | Heap是一种数据结构,内核里经常使用,例如优先级队列,进程调度等等。Heap是一种完全二叉树结构,但是使用数组表示。其操作性能与树的高度成正比。 9 | 10 | ### 堆的基本性质 ### 11 | 12 | 堆用数组表示,由于是完全二叉树,则满足: 13 | 14 | 1. t/2表示父节点。 15 | 2. t*2表示左孩子。 16 | 3. t*2+1表示右孩子。 17 | 18 | 这里简单的讲解一下创建、插入元素和弹出元素的操作。 19 | 20 | ### CODE ### 21 | 22 | 创建一个堆: 23 | ```cpp 24 | typedef int heap_elem_t; 25 | 26 | typedef struct heap_t 27 | { 28 | int size; 29 | int capacity; 30 | heap_elem_t *elems; 31 | int (*cmp)(const heap_elem_t*, const heap_elem_t*); 32 | }heap_t; 33 | 34 | heap_t* 35 | heap_create(const int capacity, 36 | int(*cmp)(const heap_elem_t*, const heap_elem_t*)){ 37 | //给堆分配内存 38 | heap_t *h = (heap_t*)malloc(sizeof(heap_t)); 39 | h->size = 0; 40 | h->capacity = capacity; 41 | //堆的元素本质上是数组 42 | h->elems = (heap_elem_t*)malloc(sizeof(heap_elem_t)); 43 | h->cmp = cmp; 44 | return h; 45 | } 46 | ``` 47 | 48 | 插入一个元素,基本思想是在数组的最后插入一个元素,然后和父亲比较,如果比父亲小,则交换数据。然后从最后一个元素往前重复之前的和父亲比较的逻辑。 49 | 50 | ```cpp 51 | void 52 | heap_push(heap_t *h, const heap_elem_t e){ 53 | if(h->size == h->capacity) { 54 | heap_elem_t *tmp = (heap_elem_t*)realloc(h->elems, 55 | h->capacity*2*sizeof(heap_elem_t)); 56 | h->elems = tmp; 57 | h->capacity *= 2; 58 | } 59 | 60 | h->elems[h->size] = e; 61 | h->size++; 62 | heap_shift_up(h, h->size-1); 63 | } 64 | ``` 65 | 66 | 其中*heap_shift_up*方法如下: 67 | 68 | ```c++ 69 | void 70 | heap_shift_up(const heap_t *h, const int start){ 71 | // 当前元素 72 | int j = start; 73 | // 父元素 74 | int i = (j-1)/2; 75 | const heap_elem_t tmp = h->elems[start]; 76 | 77 | //除非到根节点 78 | while(j>0){ 79 | // 比较父元素 80 | if(h->cmp(&(h->elems[i]), &tmp)<=0){ 81 | // 如果当前元素比父元素小,则直接返回 82 | break; 83 | } else { 84 | // 如果当前元素比父元素大,交换元素 85 | h->elems[j] = h->elems[i]; 86 | // 继续往上重复逻辑 87 | j = i; 88 | i = (i-1)/2; 89 | } 90 | } 91 | 92 | h->elems[j] = tmp; 93 | } 94 | ``` 95 | 96 | 堆中弹出逻辑如下,基本思想为把第0个元素弹出,用最后一个元素替换。也就是把最后一个元素拿到跟节点,然后从根节点向下比较。 97 | ```cpp 98 | void 99 | heap_pop(heap_t *h){ 100 | h->elems[0] = h->elems[h->size - 1]; 101 | h->size--; 102 | heap_shift_down(h, 0); 103 | } 104 | ``` 105 | 106 | 其中*heap_shift_down*逻辑如下: 107 | 108 | ```cpp 109 | void 110 | heap_shift_down(const heap_t *h, const int start){ 111 | // 父节点 112 | int i = start; 113 | int j; 114 | const heap_elem_t tmp = h->elems[start]; 115 | 116 | // j为子节点中的右孩子,如果不小于size 117 | for(j=2*i+1; jsize; j=2*j+1) { 118 | 119 | // 如果没有找到最下层的最右节点并且 120 | // 比较两个孩子的大小,找到小的那个一 121 | if(j<(h->size -1)&& 122 | h->cmp(&(h->elems[j]), &(h->elems[j+1]))>0){ 123 | j++; 124 | } 125 | 126 | // 和子节点比较,如果比子节点小,退出 127 | if(h->cmp(&tmp, &(h->elems[j]))<=0) { 128 | break; 129 | } else { 130 | // 如果比子节点大,交换元素 131 | h->elems[i] = h->elems[j]; 132 | // 继续往下查找 133 | i = j; 134 | } 135 | } 136 | 137 | h->elems[i] = tmp; 138 | } 139 | ``` 140 | 141 | 完整代码看这个[GIST](https://gist.github.com/GuoJing/10355201)。 142 | -------------------------------------------------------------------------------- /2014-03-13-how-kernel-work.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内核 4 | category: 基础 5 | description: 内核的任务,以及内核的一些工作... 6 | tags: 微内核 宏内核 模块 代码结构 7 | --- 8 | 什么是内核,内核做什么的呢?内核有哪些需要了解的概念? 9 | 10 | ### 内核的任务 ### 11 | 12 | 在纯技术层面上,内核是硬件与软件之间的一个中间层。其作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。尽管如此,仍然可以从其他一些有趣的视角对内核进行研究。 13 | 14 | 1. 从应用程序的视角来看,内核可以被认为是一台`增强的计算机`,将计算机抽象到一个高的层次上。比如,在内核寻址硬盘时,它必须确定使用哪个路径来从磁盘向内存复制数据,数据的位置,从哪个路径向磁盘发送哪一条命令,等等。另一方面,应用程序只需发出传输数据的命令。实际的工作如何完成与应用程序是不相关的,因为内核抽象了相关的细节。应用程序与硬件喷神没有联系,只与内核有联系,内核是应用程序所知道的层次结构中的最底层。 15 | 2. 当若干程序在同一系统中并发运行时,也可以将内核视为`资源管理程序`。在这种情况下,内核负责将可用共享资源(如CPU时间、磁盘空间、网络连接)分配到各个系统进程,同事还需要保证系统的完整性。 16 | 3. 另一种研究内核的视角是将内核视为库,其提供了一组面相系统的命令。比如说,系统调用用于向计算机发送请求,借助于C标准库,系统调用对于应用程序就像是普通函数一样。 17 | 18 | ### 实现策略 ### 19 | 20 | 微内核 21 | 22 | 只有最基本的功能直接由中央内核(微内核)实现。所有其他的功能都委托给一些独立进程,这些进程通过明确定义的通信接口与重心内核通信。比如说,独立进程可能负责实现各种文件系统,内存管理等等。理论上,这是一种很完美的实现方法,因为系统各个部分彼此都很清楚的划分开来,同时也能够让程序员使用干净的程序。这种方法的好处也包括:动态可扩展性和在运行时切换重要的组件。 23 | 24 | 宏内核 25 | 26 | 与微内核相反,宏内核是构建系统内核的传统方法。在这种方法中,内核的全部代码都打包到一个文件中。内核中的每个函数都可以访问内核中其他部分。如果编程不小心,可能导致代码中出现复杂的嵌套。目前Linux依旧是使用宏内核。 27 | 28 | 不过其中有了一个重要革新,就是在系统运行中,模块可以插入到内核代码中,也可以移除,这使得可以向内核动态添加功能,弥补了宏内核的一些缺陷。 29 | 30 | ### 模块 ### 31 | 32 | 为了达到微内核理论上很多的优点又不影响性能,Linux内核提供了模块(module)。模块是一个目标文件,其代码可以运行在运行时链接到内核或从内核解除链接。这种目标代码通常由一组函数组成,用来实现文件系统、驱动程序或其他内核上层功能。与微内核操作系统的外层不同,模块不是作为一个特殊的进程执行的。相反,与任何其他静态链接的内核函数一样,它代表当前进程在内核态下执行。 33 | 34 | 模块的优点包括: 35 | 36 | 1. 模块化方法:任何模块都可以运行时被链接或解除,开发模块变得容易。 37 | 2. 平台无关性:即使依赖于某些特殊的硬件特点,但它也不依赖于固定的硬件平台。 38 | 3. 节省内存使用:只有需要模块功能时,才把它链接到正在运行的内核中。 39 | 4. 无性能损失:模块的目标代码一旦被链接倒内核,其作用与静态链接的内核的目标代码完全等价。 40 | 41 | ### 内核的代码结构 ### 42 | 43 | 内核代码包含了操作系统的方方面面,了解代码结构有助于帮助我们深入的查找内核代码。 44 | 45 | 文件夹 | 说明 46 | ------------ | ------------- 47 | arch | 所有与体系结构相关的代码,例如arm、x86、a_64等不同体系结构的核心文件。 48 | include | 所有编译需要的核心头文件。 49 | init | 包含核心的初始化代码。 50 | mm | 包含内存管理的代码。 51 | kernel | 内核的主要代码。 52 | drivers | 驱动的主要代码。 53 | fs | 文件系统的各种操作代码。 54 | -------------------------------------------------------------------------------- /2014-03-13-parts-of-kernel.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内核的体系结构 4 | category: 基础 5 | description: 内核的体系结构... 6 | tags: 进程 切换 调度 线程 命名空间 7 | --- 8 | 尽管Linux是整体式的宏内核,但其具有相当良好的结构,尽管如此,Linux内核各个组成部分之间的彼此交互是不可避免的。各部分会共享数据结构,而且与严格隔离的系统相比,各部分(因为性能原因)协同工作时需要更多的函数。 9 | 10 | 下面是一个粗略的草图,概述了组成完成Linux系统的各个层次,以及内核所包含的一些重要子系统。 11 | 12 | ![system](images/linux-system.png) 13 | 内核大体的体系结构 14 | 15 | ### 进程/切换/调度 ### 16 | 17 | 传统上,UNIX操作系统下运行的应用程序、服务器以及其他程序都称为进程。每个进程都在CPU的虚拟内存中分配地址空间。各个进程的地址空间是完全独立的,因此进程并不会意识倒彼此的存在。对它们自己来说,它也会认为自己是系统中唯一的进程,如果进程想要彼此通信,就需要特定的内核机制。 18 | 19 | Linux是多任务系统,虽然只是看上去并发的执行若干进程,但其实系统中同时真正在运行的进程数目不超过CPU的数目,因此内核会按照短的时间间隔在不同的进程之间切换,由于时间太快,用户是注意不到的,所以产生了并发的假象。 20 | 21 | 1. 内核借助于CPU的帮助,负责进程切换的技术细节。给每个进程一种错觉,造成CPU总是可用的。 22 | 2. 内核还必须确定如何在现存进程之间共享CPU时间,重要的获得的CPU时间多一些,次要的得到的少一点,确定哪个进程运行多长时间称为调度。 23 | 24 | ### UNIX进程 ### 25 | 26 | Linux对进程采用了一种层次系统,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程,这个进程负责进一步的系统初始化操作。 27 | 28 | 树型结构的扩展方式与新进程的创建方式密切相关,UNIX操作系统中创建进程有两种方式,一种是fork,一种是exec。 29 | 30 | 1. fork可以创建当前进程的一个副本,父进程和子进程只有PID(进程ID)不同。从使用角度来说,父进程内存的内存将被复制,Linux使用了copy on write[^copyonwrite]来提高性能。 31 | 2. exec将一个新程序加载到当前进程的内存中并执行,旧程序的内存页将刷出,内容被替换为新数据,并执行新程序。 32 | 33 | [^copyonwrite]: 主要的原理是将内存复制操作延迟倒父进程或子进程向内存页面写入数据之前,在只读访问的情况下,主进程和子进程可以共享同一份内存块。 34 | 35 | #### 线程 #### 36 | 37 | 当然系统除了进程以外,还提供了线程,有时也被成为轻量级进程。Linux使用clone方法创建线程。工作方式类似于fork,但启用了精确的检查,以确认哪些资源是和父进程共享,哪些资源为线程自己创建。在一定程度上,允许线程和进程之间的连续转换。 38 | 39 | #### 命名空间 #### 40 | 41 | 命名空间则是将不同的进程进行分组,将它们隔离起来。在每个组里,进程同样有一个类似init的进程,每个组的资源对外是隔离的,这样的技术使得很多虚拟化技术得以发展,因为计算机上只需要有一个内核管理所有容器。[^problem] 42 | 43 | [^problem]: 传统意义上来说,每个用户相当于拥有一台计算机,也就是说有一个CPU、内存等资源。当多个用户需要不同环境的时候,通常需要使用多台计算机来解决。Linux内核加上命名空间的概念之后,只需要一台计算机就能够隔离出不同的资源,进行虚拟化。 -------------------------------------------------------------------------------- /2014-03-14-lru.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: LRU 4 | category: 数据结构 5 | description: LRU... 6 | tags: LRU 最近最少使用 7 | --- 8 | 内存的页管理有很多使用最近最少使用(*LRU*)的算法,这个算法实际上很简单,这里简单的说一下。这里涉及到局部性原理。局部性原理的意思就是认为在前面几条指令中使用频繁的页面很可能在后面的几条指令中频繁使用。相反,已经很久没有使用的页面很可能在未来较长的一段时间内不会被用到。 9 | 10 | 可以用简单的数组表示我们使用的页。 11 | 12 | {% highlight c++ %} 13 | int a[22] = {1, 2, 3, 4, 2, 1, 5, 6, 2, 1, 2, 14 | 3, 7, 6, 3, 2, 1, 2, 3, 6, 6, 4}; 15 | {% endhighlight %} 16 | 17 | 例如先使用1,然后再使用2,那么我们就有[2, 1]这个数组。然后使用3,则有[3, 2, 1]这个数组。这个时候使用1,那么数组就会变为[1, 3, 2],非常好理解。这个时候我们可以认为1是最常使用的,而2是最少使用。 18 | 19 | 如果假设这个数组长度为3,那么再加入的话,就会变成[4, 1, 3],因为2是最少使用的,被我们换出了。如果我们不考虑换出的情况,可以简单的写一个lru便于了解。具体深入的和缓存各种其他结构理论上是一样的。 20 | 21 | 定义一个简单的结构,包含值和下一个结点的指针,其实用数组也可以实现,但我这里用链表更简单,因为很多情况下一个结点的结构不只是一个数值: 22 | 23 | {% highlight c++ %} 24 | struct page { 25 | int value; 26 | page *next; 27 | }; 28 | {% endhighlight %} 29 | 30 | 然后初始化一个page实例,作为最初的node,node之后的结构才是整个数组的结构。 31 | 32 | {% highlight c++ %} 33 | page *init(const int value){ 34 | /* init the first node */ 35 | page *p = (page*)malloc(sizeof(page)); 36 | p->value = value; 37 | p->next = NULL; 38 | return p; 39 | } 40 | {% endhighlight %} 41 | 42 | 插入一个数据,首先在链表中搜索,如果搜索到就替换结构,否则就在链表头node后添加当前page数据。如果对链表长度有限制,需要换出最少使用的页,这里仅仅是简单的算法,所以不过多限制。 43 | 44 | {% highlight c++ %} 45 | int put(page *node, const int value){ 46 | /* 搜索结点 */ 47 | int r = search_page(node, value); 48 | if(r==1){ 49 | hit+=1; 50 | printf("hit value %d\n", value); 51 | return 1; 52 | } 53 | miss+=1; 54 | printf("miss value %d\n", value); 55 | page *p = (page*)malloc(sizeof(page)); 56 | p->value = value; 57 | p->next = NULL; 58 | if(node->next==NULL){ 59 | node->next = p; 60 | } else { 61 | p->next = node->next; 62 | node->next = p; 63 | } 64 | return 1; 65 | } 66 | {% endhighlight %} 67 | 68 | 搜索结点具体如下,简单的说就是把碰撞到的结点拿到跟结点后,然后改变next指针。这里有很多可以优化的地方。 69 | 70 | {% highlight c++ %} 71 | int search_page(page *node, const int value){ 72 | page *p = node; 73 | page *pre = node; 74 | if(!p->next){ 75 | return 0; 76 | } 77 | p = p->next; 78 | while(p) { 79 | if(p->value == value) { 80 | /* replace */ 81 | /* 如果当前结点的next不为空,应该把pre的next设置为 82 | 当前结点的next, 否则pre结点的next设为NULL */ 83 | if(p->next!=NULL) { 84 | pre->next = p->next; 85 | } else { 86 | pre->next=NULL; 87 | } 88 | if(node->next!=p){ 89 | /* 交换结点 */ 90 | p->next = node->next; 91 | node->next = p; 92 | } 93 | return 1; 94 | } 95 | pre = p; 96 | p = p->next; 97 | } 98 | return 0; 99 | } 100 | {% endhighlight %} 101 | 102 | 我们可以用下面的方法来测试: 103 | 104 | {% highlight c++ %} 105 | int main(){ 106 | page *node = init(-1); 107 | int total=22; 108 | int a[22] = {1, 2, 3, 4, 2, 1, 5, 6, 2, 1, 2, 109 | 3, 7, 6, 3, 2, 1, 2, 3, 6, 6, 4}; 110 | for(int i=0;i #### 38 | 39 | {% highlight c++ %} 40 | struct task_struct { 41 | pid_t pid; 42 | pid_t tgid; 43 | }; 44 | {% endhighlight %} 45 | 46 | ### 管理PID ### 47 | 48 | 一个小型的子系统称之为PID分配器用于加速ID的分配,此外内核需要提供辅助函数实现查找task\_struct的功能,以及将ID的内核标识形式和用户空间转换。其数据结构大致如下: 49 | 50 | #### \ #### 51 | 52 | {% highlight c++ %} 53 | struct pid_namespace { 54 | struct task_struct *child_reaper; 55 | int level; 56 | struct pid_namespace *parent; 57 | }; 58 | {% endhighlight %} 59 | 60 | 于是: 61 | 62 | 1. 每个PID命名空间都具有一个进程,发挥相当于全局init进程的作用。 63 | 2. parent时指向父命名空间的指针,level标识当前命名空间在命名空间层次结构中的深度。 64 | 65 | 由于循环使用PID编号,内核必须通过管理一个叫pidmap\_array位图来表示当前已分配的PID号和闲置的PID号。因为一个页框包含32768个位,所以在32位体系结构中pidmap\_array位图粗放在一个单独的页。在64位体系结构中,当内核分配了超过当前位图大小的PID号时,需要位PID位图增加更多的页,系统会一直保存这些页不被释放。 66 | 67 | Linux把不同的PID与系统中每个进程或轻量级进程相关联,这种方式提供最大的灵活性,因为系统中每个执行上下文都可以被唯一的识别。另一方面,Unix程序员希望同一组中的线程有共同的PID,这样就可以把信号发送给指定的PID的一组线程,这个信号会作用于该组中的所有线程。遵照这样的设计,Linux引入线程组,一个线程组中的所有线程使用和该线程组的领头线程(*thread group leader*)相同的PID,也就是该组中第一个轻量级进程的PID,它被放入进程描述符tgid字段中。getpid()系统调用会返回当前进程的tgid值而不是pid的值。 68 | 69 | 因此,一个多线程应用的所有线程共享相同的PID。 70 | 71 | ### 进程描述符处理 ### 72 | 73 | 进程是动态实体,其生命周期范围从几毫秒到几个月。因此,内核必须能够处理很多进程,并把进程描述符存放在动态内存中。Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内,一个是与进程描述符相关的小数据结构thread_info,叫做线程描述符。另一个是内核态的进程堆栈。 74 | 75 | 进程堆栈大小通常为8192个字节。考虑到效率的因素,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是2^13的倍数。当几乎没有可用的动态内存空间时,就会很难找到这两个连续页框,因为空闲空间可能存在大量碎片。因此,在80x86体系结构中,在编译时可以设置,以使内核堆栈和线程描述符跨越一个单独的页框。 76 | 77 | ![system](images/thread_info.png) 78 | 进程堆栈 79 | 80 | esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减,因为thread_info结构是52个字节长,因此内核栈能扩展到8140个字节。 81 | 82 | current是当前进程指针。 83 | 84 | 下面C语言大概描述了线程描述符和内核栈: 85 | 86 | {% highlight c++ %} 87 | union thread_union { 88 | struct thread_info thread_info; 89 | unsigned long stack[2048]; 90 | }; 91 | {% endhighlight %} 92 | 93 | ### 标识当前进程 ### 94 | 95 | thread\_info结构与内核态堆栈之间的紧密结合提供的主要好处是内核很容易从esp寄存器的值获得当前在CPU上正在运行的进程thread\_info结构地址。如果thread\_info的结构长度是8K,则内核屏蔽掉esp的低13位有效位就可以获得thread\_info结构的基地址;如果thread\_info结构长度是4K,内核只需要屏蔽esp的低12位有效位。 96 | 97 | 进程最常用的是进程描述符的地址而不是thread_info结构的地址。为了获得当前在CPU上运行的描述符指针,内核调用current[^3]宏获取。current宏进程作为进程描述符字段的前缀出现在内核代码中,例如`current->pid`返回在CPU上正在执行的进程PID。 98 | 99 | [^3]: current_thread_info()->task。 100 | -------------------------------------------------------------------------------- /2014-04-09-process-list.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 进程链表 4 | category: 进程 5 | description: 进程链表... 6 | tags: 进程链表 run_list 7 | --- 8 | 进程链表是一种双向链表数据结构(*list_head*),简单的说一下双向链表。每一个双向链表,都有一组操作,插入和删除一个元素,扫描链表等等。双向链表和链表相同,但双向链表除了有指向下一个元素的指针,还有指向上一个元素的指针,所以被称为双向连表。 9 | 10 | ![system](images/link.png) 11 | 进程链表 12 | 13 | Linux内核提供了*list_head*数据结构,字段*next*和*prev*分表标识链表向后和向前的指针元素。list\_head字段的指针中存的是另一个list\_head字段的地址。新链表使用宏LIST\_HEAD(list_name)创建。 14 | 15 | ### 进程链表 ### 16 | 17 | 进程链表是一个双向链表,进程链表把所有进程的描述符链接起来。每个*task_struct* 18 | 结构都包含一个*list_head*类型的*tasks*字段,这个类型的*orev*和*next*字段分别指向前面和后面的*task_struct*元素。 19 | 20 | 进程链表的表头是*init_task*描述符,就是0进程(*process 0*)或*swapper*进程的进程描述符。*init_task*的*tasks.prev*字段指向链表中最后插入的进程描述符的*tasks*字段。SET\_LINKS和REMOVE\_LINKS宏分别用于从进程链表中插入和删除一个进程描述符。另外*for_each_process*宏用来扫描整个进程链表。 21 | 22 | ### TASK_RUNNING状态的进程链表 ### 23 | 24 | 当内核寻找一个在CPU上运行的进程,必须值考虑可运行的进程。早先的Linux把所有可运行的进程都放在一个叫做运行队列(*runqueue*)的链表中,由于维持连表中的进程优先级排序开销过大,因此早起的调度程序不得不为了某些特殊的功能扫描整个链表。在Linux 2.6实现的运行队列则有所不同。其目的是让调度程序能在固定的时间内选出『最佳』可运行进程,与队列中可运行的进程数无关。 25 | 26 | **提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先级对应一个不同的链表**。 27 | 28 | 每个*task_struct*描述符包含一个*list_head*类型的字段*run_list*。如果进程的优先级等于k[^1],*run_list*字段把该进程加入优先权为k的可允许进程连表中。在多CPU系统中,每个CPU都有自己的运行队列,这是一个通过使数据结构更复杂来改善性能的典型例子。 29 | 30 | [^1]: 其取值范围从0到139。 31 | 32 | 进程描述符的*prio*字段存放进程的动态优先级,而*array*字段是一个指针,指向当前运行队列的*prio_array_t*的数据结构。 -------------------------------------------------------------------------------- /2014-04-09-process-relationship.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 进程之间的关系 4 | category: 进程 5 | description: 进程之间的关系... 6 | tags: 父进程 子进程 写时复制 fork vfork pidhash 7 | --- 8 | 进程之间除了ID连接的关系之外,内核还负责管理建立在UNIX进程创建模型之上的『家族关系』。 9 | 10 | ![system](images/relation.png) 11 | 进程之间的关系 12 | 13 | 如果进程A分支形成进程B,进程A称之为父进程,而进程B称之为子进程。如果进程A分支若干次形成几个子进程B、C、D,则这几个子进程之间称为兄弟关系。 14 | 15 | 下面简单的列举了进程描述符中相关亲属关系的描述,进程描述符结构可以看[《进程描述符》](/linux-kernel-architecture/posts/process-descriptor/),进程数据结构可以看[《进程链表》](/linux-kernel-architecture/posts/process-list/)。 16 | 17 | 字段名 | 说明 18 | ------------ | ------------- 19 | real_parent | 指向创建了P的进程描述符,如果P的父进程不存在,就指向进程init的描述符 20 | parent | 指向P的当前父进程,当此进程终止时,必须向父进程发信号 21 | children | 链表的头部,链表中的所有元素都是P创建的子进程 22 | sibling | 指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟进程的父进程都是P 23 | 24 | 建立非亲属关系的进程描述符字段有: 25 | 26 | 字段名 | 说明 27 | ------------ | ------------- 28 | group_leader | P所在进程组的组长的描述符指针 29 | signal->pgrp | P所在进程组的组长的PID 30 | tgid | P所在线程组的组长的PID 31 | signal->session | P的登录会话组长的PID 32 | ptrace_children | 链表的头,该聊表包含所有被debugger程序跟踪的P的子进程 33 | ptrace_list | 指向锁跟踪进程的实际父进程链表的前一个和下一个元素 34 | 35 | ### pidhash表及链表 ### 36 | 37 | 系统调用提供服务的时候会发生如下情况,当进程P1希望向另一个进程P2发送一个信号时,P1调用kill()系统调用,其参数为P2的PID,内核从这个PID导出其对应的进程描述符,然后从P2的进程描述符中取出记录挂起信号的数据结构指针。顺序扫描进程描述符的pid字段是相当低效的,为了加速查找,引入了4个散列表,分别为: 38 | 39 | Hash表的类型 | 说明 40 | ------------ | ------------ 41 | PIDTYPE_PID | 进程的PID 42 | PIDYTPE_TGID | 进程组的组长的PID 43 | PIDTYPE_PGID | 进程组的组长的PID 44 | PIDTYPE_SID | 会话组长的PID 45 | 46 | 内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入*pid_hash*数据组,一个散列表的长度依赖于可用的RAM的容量。用*pid_hashfn*把PID转换为表索引。*pid_hash*函数并不一定能保证与表的索引一一对应,不同的PID可能会存在相同的key,就会发生冲突。Linux利用链表来处理冲突。 47 | 48 | ### 进程复制 ### 49 | 50 | 传统的UNIX中用于复制进程的系统调用是*fork*,但它并不是Linux为此实现的唯一调用,Linux实现了3个。 51 | 52 | (1) *fork*是重量级调用,因为它建立了父进程的一个完整副本,然后作为子进程。 53 | 54 | (2) *vfork[^1]*类似于fork,但并不创建父进程数据的副本,相反,父子进程共享数据,节省了大量的CPU。vfork设计用于子进程形成后立即执行*execve*系统调用,在子进程退出或开始新程序之前,父进程处于堵塞状态。 55 | 56 | (3) *clone*用于产生线程,可以堆父子进程之间的共享、复制进行精确控制。 57 | 58 | 所有的3个fork机制最终都调用了kernel/fork.c中的*do_fork*函数,在*do_fork*中,大多数工作都是由*copy_process*函数完成的。 59 | 60 | ### 写时复制 ### 61 | 62 | 内核使用了写时复制(*Copy-On-Write, COW*)技术,为防止在fork执行时将父进程的所有数据复制到子进程。因为这样的操作使用的很长的时间,并且耗费了大量的内存。如果应用程序在进程复制之后使用exec立即加载新程序,那么就表明之前的操作完全时没有必要的,因为拷贝了父进程的数据,但立即刷出内存用于新程序。所以内核不复制进程的整个地址空间,而是只复制其页表,这样就建立了虚拟地址空间和物理内存页之间的联系。 63 | 64 | 当父子进程不允许修改彼此的页,只能够共享的读取数据,则可以共享内存区域。如果任意一个进程试图向复制的内存页中写入数据,那么处理器会向内核报告访问错误,这类错误通常会被视作缺页异常。内核然后查看额外的内存管理数据结构,检查该页是否可以用读写模式访问。如果该页只能以只读模式访问,则向应用程序报告段错误,如果是可写的,内核会创建该页专用于当前进程的副本,并与父进程独立开。 65 | 66 | [^1]: 由于写时复制技术,现在不推荐使用vfork。 67 | -------------------------------------------------------------------------------- /2014-04-10-process-waiting-link-list.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 组织进程 4 | category: 进程 5 | description: 组织进程... 6 | tags: 等待队列 互斥进程 7 | --- 8 | 运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起,当要求把其他状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之。 9 | 10 | 没有为处于TASK\_STOPPED、EXIT\_ZOMBIE或EXIT\_DEAD状态的进程建立专门的链表。由于对处理暂停、僵死、死亡状态的进程访问比较简单,或者通过PID,或者通过特定父进程的子进程链表,所以不必堆这三种状态进行分组。 11 | 12 | 根据不同的特殊事件把处于TASK\_INTERRUPTIBLE或TASK\_UNINTERRUPTIBLE状态的进程细分为许多类,每一类都对应某个特殊的事件。在这种情况下,进程状态提供的信息满足不了快速检索进程的需要,所以必须引入另外的进程链表,这些链表被称作等待队列。 13 | 14 | ### 等待队列 ### 15 | 16 | 等待队列在内核中有很多用途,尤其用在中断处理、进程同步以及定时。这里并不详细解释,但进程必须经常等待某些事件的发生,例如等待I/O操作中止,等待释放系统资源,或者等待时间经过固定的间隔。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放进和式的等待队列,并放弃控制权。因为,等待队列表示一组睡眠的进程,当某个条件触发,内核会唤醒这些等待队列。 17 | 18 | 等待队列由双向链表实现[^1]。元素包括指向进程描述符的指针,每个等待队列都有一个等待队列头(*wait queue head*),等待队列头是一个类型为*wait_queue_head_t*的数据结构: 19 | 20 | #### #### 21 | 22 | {% highlight c++ %} 23 | struct __wait_queue_head { 24 | spinlock_t lock; 25 | struct list_head task_list; 26 | }; 27 | typedef struct __wait_queue_head wait_queue_head_t; 28 | {% endhighlight %} 29 | 30 | 因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护以免对其进行同时访问,因为同时访问会导致不可预测的后果。同步是通过等待队列头中的*lock*自旋锁实现的。*task_list*字段是等待进程链表的头。 31 | 32 | 等待进程链表中的元素类型为wait\_queue\_t,定义如下: 33 | 34 | #### #### 35 | 36 | {% highlight c++ %} 37 | struct __wait_queue { 38 | unsigned int flags; 39 | struct task_struct * task; 40 | wait_queue_func_t func; 41 | struct list_head task_list; 42 | }; 43 | typedef struct __wait_queue wait_queue_t; 44 | {% endhighlight %} 45 | 46 | 等待队列链表中的每个元素代表一个睡眠的进程,该进程等待某一事件的发生,它的描述符地址存放在*task*字段中,*task_list*字段中包含的是指针,由这个指针把一个元素链接到等待相同事件的进程链表中。 47 | 48 | 有时候,唤醒等待队列中的睡眠的进程有时并不方便,例如,如果两个或多个进程在等待互斥访问某一要释放的资源,仅仅唤醒一个进程才有意义。这个进程占有资源,而其他进程继续睡眠。否则唤醒多个进程只为了竞争一个资源,而这个资源只有一个进程能访问,结果其他进程必须再回到睡眠状态。 49 | 50 | 因此,有两种睡眠进程:互斥进程[^2]由内核有选择地唤醒,而非互斥进程[^2]总是由内核在事件发生时唤醒。等待访问临界资源的进程就是互斥进程的典型例子。 51 | 52 | [^1]: 双向链表真是内核里无所不在的数据结构,树也是。 53 | 54 | [^2]: wait\_queue\_t结构提里flag字段为1为互斥进程,为0则为非互斥进程。 55 | -------------------------------------------------------------------------------- /2014-04-10-the-kernel-thread.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内核线程 4 | category: 进程 5 | description: 内核线程... 6 | tags: 内核线程 idel swapper init 守护进程 7 | --- 8 | 内核线程(*kernel thread*)是直接由内核本身启动的进程,内核线程实际上是将内核函数委托给独立的进程,与系统中其他进程『并行』执行。内核线程经常被称之为守护进程。它们用于执行下列任务: 9 | 10 | 1. 周期性地将修改的内存页与页面来源块设备同步。 11 | 2. 如果内存页很少使用,则写入交换区。 12 | 3. 管理延时动作。 13 | 4. 实现文件系统的事物日志。 14 | 15 | 基本上,有两种类型的内核线程: 16 | 17 | 1. 线程启动后一直等待,直至内核请求线程执行某一特定的操作。 18 | 2. 线程启动后按周期性间隔运行,监测特定的资源使用,在用量超出或者低于预置的限制时采取行动。 19 | 20 | 这些任务包括刷新磁盘高速缓存,交换出不用的页框,维护网络连接等等。实际上,如果以严格的线性方式执行这些任务效率不高,如果把它们放在后台调度,则会有较好的效率。这些任务委托给内核线程,内核线程不受不必要的用户态上下文的拖累,内核线程和普通进程的区别有: 21 | 22 | 1. 内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。 23 | 2. 因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。普通进程可以使用4GB的线性地址空间。 24 | 25 | 调用kernel_thread函数可以启动一个内核线程。 26 | 27 | #### #### 28 | {% highlight c++ %} 29 | int kernel_thread(int (*fn)(void *), void * arg, 30 | unsigned long flags) 31 | {% endhighlight %} 32 | 33 | 产生的线程将执行*fn*指针传递的函数,以便内核线程可以根据函数的不同而执行不同的函数,而用*arg*指定的参数将自动化传递给该函数。 34 | 35 | 大多数计算机上的系统的全部虚拟地址空间分成两部分:地步可以由用户层程序访问,上部则专供内核使用。在内核代表用户层运行时,虚拟地址空间的用户空间部分有*mm*指向*mm_struct*实例描述。每当内核执行上下文切换时,虚拟地址空间的用户层部分都会切换,以便与当前运行的进程匹配。 36 | 37 | 这为优化提供了一些方法,可遵循所谓的惰性TLB处理。由于内核线程不与任何特定的用户层进程相关,内核不需要倒换虚拟地址空间的用户层部分。由于内核线程之前可能有其他用户层进程在执行,因此用户空间部分的内存本质上时随机的。内核线程绝不能修改其内容。 38 | 39 | 为强调用户空间部分不能访问,*mm*设置为空指针。但由于内核必须知道用户空间当前包含了什么,所以在*active_mm*中保存了指向*mm_struct*的一个指针来描述它。 40 | 41 | 内核线程可以用两种方法实现。第一种是将一个函数直接传递给kernel_thread,这个函数接下来负责帮助内核调用daemonize以转换为守护进程。将触发下列操作。 42 | 43 | 1. 该函数从内核线程释放其父进程的所有资源,因为守护进程只操作内核地址区域,不需要这些资源。 44 | 2. daemonize阻塞信号的接受。 45 | 3. 将init用作守护进程的父进程。 46 | 47 | *kthread_create*帮助创建内核线程。 48 | 49 | #### #### 50 | 51 | {% highlight c++ %} 52 | struct task_struct *kthread_create(int (*threadfn)(void *data), 53 | void *data, 54 | const char namefmt[], 55 | ) 56 | {% endhighlight %} 57 | 58 | 另一个被选方案是使用宏指令*kthread_run*,参数与*thread_create*相同。它会调用*kthread_create*创建新线程,但是立即唤醒它。 59 | 60 | ### 进程0 ### 61 | 62 | 所有进程的祖先叫做进程0,*idle*进程,因为历史原因也可叫做*swapper*进程,它是在Linux的初始化阶段从无到有创建的一个内核线程。 63 | 64 | ### 进程1 ### 65 | 66 | 由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用转入可执行程序*init*,*init*内核线程变为一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,*init*进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。 67 | -------------------------------------------------------------------------------- /2014-04-11-clone-fork-and-vfork.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 进程复制 4 | category: 进程 5 | description: clone()、fork()以及vfork()... 6 | tags: clone fork vfork 写时复制 do_fork copy_process pidhash 7 | --- 8 | 系统进程复制通常使用clone()、fork()以及vfork()系统调用。 9 | 10 | 这里只针对这几个函数做了少量的笔记,实际上在原书中有大量的细节讲解。建议打开**和原书中的解析一一对比。这里仅仅是笔记,我认为比较有价值的部分。如果不感兴趣,可以跳过,只要知道有这几种创建进程的方式即可。 11 | 12 | 在Linux中,轻量级进程是由clone()函数创建的,这个函数使用下列参数。 13 | 14 | 字段名 | 说明 15 | ------------ | ------------- 16 | fn | 指定一个由新进程执行的函数,当这个函数返回时,子进程终止。函数返回一个整数,表示子进程的退出代码 17 | arg | 指向传递给fn()函数的数据 18 | flags | 低字节指定子进程结束时发送到父进程的信号代码,通常为SIGCHLD信号 19 | child_stack | 表示把用户态堆栈指针赋给子进程esp寄存器。调用进程应该总时为子进程分配新的堆栈。 20 | tls | 表示线程局部存储段TLS数据结构的地址,该结构时为新轻量级进程定义的,只有在CLONE_SETTLE标志被设置时才有意义 21 | ptid | 表示父进程的用户态变量地址,该父进程具有与新轻量级进程相同的PID,只有在CLONE\_PARENT\_SETTID标志被设置时才有意义 22 | ctid | 表示新轻量级进程的用户态变量地址,该进程具有这一类进程的PID,只有在CLONE\_CHILD\_SETTID标志被设置时才有意义 23 | 24 | 实际上,clone()是在C语言库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏的clone()系统调用。实现clone()系统调用的sys_clone()服务里程没有fn和arg参数。 25 | 26 | 封装函数把fn指针存放在子进程堆栈的某个位置处,该位置就是该封装函数本身返回地址存放的位置。*arg*指针正好存放在子进程堆栈中*fn*的下面,当封装函数结束时,CPU从堆栈中取出返回地址,然后执行*fn(arg)*函数。 27 | 28 | 传统的fork()系统调用时在Linux中是用clone()实现的,其中clone()的flags参数指定为SIGCHLD信号及所有清0的clone标志,而它的child_stack参数是父进程当前的堆栈指针。因此,父进程和子进程暂时共享同一个用户堆栈。而由于写时复制技术,当任何一个进程试图改变栈,则立即各自得到用户堆栈的一份拷贝。 29 | 30 | ### do_fork()函数 ### 31 | 32 | do\_fork()函数负责处理clone()、fork()和vfork()系统调用,也就是说,无论是clone()、fork()还是vfork(),都会调用do\_fork()函数。其参数列表如下: 33 | 34 | 字段名 | 说明 35 | ------------ | ------------- 36 | clone_flags | 与clone()函数的flag参数相同 37 | stack_start | 与clone()函数的child\_stack参数相同 38 | regs | 指向通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的 39 | stack_size | 未使用,总是被设置为0 40 | 41 | 函数的原型如下: 42 | 43 | #### #### 44 | 45 | {% highlight c++ %} 46 | long do_fork(unsigned long clone_flags, 47 | unsigned long stack_start, 48 | struct pt_regs *regs, 49 | unsigned long stack_size, 50 | int __user *parent_tidptr, 51 | int __user *child_tidptr){ 52 | } 53 | {% endhighlight %} 54 | 55 | 所有的3个fork机制都调用了do_fork这个与体系结构无关的函数,代码流程图如下: 56 | 57 | ![system](images/do_fork.png) 58 | 59 | 其中执行了下列操作: 60 | 61 | 通过查找pidmap\_array位图,为子进程分配新的PID。然后检查父进程的ptrace字段,如果它的值不等于0,说明有另外一个进程正在跟踪父进程,因而,do\_fork()检查debugger程序是否自己想跟踪子进程。如果子进程不是内核线程,那么do\_fork()设置CLONE\_PTRACE标志。 62 | 63 | 调用copy_process()复制进程描述符,需要所有资源都是可用的,并返回进程描述符地址。 64 | 65 | 如果设置了CLONE\_STOPPED标志,或者必须跟踪子进程,则子进程的状态设置为TASK\_STOPPED,否则,调用*wake_up_new_task()*函数执行。 66 | 67 | 如果设置了CLONE\_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间[^1]。 68 | 69 | [^1]: vfork设计用于子进程形成后立即执行*execve*系统调用,在子进程退出或开始新程序之前,父进程处于堵塞状态。 70 | 71 | 其中一个重要的函数是*copy_process*。 72 | 73 | #### #### 74 | 75 | {% highlight c++ %} 76 | static struct task_struct *copy_process( 77 | unsigned long clone_flags, 78 | unsigned long stack_start, 79 | struct pt_regs *regs, 80 | unsigned long stack_size, 81 | int __user *child_tidptr, 82 | struct pid *pid, 83 | int trace){ 84 | } 85 | {% endhighlight %} 86 | 87 | 函数流程图如下: 88 | 89 | {:.center} 90 | ![system](images/copy_process.png){:style="max-width:300px"} 91 | 92 | 该函数执行下列操作: 93 | 94 | 检查参数clone\_flags锁传递的标志的一致性,在某种情况下会返回错误代号。没有问题,通过调用*security_task_create()*以及*security_task_alloc()*执行所有附加的安全检查。完成后调用*dup_task_struct()*为子进程获取进程描述符,检查[资源限制](/linux-kernel-architecture/2014/03/30/process-descriptor/)并递增计数器,更新PID并存入*tsk->pid*字段。复制/共享进程的各个部分代码包括拷贝文件,命名空间。然后初始化进程的亲子关系。 95 | 96 | 完成后执行SET\_LINK宏,把新进程描述符插入进程链表。调用*attach\_pid()*把新进程描述符的PID插入到pidhash散列表。经过一系列繁琐的检查和线程组操作后返回进程描述符指针。 97 | 98 | 其中『复制/共享进程的各个部分代码包括拷贝文件,命名空间』相关的处理代码如下: 99 | 100 | {% highlight c++ %} 101 | if ((retval = audit_alloc(p))) 102 | goto bad_fork_cleanup_policy; 103 | /* copy all the process information */ 104 | if ((retval = copy_semundo(clone_flags, p))) 105 | goto bad_fork_cleanup_audit; 106 | if ((retval = copy_files(clone_flags, p))) 107 | goto bad_fork_cleanup_semundo; 108 | if ((retval = copy_fs(clone_flags, p))) 109 | goto bad_fork_cleanup_files; 110 | if ((retval = copy_sighand(clone_flags, p))) 111 | goto bad_fork_cleanup_fs; 112 | if ((retval = copy_signal(clone_flags, p))) 113 | goto bad_fork_cleanup_sighand; 114 | if ((retval = copy_mm(clone_flags, p))) 115 | goto bad_fork_cleanup_signal; 116 | if ((retval = copy_namespaces(clone_flags, p))) 117 | goto bad_fork_cleanup_mm; 118 | if ((retval = copy_io(clone_flags, p))) 119 | goto bad_fork_cleanup_namespaces; 120 | {% endhighlight %} 121 | 122 | 其中部分拷贝的意义分别为: 123 | 124 | 字段名 | 说明 125 | ------------ | ------------- 126 | copy\_semundo | 如果COPY\_SYSVSEM置位,则使用父进程的System V信号量 127 | copy\_fs | 如果CLONE\_FILES置位,则使用父进程的文件描述符,否则创建新的files结构,其中包含的信息与父进程相同。该信息的修改可以独立于原结构 128 | copy\_sighand | 如果CLONE\_THREAD置位,则使用父进程的信号处理程序 129 | copy\_signal | 如果CLONE\_THREAD置位,则与父进程共同使用信号处理中不特定于处理程序的部分 130 | copy\_mm | 如果COPY\_MM置位,则让父进程和子进程共享同一地址空间 131 | copy\_namespace | 有特别的调用语意,建立于子进程的命名空间 132 | copy\_thread | 这是一个特定于体系结构的函数,用于复制进程中特定于线程的数据 133 | 134 | 这里的特定于线程并不是指某个CLONE标志,也不是指操作堆线程而非整个进程执行。其语意无非是指复制执行上下午中特定于体系结构的所有数据。 135 | 136 | 重要的是填充*task_struct->thread*的各个成员,这是一个*thread_struct*类型的结构,其定义是体系结构相关的,需要深入了解各种CPU的相关知识。 137 | -------------------------------------------------------------------------------- /2014-04-11-execve-replace-process.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 进程替换 4 | category: 进程 5 | description: 替换进程... 6 | tags: 进程替换 execve 7 | --- 8 | 可以通过新代码替换现存的程序,即可以启用新程序。Linux提供的execve系统调用可以用于该目的。C标准库中有其他的exec变体,但最终都基于execve。 9 | 10 | execve的入口点是体系结构相关的sys\_execve函数,该函数很快将工作委托给体系结构无关的do\_execve例程。 11 | 12 | #### #### 13 | 14 | {% highlight c++ %} 15 | int do_execve(char * filename, 16 | char __user *__user *argv, 17 | char __user *__user *envp, 18 | struct pt_regs * regs){ 19 | } 20 | {% endhighlight %} 21 | 22 | 这里不仅用参数传递了寄存器集合和可执行文件的名称(*filename*),而且还传递了指向程序的参数和环境的指针,这里的记号稍微有些笨拙,因为argv和envp都是指针数组,而且指向的两个数组自身的指针以及数组中的所有指针都位于虚地址的用户空间部分。 23 | 24 | do_execve代码流程图如下: 25 | 26 | ![system](images/execve.png) 27 | 进程替换流程图 28 | 29 | 首先打开要执行的文件,内核找到相关的inode并生成一个文件描述符,用于寻址该文件。bprm\_init接下来处理若干管理性任务,例如mm\_alloc生成一个新的mm\_struct实例来管理进程地址空间。init\_new\_context是一个特定于体系结构的函数,用于初始化该实例,而\_\_bprm\_mm\_init则建立初始的栈。 30 | 31 | prepare_binprm用于提供一些父进程相关的值,特别是有效的UID和GID,剩余的数据,参数列表直接复制到结构中。简约代码如下: 32 | 33 | #### #### 34 | 35 | {% highlight c++ %} 36 | int prepare_binprm(struct linux_binprm *bprm) 37 | { 38 | if (!(bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID)) { 39 | /* Set-uid? */ 40 | if (mode & S_ISUID) { 41 | bprm->per_clear |= PER_CLEAR_ON_SETID; 42 | bprm->cred->euid = inode->i_uid; 43 | } 44 | 45 | /* Set-gid? */ 46 | /* 47 | * If setgid is set but no group execute bit then this 48 | * is a candidate for mandatory locking, not a setgid 49 | * executable. 50 | */ 51 | if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) { 52 | bprm->per_clear |= PER_CLEAR_ON_SETID; 53 | bprm->cred->egid = inode->i_gid; 54 | } 55 | } 56 | } 57 | {% endhighlight %} 58 | 59 | 在确认文件来源卷在装载是没有置位MNT\_NOSUID之后,内核会监测SUID或SGID是否置位。第一种情况很容易处理,如果S\_ISUID置位,那么有效的UID和inode相同,否则使用进程的有效UID。SGID类似,但内核还需要确认组执行位也已经置位。 60 | 61 | search\_binary\_handler用于在do\_execve结束是查找一种适当的二进制格式,用于所要执行的特定文件。可以看作根据不同的可执行文件格式来选择适当的程序。二进制格式处理程序负责将新程序的数据加载到旧的地址空间中。通常,二进制格式处理程序执行下列操作。 62 | 63 | 1. 释放原进程使用的所有资源。 64 | 2. 将应用程序隐射到虚拟地址空间中。 65 | 3. 设置进程的指令指针和其他特定于体系结构的寄存器,以便在调度程序选择该进程的时候开始执行。 -------------------------------------------------------------------------------- /2014-04-11-group-exit-and-do-exit-a-process.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 进程退出 4 | category: 进程 5 | description: 进程退出 6 | tags: 进程撤销 do_group_exit do_exit 7 | --- 8 | 很多进程终止了它们需要执行的代码,这些进程就已经结束了,当这种情况发生时,就必须通知内核以便内核释放进程所拥有的资源,包括内存、打开的文件描述符、信号量之类的东西。 9 | 10 | 进程终止的一般方式时调用exit()库函数,该函数释放C函数库锁分配的资源,执行编程者锁注册的每一个函数,并结束从系统回收进程的那个系统调用。exit()函数可能由编程者显式地插入。另外,C编译程序总是把exit()函数插入到main()函数的最后一条语句。 11 | 12 | 当进程接收到一个不能处理或忽视的信号时,或者当内核正在代表进程运行时在内核态产生一个不可恢复的CPU异常时,内核可以有选择地强迫整个线程组死掉。 13 | 14 | 进程终止可以使用两个系统调用: 15 | 16 | 1. exit_group()系统调用,终止整个线程组。 17 | 2. exit()系统调用,终止某个线程。 18 | 19 | ### do\_group\_exit()函数 ### 20 | 21 | 该函数代码如下: 22 | 23 | #### #### 24 | 25 | {% highlight c++ %} 26 | NORET_TYPE void 27 | do_group_exit(int exit_code) 28 | { 29 | struct signal_struct *sig = current->signal; 30 | 31 | BUG_ON(exit_code & 0x80); /* core dumps don't get here */ 32 | 33 | if (signal_group_exit(sig)) 34 | exit_code = sig->group_exit_code; 35 | else if (!thread_group_empty(current)) { 36 | struct sighand_struct *const sighand = current->sighand; 37 | spin_lock_irq(&sighand->siglock); 38 | if (signal_group_exit(sig)) 39 | /* Another thread got here before we took the lock. */ 40 | exit_code = sig->group_exit_code; 41 | else { 42 | sig->group_exit_code = exit_code; 43 | sig->flags = SIGNAL_GROUP_EXIT; 44 | zap_other_threads(current); 45 | } 46 | spin_unlock_irq(&sighand->siglock); 47 | } 48 | 49 | do_exit(exit_code); 50 | /* NOTREACHED */ 51 | } 52 | {% endhighlight %} 53 | 54 | do\_group\_exit()函数杀死属于current线程组的所有进程,它接受进程的终止代码号作为参数,进程终止代码号可能时系统调用exit\_group()指定的一个值,也可以时内核提供的一个代码号。通常情况下exit\_group()说明进程正常中止,而内核提供的代码号通常表示进程异常结束。 55 | 56 | 代码检查退出进程的SIGNAL\_GROUP\_EXIT标志是否不为0,如果不为0,说明内核已经开始为线程组执行退出过程,exit\_code直接为*current->signal->group_exit_code*。否则,设置进程的SIGNAL\_GROUP\_EXIT标志并把终止代码号存放到*current->signal->group_exit_code*中。 57 | 58 | 调用*zap_other_threads()*函数杀死*current*线程组中的其他进程,扫描与*current->tgid*对应的PIDTYPE_TGID类型的散列表中的每个PID链表,向表中所有不同于current的进程发送SIGKILL信号,以便每个进程都能执行do\_exit()函数。 59 | 60 | 最终调用do_exit()函数。 61 | 62 | ### do\_exit()函数 ### 63 | 64 | do\_exit()函数体比较大,并且涉及的知识点过多,所以简单记录一下笔记: 65 | 66 | #### #### 67 | 68 | {% highlight c++ %} 69 | /* 70 | * 实际上do_exit做的比我们想象的要多 71 | * 虽然是退出一个进程,但要清除进程所 72 | * 有使用的资源,包括进程自身,还需要 73 | * 注意读写保护,进程组等多种复杂的结 74 | * 构。 75 | */ 76 | NORET_TYPE void do_exit(long code){ 77 | struct task_struct *tsk = current; 78 | int group_dead; 79 | 80 | profile_task_exit(tsk); 81 | 82 | WARN_ON(atomic_read(&tsk->fs_excl)); 83 | 84 | if (unlikely(in_interrupt())) 85 | panic("Aiee, killing interrupt handler!"); 86 | if (unlikely(!tsk->pid)) 87 | panic("Attempted to kill the idle task!"); 88 | 89 | set_fs(USER_DS); 90 | 91 | tracehook_report_exit(&code); 92 | 93 | validate_creds_for_do_exit(tsk); 94 | 95 | // 表示进程正在被删除 96 | if (unlikely(tsk->flags & PF_EXITING)) { 97 | printk(KERN_ALERT 98 | "Fixing recursive fault but reboot is needed!\n"); 99 | tsk->flags |= PF_EXITPIDONE; 100 | set_current_state(TASK_UNINTERRUPTIBLE); 101 | schedule(); 102 | } 103 | 104 | exit_irq_thread(); 105 | 106 | exit_signals(tsk); 107 | smp_mb(); 108 | spin_unlock_wait(&tsk->pi_lock); 109 | 110 | if (unlikely(in_atomic())) 111 | printk(KERN_INFO "note: %s[%d] exited " \ 112 | "with preempt_count %d\n", 113 | current->comm, task_pid_nr(current), 114 | preempt_count()); 115 | 116 | acct_update_integrals(tsk); 117 | 118 | group_dead = atomic_dec_and_test(&tsk->signal->live); 119 | if (group_dead) { 120 | hrtimer_cancel(&tsk->signal->real_timer); 121 | exit_itimers(tsk->signal); 122 | if (tsk->mm) 123 | setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm); 124 | } 125 | acct_collect(code, group_dead); 126 | if (group_dead) 127 | tty_audit_exit(); 128 | if (unlikely(tsk->audit_context)) 129 | audit_free(tsk); 130 | 131 | tsk->exit_code = code; 132 | taskstats_exit(tsk, group_dead); 133 | 134 | exit_mm(tsk); 135 | 136 | if (group_dead) 137 | acct_process(); 138 | trace_sched_process_exit(tsk); 139 | 140 | exit_sem(tsk); 141 | exit_files(tsk); 142 | exit_fs(tsk); 143 | check_stack_usage(); 144 | exit_thread(); 145 | cgroup_exit(tsk, 1); 146 | 147 | if (group_dead && tsk->signal->leader) 148 | disassociate_ctty(1); 149 | 150 | module_put(task_thread_info(tsk)->exec_domain->module); 151 | 152 | proc_exit_connector(tsk); 153 | perf_event_exit_task(tsk); 154 | 155 | exit_notify(tsk, group_dead); 156 | #ifdef CONFIG_NUMA 157 | mpol_put(tsk->mempolicy); 158 | tsk->mempolicy = NULL; 159 | #endif 160 | #ifdef CONFIG_FUTEX 161 | if (unlikely(current->pi_state_cache)) 162 | kfree(current->pi_state_cache); 163 | #endif 164 | debug_check_no_locks_held(tsk); 165 | tsk->flags |= PF_EXITPIDONE; 166 | 167 | if (tsk->io_context) 168 | exit_io_context(tsk); 169 | 170 | if (tsk->splice_pipe) 171 | __free_pipe_info(tsk->splice_pipe); 172 | 173 | validate_creds_for_do_exit(tsk); 174 | 175 | preempt_disable(); 176 | exit_rcu(); 177 | tsk->state = TASK_DEAD; 178 | schedule(); 179 | BUG(); 180 | for (;;) 181 | cpu_relax(); 182 | } 183 | {% endhighlight %} 184 | 185 | 所有的进程的终止都是由do\_exit()函数来处理,这个函数从内核数据结构中删除堆终止进程的大部分引用,同样,do\_exit()函数接受终止代号作为参数执行。 186 | 187 | ![system](images/exit.png) 188 | 进程终止流程图 189 | 190 | 该函数执行了下列操作: 191 | 192 | 把进程描述符flag字段设置为PF\_EXITING标志,表示进程正在被删除。如果需要,通过函数*del\_timer\_sync()*从动态定时器队列中删除进程描述符。 193 | 194 | 分别调用exit\_mm()、exit\_sem()、\_\_exit\_files()、\_\_exit\_fs()、exit\_namespace()和exit\_thread()函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O位图相关的数据结构,如果没有其他进程共享这些数据结构,那么这些函数还删除所有这些这些数据结构中的数据。 195 | 196 | 如果实现了被杀死进程的执行域和可执行格式的内核函数包含在内核模块中,则函数递减计数器。 197 | 198 | 把进程描述符的exit\_code字段设置成进程的终止戴好。这个值要么是\_exit()或exit\_group()系统调用参数,要么是内核提供的错误代码。 199 | 200 | 调用exit_notify()函数,如果出问题,则变为僵尸进程[^1]。完成后调用schedule()函数选择一个新进程运行。 201 | 202 | [^1]: 僵尸进程最后由内核改变其父进程为init进程,最终由init进程释放资源。 -------------------------------------------------------------------------------- /2014-04-13-memory-address.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内存地址 4 | category: 内存寻址 5 | description: 内存地址... 6 | tags: 内存寻址 逻辑地址 线性地址 物理地址 内存控制单元 MMU 7 | --- 8 | 现代的操作系统自身不必完全了解物理内存;如今的微处理器包含的硬件线路使得内存管理既高效又健壮,所以编程错误就不会对该程序之外的内存产生非法访问。 9 | 10 | 陈许愿偶尔会引用内存地址(*memory address*)作为访问单元内容的一种方式,但是,当使用80x86微处理器时,必须区分三种不同的地址: 11 | 12 | ### 逻辑地址(*logical address*) ### 13 | 14 | 包含在机器语言指令中用来规定一个操作数或一条指令的地址。这种寻址方式在80x86著名的分段结构中表现的尤为具体,它促使MS-DOS或Windows程序员把程序分成若干段。每个逻辑地址都由一个段(*segment*)和偏移量(*offset*或*displacement*)组成,偏移量指明了从段开始的地方到实际地址之间的举例。 15 | 16 | ### 线性地址(*linear address*) ### 17 | 18 | 线性地址也成虚拟地址(*virtual address*)。线性地址是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值的范围从0x00000000到0xffffffff。 19 | 20 | ### 物理地址(*physical address*)### 21 | 22 | 用于内存芯片级内存单元寻址。它们与从微处理器的地址引脚发送到内存总线上的电信号相对应。物理地址由32位或36位无符号整数表示。 23 | 24 | ### 内存控制单元 ### 25 | 26 | 内存控制单元(*MMU*)通过一种称为分段单元(*segmentation unit*)的硬件电路把一个逻辑地址转换成线性地址;然后,第二个称为分页单元(*paging unit*)的硬件电路把线性地址转换成一个物理地址。 27 | 28 | ![system](images/mmu.png) 29 | 内存控制单元转换示意图 30 | 31 | 在多处理系统中,所有CPU都共享同一内存;这意味着RAM芯片可以由独立的CPU并发地访问。因为在RAM芯片上的读或写操作必须串行地执行,因此,一种所谓的内存仲裁器(*memory arbiter*)的硬件电路插在总线和每个RAM芯片之间。 32 | 33 | 内存仲裁器的作用是如果某个RAM芯片空闲,就准予一个CPU访问,如果该芯片忙于为另一个处理器提出的请求服务,就延迟这个CPU的访问。即使在单处理器上也使用内存仲裁器,因为单处理器系统中包含一个叫做DMA控制器的特殊处理器。而DMA控制器与CPU并发操作。 34 | 35 | 在多处理器系统的情况下,因为仲裁器有多个输入端口,所以其结构更加复杂。例如,双Pentium在每个芯片的入口维持一个两端口仲裁器,并在试图使用公用总线前请求两个CPU交换同步信息。但是从编程的角度来看,因为仲裁器由硬件电路管理,因此它是隐藏的。 36 | 37 | ### 硬件中的分段 ### 38 | 39 | 从80286模型开始,Intel微处理以两种不同的方式执行地址转换,这两种方式分别为实模式(*real mode*)和保护模式(*protected mode*)。现在主要讨论保护模式下的地址转换,实模式的存在主要原因是要维持处理器与早起模型兼容,并让操作系统自举。 -------------------------------------------------------------------------------- /2014-04-13-segment-selector.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 段选择符和段寄存器 4 | category: 内存寻址 5 | description: 段选择符和段寄存器... 6 | tags: 段选择符 段寄存器 特权级 CPL 用户态 内核态 7 | --- 8 | 一个逻辑地址由两部分组成:一个段标识符和一个指定段内相对地址的偏移量。段标识符是一个16位长的字段,称为段选择符(*Segment Selector*),而偏移量是一个32位长的字段。 9 | 10 | ![system](images/segment_selector.png) 11 | 段选择符 12 | 13 | 为了快速方便地找到段选择符,处理器提供段寄存器,段寄存器地唯一目的是存放段选择符。这些段寄存器称为cs、ss、ds、es、fs和gs。尽管只有6个段寄存器,但程序可以把同一个段寄存器用于不同地目的,这6个段寄存器3个有专门的用途: 14 | 15 | 1. cs:代码段寄存器,指向包含程序指令的段。 16 | 2. ss:栈段寄存器,指向包含当前程序栈的段。 17 | 3. ds:数据段寄存器,指向包含静态数据或者全局数据段。 18 | 19 | 其他三个段寄存器用作一般用途,可以指向任意的数据段。除此之外,cs寄存器还有一个很重要的功能:它含有一个两位的字段,用以指明CPU的当前特权级(*Current Privilege Level,CPL*)。值为0代表最高优先级,而值为3代表最低优先级。Linux只用0级和3级,分别称之为内核态和用户态。 20 | 21 | 段选择符所包含的3个字段及其意义为: 22 | 23 | 字段名 | 说明 24 | ------------ | ------------- 25 | index | 指定了放在GDT或LDT中的相应段描述符的入口 26 | TI | TI(*Table Indicator*)标志,指明了段描述符是在GDT中或是在LDT中[^1]。 27 | RPL | 请求者特权级,当相应的段选择符装入到cs寄存器中时指示出CPU当前的特权级,它还可以用于访问数据段时由选择地削弱处理器的特权级 28 | 29 | [^1]: TI=1则在LDT中,TI=0则在GDT中。 -------------------------------------------------------------------------------- /2014-04-14-segment-descriptor.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 段描述符 4 | category: 内存寻址 5 | description: 段描述符... 6 | tags: 段描述符 全局描述符表 代码段 数据段 任务状态段 TSS 7 | --- 8 | 每个段由一个8字节的段描述符(*Segment Descriptor*)表示,它描述了段的特征。段描述符放在全局描述符表(*Global Descriptor Table,GDT*)或局部描述符表(*Local Descriptor Table,LDT*)中。 9 | 10 | 通常只定义一个GDT,而每个进程除了存放在GDT中的段之外如果还需要创建附加的段,可以拥有自己的LDT。GDT在主存中的地址和大小存在*gdtr*控制寄存器中,当前正被使用的LDT地址和大小放在*ldtr*控制寄存器中。 11 | 12 | 有几种不同类型的段以及它们对应的段描述符,下面列出了Linux中广泛采用的类型: 13 | 14 | ### 代码段描述符 ### 15 | 16 | 表示这个段描述符代表一个代码段,它可以放在GDT或LDT中。该描述符置S标志为1,并且为非系统段。 17 | 18 | ### 数据段描述符 ### 19 | 20 | 表示这个段描述符代表一个数据段,它可以放在GDT或LDT中。该描述符置S标志为1,栈段是通过数据段实现的。 21 | 22 | ### 任务状态段描述符(*TSSD*) ### 23 | 24 | 表示这个段描述符代表一个任务状态段(*Task State Segment,TSS*),也就是说这个段用于保存处理器寄存器的内容。它只能出现在GDT中。根据相应的进程是否正在CPU上运行,其Type字段的值分别为11或9。这个描述符的S标志被置为0。 25 | 26 | ### 局部描述符表描述符(*LDTD*) ### 27 | 28 | 这个表示段描述符代表一个包含LDT的段,他置出现在GDT中。相应的Type字段的值为2,S标志被置为0。 29 | 30 | ![system](images/segment_descriptor.png) 31 | 段描述符示意图 32 | 33 | 相应字段的意义如下: 34 | 35 | 字段名 | 说明 36 | ------------ | ------------- 37 | Base | 包含段的首字节的线性地址 38 | G | 粒度标志,如果为0,则段大小以字节为单位,否则以4096字节的倍数计算 39 | Limit | 存放段最后一个内存单元的偏移量,从而决定段的长度。如果G被置为0,则一个段的大小在一个字节到1MB之间变化,否则,将在4KB到4GB之间变化 40 | S | 系统标志,如果被置为0,则这是一个系统段,否则为普通的代码段或者数据段 41 | Type | 描述了段的类型特征和它的存取权限 42 | DPL | 描述符特权等级字段,用于限制这个段的存取。它表示为访问这个段而要求的CPU最小的优先级,因此DPL设置为0的段只能当CPL为0时,也就是内核态才可以访问。DPL设为3则堆任何CPL值都是可访问的 43 | P | *Segment-Present标志*,等于0表示段当前不在主存中。Linux总是把此标志设为1,因为Linux从来不把整个段交换到磁盘上去 44 | D或B | 称为D或B标志,取决于是代码段还是数据段,D和B的含义在两种情况下有区别,如果段偏移量的地址是32位长,就基本上把它设置为1,如果偏移量是16位长,则清零 45 | AVL | 可以由操作系统使用,但是被Linux忽略 -------------------------------------------------------------------------------- /2014-04-14-visit-segment-descriptor.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 访问段描述符及分段单元 4 | category: 内存寻址 5 | description: 快速访问段描述符... 6 | tags: 段描述符 分段单元 7 | --- 8 | 由于逻辑地址由16位段选择符和32位偏移量组成,段寄存器仅仅存放段选择符。为了加速逻辑地址到线性地址的转换,80x86处理器提供一种附加的非编程的寄存器[^1],供6个可编程的段寄存器使用。每一个非编程的寄存器含有8个字节的段描述符,由相应的段寄存器中的段选择符来指定。 9 | 10 | [^1]: 一个不能被程序员所设置的寄存器。 11 | 12 | ### 快速访问段描述符 #### 13 | 14 | 每当一个段选择符被装入段寄存器时,相应的段描述符就由内存装入到对应的非编程CPU寄存器。从那时起,针对那个段的逻辑地址转换就可以不访问主存中的GDT或LDT。处理器只需直接引用存放段描述符的CPU寄存器即可。只有当段寄存器的内容改变时,才有必要访问GDT或LDT。 15 | 16 | ![system](images/visit_segment.png) 17 | 快速访问段描述符 18 | 19 | 由于一个段描述符是8字节长,因此它在GDT或LDT内的相对地址是由段选择符的最高13位的值乘以8[^3]得到的。例如如果GDT在0x00020000[^2],且由段选择符所指定的索引号为2,即段选择符中的index值为2.那么相应的段描述符地址是0x00020000 + (2*8)。能够保存在GDT中的段描述符的最大数目是8191,即2^13-1。 20 | 21 | [^2]: 这个值保存在gdtr寄存器中。 22 | 23 | [^3]: 一个段描述符的大小。 24 | 25 | 这样就可以在GDT中找到段描述符的地址。 26 | 27 | ### 分段单元 ### 28 | 29 | 下图显示了一个逻辑地址是怎样转换成相应的线性地址。 30 | 31 | ![system](images/segmentation.png) 32 | 分段单元执行的操作 33 | 34 | 分段单元(*segmentation*)执行下列操作: 35 | 36 | 先检查段选择符中的TI字段,以决定段描述符保存在哪一个描述符表中,TI=1则在LDT中,TI=0则在GDT中。如果在GDT中,分段单元从*gdtr*寄存器中得到GDT的线性地址,否则从*ldtr*寄存器中得到LDT的线性地址。 37 | 38 | 从段选择符的*index*字段计算段描述符的地址,*index*字段的值乘以8,这个结果与*gdtr*或*ldtr*的内容相加。 39 | 40 | 把逻辑地址的偏移量与段描述符的*Base*字段的值相加就得到了线性地址。 -------------------------------------------------------------------------------- /2014-04-15-segment-in-linux.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Linux中的分段 4 | category: 内存寻址 5 | description: Linux中的分段... 6 | tags: 分段 GDT 全局描述符表 RPL TLS 7 | --- 8 | 80x86处理器中的分段鼓励程序员把程序分成逻辑上相关的实体,比如子程序或者全局与局部的数据区。然而,Linux以非常有限的方式使用分段,实际上分段和分页某种程度上有点多余,因为它们都可以划分进程的物理地址空间。分段可以给每一个进程分配不同的线性地址空间,而分页可以把同一线性地址空间映射到不同的物理空间。与分段相比,Linux更喜欢分页的方式: 9 | 10 | * 当所有进程使用相同的段寄存器时,内存管理变得简单,它们可以共享同一组线性定制。 11 | * Linux设计目标之一时可以把它移植到绝大多数留下的处理器平台上,然而RISC[^1]体系结构对分段的支持有限。 12 | 13 | [^1]: 精简指令集系统,大多数系统还属于CISC(*complex instruction set computer*,复杂指令集)系统。 14 | 15 | 2.6版本的Linux只有在80x86结构下才需要使用分段。 16 | 17 | 运行在用户态的所有Linux进程都使用一对相对的段来对指令和数据寻址。这两个段就是所谓的用户代码段和用户数据段,类似地,运行在内核态的所有Linux进程都使用一对相同的段对指令和数据寻址:它们分别叫做内核代码段和内核数据段。 18 | 19 | 相应的段选择符由宏\_\_USER\_CS,\_\_USER\_DS,\_\_KERNEL\_CS和\_\_KERNEL\_DS分别定义。如果为了对内核代码段寻址,内核只需要把\_\_KERNEL\_CS装进cs段寄存器就好。而对内核数据段寻址,则将\_\_KERNEL\_DS装进ds段寄存器。 20 | 21 | 所有与段相关的线性地址从0开始,到达2^32-1的寻址限长。所以用户态或内核态下的所有进程可以使用相同的逻辑地址。而所有段从0开始,可以得出,在Linux下逻辑地址与线性地址是一致的,即**逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的**。 22 | 23 | CPU当前的特权级(*CPL*)反映了进程是在用户态还是内核态,并由存放在*cs*寄存器中的段选择符的*RPL*字段指定。只要当前特权级被改变,一些段寄存器必须相应地更新。例如CPL=3时,说明在用户态,*ds*寄存器必须含有用户端的段选择符,如果进入内核态,则CPL=0,*ds*寄存器则必须含有内核数据段的段选择符。 24 | 25 | 这种情况也出现在*ss*寄存器中,当CPL为3时,它必须指向一个用户数据段中的用户栈。当CPL=0时,它必须指向内核数据段中的一个内核栈。当状态转换时,Linux总是确保*ss*寄存器中装有相应权限的数据段的段选择符。 26 | 27 | 当对指向指令或者数据结构指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符。因为*cs*寄存器就含有当前的段选择符。例如,当内核调用一个函数时,它执行一条*call*汇编语言指令,该指令仅仅指定其逻辑地址的偏移量部分。而段选择符不用设置,它已经隐含在*cs*寄存器中了。因为在内核态执行的段只有一种,叫做代码段,由宏\_\_KERNEL\_CS定义。 28 | 29 | 所以只要当CPU切换到内核态时将\_\_KERNEL\_CS装载到*cs*就足够了。同样的道理也适用于指向内核数据结构的指针以及指向用户结构的指针。实际上这里只需要了解,每当CPU特权级更新,内核就应该保证相应的寄存器存放相应的段选择符。 30 | 31 | ### Linux GDT ### 32 | 33 | 在单处理器系统只有一个全局描述符表(*GDT*),在多处理器中每个CPU对应一个GDT。所有GDT都存放在*cpu_gdt_table*数组中,而所有GDT的地址和它们的大小被存放在*cpu_gdt_descr*数组中。在新的内核中更建议使用*get_cpu_gdt_table*函数,其参数为*cpu*的实例。 34 | 35 | GDT的布局如下,每个GDT包含18个段描述符和14个空的未使用的保留项。插入未使用的项时为了使经常一起访问的描述符能够处于同一32字节的硬件高速缓存中。 36 | 37 | ![system](images/gdt.png) 38 | GDT结构表 39 | 40 | 每一个GDT中包含的18个段描述符指向下列的段: 41 | 42 | 用户态和内核态下的代码段和数据段,如\_\_USER\_CS,\_\_USER\_DS,\_\_KERNEL\_CS和\_\_KERNEL\_DS。 43 | 44 | 任务状态段(*TSS*),每个处理器有1个,每个TSS相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集。所有的任务状态段都是顺序地存放在*init_tss*数组中。 45 | 46 | 1个包括缺省局部描述符表的段,这个段通常是被所有进程共享的段。 47 | 48 | 3个局部线程存储(*Thread-Local Storage,TLS*)段:这种机制允许多线程应用程序使用最多3个局部线程的数据段。系统调用*set_thread_area*和*get_thread_area*分别为正在执行的进程创建和撤销一个TLS段 49 | 50 | 与高级电源管理(*AMP*)相关的3个段,由BIOS代码使用。 51 | 52 | 与支持即插即用(*PnP*)功能的BIOS服务程序相关的5个段,由BIOS代码使用。 53 | 54 | 被内核用来处理双重错误[^2]异常的特殊TSS段。 55 | 56 | [^2]: 处理一个异常时可能引发另一个异常,这种情况下产生双重异常。 57 | 58 | 其实看图就已经非常好理解了。 59 | 60 | ### Linux LDT ### 61 | 62 | 大多数用户态下的Linux程序不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享。缺省的局部描述附表放在*default_ldt*数组中。它包含5个项,但内核仅仅有效地使用了其中的两个项:用于iBCS执行文件的调用门和Solaris/x86可执行文件的调用门[^3]。 63 | 64 | [^3]: 调用门是80x86微处理器提供的一种机制,用于在调用预定义函数时改变CPU的特权级。 65 | 66 | 在某些情况下,进程仍需要创建自己的局部描述符表,这对有些应用程序很有用,比如Wine,它们执行面向段的微软Windows应用程序。*modify_ldt()*系统调用允许进程创建自己的局部描述符表。 67 | 68 | 任何被*modify_ldt()*创建的自定义局部描述符表仍然需要它自己的段。当处理器开始执行拥有自定义局部描述符表的进程时,该CPU的GDT副本中的LDT表项相应地就被修改了。 69 | 70 | 用户态下地程序同样也利用*modify_ldt()*来分配新地段,但内核从不使用这些段,它也不需要了解相应地段描述符,因为这些段描述符被包含在进程自定义的局部描述符表中了。 71 | -------------------------------------------------------------------------------- /2014-04-15-system-paging-unit.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 硬件中的分页 4 | category: 内存寻址 5 | description: 硬件中的分页... 6 | tags: 页 页框 页表 页帧 分页 扩展分页 冷热页 7 | --- 8 | 分页单元(*paging unit*)把线性地址转换成物理地址,其中一个关键任务是把所请求的访问类型与线性地址的访问权限相比较,如果这次访问是无效的,就产生一个缺页异常。 9 | 10 | 为了效率起见,线性地址被分成以固定长度为单位的组,称为页(*page*)。页内部连续的线性地址被映射到连续的物理地址中。这样,内核可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限。 11 | 12 | **我们使用『页』既指一组线性地址,又指包含在这组地址中的数据。** 13 | 14 | 分页单元把所有的RAM分成固定长度的页框(*page frame*),有时候页叫做物理页。每一个页框包含一个页,也就是说一个页框的长度与一个页的长度一致。页框是主存的一部分因此也是一个存储区域。 15 | 16 | 区分一个页和一个页框[^page]很重要,页只是一个数据块,可以存放在任何页框或磁盘中。 17 | 18 | 把线性地址映射到物理地址的数据结构称为页表(*page table*),页表存放在主存中,并在启动分页单元之前必须由内核对页表进行适当的初始化。从80386开始,所有的80x86处理器都支持分页,它通过设置*cr0*寄存器的*PG*标志启用。当PG=0时,线性地址就被解释成物理地址。 19 | 20 | [^page]: 页是一组数据,页框是主存的物理地址。 21 | 22 | ### 页帧 ### 23 | 24 | 页帧代表系统内存的最小单位,对内存中的每个页都会创建*struct page*的实例。内核程序员需要保持该结构尽可能的小,因为即使在中等程度的内存配置下,系统的内存同样会分解位大量的页,只要增加少量的结构数据,就会成百上千的在系统中出现。例如系统标准页长度为4KB,主存大小为384MB的时候,会有大约100000个页,即便增加一个简单的整型指针,也会扩大上十万倍。 25 | 26 | 内存管理的许多部分都使用页,由于页的数目巨大,因此对*page*结构的小改动都会导致内存暴涨。而另一方面,由于页的广泛使用,增加了保持结构长度的南都,由于不同用图,内核一个部分可能完全依赖于*page*提供的特定信息,但该信息对内核的另一部分可能完全无用。 27 | 28 | C语言中的联合(*union*)很适合该问题,尽管没有办法增加*page*结构体的清晰度。代码如下: 29 | 30 | #### #### 31 | 32 | {% highlight c++ %} 33 | struct page { 34 | unsigned long flags; 35 | atomic_t _count; 36 | union { 37 | /* Count of ptes mapped in mms, 38 | * to show when page is mapped 39 | * & limit reverse map searches. 40 | */ 41 | atomic_t _mapcount; 42 | /* SLUB */ 43 | struct { 44 | u16 inuse; 45 | u16 objects; 46 | }; 47 | }; 48 | union { 49 | struct { 50 | unsigned long private; 51 | struct address_space *mapping; 52 | }; 53 | #if USE_SPLIT_PTLOCKS 54 | spinlock_t ptl; 55 | #endif 56 | struct kmem_cache *slab; 57 | struct page *first_page; 58 | }; 59 | union { 60 | pgoff_t index; 61 | void *freelist; 62 | }; 63 | struct list_head lru; 64 | #if defined(WANT_PAGE_VIRTUAL) 65 | void *virtual; 66 | #endif 67 | #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS 68 | unsigned long debug_flags; 69 | #endif 70 | 71 | #ifdef CONFIG_KMEMCHECK 72 | void *shadow; 73 | #endif 74 | }; 75 | {% endhighlight %} 76 | 77 | ### 页表 ### 78 | 79 | 层次化的页表用于支持对大地址空间的快速、高效的管理。 80 | 81 | > 我们知道页表用于建立用户进程的虚拟地址空间和系统物理内存之间的关联。到目前为止的结构主要用来描述内存的结构,例如划分的节点和内存域,同时指定了其中包含的页帧的数量和状态,页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区,该表也将虚拟内存映射到物理内存,因而支持共享内存的实现,还可以在不额外增加物理内存的情况下,将页换出到设备来增加有效的可用内存。 82 | 83 | 内核内存管理总是假定使用四级页表,而不管底层是否如此,但有些情况下,有些系统只使用两级分页页表系统,因此第三和第四级页表由特定于体系结构的代码模拟。 84 | 85 | 页表管理分为两个部分,第一部分依赖于体系结构,第二部分是体系结构无关。所有的数据结构和操作数据结构的几乎所有函数都是定义在特定于体系额机构的文件中。由于特定不同的CPU的实现差别比较大,所以不深入。 86 | 87 | ### 冷热页 ### 88 | 89 | *struct zone*的pageset成员用于实现冷热分配器(*hot-n-cold allocator*)。内核说页是热的,意味着页已经加载到CPU高速缓存,与在内存中的页相比,其数据能够尽快的访问。相反,冷页面则不在高速缓存中。 90 | 91 | ### 常规分页 ### 92 | 93 | 从80386开始,Intel处理器的分页单元处理4KB的页。32位的线性地址被氛围3个域: 94 | 95 | 1. *Directory(目录)*:最高10位。 96 | 2. *Table(页表)*:中间10位。 97 | 3. *Offset(偏移量)*:最低12位。 98 | 99 | 线性地址的转换分两步完成,每一步都基于一种转换表,第一种转换表称为页目录表(*page directory*),第二种转换表称为页表(*page table*)。 100 | 101 | 使用这种二级模式的目的在于减少每个进程页表所需RAM的数量。如果使用简单的一级页表,那将需要高达2^20个表项来表示每个进程的页表。即使一个进程并不使用那个范围内的素有地址。二级模式通过置位进程实际使用的哪些虚拟内存区请求页表来减少内存的使用量。 102 | 103 | 每个活动进程必须有一个分配给它的页目录。不过,没有必要马上位进程的所有页表都分配RAM,只有进程在实际需要一个页表是才给该页表分配RAM会更有效率。正在使用的页目录的物理地址存放在控制寄存器*cr3*中。线性地址内的*Directory*字段决定页目录中的目录项,而目录项指向适当的页表。 104 | 105 | 地址的*Table*字段依次又决定页表中的表项,而表项含有页所在页框的物理地址。*Offset*字段决定页框内的相对位置,如下图。 106 | 107 | ![system](images/paging_unit.png) 108 | 硬件中的分页 109 | 110 | *Directory*字段和*Table*字段都是10位长,因此页目录和页表都可以多大1024项,那么一个页目录可以寻址到高达2^32个存储单元。 111 | 112 | 页目录和页表项有相同的结构,每项都包含如下字段: 113 | 114 | 字段名 | 说明 115 | ------------ | ------------- 116 | Present | Present=1则说明所指的页或页表就在主存中,如果为0,则这一页不在主存中。这个表项的剩余的位可由操作系统用于自己的目的。如果执行一个地址转换所需的页表项或页目录项中的Present标志为0,那么分页单元就把该线性地址存放在寄存器cr2中,并产生缺页异常 117 | Field | 由于每个页框有4KB的容量,那么它的物理地址必须是4096的倍数,因此物理地址的最低12位总是为0.如果这个字段指向一个页目录,相应的页框就含有一个页表,如果它指向一个页表,相应的页框就含有一页数据 118 | Accessed | 每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时,这个标志就可以由操作系统使用 119 | Dirty | 只应用于页表项中,每当对一个页框进行写操作时就设置这个标志,与Accessed标志一样,当选中的页被交换出去时,这个标志就可以由操作系统使用 120 | Read/Write | 含有页或页表的存取权限 121 | User/Supervisor | 含有访问页或页表所需的特权级 122 | PCD/PWT | 控制硬件高速缓存处理页或页表的方式 123 | Page Size | 值应用于页目录项,如果设置为1,则页目录指定的是2MB或4MB的页框 124 | Global | 只应用于页表项,这个标志是在Pentium Pro引入的,用来防止常用页从TLB告诉缓存中刷新出去。只有在cr4寄存器的页全局启用(PGE)标志置位时这个标志才起作用。 125 | 126 | ### 扩展分页 ### 127 | 128 | 从Pentinum模型开始,80x86微处理器引入了扩展分页(*extended paging*),它允许页框大小为4MB而不是4KB。扩展分页用于把大段连续的线性地址转换成相应的物理地址。在这些情况下,内核可以不用中间页表进行转换,从而节省内存并保留TLB[^1]项。 129 | 130 | [^1]: 转换后援缓冲器。 131 | 132 | ![system](images/paging_unit_2.png) 133 | 扩展分页 134 | 135 | 如上图所述,通过设置页目录项的Page Size标志启用扩展分页乖哦能,在这种情况下,分页单元把32位线性地址分成两个字段: 136 | 137 | 1. Directory: 最高10位。 138 | 2. Offset: 其余22位。 139 | 140 | 扩展分页和正常分页的页目录基本相同,除了以下两点: 141 | 142 | 1. Page Size标志必须被设置。 143 | 2. 20位物理地址字段只有最高10位时有意义的,因为每一个物理地址都是在以4MB为辩解的地方开始,所以这个地址的低22位为0. 144 | 145 | 通过设置*cr4*处理器寄存器的PSE标志能使扩展分页与常规分页共存。 146 | 147 | 148 | ### 常规分页举例 ### 149 | 150 | 这里有一个简单的例子阐明常规分页是如何工作的,假定内核已给一个正在运行的进程分配的线性地址空间范围是0x200000000到0x2003ffff,这个空间正好由64个页组成[^2]。 151 | 152 | [^2]: 其实我们不必关心包含这些页的页框和物理地址,实际上,其中一些页甚至可能不在主存中,只需关注表项中剩余的字段。 153 | 154 | 我们从分配给进程的线性地址的最高10位,也就是*Directory*字段开始,这两个地址都以2开头后面跟着0,所以高10位有相同的值,即0x080或十进制的128[^3]。因此,这两个地址的*Directory*字段都指向进程页目录的第129项。相应的目录项中必须包含分配给该进程的页表的物理地址。如果没有,则页目录的其余1023项都填0. 155 | 156 | [^3]: 0x2003ffff的最高十位转换成二进制是0010000000,则为0x080,这里20和03f之间被隔断了,需要转换成二进制去理解。 157 | 158 | 中间10位的值,也就是*Table*字段,其范围从0到0x03f,十进制的从0到63,因而只有页表的前64项是有意义的,其余的960个表项都填0. 159 | 160 | 假设进程需要读线性地址0x20021406中的字节,这个地址由分页单元按下面方法处理: 161 | 162 | 1. *Directory*字段的0x80用于选择页目录的0x80目录项,此目录项指向和该进程的页相关的页表。 163 | 2. *Table*字段0x21用于选择页表的第0x21表项,指向了包含所需页的页框。 164 | 3. *Offset*字段0x406用于在目标页框中度偏移量位0x406中的字节。 165 | 166 | 如果0x21表项的*Present*标志为0,则说明此页不在主存当中,这种情况会产生一个缺页异常。当然,当进程试图访问任何超过0x200000000到0x2003ffff地址空间的范围之外的线性地址时,都会产生一个缺页异常。 -------------------------------------------------------------------------------- /2014-04-16-pae.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 扩展分页PAE机制 4 | category: 内存寻址 5 | description: 物理地址扩展分页机制... 6 | tags: 物理地址扩展 PAE 分页 扩展分页 7 | --- 8 | 处理器所支持的RAM容量受链接到地址总线上的地址管脚数限制。早起Intel处理器从80386到Pentium使用32位物理地址。从理论上讲,这样的系统上可以安装高达4GB的RAM,而实际上,由于用户进程线性地址空间的需要,内核不能直接对1GB以上的RAM进行寻址[^1]。 9 | 10 | [^1]: 这个笔记会在后面Linux的分页中记录。 11 | 12 | 然而,大型服务器需要大于4GB的RAM来同时运行上千的进程,实际上我们现在的很多计算机的RAM都可能超过这个量级。所有必须扩展32位的80x86结构所支持的RAM容量。实际上即便是使用32位操作系统模拟64位[^2],也会遇到一些问题,例如高低两个32位的数据同步问题。 13 | 14 | [^2]: 用两个32位地址模拟成64位扩展寻址范围。 15 | 16 | ### 物理地址扩展分页机制 ### 17 | 18 | Intel通过在它的处理器上把管脚数从32增加到36已经满足了这些需求。从Pentium Pro开始,Intel所有的处理器现在的寻址能力达2^36=64GB.不过,只有引入一种新的分页机制把32位线性地址转换为36位物理地址才能使用所增加的物理地址。 19 | 20 | 从Pentium Pro处理器开始,Intel引入一种叫做物理地址扩展的机制(*Physical Address Extension,PAE*),另外一种叫页大小扩展(*Page Size Extension,PSE*),但Linux并没有采用这种机制。 21 | 22 | 通过设置*cr4*控制寄存起中的物理地址扩展(PAE)标志激活PAE。页目录项中的页大小标志*PS*启动用大尺寸页,在PAE启用时,大小位2MB。当启用了PAE机制之后,系统的分页机制也做了相应的改变: 23 | 24 | 64GB的RAM被分为2^24个页框,页表项的物理地址字段从20位扩展到了24位,因为PAE页表项必须包含12个标志位和24个物理地址位,总数之和位36,页表项大小从32位变为64位增加了以北,结果一个4KB的页表包含512个表项而不是1024个表项。 25 | 26 | 引入一个叫做页目录指针表(*Page Directory Pointer Table,PDPT*)的页表新级别,它由4个64位表项组成。 27 | 28 | *cr3*控制寄存器包含一个27位的页目录指针表(*PDPT*)基地址字段。因为PDPT存放在RAM的前4GB中,并在32字节的倍数上对齐,因此27位足以表示这种表的基地址。 29 | 30 | 把线性地址映射到4KB的页时,页目录项中的PS标志清0,32位线性地址按下列方式解释: 31 | 32 | 字段名 | 说明 33 | ------------ | ------------- 34 | cr3 | 指向一个PDPT 35 | cr3的31-30位 | 指向PDPT中4个项中的一个 36 | cr3的29-21位 | 指向页目录中512个项中的一个 37 | cr3的20-12位 | 指向页表中512项中的一个 38 | cr3的11-0位 | 4KB页中的偏移量 39 | 40 | 当把线性地址隐射到2MB的页时,页目录项中的PS标志为1,32位线性地址按下列方式解释: 41 | 42 | 字段名 | 说明 43 | ------------ | ------------- 44 | cr3 | 指向一个PDPT 45 | cr3的31-30位 | 指向PDPT中4个项中的一个 46 | cr3的29-21位 | 指向页目录中512个项中的一个 47 | cr3的20-0位 | 2MB页中的偏移量 48 | 49 | 显然,PAE并没有扩大进程的线性地址空间,因为它只能处理物理地址,此外,只有内核能够修改进程的页表,所以用户态下运行的进程不能使用大于4GB的物理地址空间。另一方面,PAE允许内核使用高达64GB的RAM,从而显著增加了系统中的进程数量。 50 | 51 | ### 64位系统中的分页 ### 52 | 53 | 32为处理器普遍采用两级分页[^3],但是两级分页并不适用于64位计算机系统。 54 | 55 | 首先假设一个大小为4KB的标准页,因为1KB覆盖2^10个地址范围,4KB覆盖2^12个地址,所以offset字段时12位,这样线性地址就剩下52位分配给*Table*和*Directory* 56 | 字段。这样可寻址范围非常之大。 57 | 58 | 如果现在决定使用64位中的48位来寻址,这样寻址范围页可以寻址256T呃空间!如果剩下的48-12=36位将被分配给*Table*和*Directory*,如果决定给两个字段各18位,那么每个进程的页目录和页表都含有2^18个项,即256000个项。还是过于庞大。 59 | 60 | 由于这个原因,所有的64位处理器的硬件分页都使用了额外的分页级别,也就是说多级分页级别。使用的级别数量取决于处理器的类型。不再深入讨论。 61 | 62 | [^3]: 也有处理器引入三级分页并激活PAE机制,总之我们可以认为多级分页可以减少页表的数目,便于高效的管理。 -------------------------------------------------------------------------------- /2014-04-16-system-hardware-cache-and-tlb.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 硬件高速缓存和TLB 4 | category: 内存寻址 5 | description: 硬件高速缓存... 6 | tags: 硬件高速缓存 缓存 TLB 7 | --- 8 | RAM有动态RAM和静态RAM,静态RAM用触发器存储信息,只要不断电,信息就不会丢失,不需要刷新。但是,静态RAM集成度相对较低,功耗大。 9 |   10 | 动态RAM用电容存储信息,为了保持信息必须每隔1~2ms就要对高电平电容重新充电,称为刷新,因此必须含有刷新电路,在电路上较复杂,但动态RAM集成度高,且价格便宜。 11 | 12 | RAM相对于硬盘来说,已经相当的快了。但和CPU相比,性能还是不够。当今的CPU时钟频率接近几个GHz,而动态RAM芯片的存取时间是时钟周期的数百倍,这就意味着从RAM中取操作数或向RAM存放结果这样的指令执行时,CPU可能需要等待过长的时间。 13 | 14 | 为了缩小CPU和RAM之间的速度不匹配,引入了硬件高速缓存(*hardware cache memory*)。硬件高速缓存基于局部性原理(*locality principle*)。该原理既适用程序结构也适用数据结构。这表面由于程序循环结构及相关数组可以组成线性数组,最近最常用的相邻地址在最近的将来又被用到的可能性很大。因此,引入小而快的内存来存放最近最常用的代码和数据变得很有意义。 15 | 16 | 为此,80x86体系结构中引入了一个叫行(*line*)的单位。行由几十个连续的字节组成,它们以脉冲突发模式在慢速DRAM和快速的用来实现高速缓存的片上静态RAM(SRAM)之间传送,用来实现高速缓存。 17 | 18 | 高速缓存再被细分为行的子集,在一种极端情况下,高速缓存可以是直接映射的,这时主存中的一个行总是存放在高速缓存中完全相同的位置。在另一种极端的情况下,高速缓存是充分关联的,这意味着主存中的任意一个行可以存放在高速缓存中的任意位置。但大多数高速缓存在某种成都上是N-路关联的,意味着主存中的任意一个行可以存放在高速缓存N行中的任意一行中。 19 | 20 | 高速缓存单元插在分页单元和主内存之间,它包含一个硬件高速缓存内存和一个高速缓存控制器。高速缓存内存存放内存中真正的行。高速缓存控制器存放一个表项数组,每个表项对应高速缓存内存中的一个行。每个表项有一个标签和描述高速缓存状态的几个标志[^1]。 21 | 22 | [^1]: 这些标志由一些位组成,这些位让高速缓存控制器能够辨别由这个行当前所映射的内存单元。 23 | 24 | ![system](images/dram_cache.png) 25 | 硬件高速缓存 26 | 27 | 当访问一个RAM存储单元时,CPU从物理地址中提取出子集的索引号并把自己中所有行的标签与物理地址的高几位比较,如果发现某个行标签与这个物理地址的高位相同,则CPU命中一个高速缓存。 28 | 29 | 当命中高速缓存时,高速缓存控制器进行不同的操作,具体根据存取类型相关。控制器从高速缓存中选择数据并从到CPU寄存器,省去了访问DRAM的时间。当高速缓存没有命中,高速缓存行被写回内存,如果有必要的话,把正确的行从RAM中取出放到高速缓存的表项中。实际上和web开发中常用的memcached或redis、mysql类似。 30 | 31 | 多处理系统的每一个处理器都有一个单独的硬件高速缓存,因此它们需要额唉的硬件电路用于保持高速缓存内容的同步,每个CPU都有自己的本地硬件高速缓存,但是,现在更新变得更耗时,只要一个CPU修改了它的硬件高速缓存,它就必须检查同样的数据是否包含在其他硬件高速缓存中,如果是,就必须通知其他CPU修改高速缓存中的值。通常把这种活动称作高速缓存侦听。这些实现和内核无关,都由硬件实现。 32 | 33 | Pentium处理器高速缓存的一个特点是让操作系统把不同的高速缓存管理策略与没一个页框关联,因此,每一个页目录项和每一个页表项都包含PCD(*Page Cache Disable*)标志和PWD(*Page Write-Through*)标志。PCD指明当访问包含在这个页框的数据时,高速缓存功能必须被启用还是禁用。PWD标志指明数据写到页框时,必须使用策略是回写策略还是通写策略。 34 | 35 | Linux清除了所有页目录项和页表项中的PCD和PWT标志,导致的结果是,对于所有的页框都启用高速缓存,对于写操作总是采用回写策略。 36 | 37 | ### 转换后援缓冲器(TLB) ### 38 | 39 | 除了通用硬件高速缓存之外,80x86处理器还包含了另一个称为转换后援缓冲期或TLB(*Translation Lookaside Buffer*)的高速缓存用于存储物理地址,从而加快了线性地址的转换。当一个地址被第一次使用时,通过慢速访问RAM中的页表计算出相应的物理地址,同时物理地址被存放在一个TLB表项中。以便以后使用。 40 | 41 | 在多处理器系统中,每个CPU都有自己的本地TLB,TLB中的对应项不必同步,因为运行在现有的CPU上的进程可以使用同一线性地址与不同的物理地址发生联系。当CPU的*cr3*控制寄存器被修改时,硬件自动使本地的TLB中的所有项都无效,这是因为新的一组页表被启用而TLB指向的是旧数据。 -------------------------------------------------------------------------------- /2014-04-17-linux-paging.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Linux中的分页 4 | category: 内存寻址 5 | description: Linux中的分页... 6 | tags: 分页 页目录 页目录指针表 PDPT 7 | --- 8 | Linux采用了一种兼容32位和64位系统的普通分页模型。在32位系统模型中,两级分页已经足够了,但在64位系统中需要更多的分页级别。直到2.6.10版本,Linux采用三级分页模型,从2.6.11之后,采用了四级分页模型[^1]。这4种页表分别为: 9 | 10 | 1. 页全局目录(Page Global Directory) 11 | 2. 页上级目录(Page Upper Directory) 12 | 3. 页中间目录(Page Middle Directory) 13 | 4. 页表(Page Table) 14 | 15 | 页全局目录包含若干页上级目录的地址,页上级目录又包含页中间目录的地址,依次类推,页中间目录则包含页表的目录地址。而每个页表指向一个页框。线性地址因此被划分成五个部分。下图的线性地址并没有显示位数,因为每一部分的大小跟具体的计算机体系结构相关。 16 | 17 | ![system](images/linux-paging.png) 18 | Linux中的分页 19 | 20 | 上图显示了一个Linux的四级分页中是如何通过页目录找到页表然后寻址到相应的页。实际上,和之前两级分页的概念一样。针对32位系统。Linux取消了上级页目录和中间页目录字段。不过,页上级目录和页中间目录在指针中的位置被保留,以便同样的代码在32位系统和64位系统下都能使用。 21 | 22 | 内核位页上级目录和页中间目录保留了一个位置,这是通过它们的页目录项数设置位1,并把这两个目录项映射到页全局目录的一个适当的目录项而实现的。 23 | 24 | 启用了物理地址扩展的32位系统使用了三级页表,Linux的全局页目录对应80x86的页目录指针表(*PDPT*),取消了页上级目录,页中间目录对应80x86的页目录,Linux的页表对应80x86的页表。 25 | 26 | 最后,64位系统使用三级还是四级目录取决于硬件对线性地址的划分。 27 | 28 | Linux的进程处理依赖于分页,实际上,线性地址到物理地址的自动转换使得下面的设计目标得以实现: 29 | 30 | 1. 给每一个进程分配一块不同的物理地址空间,确保了可以有效地防止寻址错误。 31 | 2. 区别页和页框[^page]的不同,就允许存放在某个页框中的一个页,然后保存到磁盘上,以后重新装入这一页时又可以被装在不同的页框中。这是虚拟内存机制的基本要素。 32 | 33 | [^1]: 这个变化用来全力支持x86_64平台使用的对线性地址的划分。 34 | 35 | 每一个进程有它自己的页全局目录和自己的页表集,当发生进程切换时,Linux把*cr3*寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入到*cr3*寄存器中。因此当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。 36 | 37 | [^page]: 页是一组数据,页框是主存的物理地址。 38 | 39 | 线性地址字段,页表处理的函数和宏我就不列举了,可能在以后的(如果我深入了解内存管理的话)会再补充,这里只是笔记。 -------------------------------------------------------------------------------- /2014-04-17-page-and-page-descriptor.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 页及其描述符 4 | category: 内存管理 5 | description: 内存组织... 6 | tags: 内存 UMA NUMA 页描述符 page 7 | --- 8 | 内存管理是内核中最为复杂的一部分,我之前想要跳过这里了解后面的内容,而且我确实看书看到比较后面,但最后还是得回来看内存管理这一部分,因为不仅仅内存需要进行内存管理,进程调度等算法也涉及到内存管理,所以没办法还得看。内存管理涵盖了许多领域,是一个旷日持久的学习过程。这一部分可能会涉及很多的代码,但我自己不一定能够全部理解。 9 | 10 | 内存管理涉及的领域有: 11 | 12 | 1. 内存中的物理内存页的管理。 13 | 2. 分配大块内存的伙伴系统。 14 | 3. 分配较小内存的slab、slub和slob分配器。 15 | 4. 分配非连续内存块的vmalloc机制。 16 | 5. 进程的地址空间。 17 | 18 | 之前的笔记里有[进程的地址空间](/linux-kernel-architecture/posts/task-size/)和[内存管理](/linux-kernel-architecture/posts/mm-management/)的基本知识,需要了解,如果对于细节不感兴趣的话,我觉得可以跳过。不过也没有什么可看的了,基本的内存和进程知识前面的笔记也说的很详细了。 19 | 20 | 我们知道,Linux内核一般将处理器的虚拟地址空间划分为两个部分,底部比较大的部分用于用户进程,顶部则用于内核。虽然上下文切换期间会修改底部的用户进程部分,但虚拟地址空间的内核部分总是保持不变。地址空间在用户进程和内核之间划分的典型比例为3:1。给出4GB的虚拟地址空间,3GB将用于用户空间而1GB而用于内核。 21 | 22 | 可用的物理内存将映射到内核的地址空间中。访问内存时,如果所用的虚拟地址与内核区域的起始地址之间的偏移量不超过可用物理内存的长度,那么该虚拟地址会自动关联到物理页帧。不过,还有一个问题,虚拟地址空间的内核部分必然小于CPU理论地址空间的最大长度。如果物理内存比可以映射到内核地址空间的数量要多,那么内核必须借助高端内存方法来管理多的内存。普通的32位80x86系统上,可以直接接管的物理内存数量不超过896MB,超过最大4GB的内存只能通过高端内存寻址[^1]。在64位计算机上,由于可用的地址空间非常巨大,因此不需要高端内存模式[^3]。 23 | 24 | [^1]: 具体可以参考内存寻址里的PAE机制。 25 | 26 | 一般情况下,有两种计算机,分别为UMA和NUMA计算机来管理物理内存,虽然之前的笔记已经提到过,这里再拿出来。 27 | 28 | ![numa](images/numa.png) 29 | UMA和NUMA 30 | 31 | (1):UMA计算机(*一致内存访问,uniform memory access*)将可用内存以连续方式组织起来,系统中的每个处理器访问各个内存都是同样的块。 32 | 33 | (2):NUMA计算机(*非一致内存访问,non uniform memory access*)总是多处理器计算机。系统的各个CPU都有本地内存,可支持特别快的访问,各个处理器之间通过总线连接起来。 34 | 35 | 在UMA系统上,值使用一个NUMA节点来管理系统内存,所以首先考虑NUMA系统,这样UMA系统就比较好理解了。两种类型的计算机的混合也是可能的,其中使用不连续的内存。在UMA系统中,内存不是连续的,而会有比较大的洞。在这里应用NUMA体系结构的原理会有帮助,可以使内核的内存访问更简单。 36 | 37 | 实际上内核会区分3种内存管理的配置选项,FLATMEM、DISCOUNTIGMEM和SPARSEMEM[^2]。真正的NUMA会设置配置选项CONFIG_NUMA,相关的内存管理代码也有很大的不同。 38 | 39 | [^2]: 实际上这种方式不太稳定,但有一些性能优化。 40 | 41 | [^3]: 只有内核自身使用高端内存页的时候才会有问题,在内核使用高端内存页之前,必须使用kmap和kunmap函数将其映射到内核虚拟地址中,对普通内存页这是不必的。对用户空间进程来说,是否是高端内存页没有任何差别,因为用户进程总是通过页表访问内存。 42 | 43 | ### 页描述符 ### 44 | 45 | 内核必须记录每个页框当前的状态,例如,内核必须能够区分哪些页框包含的是属于进程的页而哪些页框包含的是内核代码或内核数据。类似的,内核还必须能够确定动态内存中的页框是否空闲。如果动态内存中的页框不包含有用的数据,那么这个页框就是空的。 46 | 47 | 页框的状态信息保存在一个类型为*page*的页描述符中,虽然在前面的内存寻址里的笔记里有列出,但代码再列出来如下,记录一些详细的字段。 48 | 49 | #### #### 50 | 51 | {% highlight c++ %} 52 | struct page { 53 | unsigned long flags; 54 | atomic_t _count; 55 | union { 56 | /* Count of ptes mapped in mms, 57 | * to show when page is mapped 58 | * & limit reverse map searches. 59 | */ 60 | atomic_t _mapcount; 61 | /* SLUB */ 62 | struct { 63 | u16 inuse; 64 | u16 objects; 65 | }; 66 | }; 67 | union { 68 | struct { 69 | unsigned long private; 70 | struct address_space *mapping; 71 | }; 72 | #if USE_SPLIT_PTLOCKS 73 | spinlock_t ptl; 74 | #endif 75 | struct kmem_cache *slab; 76 | struct page *first_page; 77 | }; 78 | union { 79 | pgoff_t index; 80 | void *freelist; 81 | }; 82 | struct list_head lru; 83 | #if defined(WANT_PAGE_VIRTUAL) 84 | void *virtual; 85 | #endif 86 | #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS 87 | unsigned long debug_flags; 88 | #endif 89 | 90 | #ifdef CONFIG_KMEMCHECK 91 | void *shadow; 92 | #endif 93 | }; 94 | {% endhighlight %} 95 | 96 | 其中各个字段的意义如下: 97 | 98 | 字段名 | 说明 99 | ------------ | ------------- 100 | flags | 一组标志,对页框所在的管理区进行编号 101 | _count | 页框的引用计数器 102 | _mapcount | 页框中的页表项数目,如果没有则为-1 103 | private | 可用于正在使用页的内核成分 104 | mapping | 当页被插入页高速缓存中的时候使用 105 | index | 作为不同的含义被几种内核成分使用 106 | lru | 包含页的最近最少使用(LRU)双向链表的指针 107 | 108 | 其中重要的两个字段为*_count*和*flags*。*_count*是页的引用计数器,如果字段为-1,则相应的页框空闲,并可以被分配给任意一个进程甚至内核本身,如果该字段大于或等于-,则说明页框被分配给了一个或多个进程,用于存放一些内核数据结构。*flags*包含多大32个用来描述页框标志的状态,对于每个PG_xxx标志内核都定义了操作其值的一些宏。 109 | 110 | 标志名 | 说明 111 | ------------ | ------------- 112 | PG_locked | 页被锁定 113 | PG_error | 在传输过程中发生I/O错误 114 | PG_referenced | 刚刚访问过的页 115 | PG_uptodate | 在完成读操作后置位 116 | PG_dirty | 页已经被修改 117 | PG_lru | 页在活动或非活动页链表中 118 | PG_active | 页在活动页链表中 119 | PG_slab | 包含在slab中的页框 120 | PG_highmem | 页框属于ZONE\_HIGHMEM管理区 121 | PG_checked | 由一些文件系统使用的标识 122 | PG\_arch\_1 | 在80x86体系结构上没有使用 123 | PG_reserved | 页框留给内核代码或没有使用 124 | PG_private | 页描述符的private字段存放了有意义的数据 125 | PG_writeback | 页正在使用writepage方法将页写到磁盘上 126 | PG_nosave | 系统挂起/唤醒时使用 127 | PG_compound | 通过扩展分页机制处理页框 128 | PG_swapcache | 页属于对换高速缓存 129 | PG_mappedtodisk | 页框中的所有数据对应于磁盘上分配的块 130 | PG_reclaim | 为回收内存对页已经做了写入磁盘标记 131 | PG\_nosave\_free | 系统挂起/恢复时使用 132 | 133 | 所有的页描述符存放在*mem_map*数组中。因为每个描述符的长度为32字节,所以*mem_map*所需要的空间略小于整个RAM的1%。*virt_to_page(addr)*宏产生线性地址*addr*对应的页描述符地址。 134 | 135 | #### #### 136 | 137 | {% highlight c++ %} 138 | #ifndef CONFIG_DISCONTIGMEM 139 | extern struct page *mem_map; 140 | #endif{% endhighlight %} -------------------------------------------------------------------------------- /2014-04-17-physical-memory.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 物理内存布局 4 | category: 内存寻址 5 | description: 物理内存布局... 6 | tags: 内存 物理内存 物理内存布局 7 | --- 8 | 在系统初始化阶段,内核必须建立一个物理地址映射来指定哪些物理地址范围对内核可用,哪些对内核不可用,因为会有一些物理内存被用作特殊的目的,例如BIOS的一些数据会包含在前1M的物理内存中。 9 | 10 | 内核将下列页框记为保留: 11 | 12 | 1. 在不可用的物理地址范围内的页框。 13 | 2. 含有内核代码和已初始化的数据结构的页框。 14 | 15 | 保留页框中的页框绝对不能被动态分配或交换到磁盘上。 16 | 17 | 一般来说,Linux内核安装在RAM中的从物理地址0x00100000开始的地方,就是从第二个MB开始,所需页框总数依赖于内核的配置方案,景点的配置所得到的内核可以被安装在小于3MB的RAM中。 18 | 19 | 之所以内核没有被安装在物理内存最开始的地方,是因为PC体系结构有几个特殊的地方必须考虑到,例如,页框0由BIOS使用,存放加电自检(*Power-On Self-Test,POST*)期间检查到的系统硬件配置。因此,很多笔记本电脑的BIOS在系统初始化后还将数据写回该页框。 20 | 21 | 物理地址从0x000a0000到0x000fffff的范围通常刘改BIOS例程,并且映射ISA图形卡上的内部内存。这个区域是所有IBM兼容PC上从640KB到1MB之间著名的洞[^1]。第一个MB内的其他页框可能由特定计算机模型保留。 22 | 23 | [^1]: 物理地址存在但被保留,对应的页框不能由操作系统使用。 24 | 25 | 在启动过程的早期阶段,内核询问BIOS并了解物理内存的大小,通常,内核页调用BIOS过程建立一组物理地址范围和其对应的内存类型。随后,内核执行*machine_specific_memory_setup()*函数,该函数建立物理地址映射,如果这张表是可获取的,那是内核在BIOS列表的基础上构建的,否则,内核保守的设置这张表为默认值[^2]。 26 | 27 | [^2]: 从0x9f(LOWMEMSIZE)到0x100(HIGH_MEMORY)号的所有页框都标记为保留。 28 | 29 | 内核可能不会见到BIOS报告的所有物理内存,如果没有使用PAE机制,则最高寻址4GB大小的RAM。*setup_memory()*函数在*machine_specific_memory_setup()*函数之后被调用,它分析物理内存区域表示并初始化一些变量来描述内核物理内存布局。 30 | 31 | ![system](images/page_frame.png) 32 | 物理内存的布局 33 | 34 | 为了避免把内核装入一组不连续的页框里,Linux更愿意跳过RAM的第一个MB,明确地说,Linux用PC体系结构未保留的页框来动态存放所分配的页。 35 | 36 | 符号\_text对应于物理地址0x00100000,来表示内核代码第一个字节的地址。内核代码的结束位置由另外一个类似的符号\_etext。内核数据分为两组,初始化过的数据的和没有初始化的数据。初始化过的数据在\_etext后开始,在\_edata处结束。紧接着是未初始化的数据并以\_end结束。这些符号并没有在Linux源代码中定义,它们是内核编译中产生的[^3]。 37 | 38 | [^3]: 在System.map文件中找到这些符号的线性地址,System.map是编译内核以后所创建的。 -------------------------------------------------------------------------------- /2014-04-17-thread-page-table-and-kernel-page-table.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 进程页表页和内核页表 4 | category: 内存寻址 5 | description: 进程页表和页内核页表... 6 | tags: 页表 进程页表 内核页表 实模式 保护模式 7 | --- 8 | 页表的概念中,还有几个页表结构需要了解,分别为进程页表和内核页表: 9 | 10 | ### 进程页表 ### 11 | 12 | 进程的线性地址空间分为两个部分: 13 | 14 | 1. 从0x00000000到0xbfffffff的线性地址,无论进程运行在用户态还是内核态,都可以寻址。 15 | 2. 从0xc0000000到0xfffffff的线性地址,只有内核态的进程才能访问。 16 | 17 | 当进程运行在用户态时,它产生的线性地址小于0xc0000000,当进程运行在内核态时,它执行的是内核代码,所产生的地址大于等于0xc0000000,但是,在某些情况下,内核为了检索或存放数据必须访问用户态线性地址空间。 18 | 19 | 宏PAGE_OFFSET的值是0xc0000000。这就是线性地址空间中的偏移量,页是内核生存空间的开始之处。页全局目录的第一部分表项映射的线性地址小于0xc0000000,具体大小依赖于特定的进程。相反,剩余的表项对所有的进程来说都应该是相同的,它们等于主内核页全局目录的相应表项。 20 | 21 | ### 内核页表 ### 22 | 23 | 内核维持着一组自己使用的页表,驻留在所谓的主内核页全局目录(*master kernel Page Global Directory*)中,系统初始化后,这组表还未被任何进程或任何内核线程直接使用,更准确的说,主内核页全局目录的最高目录项部分作为参考模型,为系统中每一个普通进程对应的页全局目录项提供参考模型。 24 | 25 | 内核初始化自己的页表需要两个阶段,内核映象刚刚被装入内存后,CPU仍然运行于实模式,所以分页功能没有被启用。 26 | 27 | x86体系的处理器刚开始时只有20根地址线,寻址寄存器是16位。所以实模式是指寻址采用和8086相同的16位段和偏移量,最大寻址空间1MB[^1],最大分段64KB。可以使用32位指令。事实上,实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,系统程序和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这虽然灵活但一方面给程序最大的权利,另一方面也带来了维护的困难,因为内存地址没有收到保护,所以可能因为内存被复写,导致系统崩溃。 28 | 29 | 为了克服这种问题,保护进程地址空间,处理器厂商开发出保护模式[^2]。在保护模式中,寻址能力大大提高,而且因为内存不能直接被程序访问,需要通过逻辑地址到物理地址的转换去访问,所以物理地址对程序透明,每个进程都无法访问其他进程的地址,甚至也无法访问自己的虚地址。如果有访问,则会产生段错误。 30 | 31 | Linux只有在刚刚启动时是实模式,然后就进入保护模式。 32 | 33 | 在第一个阶段,内核创建一个有限的地址空间,包括内核的代码段和数据段,初始页表和用于存放动态数据结构的一共128KB大小的空间。这个最小限度的地址空间仅能够将内核装入RAM并对其初始化核心数据结构。 34 | 35 | 第二个阶段,内核重复的利用剩余的RAM并适当地建立分页表。 36 | 37 | [^1]: 16位的寄存器可以访问64K的地址空间,如果程序要想访问大于64K的内存,就需要把内存分段,每段64K,用段地址+偏移量的方式来访问,这样使20根地址线全用上,最大的寻址空间就可以到1M字节。 38 | 39 | [^2]: 除了实模式和保护模式,还有虚拟8086模式。虚拟8086模式是运行在保护模式中的实模式,为了在32位保护模式下执行纯16位程序。它不是一个真正的CPU模式,还属于保护模式。 40 | -------------------------------------------------------------------------------- /2014-04-19-page-and-page-table.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 页表的数据结构 4 | category: 内存管理 5 | description: 页帧和页表的数据结构... 6 | tags: 页帧 页表 7 | --- 8 | 在[硬件中的分页](/linux-kernel-architecture/posts/system-paging-unit/)中记录了什么是页、页帧和页表,页帧的数据结构已经在[内存管理](/linux-kernel-architecture/posts/system-paging-unit/)中详细记录了,这里主要记录一下页表的数据结构。 9 | 10 | 从内存寻址的笔记可以知道,页表用于支持对大地址空间的快速、高效的管理。内核内存管理总是假定使用四级页表,而不管底层处理器是否这样,有的系统只使用两级分页系统,只要把高位的UPPER DIRECTORY和MIDDLE DIRECTORY指定为1即可。但内核还是保留指针,以便使用相同的代码。 11 | 12 | 根据内核代码,我们把全局页表,上层页表,中间页表和页表分别定义为PGD、PUD、PMD和PTE。所以页表结构可以简单归纳如下。 13 | 14 | ![system](images/page.png) 15 | 页表的简单结构 16 | 17 | 图中说明了如何用比特位移来定义各字段分量的位置,这些分量根据不同的体系结构有所不同,比特位的具体数由*PAGE_SHIFT*指定。 18 | 19 | *PMD_SHIFT*指定了页内偏移量和最后一级页表所需比特位的总数。这个值减去*PAGE_SHIFT*就可以得到PTE,也就是最后一级页表索引所需比特位的数目,这个值表名了一个中间层页表项管理的部分地址空间大小。各级页目录/页表中所能存储的指针数目,页可以通过宏定义确定*PTRS_PER_PGD*指定了全局页目录中项的数目,同理*PTRS_PER_PMD*指定了中间页的数目。 20 | 21 | 两级页表的体系结构也会将*PTRS_PER_PMD*和*PTRS_PER_PUD*指定为1,使得内核的剩余部分感觉体系结构页提供了四级页表的转换,尽管实际上只有两级页表。中间层页目录和上层页目录实际上被消除掉了。因为其中只有一项。 22 | 23 | *PTRS_PER_PMD*定义的代码如下: 24 | 25 | #### ### 26 | 27 | {% highlight c++ %} 28 | #define PMD_SHIFT PUD_SHIFT 29 | #define PTRS_PER_PMD 1 30 | #define PMD_SIZE (1UL << PMD_SHIFT) 31 | #define PMD_MASK (~(PMD_SIZE-1)) 32 | {% endhighlight %} 33 | 34 | *PTRS_PUD_PMD*定义的代码如下: 35 | 36 | #### ### 37 | 38 | {% highlight c++ %} 39 | #define PUD_SHIFT PGDIR_SHIFT 40 | #define PTRS_PER_PUD 1 41 | #define PUD_SIZE (1UL << PUD_SHIFT) 42 | #define PUD_MASK (~(PUD_SIZE-1)) 43 | {% endhighlight %} 44 | 45 | 可以看到默认情况下,上级和中间页表指定为1,内核提供了4个数据结构来表示页表项的结构: 46 | 47 | 1. pgd_t用于全局页表项。 48 | 2. pud_t用于上层页表项。 49 | 3. pmd_t用于中间页表项。 50 | 4. pte_t用于直接页表项。 51 | 52 | 代码如下,有时候根据体系结构代码不一样: 53 | 54 | #### ### 55 | 56 | {% highlight c++ %} 57 | unsigned long pte; 58 | } pte_t; 59 | typedef struct { 60 | unsigned long pmd[16]; 61 | } pmd_t; 62 | typedef struct { 63 | unsigned long pgd; 64 | } pgd_t; 65 | typedef struct { 66 | unsigned long pgprot; 67 | } pgprot_t; 68 | typedef struct page *pgtable_t; 69 | {% endhighlight %} 70 | 71 | ### 页表(*PTE*)的信息 72 | 73 | 最后一级页表中的项不仅包含了指向页的内存指针位置,还在上述的多余比特位包含了与页有关的附加信息,尽管这些数据是特定于CPU的,但至少提供了有关访问控制的一些信息。下面列举一些信息。 74 | 75 | 字段名 | 说明 76 | ------------ | ------------- 77 | \_PAGE_PRESENT | 指定了虚拟内存页是否存在于内存中,因为页不一定总在内存中 78 | \_PAGE_ACCESSED | CPU每次访问页时,会自动设置该值,内核会定期检查比特位,以确认页使用的活跃度,不经常使用的页会被换出,在读写或访问之后会设置该比特位 79 | \_PAGE_DIRTY | 表示该页是否为脏页,即页的内容是否已经修改过 80 | \_PAGE_FILE | 这个值与\_PAGE\_DIRTY相同,但用于不同的上下文,即页不在内存中的时候,不存在的页肯定不可能是脏的,因此可以重新解释该比特位,如果没有设置,则指向一个换出的页的位置 81 | \_PAGE_USER | 如果设置了这个值,则允许用户空间代码访问该页 82 | \_PAGE_READ | 指定了普通用户进程是否可读 83 | \_PAGE_WRITE | 指定了普通用户进程是否可写 84 | \_PAGE_EXECUTE | 指定了普通用户进程是否允许执行机器代码 85 | 86 | 创建页表项可以通过使用特定页表的创建函数,例如*pud_alloc*初始化一个完整的页表的内存,也可以使用如*pud_free*释放一个页表项的内存。可以通过*pte_page*获得一个页表。 87 | -------------------------------------------------------------------------------- /2014-04-19-page-frame-allocator.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 分区页框分配器 4 | category: 内存管理 5 | description: 分区页框分配器... 6 | tags: 分区 页框 页框分配器 7 | --- 8 | 内核有一个子系统,称之为分区页框分配器(*zoned page frame allocator*),这个子系统处理对连续页框组的内存分配请求,其主要组成如下。 9 | 10 | ![numa](images/page_frame_alloc.png) 11 | 内存管理区分配器示意图 12 | 13 | 其中,管理区分配器接受动态内存分配与释放的请求,再请求分配的情况下,该部分搜索一个能满足所有请求的一组连续页框内存的管理区。在每个管理区内,页框被名为『伙伴系统』的部分来处理,为大刀更好的系统性能,一小部分页框保留在高速缓存中用于快速地满足对单个页框分配的请求。 14 | 15 | 请求和释放页框的几个重要函数如下,如果分配成功,则返回一个分配页的线性地址,如果分配失败,则返回NULL。 16 | 17 | ### 分配页框 ### 18 | 19 | 分配页框一般使用*alloc_pages*,如果分配失败则返回NULL,可以通过参数*gfp_mask*指定寻找的方法。 20 | 21 | 函数名 | 说明 22 | ------------ | ------------- 23 | alloc_pages | 申请一个连续的页框,返回第一个所分配的页框描述符的地址 24 | alloc_page | 用于获得一个页框的宏 25 | \_\_get_\_free\_pages | 类似于alloc\_pages,但返回第一个所分配页的线性地址 26 | \_\_get_free\_page | 用于获得一个单独的页框的宏 27 | get\_zeroed\_page | 用来获取填满0页框的宏,返回所获取页框的线性地址 28 | \_\_get\_dma\_pages | 用这个宏获得适用于DMA的页框 29 | 30 | alloc\_pages函数的完整带参数是alloc\_pages(gfp\_mask, order),其中order是次方,用于请求2^order个连续的页框。gfp\_mask是一组标志,它指明了如何寻找空闲的页框,gfp\_mask标志如下。 31 | 32 | 标志名 | 说明 33 | ------------ | ------------- 34 | \_\_GFP\_DMA | 所请求的页框必须处于ZONE\_DMA管理区 35 | \_\_GFP\_HIGHMEM | 所请求的页框必须处于ZONE\_HIGHMEM管理区 36 | \_\_GFP\_WAIT | 允许内核对等待空闲页框的当前进程进行阻塞 37 | \_\_GFP\_HIGH | 允许内核访问保留的页框池 38 | \_\_GFP\_IO | 允许内核再地段内存页上执行I/O传输以释放页框 39 | \_\_GFP\_FS | 如果为0,则不允许内核执行依赖于文件系统的操作 40 | \_\_GFP\_COLD | 所请求的页框可能为冷页 41 | \_\_GFP\_NOWARN | 一次内存分配失败将不产生警告信息 42 | \_\_GFP\_REPEAT | 内核重试内存分配直到成功 43 | \_\_GFP\_NOFAIL | 与\_\_GFP\_REPEAT相同 44 | \_\_GFP\_NORETRY | 一次内存分配失败后不再重试 45 | \_\_GFP\_NO\_GROW | slab分配器不允许增大slab高速缓存 46 | \_\_GFP\_COMP | 属于扩展页的页框 47 | \_\_GFP\_ZERO | 返回任何的页框必须被填满0 48 | 49 | 实际上,Linux大多数都是用的组合值,而不是单独的某一个值。所以gfp\_mask参数如下: 50 | 51 | 标志名 | 说明 52 | ------------ | ------------- 53 | GFP_ATOMIC | \_\_GFP\_HIGH 54 | GFP_NOIO | \_\_GFP\_WAIT 55 | GFP_NOFS | \_\_GFP\_WAIT \| \_\_GFP\_IO 56 | GFP_KERNEL | \_\_GFP\_WAIT \| \_\_GFP\_IO \| \_\_GFP\_FS 57 | GFP_USER | \_\_GFP\_WAIT \| \_\_GFP\_IO \| \_\_GFP\_FS 58 | GFP_HIGHUSER | \_\_GFP\_WAIT \| \_\_GFP\_IO \| \_\_GFP\_FS \| \_\_GFP\_HIGHMEM 59 | 60 | ### 释放页框 ### 61 | 62 | 下面几个函数中的任何一个宏都可以释放页框,但是有细微的差别: 63 | 64 | 函数名 | 说明 65 | ------------ | ------------- 66 | \_\_free\_pages | 该函数首先检查page指向的页描述符(*page*),如果该页框未被保留,就把描述符的count字段减1,如果count变为0,就假定从page对应的页框开始的一段连续的页框不再被使用。在这种情况下,该函数释放页框。 67 | free_pages | 释放页框 68 | \_\_free\_page | 释放单个页框 69 | free_page | 释放单个页框 70 | -------------------------------------------------------------------------------- /2014-04-20-highmem.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 高端内存页框的内核映射 4 | category: 内存管理 5 | description: 高端内存页框的内核映射... 6 | tags: 页框 高端内存 内核映射 7 | --- 8 | 与直接映射的物理内存末端,高端内存的始端所对应的线性地址存放在*high_memory*变量中,它被设置为896MB。896MB边界以上的页框并不映射再内核线性地址空间的第4个GB,因此,内核不能直接访问它们。这就意味着,返回所分配页框线性地址的页分配器函数不适用于高端内存,即不适用于ZONE\_HIGHMEM内存管理区的页框。 9 | 10 | 之前也提到过,假定内核调用\_\_get\_free\_pages(GFP\_HIGHMEM, 0),则在高端内存区确实申请并分配了一个页框,但是\_\_get\_free\_pages()不能返回它的线性地址,因为它根本就不存在,所以返回NULL[^1]。 11 | 12 | [^1]: 在64位的硬件平台上不存在这个问题。 13 | 14 | 在32位平台上,必须让内核使用者用所有可以使用的RAM,达到PAE所支持的64GB,所以采用如下方法。 15 | 16 | * 高端内存页框的分配只能通过alloc\_pages()函数和它的快捷函数alloc\_page()。这些函数不返回第一个被分配的页框的线性地址,因为如果该页属于高端内存,那么这样的线性地址根本不存在。取而代之,这些函数放回第一个被分配页框的页描述符(*page*)的线性地址,这些线性地址总是存在的[^2]。 17 | * 没有线性地址的高端内存中的页框不能被内核访问,因此,内核线性地址空间最后128MB中的一部分专门用于映射高端内存页框[^3]。当然,这种映射是暂时的,否则只有128MB的高端内存可以被访问。取而代之,通过重复使用线性地址,使得整个高端内存能够在不同的时间被访问。 18 | 19 | [^2]: 因为页描述符一旦被分配在地段内存中,它们在内核初始化阶段就不会变。 20 | [^3]: 所以虽然我们有896MB的空间可以分配页框,但实际上并没有这么多的地址空间。 21 | 22 | 内核可以采用三种不同的机制将页框映射到高端内存,分别叫做永久内核映射、临时内核映射和非连续内存分配。 23 | 24 | ### 永久内核映射 ### 25 | 26 | 建立永久内核映射可能阻塞当前进程,这发生在空闲页表不存在时,页就是高端内存上没有页表项可以用作页框使用时。因此,永久内核映射不能用于中断处理程序和可延迟函数。相反,建立零食内核映射绝不会要求阻塞当前进程。 27 | 28 | 永久内核映射允许内核建立高端页框到内核地址空间的长期映射,它们使用主内核页表中一个专门的页表,其地址存放在*pkmap_page_table*变量中。页表中的表项数由*LAST\_PKMAP*宏产生。页表包含512或1024项,这取决于PAE机制是否被激活。因此,内核最多一次性访问2M或4M的高端内存。 29 | 30 | 页表映射的线性地址从*PKMAP\_BASE*开始,*pkmap\_count*数组包含*LAST\_PKMAP*个计数器,*pkmap\_page\_table*页表中的每一个项都有一个。计数器可能为0、1或大于1。 31 | 32 | #### ### 33 | 34 | {% highlight c++ %} 35 | pte_t * pkmap_page_table; 36 | {% endhighlight %} 37 | 38 | 如果计数器为0,则说明对应的页表项没有映射任何高端内存,所以是可用的。 39 | 40 | 如果计数器为1,则说明对应的页表项没有映射任何高端内存,但是不能被使用,因为自从它最后一次使用以来,其TLB表项还未被刷新。 41 | 42 | 如果计数器大于1,则说明映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。 43 | 44 | 为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用*page\_address\_htable*做散列表,它使用*page_address_map*数据结构用于为高端内存中的每一个页框进行映射。 45 | 46 | #### ### 47 | 48 | {% highlight c++ %} 49 | struct page_address_map { 50 | struct page *page; 51 | void *virtual; 52 | struct list_head list; 53 | }; 54 | {% endhighlight %} 55 | 56 | *page_address()*函数返回页框对应的线性地址,如果页框在高端内存中并且没有被映射,则返回NULL。 57 | 58 | #### ### 59 | 60 | {% highlight c++ %} 61 | void *page_address(struct page *page) 62 | { 63 | unsigned long flags; 64 | void *ret; 65 | struct page_address_slot *pas; 66 | 67 | if (!PageHighMem(page)) 68 | return lowmem_page_address(page); 69 | 70 | pas = page_slot(page); 71 | ret = NULL; 72 | spin_lock_irqsave(&pas->lock, flags); 73 | if (!list_empty(&pas->lh)) { 74 | struct page_address_map *pam; 75 | 76 | list_for_each_entry(pam, &pas->lh, list) { 77 | if (pam->page == page) { 78 | ret = pam->virtual; 79 | goto done; 80 | } 81 | } 82 | } 83 | done: 84 | spin_unlock_irqrestore(&pas->lock, flags); 85 | return ret; 86 | } 87 | {% endhighlight %} 88 | 89 | 我们可以从上面的函数可以看出,如果页框不在高端内存中,就通过*lowmem_page_address*返回线性地址。如果在高端内存中,则通过函数*page_slot*在*page_address_htable*中查找,如果在散列表中查找到,就返回线性地址。 90 | 91 | kmap()用来建立内存区映射,代码如下: 92 | 93 | #### #### 94 | 95 | {% highlight c++ %} 96 | void *kmap(struct page *page) 97 | { 98 | might_sleep(); 99 | if (!PageHighMem(page)) 100 | return page_address(page); 101 | return kmap_high(page); 102 | } 103 | {% endhighlight %} 104 | 105 | 本质上如果是高端内存区域,则使用kmap_high()函数用来建立高端内存区的永久内核映射,代码如下: 106 | 107 | #### #### 108 | 109 | {% highlight c++ %} 110 | void *kmap_high(struct page *page) 111 | { 112 | unsigned long vaddr; 113 | 114 | /* 115 | * For highmem pages, we can't trust "virtual" until 116 | * after we have the lock. 117 | */ 118 | lock_kmap(); 119 | vaddr = (unsigned long)page_address(page); 120 | if (!vaddr) 121 | vaddr = map_new_virtual(page); 122 | pkmap_count[PKMAP_NR(vaddr)]++; 123 | BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); 124 | unlock_kmap(); 125 | return (void*) vaddr; 126 | } 127 | {% endhighlight %} 128 | 129 | ### 临时内存映射 ### 130 | 131 | 虽然不像永久内存映射那样会阻塞当前进程,但缺点时只有很少的临时内核映射可以同时建立起来。临时内存映射的内核控制路径必须保证当前没有其他的内核控制路径在使用同样的映射。这意味着内核控制路径永远不能被阻塞,否则只其他的内核控制路径有可能使用同一个窗口来映射其他的高端内存页。 132 | 133 | 临时内核映射比永久内核映射要简单,此外,它们可以用在中断处理程序和可延迟函数的内部,因为它们从不阻塞当前进程。在高端内存的仁一页框都可以通过一个『窗口』映射到内核地址空间。留给临时内核映射的窗口是非常少的。 134 | 135 | 每个CPU都有它自己包含的13个窗口集合,它们用*enum km\_type*数据结构表示。 136 | 137 | #### #### 138 | 139 | {% highlight c++ %} 140 | enum km_type { 141 | KMAP_D(0) KM_BOUNCE_READ, 142 | KMAP_D(1) KM_SKB_SUNRPC_DATA, 143 | KMAP_D(2) KM_SKB_DATA_SOFTIRQ, 144 | KMAP_D(3) KM_USER0, 145 | KMAP_D(4) KM_USER1, 146 | KMAP_D(5) KM_BIO_SRC_IRQ, 147 | KMAP_D(6) KM_BIO_DST_IRQ, 148 | KMAP_D(7) KM_PTE0, 149 | KMAP_D(8) KM_PTE1, 150 | KMAP_D(9) KM_IRQ0, 151 | KMAP_D(10) KM_IRQ1, 152 | KMAP_D(11) KM_SOFTIRQ0, 153 | KMAP_D(12) KM_SOFTIRQ1, 154 | KMAP_D(13) KM_SYNC_ICACHE, 155 | KMAP_D(14) KM_SYNC_DCACHE, 156 | KMAP_D(15) KM_UML_USERCOPY, 157 | KMAP_D(16) KM_IRQ_PTE, 158 | KMAP_D(17) KM_NMI, 159 | KMAP_D(18) KM_NMI_PTE, 160 | KMAP_D(19) KM_TYPE_NR 161 | }; 162 | {% endhighlight %} 163 | 164 | 这个数据结构中定义的每一个符号都标识了窗口的线性地址。 165 | 166 | 为了建立临时内核映射,内核调用*kmap_atomic()*函数。在后来的内核代码中,*kmap_atomic()*函数只是使用了*kmap_atomic_prot*。 167 | 168 | #### #### 169 | 170 | {% highlight c++ %} 171 | void *kmap_atomic_prot(struct page *page, enum km_type type) 172 | { 173 | unsigned int idx; 174 | unsigned long vaddr; 175 | void *kmap; 176 | 177 | pagefault_disable(); 178 | if (!PageHighMem(page)) 179 | return page_address(page); 180 | 181 | debug_kmap_atomic(type); 182 | 183 | kmap = kmap_high_get(page); 184 | if (kmap) 185 | return kmap; 186 | 187 | idx = type + KM_TYPE_NR * smp_processor_id(); 188 | vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); 189 | #ifdef CONFIG_DEBUG_HIGHMEM 190 | BUG_ON(!pte_none(*(TOP_PTE(vaddr)))); 191 | #endif 192 | set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0); 193 | local_flush_tlb_kernel_page(vaddr); 194 | 195 | return (void *)vaddr; 196 | } 197 | {% endhighlight %} -------------------------------------------------------------------------------- /2014-04-22-init-mm-management.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 初始化内存管理 4 | category: 内存管理 5 | description: 初始化内存管理... 6 | tags: 初始化 内存管理 7 | --- 8 | 在内存管理上下文中,初始化(*iniitialization*)可以有多种含义,在许多CPU上,必须显式设置适用于Linux内核的内存模型,例如在IA-32系统上需要切换到保护模式,然后内核才能检测可用内存和寄存器。在初始化过程中,还必须建立内存管理的数据结构,以及其他很多事物,因为内核在内存管理完全初始化之前就需要使用内存,在系统启动过程期间,使用了一个额外的简化形式的内存管理模块,然后又丢弃掉。 9 | 10 | 因为内存管理初始化中特定于CPU的部分使用了底层体系结构中许多次要的细节,这些与内核结构没有什么关系。我们只要关心*pg_data_t*数据结构初始化即可。 11 | 12 | ### NODE_DATA ### 13 | 对相关数据结构的初始化是从全局启动例程*start_kernel*中开始的,该例程在加载内核并激活各个子系统后执行。由于内存管理是内核的一个非常重要的部分,因此在特定于体系结构的设置步骤中监测内存并确定系统中内存的分配情况后,会立即执行初始化。 14 | 15 | 所以,已经对各种系统内存模式生成了一个*pgdata_t*实例,用于保存诸如结点中内存数量以及内存在各个内存域之间的分配情况信息。所有陪你过台上都实现了特定于体系结构的NODE\_DATA宏,用来查询与一个NUMA结点相关的*pgdata_t*实例。 16 | 17 | #### #### 18 | 19 | {% highlight c++ %} 20 | #define NODE_DATA(nid) (&contig_page_data) 21 | {% endhighlight %} 22 | 23 | 尽管这个宏有一个形式参数用于选择NUMA结点,但在UMA系统只有一个伪结点。所以总是使用相同的数据。 24 | 25 | ### 系统启动 ### 26 | 27 | 系统启动如下图所示: 28 | 29 | ![system](images/start_kernel.png) 30 | 初始化内存管理流程图 31 | 32 | 这些函数及其意义分别如下: 33 | 34 | 函数名 | 说明 35 | ------------ | ------------- 36 | setup_arch | 这是一个特定于体系结构的函数,其中一项任务是负责初始化自举分配器 37 | setup\_per\_cpu_areas | 在SMP系统上,setup\_per\_cpu\_areas初始化源代码中定的静态per-cpu变量,这个变量对系统中的每个CPU都有一个独立的副本,此类变量保存在内核二进制映象的一个独立的段中。这个函数目的是为系统的各个CPU分别创建一份这些数据的副本,在非SMP系统上这个函数是一个空操作 38 | build\_all\_zonelists | 建立结点和内存域的数据结构 39 | mem_init | 也是一个独立于体系结构的函数,用于停用bootmem分配器并迁移到实际的内存管理函数 40 | setup\_per\_cpu\_pageset | 为pageset数组的第一个数组元素分配内存 41 | 42 | 由于大部分函数实现都是体系结构相关的,所以不再详细的介绍。 -------------------------------------------------------------------------------- /2014-04-25-interrupt.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 中断和异常 4 | category: 中断和异常 5 | description: 中断和异常... 6 | tags: 中断 异常 7 | --- 8 | 中断(*interrupt*)被定义为一个事件,该事件改变处理器执行的指令顺序,这样的事件与CPU芯片内外部硬件电路产生的电信号相对应。中断通常分为同步(*synchronous*)中断和异步(*asynchronous*)中断。 9 | 10 | 1. 同步中断指的是当指令执行时由CPU控制单元产生的,之所以称为同步,是因为只有在一条指令终止执行后CPU才会发出中断。 11 | 2. 异步中断是由其他硬件设备依照CPU时钟信号随机产生的。 12 | 13 | 在Intel处理器中,把同步中断和异步中断分别称为异常(*exception*)和中断(*interrupt*)。我们这里也采用这种分类。我们也使用**中断信号**来统称中断和异常。 14 | 15 | 中断是由间隔定时器和I/O设备产生的,例如,用户的一次按键会引起一个中断,虽然用户没有感觉,但是按键这个过程到下一次按键之间的间隔对于计算机指令时间来说是非常长的。 16 | 17 | 另一方面,异常是由程序的错误产生的[^1],或者由内核必须处理的异常条件产生的。第一种情况下,内核通过发送一个每个Unix程序员都熟悉的信号来处理异常,第二种情况下,内核执行恢复异常需要的所有步骤,例如缺页异常。 18 | 19 | [^1]: 产生异常的时候,CPU正在执行CS寄存器里的命令,出现中断一般都是应用程序引发的。 20 | 21 | 中断信号提供了一种特殊的方式,使处理器转而去运行正常的控制流之外的代码。当一个中断信号到达时,CPU必须停止它当前正在做的事情,保留上下文[^2],并切换产生中断后的一个空间。 22 | 23 | 虽然进程切换和中断都会导致内核保存上下文并且切换到另一空间,但中断处理程序和进程切换有一个明显的差异,由中断或异常处理程序执行的代码不是一个进程,更确切的说,它是一个内核执行路径,代表中断发生时正在运行的进程执行。作为一个内核控制路径,中断处理程序要比一个进程更轻量。 24 | 25 | [^2]: 为了做到这一点,就要在内核态堆栈保存程序计数器和当前的值。 26 | 27 | 中断处理是由内核执行的最敏感的任务之一,因此它必须满足下面的约束: 28 | 29 | 当内核正打算去完成一些别的事情时,中断会随时到来。因此,内核的目标就是让中断尽可能快的处理完,尽其所能把更多的处理向后推迟。例如一个数据块已经到达了网线,当硬件中断内核时,内核只简单的标志数据到来了,让处理器恢复到它以前的运行状态,其余的处理稍后再进行。因此,内核响应中断后需要进行的操作氛围两部分,关键而紧急的部分内核立即执行,其他的推迟的部分内核随后会执行。 30 | 31 | 因为中断随时到来,所以内核可能正在处理其中一个中断的时候,另一个中断又会到来,应该尽可能多的允许这样的情况发生,因为这能维持更多的I/O设备处于忙状态,提高I/O设备的吞吐量。因此中断处理程序必须便写成使相应的内核控制路径能以嵌套的方式执行。当最后一个内核控制路径终止时,内核必须能恢复被中断执行的进程。 32 | 33 | 尽管内核在处理前一个中断时可以接受一个新的中断,但在内核代码中还是存在一些临界区,在临界区中,中断必须被禁止。必须尽可能的限制这样的临界区,因为根据以前的要求,内核,尤其时中断处理程序,应该在大部分时间内以开中断的方式运行。 34 | 35 | Intel文档把中断和异常分为以下几类: 36 | 37 | **中断**: 38 | 39 | 1. 可屏蔽中断,I/O设备发出的所有中断请求(IRQ)都产生可屏蔽中断,一个屏蔽的中断只要还是屏蔽的,控制单元就可以忽略它。 40 | 2. 非屏蔽中断,有一些危险的事件才能引起非屏蔽中断,例如硬件故障,非屏蔽中断总是由CPU辨认。 41 | 42 | **异常**: 43 | 44 | 当CPU执行指令时探测到一个异常,会产生一个处理器探测异常(*processor-detected exception*),可以进一步区分,这取决于CPU控制单元产生异常时保存在内核堆栈*eip*寄存器的值。 45 | 46 | 1. 故障(*fault*),通常可以纠正,一旦纠正,程序就可以重新开始,保存在*eip*寄存器中的值是引起故障的指令地址。 47 | 2. 陷阱(*trap*)在陷阱指令执行后立即报告,内核把控制权烦给程序后就可以继续它的执行而不失连续性。保存在*eip*中的值是一个随后要执行的指令地址。陷阱的主要作用是为了调试程序。 48 | 3. 异常中止(*abort*),发生一个严重的错误,控制单元出了问题,不能在eip寄存器中保存引起异常的指令所在的确切位置。异常中止用于报告严重的错误,例如硬件故障或系统表中无效的值或者不一致的值。这种异常会强制中止进程。 49 | 4. 编程异常(*programmed exception*),在编程者发出的请求时发送,是由*int*或*int3*指令触发的。 50 | 51 | 每个中断和异常是由0~255之间的一个数来标识的,Intel把这个8位无符号整数叫做一个向量(*vector*)。非屏蔽中断的向量和异常的向量是固定的,而可屏蔽中断的向量是可以通过对中断控制器的编程来改变。 52 | -------------------------------------------------------------------------------- /2014-04-26-kernel-preemption.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内核抢占 4 | category: 内核同步 5 | description: 内核抢占... 6 | tags: 内核抢占 临界区 读写安全 7 | --- 8 | 我们可以把内核看作是不断对请求响应的一个服务器,这些请求可能来自在CPU上执行的进程,也可能来自发出中断请求的外部设备。内核的各个部分不是严格按照顺序依次自行的,而是采用交错执行的方式,这和进程切换的感觉是一样。所以,这些请求可能引起竞态条件,所以内核需要对这种情况进行适当的控制,这需要一些同步机制。 9 | 10 | 内核可以被看作是必须满足两种请求的服务器,一种请求来自中断,一种来自进程。 11 | 12 | 1. 如果产生了一个中断,内核如果正在执行用户空间进程,那么必须响应中断,不过通常情况下这种情况比较少。 13 | 2. 如果内核正在执行一个进程,无论是处于用户空间还是内核空间,一旦产生了中断,就立即停止服务,响应中断。 14 | 3. 如果一个中断提出请求的时候内核正在处理另一个中断,那么内核就暂停中断,处理刚刚产生的中断,然后再处理之前的中断。有时候,中断也有优先级,如果处理的中断有最高的优先级,而新的中断可以被延迟,那么就先处理最高优先级的中断。 15 | 4. 一个中断可以改变内核服务的对象,当内核正在执行处理一个进程A,中断可以改变内核服务对象,改变后,进程可能被切换到B。 16 | 17 | 内核抢占的概念比较复杂,无法精确的下一个定义,我们可以看做一件事情正在执行过程中,老板让你去左另外一件事情,那么可以说你是可以被抢占的,但内核抢占的概念实际上要复杂更多: 18 | 19 | 无论在抢占内核还是非抢占内核中,运行在内核态的进程都可以自动放弃CPU,比如,进程可能由于等待资源而不得不进入睡眠状态,如I/O请求,这个时候进程可以自己放弃CPU。我们把这种进程切换称为计划性进程切换。但是,抢占式内核在响应引起进程切换的异步时间的方式上与非抢占内核是有差别的,抢占式的进程切换可以被称作强制性进程切换。 20 | 21 | 所有的进程切换都是由*switch_to*来完成的,在抢占内核和非抢占内核中,当执行完某些具有内核功能的线程,而且调度程序被调用后,就发送进程切换,不过,在非抢占内核中,当前进程是无法被切换掉的。 22 | 23 | 因此抢占性内核的主要特点是,一个内核态运行的进程,可能在执行内核函数期间被另外一个进程取代。 24 | 25 | 让内核可抢占的目的是减少用户态进程的分派延迟(*dispatch latency*),即从进程变为可执行状态它实际开始运行之间的时间间隔。内核抢占对执行及时被调度的任务的进程确实是有好处的,因为它降低了这种进程被另一个运行在内核态的进程延迟的风险。 26 | 27 | 我们知道*preempt_count*字段大于0时,就禁止内核抢占。这个字段的编码对应三个不同的计数器[^1],因此它们在如下任何一种情况发生时,都会禁止内核抢占,而值会大于0。 28 | 29 | 1. 内核正在执行中断服务例程。 30 | 2. 可延迟函数被禁止,当内核正在执行软中断或者tasklet的时候。 31 | 3. 通过把抢占计数器设为正数显式地禁止内核抢占。 32 | 33 | [^1]: [软中断](/linux-kernel-architecture/posts/soft-irq/)中记录了。 34 | 35 | 所以,只有当内核正在执行异常处理程序,尤其是系统调用,而且内核抢占没有被指明显式地被禁用时,才可能抢占内核。另外,本地CPU必须打开本地中断,否则无法完成内核抢占。 36 | 37 | 下面列举了一些用于抢占计数器字段的宏: 38 | 39 | 宏 | 说明 40 | ------------ | ------------- 41 | preempt\_count() | 在thread\_info描述符中选择preempt\_count字段 42 | preempt\_disable() | 让抢占计数器的值加1 43 | preempt\_enable\_no\_resched() | 让抢占计数器的值减1 44 | preempt\_enable() | 让抢占计数器的值减1,并且进行相关的处理[^2] 45 | get\_cpu() | 同preempt\_disable,但要返回CPU的数量 46 | put\_cpu() | 同preempt\_enable,但要返回CPU的数量 47 | put\_cpu\_no\_resched() | 同preempt\_enable\_no\_resched() 48 | 49 | [^2]: 在thread_info描述符的TIF_NEED_RESCHED标志被设置为1的情况下,调用preempt_schedule() 50 | 51 | 内核抢占会引起内核开销,并且不是可以视而不见的开销,所以Linux可以允许用户在编译内核时通过设置选项来禁用或者启用内核抢占。 52 | 53 | ---- 54 | 55 | 虽然并不是所有的情况都需要内核同步,但对于重要的数据结构,内核需要保证读写安全。我们知道内核有竞争条件和进程临界区的概念,这些情况在内核控制路径中同样也是如此。 56 | 57 | 当计算的结果依赖于两个或两个以上的交叉内核控制路径的嵌套方式时,可能出现竞争,我们称作竞态条件。临界区是一段代码,在其它内核控制路径能够进入临界区之前,进入临界区的内核控制路径必须全部执行完这段代码。 58 | 59 | 交叉内核控制路径使内核的开发更加复杂,我们必须特别小心地识别异常处理程序,中断处理程序,可延迟函数和内核线程中的临界区。一旦临界区被确定,就必须对其采用适当的保护措施,以确保在任意时刻只有一个内核控制路径处于临界区。 60 | 61 | 假设两个不同的中断处理程序要访问同一个包含了几个相关变量的数据结构,比如一个缓冲区大小的整型变量,所有影响该数据结构的语句都必须放入一个单独的临界区。如果是单CPU系统,可以参去访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套。 62 | 63 | 另外,如果相同的数据结构仅被系统调用服务例程所访问,而且系统中只有一个CPU。就可以非常简单的通过在访问共享数据结构中禁止内核抢占的功能来实现临界区。但是,在多处理器系统中这个情况要复杂的多。 64 | 65 | 由于许多CPU可能同时执行一个内核控制路径,所以不能假设只要禁用内核抢占功能,而且中断,异常和软中断处理程序都没有访问过数据结构,就能够保证这个数据结构可以安全的访问。这需要更多的判断条件。多CPU的内核同步是一个复杂的情况,我们可以通过使用禁止本地中断或者自旋锁的方式来实现,后面会一点一点的记录。 66 | 67 | 内核同步中有几个同步技术需要了解,这些都是内核中重要的同步技术: 68 | 69 | 技术 | 说明 | 适用范围 70 | ------------ | ------------- | ------------- 71 | per-CPU变量 | 在CPU之间复制数据结构 | 所有CPU 72 | 原子操作 | 对一个指令原子地读写和修改的指令 | 所有CPU 73 | 内存屏障 | 避免指令重新排序 | 本地或所有CPU 74 | 自旋锁 | 加锁时忙等 | 所有CPU 75 | 信号量 | 枷锁时阻塞等待 | 所有CPU 76 | 顺序所 | 基于访问计数器的锁 | 所有CPU 77 | 禁止本地中断 | 禁止单个CPU上的中断处理 | 本地CPU 78 | 禁止本地软中断 | 禁止单个CPU上的可延迟函数处理 | 本地CPU 79 | 读写拷贝的更新(RCU) | 用指针而不是锁来访问共享数据结构 | 所有CPU 80 | 81 | 这些同步技术都会在后面单独记录笔记。 -------------------------------------------------------------------------------- /2014-04-27-per-cpu.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: per-CPU变量 4 | category: 内核同步 5 | description: per-CPU变量... 6 | tags: per-CPU 7 | --- 8 | 最好的同步技术是把设计不需要同步的内核放在首位,因为任何一种同步技术都需要很大的开销。最简单也是最为重要的同步技术有per-CPU变量,这个变量是数组数据结构,系统的每一个CPU都对应一个元素。 9 | 10 | 一个CPU不应该访问与其他CPU对应的数组元素,另外,它可以随意读或修改自己的元素而不会担心出现竞争状况,因为它是唯一有资格这么做的CPU,因为如果自己的CPU针对自己的数据结构进行修改而会出现竞争状况的话,那将是一个灾难。 11 | 12 | 但是,这也意味着每个CPU变量基本上只能在特殊的情况下使用,也就是在系统的CPU上的数据结构在逻辑上是独立的时候,per-CPU变量才能够使用。 13 | 14 | per-CPU的数组元素在主存中被排列以使每个数据结构存放在硬件高速缓存的不同行,因此,对per-CPU数据结构的并发访问不会导致高速缓存行的窃用和失效,否则会带来非常昂贵的系统开销。 15 | 16 | 虽然per-CPU变量为来自不同CPU的并发访问提供了保护,但对来自异步函数比如中断处理程序和可延迟函数的访问不能提供保护,在这种情况下需要其他的同步技术。 17 | 18 | 此外,在单处理器和多处理器的系统中,内核抢占都可能使per-CPU变量产生竞争,总的原则是内核控制路径应该在禁用抢占的情况下访问per-CPU变量。如果不这样,那么可以想象,如果一个内核控制路径获得了一个per-CPU变量,并且引用当前per-CPU变量相应元素的地址。然后因为被抢占而转移到另一个per-CPU变量上,但仍然可能会引用原来的per-CPU变量元素的地址,这样就会产生系统错误。 -------------------------------------------------------------------------------- /2014-04-27-timing-measurement.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 定时测量 4 | category: 定时测量 5 | description: 定时测量... 6 | tags: 定时测量 7 | --- 8 | 很多计算机化的活动都是由定时测量(*timing measurement*)来驱动的,这对用户是不可见的,例如,当我们停止使用计算机的控制台以后,屏幕会自动关闭,又或者设定了关机时间,当到达具体的时间之后,系统就自动关闭。这都是定时器实现的。 9 | 10 | 定时器允许内核跟踪按键或鼠标移动后到现在过了多少时间,如果收到了一个来自系统的警告信息,希望删除一组不用的文件,这就是由于有一个程序能识别长时间未被访问的所有用户文件。为了进行这些操作,程序必须能从每个文件中检索到文件的最后访问时间,即时间戳。因此,这样的时间标记必须由内核自动地设置。 11 | 12 | 更重要的是,定时机制连同一些更可见的内核活动,例如超时和延迟队列等等。Linux内核必须要完成两种主要的定时测量: 13 | 14 | 1. 保存当前的时间和日期,以便可以通过方便的函数来获取系统的当前时间。 15 | 2. 维持定时器,这种机制能够高速内核某一时间间隔已经过去了,在软定时器和延迟函数中有大量的应用。 16 | 17 | 定时测量是由基于固定频率振荡器和计数器的几个硬件电路完成的。 -------------------------------------------------------------------------------- /2014-04-28-atomic-operations.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 原子操作 4 | category: 内核同步 5 | description: 原子操作... 6 | tags: 原子操作 7 | --- 8 | 有些汇编语言指令具有『读』、『写』、『修改』三种类型,也就是说,它们有时候是以只读的形式从寄存器中读取内容,也有的时候以只读的形式向寄存器中写入内容。 9 | 10 | 假定运行在两个CPU上的两个内核控制路径试图通过非原子的操作来同时修改一个存储单元,首先两个CPU都试图同时进行读操作,存储器仲裁器[^1]进行干预,只允许其中一个访问而让另一个延迟。当第一个操作已经完成后,延迟的CPU从那个存储单元正好读到一个旧的值,这没有关系。 11 | 12 | 然后两个又尝试去写一个新的值,两次操作同样被存储器仲裁器串行化并同时写入一个新的值,最终,两个写入值的操作都成功,但全局的结果是不对的。为了避免这种情况,就是要确保这样的操作在芯片级别是原子的,任何一个这样的操作都必须以单个指令执行,中间不能中断,而且避免其他的CPU访问同一存储单元,这些很小的操作叫做原子操作。 13 | 14 | 这些原子操作可以建立在其他更灵活的机制的基础上以创建临界区,80x86的原子指令的情况可以考虑如下: 15 | 16 | 1. 进行零次或一次对齐的内存访问的汇编指令是原子的。 17 | 2. 如果在读操作之后,写操作之前没有其他处理器占用内存总线,那么从内存中读取数据、更新数据并把更新后的数据写回内存中的这些汇编指令如inc或dec是原子的。而且在但处理系统中,永远都不会发生内存总线窃用的情况。 18 | 3. 操作码前缀是lock字节(0xf0)的『读写修改』的汇编语言指令即使在多处理器系统中也是原子的。当控制单元监测到这个前缀时,就『锁定』内存总线,直到这条指令执行完位置。因此,当枷锁的指令执行时,其他处理器就不能访问这个内存单元。 19 | 4. 操作码前缀是一个rep字节(0xf2,0xf3)的汇编语言指令不是原子的,这条指令强行让控制单元多次重复执行相同的指令,控制单元在执行新的循环之前要检查挂起的中断。 20 | 21 | 在使用C代码便携程序时,并不能保证编译器会为a=a+1或者a++这样的代码的操作限定为一个原子指令,所以Linux内核提供专门*atomic_t*类型和一些专门的函数和宏,这些函数和宏作用于*atomic_t*类型的变量,并当作单独的,原子的汇编语言指令来使用。 22 | 23 | [^1]: 对访问RAM芯片的操作进行串行化的一种电路。 24 | 25 | Linux中的原子操作的函数有: 26 | 27 | 函数 | 说明 28 | ------------ | ------------- 29 | atomic_read(v) | 返回*v 30 | atomic_set(v, i) | 把*v置为i 31 | atomic_add(i, v) | 给*v增加i 32 | atomic_sub(i, v) | 从*v减去i 33 | atomic\_sub\_and_test(i, v) | 从*v减去i,如果结果为0则返回1,否则返回0 34 | atomic_inc(v) | 把1加到*v 35 | atomic_dec(v) | 从*v减去1 36 | atomic\_dec\_and\_test(v) | 从*v减去1,如果结果为0则返回1,否则返回0 37 | atomic\_inc\_and\_test(v) | 把1加到*v,如果结果为0则返回1,否则返回0 38 | atomic\_add\_negative(i, v) | 把i加到*v,如果结果为负,则返回1,否则返回0 39 | atomic\_inc\_return(v) | 把1加到*v,返回\*v的值 40 | atomic\_dec\_return(v) | 从*v总减去1,并返回\*v的值 41 | atomic\_add\_return(i, v) | 把i加到*v并返回 42 | atomic\_sub\_return(i, v) | 从*v减去i并返回 43 | 44 | 还有一些操作掩码的函数: 45 | 46 | 函数 | 说明 47 | ------------ | ------------- 48 | test_bit(nr, addr) | 返回*addr的nr位的值 49 | set_bit(nr, addr) | 设置*addr的nr位 50 | clear_bit(nr, addr) | 清空*addr的nr位 51 | change_bit(nr, addr) | 转换*addr的nr位 52 | test\_and\_set_bit(nr, addr) | 设置*addr的nr位并返回原值 53 | test\_and\_clear_bit(nr, addr) | 清*addr的nr位并返回原值 54 | test\_and\_change_bit(nr, addr)| 转换*addr的nr位并返回原值 55 | atomic\_clear\_mask(mask, addr)| 清mask指定的*addr的所有位 56 | atomic\_set\_mask(mask, addr) | 设置mask指定的*addr的所有位 57 | -------------------------------------------------------------------------------- /2014-04-28-time-system.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 时钟和定时器电路 4 | category: 定时测量 5 | description: 时钟和定时器电路... 6 | tags: 时钟 定时器电路 RTC TSC PIT ACPI 7 | --- 8 | 在80x86体系结构上,内核必须显式地与几种时钟和定时电路打交道,时钟电路同时用于跟踪当前时间和产生精确的时间度量。定时器电路由内核编程,所以它们以固定的、预先定义的频率发出中断。这样的周期性中断对于实现内核和用户程序使用的软定时器都是非常重要的,这里有几种IBM兼容PC上的时钟和硬件电路。 9 | 10 | ### 实时时钟 - RTC ### 11 | 12 | 所有的PC都包含一个叫实时时钟(*Renl Time Clock RTC*)的时钟,它是独立于CPU和所有其他芯片的。 13 | 14 | 即使当PC被切断电源,RTC依旧继续工作,因为它靠一个小电池或蓄电池供电。CMOS TAM和RTC被继承在一个芯片[^1]上。 15 | 16 | [^1]: Motorola 146818或其他芯片上。 17 | 18 | RTC能在IRQ8上发出周期性的中断,频率在2~8192Hz之间,也可以对RTC进行编程以使当RTC到达某个特定的值时激活IRQ8线,也就是作为一个闹钟来工作。 19 | 20 | Linux只用RTC来获取时间和日期,不过通对*/dev/rtc*设备文件进行操作,也允许进程对RTC编程。内核通过0x7x和0x71 I/O端口访问RTC。系统管理员通过执行Unix系统时钟程序可以设置时钟。 21 | 22 | ### 时间戳计数器 - TSC ### 23 | 24 | 所有的80x86微处理器都包含一条CLK输入引线,它接收玩不振荡器的时钟信号。从Pentium开始,80x86微处理器就都包含一个计数器,它在每个时钟信号到来时加1。该计数器是利用64位的时间戳计数器(*Time Stamp Counter TSC*)寄存器来实现的,可以通过汇编语言指令*rdtsc*来读这个寄存器。当使用这个寄存器时,内核必须考虑到时钟信号的频率,例如时钟节拍的频率时1GHz,那么时间戳计数器每那庙增加一次。 25 | 26 | 与可编程间隔定时器传递来的时间测量相比,Linux利用这个寄存器可获得更精确的时间测量,为了做到这点,Linux在初始化系统的时候必须确定时钟信号的频率。因为编译内核时并不声明这个频率,所以同一内核映象可以运行在产生任何时钟频率的CPU上。 27 | 28 | 算出CPU实际频率的任务时在系统初始化期间完成的。*calibrate_tsc()*函数通过计算一个大约在5ms的时间间隔内所产生的时钟信号的个数算出CPU实际的频率。通过适当地设置可编程间隔定时器的一个通道来产生这个时间常量。 29 | 30 | 在x86平台初始化的代码可以看到,*calibrate_tsc*被初始化为*native_calibrate_tsc*。 31 | 32 | #### #### 33 | 34 | {% highlight c++ %} 35 | struct x86_platform_ops x86_platform = { 36 | .calibrate_tsc = native_calibrate_tsc, 37 | .get_wallclock = mach_get_cmos_time, 38 | .set_wallclock = mach_set_rtc_mmss, 39 | }; 40 | {% endhighlight %} 41 | 42 | 其中*native_calibrate_tsc*代码在文件**中。 43 | 44 | ### 可编程间隔定时器 - PIT ### 45 | 46 | 除了实时时钟和时间戳计数器,IBM兼容PC还包含第三种类型的时间测量设备叫做可编程间隔定时器(*Programmable Interval Timer PIT*)。PIT的作用类似于微波炉的闹钟,让用户意识到烹饪调整的时间见过已经过去了。不同的是,这个设备不是通过震铃,而是发出一个特殊的中断,叫做时钟中断(*timer interrupt*)来通知内核又一个时间间隔过去了。 47 | 48 | Linux给PC的第一个PIT进行编程,使它以大约1000Hz的频率向IRQ0发出时钟中断,即每1ms产生一次时钟中断。这个时间间隔叫做一个节拍(*tick*),它的长度以纳秒为单位放在*tick_nsec*变量中。 49 | 50 | 一般而言,短的节拍产生较高分辨率的定时器,当这种定时器执行同步I/O多路复用[^1]时,有助于多媒体的平滑播放和较快的响应时间。不过,这是一种折中,段的节拍需要CPU在内核态花费更多的时间,也就是在用户态花费较少的时间。因而,用户程序运行得稍微慢一些。 51 | 52 | [^1]: 例如poll()或者select()。 53 | 54 | 时钟中断的频率取决于硬件体系结构,较慢的机器节拍大约为10ms,而较快的机器节拍为大约1ms。在Linux代码中,有几个宏产生决定时钟中断频率的常量: 55 | 56 | 1. HZ产生美妙时钟中断的近似个数,也就是时钟中断的频率,在IBM PC下其值设置为1000。 57 | 2. CLOCK\_TICK\_RATE产生的值为1193182,这个值是8254芯片的内部震荡器频率。 58 | 3. LATCH产生CLOCK\_TICK\_RATE和HZ的比值再四舍五入后的整数值,这个值用来对PIT编程。 59 | 60 | ### CPU本地定时器 ### 61 | 62 | 在最近的80x86微处理的本地APIC中还提供了另一种定时测量设备,即CPU本地定时器。 63 | 64 | CPU本地定时器是一种能够产生单步中断或周期性中断的设备,它类似于刚才描述的可编程间隔定时器,不过,还是有几种区别: 65 | 66 | 1. APIC计数器是32位,而PIC的计数器是16位,因此,可以对本地定时器编程来产生很低频率的中断。 67 | 2. 本地APIC定时器把中断值发送给自己的处理器,而PIT产生一个全局性的中断,系统中的任一CPU都可以对其进行处理。 68 | 3. APIC定时器是基于总线时钟信号的,每隔1,2,4,8,16,32,64或128总线时钟信号到来时对该定时器进行递减可以实现对其编程的目的。相反,PIT有其自己的内部时钟振荡器,可以更灵活的编程。 69 | 70 | ### 高精度事件定时器 ### 71 | 72 | 高精度事件定时器是由Intel和Microsoft联合开发的一种新型定时器芯片,尽管这种定时器在终端用户机器上还不普遍,但Linux 2.6已经能够支持它们。 73 | 74 | HPET提供了许多可以被内核使用的硬件定时器,这种新定时器芯片主要包含8个32位或64位的独立计数器。每个计数器由它自己的时钟信号所驱动,该时钟信号的频率必须至少位10MHz,因此,计数器最少可以每100ns增长一次。任何计数器最多可以与32个定时器相关联,每个定时由一个比较器和一个匹配寄存器组成。比较器是一组用于检测计数器中的值与匹配寄存器中的值是否匹配的电路,如果找到一组匹配值就产生一个硬件中断,一些定时器可以被激活来产生周期性中断。 75 | 76 | 可以通过映射到内存空间的寄存器来对HPET芯片编程。BIOS在自举阶段建立起映射并向操作系统内核报告它的起始内存地址。HPET寄存器允许内核对计数器和匹配寄存器的值进行读和写,允许内核对单步中断进行编程,还允许内核再支持HPET的定时器上激活或禁止周期性中断。 77 | 78 | ### ACPI电源管理定时器 ### 79 | 80 | ACPI电源管理定时器[^2]是另一种时钟设备,包含再几乎所有基于ACPI的主板上。它的时钟信号拥有大约位3.58MHz的固定频率。该舍比实际上是一个简单的计数器,它再每个时钟节拍到来时增加一次。为了读取计数器的当前值,内核需要访问某个I/O端口,该端口的地址由BIOS在初始化阶段确定。 81 | 82 | 如果操作系统或者BIOS可以通过动态降低CPU的工作频率或者工作典雅来节省电池的电能,那么ACPI电源管理定时器就比TSC更优越。当发生这种情况时,TSC的频率发生改变,而ACPI PMT的频率不会改变。另一方面,TSC计数器的高频率非常便于测量特别小的时间间隔。 83 | 84 | [^2]: 也称ACPI PMT。 85 | 86 | 不过,如果系统中存在HPET设备,那么比起其他的电路而言它总是首选,因为它更复杂的结构使得功能更强。 -------------------------------------------------------------------------------- /2014-04-29-memory-barrier.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 优化和内存屏障 4 | category: 内核同步 5 | description: 优化和内存屏障... 6 | tags: 内存屏障 7 | --- 8 | 当使用优化的编译器时,我们的指令有时候并不会严格按照它们再源代码中出现的顺序执行,因为编译器可能重新安排汇编语言指令以另寄存器以最优化的方式使用。此外,现代的CPU通常并行地执行若干条指令,且可能重新安排内存访问,这种重新安排可以极大地加速程序的执行。 9 | 10 | 当然,当处理器同步时,必须避免指令的重新排序,如果放在同步语句之后的一条指令再同步语句本身执行之前,就会出现失控,事实上,所有的同步原语起优化和内存屏障的作用。 11 | 12 | 优化屏障(*optimization barrier*)保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令,这些汇编语言指令在C中都有对应的语句。再Linux中,优化屏障就是*barrier()*宏,它展开为*asm volatile("":::"memory")*。指令*asm*高速编译程序要插入汇编语言的片段。*volatile*关键字禁止编译器把*asm*指令与程序中的其他指令重新组合排序。*memory*关键字强制编译器假定RAM中的所有内存单元已经被汇编语言指令修改,因此,编译器不能使用存放在CPU寄存器中的内存单元的值来优化*asm*指令前的代码。不过值得注意的是,优化屏障并不保证不使当前的CPU把汇编语言指令混在一起执行。 13 | 14 | 内存屏障(*memory barrier*)确保,再此之后的操作开始执行之前,操作已经完成,因此,内存屏障类似于防火墙,可以让任何汇编语言指令都无法通过。再80x86处理器中,下列种类的汇编语言指令是『串行的』,因为它们起内存屏障的作用: 15 | 16 | 1. 对I/O端口进行操作的所有指令。 17 | 2. 有*lock*前缀的所有指令。 18 | 3. 写控制寄存器、系统寄存器或调试寄存器的所有指令。 19 | 4. 一些特别的汇编语言指令例如*lfence*、*sfence*和*mfence*等等。 20 | 5. 少数专门的汇编语言指令,例如终止中断处理程序或异常处理程序的iret指令。 21 | 22 | Linux使用六个内存屏障原语,这些原语也被当作优化屏障,因为我们必须保证编译程序不在屏障前后移动汇编语言指令。『读内存屏障』仅仅作用域从内村读指令,而『写内存品质』仅仅作用域写内存的指令。 23 | 24 | 内存屏障既用于多处理器系统也同样适用于单处理器系统,当内存屏障应该防止仅出现于多处理器系统上的竞争条件时,就使用*smp_xxx()*,在单处理器系统上,它们什么也不做,其他的内存屏障防止出现再单处理器和多处理器系统上的竞争条件。 25 | 26 | 这些内存屏障包括: 27 | 28 | 宏 | 说明 29 | ------------ | ------------- 30 | mb() | 适用于MP和UP的内存屏障 31 | rmb() | 适用于MP和UP的读内存屏障 32 | wmb() | 适用于MP和UP的写内存屏障 33 | smp_mb() | 仅适用于MP的内存屏障 34 | smp_rmb() | 仅适用于MP的读内存屏障 35 | smp_wmb() | 仅适用于MP的写内存屏障 36 | -------------------------------------------------------------------------------- /2014-04-29-timing-in-linux.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Linux计时体系结构 4 | category: 定时测量 5 | description: Linux计时体系结构... 6 | tags: 计时 7 | --- 8 | Linux必定执行与定时相关额度操作,所以内核会周期性地做如下操作: 9 | 10 | 1. 更新自系统启动一来所经过的时间。 11 | 2. 更新时间和日期。 12 | 3. 确定当前进程在每个CPU上已经运行了多长时间,如果已经超过了分配给它的时间,则抢占它。 13 | 4. 更新资源使用统计数。 14 | 5. 检查每个软定时器的时间间隔是否已经达到。 15 | 16 | Linux的计时体系结构是一组与时间流相关的内核数据结构和函数。实际上,基于80x86多处理器机器所具有的计时体系结构与单处理器机器所具有的稍有不同: 17 | 18 | 1. 在单处理器系统上,所有的计时活动都是由全局定时器[^1]产生的中断触发的。 19 | 2. 再多处理器系统上,所有普通的活动,像软定时器的处理,都是由全局定时器产生的中断触发的,而具体CPU的活动,类似监控当前运行进程的执行时间是由本地APIC定时器产生的中断触发的。 20 | 21 | [^1]: 可以是可编程间隔定时器也可以是高精度事件定时器。 22 | 23 | 可惜,以上两种情况的区别有点模糊,例如,某些早期基于Intel 80486处理器的SMP系统不拥有本地APIC。即使到了今天,还有一些SMP主板有瑕疵,因此本地时钟中断根本不文档。在这些情况下,SMP内核必须采用单处理器系统的计时体系结构。另一方面,新近的单处理器系统拥有本地APIC,因此UP内核通常可以使用SMP的计时体系结构。 24 | 25 | Linux的计时体系结构还依赖于时间戳计数器TSC、ACPI电源管理定时器、高精度事件定时器HPET的可用性。内核使用两个基本的计时函数,一个保持当前最新的时间,另一个计算再当前秒内走过的纳秒数。 26 | 27 | 有几种不同的方式获得后一个值,如果CPU有TSC或HPET,就可以用一些更精确的方法,在其他情况下,使用精确性差一些的方法。 28 | 29 | ### 计时体系结构的数据结构 ### 30 | 31 | 计时是体系结构相关的,所以我们只关心80x86体系结构下最重要的变量。 -------------------------------------------------------------------------------- /2014-05-02-buddy-system-struct.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 伙伴系统的结构 4 | category: 内存管理 5 | description: 伙伴系统的结构... 6 | tags: 伙伴系统 7 | --- 8 | 在内核初始化完成后,内存管理的责任交由给伙伴系统(*buddy system*)承担,伙伴系统基于一个异常简单却令人吃惊的强大算法,伙伴系统结合了分配器的两大特征,速度和效率,正是因为如此,伙伴系统已经持续了许多年,一直到现在也在使用。 9 | 10 | ### 伙伴系统 ### 11 | 12 | 系统中的空闲内存块总是两两分组的,每组中的两个内存块被称作伙伴,伙伴的分配可以是彼此独立的,但如果两个伙伴都是空闲的,那么内核将会将其合并成一个更大的内存块,作为下一层次上某个内存块的伙伴。 13 | 14 | 举个例子,当有128KB个内存大小的时候,我们申请一个24KB的内存,这128KB的内存首先分开为两个64KB的大小,其中一个64KB的内存块保留,另一个64KB大小的内存会继续分裂成两个伙伴,变为两个32KB的内存大小,给我们申请使用。反之,当我们24KB的内存已经不再使用,那么这一个32KB的内存块就会返回,和之前保留的一个32KB的内存块合并成64KB内存块,再进行逆运算合并。 15 | 16 | 现在系统实现以上情况时,使用多个内存块链表来实现,例如128KB的内存块会有一个链表,64KB的内存块会有一个链表,当64KB的相邻的内存块已经不再使用,那么就合并成123KB的内存块,并且从64KB内存块链表中移除,返回给128KB内存块链表。 17 | 18 | 而链表的索引通常用阶来表示,例如2^1、2^2分别表示2和4单位的内存块的链表。 19 | 20 | ### 伙伴系统的结构 ### 21 | 22 | 系统内存中的每个物理内存页,也就是页帧,都对应一个*struct page*,每个内存管理区都关联了一个*struct zone*的实例,其中保存了用于管理伙伴数据的主要数组。 23 | 24 | 在[内存管理区](/linux-kernel-architecture/posts/pglist-data-and-zone/)里面有详细的代码,这里只抽出部分代码: 25 | 26 | #### #### 27 | 28 | {% highlight c++ %} 29 | struct zone { 30 | //... 31 | struct free_area free_area[MAX_ORDER]; 32 | //... 33 | } ____cacheline_internodealigned_in_smp; 34 | {% endhighlight %} 35 | 36 | 其中*free_area*是一个非常重要的辅助数据结构,我们可以看如下代码: 37 | 38 | #### #### 39 | 40 | {% highlight c++ %} 41 | struct free_area { 42 | struct list_head free_list[MIGRATE_TYPES]; 43 | unsigned long nr_free; 44 | }; 45 | {% endhighlight %} 46 | 47 | 其中*nr_free*指定了当前内存区中空闲页块的数目。*free_list*是用于链接空闲页的链表,页链表包含大小相同的连续内存区。**阶**是伙伴系统里非常重要的概念,它描述了内存分配的数量单位,例如2^order,order就是阶,并且其范围从0到MAX_ORDER。order通常设置为11,这意味着一次分配可以请求的页的最大数是2^11=2048。 48 | 49 | *free_area[]*数组中的各个元素索引也可以理解为索引,第0个链表代表内存管理区里的单页,第1个链表代表内存管理区里的两页。从下图可以看出,*free_area[0]*每个元素代表单个页帧,*free_area[2]*的每个元素代表4个页帧。 50 | 51 | ![system](images/buddy-system.png){:style="max-width:600px"} 52 | 伙伴系统分配 53 | 54 | 伙伴系统不必是彼此链接的,如果一个内存区在分配期间被分解成两块,内核会自动将另外一块加入到对应的链表中,如果在未来的某个时刻,由于内存释放的缘故,两个内存区域都处于空闲状态,可通过其他地址判断是否为伙伴。管理工作较少也是伙伴系统的一个主要优点。 55 | 56 | 基于伙伴系统的内存管理专注于某个节点的某个内存域,例如DMA或高端内存域。但所有内存管理区和节点的伙伴系统都通过备用分配列表链接起来。在首选内存管理区或节点无法满足分配请求时,首先尝试用同一节点的另一个内存管理区分配,反复尝试下一个结点直到满足请求。 57 | -------------------------------------------------------------------------------- /2014-05-02-memory-fragmentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内存碎片 4 | category: 内存管理 5 | description: 内存碎片... 6 | tags: 反碎片 内存碎片 伙伴系统 7 | --- 8 | 虽然伙伴系统的让内存管理变得简单,但是会造成另外一个问题,就是内存碎片。内存碎片是非常重要的问题因为如果长期占用不同大小的内存产生了内存碎片,则在申请内存的时候会引发缺页异常,但是实际上,依旧有很多的空闲的内存可供使用。 9 | 10 | ![system](images/memory-fragmentation.png) 11 | 12 | 正如上图所示,上图有非常多的剩余空间,但因为内存碎片的问题,当系统需要申请4个内存页的时候,就无法申请内存了,虽然我们的内存空间里剩余内存的数量远远大于4个内存页,但无法找到连续的4个内存页,则会产生一个缺页异常。 13 | 14 | 而对于理想情况下的内存而言,应该如下图中的图例所示,对于一个程序,在其可用的内存地址空间内,不应该存在大片的不连续的内存,应该说,对于应用程序看到的内存区而言,总应该是连续的。 15 | 16 | 我们谈到内存碎片的时候,大多数只涉及到内核,因为对于内核而言,内存碎片确实是一个非常大的问题。虽然大多数现代的CPU都提供了巨型的页使用,但解决内存碎片依旧对内存使用密集型的应用程序有好处。在使用更大的页的时候,地址转换后备缓冲器只需处理较少的项,降低了TLB缓存失效的可能性。但巨型页的分配依旧需要连续的空闲物理内存。 17 | 18 | ### 反碎片 ### 19 | 20 | 在2.6.24版本开发中,防止碎片的方法最终加入到内核,内核认为预防比治疗更加有效,所以内核使用**反碎片**(*anti-fragmentation*)的方法试图从最开始尽可能的防止碎片。 21 | 22 | 内核已经将已分配的页划分为以下3种类型: 23 | 24 | 1. 不可移动的页:在内存中有固定位置,不能移动到其他地方,例如内核。 25 | 2. 可回收的页:不能直接移动,但可以删除,其内容可以从某些源重新生成,例如映射自文件系统的数据。 26 | 3. 可移动的页:可以随意地移动,属于用户空间和应用程序的页属于这个类别,它们是通过页表映射的,如果它们复制到新的位置,页表项可以相应的更新,而不会影响到应用程序。 27 | 28 | 页的可移动性依赖页属于以上3种类别中的哪一种,内核使用的反碎片技术基于将具有相同可移动性分组的思想。也就是说可移动的页和可移动的页具有相同的分组,相同,不可移动的页和不可移动的页具有相同的分组。 29 | 30 | 但要注意的是,从最初开始,内存并未划分成可移动页等不同移动性的不同的区,这些是在运行时行程的,内核的另一种方法确实将内存划分为不同的区,分别用于可移动页和不可移动页的分配。 31 | 32 | 内核使用一些宏来表示不同的迁移类型: 33 | 34 | #### #### 35 | 36 | {% highlight c++ %} 37 | #define MIGRATE_UNMOVABLE 0 38 | #define MIGRATE_RECLAIMABLE 1 39 | #define MIGRATE_MOVABLE 2 40 | #define MIGRATE_PCPTYPES 3 41 | #define MIGRATE_RESERVE 3 42 | #define MIGRATE_ISOLATE 4 /* can't allocate from here */ 43 | #define MIGRATE_TYPES 5 44 | {% endhighlight %} 45 | 46 | 其中变量的意义如下: 47 | 48 | 字段名 | 说明 49 | ------------ | ------------- 50 | MIGRATE_UNMOVABLE | 不可移动内存区 51 | MIGRATE_RECLAIMABLE | 可回收内存区 52 | MIGRATE_MOVABLE | 可移动内存区 53 | MIGRATE_PCPTYPES | 在PCP列表上类型的数量 54 | MIGRATE_RESERVE | 如果向具有特定的内存区的内存分配失败,则可以从此内存区分配内存 55 | MIGRATE_ISOLATE | 不能通过这个区域申请内存,这是一个特殊的虚拟区域,用于跨越NUMA结点移动物理内存页。 56 | MIGRATE_TYPES | 代表迁移类型的数目,不代表具体的区域 57 | 58 | 对伙伴系统数据结构的主要调整,是将空闲列表分解为*MIGRATE_TYPES*个列表: 59 | 60 | #### #### 61 | 62 | {% highlight c++ %} 63 | struct free_area { 64 | struct list_head free_list[MIGRATE_TYPES]; 65 | unsigned long nr_free; 66 | }; 67 | {% endhighlight %} 68 | 69 | 其中*nr_free*统计了所有页表上空闲页的数目,而每种迁移类型都对应一个空闲列表。如果内核无法满足针对某一给定迁移类型的分配请求,则内核提供了一个备用列表,指定了列表中无法满足分配请求时,接下来使用哪种迁移类型。 70 | 71 | #### #### 72 | 73 | {% highlight c++ %} 74 | static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = { 75 | [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, 76 | MIGRATE_MOVABLE, 77 | MIGRATE_RESERVE }, 78 | [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, 79 | MIGRATE_MOVABLE, 80 | MIGRATE_RESERVE }, 81 | [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, 82 | MIGRATE_UNMOVABLE, 83 | MIGRATE_RESERVE }, 84 | [MIGRATE_RESERVE] = { MIGRATE_RESERVE, 85 | MIGRATE_RESERVE, 86 | MIGRATE_RESERVE }, /* Never used */ 87 | }; 88 | {% endhighlight %} 89 | 90 | 在内核想要分配不同的移动页时,如果对应链表为空,则后退到可回收页链表,接下来再到可移动页链表,最后到紧急分配链表。 91 | 92 | ### 虚拟可移动内存区 ### 93 | 94 | 依据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另一种组织该问题的手段,虚拟内存域*ZONE_MOVABLE*。这个机制和可移动性分组框架相比,*ZONE_MOVABLE*必须由管理员显式激活。 95 | 96 | *ZONE_MOVABLE*的基本思想很简单,可用的物理内存划分为两个内存域,一个用于可移动分配,一个用于不可移动分配。这会自动防止不可移动页向可移动内存域引入碎片。 97 | 98 | 不过这也会造成另一个问题,内存如何在两个竞争的区域之间分配可用内存?这显然对内核要求太高,所以管理员必须做出决定。这个数据结构是我们非常熟悉的*zone_type*数据结构。 99 | 100 | #### #### 101 | 102 | {% highlight c++ %} 103 | enum zone_type { 104 | #ifdef CONFIG_ZONE_DMA 105 | ZONE_DMA, 106 | #endif 107 | #ifdef CONFIG_ZONE_DMA32 108 | ZONE_DMA32, 109 | #endif 110 | ZONE_NORMAL, 111 | #ifdef CONFIG_HIGHMEM 112 | ZONE_HIGHMEM, 113 | #endif 114 | ZONE_MOVABLE, 115 | __MAX_NR_ZONES 116 | }; 117 | {% endhighlight %} 118 | 119 | 取决于体系结构和配置,其中*ZONE_MOVABLE*可能位于任何区域,甚至是高端内存区域。与系统中其他的内存区相反,*ZONE_MOVABLE*从不关联到任何硬件上有意义的内存范围,实际上,该内存域中的内存取自高端内存域或者是普通内存域,所以*ZONE_MOVABLE*是一个虚拟内存域。 120 | 121 | 从物理内存域中提取多少内存用于*ZONE_MOVABLE*必须考虑下面的情况: 122 | 123 | 1. 用于不可移动分配的内存会平均分布到所有的内存节点上。 124 | 2. 只使用来自最高内存域的内存[^1]。 125 | 126 | 以上情况所起到的结果是: 127 | 128 | 1. 用于为虚拟内存域ZONE\_MOVABLE提取内存页的物理内存域,保存在全局变量*movable_zone*中。 129 | 2. 对每个节点来说,*zone_movable_pfn[node_id]*表示*ZONE_MOVABLE*在*movable_zone*内存域中所取得内存的起始地址。 130 | 131 | [^1]: 在32位系统上,这可能是ZONE_HIGHMEM,但对于64系统,可能是ZONE_NORMAL或ZONE_DMA32。 -------------------------------------------------------------------------------- /2014-05-02-spin-lock.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 自旋锁 4 | category: 内核同步 5 | description: 自旋锁... 6 | tags: 自旋锁 spin_lock 7 | --- 8 | 在同步技术中,最为广泛应用的就是加锁(*locking*)机制了。当内核控制路径必须访问共享数据结构或进入临界区时,就需要为自己获取一把『锁』。由锁机制保护的资源非常类似于限制于房间内的资源,如果某人进入房间,就把门锁上,等这个人使用完成后,后面的人才可以使用。 9 | 10 | 当内核控制路径希望访问某个同步的资源,就需要试图获取钥匙『打开门』,当且仅当资源空闲时,它才能成功,然后,只要它还想继续使用这个资源,那么锁就会一直关闭着。当内核控制路径不再使用资源,就释放了锁,门就打开了,另一个内核控制路径就可以访问。 11 | 12 | 自旋锁(*spin lock*)是用来在多处理环境中工作的一种特殊的锁,如果内核控制路径发现自旋锁开着,就获取锁并继续自己的执行,相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径『锁着』,就在周围『旋转』,反复执行一条紧密的循环指令[^1],直到锁被释放。 13 | 14 | [^1]: 可以想象成一个简单的while(is_lock),其中is_lock在有锁的时候为True,但逻辑不止如此简单。 15 | 16 | 自旋锁的循环指令表示『busy』,即使等待的内核控制路径无事可做,它也在CPU上保持运行。但是自旋锁非常方便,因为很多内核资源只锁1毫秒的时间片段,所以说,释放CPU和随后又获得CPU都不会消耗多少时间。 17 | 18 | 一般来说,由自旋锁所保护的每个临界区都是禁止内核抢占的,在单处理器系统上,这种锁并不起锁的作用,自旋锁紧紧是禁止或启用内核抢占。在自旋锁等待的时候,内核抢占依旧有效,因此,等待自旋锁释放的进程有可能被更高的优先级的进程替代。 19 | 20 | 在Linux中,每个自旋锁都用*spinlock_t*结构表示: 21 | 22 | #### #### 23 | 24 | {% highlight c++ %} 25 | typedef struct { 26 | raw_spinlock_t raw_lock; 27 | #ifdef CONFIG_GENERIC_LOCKBREAK 28 | unsigned int break_lock; 29 | #endif 30 | #ifdef CONFIG_DEBUG_SPINLOCK 31 | unsigned int magic, owner_cpu; 32 | void *owner; 33 | #endif 34 | #ifdef CONFIG_DEBUG_LOCK_ALLOC 35 | struct lockdep_map dep_map; 36 | #endif 37 | } spinlock_t; 38 | {% endhighlight %} 39 | 40 | 其中*raw_spinlock_t*代码为: 41 | 42 | #### #### 43 | 44 | {% highlight c++ %} 45 | typedef struct { 46 | volatile unsigned int slock; 47 | } 48 | {% endhighlight %} 49 | 50 | 其中主要的两个参数为*slock*和*break_lock*,其中*slock*表示自旋锁的状态,值为1则表示『未枷锁』状态,而任何负数和0都表示已加锁的状态。而*break_lock*表示进程正在等待自旋锁[^2]。 51 | 52 | [^2]: 正在进行紧凑的循环来等待锁的释放。当然这个仅在CMP和内核抢占的情况下使用。 53 | 54 | 与自旋锁有关的宏如下。 55 | 56 | 宏 | 说明 57 | ------------ | ------------- 58 | spin_lock\_init() | 把自旋锁设置为1,未锁的状态 59 | spin_lock() | 循环,知道自旋锁变为1,然后把自旋锁设置为0 60 | spin_unlock() | 把自旋锁设置为1 61 | spin_unlock\_wait() | 等待,直到自旋锁变为1 62 | spin_is\_locked() | 如果自旋锁设置为1,返回0,否则返回1 63 | spin_trylock() | 把自旋锁设置为0,若原来锁是1,则返回1,否则为0 64 | 65 | ### 具有内核抢占的spin_lock宏 ### 66 | 67 | 自旋锁的实现和体系结构相关,也和编译时是否允许内核抢占有关,所以这个笔记就仅仅列举了逻辑,并不会找代码,代码实际上可以在**中找,内核提供了两种api接口,一个是smp api,另一个是up api。 68 | 69 | 在支持内核抢占的情况下,这个宏会获取自旋锁的地址*slp*作为它的参数,并执行下面的操作。 70 | 71 | 1. 调用*preemet_disbale()*禁用内核抢占。 72 | 2. 调用函数*__raw_spin_trylock()*,它对自旋锁的*slock*字段执行原子性的测试和设置操作。 73 | 3. 如果自旋锁中是开着的,则宏结束,内核控制路径获得自旋锁。 74 | 4. 否则内核控制路径无法获得自旋锁,因此宏必须执行循环一直到其他CPU上运行的内核控制路路径释放自旋锁。 75 | 5. 如果*break_lock*字段等于0,则设置为1,通过监测该字段,拥有锁并在其他CPU上运行的进程可以知道是否有其他进程在等待这个锁。 76 | 6. 执行紧密的循环。 77 | 7. 跳转到第一步再次试图获取自旋锁。 78 | 79 | ### 非抢占式的spin_lock宏 ### 80 | 81 | 如果在没有选择内核抢占的情况下,*spin_lock*所做的操作就非常不同,宏生成一个汇编语言片段,用于下面紧凑的循环[^3]: 82 | 83 | {% highlight asm %} 84 | 1: lock; decb slp->lock 85 | jns sf 86 | 2: pause 87 | cmpb $0, slp->slock 88 | jle 2b 89 | mp 1b 90 | 3: 91 | {% endhighlight %} 92 | 93 | 汇编语言指令*decb*递减自旋锁的值,该指令是原子的,因为带有*lock*字节前缀,随后检测符号标志,如果它被清0,说明自旋锁被设置为1,因此从标记3处继续执行,否则在标签2处执行紧凑的循环直到自旋锁出现正直,然后从标签1处开始执行。 94 | 95 | [^3]: 实际上代码更加复杂。 96 | -------------------------------------------------------------------------------- /2014-05-03-irq-and-interrupt.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: IRQ和中断 4 | category: 中断和异常 5 | description: IRQ和中断... 6 | tags: IRQ APIC PIC 中断 7 | --- 8 | 中断是提高计算机性能和吞吐性的一种设计,在没有中断的时候,每个设备都要等待其他设备,造成资源浪费,所以中断是一个非常有用的。中断中,IRQ和PIC是基础,具体可以看[中断](http://zh.wikipedia.org/wiki/中斷)的维基百科。 9 | 10 | 虽然中断可以提高计算机处理性能,但过于密集的中断请求和响应反而会影响系统性能。这种情况下称为中断风暴。 11 | 12 | 每个能够发出中断请求的硬件设备控制器都有一条名为IRQ(*Interrupt ReQuest*)的输出线。复杂的设备有多条IRQ线,例如PCI卡可能使用多达4条的IRQ线。所有的IRQ线都与一个名为可编程中断控制器(*Programmable Interrupt Controller,PIC*)的硬件电路的输入引脚相连,可变成中断控制器执行下面的动作: 13 | 14 | 1. 监视IRQ线,检查产生的信号,如果有两条或两条以上的IRQ线上产生信号,就选择引脚编号较小的IRQ线。 15 | 2. 如果一个引发信号出现在IRQ线上,就做相应的处理。 16 | 17 | PIC对一个引发在IRQ线上的信号做如下处理: 18 | 19 | 1. 把接收到的引发信号转换成对应的向量。 20 | 2. 把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量。 21 | 3. 把引发信号发送到处理器的INTR引脚,即产生了一个中断。 22 | 4. 等待,直到CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它,当这种情况发生时,清INTR线。 23 | 5. 返回第一步循环处理。 24 | 25 | IRQ线总是从0开始顺序编号的,因此,第一条IRQ线通常表示成IRQ0,与IRQn关联的Intel的缺省向量是n+32。正如前面所说,通过向中断控制器端口发布和式的指令,就可以修改IRQ和向量之间的映射。 26 | 27 | 可以有选择地禁止每条IRQ线,因此,可以对PIC编程从而禁止IRQ,也就是说,可以高速PIC停止对给定的IRQ线发布中断,或者激活它们。禁止的中断是丢失不了的,它们一旦被激活,PIC就又把它们发送到CPU,这个特点被大多数中断处理程序使用,因为这允许中断处理程序逐次地处理同一类型的IRQ。 28 | 29 | 有选择地激活或禁止IRQ线不同于可屏蔽中断的全局屏蔽和非屏蔽。当*eflags*寄存器的*IF*标志被清0时,由PIC发布的每个可屏蔽中断都由CPU暂时忽略。*cli*和*sti*汇编指令分别清除和设置该标志位。 30 | 31 | 传统的PIC是由两片8259A风格的外部芯片以『级联』的方式连接在一起的,每个芯片可以处理多达8个不同的IRQ输入线,因为从PIC的INT输出线连接到主PIC的IRQ2引脚,因此,可用IRQ线的个数限制为15。 32 | 33 | ### 高级可编程中断控制器 ### 34 | 35 | 以前的描述仅仅涉及为单处理器系统涉及的PIC,如果系统只有一个单独的CPU,那么主PIC的输出线以直接了当的方式连接到CPU的INTR引脚。然而,如果系统中包含两个或者多个CPU,那么这种方式不再有效。 36 | 37 | 为了充分发挥SMP体系结构的并行性,能够把中断传递给系统中的每个CPU就非常重要,因此Intel从Pentiun III引入了一种名为I/O高级可编程控制器的新组件,用以代替老式的可编程中断控制器。新近的主板为了支持以前的操作都包括两种芯片,此外80x86微处理器当前所有的CPU都含有一个本地的APIC,每个本地APIC都有32位寄存器,一个内部时钟,一个本地定时设备以及为本地APIC中断保留的两条额外的IRQ线LINT0和LINT1,所有本地APIC都连接到一个外部I/O APIC,形成一个多APIC系统。 38 | 39 | ![system](images/APIC.png) 40 | 多APIC系统 41 | 42 | 上图是一个多APIC系统的结构,一条APIC总线把『前端』的I/O APIC连接到本地APIC。来自设备的IRQ线连接到I/O APIC,因此,相对于本地APIC,I/O APIC起路由器的作用。I/O APIC的组成为: 43 | 44 | 1. 一组24条的IRQ线。 45 | 2. 一张24项的中断重定向表。 46 | 3. 可编程寄存器。 47 | 4. 通过APIC总线发送和接收APIC信息的一个信息单元。 48 | 49 | 与8259A的IRQ引脚不同,中断优先级并不与引脚号相关联,中断重定向表中的每一项都可以被单独编程以指明中断向量和优先级、目标处理器以及选择处理器的方式。重定向表中的信息用于把每个外部IRQ信号转换成一条消息,然后通过APIC总线发给一个或多个APIC单元。 50 | 51 | 来自外部硬件设备的中断请求以两种方式在可用CPU之间分发: 52 | 53 | 1. 静态分发,IRQ信号传递给重定向表和相应项中所列出的本地APIC,中断立即传递给一个特定的CPU或一组CPU,也可以通过广播的方式发送给所有CPU。 54 | 2. 动态分发,如果处理器正在执行最低优先级的进程,IRQ信号就传递给这种处理器的本地APIC,每个本地APIC都有一个可编程任务优先级寄存器TPR,TPR用来计算当前运行进程的优先级。 55 | 56 | 如果两个或多个CPU共享最低优先级,就利用仲裁技术在这些CPU之间分配负荷,在本地APIC的仲裁优先级寄存器中,给每个CPU都分配一个0~15范围内的值,其中0最低,15最高。 57 | 58 | 每当中断传递给一个CPU时,其相应的仲裁优先级就自动设置为0,而其他的每个CPU的仲裁优先级都增加1,当仲裁优先级寄存器大于15时,就把它置为获胜CPU的前一个仲裁优先级加1的值。因此中断以轮转方式在CPU之间分发,具有相同的任务优先级。 59 | 60 | 除了在处理器之间分发中断外,多APIC系统还允许CPU产生处理器间中断。当一个CPU希望把一个中断发送给另一个CPU时,它就在自己本地APIC的中断指令寄存器中存放这个中断向量和目标本地APIC的标识符,然后通过APIC总线向目标本地APIC发送一条消息,从而向自己的CPU发出一个相应的中断。 61 | 62 | 处理器之间的中断简称IPI,时SMP体系结构定义非常重要的一部分,并由Linux有效地用来在CPU之间交换信息。 63 | 64 | 目前大部分处理器系统都包含一个I/O APIC芯片,可以用以下两种方式对这种芯片进行配置。 65 | 66 | 1. 作为一种标准8259A方式地外部PIC连接到CPU,本地APIC被禁止,两条LINT0和LINT1本地IRQ线分别配置为INTR和NMI引脚。 67 | 2. 作为一种标准外部I/O APIC,本地APIC被激活,且所有地外部中断都通过I/O APIC接收。 -------------------------------------------------------------------------------- /2014-05-03-read-and-write-spin-lock.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 读/写自旋锁 4 | category: 内核同步 5 | description: 读/写自旋锁... 6 | tags: 自旋锁 7 | --- 8 | 读/写自旋锁的引入是为了增加内核的并发能力,只要没有内核控制路径对数据结构进行修改,读/写自旋锁就允许多个内核控制路径同时读一个数据结构,如果一个内核控制路径想对这个数据结构进行操作,那么它必须首先获取读/写自旋锁的写锁,写锁的授权独占访问这个资源。当然,允许对数据结构的并发可以提高系统性能。 9 | 10 | 每个读/写自旋锁都是一个*rwlock_t*结构,代码如下: 11 | 12 | #### #### 13 | 14 | {% highlight c++ %} 15 | typedef struct { 16 | raw_rwlock_t raw_lock; 17 | #ifdef CONFIG_GENERIC_LOCKBREAK 18 | unsigned int break_lock; 19 | #endif 20 | #ifdef CONFIG_DEBUG_SPINLOCK 21 | unsigned int magic, owner_cpu; 22 | void *owner; 23 | #endif 24 | #ifdef CONFIG_DEBUG_LOCK_ALLOC 25 | struct lockdep_map dep_map; 26 | #endif 27 | } rwlock_t; 28 | {% endhighlight %} 29 | 30 | 自旋锁的实现是体系结构相关的,所以我们可以看x86里面自旋锁的结构,从上面的结构体中我们可以看到有一个*raw_rwlock_t*的结构体对象*raw_lock*,我们可以看看在arch/x86中的*raw_rwlock_t*对象。 31 | 32 | #### #### 33 | 34 | {% highlight c++ %} 35 | typedef struct { 36 | unsigned int lock; 37 | } raw_rwlock_t; 38 | {% endhighlight %} 39 | 40 | 我们可以看到,这个结构体里就就只有一个*lock*字段。 41 | 42 | *lock*字段是一个32位的字段,分为两个不同的部分: 43 | 44 | 1. 24位计数器,表示对受保护的数据结构并发地进行读操作和内核控制路径的数目,这个计数器的二进制补码存放在这个字段的0~23位。 45 | 2. 『未锁』标志字段,当没有内核控制路径在读或者写时设置这个位,否则就清0。这个『未锁』标志存放在*lock*字段的第24位。 46 | 47 | 如果自旋锁为空,那么*lock*字段的值位0x010000000,如果一个两个或者多个进程因为读获取了自旋锁,那么*lock*字段的值位0x00ffffff,0x00fffffe等。与*spinlock_t*结构一样,*rwlock_t*结构也包含*break_lock*字段。 48 | 49 | *rwlock_init*宏把读/写自旋锁的*lock*字段初始化位未锁的状态,把*break_lock*初始化为0。 50 | 51 | ### 为读获取和释放一个锁 ### 52 | 53 | *read_lock*宏作用于读/写自旋锁的地址*rwlp*,与*spin_lock*宏非常相似,如果编译内核时选择了内核抢占选项,*read_lock*宏执行与*spin_lock()*非常相似的操作,只是有一点不同,该宏执行了*__raw_read_trylock()*函数获得读/写自旋锁。 54 | 55 | #### #### 56 | 57 | {% highlight c++ %} 58 | static inline int __raw_read_trylock(raw_rwlock_t *lock) 59 | { 60 | atomic_t *count = (atomic_t *)lock; 61 | 62 | if (atomic_dec_return(count) >= 0) 63 | return 1; 64 | atomic_inc(count); 65 | return 0; 66 | } 67 | {% endhighlight %} 68 | 69 | 读/写锁计数器*lock*字段时通过原子操作来访问的,尽管如此,但整个函数对计数器的操作并不是原子性的。例如,在用*if*语句完成对计数器的值的测试之后并返回1之前,计数器的值可能发生变化。不过函数能够正常工作。 70 | 71 | 实际上,只有在递减之前计数器的值不为0或者负数的情况下,函数才返回1,因为计数器等于0x01000000表示没有任何进程占用锁,等于0x00ffffff表示有一个读者,而等于0x00000000表示有一个写者。 72 | 73 | *read_lock*宏原子地把自旋锁的值减去1,以此增加读者的个数,如果函数递减操作产生一个非复制,就获得自旋锁,否则就调用*_\_read_lock_failed()*函数,这个函数原子地增加*lock*字段以取消由*readl_lock*宏执行的递减操作,然后循环,直到*lock*字段变为正数。接下来,*__read_lock_failed()*又试图获得自旋锁。 74 | 75 | 解锁的过程相当简单,使用*read_unlock*宏只是简单的增加了*lock*字段的计数器以减少读者的计数,然后调用*preempt_enable()*重新启用内核抢占。 76 | 77 | ### 为写获取和释放一个锁 ### 78 | 79 | *write_lock*宏的实现方式与*spin_lock()*和*read_lock()*相似,例如,如果支持内核抢占,则该函数禁用内核抢占并通过*__raw_write_trylock()*立即获得锁,如果该函数返回0,则说明锁已经被占用,然后该宏就重新启用内核抢占并开始等待循环。 80 | 81 | #### #### 82 | 83 | {% highlight c++ %} 84 | static inline int __raw_write_trylock(raw_rwlock_t *lock) 85 | { 86 | atomic_t *count = (atomic_t *)lock; 87 | 88 | if (atomic_sub_and_test(RW_LOCK_BIAS, count)) 89 | return 1; 90 | atomic_add(RW_LOCK_BIAS, count); 91 | return 0; 92 | } 93 | {% endhighlight %} 94 | 95 | 函数*__raw_write_trylock()*从读/写自旋锁中减去0x01000000,从而清除未上锁标志,如果减操作产生0,说明没有读者,则获取锁并返回1,否则,函数原子地在自旋锁上加0x01000000以取消减操作。 96 | 97 | 释放锁同样就暗淡,使用*write_unlock*宏即可,将相应地位标记为未锁状态,然后再调用*preempt_enable()*重新启用内核抢占。 98 | -------------------------------------------------------------------------------- /2014-05-04-exception.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 异常定义 4 | category: 中断和异常 5 | description: 异常定义... 6 | tags: 异常 7 | --- 8 | 80x86处理器发布了许多种异常,内核必须为每一种异常提供一个专门地异常处理程序,对于某些异常,CPU控制单元在开始执行异常处理程序前会产生一个硬件出错码,并押入内核态堆栈。 9 | 10 | 下面列举了一些常用的异常: 11 | 12 | 异常 | 说明 13 | ------------ | ------------- 14 | 0 | 当一个程序试图执行整数被0除操作时产生 15 | 1 | 设置eflags的TF标志或者一条指令或操作数地地址落在一个活动debug寄存器范围内 16 | 2 | 为非屏蔽中断保留,但没有使用 17 | 3 | 由int3(断点)指令引起,通常由debugger插入 18 | 4 | 当eflags地OF标志被设置时,into检查溢出指令被执行 19 | 5 | 对于有效地址范围之外地操作数,检查地址边界指令被执行 20 | 6 | CPU执行单元监测到一个无效地操作码 21 | 7 | 随着cr0地TS标志被设置,ESCAPE、MMX或XMM指令被执行 22 | 8 | 正常情况下,当CPU正试图为前一个异常调用处理程序时,同时又检测到一个异常,两个异常能被串行地处理,然而,在少数情况下,处理器不能串行地处理它们,因而产生这个异常。 23 | 9 | 因外部地数字协处理器引起地问题 24 | 10 | CPU试图让一个上下文切换到无效的TSS进程 25 | 11 | 引用一个不存在的内容段 26 | 12 | 试图超过栈段界限地指令,或者由ss标识地段不在内存 27 | 13 | 违反了80x86保护模式下的保护规则 28 | 14 |寻址地页不在内存,相应的页表项为空,或者违反了一种分页保护机制 29 | 15 | 由Intel保留 30 | 16 | 集成到CPU芯片中地浮点单元用信号通知一个错误情形,如数字溢出 31 | 17 | 操作地地址没有被正确地对齐 32 | 18 | 机器检查机制检测到一个CPU错误或总线错误 33 | 19 | 集成到CPU芯片中地SSE或SSE2单元对浮点操作用信号通知一个错误情形 34 | 20-31 | 这些值由Intel留作将来开发 35 | 36 | 下面列举了一些常用地异常处理程序和信号: 37 | 38 | 39 | 异常 | 处理程序 | 信号 40 | ------------ | ------------- | ------------ 41 | 0 | divide_error() | SIGFPE 42 | 1 | debug() | SIGTRAP 43 | 2 | nmi() | None 44 | 3 | int3() | SIGTRAP 45 | 4 | overflow() | SIGSEGV 46 | 5 | bounds() | SIGSEGV 47 | 6 | invalid_op() | SIGILL 48 | 7 | device\_not\_available() | None 49 | 8 | doublefault_fn() | None 50 | 9 | coprocessor\_segment\_overrun()| SIGFPE 51 | 10 | invalid_tss() | SIGSEGV 52 | 11 | segment\_not\_present() | SIGBUS 53 | 12 | stack_segment() | SIGBUS 54 | 13 | general_protection() | SIGSEGV 55 | 14 | page_fault() | SIGSEGV 56 | 15 | None | None 57 | 16 | coprocessor_error() | SIGFPE 58 | 17 | alignment_check() | SIGSEGV 59 | 18 | machine_check() | None 60 | 19 | simd\_coprocessor\_error() | SIGFPE 61 | -------------------------------------------------------------------------------- /2014-05-04-interrupt-descriptor-table.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 中断描述符表 4 | category: 中断和异常 5 | description: 中断描述符表... 6 | tags: IDT 中断描述符表 7 | --- 8 | 中断描述符表(*Interrupt Descriptor Table,IDT*)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应地中断或异常处理程序地入口地址,内核在允许中断发生前,必须适当地初始化IDT。 9 | 10 | GDT和IDT地格式非常相似,表中地每一项对应一个中断或异常向量,每个向量由8个字节组成,因此,最多需要256x8=2048字节来存放IDT。*idtr*CPU寄存器使IDT可以位于内存地任何地方,它指定IDT地线性地址及其限制地最大长度。在允许中断之前,必须用*lidt*汇编指令初始化*idtr* 11 | 12 | IDT包含三种类型地描述符: 13 | 14 | ![IDT](images/idt.png) 15 | 中断描述符表 16 | 17 | 这些描述符是: 18 | 19 | 1. 任务门(*task gate*),当中断信号发生时,必须取代当前进程地那个进程地TSS选择符存放在任务门中。 20 | 2. 中断门(*interrupt gate*),包含段选择符和中断或异常处理程序地段内偏移量,当控制权转移到一个适当地段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。 21 | 3. 陷阱门(*trap gate*),和中断门类似,只是控制权传递到一个适当地段处理器不修改IF标志。 22 | 23 | ### 中断和异常的硬件处理 ### 24 | 25 | 假定内核已经被初始化,因此,CPU在保护模式下运行,Linux只有在刚刚启动的时候是在实模式,之后便进入保护模式。 26 | 27 | 在执行了一条指令之后,*cs*和*eip*这对寄存器包含下一条将要执行的指令的逻辑地址,在处理了那条指令之后,控制单元会检查在运行前一条指令时是否已经发生了一个中断异或异常[^1]。如果发生了一个中断或者异常,那么控制单元执行下列操作: 28 | 29 | [^1]: 汇编里通常一条指令执行结束后才会产生一个异常。 30 | 31 | 1. 确定与中断或异常的关联向量i。 32 | 2. 读由*idtr*寄存器指向的IDT表中的第i项门描述符。 33 | 3. 从*gdtr*寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符,这个描述符指定只哦你果断或异常处理程序所在的段的基地址。 34 | 4. 确定中断是由授权的中断发生源发出的。 35 | 5. 检查是否发生了特权等级变化。 36 | 6. 如果故障已经发生,用引起异常的指令地址装载*cs*和*eip*寄存器[^2],从而使这条指令能够再次被执行。 37 | 7. 在栈中保存*eflags*、*cs*以及*eip*的内容。 38 | 8. 如果异常产生了一个硬件出错码,则保存在栈中。 39 | 9. 装载*cs*和*eip*寄存器,其值分别是IDT表中的第i项门描述符的段选择符和偏移量,这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。 40 | 41 | [^2]: CPU执行的指令跟*cs*和*eip*有关,CPU会读取这eip寄存器的内容并找到cs寄存器里相应的地址,然后执行指令。 42 | 43 | 控制单元所执行的最后一步就是跳转到中断或者异常处理程序,也就是说,醋栗完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。 44 | 45 | 中断或异常被处理完毕后,相应的处理程序必须产生一条*iret*指令,把控制权转交给被中断的进程,这样控制单元就会产生以下操作: 46 | 47 | 1. 用保存在栈中的值装载*cs*、*eip*或*eflags*寄存器,如果一个硬件出错码曾被押入栈中,并且在*eip*内容上面,那么执行*iret*指令必须先弹出这个硬件出错码。 48 | 2. 检查处理程序的CPL是否等于*cs*中的最低两位的值,如果是,说明在同一特权级,*iret*中止执行,否则转入下一步。 49 | 3. 从栈中装载*ss*和*esp*寄存器,返回到与旧特权级相关的栈。 50 | 4. 检查*ds*、*es*、*fs*以及*gs*段寄存器的内容,如果其中一个寄存器包含的选择符是个段描述符,并且其DPL值小于CPL,那么就清除相应的段寄存器。 51 | 52 | ### 初始化中断描述符表 ### 53 | 54 | 内核启用中断之前,必须把IDT表的初始地址装到*idtr*寄存器,并初始化表中的每一个项,这项工作是在初始化系统时完成的。 55 | 56 | *int*指令允许用户态进程发出一个中断信号,其值可以时0~255的任意一个向量。因此,为了防止用户通过*int*指令模拟非法的中断和异常,IDT的初始化必须非常小心,因此可以通过把中断和陷进门描述符的DPL字段设置为0来实现。 57 | 58 | 如果进程试图发出其中的一个中断信号,控制单元将检查出CPL的值与DPL字段有冲突,并且产生一个『General protection』异常。 59 | 60 | 在一些少数的情况下,用户态进程必须能够发出一个编程异常,为此,只要把中断或陷阱门描述符的DPL字段设置为3,即特权级尽可能高一点就可以了。 -------------------------------------------------------------------------------- /2014-05-04-seqlock.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 顺序锁 4 | category: 内核同步 5 | description: 顺序锁... 6 | tags: 顺序锁 7 | --- 8 | 当使用读/写自旋锁时,内核控制路径发出的执行*read_lock*或*write_lock*操作的请求具有相同的优先级,读者必须等待,直到写操作完成,同样,写者也必须等待,直到读操作的完成。 9 | 10 | Linux2.6中引入了顺序锁(*seqlock*),它与读/写自旋锁非常相似,只是它为写者赋予了较高的优先级,事实上,即使在读者正在读的时候也可以允许写者继续运行。这种策略的好处时写者永远不会等待,除非另一个写者也尝试写入同样的一个数据结构。但同样有缺点,缺点时有些时候读者不得不反复多次读取相同的数据结构直到获得它有效的副本。 11 | 12 | 每个顺序所都是包括两个字段的*seqlock_t*结构,代码如下: 13 | 14 | #### #### 15 | 16 | {% highlight c++ %} 17 | typedef struct { 18 | unsigned sequence; 19 | spinlock_t lock; 20 | } seqlock_t; 21 | {% endhighlight %} 22 | 23 | *seqlock_t*是一个包含两个字段的结构,其中一个类型为*spinlock_t*的*lock*锁字段,用于实现锁,而*sequence*是一个顺序计数器。每个读者都必须在读取数据前后两次读顺序计数器,并检查两次读到的值是否相同,如果不相同,说明新的写者已经开始写并增加了顺序计数器,所以刚才读到的数据结构是无效的。 24 | 25 | 初始化一个顺序锁的代码如下: 26 | 27 | #### #### 28 | 29 | {% highlight c++ %} 30 | #define seqlock_init(x) 31 | do { 32 | (x)->sequence = 0; 33 | spin_lock_init(&(x)->lock); 34 | } while (0) 35 | {% endhighlight %} 36 | 37 | 通过把*SEQLOCK_UNLOCKED*赋值给变量*seqlock_t*或执行*seqlock_init*宏可以将锁初始化为未锁的状态。从上面的代码可以看出,除了初始化锁,也将读取顺序计数器初始化为0。 38 | 39 | 写者通过调用*write_seqlock()*和*write_sequnlock()*获取和释放顺序锁,第一个函数获取*seqlock_t*数据结构中的自旋锁,然后使顺序计数器加1,第二个函数再次增加顺序计数器,然后释放自旋锁,这样可以保证写者在写的过程中,计数器会是技术,并且当没有写者改变数据的时候,计数器的值是偶数。 40 | 41 | #### #### 42 | 43 | {% highlight c++ %} 44 | static inline void write_seqlock(seqlock_t *sl) 45 | { 46 | spin_lock(&sl->lock); 47 | ++sl->sequence; 48 | smp_wmb(); 49 | } 50 | 51 | static inline void write_sequnlock(seqlock_t *sl) 52 | { 53 | smp_wmb(); 54 | sl->sequence++; 55 | spin_unlock(&sl->lock); 56 | } 57 | {% endhighlight %} 58 | 59 | *read_seqbegin()*返回顺序锁当前的顺序号,如果局部变量*seq*的值是负数,或者*seq*的值与顺序所的顺序计数器的当前值不匹配,那么该函数就返回1。 60 | 61 | #### #### 62 | 63 | {% highlight c++ %} 64 | static inline unsigned read_seqcount_begin( 65 | const seqcount_t *s) 66 | { 67 | unsigned ret; 68 | 69 | repeat: 70 | ret = s->sequence; 71 | smp_rmb(); 72 | if (unlikely(ret & 1)) { 73 | cpu_relax(); 74 | goto repeat; 75 | } 76 | return ret; 77 | } 78 | {% endhighlight %} 79 | 80 | 当读者进入临界区时,无需禁用内核抢占,另一方面,由于写者获取自旋锁,所以它进入临界区时自动禁用内核抢占。当然,并不是每一种锁都可以使用顺序锁来保护,如果要使用顺序锁,则必须满足以下条件: 81 | 82 | 1. 被保护的数据结构不包括被写者和被读者间接引用的指针。 83 | 2. 读者的临界区代码没有副作用[^1]。 84 | 85 | 此外,读者临界区代码应该简短,而且写者应该不常获取顺序所,否则,反复的读访问会引起严重的开销,在Linux中,使用顺序锁的典型例子包括保护一些与系统时间处理相关的数据结构。 86 | 87 | [^1]: 否则多个读者的操作会与单独的读操作有不同的结果。 -------------------------------------------------------------------------------- /2014-05-05-loop-interrupt.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 处理程序的嵌套执行 4 | category: 中断和异常 5 | description: 处理程序的嵌套执行... 6 | tags: 异常 中断 嵌套执行 内核控制路径 7 | --- 8 | 每个中断或异常都会引起一个内核控制路径,或者说代表当前进程在内核态执行单独的指令序列。我们再来回顾一下什么是内核控制路径。内核控制路径(kernel control path)表示内核处理系统调用、异常或中断所执行的指令序列。所以,我们只要知道内核控制路径是一些指令序列即可。 9 | 10 | 当I/O设备发出一个中断时,相应的内核控制路径的第一部分指令就是那些把寄存器的内容保存在内核堆栈的指令,而最后一部分指令就是恢复寄存器内容并让CPU返回到用户态的哪些指令。 11 | 12 | 内核控制路径可以任意嵌套,一个中断处理程序可以被另一个中断处理程序『中断』,所以就会产生中断处理程序的嵌套执行,如下图: 13 | 14 | ![IRQ](images/irq_loop.png) 15 | 嵌套中断处理 16 | 17 | 如上图所示,当产生一个中断之后,进入内核态的一个中断处理程序。中断处理程序被另外一个中断给『中断』,于是进入第二个中断处理程序。当第二个中断处理程序处理完毕后,返回第一个中断处理程序,第一个中断处理程序处理完毕后,返回用户态进程。 18 | 19 | 允许内核控制路径嵌套必须有特定的规定,那就是中断处理程序必须永不阻塞,事实上,异常要么是由编程错误引起的,要么是由调试程序触发的,然而缺页异常发生在内核态。这发生在当进程试图对属于其他地址空间的页进行寻址,而这个页现在不在RAM中时。 20 | 21 | 当处理这样一个异常时,内核可以挂起当前进程,并用另一个进程替代它,知道请求的页可以使用位置,只要被挂起的进程又获得处理器,处理缺页异常的内核控制路径就恢复执行。因为缺页异常从不引起另一个异常,所以与异常相关的至多两个内核控制路径会堆叠在一起。 22 | 23 | 与异常形成对照的时,尽管中断的内核控制路径代表当前进程运行,但由I/O设备产生的中断并不引用当前进程的数据结构,实际上,当一个给定的中断发生时,要预测哪个进程即将运行是不可能的。 24 | 25 | 一个中断处理程序既可以抢占其他的中断处理程序,页可以抢占异常处理程序,相反,异常处理程序从不抢占中断处理程序,在内核态能触发的唯一异常就是缺页异常,但中断处理程序从不知心可以导致缺页的操作。 26 | 27 | 基于下面两个原因,Linux交错执行内核控制路径: 28 | 29 | 1. 为了提高可编程中断控制器和设备控制器的吞吐量。假定设备控制器在一条IRQ线上产生了一个信号,PIC把这个信号转换成一个外部中断,然后PIC和设备控制器保持阻塞,一直到PIC从CPU处接受到一个条应答信息。由内核控制路径的交错执行,内核即使在处理前一个中断,也能发送应答。 30 | 2. 为了实现一个没有优先级的中断模型。因为每个中断处理程序都可以被另一个中断处理程序延缓,因此,硬件设备之间没必要建立预定义优先级,这就简化了代码,提高了内核的可移植性。 31 | 32 | 在多处理器系统上,几个内核控制路径可以并发执行,此外,与异常相关的内核控制路径可以开始在一个CPU上执行,并且由于进程切换而迁移到另一个CPU上执行。 33 | -------------------------------------------------------------------------------- /2014-05-05-read-copy-update.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 读-拷贝-更新 4 | category: 内核同步 5 | description: 读-拷贝-更新 RCU... 6 | tags: RCU 读-拷贝-更新 7 | --- 8 | 读-拷贝-更新(*RCU*)是为了保护在多数情况下被多个CPU读的数据结构而设计的一种同步技术,RCU允许多个读者和写者并发执行,相对于值允许一个写者执行的顺序锁,RCU有了本质上的改进。 9 | 10 | 简单的说RCU的逻辑是,当读者指向共享数据结构时,一旦有写者需要对共享数据结构进行写操作,那么写者就创建一个共享数据结构的副本,当完成写操作之后,将所有的读者指向新的数据结构,然后释放旧的数据结构即可。 11 | 12 | RCU是不使用锁的,也就是说,它不使用被所用CPU共享的锁或计数器,在这一点上和读/写自旋锁和顺序锁相比,RCU具有更大的优势。RCU可以不使用共享数据机构而实现多个CPU同步,因为: 13 | 14 | 1. RCU只保护被动态分配并通过指针引用的数据结构。 15 | 2. 在被RCU保护的临界区中,任何内核控制路径都不能睡眠。 16 | 17 | 当内核控制路径要读取被RCU保护的数据结构是,执行宏*rcu_read_lock()*,该宏等同于*preempt_disable()*函数。同样,也可以执行*rcu_read_unlock()*,这个宏等同于*preempt_enable()*,代码如下: 18 | 19 | #### #### 20 | 21 | {% highlight c++ %} 22 | static inline void __rcu_read_lock(void) 23 | { 24 | preempt_disable(); 25 | } 26 | 27 | static inline void __rcu_read_unlock(void) 28 | { 29 | preempt_enable(); 30 | } 31 | {% endhighlight %} 32 | 33 | 接下来,读者间接引用该数据结构指针锁对应的内存单元并开始读这个数据结构,读者在完成对数据结构的读操作之前,是不能睡眠的,用*rec_read_unlock()*宏标记临界区的结束。 34 | 35 | 由于读者几乎不会做任何事情,所以也不会有任何竞争条件出现,所以写者不得不做得更多一些。实际上,当写者要更新数据结构的时候,它间接引用指针并生成整个数据结构的副本,然后,写者修改这个副本,一旦修改完毕,写者改变指向数据结构的指针,以便使它指向被修改后的副本。 36 | 37 | 由于修改指针值的操作是一个原子操作,所以旧副本和新副本对每个读者或写者都是可见的,在数据结构中并不会出现数据崩溃。尽管如此,还需要内存屏障来保证只有在数据结构被修改后,已更新的指针对其他CPU才是可见的,如果把自旋锁与RCU结合起来以禁止写者的并发执行,就隐含地引入了这样的内存屏障。 38 | 39 | 然而,使用RCU技术的真正困难在于,写者修改指针时不能立即释放数据结构的旧副本。实际上,写者开始修改时,正在访问数据结构的读者可能还在读旧副本,只有在CPU上的所有的读者都执行完*rcu_read_unlock()*之后,才可以释放旧的副本,内核要求每个潜在的读者在下面的操作之前执行*rcu_read_unlock()*宏。 40 | 41 | 1. CPU执行进程切换。 42 | 2. CPU开始在用户态执行。 43 | 3. CPU执行空循环。 44 | 45 | 对于上述的每种情况,我们都说CPU已经经过了静止状态(*quiescent state*)。 46 | 47 | 写者调用函数*call_rcu()*来释放数据结构的旧副本,该函数定义如下。 48 | 49 | #### #### 50 | 51 | {% highlight c++ %} 52 | extern void call_rcu( 53 | struct rcu_head *head, 54 | void (*func)(struct rcu_head *head)); 55 | {% endhighlight %} 56 | 57 | 当所有的CPU都通过静止状态之后,*call_rcu()*接收*rcu_head*描述符[^1]的地址和将要调用的回调函数的地址作为参数,一旦回调函数被执行,它同城释放数据结构的旧副本。 58 | 59 | [^1]: 这个描述符通常嵌在要被释放的数据结构中。 60 | 61 | 函数*call_rcu()*把回调函数和其他参数的地址存放在*rcu_head*描述符中,代码如下: 62 | 63 | #### #### 64 | 65 | {% highlight c++ %} 66 | struct rcu_head { 67 | struct rcu_head *next; 68 | void (*func)(struct rcu_head *head); 69 | }; 70 | {% endhighlight %} 71 | 72 | 然后把描述符插入到回调函数的per-CPU链表中,内核每经过一个时钟抵达旧周期性地检查本地CPU是否经过了一个静止状态,如果所有的CPU都经过了静止状态,本地tasklet旧执行链表中的所有回调函数。 73 | 74 | RCU最常用的场景是Linux中的网络层和虚拟文件系统。 75 | -------------------------------------------------------------------------------- /2014-05-05-working-on-exception.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 异常处理 4 | category: 中断和异常 5 | description: 异常处理... 6 | tags: 异常 7 | --- 8 | CPU产生的大部分异常都由Linux解释为出错条件,当其中一个异常发生时,内核就向引起异常的进程发送一个信号通知进程系统出现了一个反常条件。例如,如果进程执行了一个被0除的操作,CPU就产生一个『Divide error』异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号,这个进程必须采取一些特定的步骤恢复或者中止运行。 9 | 10 | 异常处理程序有一个标准结构,由以下三部组成: 11 | 12 | 1. 用汇编语言在内核堆栈中保存大多数寄存器内容。 13 | 2. 用C语言便携的函数处理异常。 14 | 3. 通过ret\_from\_exception()函数从异常处理程序退出。 15 | 16 | 为了利用异常,必须对IDT进行适当的初始化,使得每个被确认的异常都有一个异常处理程序。*trap_init()*函数的工作是将一些异常处理的函数插入到IDT的非屏蔽中断及异常表项中。这些函数包括: 17 | 18 | #### #### 19 | 20 | {% highlight c++ %} 21 | static 22 | inline void set_system_intr_gate( 23 | unsigned int n, void *addr) 24 | { 25 | BUG_ON((unsigned)n > 0xFF); 26 | _set_gate(n, GATE_INTERRUPT, addr, 0x3, 0, __KERNEL_CS); 27 | } 28 | 29 | static 30 | inline void set_system_trap_gate( 31 | unsigned int n, void *addr) 32 | { 33 | BUG_ON((unsigned)n > 0xFF); 34 | _set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS); 35 | } 36 | 37 | static 38 | inline void set_trap_gate( 39 | unsigned int n, void *addr) 40 | { 41 | BUG_ON((unsigned)n > 0xFF); 42 | _set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS); 43 | } 44 | 45 | static 46 | inline void set_task_gate( 47 | unsigned int n, unsigned int gdt_entry) 48 | { 49 | BUG_ON((unsigned)n > 0xFF); 50 | _set_gate(n, GATE_TASK, (void *)0, 0, 0, (gdt_entry<<3)); 51 | } 52 | 53 | static 54 | inline void set_intr_gate_ist( 55 | int n, void *addr, unsigned ist) 56 | { 57 | BUG_ON((unsigned)n > 0xFF); 58 | _set_gate(n, GATE_INTERRUPT, addr, 0, ist, __KERNEL_CS); 59 | } 60 | 61 | static 62 | inline void set_system_intr_gate_ist( 63 | int n, void *addr, unsigned ist 64 | ) 65 | { 66 | BUG_ON((unsigned)n > 0xFF); 67 | _set_gate(n, GATE_INTERRUPT, addr, 0x3, ist, __KERNEL_CS); 68 | } 69 | {% endhighlight %} 70 | 71 | 上面的函数我们先要了解中断门、陷阱门以及系统门。 72 | 73 | **中断门(*interrupt gate*)** 74 | 75 | 用户态的进程不能访问一个Intel中断门,门的DPL字段为0,所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。 76 | 77 | **系统门(*system gate*)** 78 | 79 | 用户态的进程可以访问的一个Intel陷阱门,门的DPL字段为3。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5以及128。 80 | 81 | **系统中断门(*system interrupt gate*)** 82 | 83 | 能够被用户态进程访问的Intel中断门,门的DPL字段为3。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。 84 | 85 | **陷阱门(*tarp gate*)** 86 | 87 | 用户态的进程不能访问一个Intel陷阱门,门的字段为0,大部分Linux异常处理程序都通过陷阱门来激活。 88 | 89 | **任务门(*task gate*)** 90 | 91 | 不能被用户态进程访问的Intel任务门,门的DPL字段为0。Linux对『Double fault』异常的处理程序是由任务门激活的。由于『Double fault』异常表示内核有严重的非法操作,其处理程序是通过任务门而不是陷阱门或系统门完成的。 92 | 93 | 产生这种异常的时候,CPU取出存放在IDT的第8项中的任务门描述符,该描述符指向存放在GDT表第32项中的TSS段描述符,然后CPU利用TSS段中的相关值装载*eip*和*esp*,结果是,处理器在自己的私有栈上执行*doubleefault_fn()*异常处理函数。 94 | 95 | 如果我们继续看\_\_set\_gate函数: 96 | 97 | #### #### 98 | 99 | {% highlight c++ %} 100 | static inline void _set_gate( 101 | int gate, unsigned type, void *addr, 102 | unsigned dpl, unsigned ist, unsigned seg) 103 | { 104 | gate_desc s; 105 | pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg); 106 | /* 107 | * 不需要原子化操作因为在初始化时只执行一遍 108 | */ 109 | write_idt_entry(idt_table, gate, &s); 110 | } 111 | {% endhighlight %} 112 | 113 | 可以看到最终会将初始化需要的IDT信息写入到IDT中。 114 | 115 | ---- 116 | 117 | 执行异常处理程序的C函数总是由do_前缀和处理程序名组成,其中大部分函数把硬件出错码和异常向量保存在当前的进程描述符中,然后项当前进程发送一个适当的信号。 118 | 119 | 异常处理程序中止后,当前进程就立即关注这个信号,这个信号要么由用户态进程自己处理,那么就由内核来处理,如果由内核处理,那么一般内核都会杀死当前进程。 120 | -------------------------------------------------------------------------------- /2014-05-06-kernel-semaphore.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内核信号量 4 | category: 内核同步 5 | description: 内核信号量... 6 | tags: 内核信号量 信号量 7 | --- 8 | 内核信号量类似于自旋锁,因为当锁关闭着,它不允许内核控制路径继续执行,然而,当内核控制路径试图获取内核信号量所保护的忙资源时,相应的进程被挂起,只有在资源被释放时,进程才再次变为可运行状态。因此,只有可以睡眠的函数才能获取内核信号量,中断处理程序和可延迟函数都不能使用内核信号量。 9 | 10 | 内核信号量是*semaphore*类型的对象,代码如下: 11 | 12 | #### #### 13 | 14 | {% highlight c++ %} 15 | struct semaphore { 16 | spinlock_t lock; 17 | unsigned int count; 18 | struct list_head wait_list; 19 | }; 20 | {% endhighlight %} 21 | 22 | 其中字段及其意义如下: 23 | 24 | 字段 | 说明 25 | ------------ | ------------- 26 | count | 计数器,如果该值大于0,那么资源就是空闲的,也就是说该资源可以被使用,相反,如果count等于0,那么信号量是忙的。如果count的值等于负数,则资源是不可用的 27 | lock | 信号量的锁 28 | wait_list | 存放等待队列链表的地址,当前等待资源的所有睡眠进程都放在这个列表中,如果count大于0,那么等待队列就为空 29 | 30 | #### #### 31 | 32 | {% highlight c++ %} 33 | #define init_MUTEX(sem) sema_init(sem, 1) 34 | #define init_MUTEX_LOCKED(sem) sema_init(sem, 0) 35 | {% endhighlight %} 36 | 37 | 可以使用*init_MUTEX()*和*init_MUTEX_LOCKED()*宏来初始化互斥访问所需的信号量,这两个宏分别把*count*的值设置为1和0.其中1表示互斥访问的资源空闲,0表示对信号量进行初始化的进程当前互斥访问的资源忙。宏*DECLARE_MUTEX*用于静态的分配*semaphore*结构的变量。 38 | 39 | #### #### 40 | 41 | {% highlight c++ %} 42 | #define DECLARE_MUTEX(name) 43 | struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1) 44 | {% endhighlight %} -------------------------------------------------------------------------------- /2014-05-06-working-on-interrupt.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 中断处理 4 | category: 中断和异常 5 | description: 中断处理... 6 | tags: 中断处理 7 | --- 8 | 内核只要给引起异常的进程发送一个信号就可以处理异常,如果程序自己不处理异常,那么就让内核处理,内核通常会杀死进程。但是这种方法并不适合中断,因为经常会出现一个进程被挂起好久后中断才达到,或者切换到另外一个进程,所以当中断到来的时候,一个可能完全无关的进程正在运行,所以给当前进程发送一个信号是没有用的。 9 | 10 | 例如当程序员在一个编辑器开发程序,需要输入,这个时候进程切换到一个浏览器的Tab,这个时候程序员通过键盘输入产生了一个中断,这个中断响应在浏览器上是毫无作用的,所以需要其他方法来处理中断。中断处理依赖于中断类型,主要有三种中断: 11 | 12 | **I/O中断** 13 | 14 | 某些I/O设备需要关注,相应的中断处理程序必须查询设备以确定适当的操作过程。 15 | 16 | **时钟中断** 17 | 18 | 某种时钟[^1]产生一个中断,这种中断高速内核一个固定的时间间隔已经过去,这些中断大部分是作为I/O中断来处理的。 19 | 20 | **多处理器中断** 21 | 22 | 多处理系统中一个CPU对另一个CPU发出一个中断。 23 | 24 | [^1]: 例如一个本地的APIC时钟或者一个外部时钟,会在时钟笔记里记录。 25 | 26 | 其中时钟中断以后再说,这里主要记I/O中断和多处理中断。 -------------------------------------------------------------------------------- /2014-05-07-io-interrupt-dynamic.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 动态分配IRQ线 4 | category: 中断和异常 5 | description: 动态分配IRQ线... 6 | tags: IRQ 动态分配 7 | --- 8 | IRQ中除了几个向量留给特定的设备,其余的向量都被动态地分配,因此有一种方式下同一条IRQ线可以让几个硬件设备使用,即使这些设备不允许IRQ共享,技巧就是使这些硬件设备的活动串行化,以便一次只能有一个设备拥有这个IRQ线。 9 | 10 | 在激活一个准备利用IRQ线的设备之前,其相应的驱动程序调用*request_irq()*,这个函数建立一个新的*irqaction*描述符,并用参数值初始化它。然后调用*setup_irq()*函数把这个描述符插入到合适的IRQ链表。 11 | 12 | 如果*setuo_irq*返回一个出错码,设备驱动程序中止操作,这意味着IRQ线已由另一个设备所使用,而这个设备不允许中断共享。当设备操作结束后,使用*free_irq()*函数从IRQ连表中删除这个描述符,并释放相应的内存区。 13 | 14 | 假定一个程序想要访问*/dev/fd0*设备文件,这个设备文件对应于系统中的第一个软盘,程序要做到这点,可以通过直接访问*/dev/fd0*,也可以通过在系统上安装一个文件系统,通常IRQ6分配给软盘控制器,给定这个号,软盘驱动程序发出下列请求: 15 | 16 | {% highlight c++ %} 17 | request_irq(6, floppy_interrupt, 18 | SA_INTERRUPT|SA_SAMPLE_RDANDOM, 19 | "floppy", NULL) 20 | {% endhighlight %} 21 | 22 | 可以看到,*floppy_interrupt()*中断服务例程必须以关中断的方式执行,并且不共享这个IRQ,设置SA\_SAMPLE\_RANDOM标志意味着对软盘的访问是内核用于产生随机数的一个较好的随机事件源。当软盘的操作被终止时,要么中止*/dev/fd0*的I/O操作,要么卸载这个文件系统。然后驱动程序就释放IRQ线。 23 | 24 | {% highlight c++ %} 25 | free_irq(6, NULL) 26 | {% endhighlight %} 27 | 28 | 为了把一个*irqaction*描述符插入到适当的连表中,内核调用*setup_irq()*函数,传递给这个函数的参数为*irq_nr*和*new*,*irq_nr*就是IRQ号,*new*为刚刚分配的*irqaction*描述符。*setup_irq()*是体系结构相关的,所以了解一下这个函数做了什么。 29 | 30 | 1. 检查另一个设备是否已经使用*irq_nr*这个IRQ,如果是,检查两个设备的irqaction描述符中的SA_SHIREQ标志是否都指定了IRQ线能被共享,否则就返回一个错误码。 31 | 2. 把*new加到链表中。 32 | 3. 如果没有其他设备共享同一个IRQ,清理相关的设置并调用irq_desc->handler PIC对象的*startup*方法以确保IRQ信号被激活。 33 | -------------------------------------------------------------------------------- /2014-05-07-multi-cpu-interrupt.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 处理器间中断处理 4 | category: 中断和异常 5 | description: 处理器间中断处理... 6 | tags: 中断处理 7 | --- 8 | 处理器间中断允许一个CPU向系统其他的CPU发送中断信号,处理器间中断(IPI)不是通过IRQ线传输的,而是作为信号直接放在连接所有CPU本地APIC的总线上。在多处理器系统上,Linux定义了下列三种处理器间中断: 9 | 10 | **CALL\_FUNCTION\_VECTOR** (*向量0xfb*) 11 | 12 | 发往所有的CPU,但不包括发送者,强制这些CPU运行发送者传递过来的函数,相应的中断处理程序叫做*call_function_interrupt()*,例如,地址存放在群居变量*call_data*中来传递的函数,可能强制其他所有的CPU都停止,也可能强制它们设置内存类型范围寄存器的内容。通常,这种中断发往所有的CPU,但通过*smp_call_function()*执行调用函数的CPU除外。 13 | 14 | **RESCHEDULE\_VECTOR** (*向量0xfc*) 15 | 16 | 当一个CPU接收这种类型的中断时,相应的处理程序限定自己来应答中断,当从中断返回时,所有的重新调度都自动运行。 17 | 18 | **INVALIDATE\_TLB\_VECTOR** (*向量0xfd*) 19 | 20 | 发往所有的CPU,但不包括发送者,强制它们的转换后援缓冲器TLB变为无效。相应的处理程序刷新处理器的某些TLB表项。 21 | 22 | ---- 23 | 24 | 处理器间中断处理程序的汇编语言代码是由*BUILD_INTERRUPT*宏产生的,它保存寄存器,从栈顶押入向量号减256的值,然后调用高级C函数,其名字就是第几处理程序的名字加前缀*smp_*,例如*CALL_FUNCTION_VECTOR*类型的处理器间中断的低级处理程序时*call_function_interrupt()*,它调用名为*smp_call_function_interrupt()*的高级处理程序,每个高级处理程序应答本地APIC上的处理器间中断,然后执行由中断触发的特定操作。 25 | 26 | Linux有一组函数使得发生处理器间中断变为一件容易的事: 27 | 28 | 函数 | 说明 29 | ------------ | ------------- 30 | send\_IPI\_all() | 发送一个IPI到所有CPU,包括发送者 31 | send\_IPI\_allbutself() | 发送一个IPI到所有CPU,不包括发送者 32 | send\_IPI\_self() | 发送一个IPI到发送者的CPU 33 | send\_IPI\_mask() | 发送一个IPI到位掩码指定的一组CPU 34 | -------------------------------------------------------------------------------- /2014-05-07-slab.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: slab分配器 4 | category: 内存管理 5 | description: slab分配器... 6 | tags: slab 分配器 备选分配器 7 | --- 8 | 每当我们要分配内存的时候,我们会使用malloc,malloc是C语言中分配内存的函数,我们可以使用malloc及其在C标准库中的相关函数,大多数程序分配若干字节内存时,经常会调用这些函数。内核也必须经常分配内存,但无法借助于标准库的函数,之前描述的伙伴系统支持按页分配内存,但这个单位对于内核而言实在太大了。 9 | 10 | 如果需要为一个10个字符的字符串分配空间,分配一个4KB或更多空间的完整页面,不仅仅浪费而且完全不可接收,所以内核需要将页拆分为更小的单位,以便可以容纳大量的小对象。 11 | 12 | 为此,内核必须引入新的管理机制,这会给内核带来更大的开销,为了最小化这个额外的负担对系统性能的影响,该管理层的实现应该尽可能的紧凑,以便不要对处理器的高速缓存TLB带来特别的影响。同事,内核还必须保证内存利用的速度和效率。 13 | 14 | 不仅Linux,而且类似的UNIX和所有其他的操作系统,都需要面对这个问题,经过一定的时间,已经提出了一些或好或坏的解决方案,在一般的操作系统的文献中都有讲解。其中slab分配的解决办法对许多特定的要求工作情况而言,都是非常高效的。 15 | 16 | 提供小内存块不是slab分配器的唯一任务,由于结构上的特点,它也用作一个缓存,主要针对经常分配并释放的对象。通过建立slab缓存,内核能够存储一些对象,供后续使用。即便在初始化状态也是如此。 17 | 18 | 例如,为管理与进程关联的文件系统数据,内核必须经常生成*struct fs_struct*的新实例,此类型实例占据的内存块同样需要经常回收[^1]。换句话说,内核趋向于非常有规律地分配并释放大小为*sizeof(fs_struct)*的内存块。 19 | 20 | [^1]: 在进程结束的时候。 21 | 22 | slab分配器将释放的内存块保存在一个内部列表中,并不能马上返回给伙伴系统。在请求为该类对象分配一个新实例时,会使用最近释放的内存块。这有两个有点,首先,由于内核不必使用伙伴系统算法,处理时间会变短,其次,由于该内存块仍然时新的,因此其仍然驻留在CPU高速缓存的概率较高。 23 | 24 | slab分配器还有两个更进一步的好处: 25 | 26 | 1. 调用伙伴系统的操作对系统的数据和指令高速缓存有相当的影响。内核越浪费这些资源,这些资源对用户空间进程就越不可用。更轻量级的slab分配器在可能的情况下减少了对伙伴系统的调用,有助于防止不受欢迎的缓存污染。 27 | 2. 如果数据存储在伙伴系统直接提供的页中,那么其地址总是出现在2的幂次数的整数倍附近。这对CPU高速缓存的利用有负面影响,由于这种地址分布,使得某些缓存行过度使用,而其他的则几乎为空。多处理器会更加剧这种不利情况,因为不同的内存地址可能在不同的总线上传输。 28 | 29 | 通过slab着色(*slab coloring*),slab分配器能够均匀地分配对象,以实现均匀的缓存利用。着色这个术语时隐喻性的,它与颜色无关,只是表示slab中的对象需要移动的特定偏移量,以便使对象放置到不同的缓存行。 30 | 31 | ### 备选分配器 ### 32 | 33 | 尽管slab分配器对许多可能的工作负荷都工作良好,但也有一些情形,它无法提供最优性能。如果某些计算机处于当前硬件尺度的边界上,这类计算机上使用slab分配会出现一些问题,例如微小的嵌入式系统,备有大量物理内存的大规模并行系统。在大规模系统中,slab分配器所需大量的元数据可能会成为更严重的问题[^2]。 34 | 35 | [^2]: 在大型系统上仅slab的数据结构就需要很多G字节内存,对嵌入式系统来说,slab分配器代码量和复杂性都太高。 36 | 37 | 为了处理此类情况,内核版本在2.6开发期间,增加了slab分配器的两个替代品,分别为slob分配器和slub分配器。 38 | 39 | **slob分配器** 40 | 41 | slob分配器进行了特别的优化,以减少代码量,它围绕一个简单的内存块链表展开。在分配内存时,使用了同样简单的最先适配算法。slob分配器只有大约600行代码,总的代码量很小,但从速度上来说,它并不是最高效的分配器,也无法为大型系统所使用。 42 | 43 | **slub分配器** 44 | 45 | slub分配器通过将页帧打包为组,并通过*struct page*中未使用的字段来管理这些数组,试图最小化所需内存的开销,这样做不会简化该结构的定义,但是在大型计算机上,slub分配器比slab分配器提供了更好的性能。 46 | 47 | ---- 48 | 49 | 由于slab分配是大多数内核的默认选项,所以不会详细记录slob或slub分配器。但无论如何,内核都无需关心底层使用什么分配器,任何一种分配器对内核的接口都是相同的。 50 | 51 | ![slab](images/slab.png) 52 | 伙伴系统、通用分配器和内核代码之间的关系 53 | 54 | 普通内核代码只需要包含slab.h就可以使用内存分配的所有标准内核函数,连编系统会保证使用编译时选择的分配器来满足程序的内存分配请求。 -------------------------------------------------------------------------------- /2014-05-07-soft-irq-and-tasklet.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 软中断和tasklet 4 | category: 中断和异常 5 | description: 软中断和tasklet... 6 | tags: 软中断 tasklet 7 | --- 8 | 在由内核执行的几个任务之间有些不是紧急的,在必要的情况下它们可以被推迟一段时间。一个中断处理程序的几个中断服务例程之间时串行执行的,并且通常在一个中断的处理程序结束前,不应该再次出现这个中断。 9 | 10 | 相反,可延迟中断可以在开中断的情况下执行,把可延迟中断从中断处理程序中抽出来有助于内核保持较短的响应时间。这对于哪些期望它们的中断能在几号秒内大刀处理的『急迫』应用来说是非常重要的。Linux 2.6通过可延迟函数和工作队列来执行的函数来实现以上问题。 11 | 12 | 软中断和tasklet有密切的关系,tasklet是在软中断之上的实现,事实上,出现在内核代码中的术语软中断(*softirq*)通常表示可延迟函数的所有种类,另外一种被广泛使用的术语是『中断上下文』,中断上下文表示当前正在执行一个中断处理程序或一个可延迟的函数。 13 | 14 | 软中断的分配是静态的,即在编译时定义的,而tasklet的分配和初始化可以在运行时进行。软中断可以并发地运行在多个CPU上,因此软中断是可重入函数并且必须明确地使用自旋锁保护其数据结构。但tasklet不必担心这些问题,因为内核对tasklet的执行进行了更加严格的控制。 15 | 16 | 相同类型的tasklet总是被串行地执行,所以说不能在两个CPU上同事运行相同类型的tasklet,但是类型不同的tasklet可以在几个CPU上并发执行。tasklet的串行化使tasklet函数不必是可重入的,因此简化了驱动设备程序开发者的工作。 17 | 18 | 一般而言,在可延迟函数上可以执行四种操作: 19 | 20 | **初始化**(*initialization*) 21 | 22 | 定义一个新的可延迟函数,这个操作通常在内核自身初始化或加载模块时进行。 23 | 24 | **激活**(*activation*) 25 | 26 | 标记一个可延迟函数为『挂起』,激活可以在任何时候进行,即便正在处理中断。 27 | 28 | **屏蔽**(*masking*) 29 | 30 | 有选择地屏蔽一个可延迟函数,这样,即使它被激活,内核也不执行它。 31 | 32 | **执行**(*execution*) 33 | 34 | 执行一个挂起的可延迟函数和同类型的其他所有挂起的可延迟函数,执行是在特定的时间进行的。 35 | 36 | 激活和执行总是捆绑在一起的。由给定CPU激活的一个可延迟函数必须在同一个CPU上执行,不过并没有什么明显的理由说明这条规则是对系统有益的。把可延迟函数绑定在激活CPU上从理论上说可以更好地利用CPU地硬件高速缓存,毕竟,激活地内核线程访问一些数据结构,可延迟函数也可能会使用。 37 | 38 | 然而,当可延迟函数运行时,因为它地执行可能延迟一段时间,因此相关地高速缓存行可能就根本不再高速缓存中了,因此,帮一个函数绑定在一个CPU上总是有潜在『危险』的操作,因为一个CPU可能忙死而其他CPU又无所事事。 -------------------------------------------------------------------------------- /2014-05-08-kernel-mm-management.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 内核中的内存管理 4 | category: 内存管理 5 | description: 内核中的内存管理... 6 | tags: 内核 内存 7 | --- 8 | 内核中的一般的内存分配和释放函数与C标准库中等价函数的名称类似,用法也几乎相同,如: 9 | 10 | **kmalloc(size, flags)** 11 | 12 | 分配长度为*size*字节的一个内存区,并返回指向改内存区起始处的一个*void*指针,如果没有足够的内存,则为NULL指针。其中*flags*参数使用*GFP_*常数,来指定分配内存的具体内存域。 13 | 14 | **kfree(*ptr)** 15 | 16 | 用于释放*ptr*指向的内存区。 17 | 18 | 与用户空间程序设计相比,内核还包括*percpu_alloc*和*percpu_free*函数,用于为各个系统CPU分配和释放所需内存区。*kmalloc*在内核源代码中的使用数以千记,但模式都是相同的。用*kmalloc*分配内存区,首先通过类型转换变为正确的类型,然后赋值到指针变量。 19 | 20 | {% highlight c++ %} 21 | info = (struct cdrom_info *)kmalloc( 22 | sizeof(struct cdrom_info), 23 | GFP_KERNEL) 24 | {% endhighlight %} 25 | 26 | 从程序员的角度来看,建立和使用缓存的任务不是特别困难,必须首先用*kmem_cache_create*建立一个适当的缓存,接下来即可使用*kmem_cache_alloc*和*kmem_cache_free*分配和释放其中包含的对象。slab分配器负责完成与伙伴系统交互来分配所需的页。 27 | 28 | 所有活动缓存的列表保留在*/proc/slabinfo*中。 29 | 30 | ![slabinfo](images/slabinfo.png) 31 | slabinfo示例 32 | 33 | 输出的各列除了包含用于标识各个缓存的字符串名称之外,还包含下列信息: 34 | 35 | 1. 缓存中活动对象的数量。 36 | 2. 缓存中对象的总数,包括已用和未知。 37 | 3. 所管理对象的长度,按字节计算。 38 | 4. 一个slab中对象的数量。 39 | 5. 每个slab中页的数量。 40 | 6. 活动slab的数量。 41 | 7. 在内核决定向缓存分配更多内存时,所分配的对象的数量[^1]。 42 | 43 | [^1]: 每次会分配一个较大的内存块,以减少与伙伴系统的交互,在缩小缓存时,也使用该值作为释放内存块的大小。 44 | 45 | 除了容易识别的缓存名称如用于UNIX域套接字的*unix_sock*,还有其他字段名称如*kmalloc-size*。这些字段提供DMA内存域的计算机还包括用于DMA分配的缓存。这些是*kmalloc*函数的基础,是内核为不同长度提供的slab缓存,除极少例外,其长度都是2的幂次方,长度范围从32B或64B到2^25B。上界也可以更小,由*KMALLOC_MAX_SIZE*设置。 46 | 47 | 每次调用*kmalloc*时,内核找到最合适的缓存,从中分配一个对象满足请求,如果没有刚刚好和式的缓存,则分配稍大一点的对象,但不会分配小的对象。在实际实现中,slab分配器和缓存之间的差异几乎没有,所以有些时候我们可以将这两个名词看作同义词。 -------------------------------------------------------------------------------- /2014-05-08-soft-irq.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 软中断 4 | category: 中断和异常 5 | description: 软中断... 6 | tags: 软中断 7 | --- 8 | 目前在我看的Linux代码中定义了10种软中断,分别如下,每个软中断的索引下标代表了优先级,下标越小优先级越大: 9 | 10 | {:.table_center} 11 | 字段 | 说明 12 | ------------ | ------------- 13 | HI_SOFTIRQ | 处理高优先级tasklet 14 | TIMER_SOFTIRQ | 和时钟中断相关的tasklet 15 | NET_TX_SOFTIRQ | 把数据包传送到网卡 16 | NET_RX_SOFTIRQ | 从网卡接受数据包 17 | BLOCK_SOFTIRQ | 块设备软中断 18 | BLOCK_IOPOLL_SOFTIRQ | 支持IO轮询的块设备软中断 19 | TASKLET_SOFTIRQ | 处理常规的IRQ 20 | SCHED_SOFTIRQ | 调度程序软中断 21 | HRTIMER_SOFTIRQ | 高精度计时器软中断 22 | RCU_SOFTIRQ | RCU锁软中断 23 | NR_SOFTIRQS | 当前Linux内核允许注册的最大软中断数 24 | 25 | 代码可以看到: 26 | 27 | #### #### 28 | 29 | {% highlight c++ %} 30 | enum 31 | { 32 | HI_SOFTIRQ=0, 33 | TIMER_SOFTIRQ, 34 | NET_TX_SOFTIRQ, 35 | NET_RX_SOFTIRQ, 36 | BLOCK_SOFTIRQ, 37 | BLOCK_IOPOLL_SOFTIRQ, 38 | TASKLET_SOFTIRQ, 39 | SCHED_SOFTIRQ, 40 | HRTIMER_SOFTIRQ, 41 | RCU_SOFTIRQ, /* 最后一个软中断 */ 42 | /* 当前Linux内核允许注册的最大软中断数 */ 43 | NR_SOFTIRQS 44 | }; 45 | {% endhighlight %} 46 | 47 | 其中*HI_SOFTIRQ*和*TIMER_SOFTIRQ*用来实现tasklet。 48 | 49 | 表示软中断的主要数据结构是*softirq_vec*数组,这个数组包含类型为*softirq_action*的32个元素,*softirq_action*代码如下: 50 | 51 | #### #### 52 | 53 | {% highlight c++ %} 54 | struct softirq_action 55 | { 56 | void (*action)(struct softirq_action *); 57 | }; 58 | {% endhighlight %} 59 | 60 | 一个软中断的优先级是相应的*softirq_action*元素在数组内的下标,*softirq_action*数据结构含有一个字段,指向中断函数的一个action指针。 61 | 62 | 另外一个关键字段是*preempt_count*字段,用它来跟踪内核抢占和内核控制路径的嵌套,该字段存放在每个进程描述符的*thread_info*字段中。*preempt_count*字段的编码表示三个不同的计数器和一个标志: 63 | 64 | 字段 | 说明 65 | ------------ | ------------- 66 | 0~7 | 抢占计数器 67 | 8~15 | 软中断计数器 68 | 16~27 | 硬中断计数器 69 | 28 | PREEMPT_ACTIVE标志 70 | 71 | 第一个计数器记录显式禁用本地CPU内核抢占的次数,值等于0表示允许内核抢占。第二个计数器表示可延迟函数被禁用的成都,值为0表示可延迟函数处于激活状态。第三个计数器表示本地CPU上中断处理的嵌套数。 72 | 73 | *preempt_count*字段表示:当内核代码明确不允许发生抢占,或当内核正在中断上下文中运行时,必须禁用内核的抢占功能。因此,为了确定是否能抢占当前进程,内核快速检查*preempt_count*字段中的相应的值是否等于0。 74 | 75 | 宏*in_interrupt*检查*preempt_count*字段产生的硬件中断计数器和软中断计数器,只要这两个计数器中的一个值为正数,该宏就产生一个非零的值。 76 | 77 | #### #### 78 | 79 | {% highlight c++ %} 80 | #define irq_count() (preempt_count() 81 | & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)) 82 | 83 | #define in_interrupt() (irq_count()) 84 | {% endhighlight %} 85 | 86 | 如果内核不使用多内核栈,则该宏只检查当前进程的*thread_info*描述符的*preempt_count*字段,但是如果内核使用多内核栈,则该宏可能还要检查本地CPU的*irq_ctx*结构中的*thread_info*描述符的*preempt_count*字段,在这种情况下由于该字段总是正数值,所以返回非零值。 87 | 88 | 实现软中断的最后一个关键数据结构是每个CPU都有的32位掩码,它存放在*irq_cpustat_t*数据结构的*__softirq_pending*字段中,在系统中,每个CPU都有一个这样的数据结构。 89 | 90 | 软中断必须首先注册,然后内核才能执行软中断。*open_softirq*函数用来注册一个软中断,它在*softirq_vec*表中指定的位置写入新的软中断: 91 | 92 | #### #### 93 | 94 | {% highlight c++ %} 95 | void open_softirq(int nr, void (*action)(struct softirq_action *)) 96 | { 97 | softirq_vec[nr].action = action; 98 | } 99 | {% endhighlight %} 100 | 101 | 每个软中断都有一个唯一的编号,这说明软中断是相对稀缺的资源,使用其必须要小心,不能由各种设备和内核组件随意的使用,默认情况下,系统上只能使用32个软中断。但这个限制不会有太大的局限性,因为软中断充当实现其他延迟执行机制的函数,而且也很适合设备驱动程序。 102 | 103 | 只有中枢代码可以使用软中断,软中断只用于几个少数场合,这些都是相对重要的场合。 104 | 105 | *raise_softirq*用于引发一个软中断,编号通过参数指定,这个函数将相应的软中断执行,但这个执行是延迟执行,并不是立即执行的。通过特定于处理器的位图,内核确保多个软中断能够公平的在不同的CPU上执行。 106 | 107 | #### #### 108 | 109 | {% highlight c++ %} 110 | void raise_softirq(unsigned int nr) 111 | { 112 | unsigned long flags; 113 | 114 | local_irq_save(flags); 115 | raise_softirq_irqoff(nr); 116 | local_irq_restore(flags); 117 | } 118 | {% endhighlight %} 119 | 120 | 如果不在中断上下文中调用*raise_softirq*方法,则调用*wakeup_softirq*来唤醒软中断守护进程,这个守护进程会执行软中断。 121 | -------------------------------------------------------------------------------- /2014-05-09-how-slab-work.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: slab分配的原理 4 | category: 内存管理 5 | description: slab分配的原理... 6 | tags: slab 分配器 7 | --- 8 | slab分配器由一个紧密地交织的数据和内存结构的网络组成,出看起来不容易理解,所以,重要的是获得各个结构之间关系的一个理解,理解之后我们才能够深入的进行slab分配器代码的查看。 9 | 10 | 基本上slab缓存由下图所示的两部分组成:保存管理性数据的缓存对象和保存被管理对象的各个slab。 11 | 12 | ![slab](images/slab2.png) 13 | slab分配器的各个部分 14 | 15 | 每个缓存只负责一种对象类型[^1],或提供一般性的缓冲区。各个缓冲中slab的数目各有不同,这与已经使用的页的数目、对象长度和被管理对象的数目有关。另外,系统中所有的缓存都保存在一个双链表中。这使得内核有机会依次遍历所有的缓存,这是有必要的,例如在发生内存不足的情况下,内核可能需要缩减分配给缓存的内存数量。 16 | 17 | [^1]: 例如struct unix_sock实例。 18 | 19 | 如果更加仔细的研究缓存的结构,就会注意到一些细节,如下图: 20 | 21 | ![slab](images/slab3.png) 22 | slab缓存的精细结构 23 | 24 | 除了管理性数据,即易用和空闲对象或标志寄存器的数目,缓存结构包括两个特别重要的成员。 25 | 26 | 1. 指向一个数组的指针,其中保存了各个CPU最后释放的对象。 27 | 2. 每个内存结点都对应3个表头,用于组织slab的链表。第一个链表包含完全用尽的slab,第二个是部分空闲的slab,第三个是空闲的slab。 28 | 29 | 缓存结构指向一个数组,其中包含了与系统CPU数目相同的数组项。每一个元素都是一个指针,指向一个进一步的结构称之为数组缓存(*array cache*),其中包含了对应于特定系统CPU的管理数据,但并非用于缓存。管理性数据之后的内存区包含了一个指针数组,各个数组项指向*slab*中未使用的对象。 30 | 31 | 为了更好地利用CPU高速缓存,这些per-CPU指针是很重要的,在分配和释放对象时,采用后进先出(*last in first out,LIFO*)原理。内核假定刚释放的对象仍然处于CPU高速缓存中,会尽快再次分配它以便响应下一个分配请求。仅当per-CPU缓存为空时,才会用slab中的空闲对象重新填充它们。 32 | 33 | 这样,对象分配的体系就形成了一个三层的层次结构,分配成本和操作对CPU高速缓存和TLB的负面影响逐渐升高。 34 | 35 | 1. 仍然处于CPU高速缓存中的per-CPU对象。 36 | 2. 现存slab中未使用的对象。 37 | 3. 刚使用伙伴系统分配的新slab中未使用的对象。 38 | 39 | 对象在slab中并非连续排列,而是按照一个相当复杂的方案分布,如下图: 40 | 41 | ![slab](images/slab4.png) 42 | slab的精细结构 43 | 44 | 用于每个对象的长度并不反应其确切的大小,相反,长度已经进行了舍入以满足某些对齐方式的要求。有两种可用的备选对齐方案: 45 | 46 | 1. slab创建时使用*SLAB_HWCACHE_ALIGN*,slab用户可以要求对象按硬件缓存行对齐。要么按照*cache_line_size*的返回值进行对齐,该函数返回特定于处理器的L1缓存大小。如果对象小于缓存行长度的一般,那么将多个对象放入一个缓存行。 47 | 2. 如果不要求按硬件缓存行对齐,那么内核保证对象按*BYTES_PER_WORD*对齐,该值时表示*void*指针所需字节的数目。 48 | 49 | 在32位处理器上,*void*指针需要4个字节。因此,对有6个字节的对象,需要8=2x4个字节,对于的字节称为填充字节。 50 | 51 | 填充字节可以加速对slab中对象的访问,如果使用对齐的地址,那么几乎在所有的体系结构上,内存访问都会更快,这弥补了使用填充字节必然导致需要更多内存的不利情况。管理结构位于每个slab的起始处,保存了所有的管理结构。其后面时一个数组。 52 | 53 | 每个数组项对应于slab中的一个对象,只有在对象没有分配时,相应的数组项才有意义。在这种情况下,它指定了下一个空闲对象的索引。由于最低编号的空闲对象的编号还保存在slab起始处的管理结构中,内核无需使用链表或其他复杂的关联机制就可以轻松找到当前可用的所有对象,数组的最后一项总是一个结束标记,值为*BUFCTL_END*。 54 | 55 | ![slab](images/slab5.png) 56 | slab中空闲对象的管理 57 | 58 | 大多数情况下,slab内存区的长度时不能被对象长度整除的,因此,内核就有了一些多余的内存,可以用来以偏移量的形式给slab『着色』。缓存的各个slab成员会指定不同的偏移量,以便将数据定位到不同的缓存行,因而slab开始和结束处的空闲内存时不同的。在计算偏移量时,内核必须考虑其他的对齐因素,例如L1高速缓存中数据结构的对齐。 59 | 60 | 管理数据可以放置在slab自身,也可以放置到使用kmalloc分配的不同内存区中,内存如何选择取决于slab的长度和已用对象的数量。相应的选择标准稍后讨论,管理数据和slab内存之间的关系很容易建立,因为slab头包含了一个指针,指向slab数据区的起始处,无论管理数据是否在slab上。 61 | 62 | 最后,内核需要一种方法通过对象自身即可识别slab以及对象驻留的缓存,根据对象的物理内存地址,可以找到相关的页,因此可以在全局的*mem_map*数组中找到对应的*page*实例。*page*结构包含一个连表元素,用于管理各种链表中的页,对于slab缓存中的页而言,该指针时不必要的,可以用于其他用途。 63 | 64 | 1. page->lru.next指向页驻留的缓存和管理结构。 65 | 2. page->lru.prev指向保存该页的slab的管理结构。 66 | 67 | 设置或者读取slab信息分别由*set_page_slab*和*get_page_slab*函数完成,带有*__cache*后缀的函数则处理缓存信息的设置和读取。 68 | 69 | #### #### 70 | 71 | {% highlight c++ %} 72 | static inline void 73 | page_set_cache(struct page *page, struct kmem_cache *cache) 74 | { 75 | page->lru.next = (struct list_head *)cache; 76 | } 77 | 78 | static inline struct 79 | kmem_cache *page_get_cache(struct page *page) 80 | { 81 | page = compound_head(page); 82 | BUG_ON(!PageSlab(page)); 83 | return (struct kmem_cache *)page->lru.next; 84 | } 85 | 86 | static inline void 87 | page_set_slab(struct page *page, struct slab *slab) 88 | { 89 | page->lru.prev = (struct list_head *)slab; 90 | } 91 | 92 | static inline struct slab 93 | *page_get_slab(struct page *page) 94 | { 95 | BUG_ON(!PageSlab(page)); 96 | return (struct slab *)page->lru.prev; 97 | } 98 | {% endhighlight %} 99 | 100 | 此外,内核还对分配给slab分配器的每个物理内存页都设置标志*PG_SLAB*。 -------------------------------------------------------------------------------- /2014-05-09-soft-irq-daemon-ksoftirqd.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 软中断守护进程 4 | category: 中断和异常 5 | description: 软中断守护进程... 6 | tags: 软中断 ksoftirqd 7 | --- 8 | 我们知道如果不在中断上下文中调用*raise_softirq*方法,则调用*wakeup_softirq*来唤醒软中断守护进程,这个守护进程会执行软中断。软中断的守护进程的任务是,与其余内核代码异步执行软中断,为此,系统中每个处理分配器都有自己的守护进程,名为*ksoftirqd*。 9 | 10 | 内核中有两处调用了*wakeup_softirq*唤醒了该守护进程。 11 | 12 | 1. do_softirq中。 13 | 2. 在raise\_softirq\_irqoff末尾。 14 | 15 | *raise\_softirq\_irqoff*函数由*raise_softirq*在内部调用,如果内核当前停用了中断,也可以直接使用。唤醒函数本身只需要几行代码,首先,借助于一些宏,从一个per-CPU变量读取指向当前CPU软中断守护进程的*task_struct*的指针。如果该进程当前的状态不是*TASK_RUNNING*的话,则通过*wake_up_process*将其置放到就绪进程列表的末尾。 16 | 17 | 尽管这并不会立即开始处理所有待决的软中断。但只要调度器没有更好的选择,就会选择用该守护进来执行。在系统启动时用*initcall*机制调用*init*不就,就创建了系统的软中断守护进程。代码如下: 18 | 19 | #### #### 20 | 21 | {% highlight c++ %} 22 | static int ksoftirqd(void * __bind_cpu) 23 | { 24 | set_current_state(TASK_INTERRUPTIBLE); 25 | 26 | current->flags |= PF_KSOFTIRQD; 27 | while (!kthread_should_stop()) { 28 | // 禁止抢占 29 | preempt_disable(); 30 | if (!local_softirq_pending()) { 31 | preempt_enable_no_resched(); 32 | schedule(); 33 | preempt_disable(); 34 | } 35 | 36 | __set_current_state(TASK_RUNNING); 37 | 38 | while (local_softirq_pending()) { 39 | /* 40 | 禁止抢占会停止让CPU下线,如果已经下线,那么就 41 | 正在一个错误的CPU上,那么就不要执行 42 | goto wait_to_die 43 | */ 44 | if (cpu_is_offline((long)__bind_cpu)) 45 | goto wait_to_die; 46 | // 执行软中断 47 | do_softirq(); 48 | // 可以抢占 49 | preempt_enable_no_resched(); 50 | // 确保对当前进程设置了TIE_NEED_RESCHED 51 | // 因为所有这些函数执行时都启用了硬件中断 52 | cond_resched(); 53 | // 禁止抢占 54 | preempt_disable(); 55 | rcu_sched_qs((long)__bind_cpu); 56 | } 57 | preempt_enable(); 58 | set_current_state(TASK_INTERRUPTIBLE); 59 | } 60 | __set_current_state(TASK_RUNNING); 61 | return 0; 62 | 63 | wait_to_die: 64 | preempt_enable(); 65 | /* 等待kthread_stop停止 */ 66 | set_current_state(TASK_INTERRUPTIBLE); 67 | while (!kthread_should_stop()) { 68 | schedule(); 69 | set_current_state(TASK_INTERRUPTIBLE); 70 | } 71 | __set_current_state(TASK_RUNNING); 72 | return 0; 73 | } 74 | {% endhighlight %} 75 | 76 | 每次被唤醒时,守护进程首先检查是否有标记出的待决软中断,否则明确地调用调度器,将控制软中断交给其他进程。如果有标记出的软中断,那么守护进程接下来将处理软中断。 77 | 78 | 进程在一个*while*循环中重复调用*do_softirq*和*cond_resched*,直至没有标记出的软中断位置。*con_resched*确保在对当前进程设置了*TIE_NEED_RESCHED*标志的情况下调用调度器,这是可能的,因为所有这些函数执行时都启用了硬件中断。 79 | -------------------------------------------------------------------------------- /2014-05-09-tasklet.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: tasklet 4 | category: 中断和异常 5 | description: tasklet... 6 | tags: tasklet 7 | --- 8 | 软中断是将操作推迟到未来某一个时刻执行的最好方法,但延迟函数执行以及处理机制非常复杂。因为多个处理器是可以同时并且独立地处理软中断,同一个软中断的处理程序例程可以在几个CPU上同时运行。 9 | 10 | 对软中断的效率来说,这是一个关键,多处理器上的网络实现依靠于此。但处理程序例程的设计必须是完全可重入且线程安全的。另外,临界区必须使用自旋锁保护,或者其他的IPC机制[^1]。 11 | 12 | tasklet和工作队列是延迟函数执行工作的机制,其实现基于软中断,但是tasklet更易于使用,因而是更适用于设备的驱动程序一起其他一般性的内核代码。 13 | 14 | ### tasklet结构 ### 15 | 16 | tasklet是『小进程』,执行一些迷你的任务,对这些任务使用全功能进程会过于浪费。tasklet的结构定义如下: 17 | 18 | [^1]: 具体可以看后面的内核同步的笔记。 19 | 20 | #### #### 21 | 22 | {% highlight c++ %} 23 | struct tasklet_struct 24 | { 25 | struct tasklet_struct *next; 26 | unsigned long state; 27 | atomic_t count; 28 | void (*func)(unsigned long); 29 | unsigned long data; 30 | }; 31 | {% endhighlight %} 32 | 33 | 从设备驱动程序来看,最重要的成员是*func*,它指向一个函数的地址,该函数的执行将被延期执行。*data*用作该函数执行时的参数。其中*next*是一个指针,用于建立一个*tasklet_struct*的链表,这容许多个任务能够派对执行。其中*state*表示任务的当前状态,类似于真正的进程但是只有两个选项。 34 | 35 | 1. 当tasklet注册到内核,等待调度执行时,将设置*TASKLET_STATE_SCHED*。 36 | 2. *TASKLET_STATE_RUN*表示*tasklet*当前正在执行。 37 | 38 | 第二个状态只在SMP系统上有用,用于保护*tasklet*在多个处理器上并行执行。原子计数器*count*用于禁用已经调度的*tasklet*,如果其值不等于0,在接下来执行的所有等待的tasklet任务时,将忽略对应的tasklet。 39 | 40 | ### 注册tasklet ### 41 | 42 | *tasklet_schedule*将一个tasklet注册到系统中: 43 | 44 | #### #### 45 | 46 | {% highlight c++ %} 47 | static inline void tasklet_schedule(struct tasklet_struct *t) 48 | { 49 | if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) 50 | __tasklet_schedule(t); 51 | } 52 | {% endhighlight %} 53 | 54 | 其中*test_and_set_bit*函数是一个[原子操作](/linux-kernel-architecture/posts/atomic-operations/),其作用是设置*&t->state*的*TASKLET_STATE_SCHED*位并返回原值。如果设置了*TASKLET_STATE_SCHED*标志位,则已经注册了tasklet,则返回。否则,将该tasklet至于一个链表的起始,其表头特定于CPU的变量*tasklet_vec*向量,该链表包含了所有注册的*tasklet*。 55 | 56 | 在注册了一个tasklet之后, tasklet链表即标记为即将进行处理。 57 | 58 | ### 执行tasklet ### 59 | 60 | tasklet生命周期中最为重要的部分就是执行,因为tasklet基于软中断实现,它们总是在处理软中断时执行。 61 | 62 | *tasklet*关联到*TASKLET_SOFTIRQ*软中断,因而,调用*raise_softirq(TASKLET_SOFTIRQ)*就可以在下一个恰当的实际执行当前处理器的tasklet。内核使用*tasklet_action*作为该软中断的action函数。 63 | 64 | 这个函数首先确定特定于CPU的链表,其中保存了标记要执行的各个tasklet,它接下来将表头重定向到函数局部的一个数据项,相当于从外部公开的链表删除了所有表项,接下来,函数在循环中逐一处理tasklet。 65 | 66 | #### #### 67 | 68 | {% highlight c++ %} 69 | static void tasklet_action(struct softirq_action *a) 70 | { 71 | struct tasklet_struct *list; 72 | // 禁用本地irq 73 | local_irq_disable(); 74 | // 从外部链表删除当前CPU的tasklet项 75 | list = __get_cpu_var(tasklet_vec).head; 76 | __get_cpu_var(tasklet_vec).head = NULL; 77 | __get_cpu_var(tasklet_vec).tail = 78 | &__get_cpu_var(tasklet_vec).head; 79 | // 启用本地irq 80 | local_irq_enable(); 81 | 82 | // 循环链表处理 83 | while (list) { 84 | struct tasklet_struct *t = list; 85 | 86 | list = list->next; 87 | 88 | if (tasklet_trylock(t)) { 89 | if (!atomic_read(&t->count)) { 90 | // 清楚相应的比特位 91 | if (!test_and_clear_bit(TASKLET_STATE_SCHED, 92 | &t->state)) 93 | BUG(); 94 | // 处理注册的函数 95 | t->func(t->data); 96 | tasklet_unlock(t); 97 | continue; 98 | } 99 | tasklet_unlock(t); 100 | } 101 | 102 | local_irq_disable(); 103 | t->next = NULL; 104 | *__get_cpu_var(tasklet_vec).tail = t; 105 | __get_cpu_var(tasklet_vec).tail = &(t->next); 106 | __raise_softirq_irqoff(TASKLET_SOFTIRQ); 107 | local_irq_enable(); 108 | } 109 | } 110 | {% endhighlight %} 111 | 112 | 其中*test_and_clear_bit*也是一个[原子操作](/linux-kernel-architecture/posts/atomic-operations/),与*test_and_set_bit*相反,其作用是清除相应的比特位。 113 | 114 | 因为一个tasklet只能在一个处理器上执行一次,但其他的tasklet可以并行执行,所以需要特定于tasklet的锁。*state*状态用作锁变量,在执行一个tasklet的处理程序函数之前,内核使用*tasklet_trylock*检查tasklet的状态是否是*TASKLET_STATE_RUN*。 115 | 116 | #### #### 117 | 118 | {% highlight c++ %} 119 | static inline int tasklet_trylock(struct tasklet_struct *t) 120 | { 121 | return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state); 122 | } 123 | {% endhighlight %} 124 | 125 | *tasklet_trylock*函数检查如果对应的比特位尚未设置,则设置该比特位。 126 | 127 | 如果*count*成员不为0,则该tasklet已经停用,在这种情况下就不执行相关的代码。否则就通过*t->func(t->data)*执行相应的函数。如果在执行tasklet期间,有新的tasklet进入当前处理器的tasklet队列,则会尽快引发*TASKLET_SOFTIRQ*r软中断来执行新的tasklet。 128 | 129 | 除了普通的tasklet之外,内核还使用了另一种tasklet,它具有『较高』的优先级,除以下修改之外,其实现与普通的tasklet完全相同。 130 | 131 | 1. 使用HI\_SOFTIRQ作为软中断,而不是TASKLET\_SOFTIRQ。 132 | 2. 注册tasklet在CPU的相关变量的tasklet\_hi\_vec中站队。 133 | 134 | 当前大部分声卡驱动程序都利用了这一选项,因为操作延迟时间太长可能损害音频输出的音质。 -------------------------------------------------------------------------------- /2014-05-11-completion.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 完成量 4 | category: 进程 5 | description: 完成量... 6 | tags: 完成量 7 | --- 8 | 完成量(*completion*)机制基于等待队列,内核利用这个机制等待某一个操作结束,这两种机制使用得都比较频繁,主要用于设备的驱动程序。完成量与信号量有些相似,但完成量是基于等待队列实现的。 9 | 10 | 我们只感兴趣完成量的接口。在场景中有两个参与者,一个在等待某操作的完成,而另一个在操作完成时发出声明,实际上,这已经被简化过了。实际上,可以有任意数目的进程等待操作完成,为表示进程等待的即将完成的『某操作』,内核使用了*complietion*数据结构,代码如下: 11 | 12 | #### #### 13 | 14 | {% highlight c++ %} 15 | struct completion { 16 | unsigned int done; 17 | wait_queue_head_t wait; 18 | }; 19 | {% endhighlight %} 20 | 21 | 我们可以看到*wait*变量是一个*wait_queue_head_t*结构体,是等待队列链表的头,*done*是一个计数器。每次调用*completion*时,该计数器就加1,仅当*done*等于0时,*wait_for*系列函数才会使调用进程进入睡眠。实际上,这意味着进程无需等待已经完成的事件。 22 | 23 | 其中*wait_queue_head_t*已经在等待队列中记录过了,代码如下: 24 | 25 | #### #### 26 | 27 | {% highlight c++ %} 28 | struct __wait_queue_head { 29 | spinlock_t lock; 30 | struct list_head task_list; 31 | }; 32 | typedef struct __wait_queue_head wait_queue_head_t; 33 | {% endhighlight %} 34 | 35 | *init_completion()*函数用于初始化一个动态分配的*completion*实例,而*DECLARE_COMPLETION*宏用来建立该数据结构的静态实例。*init_completion()*函数代码如下: 36 | 37 | #### #### 38 | 39 | {% highlight c++ %} 40 | static inline void init_completion(struct completion *x) 41 | { 42 | x->done = 0; 43 | init_waitqueue_head(&x->wait); 44 | } 45 | {% endhighlight %} 46 | 47 | 从上面代码中可以看到,初始化完成量会将*done*字段初始化为0,并且初始化*wait*链表。进程可以用*wait_for_completion*添加到等待队列,进程在其中等待,并以独占睡眠状态直到请求被内核的某些部分处理,这些函数都需要一个*completion*实例: 48 | 49 | #### #### 50 | 51 | {% highlight c++ %} 52 | extern void 53 | wait_for_completion( 54 | struct completion *); 55 | 56 | extern int 57 | wait_for_completion_interruptible( 58 | struct completion *x); 59 | 60 | extern int 61 | wait_for_completion_killable( 62 | struct completion *x); 63 | 64 | extern unsigned long 65 | wait_for_completion_timeout( 66 | struct completion *x, 67 | unsigned long timeout); 68 | 69 | extern unsigned long 70 | wait_for_completion_interruptible_timeout( 71 | struct completion *x, 72 | unsigned long timeout); 73 | 74 | extern bool 75 | try_wait_for_completion( 76 | struct completion *x); 77 | 78 | extern bool 79 | completion_done( 80 | struct completion *x); 81 | 82 | extern void 83 | complete( 84 | struct completion *); 85 | 86 | extern void 87 | complete_all( 88 | struct completion *); 89 | {% endhighlight %} 90 | 91 | 通常进程在等待事件的完成时处于不可中断状态,但如果使用*wait_for_completion_interruptible*可以改变这一设置,如果进程被中断,则函数返回*-ERESTARTSYS*,否则返回0. 92 | 93 | *wait_for_completion_timeout*等待一个完成事件发送,但提供了超时的设置,如果等待时间超过了这一设置,则取消等待。这有助于防止无限等待某一时间,如果在超时之间就已经完成,函数就返回剩余时间,否则就返回0。 94 | 95 | *wait_for_completion_interruptible_timeout*是前两种的结合体。 96 | 97 | 在请求由内核的另一部分处理之后,必须调用*complete*或者*complete_all*来唤醒等待的进程。因为每次调用只能从完成量的等待队列移除一个进程,对*n*个等待进程来说,必须调用函数*n*次。另一方面,*complete_all*会唤醒所有等待该完成的进程。 98 | 99 | 除此之外,还有*complete_and_exit*方法,该方法是一个小的包装起,首先调用*complete*,然后调用*do_exit*结束内核线程。 100 | 101 | #### #### 102 | 103 | {% highlight c++ %} 104 | NORET_TYPE void complete_and_exit( 105 | struct completion *comp, 106 | long code) 107 | { 108 | if (comp) 109 | complete(comp); 110 | 111 | do_exit(code); 112 | } 113 | {% endhighlight %} 114 | 115 | 在*completion*结构体中,*done*是一个计数器。*complete_all*的工作方式与之类似,但它会将计数器设置为最大的可能值,这样,在事件完成后调用*wait_for*系列函数的进程将永远不会睡眠。 -------------------------------------------------------------------------------- /2014-05-11-virtual-filesystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 虚拟文件系统 4 | category: 虚拟文件系统 5 | description: 虚拟文件系统... 6 | tags: 文件系统 VFS 7 | --- 8 | Linux是具有与其他操作系统和谐共存的能力,可以透明地安装具有其他操作系统文件格式的磁盘或分区,这些操作系统如Windows或者其他Unix。通过所谓的虚拟文件系统的概念,Linux使用与其他Unix变体相同的方式设法支持多重文件系统类型。 9 | 10 | 虚拟文件系统所隐含的思想是把表示很多不同种类的文件系统的共同信息放入内核,其中有一个字段或函数来支持Linux所支持的所有实际文件系统所提供的任何操作。读所调用的每个读、写或其他函数,内核都能把它们替换成支持本地Linux文件系统,NTFS文件系统,或者文件所在的任何其他文件系统的实际函数。 11 | 12 | 虚拟文件系统(*Virtual Filesystem*)也可以称之为虚拟文件系统换换(*Virtual Filesystem Switch,VFS*),是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用,其表现为能为各种文件系统提供一个通用的接口。 13 | 14 | 假设一个用户拷贝一个文件,例如: 15 | 16 | cp /floopy/TEST /tmp/test 17 | 18 | 其中*/floopy*是MS-DOS磁盘的一个安装点,而*/tmp*是一个标准的Ext文件系统的目录。如下图所示: 19 | 20 | ![vfs](images/vfs.png) 21 | 一个简单的VFS架构 22 | 23 | VFS支持的文件系统可以划分为三类。 24 | 25 | **磁盘文件系统** 26 | 27 | 这些文件系统管理在本地磁盘分区中可用的存储空间或其他可以起到磁盘作用的设备[^1],VFS支持的基于磁盘的某些文件系统还有: 28 | 29 | 1. Linux使用的文件系统,例如Ext2,现在最新使用的文件系统是Ext4。 30 | 2. Unix家族的文件系统,如sysv、MINIX文件系统以及VERITAS VxFS文件系统。 31 | 3. Miscrosoft文件系统。 32 | 4. ISO9660 CD-ROM文件系统和通用磁盘格式DVD文件系统。 33 | 5. 其他有专利权的文件系统哦女孩,如HPFS、HFS、AFFS以及ADFS文件系统。 34 | 6. 起源于非Linux系统的其他日志文件系统,如IBM的JFS和SGI的XFS文件系统。 35 | 36 | **网络文件系统** 37 | 38 | 这些文件系统允许轻易地访问属于其他网络计算机的文件系统所包含的文件,虚拟文件系统所支持的一些著名的网络文件系统有:NFS、Coda、AFS、CIFS等文件系统。 39 | 40 | **特殊文件系统** 41 | 42 | 这些文件系统不管理本地或者远程磁盘空间,/proc文件系统是特殊文件系统的一个典型范例。 43 | 44 | 由于我看的书的原因,记录的更多的是Ext2和Ext3文件系统,但依旧会看Ext4文件系统并记录一些笔记。所以今后笔记中的Linux文件系统就不指明是第几代Ext文件系统,统称为Ext文件系统。 45 | 46 | Unix的目录建立了一颗根目录为『/』的树,根目录包含在根文件系统(*root filesystem*)中,在Linux中这个根文件系统通常是Ext类型,其他所有的文件系统都可以被安装在根文件系统的子目录里。 47 | 48 | 当一个文件系统被安装在某一个目录上时,在父文件系统中的目录内容不再是可访问的,因为任何路径,甚至包括安装点,都将引用已安装的文件系统。但是,当被安装文件系统卸载之后,原目录的内容又可以再现。这种Unix文件系统的特点可以由系统管理员用来隐藏文件,因为只需要把一个文件系统安装在要隐藏文件的目录中即可。 49 | 50 | 基于磁盘的文件系统通常存放在硬件块设备中,如磁盘、软盘或者CD-ROM。Linux VFS的一个有用的特点是能够处理*/dev/loop0*这样的虚拟块设备,这种设备可以用来安装普通文件所在的文件系统。作为一种可能的应用,用户可以保护自己的私有文件系统,这可以通过把自己文件系统的加密版本存放在一个普通文件中来实现。 51 | 52 | [^1]: 比如说一个USB闪存。 -------------------------------------------------------------------------------- /2014-05-12-common-file-model.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 通用文件模型 4 | category: 虚拟文件系统 5 | description: 通用文件模型... 6 | tags: 文件模型 7 | --- 8 | VFS所隐含的主要思想在于引入了一个通用文件模型(*common file model*),这个模型能够表示所有支持的文件系统。该模型严格反映传统Unix文件系统提供的文件模型。这非常强大,因为Linux希望以最小的额外开销运行它本地文件系统。不过,要实现每个具体的文件系统,必须将其物理组织结构转换为虚拟文件系统通用文件模型。 9 | 10 | 例如,在通用文件模型中,每个目录被看作一个文件,可以包含若干文件和其他的子目录。但是,存在几个非Unix的基于磁盘的文件系统,它们利用文件分配表(*File Allocation Table,FAT*)存放每个文件在目录树中的位置,在这些文件系统中,存放的是目录而不是文件。为了符合VFS的通用文件模型,对上述基于FAT的文件系统的实现,Linux必须在必要的时候能够快速建立对应于目录的文件。这样的文件只作为内核内存的对象而存在。 11 | 12 | 从本质上来说,Linux内核不能对一个特定的函数进行硬编码来执行注入*read()*或*loctl()*这样的操作,而是对每个操作都必须使用一个指针,指向要访问的具体文件系统的适当函数。 13 | 14 | 我们可以把通用文件模型看作是面向对象的,在这里,对象是一个软件结构,其中既定义了数据结构也定义了其上的操作方法。处于效率的考虑,Linux的编码并未采用面向对象的程序设计语言。因此对象作为普通的C数据结构来实现,数据机构中指向函数的字段就对应于对象的方法。通用文件模型由下列对象类型组成: 15 | 16 | **超级块对象(*superblock object*)** 17 | 18 | 存放已安装文件系统的有关信息,对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块(*filesystem control block*)。 19 | 20 | **索引节点对象(*inode object*)** 21 | 22 | 存放于具体文件的一般信息,对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块(*file control block*)。每个索引节点对象都有一个索引节点号。这个节点号唯一地标识文件系统中的文件。 23 | 24 | **文件对象(*file object*)** 25 | 26 | 存放打开文件与进程之间进行交互的有关信息,这类信息仅当进程访问文件期间存在于内核内存中。 27 | 28 | **目录项对象(*dentry object*)** 29 | 30 | 存放目录项,也就是文件的特定名称,与对应文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将改类信息存在磁盘上。 31 | 32 | ![vfs](images/vfs2.png) 33 | 进程与VFS对象之间的交互 34 | 35 | 如上图所示的一个简单的示例,说明进程怎样与文件系统交互。三个不同的进程已经打开同一个文件,其中两个进程使用同一个硬链接。在这种情况下,其中的每一个进程都能使用自己的文件对象,但只需要两个目录项对象,每个硬链接对应一个目录项对象。这两个目录项对象指向同一个索引节点对象,该索引节点对象标识超级块对象,以及随后的普通磁盘文件。 36 | 37 | VFS除了能为所有文件系统的实现提供一个通用接口外,还具有另一个与系统性能相关的重要作用。最近最常使用的目录项对象被放在所谓目录项高速缓存的磁盘高速缓存中,以加速从文件路径名到最后一个路径分量的索引节点的转换过程。 38 | 39 | 一般来说,磁盘高速缓存(*disk cache*)属于软件机制,它允许内核将原本存在磁盘上的某些信息保存在RAM中,以便对这些数据的进一步访问能快速进行而不必慢速访问磁盘本身。 40 | 41 | 磁盘高速缓存不同于硬件高速缓存或内存高速缓存,后两者都与磁盘或其他设备无关,硬件高速缓存是一个快速静态RAM,它加快了直接对慢速动态RAM的请求。内存高速缓存是一种软件机制,引入它是为了绕过内核分配器。可参考[硬件高速缓存和TLB](/linux-kernel-architecture/posts/system-hardware-cache-and-tlb/)章节。 42 | 43 | 除了目录项高速缓存和索引节点高速缓存之外,Linux还能使用其他磁盘高速缓存,其中最为重要的一种就是所谓的页高速缓存。 44 | -------------------------------------------------------------------------------- /2014-05-12-vfs-system-interfaces.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: VFS相关的系统调用 4 | category: 虚拟文件系统 5 | description: VFS相关的系统调用... 6 | tags: 系统调用 VFS 7 | --- 8 | VFS提供了诸多的接口以便可以简单的调用,这些系统调用涉及文件系统、普通文件、目录文件以及符号链接文件。另外还有少数几个由VFS处理的其他系统调用例如*ioperm()*,*ioctl()*、*pipe()*等。 9 | 10 | 系统调用名 | 说明 11 | ------------ | ------------- 12 | mount() umonut() umount2() | 安装/卸载文件系统 13 | sysfs() | 获取文件系统信息 14 | statfs() fstatfs() statfs64() fstatfs64() | 获取文件系统统计信息 15 | ustat() chroot() pivot_root() | 更改根目录 16 | chdir() fchdir() getcwd() | 对当前目录进行操作 17 | mkdir() rmdir() | 创建和删除目录操作 18 | get\_dents() getdents64() readdir() link() unlink() rename() lookup\_dcookie() readlink() symlink() | 对软连接进行操作 19 | chown() fchown() lchown() chown16() fchown16() lchown16() | 更改文件所有者性 20 | chmod() fchmod() utime() | 更改文件属性 21 | stat() fstat() lstat() access() oldstat() oldfstat() oldlstat() stat64() lstat64() fstat64() | 获取文件状态 22 | open() close() creat() umask() | 打开关闭创建文件操作 23 | dup() dup2() fcntl() fcntl64() | 对文件描述符进行操作 24 | select() poll() | 等待一组文件描述符上发生的事件 25 | truncate() ftruncate() truncated64() ftruncate64() | 更改文件长度 26 | lseek() _llseek() | 更改文件指针 27 | read() write() readv() writev() sendfile() sendfile64) readahead() | 进行文件I/O操作 28 | io\_setup() io\_submit() io\_getevents() io\_cancel() io\_destroy() | 异步I/O 29 | pread64() pwrite64() | 搜索并访问文件 30 | nmap() nmap2() munmap() madvise() mincore() remap\_file\_pages() | 处理文件内存映射 31 | fdatasync() fsync() sync() msync()| 同步文件处理 32 | flock() | 处理文件锁 33 | setxattr() lsetxattr() fsetxattr() getxattr() lgetxattr() fgetxattr() listxattr() llistxattr() flistxattr() removexattr() lremovexattr() fremovexattr() | 处理文件扩展属性 34 | 35 | 虽然VFS是应用程序和具体文件系统之间的一层,不过,在某些情况下,一个文件操作可能由VFS本身去执行,无需调用低层函数。 36 | 37 | 例如,当某个进程关闭一个打开的文件时,并不需要涉及磁盘上的相应文件,因此VFS只需释放对应的文件对象。 38 | 39 | 同样,当系统调用*lseek()*修改一个文件指针,而这个文件指针是打开文件与进程交互所涉及的一个属性时,VFS就只需修改对应的文件对象,而不必访问磁盘上的文件,因此,无需调用具体文件系统的函数,所以可以把VFS看成『通用』文件系统,它在必要时才需要依赖某种具体的文件系统。 40 | -------------------------------------------------------------------------------- /2014-05-13-return-from-interrupt-or-exception.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 从中断和异常返回 4 | category: 中断和异常 5 | description: 从中断和异常返回... 6 | tags: 中断 异常 7 | --- 8 | 在恢复中断和异常之后,通常直接恢复某个程序的执行即可,但是在这样做之前,还必须要考虑几个问题: 9 | 10 | 1. 内核控制路径并发的执行数量,如果仅仅只有一个,那么CPU必须切换到用户态。 11 | 2. 挂起进程的切换请求,如果有任何请求,内核就必须执行进程调度,否则,把控制权还给当前进程。 12 | 3. 挂起的信号,如果一个信号发送到当前进程,就必须处理它。 13 | 4. 单步执行模式,如果调试程序正在跟着当前的进程的执行,就必须在进程切换回到用户态之前恢复单步执行。 14 | 5. Virtual-8086模式,如果CPU处以该模式,则当前进程正在执行原来实模式程序,因而必须以特殊的方式处理这种情况。 15 | 16 | 需要使用一些标志来记录挂起进程切换的请求、挂起信号和单步执行,这些标志被存放在*thread_info*描述符的*flags*字段中,这个字段也存放其他与中断和异常返回无关的标志,这些标志包含: 17 | 18 | 标志 | 说明 19 | ------------ | ------------- 20 | TIF\_SYSCALL\_TRACE | 正在跟踪系统调用 21 | TIF\_NOTIFY\_RESUME | 在80x86平台上不使用 22 | TIF\_SIGPENDING | 进程有挂起信号 23 | TIF\_NEED_RESCHED | 必须执行调度程序 24 | TIF\_SINGLESTEP | 临返回用户态前恢复单步执行 25 | TIF\_IRET | 通过iret而不是sysexit从系统调用强行返回 26 | TIF\_SYSCALL\_AUDIT | 系统调用正在被审计 27 | TIF\_POLLING\_NRFLAG | 空闲进程正在轮询TIF\_NEED\_RESCHED标志 28 | TIF\_MEMDIE | 正在撤销进程以回收内容 29 | 30 | 从技术上来说,完成所有这些事情的内核汇编语言代码并不是一个函数,因为控制权从不返回到调用它的函数,它只是一个代码片段,有两个不同的入口点,分别叫做*ret_from_intr()*和*ret_from_exception()*。 31 | 32 | 中断处理程序结束时,内核进入*ret_from_intr()*,而当异常处理程序结束时,它进入*ret_from_exception*。这两个入口点并不是两个函数,只是因为方便而如此使用。具体的实现是晦涩的汇编语言,也许在今后的笔记中会更详细的记录。 -------------------------------------------------------------------------------- /2014-05-13-slab-alloc.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: slab分配 4 | category: 内存管理 5 | description: slab分配... 6 | tags: slab 7 | --- 8 | slab的分配使用*kmem_cache_alloc*函数,这个函数的流程图如下: 9 | 10 | ![slab](images/slab_alloc.png) 11 | 12 | #### #### 13 | 14 | {% highlight c++%} 15 | void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 16 | { 17 | void *ret = __cache_alloc(cachep, flags, __builtin_return_address(0)); 18 | 19 | trace_kmem_cache_alloc(_RET_IP_, ret, 20 | obj_size(cachep), cachep->buffer_size, flags); 21 | 22 | return ret; 23 | } 24 | EXPORT_SYMBOL(kmem_cache_alloc); 25 | {% endhighlight %} 26 | 27 | 可以看到无论是*kmalloc*或者*kmem_cache_alloc*最后都会调用到*\_\_cache_alloc*函数,而*\_\_cache_alloc*最后都会调用到*____cache_alloc*函数[^1]。 28 | 29 | [^1]: 四个下划线。 30 | 31 | #### #### 32 | 33 | {% highlight c++%} 34 | static inline void *____cache_alloc( 35 | struct kmem_cache *cachep, gfp_t flags) 36 | { 37 | void *objp; 38 | struct array_cache *ac; 39 | 40 | check_irq_off(); 41 | 42 | ac = cpu_cache_get(cachep); 43 | if (likely(ac->avail)) { 44 | STATS_INC_ALLOCHIT(cachep); 45 | ac->touched = 1; 46 | objp = ac->entry[--ac->avail]; 47 | } else { 48 | STATS_INC_ALLOCMISS(cachep); 49 | objp = cache_alloc_refill(cachep, flags); 50 | } 51 | 52 | kmemleak_erase(&ac->entry[ac->avail]); 53 | return objp; 54 | } 55 | {% endhighlight %} 56 | 57 | 从上面的代码来看,*cachep*是一个指针,指向缓存使用的*kmem_cache_t*实例,*ac_data*宏通过返回*cachep->array[smp_processor_id()]*,从而获得当前活动CPU相关的*array_cache*实例。 58 | 59 | 因为内存中的对象紧跟*array_cache*实例之后,内核可以借助该结构末尾的伪数组访问对象而不需要使用指针,通过将*ac->avail*减去1,就可以将对象从缓存中移除。 60 | 61 | 如果在per-CPU中没有对象的话,就需要调用*cache_alloc_refill*方法来重新填充。 62 | 63 | #### #### 64 | 65 | {% highlight c++%} 66 | static void *cache_alloc_refill( 67 | struct kmem_cache *cachep, gfp_t flags) 68 | { 69 | int batchcount; 70 | struct kmem_list3 *l3; 71 | struct array_cache *ac; 72 | int node; 73 | 74 | retry: 75 | check_irq_off(); 76 | node = numa_node_id(); 77 | ac = cpu_cache_get(cachep); 78 | batchcount = ac->batchcount; 79 | if (!ac->touched && batchcount > BATCHREFILL_LIMIT) { 80 | /* 检查batchcount的值 */ 81 | batchcount = BATCHREFILL_LIMIT; 82 | } 83 | l3 = cachep->nodelists[node]; 84 | 85 | BUG_ON(ac->avail > 0 || !l3); 86 | spin_lock(&l3->list_lock); 87 | 88 | /* 检查是否可以从共享数组中重新填充 */ 89 | if (l3->shared && transfer_objects( 90 | ac, l3->shared, batchcount)) 91 | goto alloc_done; 92 | 93 | while (batchcount > 0) { 94 | struct list_head *entry; 95 | struct slab *slabp; 96 | /* 获取对象的slab链表 */ 97 | entry = l3->slabs_partial.next; 98 | /* 首先是slabs_partial,然后是slabs_free */ 99 | if (entry == &l3->slabs_partial) { 100 | l3->free_touched = 1; 101 | entry = l3->slabs_free.next; 102 | if (entry == &l3->slabs_free) 103 | goto must_grow; 104 | } 105 | 106 | slabp = list_entry(entry, struct slab, list); 107 | check_slabp(cachep, slabp); 108 | check_spinlock_acquired(cachep); 109 | 110 | /* 111 | * 如果slab不在partial或者free连表中 112 | * 那么至少有一个对象可以被分配 113 | */ 114 | BUG_ON(slabp->inuse >= cachep->num); 115 | 116 | while (slabp->inuse < cachep->num && batchcount--) { 117 | STATS_INC_ALLOCED(cachep); 118 | STATS_INC_ACTIVE(cachep); 119 | STATS_SET_HIGH(cachep); 120 | 121 | ac->entry[ac->avail++] = slab_get_obj(cachep, slabp, 122 | node); 123 | } 124 | check_slabp(cachep, slabp); 125 | 126 | /* 将slab移动到正确的slab连表中 */ 127 | list_del(&slabp->list); 128 | if (slabp->free == BUFCTL_END) 129 | list_add(&slabp->list, &l3->slabs_full); 130 | else 131 | list_add(&slabp->list, &l3->slabs_partial); 132 | } 133 | 134 | must_grow: 135 | l3->free_objects -= ac->avail; 136 | alloc_done: 137 | spin_unlock(&l3->list_lock); 138 | 139 | if (unlikely(!ac->avail)) { 140 | int x; 141 | x = cache_grow( 142 | cachep, flags | GFP_THISNODE, 143 | node, NULL); 144 | 145 | ac = cpu_cache_get(cachep); 146 | if (!x && ac->avail == 0) 147 | return NULL; 148 | 149 | if (!ac->avail) 150 | goto retry; 151 | } 152 | ac->touched = 1; 153 | return ac->entry[--ac->avail]; 154 | } 155 | {% endhighlight %} -------------------------------------------------------------------------------- /2014-05-14-slab-free.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: slab释放 4 | category: 内存管理 5 | description: slab释放... 6 | tags: slab 释放 7 | --- 8 | 如果一个已经分配的对象已经不再需要,那么必须使用*kmem_cache_free*函数将已经分配的slab返回给slab分配器。 9 | 10 | ![slab](images/slab6.png) 11 | slab释放的流程图 12 | 13 | *kmem_cache_free*实际上是一个*__cache_free*的接口,直接调用了该函数,参数直接传递过去,其原因也是防止*kfree*实现中的代码复制。 14 | 15 | 类似于分配,根据per-CPU缓存状态的不同,可以有两种操作流程,如果per-CPU缓存中的对象数目低于允许的限制,则在其中存储一个指向缓存中对象的指针。 16 | 17 | #### #### 18 | 19 | {% highlight c++%} 20 | 21 | static inline void __cache_free( 22 | struct kmem_cache *cachep, void *objp) 23 | { 24 | struct array_cache *ac = cpu_cache_get(cachep); 25 | 26 | check_irq_off(); 27 | kmemleak_free_recursive(objp, cachep->flags); 28 | objp = cache_free_debugcheck(cachep, objp, __builtin_return_address(0)); 29 | 30 | kmemcheck_slab_free(cachep, objp, obj_size(cachep)); 31 | 32 | if (nr_online_nodes > 1 && cache_free_alien(cachep, objp)) 33 | return; 34 | 35 | if (likely(ac->avail < ac->limit)) { 36 | STATS_INC_FREEHIT(cachep); 37 | ac->entry[ac->avail++] = objp; 38 | return; 39 | } else { 40 | STATS_INC_FREEMISS(cachep); 41 | cache_flusharray(cachep, ac); 42 | ac->entry[ac->avail++] = objp; 43 | } 44 | } 45 | {% endhighlight %} 46 | 47 | 从上面的代码可以看出,这样的操作是很必要的,否则,必须将一些对象从缓存移回slab,从编号最低的数组元素开始:缓存的实现一句先进先处的原理,这些对象在数组中已经很长时间,因此不太可能依然驻留在CPU高速缓存中。 48 | 49 | 具体的实现交给函数*cache_flusharray*,这个函数又调用了*free_block*,将对象从缓存移动到原来的slab,并将剩余的对象向数组起始处移动。例如,如果缓存中有30个对象的空间,而*batchcoucnt*为15,则位置0到14的对象将会移回slab,剩余编号15~29的对象则在缓存中向上移动,现在占据位置0~14. 50 | 51 | 将对象从缓存移回到slab是非常有用的,我们看*free_block*代码如下: 52 | 53 | #### #### 54 | 55 | {% highlight c++%} 56 | static void free_block(struct kmem_cache *cachep, void **objpp, int nr_objects, 57 | int node) 58 | { 59 | int i; 60 | struct kmem_list3 *l3; 61 | 62 | for (i = 0; i < nr_objects; i++) { 63 | void *objp = objpp[i]; 64 | struct slab *slabp; 65 | 66 | slabp = virt_to_slab(objp); 67 | l3 = cachep->nodelists[node]; 68 | list_del(&slabp->list); 69 | check_spinlock_acquired_node(cachep, node); 70 | check_slabp(cachep, slabp); 71 | slab_put_obj(cachep, slabp, objp, node); 72 | STATS_DEC_ACTIVE(cachep); 73 | l3->free_objects++; 74 | check_slabp(cachep, slabp); 75 | 76 | /* 临时的slab重新插入缓存链表 */ 77 | if (slabp->inuse == 0) { 78 | if (l3->free_objects > l3->free_limit) { 79 | l3->free_objects -= cachep->num; 80 | slab_destroy(cachep, slabp); 81 | } else { 82 | list_add(&slabp->list, &l3->slabs_free); 83 | } 84 | } else { 85 | list_add_tail(&slabp->list, &l3->slabs_partial); 86 | } 87 | } 88 | } 89 | {% endhighlight %} 90 | 91 | 这个函数在更新缓存数据结构中没有使用对象的数目之后,遍历*objpp*中的所有对象。并对每个对象执行*virt_to_slab*函数。 92 | 93 | 在确定对象所属的slab之前,首先必须调用*virt_to_slab*函数找到对象所在的页,与slab之间的关联使用*page_get_slab*来确定。 94 | 95 | 临时的slab从缓存的链表中移除,*slab_put_obj*反应了在空闲链表中的这种操作,用于分配的第一个对象是刚刚删除的,而列表中的下一个对象则是此前的第一个对象,此后,该slab重新插入到缓存的链表中。 96 | 97 | 释放缓存和销毁缓存还有所不同,销毁缓存使用*kmem_cache_destory*函数,这个函数主要在删除模块的时候调用,这个时候需要将分配的内存全部释放。该函数进行一下三步来释放一个模块内的所有内存: 98 | 99 | 1. 依次扫描slabs_free链表上的slab,将slab返回给伙伴系统。 100 | 2. 释放用于pre-CPU缓存的内存空间。 101 | 3. 从cache_cache链表移除相关数据。 102 | -------------------------------------------------------------------------------- /2014-05-16-dentry-object.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 目录项对象 4 | category: 虚拟文件系统 5 | description: 目录项对象... 6 | tags: 目录项 7 | --- 8 | 我们知道VFS把每个目录看作由若干子目录和文件组成的一个普通文件。然而,一旦目录项被读入内存,VFS就把它转换成基于*dentry*结构的一个目录项对象,该结构的代码如下: 9 | 10 | #### ### 11 | 12 | {% highlight c++ %} 13 | struct dentry { 14 | atomic_t d_count; 15 | unsigned int d_flags; 16 | spinlock_t d_lock; 17 | int d_mounted; 18 | struct inode *d_inode; 19 | struct hlist_node d_hash; 20 | struct dentry *d_parent; 21 | struct qstr d_name; 22 | 23 | struct list_head d_lru; 24 | union { 25 | struct list_head d_child; 26 | struct rcu_head d_rcu; 27 | } d_u; 28 | struct list_head d_subdirs; 29 | struct list_head d_alias; 30 | unsigned long d_time; 31 | const struct dentry_operations *d_op; 32 | struct super_block *d_sb; 33 | void *d_fsdata; 34 | unsigned char d_iname[DNAME_INLINE_LEN_MIN]; 35 | }; 36 | {% endhighlight %} 37 | 38 | 其中目录项对象的字段如下: 39 | 40 | 字段 | 说明 41 | ------------ | ------------- 42 | d_count | 目录项对象引用计数器 43 | d_flag | 目录项高速缓存标志 44 | d_lock | 保护目录项对象的自旋锁 45 | d_inode | 与文件名关联的索引节点 46 | d_parent | 父目录的目录项对象 47 | d_name | 文件名 48 | d_lru | 用于未使用目录项链表的指针 49 | d_child | 对目录而言,用于同一父母路中的目录项链表的指针 50 | d_subdirs | 对目录而言,子目录链表的头 51 | d_alias | 用于与统一索引节点(别名)相关的目录项链表的指针 52 | d_time | 由d\_revalidate方法使用 53 | d_op | 目录项方法 54 | d_sb | 文件的超级块对象 55 | d_fsdata | 依赖于文件系统的数据 56 | d_rcu | 回收目录项时由RCU描述符使用 57 | d_cookie | 指向内核配置文件使用的数据结构的指针 58 | d_hash | 指向散列表表项链表的指针 59 | d_mounted | 对目录而言,用于记录安装该目录项的文件系统数的计数器 60 | d_iname | 存放短文件名的空间 61 | 62 | 每个目录项可以处于以下四种状态: 63 | 64 | 1. 空闲状态。 65 | 2. 未使用状态。 66 | 3. 正在使用状态。 67 | 4. 负状态。 68 | 69 | **空闲状态** 70 | 71 | 处于该状态的目录项对象不包括有效信息,而且还没有被VFS使用,对应的内存区由slab分配器进行处理。 72 | 73 | **未使用状态** 74 | 75 | 处于该状态的目录项对象当前还没有被内核使用。该对象的引用计数器*d_count*的值未0,但其*i_node*字段仍然指向关联的索引节点。该目录项对象包含有效信息,但为了在必要时回收内存,它的内容可能被丢弃。 76 | 77 | **正在使用状态** 78 | 79 | 处于该状态的目录项对象当前正在被内核使用,该对象的引用计数器*d_count*的值未正数,其*d_inode*字段指向关联的索引节点对象。该目录项对象包含有效的信息,并且不能被丢弃。 80 | 81 | **负状态** 82 | 83 | 与目录项关联的索引节点不存在,那是因为相应的磁盘索引节点已被删除,或者因为目录项对象时通过解析一个不存在的文件的路径名创建的。目录项对象的*d_inode*字段设置为*NULL*,但该对象仍然被保存在目录项高速缓存中,以便后续对统一文件目录名的查找操作能够快速完成[^1]。 84 | 85 | [^1]: 负状态这个名词容易使人误解,因为根本不涉及任何负值。 86 | 87 | 与目录项对象关联的方法称为目录项操作,这些方法由*dentry_operations*结构描述,该结构的地址放在目录项对象的*d_op*字段中。代码如下: 88 | 89 | #### ### 90 | 91 | {% highlight c++ %} 92 | struct dentry_operations { 93 | int (*d_revalidate)(struct dentry *, struct nameidata *); 94 | int (*d_hash) (struct dentry *, struct qstr *); 95 | int (*d_compare) ( 96 | struct dentry *, struct qstr *, struct qstr *); 97 | int (*d_delete)(struct dentry *); 98 | void (*d_release)(struct dentry *); 99 | void (*d_iput)(struct dentry *, struct inode *); 100 | char *(*d_dname)(struct dentry *, char *, int); 101 | }; 102 | {% endhighlight %} 103 | 104 | 函数名 | 说明 105 | ------------ | ------------- 106 | d_revalidate(dentry, nameidate) | 在把目录项对象转换为一个文件路径名之前,判定该目录项对象是否仍然有效。缺省的VFS函数什么也不做,而网络文件系统可以指定自己的函数 107 | d_hash(dentry, name) | 生成一个散列值,这是用于目录项散列表、特定于具体文件系统的散列函数 108 | d_compare(dir, name1, name2) | 比较两个文件名 109 | d_delete(dentry) | 当对目录项对象的最后一个引用被删除,调用该方法,缺省的VFS函数什么也不做 110 | d_release(dentry) | 当要释放一个目录项对象时,调用该方法 111 | d_input(dentry, ino) | 当一个目录对象变为『负』状态时,调用该方法 112 | 113 | -------------------------------------------------------------------------------- /2014-05-17-dentry-cache.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 目录项高速缓存 4 | category: 虚拟文件系统 5 | description: 目录项高速缓存... 6 | tags: 目录项 高速缓存 7 | --- 8 | 由于从磁盘读入一个目录项并构造相应的目录项对象需要花费大量的时间,所以,再完成对目录项对象的操作后,可能后面还要使用它,因此仍在内存中保留它有重要的意义。例如,我们经常需要编译文件,随后编译它,或者编辑并打印它,或者复制它并编辑这个拷贝,再诸如此类的情况中,同一个文件需要被反复访问。 9 | 10 | 为了最大限度地提高处理这些目录项对象的效率,Linux使用目录项高速缓存,它由两种类型的数据结构组成: 11 | 12 | 1. 一个处于正在使用,未使用或负状态的目录项对象的集合。 13 | 2. 一个散列表,从中能够快速获取与给定的文件名和目录名对应的目录项对象,同样,如果访问的对象不在目录项高速缓存中,则散列函数返回一个空值。 14 | 15 | 目录项高速缓存的作用还相当于索引节点高速缓存(*inode cache*)的控制器,在内核内存中,并不丢弃与未用目录项相关的索引节点,这是由于目录项高速缓存仍在使用它们。因此,这些索引节点对象保存在RAM中,并能够借助相应的目录项快速引用它们。 16 | 17 | 所有未使用的目录项对象都放在一个『最近最少使用(LRU)』的双向链表中,该链表按照插入的时间排序,也就是说,最后释放的目录项对象放在链表的首部,所以最近最少使用的目录项对象总是靠近链表的尾部。 18 | 19 | 一旦目录项高速缓存的空间开始变小,内核就从链表的尾部删除元素,使得最近最常使用的对象得以保留,LRU链表的首元素和尾元素的地址存放在*list_headr*类型的*dentry_unused*变量的*next*字段和*prev*字段中,目录项对象的*d_lru*字段包含指向链表中相邻目录项的指针。 20 | 21 | 每个正在使用的目录项对象都被插入一个双向链表中,该链表由相应索引节点对象的*i_dentry*字段所指向。目录项对象的*d_alias*字段存放链表中相邻元素的地址,从前面的对象笔记中就可以清楚的明白。 22 | 23 | 当指向相应文件的最后一个硬连接被删除后,一个正在使用的目录项对象可能会变成负状态。在这种情况下,该目录项对象被移到未使用目录项对象组成的LRU链表中。每当内核缩减目录项高速缓存时,负状态目录项对象就朝着LRU链表的尾部移动,这样这些对象就会被逐渐释放。 24 | 25 | 散列表是由*dentry_hashtable*数组实现的。数组中的每个元素时一个指向链表的指针,这种链表就是把具有相同散列表值的目录项进行散列而成的。该数组的长度取决于系统已安装RAM的数量,缺省值时每兆字节RAM包含256个元素。 26 | 27 | 目录项对象的*d_hash*字段包含指向具有相同散列值的链表中的相邻元素,散列函数产生的值是由目录的目录项对象及文件名计算出来的。 28 | 29 | *dcache_lock*自旋锁保护目录项高速缓存数据结构免受多处理器系统上的同时访问。*d_lookup()*函数在散列表中查找给定的父目录对象和文件名,为了避免发生竞争,使用顺序锁。*__d_lookup()*函数与之类似,但假定不会发生竞争,因此也不需要顺序锁。 30 | 31 | #### #### 32 | 33 | {% highlight c++ %} 34 | struct dentry * d_lookup( 35 | struct dentry * parent, struct qstr * name) 36 | { 37 | struct dentry * dentry = NULL; 38 | unsigned long seq; 39 | 40 | do { 41 | seq = read_seqbegin(&rename_lock); 42 | dentry = __d_lookup(parent, name); 43 | if (dentry) 44 | break; 45 | } while (read_seqretry(&rename_lock, seq)); 46 | return dentry; 47 | } 48 | {% endhighlight %} 49 | 50 | *d_lookup()*函数最终会调用到*__d_lookup()*函数。 51 | 52 | #### #### 53 | 54 | {% highlight c++ %} 55 | struct dentry * __d_lookup( 56 | struct dentry * parent, struct qstr * name) 57 | { 58 | unsigned int len = name->len; 59 | unsigned int hash = name->hash; 60 | const unsigned char *str = name->name; 61 | struct hlist_head *head = d_hash(parent,hash); 62 | struct dentry *found = NULL; 63 | struct hlist_node *node; 64 | struct dentry *dentry; 65 | 66 | rcu_read_lock(); 67 | 68 | hlist_for_each_entry_rcu(dentry, node, head, d_hash) { 69 | struct qstr *qstr; 70 | 71 | if (dentry->d_name.hash != hash) 72 | continue; 73 | if (dentry->d_parent != parent) 74 | continue; 75 | 76 | spin_lock(&dentry->d_lock); 77 | 78 | /* 79 | * 在上锁之后重新检查目录项因为 80 | * d_move可能会更改一些其他属性 81 | */ 82 | if (dentry->d_parent != parent) 83 | goto next; 84 | 85 | if (d_unhashed(dentry)) 86 | goto next; 87 | 88 | /* 89 | * 因为d_mode()不能修改qstr,因为被自旋锁保护 90 | * 所以检查和比较名字是安全的 91 | */ 92 | qstr = &dentry->d_name; 93 | if (parent->d_op && parent->d_op->d_compare) { 94 | if (parent->d_op->d_compare(parent, qstr, name)) 95 | goto next; 96 | } else { 97 | if (qstr->len != len) 98 | goto next; 99 | if (memcmp(qstr->name, str, len)) 100 | goto next; 101 | } 102 | 103 | atomic_inc(&dentry->d_count); 104 | /* 找到了 */ 105 | found = dentry; 106 | spin_unlock(&dentry->d_lock); 107 | break; 108 | next: 109 | spin_unlock(&dentry->d_lock); 110 | } 111 | rcu_read_unlock(); 112 | /* 返回命中的缓存项 */ 113 | return found; 114 | } 115 | {% endhighlight %} 116 | 117 | 可以看到,缓存项的相关逻辑并不是那么复杂。 -------------------------------------------------------------------------------- /2014-05-18-special-filesystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 特殊文件系统 4 | category: 虚拟文件系统 5 | description: 特殊文件系统... 6 | tags: 文件系统 7 | --- 8 | 当网络和磁盘文件系统能够使用户处理存放在内核之外的信息时,特殊文件系统可以为系统程序员和管理员提供一种容易的方式来操作内核的数据结构并实现操作系统的特殊特征。下面列出了一些特殊的文件系统,对于其中的每个文件系统,表中给出了它的安装点和简短的描述: 9 | 10 | 名字 | 安装点 | 说明 11 | ------------ | ------------- | ------------- 12 | bdev | 无 | 块设备 13 | binfmt_misc | 任意 | 其他可执行格式 14 | devpts | /dev/pts | 伪终端支持 15 | eventpollfs | 无 | 由有效事件轮询机制使用 16 | futexfs | 无 | 由futex机制使用 17 | pipefs | 无 | 管道 18 | proc | /pros | 对内核数据结构的常规访问点 19 | rootfs | 无 | 为启动阶段提供一个空的根目录 20 | shm | 无 | IPC共享线性区 21 | mqueue | 任意 | 实现POSIX消息队列时使用 22 | sockfs | 无 | 套接字 23 | sysfs | /sys | 对系统数据的常规访问点 24 | tmpfs | 任意 | 临时文件[^1] 25 | usbfs | /proc/bus/usb | USB设备 26 | 27 | [^1]: 如果不被交换出去就保持在RAM中。 28 | 29 | 有几个文件系统没有固定的安装点,这些文件系统可以由用户自由地安装和使用。此外,一些特殊文件系统根本没有安装点,它们不是用于与用户交互,但是内核可以用它们来很容易地重新使用VFS层的某些代码[^2]。 30 | 31 | [^2]: 例如有了pipefs特殊文件系统,就可以把管道和FIFO文件以相同的方式对待。 32 | 33 | 特殊文件系统不限于物理块设备,然而,内核给每个安装的特殊文件系统分配一个虚拟的块设备,让其主设备号为0而次设备号具有任意值,而每个特殊文件系统有不同的值。 34 | 35 | *set_anon_super()*函数用于初始化特殊文件系统的超级块,这个函数本质上获得一个未使用的次设备号dev,然后用主设备号0和次设备号dev设置超级块的*s_dev*字段。而另一个*kill_anon_super()*函数移走特殊文件系统的超级块。*unnamed_dev_idr*变量包含指向一个辅助结构的指针。 36 | 37 | 尽管有些内核设计者不喜欢虚拟块设备标识符,但是这些标识符有助于内核以统一的方式处理特殊文件系统和普通文件系统。 -------------------------------------------------------------------------------- /2014-05-19-filesystem-opts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: 文件系统处理 4 | category: 虚拟文件系统 5 | description: 文件系统处理... 6 | --- 7 | 就像每个传统的Unix系统一样,Linux也使用系统的根文件系统,它由内核在引导阶段直接安装,并拥有系统初始化脚本以及最基本的系统程序。 8 | 9 | 其他文件系统要么由初始化脚本安装,要么由用户直接安装在已安装文件系统的目录上。作为一个目录树,每个文件系统豆邮自己的根目录(*root directory*)。安装文件系统的这个目录称之为安装点(*mount point*)已安装文件系统属于安装点目录的一个子文件系统。例如*/proc*虚拟文件系统是系统的根文件系统的孩子。已安装文件系统的根目录隐藏了父文件系统的安装点目录原来的内容,而且父文件系统的整个子树位于安装点之下。 10 | 11 | ### 命名空间 ### 12 | 13 | 在传统的Unix系统中,只有一个已安装文件系统树,从系统的根文件系统开始,每个进程通过指定合适的路径名可以访问已安装文件系统中的任何文件。从这个方面考虑,Linux更加精确:即每个进程可拥有自己的已安装文件系统树,叫做进程的命名空间。 14 | 15 | 通常大多数进程共享一个命名空间,即位于系统的根文件系统且被*init*进程使用的已安装文件系统数,不过,如果*clone()*系统调用以*CLONE_NEWNS*标志创建一个新进程,那么进程将获取一个新的命名空间。这个新的命名空间随后由子进程继承。 16 | 17 | 当进程安装或卸载一个文件系统时,仅修改它的命名空间。因此,所做的修改对共享同一命名空间的所有进程都是可见的,并且也只对它们可见。进程甚至可通过使用Linux特有的*pivot_root()*系统调用来改变它的命名空间的根文件系统。 18 | 19 | ### 文件系统的安装 ### 20 | 21 | 在大多数传统的Unix内核中,每个文件只能安装一次,并且使用*mount*命令安装。在使用*umount*命令卸载该文件系统前,所有其他作用于之前挂载的文件系统的命令都会失效。 22 | 23 | 但是Linux有所不同,同一个文件系统被安装多次时可能的,当然,如果一个文件系统被安装了n次,那么它的根目录就可以通过n个安装点来访问。尽管同一个文件系统可以通过不同的安装点来访问,但是文件系统的确时唯一的,因此,不管一个文件系统被安装了多少次,都只有一个超级块对象。 24 | 25 | 把多个安装堆叠在一个单独的安装点上也是允许的,尽管已经使用先前安装下的文件和目录的进程可以继续使用,但在同一安装点上的新安装隐藏前一个安装的文件系统。当最顶层的安装被删除时,下一层的安装再一次变为可见。 26 | 27 | 但这个时候跟踪已安装的文件系统会变得非常困难。对于每一个安装操作,内核必须在内存中保存安装点和安装标志,以及要安装文件系统与其他已安装文件系统之间的关系。这样的信息保存在已安装文件系统的描述符中,每个描述符时一个*vfsmount*类型的数据结构。 28 | 29 | #### #### 30 | 31 | {% highlight c++ %} 32 | struct vfsmount { 33 | struct list_head mnt_hash; 34 | struct vfsmount *mnt_parent; 35 | struct dentry *mnt_mountpoint; 36 | struct dentry *mnt_root; 37 | struct super_block *mnt_sb; 38 | struct list_head mnt_mounts; 39 | struct list_head mnt_child; 40 | int mnt_flags; 41 | const char *mnt_devname; 42 | struct list_head mnt_list; 43 | struct list_head mnt_expire; 44 | struct list_head mnt_share; 45 | struct list_head mnt_slave_list; 46 | struct list_head mnt_slave; 47 | struct vfsmount *mnt_master; 48 | struct mnt_namespace *mnt_ns; 49 | int mnt_id; 50 | int mnt_group_id; 51 | atomic_t mnt_count; 52 | int mnt_expiry_mark; 53 | int mnt_pinned; 54 | int mnt_ghosts; 55 | #ifdef CONFIG_SMP 56 | int *mnt_writers; 57 | #else 58 | int mnt_writers; 59 | #endif 60 | }; 61 | {% endhighlight %} 62 | 63 | 字段 | 说明 64 | ------------ | ------------- 65 | mnt_hash | 用于散列链表的指针 66 | mnt_parent | 指向父文件系统,这个文件系统安装在其上 67 | mnt_mountpoint | 指向这个文件系统安装点目录的dentry 68 | mnt_root | 指向这个文件系统根目录的dentry 69 | mnt_sb | 指向这个文件系统的超级块对象 70 | mnt_mounts | 包含所有文件系统描述符链表的头 71 | mnt_child | 用于已安装文件系统链表mnt\_mounts的指针 72 | mnt_count | 引用计数器 73 | mnt_flags | 标志 74 | mnt\_expiry\_mark | 文件系统是否到期 75 | mnt_devname | 设备文件名 76 | mnt_list | 已安装文件系统描述符的namespace链表的指针 77 | mnt_fslink | 具体文件系统到期链表指针 78 | mnt_ns | 指向安装了文件系统的进程命名空间的指针 79 | mnt_share | 共享装载 80 | mnt_master | 从属装载 81 | mnt_slave | 从/子装载 82 | mnt\_slave\_list | 从/子装载的链表的头 83 | mnt_id | 装载的id 84 | mnt\_group\_id | 装载的组id -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 3 | 之前是用 Jekyll 绑定在自己的博客上的,但是很多年都不写播客了,去掉了。后来发现还是很多人从其他地方能找到这个地址,所以这次就修复成普通 Markdown 格式的,方便后来人使用。 4 | 5 | 其实 2.6 内核之后内核也改变了很多,2.6 并不一定是最好的学习版本,不过大的思路上也没有什么问题,对于想了解内核和算法的人来说依旧是一个可以选择的内核。 6 | -------------------------------------------------------------------------------- /images/APIC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/APIC.png -------------------------------------------------------------------------------- /images/authors/guojing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/authors/guojing.jpg -------------------------------------------------------------------------------- /images/buddy-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/buddy-system.png -------------------------------------------------------------------------------- /images/copy_process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/copy_process.png -------------------------------------------------------------------------------- /images/do_fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/do_fork.png -------------------------------------------------------------------------------- /images/dram_cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/dram_cache.png -------------------------------------------------------------------------------- /images/execve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/execve.png -------------------------------------------------------------------------------- /images/exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/exit.png -------------------------------------------------------------------------------- /images/free_area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/free_area.png -------------------------------------------------------------------------------- /images/free_area_init_nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/free_area_init_nodes.png -------------------------------------------------------------------------------- /images/free_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/free_page.png -------------------------------------------------------------------------------- /images/free_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/free_steps.png -------------------------------------------------------------------------------- /images/gdt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/gdt.png -------------------------------------------------------------------------------- /images/idt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/idt.png -------------------------------------------------------------------------------- /images/ioirq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/ioirq.png -------------------------------------------------------------------------------- /images/irq_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/irq_loop.png -------------------------------------------------------------------------------- /images/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/link.png -------------------------------------------------------------------------------- /images/linux-paging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/linux-paging.png -------------------------------------------------------------------------------- /images/linux-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/linux-system.png -------------------------------------------------------------------------------- /images/mem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/mem.png -------------------------------------------------------------------------------- /images/memory-fragmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/memory-fragmentation.png -------------------------------------------------------------------------------- /images/mmu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/mmu.png -------------------------------------------------------------------------------- /images/namespace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/namespace.png -------------------------------------------------------------------------------- /images/numa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/numa.png -------------------------------------------------------------------------------- /images/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/page.png -------------------------------------------------------------------------------- /images/page_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/page_frame.png -------------------------------------------------------------------------------- /images/page_frame_alloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/page_frame_alloc.png -------------------------------------------------------------------------------- /images/paging_unit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/paging_unit.png -------------------------------------------------------------------------------- /images/paging_unit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/paging_unit_2.png -------------------------------------------------------------------------------- /images/process-pri.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/process-pri.png -------------------------------------------------------------------------------- /images/relation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/relation.png -------------------------------------------------------------------------------- /images/segment_descriptor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/segment_descriptor.png -------------------------------------------------------------------------------- /images/segment_selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/segment_selector.png -------------------------------------------------------------------------------- /images/segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/segmentation.png -------------------------------------------------------------------------------- /images/slab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab.png -------------------------------------------------------------------------------- /images/slab2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab2.png -------------------------------------------------------------------------------- /images/slab3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab3.png -------------------------------------------------------------------------------- /images/slab4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab4.png -------------------------------------------------------------------------------- /images/slab5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab5.png -------------------------------------------------------------------------------- /images/slab6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab6.png -------------------------------------------------------------------------------- /images/slab_alloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab_alloc.png -------------------------------------------------------------------------------- /images/slab_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slab_create.png -------------------------------------------------------------------------------- /images/slabinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/slabinfo.png -------------------------------------------------------------------------------- /images/start_kernel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/start_kernel.png -------------------------------------------------------------------------------- /images/task-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/task-size.png -------------------------------------------------------------------------------- /images/task_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/task_struct.png -------------------------------------------------------------------------------- /images/thread_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/thread_info.png -------------------------------------------------------------------------------- /images/tree/rb_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_1.png -------------------------------------------------------------------------------- /images/tree/rb_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_delete.png -------------------------------------------------------------------------------- /images/tree/rb_delete_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_delete_3.png -------------------------------------------------------------------------------- /images/tree/rb_delete_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_delete_4.png -------------------------------------------------------------------------------- /images/tree/rb_delete_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_delete_5.png -------------------------------------------------------------------------------- /images/tree/rb_delete_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_delete_6.png -------------------------------------------------------------------------------- /images/tree/rb_delete_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_delete_steps.png -------------------------------------------------------------------------------- /images/tree/rb_insert_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_1.png -------------------------------------------------------------------------------- /images/tree/rb_insert_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_2.png -------------------------------------------------------------------------------- /images/tree/rb_insert_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_3.png -------------------------------------------------------------------------------- /images/tree/rb_insert_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_4.png -------------------------------------------------------------------------------- /images/tree/rb_insert_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_5.png -------------------------------------------------------------------------------- /images/tree/rb_insert_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_steps.png -------------------------------------------------------------------------------- /images/tree/rb_insert_steps_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rb_insert_steps_2.png -------------------------------------------------------------------------------- /images/tree/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/rotate.png -------------------------------------------------------------------------------- /images/tree/search_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/search_tree.png -------------------------------------------------------------------------------- /images/tree/search_tree_del_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/search_tree_del_1.png -------------------------------------------------------------------------------- /images/tree/search_tree_del_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/tree/search_tree_del_2.png -------------------------------------------------------------------------------- /images/vfs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/vfs.png -------------------------------------------------------------------------------- /images/vfs2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/vfs2.png -------------------------------------------------------------------------------- /images/visit_segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/visit_segment.png -------------------------------------------------------------------------------- /images/vmalloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/vmalloc.png -------------------------------------------------------------------------------- /images/vmalloc_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/vmalloc_struct.png -------------------------------------------------------------------------------- /images/zonelist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuoJing/linux-kernel-architecture/cb34cf384a2ff7b0fb5fc6bcadfec5f4d8350a31/images/zonelist.png --------------------------------------------------------------------------------