Linux内存管理实践-使用fault()实现内存映射

15 5 月, 2012 by edsionte 1 comment »

内核态与用户态进行数据交互通常是这样一种模型:内核利用自身的特权通过特定的服务程序采集、接收和处理数据;接着,用户态程序和内核服务程序进行数据交互,或接收内核态的数据,或向内核态写入数据。通过传统的那些对文件操作的系统调用就可以完成这样的工作,但是我们有时候需要通过访问用户空间的内存来直接读取内核数据,因为这样可以免去数据在内核态与用户态之间拷贝所花费的时间。

本文基于以上背景,以Linux字符设备驱动为基础,通过内存映射将内核中的一部分虚拟内存直接映射到用户空间,使得用户在访问内存时等同于直接访问内核空间,从而直接获取内核空间的数据。

1.实现原理

不管进程是在用户空间访问数据还是在内核空间访问数据,它所面临的都是虚拟地址。由于Linux对分段机制进行了特殊处理,因此这里的虚拟地址就等同于线性地址。按照一开始我们提出的要求,进程通过访问用户虚拟地址A来达到直接访问内核虚拟地址B中所存储数据的目的,这里的地址A和B必然不相同。那么,如何通过不同的虚拟地址来访问相同的数据?我们可以将虚拟地址A和B都映射到同一块物理内存,就可以实现内核空间和用户空间之间的数据共享。

示意图如下:

虚拟内存和物理内存之间如何联系?当然是通过页表了。我们在内核空间提前分配好缓冲区,并且向该缓冲区写入数据,此时内核会自动将该缓冲区对应的内核虚拟地址与实际的某一快物理内存进行关联,并将它们的映射关系保存在内核页表中。当在内核空间分配内存时时,上述工作自动被完成,比如通过kmalloc()分配内存时。

一旦在内核空间中分配了内存,随之就确定了物理内存。现在我们需要做的是将用户虚拟地址与物理内存进行关联,也就是说我们要将这个映射关系写入进程的用户页表。整个关联的过程是内核缺页异常处理程序完成的,这个处理过程比较复杂。我们要在内核中实现的并不是缺页异常处理程序,因为内核已经实现的很完美,而只需完成其中的一小部门。具体如何实现下问会详细说明。

2.用户态程序的实现

在本文所描述的内存管理试验,用户态程序首先通过open()打开字符设备文件mapdrv,该系统调用执行成功时返回文件描述符;通过mmap()将该设备文件映射到当前进程的用户空间中,该系统调用执行成功时返回指向映射区域的指针;最后通过该指针打印数据。用户态程序的实现如下所示:

#define LEN (10 * 4096)

int main(void)
{
	int fd, ret = 0;
	char *vadr;
	int i;

	if ((fd = open("/dev/mapdrv_k", O_RDWR)) < 0) {
		perror("open");
		ret = -1;
		goto fail;
	}
	vadr = mmap(0, LEN, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, fd, 0);

	if (vadr == MAP_FAILED) {
		perror("mmap");
		ret = -1;
		goto fail_close;
	}

	printf("%s\n", vadr);

	if (-1 == munmap(vadr, LEN))
		ret = -1;
fail_close:
	close(fd);
fail:
	exit(ret);
}

用户态程序的实现并不复杂,因为它的主要作用是对内核模块程序的测试。由于用户态程序是对特定的字符设备文件mapdrv进行操作,所以程序中所使用的系统调用将会调用file_operations结构中对应的钩子函数。比如mmap系统调用在执行时会调用mapdrv设备驱动中的mmap钩子函数,虽然两者同名,但是mmap钩子函数所实现的功能只是mmap系统调用执行过程中的一部门,该钩子函数是file_operations结构中的成员。两者的函数原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int (*mmap) (struct file *, struct vm_area_struct *);

如何实例化open和mmap等钩子函数便是整个内核模块程序实现的关键。

3.字符设备驱动程序的实现

整个内核模块程序是以字符设备驱动为基础进行实现的。该程序模块加载函数与一般字符设备驱动程序完成的工作一致:

1.申请设备号;

2.为描述字符设备的数据结构分配空间,并进行初始化;

3.将该字符设备注册到内核中;

