Files
LDD-LinuxDeviceDrivers/study/kernel/04-net/01-backgroud
2016-09-06 12:59:43 +08:00
..
...
2016-09-06 12:59:43 +08:00

进程虚拟地址空间

日期 内核版本 架构 作者 GitHub CSDN
2016-06-14 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理

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.862.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服务器之间的通信 流程, 如下 :


telnet用来与计算机192.168.1.2080端口建立一个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定义为下列形式的原因 :

<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客户端的源代码如下


因特网超级守护进程(inetd、xinetd或其他类似程序)通常使用内建的echo服务器。因此,上述源代码在编译之后可以立即测试.


客户端需要执行下列步骤.

  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服务器:

#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的数据流,这样服务器也会终止。具体过程如下。

![](客 户 端服 务 器)

一个四元组(192.168.1.20:7777, 192.168.1.10:3506)用来唯一标识一个连接。前两个分量是服务器本地系统的地址和端口号,后两个分量是客户端的地址和端口号。 如果元组中某个分量仍然是未定的,则用星号(*)表示。因而,在被动套接字上监听尚未有客户端连接的服务器进程,可以表示为192.168.1.20:7777, :。 在服务器调用fork复制自身来处理某个连接之后,在内核中注册了两个套接字对,如下。

尽管两个服务器进程的套接字具有相同的IP地址/端口号组合,但二者对应的四元组是不同的。 因此,内核在分配输入和输出TCP/IP分组时,必须注意到四元组的所有4个分量,才能确保正确。 该任务称为多路复用(multiplexing)。 netstat工具可以显示并检查系统上所有TCP/IP连接的状态。如果有两个客户端连接到服务器, 将生成下列样例输出:

##3.3 数据报套接字

UDP是建立在IP连接之上的第二种广泛使用的传输协议。UDP表示User Datagram Protocol(用户 数据报协议),在如下几个基本方面与TCP有所不同。

  • UDP是面向分组的。在发送数据之前,无须建立显式的连接。

  • 分组可以在传输期间丢失。不保证数据一定能到达其目的地。

  • 分组接收的次序不一定与发送的次序相同.

UDP通常用于视频会议、音频流及类似的服务. 在此类环境下, 丢失几个分组并不要紧, 用户只会注意到多媒体序列内容中出现短暂的漏失. 但类似于IP, UDP保证分组到达目的地时, 其内容不会发生改变.

分别使用TCP和UDP协议的进程, 可以同时使用同样的IP地址和端口号. 在多路复用时, 内核会根据分组的传输协议类型,将其转发到适当的进程.

如果比较TCP和UDP协议, 就像是比较电话网络和邮政业务. TCP对应电话呼叫. 在信息传输之前, 主叫方必须建立连接(必须由被叫方接受). 在通话期间, 所有信息的接收次序都与发送次序相同.

UDP可比作邮政业务. 分组(类比信件)可以直接向接收者发送, 无须预先获取许可. 不能保证信件一定会送达(尽管邮政业务和网络都会尽力保证这一点). 类似地, 同样不能保证信件一定会按照特定的顺序发出或收到.

如果读者还有兴趣看看更多UDP套接字的用法的例子, 可以参考网络和系统编程方面的大量教科书.