This commit is contained in:
gatieme
2016-09-03 21:45:05 +08:00
parent 9e5ef189ad
commit 98dc9e7949
3 changed files with 798 additions and 350 deletions
@@ -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变量申请空间.<br>在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本. <br>此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本<br>在非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) | 建立了内核的内存分配器, <br>其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)<br>然后调用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的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配<br>由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
##1.5 伙伴系统
-------
在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法.
Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述.
@@ -24,7 +200,6 @@ Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该
#2 伙伴系统的结构
-------
File diff suppressed because it is too large Load Diff
+271
View File
@@ -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,还有在<socket.h>中列出的许多其他类型).
为此, 在创建套接字时, 必须指定所需要的地址和协议类型的组合. 尽管作为过去的一项遗迹, 可以任意选择地址和协议族的组合, 但目前每个地址族都只支持一个协议族, 而且只能区分面向流的通信和面向数据报的通信. 例如, 对一个已经分配了因特网地址如192.168.1.20的套接字来说, 只能使用TCP(用于流)或UDP(用于数据报服务)作为传输协议.
套接字是使用socket库函数生成的, 该函数通过12.10.3节讨论的一个系统调用与内核通信. 除了地址族和通信类型(流或数据报)之外, 可使用第三个参数来选择协议. 但按照前文的说法, 这是不必要的, 因为前两个参数已经唯一地定义了协议. 将第三个参数指定为0, 即通知函数使用适当的默认协议.
在调用socket函数后, 套接字地址的格式(或它属于哪个地址族)已经很清楚, 但尚未给套接字分配本地地址.
bind函数用于该目的, 必须向该函数传递一个sockaddr_type结构作为参数.
该结构定义了本地地址. 因为不同地址族的地址类型也不同, 所以该结构对每个地址族都有一个不同的版本, 以便满足各种不同的要求. type指定了所需的地址类型.
因特网地址由IP地址和端口号唯一定义, 这也是sockaddr_in定义为下列形式的原因 :
```cpp
<in.h>
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的变体则表示对应的小端序数据类型. 这些类型都定义在<types.h>中。请注意,小端序和大端序类型最终都映射到同样的数据类型(即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<stdio.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<string.h>
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的数据流,这样服务器也会终止。具体过程如下。
![](客 户 端服 务 器)