数字营销 · Web开发 · 基础设施

Linux中的dup和dup2函数

通过 Python 示例理解 Linux 的 dup 和 dup2 函数,以及利用文件描述符实现标准输入输出和错误输出的重定向。

在前面一篇《用Python给Linux编写守护进程》的文章中,讲到了如何利用 Python 给 Linux 编写一个守护进程。守护进程不依赖用户终端,所以程序输出的信息用户是察觉不到的。但是在实际应用中,我们又经常需要记录一些信息,例如 Web 服务中需要记录访问日志、错误日志等。在著名的 Web 服务器 Nginx 中,我们就可以通过 access_log 和 error_log 分别指定访问日志和错误日志的存放位置。

首先要讲到 Linux 系统中的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。

在 Unix 和类 Unix 系统中,标准流是程序运行时与外部环境之间最基本的输入输出通道,通常默认连接到终端。这三个 I/O 通道分别叫做:

  • 标准输入(stdin)
  • 标准输出(stdout)
  • 标准错误(stderr)

它们对应的文件描述符分别是:

stdin   = 0
stdout = 1
stderr = 2

在正常运行的进程中,这三个标准流通常都指向用户当前使用的终端,也就是说,进程通过终端获取输入,同时也通过终端输出正常信息和错误信息。下面是三个文件描述符的指向信息:

[root@home fd]# ls -la

total 0
dr-x------ 2 root root 0 Aug 3 14:52 .
dr-xr-xr-x 7 root root 0 Aug 3 14:52 ..

lrwx------ 1 root root 64 Aug 3 14:52 0 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 3 14:52 1 -> /dev/pts/0
lrwx------ 1 root root 64 Aug 3 14:52 2 -> /dev/pts/0

从上面的信息可以知道,只要我们把相应的文件描述符重新指向日志文件,而不是原来的终端,那么进程输出的信息自然就会记录到文件中。Linux 中有两个比较常用的函数 dup 和 dup2,都可以用来复制文件描述符:

int dup(int filedes)
int dup2(int oldfd, int newfd)

dup函数

先来看一下 dup。

当 dup 函数执行时,它会复制原来的文件描述符,并返回一个新的文件描述符。这个新的文件描述符会使用当前进程中最小的可用文件描述符。利用 dup 的这个特点,就可以实现标准输入、输出和错误输出的重定向。来看下面的 Python 代码:

#!/usr/bin/env python

import os
import sys

error_log = "/var/error.log"
access_log = "/var/access.log"

ehandle = open(error_log, "a")
ahandle = open(access_log, "a")

os.close(sys.stdout.fileno())
os.close(sys.stderr.fileno())

fdout = os.dup(ahandle.fileno())
fderr = os.dup(ehandle.fileno())

ehandle.close()
ahandle.close()

if fdout == 1:
print "Stdout Redirection success!"
else:
print "error stdout redirection"

if fderr == 2:
print "Stderr Redirection success!"
else:
print "error stderr redirection"

这里首先关闭了原来的标准输出和标准错误:

os.close(sys.stdout.fileno())
os.close(sys.stderr.fileno())

因为 1 和 2 已经变成可用的文件描述符,所以后面的 dup() 就会优先使用这两个最小的文件描述符。

因此:

fdout

会得到 1,而:

fderr

会得到 2。

这样,标准输出就被重新指向了 access_log,标准错误则被重新指向了 error_log。测试之后可以发现,进程中的标准输出和标准错误已经完成了重定向,print 输出的内容会出现在相应的日志文件中。

dup2函数

再来说一下 dup2。

从上面的 dup 可以看到,系统会自动选择一个没有使用的最小文件描述符。而 dup2 不一样,它允许我们自己指定新的文件描述符。它的作用就是:

将 oldfd 复制到指定的 newfd,让 newfd 和 oldfd 最终指向同一个文件。

这样,我们就可以更加直接地实现文件描述符的重定向,例如:

#!/usr/bin/env python

import os
import sys

error_log = "/var/error.log"
access_log = "/var/access.log"

ehandle = open(error_log, "a")
ahandle = open(access_log, "a")

os.dup2(
ehandle.fileno(),
sys.stderr.fileno()
)

os.dup2(
ahandle.fileno(),
sys.stdout.fileno()
)

ehandle.close()
ahandle.close()

这里:

os.dup2(
ehandle.fileno(),
sys.stderr.fileno()
)

相当于直接把标准错误 2 重定向到了 error_log,而:

os.dup2(
ahandle.fileno(),
sys.stdout.fileno()
)

则把标准输出 1 重定向到了 access_log。

相比前面的 dup,这种写法不需要先关闭 1、2,也不需要依赖系统自动选择文件描述符,所以理解起来稍微复杂一点,但是实际操作起来反而更加直接。

总结

dup 和 dup2 都可以用来实现 Linux 下文件描述符的复制和标准流重定向,简单来说:

dup()
自动选择最小的可用文件描述符

dup2()
由我们指定新的文件描述符

通过这两个函数,就可以把原本输出到终端的信息重新指向日志文件。对于守护进程来说,这种方式尤其有用,因为守护进程已经脱离了用户终端,但程序仍然可能需要记录正常输出和错误信息。

理解和掌握 dup、dup2,对于进一步理解 Linux 文件描述符以及系统编程也是非常有帮助的。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。