在多线程环境下,什么可能导致“坏的文件描述符”错误?

4

这个问题与Bad file descriptor有点相似,但完全不同。我知道这是一个“坏问题”(可能是“太局限性了”),但我想不出来,现在已经没有任何想法。


介绍

我有一个管理线程,它启动了75个其他线程。每个线程都做很多事情,所以我只描述相关的事情。

请注意:如果我只启动几个线程——例如3个、5个或10个——就不会出现此错误!这让我认为,这是一些多线程问题,但似乎并不是这样... 在下一节中,您将看到为什么。

因此,在以下2种情况下,有时会出现Bad file descriptor错误:


情况1

错误出现在TinyXML

有一个xml文件,所有线程都需要。所有这些线程都使用TinyXML解析文件。所有这些线程都只读使用此文件!(我知道这可以优化,但无论如何)。

因此,导致Bad file descriptor错误的代码如下:

// ...
// NOTE: this is LOCAL, other threads do NOT have access to it
TiXmlDocument   doc;
doc.LoadFile( filename );

// and here's the LoadFile:
bool TiXmlDocument::LoadFile( const char* _filename, TiXmlEncoding encoding )
{
    //...
    FILE* file = fopen( value.c_str (), "rb" ); 
    if ( file )
    {
        // this IS executed, so file is NOT NULL for sure
        bool result = LoadFile( file, encoding );
        //...
    }
    //...
}

bool TiXmlDocument::LoadFile( FILE* file, TiXmlEncoding encoding )
{
    // ...
    long length = 0;
    fseek( file, 0, SEEK_END );
    // from the code above, we are SURE that file is NOT NULL, it's valid, but
    length = ftell( file ); // RETURNS -1 with errno: 9 (BAD FILE DESCRIPTOR)
    // how is this possible, as "file" is not NULL and it appears to be valid?
    // ...
}

案例2

这个有点复杂。我已经删除了返回值的检查,但是在我的实际代码中有它们,所以这不是问题。