在本文所描述的实验中,模块加载函数除了完成上述功能,还要完成以下功能:

	kmalloc_area = kmalloc(MAPLEN, GFP_KERNEL);
	if (!kmalloc_area)
		goto fail4;

	for (page = virt_to_page(kmalloc_area);
			page < virt_to_page(kmalloc_area + MAPLEN); page++) {
		SetPageReserved(page);
	}

首先,通过kmalloc()分配一块内存用于在内核空间保存数据;通过SetPageReserved()将缓存数据的页面常驻内存,防止被换出到磁盘;将一段字符串拷贝到这片内存中。完成初始化函数后,字符设备驱动中最重要的就是实现file_operations结构中的钩子函数。在本文所述的实验中,我们只需实现三个钩子函数。

static struct file_operations mapdrv_fops = {
	.owner = THIS_MODULE,
	.mmap = mapdrv_mmap,
	.open = mapdrv_open,
	.release = mapdrv_release,
};

int mapdrv_open(struct inode *inode, struct file *file)
{
	struct mapdrv *md;

	printk("device is opened..\n");
	md = container_of(inode->i_cdev, struct mapdrv, mapdev);
	atomic_inc(&md->usage);
	return 0;
}

int mapdrv_release(struct inode *inode, struct file *file)
{
        struct mapdrv* md;

        printk("device is closed..\n");
        md = container_of(inode->i_cdev, struct mapdrv, mapdev);
        atomic_dec(&md->usage);
        return 0;
}

可以看到,open和release钩子函数的实现十分简单,只是打印相应语句以及更新设备的引用计数。事实上,我们不实现这两个钩子函数对整个驱动程序也没有任何影响。因为他们分别在open和close系统调用执行的过程中被调用,而这两个系统调用已经完成了打开文件和关闭文件的所有工作。因此,open和release钩子函数只是打印一些日志信息,方便用户查看。因此,同名的钩子函数和系统调用并不是等价的关系。

4.通过fault()实现内存映射

mmap系统调用是将本地文件映射到进程的用户空间,如果执行成功,进程的地址空间中会新增一块虚拟内存区域(但并不是每次调用mmap()都会增加一个vma,因为可能会出现内存区域之间的合并)

mmap钩子函数的回调只是整个系统调用执行过程中的一部分,这个钩子函数完成的主要功能是将新增的vma中的操作集进行实例化。具体实现代码如下:

int mapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
	unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

	unsigned long size = vma->vm_end - vma->vm_start;
	if (offset & ~PAGE_MASK) {
		printk("offset not aligned: %ld\n", offset);
		return -ENXIO;
	}
	if (size > MAPLEN) {
		printk("size too big\n");
		return -ENXIO;
	}

	if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {
		printk("writeable mappings must be shared, rejecting\n");
		return -EINVAL;
	}

	vma->vm_flags |= VM_LOCKED;
	if (offset == 0) {
		vma->vm_ops = &map_vm_ops;
		map_vopen(vma);
	} else {
		printk("offset out of range\n");
		return -ENXIO;
	}
	return 0;
}

首先,offset中保存映射的首页在文件中的偏移量,该偏移量必须是页大小的整数倍,否则将不能进行映射。这一点在实现上将offset与PAGE_MASK宏进行位运算即可判断。接着,判断映射区域的长度是否超出了本实验中预设的长度大小。

如果上述两个条件都合法,那么接下来将进行最为重要的操作,将vma中的操作集进行实例化。vma的操作集就是专门对所属vma进行操作的钩子函数集合,内核中通过vm_operations_struct结构对其描述:

struct vm_operations_struct {
        void (*open)(struct vm_area_struct * area);
        void (*close)(struct vm_area_struct * area);
        int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
        ……
}

参考上述代码,具体的实例化操作就是定义该类型的变量map_vm_ops,实现所需钩子函数,并将该变量与vm中的操作集字段进行挂接。

我们这里用到的主要有以下三个钩子函数:

open:当指定的vma加入到一个地址空间时,该函数被调用。

close:当指定的vma从地址空间删除时,该函数被调用。

fault:当要访问的页不再物理内存时,该函数被缺页处理程序调用。

这三个钩子函数的实现代码如下:

static struct vm_operations_struct map_vm_ops = {
	.open = map_vopen,
	.close = map_vclose,
	.fault = map_fault,
};

void map_vopen(struct vm_area_struct *vma)
{
	printk("mapping vma is opened..\n");
}

void map_vclose(struct vm_area_struct *vma)
{
	printk("mapping vma is closed..\n");
}

