0%

背景

编程时遇到的阻塞任务一般有两类:

  1. 等待 I/O 就绪(I/O 密集型);
  2. 耗时的计算工作(CPU 密集型)。

遇到这类任务,通常首选考虑是否可以优化操作(主要是针对第 2 种情况),将阻塞限制在可以接受的范围内,若不行则考虑使用多线程或多进程,将阻塞操作交由其他线程(例如 .NET APM,.NET 异步编程模型使用线程池来异步执行任务)或进程(fork/exec、任务队列,或者异步委托第三方服务 API)去异步处理,然后在操作结束后取回结果。对于第 1 种情况,使用操作系统支持的非阻塞 I/O 来提供异步支持是最理想的方式, 这也是 Tornado 的核心工作原理。

由于 Tornado 工作模型的原因,通过异步库(指由系统级非阻塞 I/O 来提供支持的库)来处理 I/O 密集型操作才是 Tornado 的正确工作方式,否则一个任务出现阻塞(或者执行时间过长)就会导致其他请求不能被及时处理。遇上没有异步库支持的 I/O 操作(比如磁盘 I/O 操作,Linux 不能很好地提供异步支持)以及 CPU 密集型操作,在 Tornado 中一般我们可以简单地用下面两种方式进行处理:

  1. 将阻塞操作委托给 futures 模块的 ThreadPoolExecutor/ProcessPoolExecutor 去执行;
  2. 使用 tornado + celery(RabbitMQ 或 Redis 做 Broker,totoro我个人弄的一个 tornado+celery 适配库,支持 RabbitMQ 和 Redis),将阻塞操作委托给 celery 执行。

NOTE:python2 需要单独安装 futures 模块(pip install futures),python3 自带不需要单独安装。

这篇笔记主要是记录在使用 ProcessPoolExecutor 去执行时遇到的一些问题和最终解决方法。

ThreadPoolExecutor/ProcessPoolExecutor

如何选择 ThreadPoolExecutor 和 ProcessPoolExecutor

由于 Python GIL 的原因,利用多线程(“单进程,多线程”) 去处理 CPU 密集型任务并不能有效地利用多核,提高性能。在处理 I/O 密集型任务时,由于遇到 I/O 阻塞时线程会主动释放 GIL,多线程才能明显提高性能。

基于上述原因,在 Tornado 中区分一个任务是 CPU 密集型还是 I/O 密集型很重要,前者选择 ProcessPoolExecutor,后者选择 ThreadPoolExecutor 理论上是正确的。

Read more »

引言

HTTP1Connection 是 HTTP/1.x 连接的抽象,可作为 client 发起请求和解析响应,也可作为 server 接收请求和回应响应。这里主要分析 HTTP1Connection 中怎样实现对请求和响应数据的解析的。关于请求的发起和回应响应,涉及到如何写的实现,将在后续文章中分析。

在具体分析代码实现之前,先介绍一下 HTTP/1.x 相关的内容以方便后面对代码的理解。

HTTP/1.x 简介

HTTP 协议是一个应用层协议,协议本身并没有规定使用它或它支持的层。事实上,HTTP 可以在任何互联网协议上,或其他网络上实现。HTTP 假定其下层协议提供可靠的传输。因此,任何能够提供这种保证的协议都可以被其使用。在TCP/IP 协议族上使用 TCP 作为其传输层。

版本

HTTP 协议有多个版本 HTTP/1.x 和 HTTP/2.0。目前使用最广泛的是 HTTP/1.x,包括 HTTP/1.0 和 HTTP/1.1 两个版本,后者是对前者的升级改进,最大的不同有两点:

  1. 默认支持持久连接,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟。
  2. 支持 Host 请求头字段,使得 Web 服务器可以在同一个 IP 和 Port 上使用不同的 HostName 来创建多个虚拟 Web 站点。

请求(Request)

请求消息(Request Message)

请求由客户端向服务器端发出,请求消息由下面 4 部分组成(RFC 2616 Request):

  1. Request-Line,请求行,格式为:Method SP Request-URI SP HTTP-Version CRLF, eg. “GET /foo HTTP/1.1”。
  2. Request Header Fields,请求头,*(( general-header | request-header | entity-header ) CRLF),在 HTTP/1.1 中除了 Host 外其他请求头都是可选的。
  3. 空行,CRLF。
  4. 消息体,[ message-body ]