int hFileR = open( sAlarmFileName.c_str(), O_CREAT | O_RDONLY, 
                   S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
// hFileR is > 0 for sure, so success 

flock( hFileR, LOCK_EX ) /* the result is > 0 for sure, so success*/ 

// read the file into a string
while( (nRes = read(hFileR, BUFF, MAX_RW_BUFF_SIZE)) > 0 ) // ...

//Write new data to file: reopen/create file - write and truncate mode
int hFileW = open( sAlarmFileName.c_str(), 
                   O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | 
                   S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH );
// hFileW is > 0 for sure, so success

do
{
    int nWrtRes = write( hFileW, 
                        szText + nBytesWritten, nSize - nBytesWritten ); 
    // nWrtRes is always >= 0, so success
    nBytesWritten +=  nWrtRes;
}
while( nSize > nBytesWritten );

close( hFileW );    // this one is successful too

if( flock(hFileR, LOCK_UN) == -1 )
{
    // THIS FAILS and executes _Exit( FAILURE );
}

if( close( hFileR ) < 0 )
{
    // if the previous one do not fail, this one is successful too
}

抱歉问题有点长,请问您有什么想法吗?

我很惊讶TinyXML在运行ftell()之前没有检查fseek()的返回值。 - WhozCraig
几乎可以确定这是多线程问题。在使用 FILE * 的两个位置之间,其他一些线程关闭了底层文件描述符。最可能的原因是代码中某处存在双重关闭。 - David Schwartz
2
顺便说一下,这个难以找到的 bug 最常见的原因是代码有意调用另一个线程的文件描述符上的 close 方法,以此来让那个线程停止。例如,TCP 代码中有一个“读线程”和一个“写线程”,可能会愚蠢地让其他线程调用描述符上的 close 方法来使读写线程失败。除了许多可怕的后果之外,如果另一个线程在写线程即将写入时获取相同的描述符,则写线程可能会将敏感数据写入错误的连接。请使用 shutdown 方法。 - David Schwartz
3个回答

6

需要注意的一点是关闭同一文件描述符两次的代码。

在单线程程序中,这是一个无害的编程错误,因为第二个 close() 不会做任何事情,只会返回 EBADF ,而且很多代码也不会检查 close() 的返回值。然而,在多线程程序中,关闭描述符之间分配的描述符号可以在另一个线程中分配,在两个 close() 调用之间,因此第二个 close() 将关闭来自另一个线程的不相关的套接字。其他线程的进一步读取和写入将导致“坏文件描述符”错误。


@MooingDuck:你怎么想的?很可能是其他代码关闭了底层文件描述符。 - David Schwartz
@MooingDuck:另一个线程在自己的文件描述符上调用close。然后这段代码运行并分配完全相同的文件描述符,因为它现在是空闲的。然后另一个线程再次调用close,因为它已经损坏了。然后这段代码再次运行并发现其文件描述符已关闭。就另一个线程而言,一切都很好。 - David Schwartz
@DavidSchwartz:而且这个答案甚至明确地说了。我是个糟糕的读者:( - Mooing Duck

3

理解文件描述符的一些信息:

文件是全局资源。为了处理这种情况,使用(进程)全局索引:整数值,称为文件描述符。如果线程打开一个文件,则该打开的文件由索引引用。该索引对于进程唯一(而不是对于线程)。如果关闭文件,则不再使用文件描述符(整数索引),并且可以被进程(及其任何线程)重复使用。

例如:

在进程中,任何线程对open()函数的第一次调用可能会返回3,第二次可能会返回4。

如果然后关闭3,则对open()的第三个调用可能再次返回3。

如果第一个调用由线程1执行,第二个调用由线程2执行,第三个调用由线程3执行,则易于理解线程1不应再次关闭其文件描述符,因为3的值可能已经被回收并由线程3使用,它将尝试访问无效的文件描述符,因为它可能已被第一个(错误的)调用close()关闭的线程1关闭。好吧? ;-)

尝试设置一些示例代码,并检查/记录通过调用open()返回的整数值并分配为文件描述符,以了解其工作原理。

注意:

这也可能是stdinstdoutstderr,即“预定义”文件描述符012。在最近的Linux中,关闭stdin,然后调用int fd = open("myfoofile.bar", ...)可能会将0作为文件描述符fd返回。无论如何,内核或glibc都不能像预期那样处理这样的0。例如,使用lseek(fd,...)时可能会出现模糊错误。试试吧!;-)>>


1
结尾的注释是错误的。当 stdin 被关闭时,open 不仅可能会返回0;它必须返回0,因为它是最低可用的文件描述符。在单线程程序中,这是替换 stdin 的有效方法,尽管它存在竞争条件,不适用于多线程程序。内核和 glibc 都没有使用文件描述符0的问题;它们一直将其用作stdin。 - R.. GitHub STOP HELPING ICE

2

如果应用程序是多线程的,那么可能会发生这种情况:某个线程关闭了文件,而另一个线程仍然尝试访问它。

(因为像地址空间一样,文件描述符是进程的所有线程都共享的全局变量)

您可以使用strace来了解执行的系统调用。


1
实际上,本地文件描述符并不存在。它们是进程级别的资源。 - David Schwartz
1
@MooingDuck 我的回答提供了一种实现这一点的方法,但还有其他方法。文件描述符有时会在线程之间显式共享,在全局表中存储等。错误是难免的。 - user4815162342
此外,文件描述符只是整数(从进程的角度来看)。一个醉醺醺的线程可能会随意拨打一些随机整数并使用close()函数破坏其他线程的本地FILE *对象。虽然这种情况不太可能发生,但我喜欢想象它正在发生。 - frankc
1
@KirilKirov:你不需要使用互斥锁来同步线程。你只需要确保另一个线程永远不会在不应该操作的文件描述符上调用函数。例如,如果一个线程在同一文件描述符上两次调用close,那么它可能会对在这两个close调用之间调用openfopen的线程造成严重影响(如果它被分配了现在空闲的文件描述符)。 - David Schwartz
1
@KirilKirov:这段代码是受害者,很可能不是罪犯。可能是完全不同的代码错误地关闭了这段代码的文件描述符。 - David Schwartz
显示剩余6条评论

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