<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>edsionte&#039;s Linuxworld&#124;新手区</title>
	<atom:link href="http://edsionte.com/techblog/feed" rel="self" type="application/rss+xml" />
	<link>http://edsionte.com/techblog</link>
	<description>一切才刚刚开始。</description>
	<lastBuildDate>Fri, 18 May 2012 05:04:41 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.2</generator>
		<item>
		<title>Linux内存管理中的slab分配器</title>
		<link>http://edsionte.com/techblog/archives/4019</link>
		<comments>http://edsionte.com/techblog/archives/4019#comments</comments>
		<pubDate>Fri, 18 May 2012 03:06:20 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[Linux下C语言]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=4019</guid>
		<description><![CDATA[Linux内核中基于伙伴算法实现的分区页框分配器适合大块内存的请求，它所分配的内存区是以页框为基本单位的。对于内核中小块连续内存的请求，比如说几个字节或者几百个字节，如果依然分配一个页框来来满足该请求，那么这很明显就是一种浪费，即产生内部碎片（internal fragmentation） 为了解决小块内存的分配，Linux内核基于Solaris 2.4中的slab分配算法实现了自己的slab分配器。除此之外，slab分配器另一个主要功能是作为一个高速缓存，它用来存储内核中那些经常分配并释放的对象。 1.slab分配器的基本原理 slab分配器中用到了对象这个概念，所谓对象就是内核中的数据结构以及对该数据结构进行创建和撤销的操作。它的基本思想是将内核中经常使用的对象放到高速缓存中，并且由系统保持为初始的可利用状态。比如进程描述符，内核中会频繁对此数据进行申请和释放。当一个新进程创建时，内核会直接从slab分配器的高速缓存中获取一个已经初始化了的对象；当进程结束时，该结构所占的页框并不被释放，而是重新返回slab分配器中。如果没有基于对象的slab分配器，内核将花费更多的时间去分配、初始化以及释放一个对象。 slab分配器有以下三个基本目标： 1.减少伙伴算法在分配小块连续内存时所产生的内部碎片； 2.将频繁使用的对象缓存起来，减少分配、初始化和释放对象的时间开销。 3.通过着色技术调整对象以更好的使用硬件高速缓存； 2.slab分配器的结构 slab分配器为每种对象分配一个高速缓存，这个缓存可以看做是同类型对象的一种储备。每个高速缓存所占的内存区又被划分多个slab，每个slab是由一个或多个连续的页框组成。每个页框中包含若干个对象，既有已经分配的对象，也包含空闲的对象。slab分配器的大致组成图如下： 每个高速缓存通过kmem_cache结构来描述，这个结构中包含了对当前高速缓存各种属性信息的描述。所有的高速缓存通过双链表组织在一起，形成高速缓存链表cache_chain。每个kmem_cache结构中并不包含对具体slab的描述，而是通过kmem_list3结构组织各个slab。该结构的定义如下： 可以看到，该结构将当前缓存中的所有slab分为三个集合：空闲对象的slab链表slabs_free，非空闲对象的slab链表slabs_full以及部分空闲对象的slab链表slabs_partial。每个slab有相应的slab描述符，即slab结构，它的定义如下： slab描述符中的list字段标明了当前slab处于三个slab链表的其中一个。我们将上述的slab分配器进行细化，可以得到下面的结构图： 3.高速缓存的分类 slab高速缓存分为两大类，普通高速缓存和专用高速缓存。普通高速缓存并不针对内核中特定的对象，它首先会为kmem_cache结构本身提供高速缓存，这类缓存保存在cache_cache变量中，该变量即代表的是cache_chain链表中的第一个元素；另一方面，它为内核提供了一种通用高速缓存。专用高速缓存是根据内核所需，通过指定具体的对象而创建。 3.1 普通高速缓存 slab分配器中kmem_cache是用来描述高速缓存的结构，因此它本身也需要slab分配器对其进行高速缓存。cache_cache变量保存着对高速缓存描述符的高速缓存。 slab分配器所提供的小块连续内存的分配是通过通用高速缓存实现的。通用高速缓存所提供的对象具有几何分布的大小，范围为32到131072字节。内核中提供了kmalloc()和kfree()两个接口分别进行内存的申请和释放。 3.2 专用高速缓存 内核为专用高速缓存的申请和释放提供了一套完整的接口，根据所传入的参数为具体的对象分配slab缓存。 高速缓存的申请和释放 kmem_cache_create()用于对一个指定的对象创建高速缓存。它从cache_cache普通高速缓存中为新的专有缓存分配一个高速缓存描述符，并把这个描述符插入到高速缓存描述符形成的cache_chain链表中。kmem_cache_destory()用于撤销一个高速缓存，并将它从cache_chain链表上删除。 slab的申请和释放 kmem_cache_alloc()在其参数所指定的高速缓存中分配一个slab。相反，kmem_cache_free()在其参数所指定的高速缓存中释放一个slab。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/4019/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux内存管理实践-使用fault()实现内存映射</title>
		<link>http://edsionte.com/techblog/archives/3993</link>
		<comments>http://edsionte.com/techblog/archives/3993#comments</comments>
		<pubDate>Tue, 15 May 2012 14:10:44 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[内存管理]]></category>
		<category><![CDATA[内存映射]]></category>
		<category><![CDATA[缺页异常]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3993</guid>
		<description><![CDATA[内核态与用户态进行数据交互通常是这样一种模型：内核利用自身的特权通过特定的服务程序采集、接收和处理数据；接着，用户态程序和内核服务程序进行数据交互，或接收内核态的数据，或向内核态写入数据。通过传统的那些对文件操作的系统调用就可以完成这样的工作，但是我们有时候需要通过访问用户空间的内存来直接读取内核数据，因为这样可以免去数据在内核态与用户态之间拷贝所花费的时间。 本文基于以上背景，以Linux字符设备驱动为基础，通过内存映射将内核中的一部分虚拟内存直接映射到用户空间，使得用户在访问内存时等同于直接访问内核空间，从而直接获取内核空间的数据。 1.实现原理 不管进程是在用户空间访问数据还是在内核空间访问数据，它所面临的都是虚拟地址。由于Linux对分段机制进行了特殊处理，因此这里的虚拟地址就等同于线性地址。按照一开始我们提出的要求，进程通过访问用户虚拟地址A来达到直接访问内核虚拟地址B中所存储数据的目的，这里的地址A和B必然不相同。那么，如何通过不同的虚拟地址来访问相同的数据？我们可以将虚拟地址A和B都映射到同一块物理内存，就可以实现内核空间和用户空间之间的数据共享。 示意图如下： 虚拟内存和物理内存之间如何联系？当然是通过页表了。我们在内核空间提前分配好缓冲区，并且向该缓冲区写入数据，此时内核会自动将该缓冲区对应的内核虚拟地址与实际的某一快物理内存进行关联，并将它们的映射关系保存在内核页表中。当在内核空间分配内存时时，上述工作自动被完成，比如通过kmalloc()分配内存时。 一旦在内核空间中分配了内存，随之就确定了物理内存。现在我们需要做的是将用户虚拟地址与物理内存进行关联，也就是说我们要将这个映射关系写入进程的用户页表。整个关联的过程是内核缺页异常处理程序完成的，这个处理过程比较复杂。我们要在内核中实现的并不是缺页异常处理程序，因为内核已经实现的很完美，而只需完成其中的一小部门。具体如何实现下问会详细说明。 2.用户态程序的实现 在本文所描述的内存管理试验，用户态程序首先通过open()打开字符设备文件mapdrv，该系统调用执行成功时返回文件描述符；通过mmap()将该设备文件映射到当前进程的用户空间中，该系统调用执行成功时返回指向映射区域的指针；最后通过该指针打印数据。用户态程序的实现如下所示： 用户态程序的实现并不复杂，因为它的主要作用是对内核模块程序的测试。由于用户态程序是对特定的字符设备文件mapdrv进行操作，所以程序中所使用的系统调用将会调用file_operations结构中对应的钩子函数。比如mmap系统调用在执行时会调用mapdrv设备驱动中的mmap钩子函数，虽然两者同名，但是mmap钩子函数所实现的功能只是mmap系统调用执行过程中的一部门，该钩子函数是file_operations结构中的成员。两者的函数原型如下： 如何实例化open和mmap等钩子函数便是整个内核模块程序实现的关键。 3.字符设备驱动程序的实现 整个内核模块程序是以字符设备驱动为基础进行实现的。该程序模块加载函数与一般字符设备驱动程序完成的工作一致： 1.申请设备号； 2.为描述字符设备的数据结构分配空间，并进行初始化； 3.将该字符设备注册到内核中； 在本文所描述的实验中，模块加载函数除了完成上述功能，还要完成以下功能： 首先，通过kmalloc()分配一块内存用于在内核空间保存数据；通过SetPageReserved()将缓存数据的页面常驻内存，防止被换出到磁盘；将一段字符串拷贝到这片内存中。完成初始化函数后，字符设备驱动中最重要的就是实现file_operations结构中的钩子函数。在本文所述的实验中，我们只需实现三个钩子函数。 可以看到，open和release钩子函数的实现十分简单，只是打印相应语句以及更新设备的引用计数。事实上，我们不实现这两个钩子函数对整个驱动程序也没有任何影响。因为他们分别在open和close系统调用执行的过程中被调用，而这两个系统调用已经完成了打开文件和关闭文件的所有工作。因此，open和release钩子函数只是打印一些日志信息，方便用户查看。因此，同名的钩子函数和系统调用并不是等价的关系。 4.通过fault()实现内存映射 mmap系统调用是将本地文件映射到进程的用户空间，如果执行成功，进程的地址空间中会新增一块虚拟内存区域（但并不是每次调用mmap()都会增加一个vma，因为可能会出现内存区域之间的合并） mmap钩子函数的回调只是整个系统调用执行过程中的一部分，这个钩子函数完成的主要功能是将新增的vma中的操作集进行实例化。具体实现代码如下： 首先，offset中保存映射的首页在文件中的偏移量，该偏移量必须是页大小的整数倍，否则将不能进行映射。这一点在实现上将offset与PAGE_MASK宏进行位运算即可判断。接着，判断映射区域的长度是否超出了本实验中预设的长度大小。 如果上述两个条件都合法，那么接下来将进行最为重要的操作，将vma中的操作集进行实例化。vma的操作集就是专门对所属vma进行操作的钩子函数集合，内核中通过vm_operations_struct结构对其描述： 参考上述代码，具体的实例化操作就是定义该类型的变量map_vm_ops，实现所需钩子函数，并将该变量与vm中的操作集字段进行挂接。 我们这里用到的主要有以下三个钩子函数： open：当指定的vma加入到一个地址空间时，该函数被调用。 close：当指定的vma从地址空间删除时，该函数被调用。 fault：当要访问的页不再物理内存时，该函数被缺页处理程序调用。 这三个钩子函数的实现代码如下： 可以看到，vma的open和close两个钩子函数没有做什么具体工作，因为打开和关闭一个vma的工作全部由内核负责，但是在mmap钩子函数中我们必须显示的调用map_open()。 这里我们重点说明falut钩子函数的实现。当用户要访问vma中的页，而该页又不在内存时，将发生缺页异常，fault钩子函数会在整个缺页处理程序中被调用。整个过程大致如下： 1.找到缺页地址所在的vma。 2.如果有必要分配各级页表项。 3.如果页表项对应的物理页面不存在，则回调当前vma中的fault钩子函数，它返回物理页面描述符。 4.将物理页面地址填充到相应页表项中。 5.完毕。 可以看到，fault钩子函数所实现的主要功能就是返回所需的物理内存描述符。 根据本文第一部分所描述实现原理，我们通过kmalloc()分配一块虚拟内存，可以通过virt_to_page()获得该虚拟内存对应的物理页框描述符，最后将该物理页框描述符返回到缺页异常处理程序中。至于用户页表的更新，那是缺页异常处理程序负责的事情，我们不必理会。 通过上述实现过程，我们就将用户虚拟地址A和内核虚拟地址B映射到了同一的物理内存上，从而实现进程访问用户地址时直接获得内核数据的功能。 本实验涉及的知识点比较多，比如字符设备驱动程序的基本模型，Linux内存管理等相关知识。感兴趣的童鞋可以参考： 1.内核之旅网站，http://www.kerneltravel.net/journal/v/mem.htm 2.Linux设备驱动程序，第十五章 &#160;]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3993/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>pipe和fifo二三事</title>
		<link>http://edsionte.com/techblog/archives/3941</link>
		<comments>http://edsionte.com/techblog/archives/3941#comments</comments>
		<pubDate>Mon, 23 Apr 2012 13:38:31 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[进程间通信]]></category>
		<category><![CDATA[fifo]]></category>
		<category><![CDATA[pipe]]></category>
		<category><![CDATA[有名管道]]></category>
		<category><![CDATA[管道]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3941</guid>
		<description><![CDATA[1.管道是什么？ 管道是一种只存在于内存的特殊文件，没有磁盘文件与之对应。管道是通过虚拟文件系统pipefs而实现的，pipefs与proc、sysfs等特殊文件系统一样，只存在于内存中。另外，管道只能用于半双工通信。 2.pipe()一个管道意味着什么？ pipe()在pipefs文件系统中创建一个新的索引节点，同时创建两个file对象，一个file对象用于读操作，一个file对象用于写操作。pipe()最终将两个file对象对应的文件描述符返回给用户态进程，也就是向pipe()中传递的fd数组。 3.子进程execv()后是否还能继续共享父进程的管道？ 子进程execv()后，不能再继续使用父进程创建的管道，因为子进程当前的上下文已经完全被可执行文件替换。如果要继续使用管道，子进程可以在execv()之前将两个文件描述符重定向到标准输入和输出。 4.描述管道的数据结构与索引节点的关系？ 管道虽然是一种特殊文件，它仍然通过VFS框架中的inode来描述。由于VFS要对所有不同的文件进行抽象描述，因此inode只对所有文件的共性进行描述。inode中的i_pipe字段指向pipe_inode_info结构，该结构用于描述管道的特性。 5.写管道时写入的字节量与管道大小的关系？ 管道缓冲区通常为一个单独的页框，因此大小默认为4096字节。如果两个或者多个进程并发的写入一个管道，那么任何少于4096字节的写操作都是原子的。但是，如果向管道写入大于管道缓冲区大小的数据，则写操作是可以分割的，也就是说多个进程的写操作可以交叉进行，此时应该注意进程的同步。 6.有名管道是什么？ 有名管道是一种设备文件，有对应的磁盘索引节点。因为存在于磁盘上，因此可以被任何进程打开使用。有名管道是一种半双工通信方式。 7.ls &#124; more 的大致执行过程？ 在终端执行ls &#124; more时，shell进程fork()出一个进程A用来执行上述命令。A进程调用pipe()，返回文件描述符fd1和fd2，分别用于读和写管道。进程A两次调用fork()，产生两个子进程。进程A关闭fd1和fd2。 对于第一个子进程，它调用dup2(fd2,1)将写文件描述符重定向到标准输出。接下来调用execv()系统调用执行ls程序，该程序将自己的输出写入管道。 对于第二个子进程，它调用dup2(fd1,0)将读文件描述符重定向到标准输入。接下来调用execv()系统调用执行more程序，该程序从管道中读取数据。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3941/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Linux物理内存管理概述</title>
		<link>http://edsionte.com/techblog/archives/3937</link>
		<comments>http://edsionte.com/techblog/archives/3937#comments</comments>
		<pubDate>Tue, 10 Apr 2012 15:21:34 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[内存管理]]></category>
		<category><![CDATA[per-cpu]]></category>
		<category><![CDATA[slab]]></category>
		<category><![CDATA[伙伴算法]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[物理内存]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3937</guid>
		<description><![CDATA[在内核态申请内存比在用户态申请内存要更为直接，它没有采用用户态那种延迟分配内存技术。内核认为一旦有内核函数申请内存，那么就必须立刻满足该申请内存的请求，并且这个请求一定是正确合理的。相反，对于用户态申请内存的请求，内核总是尽量延后分配物理内存，用户进程总是先获得一个虚拟内存区的使用权，最终通过缺页异常获得一块真正的物理内存。 1.物理内存的内核映射 IA32架构中内核虚拟地址空间只有1GB大小（从3GB到4GB），因此可以直接将1GB大小的物理内存（即常规内存）映射到内核地址空间，但超出1GB大小的物理内存（即高端内存）就不能映射到内核空间。为此，内核采取了下面的方法使得内核可以使用所有的物理内存。 1.高端内存不能全部映射到内核空间，也就是说这些物理内存没有对应的线性地址。不过，内核为每个物理页框都分配了对应的页框描述符，所有的页框描述符都保存在mem_map数组中，因此每个页框描述符的线性地址都是固定存在的。内核此时可以使用alloc_pages()和alloc_page()来分配高端内存，因为这些函数返回页框描述符的线性地址。 2.内核地址空间的后128MB专门用于映射高端内存，否则，没有线性地址的高端内存不能被内核所访问。这些高端内存的内核映射显然是暂时映射的，否则也只能映射128MB的高端内存。当内核需要访问高端内存时就临时在这个区域进行地址映射，使用完毕之后再用来进行其他高端内存的映射。 由于要进行高端内存的内核映射，因此直接能够映射的物理内存大小只有896MB，该值保存在high_memory中。内核地址空间的线性地址区间如下图所示： 从图中可以看出，内核采用了三种机制将高端内存映射到内核空间：永久内核映射，固定映射和vmalloc机制。 2.物理内存管理机制 基于物理内存在内核空间中的映射原理，物理内存的管理方式也有所不同。内核中物理内存的管理机制主要有伙伴算法，slab高速缓存和vmalloc机制。其中伙伴算法和slab高速缓存都在物理内存映射区分配物理内存，而vmalloc机制则在高端内存映射区分配物理内存。 伙伴算法 伙伴算法负责大块连续物理内存的分配和释放，以页框为基本单位。该机制可以避免外部碎片。 per-CPU页框高速缓存 内核经常请求和释放单个页框，该缓存包含预先分配的页框，用于满足本地CPU发出的单一页框请求。 slab缓存 slab缓存负责小块物理内存的分配，并且它也作为高速缓存，主要针对内核中经常分配并释放的对象。 vmalloc机制 vmalloc机制使得内核通过连续的线性地址来访问非连续的物理页框，这样可以最大限度的使用高端物理内存。 3.物理内存的分配 内核发出内存申请的请求时，根据内核函数调用接口将启用不同的内存分配器。 3.1 分区页框分配器 分区页框分配器 (zoned page frame allocator) ,处理对连续页框的内存分配请求。分区页框管理器分为两大部分:前端的管理区分配器和伙伴系统，如下图： 管理区分配器负责搜索一个能满足请求页框块大小的管理区。在每个管理区中,具体的页框分配工作由伙伴系统负责。为了达到更好的系统性能,单个页框的申请工作直接通过per-CPU页框高速缓存完成。 该分配器通过几个函数和宏来请求页框,它们之间的封装关系如下图所示。 这些函数和宏将核心的分配函数__alloc_pages_nodemask()封装,形成满足不同分配需求的分配函数。其中，alloc_pages()系列函数返回物理内存首页框描述符，__get_free_pages()系列函数返回内存的线性地址。 3.2 slab分配器 slab 分配器最初是为了解决物理内存的内部碎片而提出的，它将内核中常用的数据结构看做对象。slab分配器为每一种对象建立高速缓存。内核对该对象的分配和释放均是在这块高速缓存中操作。一种对象的slab分配器结构图如下： 可以看到每种对象的高速缓存是由若干个slab组成，每个slab是由若干个页框组成的。虽然slab分配器可以分配比单个页框更小的内存块，但它所需的所有内存都是通过伙伴算法分配的。 slab高速缓存分专用缓存和通用缓存。专用缓存是对特定的对象，比如为内存描述符创建高速缓存。通用缓存则是针对一般情况，适合分配任意大小的物理内存，其接口即为kmalloc()。 3.3 非连续内存区内存的分配 内核通过vmalloc()来申请非连续的物理内存，若申请成功，该函数返回连续内存区的起始地址，否则，返回NULL。vmalloc()和kmalloc()申请的内存有所不同，kmalloc()所申请内存的线性地址与物理地址都是连续的，而vmalloc()所申请的内存线性地址连续而物理地址则是离散的，两个地址之间通过内核页表进行映射。 vmalloc()的工作方式理解起来很简单： 1.寻找一个新的连续线性地址空间； 2.依次分配一组非连续的页框； 3.为线性地址空间和非连续页框建立映射关系，即修改内核页表； vmalloc()的内存分配原理与用户态的内存分配相似，都是通过连续的虚拟内存来访问离散的物理内存，并且虚拟地址和物理地址之间是通过页表进行连接的，通过这种方式可以有效的使用物理内存。但是应该注意的是，vmalloc()申请物理内存时是立即分配的，因为内核认为这种内存分配请求是正当而且紧急的；相反，用户态有内存请求时，内核总是尽可能的延后，毕竟用户态跟内核态不在一个特权级。 后记：本文将Linux内核中物理内存管理这部分内容进行框架性总结，对内存管理感兴趣的同学可以从伙伴算法，slab和vmalloc()三个角度去了解和学习物理内存管理。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3937/feed</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>基于CFS算法的schedule()源码分析</title>
		<link>http://edsionte.com/techblog/archives/3819</link>
		<comments>http://edsionte.com/techblog/archives/3819#comments</comments>
		<pubDate>Thu, 05 Apr 2012 03:55:25 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[Linux内核]]></category>
		<category><![CDATA[CFS]]></category>
		<category><![CDATA[完全公平调度]]></category>
		<category><![CDATA[调度器]]></category>
		<category><![CDATA[进程调度]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3819</guid>
		<description><![CDATA[内核中的调度算法在不断变化，2.4内核中的调度器是在所有的进程中选择优先级最高的进程，2.6内核前期的调度器是基于O(1)算法的，而2.6.23版本之后的内核采用CFS调度算法，并同时对调度器进行了比较大的改善。内核主要是引入了调度器类来增加调度器的可扩展性。调度器类将各种调度策略模块化，封装了对不同调度策略的具体实现。 内核中对进程调度的方法有两种，其一为周期性调度器（generic scheduler），它对进行进行周期性的调度，以固定的频率运行；其二为主调度器（main scheduler），如果进程要进行睡眠或因为其他原因主动放弃CPU，那么就直接调用主调度器。 内核的主调度器是通过schedule()实现的，该函数的主要工作就是挑选下一个应该被调度的进程next。 该函数首先禁止内核抢占，并且依次获取当前CPU编号cpu、当前CPU对应的运行队列rq、当前进程的切换次数switch_count以及当前进程的描述符prev。 接下来通过update_rq_clock()更新就绪队列上的时钟，接着通过clear_tsk_need_resched()清除当前进程prev的重新调度标志TIF_NEED_RESCHED。 如果当前进程是可中断睡眠状态（可运性状态TASK_RUNNING宏的值为0），但它却收到了某个唤醒它的信号，那么当前进程的标志被更新为TASK_RUNNING,等待再次被调度。否则，通过deactivate_task()将当前进程prev从就绪队列中删除。 这里的deactivate_task()根据调度类的不同实现也有所不同，但这些差异对主调度器是透明的，因为调度器类在各种调度实例和调度器之间起到了连接作用。该函数的核心语句即为： sched_class是进程描述符中描述当前进程所属调度类的字段，通过这个字段回调钩子函数dequeue_task()。 通过put_prev_task()将prev进程重新插入到就绪队列合适的位置中。再通过pick_next_task()在当前的就绪队列中挑选下一个应该被执行的进程next。这两个函数都属于调度器类中的钩子函数，它们的具体实现根据调度实例的不同而不同。 有时候，调度器所选的下一个被执行的进程恰好就是当前进程，那么调度器就不必耗费精力去执行上下文切换，但这种情况不是经常发生的。如果prev和next不是同一个进程，那么先通过sched_info_switch()更新两个进程描述符的相关字段，并且更新可运行队列的相关字段。 接下来调用context_switch()进行prev和next两个进程的上下文切换，该函数由一段汇编代码组成。 切换完毕后，当前的进程就是新选择的进程，它会开始执行。而被切换出去的进程重新运行时会从切换函数的下一条语句开始执行。 根据上述对主调度器函数源码的分析，可以总结出主调度器的主要功能如下： 1.获取当前进程的描述符以及本地CPU的运行队列 2.将当前进程prev放入可运行队列中，等待下一次被重新调度 3.在当前的可运行队列中选取下一个被调度的新进程next 4.从当前进程切换到新进程]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3819/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>内部排序算法小结</title>
		<link>http://edsionte.com/techblog/archives/3857</link>
		<comments>http://edsionte.com/techblog/archives/3857#comments</comments>
		<pubDate>Tue, 20 Mar 2012 10:08:46 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[Linux下C语言]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3857</guid>
		<description><![CDATA[内部排序算法主要分为插入类排序、交换类排序和选择类排序，它们在性能上的差异主要体现在时间复杂度、空间复杂度和稳定性。各种排序算法都会进行元素间的比较和移动，时间复杂度主要由整个排序过程中的比较次数和移动次数决定。空间复杂度体现在除了待排序记录本身所占的空间，排序过程中占用了多少辅助空间。 1.插入类排序 直接插入排序 如果待排序记录之间是顺序排列，此时整个排序过程中元素比较的次数为n-1次，移动次数为0次。如果待排序记录之间是逆序排列，第i趟排序比较次数为i，移动的次数为i+1，其中i的范围是2到n。因此，整个排序过程中元素的比较次数为(n+2)(n-1)/2，移动次数为(n+4)(n-1)/2。 直接插入排序算法的时间复杂度为O(n^2)，空间复杂度为O(1)。从直接插入排序算法的实现可以发现，位置靠后的记录是不可能插入到相同大小的记录之前的，因此直接插入排序算法是稳定的。 折半插入排序 折半插入排序算法与直接插入排序算法相比，每趟的比较次数为log2^i，因此整体的比较次数为O(nlog2^n)。但是折半算法的移动次数较直接插入算法并未改变。 折半算法的时间复杂度仍然是O(n^2)，空间复杂度为O(1)。从算法实现可以看出折半插入排序算法是稳定的。 希尔排序 希尔排序利用了当“待排序记录n较小，序列基本有序”时使用直接插入算法性能较好这一特点而改进。希尔排序算法的时间复杂度为O(n^1.5)，空间复杂度为O(1)，它是不稳定的排序算法。举反例如下：待排序序列为{2，4，1，(2)}，设delta[1]=2，则一趟排序后序列为{1,(2),2,4}。 2.交换类排序 冒泡排序 当待排序记录逆序排列时，每趟冒泡算法进行i次比较和3i次交换（两个元素交换），i的范围是1到n-1。因此总的比较次数为n(n-1)/2，总的移动次数为3n(n-1)/2。冒泡排序算法的时间复杂度为O(n^2)，空间复杂度O为(1)，是一种稳定的排序算法。 快速排序 快速排序最好的情况是每次将待排序的记录都等分为二，此时时间复杂度为O(nlog2^n)。最坏情况下，待排序记录已经排好序，此时总共的比较次数为n(n-1)/2，此时时间复杂度为O(n^2)。 快速排序的平均复杂度为O(nlog2^n)，空间复杂度为O(log2^n)，它是一种不稳定的排序算法。举反例如下：待排序序列为{3,2,(2)},则一趟排序后记录为{(2),2,3}。 3.选择类排序 简单选择排序 无论待排序记录的初始排列如何，简单选择排序总的比较次数总为n(n-1)/2，因为它每趟比较的次数为n-i，i的范围从1到n-1。最好的情况下，简单选择排序算法的移动次数为0，最坏情况下移动的3(n-1)。 简单选择排序算法的时间复杂度为O(n^2)，空间复杂度为O(1)，它是不稳定的排序算法。举反例如下：待排序记录为{6,8,(6),2,7}，一趟排序后记录为{2,8,(6),6,7}，二趟排序后记录为{2,(6),8,6,7}。 堆排序 堆排序的时间复杂度为O(nlog2^n)，空间复杂度为O(1)，它是不稳定的排序算法。 4.归并排序 归并排序的时间复杂度为O(nlog2^n)，空间复杂度为O(n)，它是稳定的排序算法。 5.总结 综上所述，内部排序算法的时间复杂度、空间复杂度和稳定性总结如下表： 1.直接插入排序、折半插入排序、冒泡排序和简单选择排序合称为简单排序，它们的时间复杂度均为O(n^2)，空间复杂度为O(1)。稳定性方面只有简单选择排序是不稳定的排序算法。简单排序只适合n较小的情况。 2.当待排序记录基本有序时，直接插入排序是最佳的排序算法。它常与快速排序和归并排序等结合使用，因为这些算法会将整个打排序记录划分为若干个子记录，这些子记录数量较小而且基本有序。 3.排序性能较好的有希尔排序、快速排序、堆排序和归并排序，除了希尔排序的时间复杂度为O(n^1.5)，其他排序算法均为O(nlogn)。虽然快速排序最坏情况下时间性能下降为O(n^2)，但是就平均性能而言快速排序算法是最好的。堆排序和归并排序的性能比较稳定，当n比较大时，归并排序的性能比堆排序要好，但是它需要的辅助空间却为O(n)。 4.所有的排序算法中，不稳定的算法有简单选择排序、希尔排序、快速排序和堆排序。性能较好的排序算法中只有归并排序是稳定的。排序是按记录的主关键字进行的,此时不用考虑排序方法的稳定性。如果排序是按记录的次关键字进行的, 则应充分考虑排序方法的稳定性。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3857/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>选择类排序</title>
		<link>http://edsionte.com/techblog/archives/3817</link>
		<comments>http://edsionte.com/techblog/archives/3817#comments</comments>
		<pubDate>Sat, 17 Mar 2012 14:07:12 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[Linux下C语言]]></category>
		<category><![CDATA[堆排序]]></category>
		<category><![CDATA[简单选择排序]]></category>
		<category><![CDATA[选择类排序]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3817</guid>
		<description><![CDATA[选择类排序的基本思想是每一趟在n-(i-1)个待排序的记录中选取一个关键字最小的记录作为有序序列中的第i个记录。常用的选择类排序法有简单选择排序和堆排序。 1.简单选择排序 简单选择排序是对选择类排序基本思想的直接实现。在第一趟排序中，从第一个记录开始在待排序的n个记录中选择一个最小的记录，并和第一个记录作交换；在第二趟排序中，从第二个记录开始从待排序的n-1个记录中选择一个最小的记录，并和第二个记录做交换。依次类推，第i趟排序过程即从第i个元素开始在待排序的n-(i-1)个元素中选择一个最小的记录，并和第i个记录作交换。 在具体实现时，外部循环用来控制总的排序趟数，而内部循环用于在剩余待排序记录中挑选一个最小的记录。在每一趟排序中，使用k记录最小记录的索引。 2.堆排序 堆排序将一维的待排序记录按照完全二叉树的形式进行组织，并利用完全二叉树中父节点和孩子节点之间的关系来选择记录，最终达到排序的目的。堆排序只是利用了完全二叉树的特性，待排序记录仍然采用一维数组进行存储，而非采用树的存储结构。 堆排序的算法思想是将待排序的记录r[1,n]看作是一棵完全二叉树，将记录1作为完全二叉树的根节点，记录中的其他元素依次逐层从左至右顺序排序。首先将该完全二叉树调整为大根堆（最终排列成递减序列），也就是创建堆的过程；接着，将根元素从二叉树中移除并作为有序记录集合中的第一个元素，同时再次调整该二叉树让其形成另一个大根堆。不断重复上述过程直到该完全二叉树为空。实现代码如下： 在具体的实现堆排序时，通常是将堆顶元素和堆尾元素进行交换，再次调整剩余待排序的元素形成堆。第i趟排序是将堆顶元素r[1]和堆尾元素r[n-i+1]进行交换，此时序列r中后i个元素是有序的。 堆的创建就是自底向上逐层调整该完全二叉树的子树。对于n个元素的记录来说，先将以第n/2个元素为根的子树调整为大根堆，再将以n/2-1个元素为根的子树调整为大根堆，按照逐层自底向上的顺序，直到根节点。 整个堆排序的核心算法是调整堆，其中r[k,m]是一个以k元素为根的完全二叉树，该算法将此二叉树调整为堆。具体实现时，用r[0]备份当前二叉树的根元素，用j表示i元素的左孩子。算法中使用循环不断将二叉树调整为堆，它首先确定i节点的两个孩子中的较大值，再进一步判断是否需要与根节点进行交换。如果根节点大于i节点较大的那个孩子节点j，整个调整过程结束，因为此时该二叉树就是一个堆。否则将这个较大值的孩子j移到节点i处，接下来以j为根节点继续如上考察它的两个孩子节点的大小。 整个调整堆的过程是由根节点逐步向下筛选的过程。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3817/feed</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>交换类排序</title>
		<link>http://edsionte.com/techblog/archives/3812</link>
		<comments>http://edsionte.com/techblog/archives/3812#comments</comments>
		<pubDate>Fri, 16 Mar 2012 09:06:55 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[算法分析]]></category>
		<category><![CDATA[交换类排序]]></category>
		<category><![CDATA[冒泡排序]]></category>
		<category><![CDATA[快速排序]]></category>
		<category><![CDATA[排序]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3812</guid>
		<description><![CDATA[交换类排序的基本思想是通过交换逆序元素而最终达到所有元素有序，这里的逆序是个广义概念，如果按照降序排序，那么前小后大的相邻元素就为逆序。常见的交换类排序方法有冒泡排序和快速排序。 1.冒泡排序 冒泡排序法的思想比较简单，依次扫描待排序的序列，并且从第一个元素开始比较相邻两个元素之间的大小，如果逆序则交换。假如有一个记录序列r[1,length]，以升序为例，在第i趟排序过程中需要对前length-(i-1)个元素进行逆序交换，最终这些记录中的最大元素将被交换到序列中第length-(i-1)这个位置上。 对于n个元素的序列来说，整个冒泡排序最多进行n-1趟。这里还特别的增加了一个标志位change，只要做了逆序交换change就为真，如果某一趟中没有做任何交换，说明序列已经有序，则change为假，此时整个排序可以提前停止。 2.快速排序 冒泡算法中对相邻元素的交换一次只能消除一个逆序，如果通过交换两个不相邻的元素能一次消除多个逆序，则会增加整个排序的速度。快速排序就是基于这样的目的，在一次交换中可以消除多个逆序。 快速排序的基本思想是在待排序记录中选择一个记录作为标准，将大于该标准的所有元素移到该标准之后，将小于该标准的所有元素移到该标准之前。最终将待排序的记录分成两个子序列，而标准元素插入到两个子序列之间，上述过程为一趟快速排序过程。接下来再对两个子序列进行快速排序，不断的分割子表，直到子表长度不超过1为止，此时所有的记录都是有序的。 根据上述分析，可以通过递归来实现快速排序法。quickPass()用于一趟快速排序，并且返回标准元素的位置pos。 quickPass()用于实现一趟快速排序，一趟快速排序结束的条件是low不小于high。可以看到快速排序中是将low和high两个不相邻位置的元素进行交换。 这里也用到了r[0]，用它来存储标准元素，当一趟快速排序完成后，将标准元素插入low所指位置。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3812/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>插入类排序</title>
		<link>http://edsionte.com/techblog/archives/3797</link>
		<comments>http://edsionte.com/techblog/archives/3797#comments</comments>
		<pubDate>Thu, 15 Mar 2012 13:27:36 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[算法分析]]></category>
		<category><![CDATA[希尔排序]]></category>
		<category><![CDATA[折半排序]]></category>
		<category><![CDATA[排序]]></category>
		<category><![CDATA[插入排序]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3797</guid>
		<description><![CDATA[插入类排序的基本思路是在一个已经排好序的子记录上，每一步将下一个待排序的记录插入到已经排好序的记录子集中，直到将所有待排序记录全部插入为止。 1.直接插入排序 直接插入排序是最基本的插入排序算法，它的一趟操作是将第i个记录插入到前面i-1个已经排好序的记录中，在查找记录i的插入位置时，也在进行元素的移动。假设有一个待排序队列r[1,length]，则整个排序过程需要n-1次趟。直接插入算法的实现如下： 具体实现时，用一维数组来存储待排序的序列，其中0号元素备份待插入的记录。 2.折半插入排序 折半插入排序法与直接插入法类似，区别在于确定元素i插入的位置时利用折半查找法。每一趟排序的过程是先用折半查找法确定插入位置，再逐个进行元素的移动。 与直接插入法类似，数组r中的0号元素也备份了待插入的元素i。当确定了记录i的位置时，此时存在low=high+1这样的关系，接下来将low到i-1之间的元素都后移一位，最终记录i刚好插入空出来的位置中。 3.希尔排序 整个希尔排序的过程由若干次希尔插入组成，具体次数由增量数组delta中的元素个数n确定。在每一次的希尔插入算法中，将待排序的记录序列分成d个稀疏子序列，每个稀疏子序列中元素之间的间隔正好为d。希尔插入算法就是将每一个子序列都按照直接插入法排列成有序，从而使得整个序列基本有序。上述过程会重复n次，就是希尔排序算法的整个过程。 第i趟希尔排序中，每个稀疏子序列中元素的间隔由delta[i]决定。但希尔算法的最后一趟排序，元素的间隔必需是1。因为最后一次希尔排序就相当于直接插入排序，但是此时整个记录序列已经几乎有序，因此移动的次数会大大减少。 虽然希尔插入算法中需要依次将d个子序列排成有序，但是实际的实现过程却从第一个子序列的第二个记录(d+1)开始依次遍历整个序列，因为每个序列中的元素都是由间隔d控制的，因此就相当于每个子序列各自排序。内部的for循环相当于对每个子序列进行直接插入排序，从当前的记录i(保存在r[0]中)开始，依次扫描当前子序列之前的元素(每个元素的间隔为d，因此每次循环j都减少d)以确保插入何时的位置。 本文所涉及的排序算法源码可以在这里下载。]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3797/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>伙伴算法的实现-释放页框</title>
		<link>http://edsionte.com/techblog/archives/3785</link>
		<comments>http://edsionte.com/techblog/archives/3785#comments</comments>
		<pubDate>Mon, 12 Mar 2012 09:32:59 +0000</pubDate>
		<dc:creator>edsionte</dc:creator>
				<category><![CDATA[内存管理]]></category>
		<category><![CDATA[物理内存管理]]></category>
		<category><![CDATA[页框分配]]></category>
		<category><![CDATA[页框释放]]></category>

		<guid isPermaLink="false">http://edsionte.com/techblog/?p=3785</guid>
		<description><![CDATA[与页框分配函数的整个实现过程相比，页框释放函数的实现要简单的多。常用的页框分配函数接口有 __free_pages()和__free_page()，很明显后者是前者的特殊情况。 #define __free_page(page) __free_pages((page), 0) void __free_pages(struct page *page, unsigned int order) { if (put_page_testzero(page)) { if (order == 0) free_hot_cold_page(page, 0); else __free_pages_ok(page, order); } } 释放页框函数在一开始也兵分两路：如果分配阶为0，那么直接通过per-CPU机制来释放单一页框；否则通过__free_pages_ok()释放所申请的页框块。 1.__free_pages_ok() 该函数内部经过一些检查，这些检查是确保稍候对页框的释放是安全的，最终会调用free_one_page()，而它内部又封装了__free_one_page()。内核中函数之间的这种多次封装是很常见的，上层函数通过这种封装可以屏蔽某些参数，而底层的被封装的函数也可以被多种情况所引用。 static void __free_pages_ok(struct page *page, unsigned int order) { ………… free_one_page(page_zone(page), page, order, get_pageblock_migratetype(page)); ………… } static void free_one_page(struct zone *zone, struct page *page, [...]]]></description>
		<wfw:commentRss>http://edsionte.com/techblog/archives/3785/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