每个头字段由一个字段名称(name) + 冒号(:) + 字段值(value), 三部分组成,name 是大小写无关的,value 前可以添加任何数量的空格符,头字段可以被扩展为多行,在每行开始处,使用至少一个空格或制表符。

请求方法(Method)

HTTP/1.x 中定义了 8 中请求方法来以不同的方式操作指定的资源:OPTIONS/GET/HEAD/POST/PUT/DELETE/TRACE/CONNECT/PATCH,方法名称是区分大小写的,具体的定义请参考 RFC 2616 Request Method。当某个请求所针对的资源不支持对应的请求方法的时候,服务器应当返回状态码 405(Method Not Allowed),当服务器不认识或者不支持对应的请求方法的时候,应当返回状态码 501(Not Implemented)。

HTTP 服务器至少应该实现 GET 和 HEAD 方法,其他方法都是可选的。 GET 和 HEAD 方法,除了进行获取资源信息外,不具有其他意义,理论上是”安全的“(实际上其结果取决于服务器的实现)。

Read more »

引言

HTTPServer 是 HTTP 协议的 TCPServer 子类实现,HTTPServer 覆写了 handle_stream 方法用于处理 HTTP 协议。与 TCPServer 一样,可以有三种使用模式,源码注释中写的很详细,在 TCPServer 源码解析的部分也已经详细讨论过,这里就不再赘述。

来看看一个简单的 HTTPServer 使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import tornado.httpserver
import tornado.ioloop

def handle_request(request):
message = "You requested %s\n" % request.uri
request.connection.write_headers(
httputil.ResponseStartLine('HTTP/1.1', 200, 'OK'),
{"Content-Length": str(len(message))})
request.connection.write(message)
request.connection.finish()

http_server = tornado.httpserver.HTTPServer(handle_request)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()

示例代码实现的是一个简单的 echo 服务器,HTTPServer 接受的是一个以 HTTPServerRequest 作为参数的函数 handle_request

HTTPServerRequestconnection 字段是一个 HTTPConnection 实例对象,应用代码通过使用 HTTPConnection 的方法响应客户端(write response )。

HTTPServer 支持 HTTP keep-alive 连接和 X-Real-Ip/X-Forwarded-For HTTP Heads。

HTTPServer 实际上还接受 HTTPServerConnectionDelegate 实例作为请求处理的委托对象。如下代码把对请求的处理委托给 tornado.web.Application 对象处理:

1
2
3
4
5
6
application = web.Application([
(r"/", MainPageHandler),
])
http_server = httpserver.HTTPServer(application)
http_server.listen(8080)
ioloop.IOLoop.instance().start()

ApplicationHTTPServer 都是 HTTPServerConnectionDelegate 的子类,HTTPServer 会委托 Application 完成对请求的处理。这是大多数情况下我们使用 Tornado 的方式

Read more »

引言

有了 tornado.iolooptornado.iostream 这两个模块的帮助,在 tornado 中要实现一个异步 Web 服务器就变得容易了。

tornado.httpserver 模块是 tornado 的 Web 服务器模块, 该模块中实现了 HTTPServer —— 一个单线程 HTTP 服务器,其实现是基于 tornado.tcpserver 模块的 TCPServerTCPServer 是一个非阻塞单线程的 TCP 服务器,它负责处理 TCP 协议部分的内容,并预留接口(handle_stream 抽象方法)以便针对相应的应用层协议编写服务器。所以在分析 tornado HTTP 服务器实现之前,我们先来看看 tornado.tcpserver.TCPServer 的实现。

TCPServer

tornado.tcpserver 模块中只定义了一个 TCPServer 类,由于其实现不涉及到具体的应用层协议(例如Http协议),加上有 IOLoopIOStream 的支持,其实现比较简单。

TCPServer 是一个非阻塞的单线程 TCP Server,它提供了一个抽象接口方法 handle_stream 供具体的子类去实现,同时支持多进程的运行方式。按照源码注释,通过调用不同的方法我们有 3 中方式使用一个 TCP Server。

