Что может вызвать плохой файловый дескриптор в многопоточной среде?

Этот вопрос чем-то похож на Неверный файловый дескриптор, но это совсем не то же самое. Я знаю, что это плохой вопрос (возможно, слишком локализованный), но я не могу понять это, и теперь у меня нет никаких идей.


Введение

У меня есть поток менеджера, который запускает 75 других потоков. Каждый из этих потоков делает много вещей, поэтому я опишу только соответствующие.

Обратите внимание: если я запускаю только несколько тем - например, 3, или 5, или 10, эта ошибка не появляется! Это наводит меня на мысль, что это какая-то проблема с многопоточностью, но похоже, что это не так. И вы поймете почему в следующих разделах.

Итак, в следующих двух случаях ИНОГДА я получаю эту ошибку 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
}

Извините за длинный вопрос. Есть идеи?


person Kiril Kirov    schedule 07.12.2012    source источник
comment
Я удивлен, что TinyXML не проверяет ret-val этого fseek() перед переходом к ftell().   -  person WhozCraig    schedule 07.12.2012
comment
Это почти наверняка проблема многопоточности. Между двумя использованиями FILE * какой-то другой поток закрыл базовый файловый дескриптор. Наиболее вероятной причиной этого является двойное закрытие где-то еще в коде.   -  person David Schwartz    schedule 07.12.2012
comment
Между прочим, наиболее распространенной причиной этой трудно обнаруживаемой ошибки является код, который преднамеренно вызывает close для файлового дескриптора другого потока, чтобы остановить этот другой поток. Например, код TCP, который имеет поток чтения и поток записи, может по идее иметь какой-то другой поток, вызывающий close для дескриптора, чтобы сделать потоки чтения и записи неудачными. Среди многих ужасных вещей, которые это вызывает, если другой поток получает тот же дескриптор как раз в тот момент, когда поток записи собирается записать, поток записи может записать конфиденциальные данные в неправильное соединение. Используйте shutdown.   -  person David Schwartz    schedule 07.12.2012


Ответы (3)


Несколько слов о понимании файловых дескрипторов:

Файлы являются глобальными ресурсами. Чтобы справиться с этим, используется глобальная индексация (процесса): целочисленные значения, называемые файловыми дескрипторами. Если поток открывает файл, на этот открытый файл ссылается индекс. Этот индекс уникален для процесса (не для потока). Если файл закрыт, дескриптор файла (целочисленный индекс) больше не используется и может быть повторно использован процессом (и любым из его потоков).

Пример:

В любом потоке процесса первый вызов open() может вернуть 3, второй — 4.

Если затем 3 закрыто, 3-й вызов open() может снова вернуть 3.

Если первый вызов выполняется потоком 1, второй — потоком 2, а третий — потоком 3, легко понять, что поток 1 не должен снова закрывать свой файловый дескриптор, поскольку значение 3 уже могло быть повторно использовано и в используется потоком 3, который попытается получить доступ к недопустимому файловому дескриптору, поскольку он мог быть закрыт вторым (ошибочным) вызовом close() потоком 1. Хорошо? ;-)

Попробуйте создать какой-нибудь пример кода и проверьте/зарегистрируйте целочисленные значения, возвращаемые вызовами open() и назначенные в качестве файловых дескрипторов, чтобы получить представление о том, как это работает.

Примечание.

Это также может относиться к stdin, stdout и stderr, "предопределенным" файловым дескрипторам 0, 1 и 2. При недавнем закрытии Linux stdin с последующим вызовом int fd = open("myfoofile.bar", ...) вполне может вернуть 0 в качестве файлового дескриптора fd. В любом случае, ни ядро, ни glibc не могут обработать такой 0, как ожидалось. Непонятные ошибки могут возникать, например, при использовании lseek(fd, ...). Попытайся! ;->>

person alk    schedule 08.12.2012
comment
Примечание в конце неверно. Не только может open возвращать 0, когда стандартный ввод закрыт; он должен вернуть 0, так как это дескриптор файла с наименьшим доступным значением. В однопоточных программах это допустимый способ замены стандартного ввода, хотя, конечно, у него есть условия гонки, которые не позволяют использовать его в многопоточных программах. Ни у ядра, ни у glibc нет проблем с использованием файлового дескриптора 0; они оба используют его все время как стандартный ввод. - person R.. GitHub STOP HELPING ICE; 14.08.2013

Следует обратить внимание на код, который дважды закрывает один и тот же файловый дескриптор.

В однопоточной программе это безобидная ошибка программирования, потому что второй close() не делает ничего, кроме возврата EBADF, а большая часть кода все равно не проверяет возвращаемое close() значение. Однако в многопоточной программе номер дескриптора закрытого дескриптора может быть выделен в другом потоке между двумя вызовами close(), поэтому второй close() закроет несвязанный сокет из другого потока. Дальнейшие операции чтения и записи дескриптора другого потока вызовут ошибку «плохой файловый дескриптор».