可以看到,vma的open和close两个钩子函数没有做什么具体工作,因为打开和关闭一个vma的工作全部由内核负责,但是在mmap钩子函数中我们必须显示的调用map_open()。

这里我们重点说明falut钩子函数的实现。当用户要访问vma中的页,而该页又不在内存时,将发生缺页异常,fault钩子函数会在整个缺页处理程序中被调用。整个过程大致如下:

1.找到缺页地址所在的vma。

2.如果有必要分配各级页表项。

3.如果页表项对应的物理页面不存在,则回调当前vma中的fault钩子函数,它返回物理页面描述符。

4.将物理页面地址填充到相应页表项中。

5.完毕。

可以看到,fault钩子函数所实现的主要功能就是返回所需的物理内存描述符。

根据本文第一部分所描述实现原理,我们通过kmalloc()分配一块虚拟内存,可以通过virt_to_page()获得该虚拟内存对应的物理页框描述符,最后将该物理页框描述符返回到缺页异常处理程序中。至于用户页表的更新,那是缺页异常处理程序负责的事情,我们不必理会。

int map_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
	struct page *page = virt_to_page(kmalloc_area);

	get_page(page);
	vmf->page = page;
	printk("the requiring page is returned..\n");

	return 0;
}

通过上述实现过程,我们就将用户虚拟地址A和内核虚拟地址B映射到了同一的物理内存上,从而实现进程访问用户地址时直接获得内核数据的功能。

本实验涉及的知识点比较多,比如字符设备驱动程序的基本模型,Linux内存管理等相关知识。感兴趣的童鞋可以参考:

1.内核之旅网站,http://www.kerneltravel.net/journal/v/mem.htm

2.Linux设备驱动程序,第十五章

 

pipe和fifo二三事

23 4 月, 2012 by edsionte 1 comment »

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 | more 的大致执行过程?

在终端执行ls | more时,shell进程fork()出一个进程A用来执行上述命令。A进程调用pipe(),返回文件描述符fd1和fd2,分别用于读和写管道。进程A两次调用fork(),产生两个子进程。进程A关闭fd1和fd2。

对于第一个子进程,它调用dup2(fd2,1)将写文件描述符重定向到标准输出。接下来调用execv()系统调用执行ls程序,该程序将自己的输出写入管道。

对于第二个子进程,它调用dup2(fd1,0)将读文件描述符重定向到标准输入。接下来调用execv()系统调用执行more程序,该程序从管道中读取数据。

Linux物理内存管理概述

10 4 月, 2012 by edsionte 9 comments »

在内核态申请内存比在用户态申请内存要更为直接,它没有采用用户态那种延迟分配内存技术。内核认为一旦有内核函数申请内存,那么就必须立刻满足该申请内存的请求,并且这个请求一定是正确合理的。相反,对于用户态申请内存的请求,内核总是尽量延后分配物理内存,用户进程总是先获得一个虚拟内存区的使用权,最终通过缺页异常获得一块真正的物理内存。

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()三个角度去了解和学习物理内存管理。

基于CFS算法的schedule()源码分析

5 4 月, 2012 by edsionte 无评论 »

内核中的调度算法在不断变化,2.4内核中的调度器是在所有的进程中选择优先级最高的进程,2.6内核前期的调度器是基于O(1)算法的,而2.6.23版本之后的内核采用CFS调度算法,并同时对调度器进行了比较大的改善。内核主要是引入了调度器类来增加调度器的可扩展性。调度器类将各种调度策略模块化,封装了对不同调度策略的具体实现。

内核中对进程调度的方法有两种,其一为周期性调度器(generic scheduler),它对进行进行周期性的调度,以固定的频率运行;其二为主调度器(main scheduler),如果进程要进行睡眠或因为其他原因主动放弃CPU,那么就直接调用主调度器。

内核的主调度器是通过schedule()实现的,该函数的主要工作就是挑选下一个应该被调度的进程next。
该函数首先禁止内核抢占,并且依次获取当前CPU编号cpu、当前CPU对应的运行队列rq、当前进程的切换次数switch_count以及当前进程的描述符prev。

asmlinkage void __sched schedule(void)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq *rq;
	int cpu;

need_resched:
	preempt_disable();
	cpu = smp_processor_id();
	rq = cpu_rq(cpu);
	rcu_sched_qs(cpu);
	prev = rq->curr;
	switch_count = &prev->nivcsw;

	release_kernel_lock(prev);