三种使用模式

使用 listen 的单进程模式

通过 TCPServerlisten 方法以单进程的方式运行服务器实例。示例代码如下:

1
2
3
server = TCPServer()
server.listen(8888)
IOLoop.instance().start()

TCPServer 提供的 listen 方法可以立即启动在指定的端口进行监听,并将相应的 socket 加入到 IOLoop 中。该方法可以多次调用,同时监听多个端口。由于需要 IOLoop 来驱动,所以必须确保相应的 IOLoop 实例已经启动(上述示例代码实例化 server 用的是默认 IOLoop 实例,所以通过 IOLoop.instance().start() 启动)。

Read more »

tornado.util 模块中的 Configurable 是一个抽象类,该类通过继承机制为实现它的类型提供了一个简单工厂的功能。具体来说就是 Configurable 通过重写 __new__ 方法来自定义类的创建,把类的构造函数变成了一个简单工厂,只要一个类继承了 Configurable,那么这个类在实例化时,构造函数就能像简单工厂一样选择这个类的一个子类来实例化。

一般在其他 OO 语言中我们都是通过提供静态工厂方法来实现该功能,有赖于 python 的实例化机制,这里通过重写 __new__ 方法而把类的构造函数变成了简单工厂,使用时更直观一些,通过调用类型的构造函数就完成了选择实例化。

Configurable 类有两个抽象方法:configurable_base(cls)configurable_default(cls),前者通常返回直接继承自 Configurable 的基类类型,后者返回实例化时默认使用的子类型。

目前在 tornado 中直接继承自 Configurable 的基类类型是 AsyncHttpClient, IOLoopResolve。也就是说我们可以通过 AsyncHTTPClient(), IOLoop()Resolver() 直接完成从这些基类类型的子类型中选择一个配置好的类型来实例化。

Read more »

计算掉落概率

在游戏中经常遇到按一定的掉落概率来随机掉落指定物品的情况,例如按照:钻石10%,金币20%,银币50%,饰品10%,装备10% 来计算掉落物品的类型。

通常的做法是将物品掉落概率(或者权重)变成一个离散的列表,随后产生一个随机数,再在列表中找到第一个大于该随机数的数,这个数对应的下标也就是对应的物品类型。

对应前面的例子,第一步会构建一个列表 [0.1, 0.3, 0.8, 0.9, 1.0] ;第二步生成一个随机数 0.56(假设);第三步在列表中查找到第一个大于 0.56 的数是 0.8,下标为 2,此时掉落物品应该为银币。

这种算法比较直观,理解和实现起来也比较容易。很多时候甚至都不需要预先构建列表,而是每次累加概率直到找打大于随机数的那个数的下标。但是运用这个算法的时候,第三步我们往往使用的是顺序查找,这在掉落类型较多的时候确实不怎么好。当然大多数情况类型的种类并不是一个很大的数,所以其实也没有影响。(后面实现时候采用二分查找)

在上周的技术交流会时,同事提到了一个掉落概率算法 Alias Method,这个算法比较有意思,实现的很巧妙。算法的论文在这里:《Darts, Dice, and Coins: Sampling from a Discrete Distribution》

(以下引用自 《抽奖概率-三种算法》)
Alias Method 算法大概是这么做的:把 N 种可能性拼装成一个方形(整体),分成 N 列,每列高度为 1 且最多 2 种可能性。可能性抽象为某种颜色,即每列最多有 2 种颜色,且第 n 列中必有第 n 种可能性,这里将第 n 种可能性称为原色。 想象抛出一个硬币,会落在其中一列,并且是落在列上的一种颜色。这样就得到两个数组:一个记录落在原色的概率是多少,记为 Prob 数组,另一个记录列上非原色的颜色名称,记为 Alias 数组,若该列只有原色则记为 null。

为了直接用网上的图片,我把前面例子的掉落概率依次改为 1/4, 1/5, 1/10, 1/20, 2/5。

由上图方形可得到两个数组: Prob: [3/4, 1/4, 1/2, 1/4, 1] Alias: [4, 4, 0, 1, null] (记录非原色的下标)。之后就根据 Prob 和 Alias 获取其中一个物品,随机产生一列 C,再随机产生一个数 R,通过与Prob[C] 比较,R 较大则返回 C,反之返回 Alias[C]。

