0%

tornado.platform.posix 模块解析

写在开始之前

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 的文件,内存中索引节点表都包含一个条目。几个系统文件表条目可能对应于同一个内存索引节点表(不同进程打开同一个文件)。

python 的 fcntl 模块

fcntl 模块为 Unix 上的 ioctl(input/output control,I/O 控制) 和 fcntl(file control,文件控制)函数提供了一个接口。它们用于文件句柄和 I/O 句柄的 “out of band” 操作,包括读取扩展属性,控制阻塞,更改终端行为等等(out of band management: 指使用分离的渠道进行设备管理。 这使系统管理员能在机器关机的时候对服务器, 网络进行监视和管理。(引用自 http://en.wikipedia.org/wiki/Out-of-band_management)

其中的 fcntl 函数签名为 fcntl.fcntl(fd, op[, arg]),共有 5 中功能:

  1. 复制一个现有的描述符(op=F_DUPFD);
  2. 获得/设置文件描述符标记(op=F_GETFD 或 F_SETFD);
  3. 获得/设置文件状态标记(op=F_GETFL 或 F_SETFL);
  4. 获得/设置异步I/O所有权(op=F_GETOWN 或 F_SETOWN);
  5. 获得/设置记录锁(op=F_GETLK,F_SETLK 或 F_SETLKW);
  6. 其他更多函数请参考文档:https://docs.python.org/2/library/fcntl.html

tornado.platform.posix 模块

set_close_exec 函数

通过 fork() 创建子进程时,子进程以写时复制(COW,Copy-On-Write)方式获得父进程的数据空间、堆和栈副本,这其中也包括文件描述符。刚刚 fork() 成功时,父子进程中相同的文件描述符指向系统文件表中的同一项(这也意味着他们共享同一文件偏移量)。

一般我们会在子进程中调用 exec 一族的函数执行另一个程序,此时便会用全新的程序替换子进程的正文,数据,堆栈等,当然从父进程拷贝过来的文件描述就没法访问也就无法关闭。在 fork() 子进程时由于打开的文件可能较多,逐一去关闭有些不现实,但是为文件描述符设置 FD_CLOEXEC(close-on-exec)标识后,当子进程调用 exec 一族函数成功后便会自动(原子地)关闭该文件描述符。

1
2
3
def set_close_exec(fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
_set_nonblocking 函数

_set_nonblocking 函数将文件描述符设置为非阻塞模式。

1
2
3
def _set_nonblocking(fd):
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

这里额外提一下 O_NONBLOCK 和 O_NDELAY 的区别:
O_NONBLOCK 和 O_NDELAY 所产生的结果都是使 I/O 变成非阻塞模式(non-blocking),在读取不到数据或是写入缓冲区已满会马上 return ,而不会阻塞程序,直到有数据可读或写入完成。二者差别在于设立 O_NDELAY 会使 I/O 函数返回 0 ,于是又导致另外一个问题,因为读取到文件结尾时所返回的值也是0,这样无法得知是哪中情况。因此,O_NONBLOCK 就产生出来,它在读取不到数据时会回传 -1 ,并且设置 errno 为 EAGAIN 。

值得注意的是,在 GNU C 中 O_NDELAY 只是为了与 BSD 的程序兼容,实际上是使用 O_NONBLOCK 作为宏定义,而且 O_NONBLOCK 除了在 ioctl中使用,还可以在 open 时设定。

Waker 类

Waker 类是为了 IOLoop 进入 poll 等待时及时唤醒 poll,在 IOLoop 模块中有提到。其实现很简单,内部封装了一个管道,将写的文件描述符用于注册 signal.set_wakeup_fd。提供 wake 方法,在主动关闭 IOLoop 时可以通过写管道立即唤醒 poll。

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
39
class Waker(interface.Waker):
def __init__(self):
r, w = os.pipe()
_set_nonblocking(r)
_set_nonblocking(w)
set_close_exec(r)
set_close_exec(w)
# fdopen 返回的是 file object
self.reader = os.fdopen(r, "rb", 0)
self.writer = os.fdopen(w, "wb", 0)

# 这个方法不命名为 reader_fileno 是为了提供 file-like object(有 fileno() 方
# 法),这个在 IOLoop.split_fd 方法注释中有提到。
def fileno(self):
return self.reader.fileno()

def write_fileno(self):
return self.writer.fileno()

# 主动关闭 IOLoop 时可以通过写管道立即唤醒 poll
def wake(self):
try:
self.writer.write(b"x")
except IOError:
pass

# 注册到 IOLoop handler 中,及时清空管道
def consume(self):
try:
while True:
result = self.reader.read()
if not result:
break
except IOError:
pass

def close(self):
self.reader.close()
self.writer.close()