person user4815162342    schedule 07.12.2012
comment
@MooingDuck: Как ты думаешь? Вполне вероятно, что какой-то другой фрагмент кода закрывает базовый файловый дескриптор. - person David Schwartz; 07.12.2012
comment
@MooingDuck: другой поток вызывает close для своего собственного файлового дескриптора. Затем этот код запускается и выделяет тот же дескриптор файла, поскольку теперь он свободен. Затем другой поток снова вызывает close, потому что он сломан. Затем этот код запускается снова и обнаруживает, что его файловый дескриптор закрыт. Что касается другой темы, то все в порядке. - person David Schwartz; 07.12.2012
comment
@DavidSchwartz: И этот ответ даже ясно говорит об этом. Я плохо читаю :( - person Mooing Duck; 07.12.2012

Если приложение многопоточное, может случиться так, что какой-то поток закрыл файл, а другой все еще пытается получить к нему доступ.

(поскольку файловые дескрипторы, как и адресное пространство, являются глобальными и общими для всех потоков процесса)

Вы можете использовать strace, чтобы понять, какие системные вызовы выполняются.

person Basile Starynkevitch    schedule 07.12.2012
comment
Вы можете объяснить, как другой поток может закрыть файловый дескриптор, локальный для функций другого потока. - person Mooing Duck; 07.12.2012
comment
На самом деле нет такой вещи, как дескриптор локального файла. Это ресурсы уровня процесса. - person David Schwartz; 07.12.2012
comment
@MooingDuck Мой ответ предлагает один путь для этого, но есть и другие. Дескрипторы файлов иногда явно распределяются между потоками, хранятся в глобальных таблицах и т. д. Случаются ошибки. - person user4815162342; 07.12.2012
comment
Кроме того, файловые дескрипторы - это просто целые числа (с точки зрения процесса). Пьяный поток может буквально просто пьяным набрать несколько случайных целых чисел с помощью close() и разрушить локальные объекты FILE * других потоков. Это не так вероятно, но именно так мне нравится представлять происходящее. - person frankc; 08.12.2012
comment
О боже! потому что файловые дескрипторы, как и адресное пространство, являются глобальными и общими для всех потоков процесса, я этого не знал. Я думал, что это какие-то локальные переменные, и я думал, что каждый open открывает НОВЫЙ файловый дескриптор для каждого потока - независимый FD, который не может быть затронут другими потоками... Итак, вы имеете в виду, что все вызовы open в разных потоках будут возвращать ОДИНАКОВЫЕ файловые дескрипторы? В этом случае я должен синхронизировать потоки, чтобы открыть файл один за другим, верно? Например, с static mutex? Или я что-то не так понял? - person Kiril Kirov; 09.12.2012
comment
Я просто имею в виду, что файловые дескрипторы, как и адресное пространство, являются общими для всех потоков процесса. Конечно, два разных open (например, в двух разных потоках) возвращают другой файловый дескриптор (если close не произошло). - person Basile Starynkevitch; 09.12.2012
comment
@KirilKirov: вам не нужно синхронизировать потоки с мьютексом. Вам просто нужно убедиться, что другой поток никогда не вызывает функцию для файлового дескриптора, которого он не должен касаться. Например, если поток дважды вызывает close для одного и того же файлового дескриптора, он может нанести ущерб потоку, который вызывал open или fopen между этими двумя вызовами close (если ему был назначен теперь свободный файловый дескриптор). - person David Schwartz; 10.12.2012
comment
@DavidSchwartz - я уверен, что этого не происходит. Например, в case 1 как то короче - каждый поток имеет свой TiXmlDocument и вызывает независимо LoadFile, где и появляется эта ошибка. Я уверен, что никакие другие темы не могут коснуться этого TiXmlDocument. Мне нужны другие идеи, к сожалению :\ - person Kiril Kirov; 10.12.2012
comment
@BasileStarynkevitch - каждый поток имеет свой собственный open, свой собственный close, свой собственный файловый дескриптор (int, возвращенный из open), и другие потоки не могут получить доступ к этому int .. (кажется, я неправильно понял ваш ответ в первый раз) Итак , я хотел бы услышать некоторые другие идеи. - person Kiril Kirov; 10.12.2012
comment
Я рекомендую использовать strace, чтобы понять, что происходит. (Вы можете написать крошечный скрипт awk для изучения этого файла трассировки, полученного с помощью strace). - person Basile Starynkevitch; 10.12.2012
comment
@KirilKirov: Этот код является жертвой, а не преступником. Это может быть совершенно другой код, который по ошибке закрывает файловый дескриптор этого кода. - person David Schwartz; 10.12.2012