Alias Method 算法

算法论文中已经有了一个 java 的版本,这里我就按照作者 java 实现 “翻译” 了 python 和 C# 版本。

Read more »

从 Files/Sockets 到 Stream

tornado.iostream 模块为 Tornado 提供了一系列读写非阻塞 files/sockets 的工具类。该模块主要包括以下 4 个主要的工具类:

  • BaseIOStream: 基础流读写接口,作为特定流的父类;
  • IOStream: 针对非阻塞 sockets 的流实现;
  • SSLIOStream: SSL-aware版本的 IOStream 实现;
  • PipeIOStream: 针对管道(Pipe)的流实现;

BaseIOStream 作为基础流读写接口,实现了大部分的功能封装。后面的源代码分析中,主要就是基于该类并结合其非阻塞 socket 版本的 IOStream 来讨论。

查看该模块的时候,我们会发现两个模块内函数 _double_prefix(deque)_merge_prefix(deque, size)。这两个工具函数的实现都很简单,但是为流的读写提供通用的操作数据块(chunk)的功能:

  • _double_prefix(deque): 该函数提供了将 buffer 的第 1 个 chunk 增大至少 1 倍的功能,该功能现在用在按条件在流的 buffer 中搜索匹配字符串时逐渐扩大搜索的数据块大小。
  • _merge_prefix(deque, size): 该函数提供了将 buffer 的第 1 个 chunk 调整到指定 size 大小。这在读写流时非常有用, _double_prefix(deque) 就是通过该函数来调整 chunk 大小的。在将流的 write_buffer 写入 fd 时,通过该函数适当调整第 1 个 chunk 的大小,我们就可以直接操作 buffer 的第 1 个 chunk 来达到操作整个 buffer 的目的,简化了实现的难度。详细可见 BaseStream._handle_writ 函数实现代码。

IOStream

一些基础知识

在源码的开始部分,作者写了一大段介绍 recv/send 与 read/write 函数的区别,以及各平台的操作非阻塞 I/O 时返回的错误码。recv/send 与 read/write 函数的区别大体上就是说,前者是特化的函数,提供了一些额外的选项来控制 fd 的读写操作,针对具体的 fd 实例你可以设置选项忽略 SIGPIPE 信号或者让 socket 发送带外数据等等, 后者只是提供了通用的 fd 读写操作。对于操作非阻塞 fd 返回的错误码,如下模块的静态变量对应的注释所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 非阻塞操作时,缓冲区满(无法写)时或者缓冲区空(读不到数据)时返回 EAGAIN, BSD 下使用 EWOULDBLOCK, Windows下使用 WSAEWOULDBLOCK
_ERRNO_WOULDBLOCK = (errno.EWOULDBLOCK, errno.EAGAIN)

# For windows
if hasattr(errno, "WSAEWOULDBLOCK"):
_ERRNO_WOULDBLOCK += (errno.WSAEWOULDBLOCK,)

# These errnos indicate that a connection has been abruptly terminated.
# They should be caught and handled less noisily than other errors.
#
# `ECONNRESET`: 该异常一般发生在连接的一端(A)进程较另一端(B)提前终止时。A 进程终止时会向 B 发送 FIN 后进入
# FIN_WAIT1 状态,B 回应 ACK,A 收到 FIN 的 ACK 进入 FIN_WAIT2 状态。B 收到 FIN 时,会向应用程序交付 EOF,
# 进入 CLOSE_WAIT 状态。若此时 B 进程没有正常处理 FIN(例如被阻塞)而再次向处于 FIN_WAIT2 的 A 发送数据,将会
# 收到 RST,引发该错误。
#
# `ECONNABORTED`: 软件引起的连接中止,当服务端和客户端完成三次握手后,服务端正在等待服务进程调用 accept 时候却收到客户端
# 发来一个 RST 分节,引发该错误。POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中
# 止的连接,服务进程将永远不知道该中止的发生。服务器进程一般可以忽略该错误,直接再次调用 accept。
#
# `EPIPE`: 错误被描述为 "broken pipe" ,即 "管道破裂",这种情况一般发生在客户进程不理会(或未及时处理)socket 错误,
# 而继续向 socket 写入更多数据时,内核将向客户进程发送 SIGPIPE 信号,该信号默认会使进程终止(此时该前台进程未进行 core dump)。
#
# `ETIMEDOUT`: 连接超时, 这种错误一般发生在服务器端崩溃而不响应客户端 ACK 时,客户端最终放弃尝试连接时引发该错误。
_ERRNO_CONNRESET = (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE,
errno.ETIMEDOUT)

