From 98dc9e79490514a294a03f8fa892d84ac111214a Mon Sep 17 00:00:00 2001 From: gatieme Date: Sat, 3 Sep 2016 21:45:05 +0800 Subject: [PATCH] ... --- .../04-buddy/01-buddy_system/README.md | 177 ++++- .../03-vmarea/01-vmarea_space/README.md | 700 +++++++++--------- study/kernel/04-net/README.md | 271 +++++++ 3 files changed, 798 insertions(+), 350 deletions(-) create mode 100644 study/kernel/04-net/README.md diff --git a/study/kernel/02-memory/04-buddy/01-buddy_system/README.md b/study/kernel/02-memory/04-buddy/01-buddy_system/README.md index 5e09703..306c8a6 100644 --- a/study/kernel/02-memory/04-buddy/01-buddy_system/README.md +++ b/study/kernel/02-memory/04-buddy/01-buddy_system/README.md @@ -8,6 +8,182 @@ + + + +#1 前景回顾 +------- + + +##1.1 Linux内存管理的层次结构 +------- + + +Linux把物理内存划分为三个层次来管理 + +| 层次 | 描述 | +|:----:|:----:| +| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 | +| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 | +| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 | + +为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点 + + +* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构. + +* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射. + + +* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号. + + +##1.2 内存结点pg_data_t +------- + + +在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。 + + +* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理 + +* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node) + +可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 而UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关. + +**NODE_DATA(node_id)查找编号node_id的结点pg_data_t信息** 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA) + +```cpp +extern struct pglist_data *node_data[]; +#define NODE_DATA(nid) (node_data[(nid)]) +``` + + +在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858) + + +```cpp +extern struct pglist_data contig_page_data; +#define NODE_DATA(nid) (&contig_page_data) + +``` + +##1.2 物理内存区域 +------- + +因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束. + +* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址 + +* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分 + + +因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域 + +对于x86_32的机器,管理区(内存区域)类型如下分布 + + +| 类型 | 区域 | +| :------- | ----: | +| ZONE_DMA | 0~15MB | +| ZONE_NORMAL | 16MB~895MB | +| ZONE_HIGHMEM | 896MB~物理内存结束 | + + +##1.3 物理页帧 +------- + +内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位. + +页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存. + + + 内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页. + +出于节省内存的考虑,struct page中使用了大量的联合体union. + + +`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691) + + +UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量 + + +#1.4 启动过程中的内存初始化 +------- + + + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + + +因此我们可以把linux内核的内存管理分三个阶段。 + +| 阶段 | 起点 | 终点 | 描述 | +|:-----:|:-----:|:-----:| +| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | +| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | +| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | + + + +**系统启动过程中的内存管理** + + +首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479) + +其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 + + +```cpp +asmlinkage __visible void __init start_kernel(void) +{ + + /* 设置特定架构的信息 + * 同时初始化memblock */ + setup_arch(&command_line); + mm_init_cpumask(&init_mm); + + setup_per_cpu_areas(); + + /* 初始化内存结点和内段区域 */ + build_all_zonelists(NULL, NULL); + page_alloc_init(); + + + /* + * These use large bootmem allocations and must precede + * mem_init(); + * kmem_cache_init(); + */ + mm_init(); + + kmem_cache_init_late(); + + kmemleak_init(); + setup_per_cpu_pageset(); + + rest_init(); +} +``` + + +| 函数 | 功能 | +|:----:|:----:| +| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 | +| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 | +| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非SMP系统中这是一个空操作 | +| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 | +| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 | +| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 | +| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 | +| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | + + +##1.5 伙伴系统 +------- + 在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. @@ -24,7 +200,6 @@ Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该 - #2 伙伴系统的结构 ------- diff --git a/study/kernel/03-vmarea/01-vmarea_space/README.md b/study/kernel/03-vmarea/01-vmarea_space/README.md index 897734f..90ba7ec 100644 --- a/study/kernel/03-vmarea/01-vmarea_space/README.md +++ b/study/kernel/03-vmarea/01-vmarea_space/README.md @@ -1,349 +1,351 @@ -进程虚拟地址空间 -======= - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | - - - -#1 虚拟地址空间概述 -------- - -用户层进程的虚拟地址空间是Linux的一个重要抽象 : 它向每个运行进程提供了同样的系统视图, 这使得多个进程可以同时运行, 而不会干扰到其他进程内存中的内容. 此外, 它容许使用各种高级的程序设计技术,如内存映射 - -从今天开始, 我将讨论内核是如何实现这些概念的. 这同样需要考察可用物理内存中的页帧与所有的进程虚拟地址空间中的页之间的关联 : **逆向映射(reverse -mapping)* ***技术有助于从虚拟内存页跟踪到对应的物理内存页, 而**缺页处理(page fault handling)**则允许从块设备按需读取数据填充虚拟地址空间 - - -* 每个应用程序都有自身的地址空间,与所有其他应用程序分隔开 - -* 通常在巨大的线性地址空间中,只有很少的段可用于各个用户空间进程,这些段彼此有一定的距离。内核需要一些数据结构,来有效地管理这些(随机)分布的段。 - -* 地址空间只有极小的一部分与物理内存页直接关联。不经常使用的部分,则仅当必要时与页帧关联. - -* 内核信任自身,但无法信任用户进程。因此,各个操作用户地址空间的操作都伴随有各种检查,以确保程序的权限不会超出应有的限制,进而危及系统的稳定性和安全性. - -* fork-exec模型在UNIX操作系统下用于产生新进程. 如果实现得较为粗劣, 该模型的功能并不强大。因此内核必须借助于一些技巧,来尽可能高效地管理用户地址空间 - -#2 进程虚拟地址空间 -------- - -##2.1 进程虚拟地址空间 -------- - -各个进程的虚拟地址空间起始于地址0, 延伸到TASK_SIZE - 1, 其上是内核地址空间。 在IA-32系统上地址空间的范围可达$2^{32} = 4GB$, 总的地址空间通常按3:1比例划分,我们在下文中将关注该划分. 内核分配了1GB, 而各个用户空间进程可用的部分为3GB. 其他的划分比例也是可能的, 但正 -如前文的讨论, 只能在非常特定的配置和某些工作负荷下才有用. - -与系统完整性相关的非常重要的一方面是, 用户程序只能访问整个地址空间的下半部分,不能访问内核部分. 如果没有预先达成"协议", 用户进程也不可能操作另一个进程的地址空间,因为后者的地址空间对前者不可见. - -无论当前哪个用户进程处于活动状态, 虚拟地址空间内核部分的内容总是同样的. 取决于具体的硬件, 这可能是通过操作各用户进程的页表, 使得虚拟地址空间的上半部看上去总是相同的. 也可能是指示处理器为内核提供一个独立的地址空间, 映射在各个用户地址空间之上. 读者可以回想一下图1-3, 其中给出了相关的图示. - -虚拟地址空间由许多不同长度的段组成, 用于不同的目的, 必须分别处理. - -例如在大多数情况下, 不允许修改text段, 但必须可以执行其内容. 另一方面,必须可以修改映射到地址空间中的文本文件 -内容,而不能允许执行其内容. 因为这没有意义,文件的内容只是数据,并非机器代码. - -##2.2 进程地址空间的布局 -------- - -虚拟地址空间中包含了若干区域. 其分布方式是特定于体系结构的,但所有方法都有下列共同成分. - -* 当前运行代码的二进制代码. 该代码通常称之为text,所处的虚拟内存区域称之为代码段(text section). - -* 可执行文件的已初始化全局变量的内存映射, 称为数据段(data section). - -* 包括未初始化全局变量(也就是bss段的零页)的内存映射, 页面中的信息全部为0值, 所以可用于映射bss段等目的. - -* 用于保存局部变量和实现函数/过程调用栈(不要和进程内核栈混淆, 进程的内核栈独立存在并由内核维护)的零页内存映射 - - -* 程序使用的动态库的代码, 诸如C库或动态连接程序等共享库的代码段, 数据段和bss段. - -* 存储动态产生的数据的堆 - -* 环境变量和命令行参数的段. - -* 将文件内容映射到虚拟地址空间中的内存映射 - - -进程的虚拟地址空间中的任何有效地址都只能位于唯一的区域, 这些内存区域不能相互覆盖. 可以看到, 在执行的进程中, 每个不同的内存片段都对应一个独立的内存区域 : 栈, 对象代码, 全局变量, 被映射的文件等. - - -一个进程的虚拟地址空间主要由两个数据结来描述 - -* 一个是最高层次的 : `mm_struct` - -* 一个是较高层次的 : `vm_area_structs` - -最高层次的`mm_struct`结构描述了一个进程的整个虚拟地址空间。较高层次的结构`vm_area_truct`描述了虚拟地址空间的一个区间(简称虚拟区). - -每个进程只有一个mm_struct结构, 在每个进程的`task_struct`结构中, 有一个指向该进程的结构, 参见task_struct的定义[include/linux/sched.h?v=4.7, line 1457](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.7#L1457) - - -```cpp -// http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.7#L1457 -struct task_struct -{ - struct mm_struct *mm, *active_mm; -}; -``` -可以说, `mm_struct`结构是对整个用户空间的描述. `mm_strcut`用来描述一个进程的虚拟地址空间. - - -##2.3 内存描述符`mm_struct` -------- - -内核使用内存描述符结构体`struct mm_struct`表示进程的地址空间, 该结构体包含了和进程地址空间相关的全部信息 - - -系统中的各个进程都具有一个`struct mm_struct`的实例,可以通过`task_struct`访问. 这个实例保存了进程的内存管理信息, 定义在[`include/linux/mm_types.h?v=4.7, line 395`](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L395) - - - -**可执行代码**占用的虚拟地址空间区域, 其开始和结束分别通过 `start_code`和`end_code`标记. - - -类似地, `start_data`和`end_data`标记了包含**已初始化数据的区域**. 请注意, 在`ELF`二进制文件映射到地址空间中之后,这些区域的长度不再改变. - -**堆**的起始地址保存在`start_brk`, `brk`表示堆区域当前的结束地址. 尽管堆的起始地址在进程生命周期中是不变的, 但堆的长度会发生变化,因而`brk`的值也会变. - -**参数列表**和**环境变量**的位置分别由`arg_start`和 `arg_end`、`env_start`和`env_end`描述. 两个区域 -都位于栈中最高的区域. - -[`mmap_base`](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L404)表示虚拟地址空间中用于内存映射的起始地址, 可调用`get_unmapped_area`在`mmap` -区域中为新映射找到适当的位置. - -`task_size`, 顾名思义, 存储了对应进程的地址空间长度. 对本机应用程序来说, 该值通常是`TASK_SIZE`. 但64位体系结构与前辈处理器通常是二进制兼容的. 如果在64位计算机上执行32位二进制代码, 则`task_size`描述了该二进制代码实际可见的地址空间长度. - -各个体系结构可以通过几个配置选项影响虚拟地址空间的布局。 - -* 如果体系结构想要在不同`mmap`区域布局之间作出选择, 则需要设置`HAVE_ARCH_PICK_MMAP_LAYOUT`, 并提供`arch_pick_mmap_layout`函数. - -* 在创建新的内存映射时, 除非用户指定了具体的地址, 否则内核需要找到一个适当的位置. 如果体系结构自身想要选择合适的位置,则必须设置预处理器符号`HAVE_ARCH_UNMAPPED_AREA`, 并相应地定义`arch_get_unmapped_area`函数。 - -* 在寻找新的内存映射低端内存位置时, 通常从较低的内存位置开始, 逐渐向较高的内存地址搜索. 内核提供了默认的函数`arch_get_unmapped_area_topdown`用于搜索, 但如果某个体系结构想要提供专门的实现, 则需要设置预处理器符号`HAVE_ARCH_GET_UNMAPPED_AREA`. - -* 通常, 栈自顶向下增长. 具有不同处理方式的体系结构需要设置配置选项`CONFIG_STACK_GROWSUP` - -最后, 我们需要考虑进程标志`PF_RANDOMIZE`. 如果设置了该标志, 则内核不会为栈和内存映射的起点选择固定位置,而是在每次新进程启动时随机改变这些值的设置. 这引入了一些复杂性, 例如, 使得攻击因缓冲区溢出导致的安全漏洞更加困难. 如果攻击者无法依靠固定地址找到栈,那么想要构 -建恶意代码, 通过缓冲器溢出获得栈内存区域的访问权, 而后恶意操纵栈的内容,将会困难得多. - - -下图说明了前述的各个部分在大多数体系结构的虚拟地址空间中的分布情况. - -`text`段如何映射到虚拟地址空间中由ELF标准确定(有关该二进制格式的更多信息,请参见), 每个体系结构都指定了一个特定的起始地址 : IA-32系统起始于0x08048000, 在text段的起始地址与最低的可用地址之间有大约128 MiB的间距,用于捕获NULL指针. 其他体系结构也有类似的缺口 : `UltraSparc`计算机使用0x100000000作为text段的起始点, 而AMD64使用0x0000000000400000. 堆紧接着text段开始, 向上增长. 栈起始于STACK_TOP, 如果设置了 `PF_RANDOMIZE`, 则起始点会减少一个小的随机量. 每个体系结构都必须定义`STACK_TOP`, 大多数都设置为 `TASK_SIZE`, 即用户地址空间中最高的可用地址. 进程 -的参数列表和环境变量都是栈的初始数据. - -用于内存映射的区域起始于`mm_struct->mmap_base`, 通常设置为`TASK_UNMAPPED_BASE`, 每个体系结构都需要定义. 几乎所有的情况下, 其值都是`TASK_SIZE/3`. 要注意,如果使用内核的默认配置, 则`mmap`区域的起始点不是随机的. - - -![进程的线性地址空间的组成]() - - - -如果计算机提供了巨大的虚拟地址空间, 那么使用上述的地址空间布局会工作得非常好. 但在32位计算机上可能会出现问题. 考虑IA-32的情况 : 虚拟地址空间从0到0xC0000000 , 每个用户进程有3GB可用. `TASK_UNMAPPED_BASE`起始于0x4000000, 即1GB处. 糟糕的是, 这意味着堆只有1GB -空间可供使用, 继续增长则会进入到`mmap`区域, 这显然不是我们想要的. - -问题在于, 内存映射区域位于虚拟地址空间的中间. 这也是在内核版本2.6.7开发期间为IA-32计算机引入一个新的虚拟地址空间布局的原因(经典布局仍然可以使用). - -![mmap区域自顶向下扩展时,IA-32计算机上虚拟地址空间的布局]() - - -其想法在于使用固定值限制栈的最大长度. 由于栈是有界的, 因此安置内存映射的区域可以在栈末端的下方立即开始. 与经典方法相反, 该区域现在是自顶向下扩展. 由于堆仍然位于虚拟地址空间中较低的区域并向上增长, 因此`mmap`区域和堆可以相对扩展, 直至耗尽虚拟地址空间中剩余的区域. - -为确保栈与`mmap`区域不发生冲突,两者之间设置了一个安全隙. - - -另外, 它还包括下列成员,用于管理用户进程在虚拟地址空间中的所有内存区域. - -```cpp - -struct mm_struct { - struct vm_area_struct * mmap; /* 虚拟内存区域列表 */ - struct rb_root mm_rb; /* 虚拟内存区域的红黑树 */ - /* ...... */ -}; -``` - -每个区域都通过一个`vm_area_struct`实例描述, 进程的各区域按两种方法排序. - -1. 在一个单链表上(开始于`mm_struct->mmap` - -2. 在一个红黑树中,根结点位于`mm_struct->mm_rb` - -用户虚拟地址空间中的每个区域由开始和结束地址描述. 现存的区域按起始地址以递增次序被归入链表中. 扫描链表找到与特定地址关联的区域, 在有大量区域时是非常低效的操作(数据密集型的应用程序就是这样). 因此`vm_area_struct`的各个实例还通过红黑树(由mm_struct->mm_rb来标识)管理, 可以显著加快扫描速度. - -增加新区域时, 内核首先搜索红黑树, 找到刚好在新区域之前的区域. 因此, 内核可以向树和线性链表添加新的区域, 而无需扫描链表. - - - -##2.4 虚拟内存区域`vm_area_struct` -------- - -`vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围. 内核将每个内存区域作为一个单独的内存对象管理, 每个内存区域都拥有一致的属性, 比如访问权限等. 另外相应的操作也都一致. 按照这样的方式, 每一个VMA就可以代表不同类型的内存区域(比如内存映射文件或者进程用户空间栈). 这种管理方式类似于使用CFS层面向对象的方法. - -每个区域表示为`vm_area_struct`的一个实例, 其定义在[`include/linux/mm_types.h?v=4.7, line 299`](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L299) - - -```cpp -struct vm_area_struct { - /* The first cache line has the info for VMA tree walking. */ - - unsigned long vm_start; /* Our start address within vm_mm. */ - unsigned long vm_end; /* The first byte after our end address - within vm_mm. */ - - /* linked list of VM areas per task, sorted by address */ - struct vm_area_struct *vm_next, *vm_prev; - - struct rb_node vm_rb; - - - struct mm_struct *vm_mm; /* The address space we belong to. */ -``` - - -每个内存描述符都对应于进程地址空间上的唯一区间. `vm_start`指向区间的首地址(最低地址). `vm_end`指向了区域的尾地址(最高地址)之后的第一个字节. 也就是说, `vm_start`是内存区间的开始地址(它本身在区间内), 而`vm_end`是内存区间的结束地址(它本身在区间外). 因此, `vm_end - vm_start`的大小便是区间的长度. 即内存区域就在[vm_start, vm_end]之中. 注意, 在同一个地址空间内的不同内存区域不能重叠. - -所有的内存域组织在链表和红黑树中, 因此`vm_next`和`vm_prev`就指向了该虚拟内存区域在链表中的后继和前驱. `vm_rb`则作为内置的红黑树节点. - - -`vm_mm`域指向和VMA相关的`mm_struct`结构体. 注意, 每个VMA对其相关`mm_struct`结构体都是唯一的. - -* 即使两个独立的进程将同一个文件映射到各自的地址空间, 他们非别都会有一个vm_area_struct结构体来标志自己的内存区域. - -* 反过来, 如果两个线程共享一个地址空间, 那么他们也会同时共享其中所有的vm_area_struct结构体. - - - -##2.5 建立布局 -------- - -在使用`load_elf_binary`装载一个ELF二进制文件时,将创建进程的地址空间, 该函数定义在[fs/binfmt_elf.c?v=4.7, line 666](http://lxr.free-electrons.com/source/fs/binfmt_elf.c?v=4.7#L666) - - - 而`exec`系统调用刚好使用了该函数. 加载`ELF`文件涉及大量纷繁复杂的技术细节, 之前我们讲解进程调度的时候曾经专门讲解过这个函数, 因此我们现在只给出的代码流程图来主要关注建立虚拟内存区域所需的各个步骤. - - - -![`load_elf_binary`的代码流程图]() - - -如果全局变量`randomize_va_space`设置为1, 则启用地址空间随机化机制. 通常情况下都是启用的, 但在`Transmeta CPU`上会停用,因为该设置会降低此类计算机的速度. 此外,用户可以通过`/proc/sys/kernel/randomize_va_space`停用该特性 - -![cat](./images/cat_proc_sys_kernel_randomize_va_space.png) - - - -选择布局的工作由`arch_pick_mmap_layout`完成. 如果对应的体系结构没有提供一个具体的函数, 则使用内核的默认例程, 按如图4-1所示建立地址空间. 但我们更感兴趣的是, IA-32如何在经典布局和新的布局之间选择. 该函数定义在[`/arch/对应架构/mm/mmap.c`](http://lxr.free-electrons.com/ident?v=4.7;i=arch_pick_mmap_layout) - - -| 设置进程的虚拟内存布局 | x86 | arm | arm64 | -|:---------------------:|:---:|:---:|:-----:| -| arch_pick_mmap_layout | [arch/x86/mm/mmap.c?v=4.7, line 100](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L100) | [arch/arm/mm/mmap.c?v=4.7, line 181](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L181) | [arch/arm64/mm/mmap.c?v=4.7, line 79](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L79) | - - -参见arm下arch_pick_mmap_layout函数的实现, 定义在[arch/arm/mm/mmap.c?v=4.7, line 181](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L181) - -```cpp -void arch_pick_mmap_layout(struct mm_struct *mm) -{ - unsigned long random_factor = 0UL; - - if (current->flags & PF_RANDOMIZE) - random_factor = arch_mmap_rnd(); - - if (mmap_is_legacy()) { - mm->mmap_base = TASK_UNMAPPED_BASE + random_factor; - mm->get_unmapped_area = arch_get_unmapped_area; - } else { - mm->mmap_base = mmap_base(random_factor); - mm->get_unmapped_area = arch_get_unmapped_area_topdown; - } -} -``` - - -如果用户通过`/proc/sys/vm/legacy_va_layout`给出明确的指示, 或者要执行为不同的UNIX变体编译、需要旧的布局的二进制文件, 或者栈可以无限增长(最重要的一点),则系统会选择旧的布局. 这使得很难确定栈的下界, 亦即`mmap`区域的上界. - -在经典的配置下, `mmap`区域的起始点是`TASK_UNMAPPED_BASE`, 其值为0x4000000, 而标准函数`arch_get_unmapped_area`(其名称虽然带有`arch` , 但该函数不一定是特定于体系结构的, 内核也提供了一个标准实现)用于自下而上地创建新的映射. - -在使用新布局时, 内存映射自顶向下增长. - -标准函数`arch_get_unmapped_area_topdown`(我不会详细描述)负责该工作. - -| 函数or变量 | x86 | arm | arm64 | -|:---------------:|:---:|:---:|:-----:| -| MIN_GAP/MAX_GAP | [arch/x86/mm/mmap.c?v=4.7, line 54](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L54) | [arch/arm/mm/mmap.c?v=4.7, line 19](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L19) | [arch/arm64/mm/mmap.c?v=4.7, line 36](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L36) | -| mmap_is_legacy | [arch/x86/mm/mmap.c?v=4.7, line 57](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L57) | [arch/arm/mm/mmap.c?v=4.7, line 22](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L22) | [arch/arm64/mm/mmap.c?v=4.7, line 39](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L39) | -| arch_mmap_rnd | [arch/x86/mm/mmap.c?v=4.7, line 68](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L68) | [arch/arm/mm/mmap.c?v=4.7, line 172](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L172) | [arch/arm64/mm/mmap.c?v=4.7, line 50](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L50) | -| mmap_base | [arch/x86/mm/mmap.c?v=4.7, line 84](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L84) | [arch/arm/mm/mmap.c?v=4.7, line 33](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L33) | [arch/arm64/mm/mmap.c?v=4.7, line 63](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L63) | - - -```cpp -// http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L54 -#define MIN_GAP (128*1024*1024) -#define MAX_GAP (TASK_SIZE/6*5) - -// http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L19 -/* gap between mmap and stack */ -#define MIN_GAP (128*1024*1024UL) -#define MAX_GAP ((TASK_SIZE)/6*5) - -// http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L36 -/* - * Leave enough space between the mmap area and the stack to honour ulimit in - * the face of randomisation. - */ -#define MIN_GAP (SZ_128M + ((STACK_RND_MASK << PAGE_SHIFT) + 1)) -#define MAX_GAP (STACK_TOP/6*5) -``` - - -更有趣的问题是如何选择内存映射的基地址, 该工作由`mmap_base`来完成, arm架构下该函数定义在[`arch/arm/mm/mmap.c?v=4.7, line 19`](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L19) - -```cpp -static unsigned long mmap_base(unsigned long rnd) -{ - unsigned long gap = rlimit(RLIMIT_STACK); - - if (gap < MIN_GAP) - gap = MIN_GAP; - else if (gap > MAX_GAP) - gap = MAX_GAP; - - return PAGE_ALIGN(TASK_SIZE - gap - rnd); -} -``` - - -可以根据栈的最大长度, 来计算栈最低的可能位置, 用作`mmap`区域的起始点. 但内核会确保栈至少跨越128MB的空间. 另外, 如果指定的栈界限非常巨大, 那么内核会保证至少有一小部分地址空间不被栈占据. - -如果要求使用地址空间随机化机制, 上述位置会减去一个随机的偏移量,最大为1MB. - -另外, 内核会确保该区域对齐到页帧, 这是体系结构的要求. - -初看起来, 读者可能认为64位体系结构的情况会好一点, 因为不需要在不同的地址空间布局中进行选择. 虚拟地址空间是如此巨大, 以至于堆和`mmap`区域的碰撞几乎不可能. - - - -但从AMD64体系结构的`arch_pick_mmap_layout`定义来看,此中会出现另一个复杂情况: - -```cpp -arch_pick_mmap_layout -``` - - -如果启用对32位应用程序的二进制仿真,任何以兼容模式运行的进程都应该看到与原始计算机上相同的地址空间。因此, `ia32_pick_mmap_layout`用于为32位应用程序布置地址空间。该函数实际上是IA-32系统上`arch_pick_mmap_layout`的一个相同副本,前文已经讨论过. - - -AMD64系统上对虚拟地址空间总是使用经典布局,因此无需区分各种选项。如果设置了`PF_RANDOMIZE`标志,则进行地址空间随机化,变动原本固定的`mmap_base`. - -我们回到`load_elf_binary`. 该函数最后需要在适当的位置创建栈: - -```cpp -load_elf_binary -``` - -标准函数setup_arg_pages即用于该目的. 因为该函数只是技术性的,我不会详细讨论. 该函数需要栈顶的位置作为参数. 栈顶由特定于体系结构的常数STACK_TOP给出, 而后调用randomize_stack_top, 确保在启用地址空间随机化的情况下,对该地址进行随机偏移. +进程虚拟地址空间 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + + +#1 虚拟地址空间概述 +------- + + + +用户层进程的虚拟地址空间是Linux的一个重要抽象 : 它向每个运行进程提供了同样的系统视图, 这使得多个进程可以同时运行, 而不会干扰到其他进程内存中的内容. 此外, 它容许使用各种高级的程序设计技术,如内存映射 + +从今天开始, 我将讨论内核是如何实现这些概念的. 这同样需要考察可用物理内存中的页帧与所有的进程虚拟地址空间中的页之间的关联 : **逆向映射(reverse +mapping)* ***技术有助于从虚拟内存页跟踪到对应的物理内存页, 而**缺页处理(page fault handling)**则允许从块设备按需读取数据填充虚拟地址空间 + + +* 每个应用程序都有自身的地址空间,与所有其他应用程序分隔开 + +* 通常在巨大的线性地址空间中,只有很少的段可用于各个用户空间进程,这些段彼此有一定的距离。内核需要一些数据结构,来有效地管理这些(随机)分布的段。 + +* 地址空间只有极小的一部分与物理内存页直接关联。不经常使用的部分,则仅当必要时与页帧关联. + +* 内核信任自身,但无法信任用户进程。因此,各个操作用户地址空间的操作都伴随有各种检查,以确保程序的权限不会超出应有的限制,进而危及系统的稳定性和安全性. + +* fork-exec模型在UNIX操作系统下用于产生新进程. 如果实现得较为粗劣, 该模型的功能并不强大。因此内核必须借助于一些技巧,来尽可能高效地管理用户地址空间 + +#2 进程虚拟地址空间 +------- + +##2.1 进程虚拟地址空间 +------- + +各个进程的虚拟地址空间起始于地址0, 延伸到TASK_SIZE - 1, 其上是内核地址空间。 在IA-32系统上地址空间的范围可达$2^{32} = 4GB$, 总的地址空间通常按3:1比例划分,我们在下文中将关注该划分. 内核分配了1GB, 而各个用户空间进程可用的部分为3GB. 其他的划分比例也是可能的, 但正 +如前文的讨论, 只能在非常特定的配置和某些工作负荷下才有用. + +与系统完整性相关的非常重要的一方面是, 用户程序只能访问整个地址空间的下半部分,不能访问内核部分. 如果没有预先达成"协议", 用户进程也不可能操作另一个进程的地址空间,因为后者的地址空间对前者不可见. + +无论当前哪个用户进程处于活动状态, 虚拟地址空间内核部分的内容总是同样的. 取决于具体的硬件, 这可能是通过操作各用户进程的页表, 使得虚拟地址空间的上半部看上去总是相同的. 也可能是指示处理器为内核提供一个独立的地址空间, 映射在各个用户地址空间之上. 读者可以回想一下图1-3, 其中给出了相关的图示. + +虚拟地址空间由许多不同长度的段组成, 用于不同的目的, 必须分别处理. + +例如在大多数情况下, 不允许修改text段, 但必须可以执行其内容. 另一方面,必须可以修改映射到地址空间中的文本文件 +内容,而不能允许执行其内容. 因为这没有意义,文件的内容只是数据,并非机器代码. + +##2.2 进程地址空间的布局 +------- + +虚拟地址空间中包含了若干区域. 其分布方式是特定于体系结构的,但所有方法都有下列共同成分. + +* 当前运行代码的二进制代码. 该代码通常称之为text,所处的虚拟内存区域称之为代码段(text section). + +* 可执行文件的已初始化全局变量的内存映射, 称为数据段(data section). + +* 包括未初始化全局变量(也就是bss段的零页)的内存映射, 页面中的信息全部为0值, 所以可用于映射bss段等目的. + +* 用于保存局部变量和实现函数/过程调用栈(不要和进程内核栈混淆, 进程的内核栈独立存在并由内核维护)的零页内存映射 + + +* 程序使用的动态库的代码, 诸如C库或动态连接程序等共享库的代码段, 数据段和bss段. + +* 存储动态产生的数据的堆 + +* 环境变量和命令行参数的段. + +* 将文件内容映射到虚拟地址空间中的内存映射 + + +进程的虚拟地址空间中的任何有效地址都只能位于唯一的区域, 这些内存区域不能相互覆盖. 可以看到, 在执行的进程中, 每个不同的内存片段都对应一个独立的内存区域 : 栈, 对象代码, 全局变量, 被映射的文件等. + + +一个进程的虚拟地址空间主要由两个数据结来描述 + +* 一个是最高层次的 : `mm_struct` + +* 一个是较高层次的 : `vm_area_structs` + +最高层次的`mm_struct`结构描述了一个进程的整个虚拟地址空间。较高层次的结构`vm_area_truct`描述了虚拟地址空间的一个区间(简称虚拟区). + +每个进程只有一个mm_struct结构, 在每个进程的`task_struct`结构中, 有一个指向该进程的结构, 参见task_struct的定义[include/linux/sched.h?v=4.7, line 1457](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.7#L1457) + + +```cpp +// http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.7#L1457 +struct task_struct +{ + struct mm_struct *mm, *active_mm; +}; +``` +可以说, `mm_struct`结构是对整个用户空间的描述. `mm_strcut`用来描述一个进程的虚拟地址空间. + + +##2.3 内存描述符`mm_struct` +------- + +内核使用内存描述符结构体`struct mm_struct`表示进程的地址空间, 该结构体包含了和进程地址空间相关的全部信息 + + +系统中的各个进程都具有一个`struct mm_struct`的实例,可以通过`task_struct`访问. 这个实例保存了进程的内存管理信息, 定义在[`include/linux/mm_types.h?v=4.7, line 395`](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L395) + + + +**可执行代码**占用的虚拟地址空间区域, 其开始和结束分别通过 `start_code`和`end_code`标记. + + +类似地, `start_data`和`end_data`标记了包含**已初始化数据的区域**. 请注意, 在`ELF`二进制文件映射到地址空间中之后,这些区域的长度不再改变. + +**堆**的起始地址保存在`start_brk`, `brk`表示堆区域当前的结束地址. 尽管堆的起始地址在进程生命周期中是不变的, 但堆的长度会发生变化,因而`brk`的值也会变. + +**参数列表**和**环境变量**的位置分别由`arg_start`和 `arg_end`、`env_start`和`env_end`描述. 两个区域 +都位于栈中最高的区域. + +[`mmap_base`](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L404)表示虚拟地址空间中用于内存映射的起始地址, 可调用`get_unmapped_area`在`mmap` +区域中为新映射找到适当的位置. + +`task_size`, 顾名思义, 存储了对应进程的地址空间长度. 对本机应用程序来说, 该值通常是`TASK_SIZE`. 但64位体系结构与前辈处理器通常是二进制兼容的. 如果在64位计算机上执行32位二进制代码, 则`task_size`描述了该二进制代码实际可见的地址空间长度. + +各个体系结构可以通过几个配置选项影响虚拟地址空间的布局。 + +* 如果体系结构想要在不同`mmap`区域布局之间作出选择, 则需要设置`HAVE_ARCH_PICK_MMAP_LAYOUT`, 并提供`arch_pick_mmap_layout`函数. + +* 在创建新的内存映射时, 除非用户指定了具体的地址, 否则内核需要找到一个适当的位置. 如果体系结构自身想要选择合适的位置,则必须设置预处理器符号`HAVE_ARCH_UNMAPPED_AREA`, 并相应地定义`arch_get_unmapped_area`函数。 + +* 在寻找新的内存映射低端内存位置时, 通常从较低的内存位置开始, 逐渐向较高的内存地址搜索. 内核提供了默认的函数`arch_get_unmapped_area_topdown`用于搜索, 但如果某个体系结构想要提供专门的实现, 则需要设置预处理器符号`HAVE_ARCH_GET_UNMAPPED_AREA`. + +* 通常, 栈自顶向下增长. 具有不同处理方式的体系结构需要设置配置选项`CONFIG_STACK_GROWSUP` + +最后, 我们需要考虑进程标志`PF_RANDOMIZE`. 如果设置了该标志, 则内核不会为栈和内存映射的起点选择固定位置,而是在每次新进程启动时随机改变这些值的设置. 这引入了一些复杂性, 例如, 使得攻击因缓冲区溢出导致的安全漏洞更加困难. 如果攻击者无法依靠固定地址找到栈,那么想要构 +建恶意代码, 通过缓冲器溢出获得栈内存区域的访问权, 而后恶意操纵栈的内容,将会困难得多. + + +下图说明了前述的各个部分在大多数体系结构的虚拟地址空间中的分布情况. + +`text`段如何映射到虚拟地址空间中由ELF标准确定(有关该二进制格式的更多信息,请参见), 每个体系结构都指定了一个特定的起始地址 : IA-32系统起始于0x08048000, 在text段的起始地址与最低的可用地址之间有大约128 MiB的间距,用于捕获NULL指针. 其他体系结构也有类似的缺口 : `UltraSparc`计算机使用0x100000000作为text段的起始点, 而AMD64使用0x0000000000400000. 堆紧接着text段开始, 向上增长. 栈起始于STACK_TOP, 如果设置了 `PF_RANDOMIZE`, 则起始点会减少一个小的随机量. 每个体系结构都必须定义`STACK_TOP`, 大多数都设置为 `TASK_SIZE`, 即用户地址空间中最高的可用地址. 进程 +的参数列表和环境变量都是栈的初始数据. + +用于内存映射的区域起始于`mm_struct->mmap_base`, 通常设置为`TASK_UNMAPPED_BASE`, 每个体系结构都需要定义. 几乎所有的情况下, 其值都是`TASK_SIZE/3`. 要注意,如果使用内核的默认配置, 则`mmap`区域的起始点不是随机的. + + +![进程的线性地址空间的组成]() + + + +如果计算机提供了巨大的虚拟地址空间, 那么使用上述的地址空间布局会工作得非常好. 但在32位计算机上可能会出现问题. 考虑IA-32的情况 : 虚拟地址空间从0到0xC0000000 , 每个用户进程有3GB可用. `TASK_UNMAPPED_BASE`起始于0x4000000, 即1GB处. 糟糕的是, 这意味着堆只有1GB +空间可供使用, 继续增长则会进入到`mmap`区域, 这显然不是我们想要的. + +问题在于, 内存映射区域位于虚拟地址空间的中间. 这也是在内核版本2.6.7开发期间为IA-32计算机引入一个新的虚拟地址空间布局的原因(经典布局仍然可以使用). + +![mmap区域自顶向下扩展时,IA-32计算机上虚拟地址空间的布局]() + + +其想法在于使用固定值限制栈的最大长度. 由于栈是有界的, 因此安置内存映射的区域可以在栈末端的下方立即开始. 与经典方法相反, 该区域现在是自顶向下扩展. 由于堆仍然位于虚拟地址空间中较低的区域并向上增长, 因此`mmap`区域和堆可以相对扩展, 直至耗尽虚拟地址空间中剩余的区域. + +为确保栈与`mmap`区域不发生冲突,两者之间设置了一个安全隙. + + +另外, 它还包括下列成员,用于管理用户进程在虚拟地址空间中的所有内存区域. + +```cpp + +struct mm_struct { + struct vm_area_struct * mmap; /* 虚拟内存区域列表 */ + struct rb_root mm_rb; /* 虚拟内存区域的红黑树 */ + /* ...... */ +}; +``` + +每个区域都通过一个`vm_area_struct`实例描述, 进程的各区域按两种方法排序. + +1. 在一个单链表上(开始于`mm_struct->mmap` + +2. 在一个红黑树中,根结点位于`mm_struct->mm_rb` + +用户虚拟地址空间中的每个区域由开始和结束地址描述. 现存的区域按起始地址以递增次序被归入链表中. 扫描链表找到与特定地址关联的区域, 在有大量区域时是非常低效的操作(数据密集型的应用程序就是这样). 因此`vm_area_struct`的各个实例还通过红黑树(由mm_struct->mm_rb来标识)管理, 可以显著加快扫描速度. + +增加新区域时, 内核首先搜索红黑树, 找到刚好在新区域之前的区域. 因此, 内核可以向树和线性链表添加新的区域, 而无需扫描链表. + + + +##2.4 虚拟内存区域`vm_area_struct` +------- + +`vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围. 内核将每个内存区域作为一个单独的内存对象管理, 每个内存区域都拥有一致的属性, 比如访问权限等. 另外相应的操作也都一致. 按照这样的方式, 每一个VMA就可以代表不同类型的内存区域(比如内存映射文件或者进程用户空间栈). 这种管理方式类似于使用CFS层面向对象的方法. + +每个区域表示为`vm_area_struct`的一个实例, 其定义在[`include/linux/mm_types.h?v=4.7, line 299`](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L299) + + +```cpp +struct vm_area_struct { + /* The first cache line has the info for VMA tree walking. */ + + unsigned long vm_start; /* Our start address within vm_mm. */ + unsigned long vm_end; /* The first byte after our end address + within vm_mm. */ + + /* linked list of VM areas per task, sorted by address */ + struct vm_area_struct *vm_next, *vm_prev; + + struct rb_node vm_rb; + + + struct mm_struct *vm_mm; /* The address space we belong to. */ +``` + + +每个内存描述符都对应于进程地址空间上的唯一区间. `vm_start`指向区间的首地址(最低地址). `vm_end`指向了区域的尾地址(最高地址)之后的第一个字节. 也就是说, `vm_start`是内存区间的开始地址(它本身在区间内), 而`vm_end`是内存区间的结束地址(它本身在区间外). 因此, `vm_end - vm_start`的大小便是区间的长度. 即内存区域就在[vm_start, vm_end]之中. 注意, 在同一个地址空间内的不同内存区域不能重叠. + +所有的内存域组织在链表和红黑树中, 因此`vm_next`和`vm_prev`就指向了该虚拟内存区域在链表中的后继和前驱. `vm_rb`则作为内置的红黑树节点. + + +`vm_mm`域指向和VMA相关的`mm_struct`结构体. 注意, 每个VMA对其相关`mm_struct`结构体都是唯一的. + +* 即使两个独立的进程将同一个文件映射到各自的地址空间, 他们非别都会有一个vm_area_struct结构体来标志自己的内存区域. + +* 反过来, 如果两个线程共享一个地址空间, 那么他们也会同时共享其中所有的vm_area_struct结构体. + + + +##2.5 建立布局 +------- + +在使用`load_elf_binary`装载一个ELF二进制文件时,将创建进程的地址空间, 该函数定义在[fs/binfmt_elf.c?v=4.7, line 666](http://lxr.free-electrons.com/source/fs/binfmt_elf.c?v=4.7#L666) + + + 而`exec`系统调用刚好使用了该函数. 加载`ELF`文件涉及大量纷繁复杂的技术细节, 之前我们讲解进程调度的时候曾经专门讲解过这个函数, 因此我们现在只给出的代码流程图来主要关注建立虚拟内存区域所需的各个步骤. + + + +![`load_elf_binary`的代码流程图]() + + +如果全局变量`randomize_va_space`设置为1, 则启用地址空间随机化机制. 通常情况下都是启用的, 但在`Transmeta CPU`上会停用,因为该设置会降低此类计算机的速度. 此外,用户可以通过`/proc/sys/kernel/randomize_va_space`停用该特性 + +![cat](../images/cat_proc_sys_kernel_randomize_va_space.png) + + + +选择布局的工作由`arch_pick_mmap_layout`完成. 如果对应的体系结构没有提供一个具体的函数, 则使用内核的默认例程, 按如图4-1所示建立地址空间. 但我们更感兴趣的是, IA-32如何在经典布局和新的布局之间选择. 该函数定义在[`/arch/对应架构/mm/mmap.c`](http://lxr.free-electrons.com/ident?v=4.7;i=arch_pick_mmap_layout) + + +| 设置进程的虚拟内存布局 | x86 | arm | arm64 | +|:---------------------:|:---:|:---:|:-----:| +| arch_pick_mmap_layout | [arch/x86/mm/mmap.c?v=4.7, line 100](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L100) | [arch/arm/mm/mmap.c?v=4.7, line 181](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L181) | [arch/arm64/mm/mmap.c?v=4.7, line 79](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L79) | + + +参见arm下arch_pick_mmap_layout函数的实现, 定义在[arch/arm/mm/mmap.c?v=4.7, line 181](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L181) + +```cpp +void arch_pick_mmap_layout(struct mm_struct *mm) +{ + unsigned long random_factor = 0UL; + + if (current->flags & PF_RANDOMIZE) + random_factor = arch_mmap_rnd(); + + if (mmap_is_legacy()) { + mm->mmap_base = TASK_UNMAPPED_BASE + random_factor; + mm->get_unmapped_area = arch_get_unmapped_area; + } else { + mm->mmap_base = mmap_base(random_factor); + mm->get_unmapped_area = arch_get_unmapped_area_topdown; + } +} +``` + + +如果用户通过`/proc/sys/vm/legacy_va_layout`给出明确的指示, 或者要执行为不同的UNIX变体编译、需要旧的布局的二进制文件, 或者栈可以无限增长(最重要的一点),则系统会选择旧的布局. 这使得很难确定栈的下界, 亦即`mmap`区域的上界. + +在经典的配置下, `mmap`区域的起始点是`TASK_UNMAPPED_BASE`, 其值为0x4000000, 而标准函数`arch_get_unmapped_area`(其名称虽然带有`arch` , 但该函数不一定是特定于体系结构的, 内核也提供了一个标准实现)用于自下而上地创建新的映射. + +在使用新布局时, 内存映射自顶向下增长. + +标准函数`arch_get_unmapped_area_topdown`(我不会详细描述)负责该工作. + +| 函数or变量 | x86 | arm | arm64 | +|:---------------:|:---:|:---:|:-----:| +| MIN_GAP/MAX_GAP | [arch/x86/mm/mmap.c?v=4.7, line 54](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L54) | [arch/arm/mm/mmap.c?v=4.7, line 19](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L19) | [arch/arm64/mm/mmap.c?v=4.7, line 36](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L36) | +| mmap_is_legacy | [arch/x86/mm/mmap.c?v=4.7, line 57](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L57) | [arch/arm/mm/mmap.c?v=4.7, line 22](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L22) | [arch/arm64/mm/mmap.c?v=4.7, line 39](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L39) | +| arch_mmap_rnd | [arch/x86/mm/mmap.c?v=4.7, line 68](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L68) | [arch/arm/mm/mmap.c?v=4.7, line 172](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L172) | [arch/arm64/mm/mmap.c?v=4.7, line 50](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L50) | +| mmap_base | [arch/x86/mm/mmap.c?v=4.7, line 84](http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L84) | [arch/arm/mm/mmap.c?v=4.7, line 33](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L33) | [arch/arm64/mm/mmap.c?v=4.7, line 63](http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L63) | + + +```cpp +// http://lxr.free-electrons.com/source/arch/x86/mm/mmap.c?v=4.7#L54 +#define MIN_GAP (128*1024*1024) +#define MAX_GAP (TASK_SIZE/6*5) + +// http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L19 +/* gap between mmap and stack */ +#define MIN_GAP (128*1024*1024UL) +#define MAX_GAP ((TASK_SIZE)/6*5) + +// http://lxr.free-electrons.com/source/arch/arm64/mm/mmap.c?v=4.7#L36 +/* + * Leave enough space between the mmap area and the stack to honour ulimit in + * the face of randomisation. + */ +#define MIN_GAP (SZ_128M + ((STACK_RND_MASK << PAGE_SHIFT) + 1)) +#define MAX_GAP (STACK_TOP/6*5) +``` + + +更有趣的问题是如何选择内存映射的基地址, 该工作由`mmap_base`来完成, arm架构下该函数定义在[`arch/arm/mm/mmap.c?v=4.7, line 19`](http://lxr.free-electrons.com/source/arch/arm/mm/mmap.c?v=4.7#L19) + +```cpp +static unsigned long mmap_base(unsigned long rnd) +{ + unsigned long gap = rlimit(RLIMIT_STACK); + + if (gap < MIN_GAP) + gap = MIN_GAP; + else if (gap > MAX_GAP) + gap = MAX_GAP; + + return PAGE_ALIGN(TASK_SIZE - gap - rnd); +} +``` + + +可以根据栈的最大长度, 来计算栈最低的可能位置, 用作`mmap`区域的起始点. 但内核会确保栈至少跨越128MB的空间. 另外, 如果指定的栈界限非常巨大, 那么内核会保证至少有一小部分地址空间不被栈占据. + +如果要求使用地址空间随机化机制, 上述位置会减去一个随机的偏移量,最大为1MB. + +另外, 内核会确保该区域对齐到页帧, 这是体系结构的要求. + +初看起来, 读者可能认为64位体系结构的情况会好一点, 因为不需要在不同的地址空间布局中进行选择. 虚拟地址空间是如此巨大, 以至于堆和`mmap`区域的碰撞几乎不可能. + + + +但从AMD64体系结构的`arch_pick_mmap_layout`定义来看,此中会出现另一个复杂情况: + +```cpp +arch_pick_mmap_layout +``` + + +如果启用对32位应用程序的二进制仿真,任何以兼容模式运行的进程都应该看到与原始计算机上相同的地址空间。因此, `ia32_pick_mmap_layout`用于为32位应用程序布置地址空间。该函数实际上是IA-32系统上`arch_pick_mmap_layout`的一个相同副本,前文已经讨论过. + + +AMD64系统上对虚拟地址空间总是使用经典布局,因此无需区分各种选项。如果设置了`PF_RANDOMIZE`标志,则进行地址空间随机化,变动原本固定的`mmap_base`. + +我们回到`load_elf_binary`. 该函数最后需要在适当的位置创建栈: + +```cpp +load_elf_binary +``` + +标准函数setup_arg_pages即用于该目的. 因为该函数只是技术性的,我不会详细讨论. 该函数需要栈顶的位置作为参数. 栈顶由特定于体系结构的常数STACK_TOP给出, 而后调用randomize_stack_top, 确保在启用地址空间随机化的情况下,对该地址进行随机偏移. diff --git a/study/kernel/04-net/README.md b/study/kernel/04-net/README.md new file mode 100644 index 0000000..1a8b449 --- /dev/null +++ b/study/kernel/04-net/README.md @@ -0,0 +1,271 @@ +进程虚拟地址空间 +======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) | + + +`Linux`是因特网的产物, 这是无可争议的. 首先, 得感谢因特网通信, Linux的开发过程证明了一个很多人曾持有的观点是荒谬的 : 对分散在世界各地的一组程序员进行项目管理是不可能的. 第一个内核源代码版本是在十多年前通过FTP服务器提供的, 此后网络便成了数据交换的支柱, 无论是概念和代码的开发, 还是内核错误的消除, 都是如此. + +内核邮件列表是个活生生的例子, 它几乎没有改变过. 每个人都能够看到最新贡献的代码, 并为促进`Linux`的开发提出自己的意见, 当然, 得假定所表达的意见是合理的. `Linux`对各种网络适应得都很好, 这是可以理解的, 因为它是与因特网共同成长的. + +在构成因特网的服务器中, 大部分是运行Linux的计算机. 不出所料, 网络实现是Linux内核中一个关键的部分, 正在获得越来越多的关注. 实际上, Linux不支持的网络方案很少. + +网络功能的实现是内核最复杂、牵涉最广的一部分. 除了经典的因特网协议(如TCP、UDP)和相关的IP传输机制之外, Linux还支持许多其他的互联方案,使得所有想得到的计算机/操作系统能够互操作. + +`Linux`也支持大量用于数据传输的硬件, 如以太网卡和令牌环网适配器及ISDN卡和调制解调器, 但这并没有使内核的工作变得简单. + +尽管如此, `Linux`开发人员提出了一种结构良好得令人惊讶的模型, 统一了各种不同的方法. 虽然本章是本书最长的章之一, 但并没有涵盖网络实现的每个细节. 即使概述一下所有的驱动程序和协议, 也超出了一本书的范围, 由于信息量巨大, 实际上可能需要许多本书. 不算网卡驱动程序, 网络子系统的C语言实现在内核源代码中就占了15MB, 如果将相应的代码打印到纸上要有6000多页. 与网络相关的头文件的数目巨大, 使得内核开发者将这些头文件存储到一个专门的目录`include/net`中, 而不是存储到标准位置`include/linux`. 网络相关的代码中包含了许多概念, 这些形成了网络子系统的逻辑支柱, 我们在本章中最感兴趣的就是这些概念. 我们的讨论主要限于TCP/IP实现, 因为它是目前使用最广泛的网络协议. + +当然, 网络子系统的开发, 并不是从头开始的. 在计算机之间交换数据的标准和惯例都已经存在数十年之久, 这些都为大家所熟知且沿用已久. Linux也实现了这些标准, 以连接到其他计算机. + +##1 互联的计算机 +------- + +计算机之间的通信是一个复杂的主题,引出了许多问题,诸如: + +* 如何建立物理连接? 使用何种线缆? 通信介质有哪些限制和特殊要求? + +* 如何处理传输错误? + +* 如何识别网络中的每一台计算机? + +* 如果两台计算机通过其他计算机连接, 那么二者之间的数据交换如何进行? 如何查找最佳的路由? + +* 如何打包数据,使之不依赖于特定计算机的特性? + +* 如果一台计算机提供了几个网络服务, 如何识别这些服务? + + +这类问题还有很多. 令人遗憾的是, 答案的数目以及问题的数目几乎是无限的, 因此随着时间流逝,对于如何处理特定的问题,提出了许多建议. 最"合理"的系统是 : 将问题分类,创建各种层来解决明确定义的问题, 层间借助固定的机制进行通信. 这种方法大大简化了实现、维护,尤其是调试. + + + +#2 ISO/OSI 和TCP/IP 参考模型 +------- + + +众所周知的ISO(`International Organization for Standardization`, 国际标准化组织)设计了一种参考模型, 定义了组成网络的各个层. 该模型由7层组成, 称为OSI(Open Systems Interconnection, 开放系统互连)模型. + +但对某些问题来说, 划分为7层过于详细了. 因此, 实际上通常使用另一种参考模型, 其中将ISO/OSI模型的一些层合并为新层. 该模型只有4层, 因此其结构更为简单。这种模型称为TCP/IP参考模型, IP表示`Internet Protocol`(网际协议), 而TCP表示`Transmission Control Protocol`(传输控制协议). + +当今因特网上的大部分通信都是基于该模型的. 两个模型的各个层的比较见下图. + +每层都只能与紧邻(上方或下方)的层通信. 例如, TCP/IP模型的传输层只能与互联网络层和应用层通信, 而完全独立于主机到网络层(理论上,它甚至不知道存在这样的一个层). + +各层执行的任务如下: + +* 主机到网络层负责将信息从一台计算机传输到远程计算机. 它处理传输介质①的物理性质, 并将数据流划分为定长的帧(frame), 以便在发生传输错误时重传数据块. 如果几台计算机共享同一传输线路, 网络接口卡必须有一个唯一的 ID号, 称之为MAC地址(MAC address), 通常烧进硬件中. 各厂商之间的协议保证该ID 是全球唯一的. MAC地址的一个例子是`08:00:46:2B:FE:E8`. + +从内核看来, 该层是由网卡的设备驱动程序实现的. + +* OSI模型的网络层在TCP/IP模型中称为互联网络层(Internet layer, 也称IP层), 二者在本质上是相同的, 都是指在网络中的任何计算机之间交换数据的任务, 所述计算机不一定是直接连.接的,如下图所示. + +计算机A和B之间的直接传输链路是不存在的, 因为二者在物理上是未连接的. 因此, 网络层的任务是找到一条线路, 使得计算机可以彼此通信, 例如, A-E-B或A-E-C-B. + +网络层也负责其他连接细节, 如将传输的数据划分为特定长度的分组. 这是必要的, 因为对传输线路上的各个计算机而言, 所能够处理的分组最大长度可能是不同的。在发送数据时,数据流划分为分组, 这些分组在接收端重新组合。这样,高层协议可以透明地处理任意长度的数据, 而无需费力考虑互联网络层或 +网络层的特定性质. + +网络层还分配网络中唯一的地址, 以便计算机可以彼此通信(这与前述的硬件地址是不同的, 因为网络通常由物理子网组成). + +在因特网中, 网络层借助IP协议(Internet Protocol)实现, IP协议有两个版本(IPv4和IPv6). + +当前, 大多数连接是根据IPv4处理的, 但IPv6将在未来代替它. + +下文讨论IP连接时,总是指IPv4连接. + +IP使用一定格式的地址来寻址计算机, 格式如`192.168.1.8`或`62.26.212.10`. 这些地址由正式注册的权威机构或提供者分配(有时候是动态的), 或可以自由选择(在定义为私有的范围内). + +IP支持各种地址类别, 允许在地址层次上将网络灵活地划分为子网(subnet), 子网的大小取决于需求, 子网甚至可以容纳数千万台计算机. 但本书不会详细阐述该主题. 读者可以参考网络和系统管理方面的大量文献. + +* 在两种模型中, 第4层都是传输层(transport layer). 其任务是在两个建立了链路的计算机上, 控制应用程序之间的数据传输. 在计算机之间建立通信链路还不够, 还必须在客户和服务器应用程序之间建立连接, 当然, 这预先假定了计算机之间有一个现存的链路. 在因特网中, TCP(`Transmission Control Protocol`, 传输控制协议)或UDP(`User Datagram Protocol`, 用户数据报协议)用于该目的. 每个对互联网络层数据感兴趣的应用程序都使用一个唯一的端口号, 来唯一地标识目标系统上的服务器应用程序. 通常, 端口80用于Web服务器. 浏览器客户端必须向服务器地址发送请求, 以获得所需的数据. (自然,客户端也必须有一个唯一的端口号, 使得Web服务器可以响应该请求, 但客户端的端口号是动态生成的.) 为完全定义一个端口地址,通常将端口号附加在IP地址后,用冒号分隔。例如,在地址为192.168.1.8的计算机上的Web服务器,可以通过地址192.168.1.8:80来唯一标识. + + 传输层的另一项任务是可以(但不是必须的)提供一种可靠的连接,使得通过该连接的数据按给定的顺序到达。上述特性和TCP协议将在12.9.2节讨论。 + +* TCP/IP参考模型中的应用层,对应OSI模型中的5~7层(会话层、表示层和应用层). 顾名思义,应用层表示从应用程序视角来看的网络连接. 在两个应用程序之间建立通信连接之后, 应用层负责传输实际的内容. 毕竟, Web服务器与其客户端之间的通信, 不同于邮件服务器. + + 为因特网定义了大量的标准协议. 通常, 它们是以RFC(`Request for Comments`)文档的形式定义的, 打算使用或提供特定服务的应用程序必须实现相关的协议. 大多数协议可以使用简单的`telnet`工具测试, 因为它们是用简单的文本命令进行操作的。典型的例子是浏览器与Web服务器之间的通信 +流程, 如下 : + +```cpp +``` + +`telnet`用来与计算机`192.168.1.20`的`80`端口建立一个TCP连接. 所有的用户输入都通过该网络连接转发到与该地址(由IP地址和端口号唯一标识的)相关联的进程. 在接收到请求之后, 立即发送一个响应. 所要的HTML页面的内容, 连同一个包含了文档有关信息和其他资料的HTTP首部, 会发送回来. Web浏览器使用同样的过程来访问数据, 这对用户是透明的. + +由于网络的功能已经系统地划分为各个层, 希望与其他计算机通信的应用程序, 只需要关注少量细节. 计算机之间的实际链路由较低的层实现, 而应用程序只需要产生和读取文本串, 无论两台计算机是在同一房间里并排安放, 还是分别位于两个不同的地方. + +网络的层状结构在内核中反映为下述事实 : 不同的层次由分离的代码实现, 不同层次的代码之间通过明确定义的接口来交换数据或转发命令. + + +#3 通过套接字通信 +------- + +从程序员的视角来看, 外部设备在Linux(和UNIX)中不过是普通的文件, 通过正常的读写操作即可访问, 如第8章所述. 由于只需要一个通用接口, 这简化了对资源的访问. + +但对网卡而言, 情况有点复杂, 因为上述方案或者根本不能采用, 或者会带来极大的困难. 网卡的运作方式与普通的块设备和字符设备完全不同,使得经典的UNIX箴言"万物皆文件"不再完全适用. + +一个原因是(所有层次)使用了许多不同的通信协议, 为建立连接需要指定许多选项, 且无法在打开设备文件时完成这些任务. 因此, 在/dev目录下没有与网卡对应的项. + +当然, 内核必须提供一个尽可能通用的接口, 供程序访问网络功能. 这个问题不是Linux特有的, 在20世纪80年代它也让BSD UNIX的程序员们很头痛. 他们采用的解决方案是将一种称为套接字的特殊结构用作到网络实现的接口, 这种方案现在已经成为工业标准. POSIX标准中也定义了套接字, 因而Linux也实现了套接字. + +套接字现在用于定义和建立网络连接, 以便可以用操作inode的普通方法(特别是读写操作)来访问网络. 从程序员的角度来看, 创建套接字的最终结果是一个文件描述符, 它不仅提供所有的标准函数, 还包括几个增强的函数. 用于实际数据交换的接口对所有的协议和地址族都是同样的. + +在创建套接字时, 不仅要区分地址和协议族, 还要区分基于流的通信和数据报的通信. (对面向流的套接字来说)同样重要的一点是, 套接字是为客户端程序建立的, 还是为服务器程序建立的. + +为从用户角度来说明套接字的功能, 下面用一个简短的程序来示范几个网络编程方面的几个选项. 相关内容的详细描述可以参考许多专门著作. + + + +##3.1 创建套接字 +------- + +套接字不仅可以用于各种传输协议的IP连接, 也可以用于内核支持的所有其他地址和协议类型(例如, IPX、Appletalk、本地UNIX套接字、DECNet,还有在中列出的许多其他类型). + +为此, 在创建套接字时, 必须指定所需要的地址和协议类型的组合. 尽管作为过去的一项遗迹, 可以任意选择地址和协议族的组合, 但目前每个地址族都只支持一个协议族, 而且只能区分面向流的通信和面向数据报的通信. 例如, 对一个已经分配了因特网地址如192.168.1.20的套接字来说, 只能使用TCP(用于流)或UDP(用于数据报服务)作为传输协议. + +套接字是使用socket库函数生成的, 该函数通过12.10.3节讨论的一个系统调用与内核通信. 除了地址族和通信类型(流或数据报)之外, 可使用第三个参数来选择协议. 但按照前文的说法, 这是不必要的, 因为前两个参数已经唯一地定义了协议. 将第三个参数指定为0, 即通知函数使用适当的默认协议. + +在调用socket函数后, 套接字地址的格式(或它属于哪个地址族)已经很清楚, 但尚未给套接字分配本地地址. + +bind函数用于该目的, 必须向该函数传递一个sockaddr_type结构作为参数. + +该结构定义了本地地址. 因为不同地址族的地址类型也不同, 所以该结构对每个地址族都有一个不同的版本, 以便满足各种不同的要求. type指定了所需的地址类型. + +因特网地址由IP地址和端口号唯一定义, 这也是sockaddr_in定义为下列形式的原因 : + +```cpp + +struct sockaddr_in { +sa_family_t sin_family; /* 地址族 */ +__be16 sin_port; /* 端口号 */ +struct in_addr sin_addr; /* 因特网地址 */ + /* ...... */ +} +``` +除了地址族(这里是AF_INET)之外,还需要一个IP地址和端口号. + + +IP地址不能使用常见的点分十进制记法(一个字符串, 包含由点分隔的4个十进制数, 如192.168.1.10), 而必须以数字形式指定。库函数inet_aton可以将一个ASCII字符串格式(点分十进制)的IP地址转换为内核(和C库)所需的格式. 例如, 地址192.168.1.20的数字表示是335653056. 生成数字地址时, 将点分十进制格式中由点分隔的4个部分分别转换为一个字节, 然后顺序写入到一个4字节、可解释为数字的数据类型中. 这种转换在两种表示之间建立了一种一一对应. + +如第1章所述, CPU存储数值有两种惯例, 即小端序和大端序. 为确保不同字节序的机器之间能够彼此通信, 显式定义了一种网络字节序(network byte order), 它等价于大端序格式. 因而, 协议首部出现的数值都必须使用网络字节序. IP地址和端口号实际上都是数字, 因而在定义sockaddr_in结构中的数值时, 必须考虑到这个事实. C库带有许多函数, 用于将数值在CPU的本地格式和网络字节 +序格式之间转换(如果CPU和网络字节序相同, 这些函数实际上不进行处理). 好的网络应用程序总是使用这些函数, 即使是在大端序的机器上进行开发也应该如此, 这可以确保程序能够移植到不同类型的机器上. + +为明确地表示小端序和大端序类型, 内核提供了几种数据类型. __be16、__be32和__be64分别表示位长为16、32、64位的大端序数据类型, 而前缀为__le的变体则表示对应的小端序数据类型. 这些类型都定义在中。请注意,小端序和大端序类型最终都映射到同样的数据类型(即u32等,在第1章介绍过),但显式指定字节序使得自动化的类型检查工具可以检查代码的正确性. + + +##3.2 使用套接字 +------- + + +这里假定读者对用户层网络编程比较熟悉. 但为了简要说明套接字如何表示到内核网络子系统的接口, 这里需要讨论两个非常简短的示例程序, 一个充当echo请求的客户端, 另一个充当服务器. 客户端会向服务器发送一个文本串,服务器原样返回该文本串. 例子使用了TCP/IP协议. + +###3.2.1 echo客户端 +------- + +echo客户端的源代码如下 + +```cpp + +``` + +因特网超级守护进程(inetd、xinetd或其他类似程序)通常使用内建的echo服务器。因此,上述源代码在编译之后可以立即测试. + +```cpp + +``` + +客户端需要执行下列步骤. + +1. 创建一个sockaddr_in结构的实例, 用来描述要连接的服务器的地址. AF_INET表明它是一个因特网地址, 而目标服务器由其IP地址(192.168.1.20)和端口号(7)明确地限定. + 另外, 主机数据也转换为网络字节序. `htons`用于转换端口号, 而`inet_addr`辅助函数用于将包含点分十进制格式地址的文本串转换为数字. + +2. 通过socket函数在内核中创建一个套接字, 该函数基于内核提供的socketcall系统调用(下文会说明这一点). 返回的结果是一个整数, 可解释为文件描述符, 因而用于处理普通文件的所有函数都可以用于套接字, 如第8章所述. 除了这些操作之外,还有其他特定于网络的方法,可用于处理套接字文件描述符。这些特定于网络的方法可用于精确设置此处没有讨论的各种传输参数。 + +3. 对套接字文件描述符和server变量调用connect函数(也基于socketcall系统调用),即可建立到服务器的连接,server变量存储服务器连接数据. + +4. 实际的通信, 是从用write向服务器发送一个文本串("Hello World",还能是其他的吗?)开始的. 通过套接字发送数据, 等价于向套接字文件描述符写入数据. 这个步骤完全独立于服务器的位置和用于建立连接的协议。网络实现确保了字符串能够到达目标位置,不管是如何完成的 + +5. 通过read读取服务器的响应,但首先必须分配一个缓冲区, 用于容纳接收的数据. 作为预防措施, 在内存中分配了1000字节作为缓冲区, 尽管我们预期服务器只会返回原字符串。调用read会阻塞客户端程序,直至服务器发送的响应到达客户端,read会返回接收到的字节数。 + +因为C语言的字符串总是以0结尾的,所以会接收到11个字节,当然消息本身只有10个字节长。 + + +###3.2.2 echo服务器 +------- + +套接字用于服务器进程的方法,与其在客户端的使用方法稍有不同。下列示例程序示范了如何实现一个简单的echo服务器: + + +```cpp +#include +#include +#include +#include +int main() { +char* echo_host = "192.168.1.20"; +int echo_port = 7777; +int sockfd; +struct sockaddr_in *server = +(struct sockaddr_in*)malloc(sizeof(struct sockaddr_in)); +/* 设置自身地址 */ +server->sin_family = AF_INET; +server->sin_port = htons(echo_port); // 注意,是网络字节序! +server->sin_addr.s_addr = inet_addr(echo_host); +/* 创建套接字 */ +sockfd = socket(AF_INET, SOCK_STREAM, 0); +/* 绑定到一个地址 */ +if (bind(sockfd, (struct sockaddr*)server, sizeof(*server))) { +printf("bind failed\n"); +} +/* 启用套接字的服务器模式(即开始监听) */ +listen(sockfd, SOMAXCONN); +/* 等待客户端发送的数据进入 */ +int clientfd; +struct sockaddr_in* client = +(struct sockaddr_in*)malloc(sizeof(struct sockaddr_in)); +int client_size = sizeof(*client); +char* buf = (char*)malloc(1000); +int bytes; +printf("Wait for connection to port %u\n", echo_port); +/* 接受连接请求 */ +clientfd = accept(sockfd, (struct sockaddr*)client, &client_size); +printf("Connected to %s:%u\n\n", inet_ntoa(client->sin_addr), +ntohs(client->sin_port)); +printf("Numeric: %u\n", ntohl(client->sin_addr.s_addr)); +while(1) { /* 无限循环 */ +/* 接收传输的数据 */ +bytes = read(clientfd, (void*)buf, 1000); +if (bytes <= 0) +{ +close(clientfd); +printf("Connection closed.\n"); +exit(0); +} +printf("Bytes received: %u\n", bytes); +printf("Text: '%s'\n", buf); +/* 发送响应数据 */ +write(clientfd, buf, bytes); +} +} +``` + + +前一部分与客户端的代码几乎相同。需要创建一个sockaddr_in结构实例来保存服务器的因特网地址,但原因与客户端程序不同。客户端代码在该结构中指定的是想要连接到的服务器的地址。在这里,指定的是服务器等待连接时所使用的地址。创建套接字的方式与客户端相同。 +与客户端不同的是,服务器并不会主动与另一个程序建立连接,服务器只会被动地等待,直至收到连接请求。建立一个被动连接需要以下三个库函数(仍然是基于万能的socketcall系统调用)。 + +* bind将套接字绑定到一个地址(本例中是192.186.1.20:7777) + +* listen通知套接字被动地等待客户端连接请求的到来。该函数创建一个等待队列,将所有希望建立连接的(远程)进程放置在该队列上。队列的长度由listen的第二个参数指定。(SOMAXCONN是系统内部允许的等待队列的最大长度,用来防止任意指定等待队列的长度。) + +* accept函数接受等待队列上第一个客户端的连接请求。在队列为空时,该函数将阻塞,直至有一个想要进行连接的客户端到来。 + +实际通信仍然由read和write完成,这两个函数使用由accept返回的文件描述符。 + +示例程序输出了客户端连接数据(包括IP地址和端口号,由accept的输出参数提供)。虽然就具体的客户端计算机来说,客户端的IP地址是固定的,但客户端的端口号是在建立连接时由客户端计算机的内核动态选择的。 + +echo服务器的功能很容易模拟,只需要在一个无限循环中读取所有客户端的输入并原样写回即可。在客户端关闭连接时,服务器的read将返回一个长度为0的数据流,这样服务器也会终止。具体过程如下。 + + + +![](客 户 端服 务 器) \ No newline at end of file