need_resched_nonpreemptible:

	schedule_debug(prev);

	if (sched_feat(HRTICK))
		hrtick_clear(rq);

接下来通过update_rq_clock()更新就绪队列上的时钟,接着通过clear_tsk_need_resched()清除当前进程prev的重新调度标志TIF_NEED_RESCHED。

	raw_spin_lock_irq(&rq->lock);
	update_rq_clock(rq);
	clear_tsk_need_resched(prev);

如果当前进程是可中断睡眠状态(可运性状态TASK_RUNNING宏的值为0),但它却收到了某个唤醒它的信号,那么当前进程的标志被更新为TASK_RUNNING,等待再次被调度。否则,通过deactivate_task()将当前进程prev从就绪队列中删除。

这里的deactivate_task()根据调度类的不同实现也有所不同,但这些差异对主调度器是透明的,因为调度器类在各种调度实例和调度器之间起到了连接作用。该函数的核心语句即为:

p->sched_class->dequeue_task(rq, p, sleep);

sched_class是进程描述符中描述当前进程所属调度类的字段,通过这个字段回调钩子函数dequeue_task()。

	if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
		if (unlikely(signal_pending_state(prev->state, prev)))
			prev->state = TASK_RUNNING;
		else
			deactivate_task(rq, prev, 1);
		switch_count = &prev->nvcsw;
	}

	pre_schedule(rq, prev);

	if (unlikely(!rq->nr_running))
		idle_balance(cpu, rq);

通过put_prev_task()将prev进程重新插入到就绪队列合适的位置中。再通过pick_next_task()在当前的就绪队列中挑选下一个应该被执行的进程next。这两个函数都属于调度器类中的钩子函数,它们的具体实现根据调度实例的不同而不同。

	put_prev_task(rq, prev);
	next = pick_next_task(rq);

有时候,调度器所选的下一个被执行的进程恰好就是当前进程,那么调度器就不必耗费精力去执行上下文切换,但这种情况不是经常发生的。如果prev和next不是同一个进程,那么先通过sched_info_switch()更新两个进程描述符的相关字段,并且更新可运行队列的相关字段。

接下来调用context_switch()进行prev和next两个进程的上下文切换,该函数由一段汇编代码组成。

	if (likely(prev != next)) {
		sched_info_switch(prev, next);
		perf_event_task_sched_out(prev, next);

		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;

		context_switch(rq, prev, next); /* unlocks the rq */
		/*
		 * the context switch might have flipped the stack from under
		 * us, hence refresh the local variables.
		 */
		cpu = smp_processor_id();
		rq = cpu_rq(cpu);
	} else
		raw_spin_unlock_irq(&rq->lock);

切换完毕后,当前的进程就是新选择的进程,它会开始执行。而被切换出去的进程重新运行时会从切换函数的下一条语句开始执行。

	post_schedule(rq);

	if (unlikely(reacquire_kernel_lock(current) < 0)) { 		prev = rq->curr;
		switch_count = &prev->nivcsw;
		goto need_resched_nonpreemptible;
	}

	preempt_enable_no_resched();
	if (need_resched())
		goto need_resched;
}

根据上述对主调度器函数源码的分析,可以总结出主调度器的主要功能如下:

1.获取当前进程的描述符以及本地CPU的运行队列

2.将当前进程prev放入可运行队列中,等待下一次被重新调度

3.在当前的可运行队列中选取下一个被调度的新进程next

4.从当前进程切换到新进程

内部排序算法小结

20 3 月, 2012 by edsionte 无评论 »

内部排序算法主要分为插入类排序、交换类排序和选择类排序,它们在性能上的差异主要体现在时间复杂度、空间复杂度和稳定性。各种排序算法都会进行元素间的比较和移动,时间复杂度主要由整个排序过程中的比较次数和移动次数决定。空间复杂度体现在除了待排序记录本身所占的空间,排序过程中占用了多少辅助空间。

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.所有的排序算法中,不稳定的算法有简单选择排序、希尔排序、快速排序和堆排序。性能较好的排序算法中只有归并排序是稳定的。排序是按记录的主关键字进行的,此时不用考虑排序方法的稳定性。如果排序是按记录的次关键字进行的, 则应充分考虑排序方法的稳定性。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key