# For windows
if hasattr(errno, "WSAECONNRESET"):
_ERRNO_CONNRESET += (errno.WSAECONNRESET, errno.WSAECONNABORTED, errno.WSAETIMEDOUT)

# More non-portable errnos:
# 在以非阻塞方式 connect() 时,返回的结果如果是 -1 ,并且错误号为 EINPROGRESS ,那么表示
# 连接还在进行并处理中(IN PROGRESS),而不是真的发生了错误。
_ERRNO_INPROGRESS = (errno.EINPROGRESS,)

# For windows
if hasattr(errno, "WSAEINPROGRESS"):
_ERRNO_INPROGRESS += (errno.WSAEINPROGRESS,)

Read more »

基础概念

机器数与真值

机器数(computer number)是将符号”数字化”的数,是数字在计算机中的二进制表示形式。机器数有2个特点:一是符号数字化,在计算机用一个数的最高位存放符号, 正数为0, 负数为1;二是其数的大小受机器字长的限制。比如在字长 8bit 的计算机中,+8 机器数就是 00001000,而 -8 的机器数则是 10001000。

不带符号的数是数的绝对值,在绝对值前加上表示正负的符号就成了符号数。直接用正号 “+” 和负号 “-” 来表示其正负的二进制数叫做符号数的真值。比如, 01101 和 11101 是两个机器数,而它们的真值分别为 +1101 和 -1101。

根据小数点位置固定与否,机器数又可以分为定点数和浮点数。 通常,使用定点数表示整数,而用浮点数表示实数。后面我们讨论的是定点数,即有符号整数。

有符号整数的表示:原码、反码、补码

注:只有有符号整数才存在不同的编码方式,无符号数没有原码、反码和补码一说。

下面以字长 8bit 的机器数来举例。

原码

将真值中的 “+” 、“-” 分别用 1、0 代替就叫做数的原码形式,简称原码。

1
2
[+1]原 = 0000 0001
[-1]原 = 1000 0001
反码

对正数来说,其反码和原码的相同。负数的反码是在其原码的基础上, 符号位不变,其余各个位取反。

1
2
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
补码

对正数来说,其反码和原码的相同。负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后加1(也就是反码末位加 1)。

1
2
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补

综上所述,对于正整数而言,原码、反码、补码都一样,只有负整数原码、反码、补码表示不同。

计算机中有符号整数的存储

我们都知道, 在数学上减一个数等于加上这个数的相反数,所以有符号整数的加减法运算都可以视为加法运算。这没有什么问题,但是对于二进制存储的有符号整数,由于 “符号位” 参与运算便会有一些问题:

  1. 若使用原码计算,涉及到负整数时就必须对符号位做特殊处理,并且还有 +0 和 -0 的问题;
  2. 若使用反码计算,符号位不需要特殊处理,但由于反码与原码的取值范围相同,所以也有 +0 和 -0 的问题;
  3. 使用补码则没有上述问题,但是补码的取值范围与原码不同,补码表示的最小值没有对应的原码。

注:使用原码和反码存储数都会存在一个 +0 与 -0 的问题,比如 8bit 字长的有符号整数,[0000 0000]原 和 [1000 0000]原 都表示0,[0000 0000]反和[1111 1111]反 都表示0,虽然能够理解,但这实际上没有什么意义。用补码表示时,[0000 0000]补 表示 +0,没有 -0;[1111 1111]补 表示 -1, [1000 0000]补 表示 -128,可以多保存一个最小值,所以 32 位 int 类型, 可以表示范围是: [-2^31, 2^31-1],用原码或者补码都只能表示 [-2^31 + 1, 2^31-1]。

