Linux中文件描述符3有什么特别之处?

19

我正在开发一个服务器应用程序,将在Linux和Mac OS X上运行。它的步骤如下:

  • 启动主应用程序
  • 分叉控制器进程
  • 在控制器进程中调用lock_down()
  • 终止主应用程序
  • 然后,控制器进程再次分叉,创建工作进程
  • 最终,控制器将继续分叉更多的工作进程

我可以使用多种方法记录日志(例如syslog或文件),但现在我正在考虑syslog。有趣的是,除非我包括下面的#ifdef部分,否则在控制器进程中永远不会看到任何syslog输出。

在Mac OS X和Linux中,工作进程都可以无缝地记录日志,无论是否包含#ifdef部分。在Mac OS X中,即使没有#ifdef部分,控制器也可以无缝记录日志,但在Linux上,如果我想从控制器进程中看到任何输出到syslog(或日志文件)的内容,就需要#ifdef部分。

那么,为什么会这样呢?

static int
lock_down(void)
{
    struct rlimit rl;
    unsigned int n;
    int fd0;
    int fd1;
    int fd2;

    // Reset file mode mask
    umask(0);

    // change the working directory
    if ((chdir("/")) < 0)
        return EXIT_FAILURE;

    // close any and all open file descriptors
    if (getrlimit(RLIMIT_NOFILE, &rl))
        return EXIT_FAILURE;
    if (RLIM_INFINITY == rl.rlim_max)
        rl.rlim_max = 1024;

    for (n = 0; n < rl.rlim_max; n++) {
#ifdef __linux__        
        if (3 == n) // deep magic...
            continue;
#endif
        if (close(n) && (EBADF != errno))
            return EXIT_FAILURE;
    }

    // attach file descriptors 0, 1 and 2 to /dev/null
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup2(fd0, 1);
    fd2 = dup2(fd0, 2);
    if (0 != fd0)
        return EXIT_FAILURE;

    return EXIT_SUCCESS;
}

camh 的想法接近正确,但最终解决问题的是 jilles 提出的使用 closelog()。除了从 syslogs 脚下关闭文件描述符之外,还有其他事情需要做。为了使代码正常工作,我在循环前添加了一个调用 closelog() 的语句:

closelog();
for (n = 0; n < rl.rlim_max; n++) {
    if (close(n) && (EBADF != errno))
        return EXIT_FAILURE;
}

我曾依靠手册页面的字面理解,其中写到:

使用openlog()是可选的;如果需要,它将由syslog()自动调用...

我曾认为这意味着syslog会检测其下的文件描述符是否关闭。但事实并非如此。在Linux上需要显式使用closelog()来告诉syslog描述符已关闭。

还有一件令我困惑的事情是,不使用closelog()防止第一个复制的进程(控制器)甚至无法打开和使用日志文件。以下复制的进程可以毫无问题地使用syslog或日志文件。也许文件系统中存在某些缓存效应,使得第一个复制的进程对可用的文件描述符有一个不可靠的“观念”,而下一组复制的进程由于延迟足够长,因此不受此影响?


4
+1 是对“// deep magic...”的赞同 :-) 当你拥有 Linux 内核时,谁还需要哈利·波特呢? - David Z
那时候在文件描述符3上是什么? - Gilles 'SO- stop being evil'
@David。和内核没有任何关系。即使是fd 0、1和2对内核也没有特殊意义。 - camh
@camh:我知道的,这只是个玩笑。如果你觉得更好,可以随意替换为“标准文件描述符分配”。 - David Z
4个回答

16

文件描述符3的特殊之处在于,通常它会是分配新文件描述符的系统调用返回的第一个文件描述符,因为0、1和2通常被设置为标准输入、标准输出和标准错误。

这意味着,如果你调用的任何库函数为了执行其功能而分配一个文件描述符用于其内部目的,那么它将得到fd 3。

openlog(3)库调用将需要打开/dev/log与syslog守护进程通信。如果随后关闭所有文件描述符,则可能会破坏syslog库函数(如果它们没有以处理该情况的方式编写)。


9
在Linux上调试这个问题的方法是使用strace跟踪实际被调用的系统调用; 通过文件描述符记录系统日志的使用方式将变得明显:
$ cat syslog_test.c
#include <stdio.h>
#include <syslog.h>

int main(void)
{
    openlog("test", LOG_PID, LOG_LOCAL0);
    syslog(LOG_ERR, "waaaaaah");
    closelog();
    return 0;
}
$ gcc -W -Wall -o syslog_test syslog_test.c
$ strace ./syslog_test
...
socket(PF_FILE, SOCK_DGRAM, 0)          = 3
fcntl64(3, F_SETFD, FD_CLOEXEC)         = 0
connect(3, {sa_family=AF_FILE, path="/dev/log"}, 16) = 0
send(3, "<131>Aug 21 00:47:52 test[24264]"..., 42, MSG_NOSIGNAL) = 42
close(3)                                = 0
exit_group(0)                           = ?
Process 24264 detached

6

syslog(3) 可能会保持一个文件描述符以打开 syslogd 的套接字; 在其脚下关闭这个描述符可能会导致问题。调用 closelog(3) 可能会有所帮助。


1

Syslog在启动时绑定到给定的描述符。大多数情况下是描述符3。如果您关闭它,则没有日志。

syslog-ng -d -v

提供更多关于它在幕后执行的信息。

输出应该类似于这样:

binding fd 3, inetaddr: 0.0.0.0, port: 514
io.c: Preparing fd 3 for reading
io.c: Preparing fd 4 for reading
binding fd 5, unixaddr: /dev/log
io.c: listening on fd 5

这对于调试syslog-ng很有用...但对于任何其他程序都没有作用。在syslog-ng中的fd3可以轻松连接到其他进程中的fd9。 - Zan Lynx
1
正确,但解释了为什么他在运行代码后没有在日志中得到任何东西,这似乎对他来说不清楚(即syslog使用给定的文件描述符进行通信)。 - jdehaan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接