在计算机中,加减法是基础运算,需要设计的足够简单。对于有符号整数,若让计算机执行加减法时还要去识别 “符号位” ,那么光基础电路至少就得设计两套,显然是复杂了。加上数字电路实现加法电路比减法电路要简单(不要问为什么,我已经还给大学老师了),所以在现代计算机中有符号整数是采用补码存储的(据说历史上曾经生产过使用反码存储的计算机)。

Read more »

引言

在 IP 分组通过路由器或防火墙时重写源 IP 地址或/和目的 IP 地址,网络地址转换 NAT(Network Address Translation)技术提供了一种完全将私有网和公共网隔离的技术。它允许用 1个或多个 IP 地址来实现 1 个私有网中的所有主机和公共网中主机的 IP 通信。

NAT 的类型

NAT 可以分为基础 NAT 和 NAPT(APT) 两大类。

基础 NAT

基础 NAT 一般用在当 NAT 拥有很多公网 IP 地址的时候,它将公网 IP 地址与私有网内部主机进行绑定,当私有网主机和公共网主机通信的 IP 包经过 NAT 网关时,将 IP 包中的源 IP(发送时)或目的 IP(接收时)在私有 IP 地址和 NAT 的公网 IP 地址之间进行转换。

注:图片来自于网络。基础 NAT 虽然只对 IP 地址进行转换,但是通过 NAT 网关可以对外部数据进行拦截,提供防火墙的功能,这与直接为主机设置公网 IP 地址还是不一样。

NAPT(PAT)

基础 NAT 中 1 台私有网内部主机要求有一个公网 IP 地址与之对应,这样就导致私有网内部主机数量受到可用公网 IP 数量的限制。显而易见,大多数情况下我们的主机数量远远多于可用的公网 IP 地址数。为了解决这个问题,NAT 进一步扩展为在进行 IP 地址转换的同时也进行 Port (端口)转换,这就是网络地址端口转 NAPT(Network Address Port Translation/Port Address Translation,所以也称为 APT)。NAPT 使得多台私有网主机可以同时利用 1 个公网 IP 地址与公网进行通信。

注:图片来自于网络。

当一个私有网主机通过 NAT 打开一个 “外出” 的 TCP 或 UDP 会话时,NAPT 分配给这个会话一个公网 IP 地址和端口,用来接收公网的响应的数据包,并经过转换通知私有网的主机。这样 NAPT 就在[私有网 IP 地址:私有端口] 和[公网 IP 地址:公网端口]之间建立了一个端口绑定。

Read more »

写在开始之前

tornado.platform.posix 模块提供了 POSIX 平台下需要用到的一些功能,内容比较少,但是由于之前的工作主要是开发 windows 应用程序,对于这部分不熟悉。虽然这个模块目前提供的功能很少,但是涉及的操作系统底层知识还是值得记录下来。

在开始阅读源码之前,先来看看 UNIX I/O 的文件描述符相关知识。

文件描述符

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念,基于文件描述符的I/O操作兼容 POSIX 标准(在 UNIX 中,一切 I/O 相关的实体都被抽象成了文件。Linux 的设计思想便是把一切设备都视作文件,文件描述符为在该系列平台上进行设备相关的编程实际上提供了一个统一的方法。)。

注:在非 UNIX/Linux 操作系统上,如 Windows 下的文件描述符和信号量、互斥锁等内核对象一样都记作 HANDLE。

文件描述符相当于一个逻辑句柄,在形式上是一个非负整数,open 、close 等 I/O 相关函数就是将文件或物理设备与该逻辑句柄相关联在一起。这个数字实际上是文件描述符表的索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。在 UNIX/Linux 下有 3 个概念与进程打开的文件描述符有关:

  • 文件描述符表,每个进程在 PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引。该表位于用户区。

  • 系统文件表,位于内核区,为系统中所有的进程共享。对每个 open 的文件, 它都包含一个条目与之对应。每个系统文件表的条目都包含文件偏移量、文件状态标识(访问模式,读、写或者读-写)以及指向它的文件描述符表的条目计数。

  • 内存索引节点表,位于内核区,对系统中的每个 open 的文件,内存中索引节点表都包含一个条目。几个系统文件表条目可能对应于同一个内存索引节点表(不同进程打开同一个文件)。

Read more »