Pull to refresh

Comments 104

По поводу mmap есть нюанс:
       M_MMAP_THRESHOLD
              For allocations greater than or equal to the limit specified
              (in bytes) by M_MMAP_THRESHOLD that can't be satisfied from
              the free list, the memory-allocation functions employ mmap(2)
              instead of increasing the program break using sbrk(2).

Причём сама glibc решает, когда mmap-ить, а когда не следует.
Детали этого выбора надо читать в glibc malloc.c
Там с первого взгляда довольно сложная логика (порог выбирается динамически?), по умолчанию — 128 КиБ.
Даже странно, что автор пытался искать это в ядре Linux.

Именно, чтобы не дёргать по каждому поводу ядро для выделения куска памяти (скажем, создание множества объектов — счёт идёт на байты), этот механизм отдаётся на откуп библиотеке libc. Недаром существует целая куча кустарных библиотек выделения памяти. Даже слов не хватает их все описать:) Кстати разница между этими всеми навороченными lockless-библиотеками в Linux — жалкие проценты как раз по причине, озвученной автором статьи.
На всю ветку сразу: пост как раз об этом — где документация? Точнее, где точка входа в документацию?
Если я слышал про mallopt(), я знаю хотя бы с чего начать. А если нет? Исходники конечно первоисточник, но никак не замена книгам типа тех что в списке литературы.
И еще, я употребляю слова «система», «ОС», «ядро» в несколько расширенном смысле, как весь набор инструментов используемых прикладным программистом из коробки, glibc сюда естественно тоже относится.
man malloc

NOTES
       Normally, malloc() allocates memory from the heap, and adjusts the size
       of the heap as required, using sbrk(2).  When allocating blocks of mem-
       ory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation
       allocates  the  memory  as  a  private anonymous mapping using mmap(2).
       MMAP_THRESHOLD is 128 kB by  default,  but  is  adjustable  using  mal-
       lopt(3).   Allocations  performed  using  mmap(2) are unaffected by the
       RLIMIT_DATA resource limit (see getrlimit(2)).

Если что-то непонятно — смотрим SEE ALSO и ходим по ссылкам. Если что-то плохо описано — contributions are welcome:)
Когда-то я уже устраивал спор на тему, поэтому напишу и сюда. Если программа выделяет 128МБ (~много МБ) памяти, будет ли при этом выделено много страниц по 4кБ, или не так много страниц по 4МБ? И почему?
UFO just landed and posted this here
Таки может:
Enabling PSE (by setting bit 4, PSE, of the system register CR4) changes this scheme. The entries in the page directory have an additional flag, in bit 7, named PS (for Page Size). This flag was ignored without PSE, but now, the page directory entry with PS set to 1 does not point to a page table, but to a single large 4 MiB page. The page directory entry with PS set to 0 behaves as without PSE.
UFO just landed and posted this here
Страница 4МБ будет вставлена вместо таблицы страниц второго уровня, т.е. вместо 1024 страниц по 4кБ. 1024*4кБ это как раз 4МБ.
Linux не использует PSE.
Виноват, ориентировался на вики («PSE-36 was never used by Linux»)
UFO just landed and posted this here
В одной системе — конечно может. Размер страницы — это свойство процесса.
Бывает ещё transparent hugepages. Тогда процесс может думать, что он работает с 4k страницами, а ему выделяют 2M и дробят потом.
Huge pages называется. Могут, и сейчас идёт большой срачик работа над тем, что делать, если huge page надо порезать на маленькие.
Особенно забавно, если на одном хосте нужно держать и БД (для которых рекомендуют отключать THP), и приложения. Например, в контексте контейнерной виртуализации/разделения ресурсов.
Начиная с версии ядра 2.6.38 (14 марта 2011) реализованы Transparent Hugepages (THP, www.kernel.org/doc/Documentation/vm/transhuge.txt, lwn.net/Articles/423584/). При установке «echo always > /sys/kernel/mm/transparent_hugepage/enabled» выделение большого количества памяти производится на больших страницах (при условии выравнивания и наличия свободных больших страниц; в Intel большие страницы обычно по 2 МБ с выравниванием на 2 МБ). «echo madvise >/sys/kernel/mm/transparent_hugepage/enabled» отключает автоматическое выделение больших страниц, для их использования требуется вызов «madvise(MADV_HUGEPAGE)». «echo never» отключит механизм THP.

Статистика работы THP доступна в /proc/meminfo (AnonHugePages — количество используемых на данный момент больших страниц в системе), /proc/PID/smaps (AnonHugePages для отдельных приложений), счетчики thp_* в /proc/vmstat (и compact_* для статистики по дефрагментатору памяти — system uses memory compaction to copy data around memory to free a huge page for use).
О как. Благодарю за информацию.
Только не mks, а µs или на самый крайний случай us.
Спасибо за список литературы, заказал себе пару книг!
Это все абсолютная классика. Все переведено кстати.
> Задержанные TCP пакеты
вот очень бы хотелось услышать решение таких проблем…
У нас была немного похожая проблема: отправляли заголовок пакета данных, а затем тело, причём тело по размеру сравнимо с заголовком. На принимающей стороне крутится epoll, и код, который сначала вычитывал заголовок, потом по нему смотрел, сколько дальше должно быть данных, и вычитывал уже их. И мы регулярно получали паузы по 40мс между заголовком и данными, потому что прочитать заголовок успевали раньше, чем отправится тело. Решили проблему установкой флага TCP_CORK перед отправкой заголовка.
Абсолютного решения нет, будем считать первым шагом к решению то что мы о проблеме знаем.
net.ipv4.tcp_slow_start_after_idle = 0 не поможет в этом случае?
Возможно и поможет, но мне например чтобы полностью убедится пришлось бы выкатывать это в продакшн. Ну его нахрен, знаю я чем такие игры кончаются. А главное — этот ключик подействует на все процессы на машине, а там не только мои и системные к сожалению…
За вариант спасибо
Да, /proc/sys/net/ipv4/tcp_delack_min это вариант, опять же плохо то что systemwide.
А переделывать kernel — это уж извините полный хардкор, во многих случаях заказчик продукта — сторонняя организация, которая диктует под какую систему и на какой версии все должно работать. Втюхать им самодельный kernel это уж какой-то беспредельно изысканный маркетинг получается.
Прочитал, что /proc/sys/net/ipv4/tcp_delack_min — уникален для рэдхэта.
Да, у них бывает. Типа /sys/kernel/mm/redhat_transparent_hugepage/
Да, действительно. Жаль
В последнем абзаце речь о «Posix Threads»?
А то, не очень ясно.
В частности о них, pthreads определяет API а не реализацию. На Linux они естественно построены поверх clone()
У вызова clone есть куча флагов, которые указывают то, что клонировать, а что нет. Мне кажется, что при вызове pthread_create в clone не передаётся флаг CLONE_FILES, а вот при вызове fork — передается.

Если вы хотите ещё прекрасных загадок, которые вам сломают голову, то можно, например, погрузиться в то, что происходит, когда многопоточному приложению делают fork() :-)
Там как раз более-менее просто (и документировано). fork() закрывает все потоки кроме того из которого был вызван, это как раз постулировано стандартом с самого начала мультипоточности.
Это да и фактически надо делать exec после этого, иначе состояния объектов синхронизации, которые клонировались после форка, в непредсказуемом состоянии. Но если ты никогда этим вопросом не заморачивался, то вопросов будет много.
А на русском про ядерное программирование Линукса что-нибудь выходило?
Например Роберт Лав — Ядро Linux. Описание процесса разработки
Поскольку никакой информации так и не нашел, пришлось мерить руками. Оказалось, на моей системе (3.13.0) — всего 120 байт

Это, мягко говоря, сомнительно, потому что единица выделения с помощью mmap — 1 страница, что сильно больше 120 байт
Правильно сомневаетесь. Как выяснилось, sbrk() под Linux тоже может возвращать память в систему если удаляется последний выделенный сегмент, и именно этот порог составляет 120 байт. Если в конце аллоцировать еще байт, порог возвращения — 128К, прямо как в мануалах обещано. Тоже между прочим малоизвестная информация.
Я добавил опцию -t к тестовой программе исключающую тримминг памяти.
Позвольте мне указать на некоторые неточности в статье, которые также повторяются в комментариях.

Выделение памяти. free() действительно возвращает память в систему, но не во всех случаях, и уж тем более про это можно забыть если память сильно фрагментирована. Более подробно (с нужными ссылками на документацию) это описано здесь: Linux: Native Memory Fragmentation and Process Size Growth
Unlike certain Java garbage collectors, glibc malloc does not have a feature of heap compaction. Glibc malloc does have a feature of trimming (M_TRIM_THRESHOLD); however, this only occurs with contiguous free space at the top of a heap, which is unlikely when a heap is fragmented.

Если есть проблемы с памятью, то лучше переключиться на другие аллокаторы, например, jemalloc — он используется в Mozilla, Facebook, во многих высокопроизводительных и встроенных системах.

clone (системный вызов sys_clone). К сожалению, здесь очень много неточностей:
  • fork() не является подобием clone(), это обертка над ним (man 2 fork: Since version 2.3.3, rather than invoking the kernel's fork() system call, the glibc fork() wrapper that is provided as part of the NPTL threading implementation invokes clone(2) with flags that provide the same effect as the traditional system call.)
  • clone() не делает адресное пространство общим, это все конфигурируется опциями (man 2 clone: If CLONE_VM is not set, the child process runs in a separate copy of the memory space of the calling process at the time of clone().)
  • Это не упоминается в статье, но был вопрос в комментариях: pthread использует clone (man 7 pthreads: Both threading implementations employ the Linux clone(2) system call.) Но в общем случае для реализации многопоточности хватит и прямого использования clone (Minimalistic Linux threading)


В целом документация есть на все, но это надо знать где смотреть. Опыт работы над встроенными системами научил меня всегда смотреть на документацию API, даже такую элементарную как malloc()/free(). Вот, например, из документации fclose (казалось бы, какие тут могут быть сюрпризы, по крайней мере, с позиции Junior/Intermediate разработчика): Note that fclose() only flushes the user-space buffers provided by the C library. To ensure that the data is physically stored on disk the kernel buffers must be flushed too, for example, with sync(2) or fsync(2).
Да, все правильно.
В ответ могу только заметить что я например знал о fclose()/fsync() с первых дней (это не Линуксная фича — общеюниксная, и ни в коем случае не баг) и никогда бы не подумал включать ее в список малоизвестных фактов.
Похоже, у каждого есть свой набор любимых малоизвестных фич.
Для меня самым неприятным сюрпризом за время моей работы с Линуксом стало то, что я не могу сказать, сколько памяти доступно в системе для выделения. free (и шаманство вокруг /proc/meminfo) даёт апроксимацию первого порядка, но понять, «где память», или «будет OOM после выделения ещё 100Мб памяти или нет» невозможно.

В лабораторных условиях можно. В продакшене (с всякими обширными shmem, tmpfs и т.д.) — нет, потому что часть памяти в cached оказывается невытесняемой.
А вот free обновили, и теперь он дает такой вывод:
% free -m
              total        used        free      shared  buff/cache   available
Mem:           7871        3389         963         361        3518        3820
Swap:             0           0           0

Теперь учитывает tmpfs, между прочим!
Очень вредная статья. Устаревшая информация приправленная домыслами и догадками.

По существу, к прокомментировавшим выше, могу добавить, что флаг TCP_QUICKACK придуман совсем для другого, а именно: задержать отсылку ACK'a если знаем, что потом будем слать данные. По сути убрать отправку пустого пакета с флагом ACK если уверены, что так нужно. К примеру, после тройного рукопожатия в http сессии мы знаем, что пошлём данные от клиента, так что можем придержать последний ACK и отправить его с телом запроса. А у вас скорее всего был включён net.ipv4.tcp_slow_start_after_idle.

По поводу документации и недокументированности не согласен. Документация в манах и в списках рассылки. Код написан довольно понятно. Вы видели лучше документированные системы которые активно разрабатываются?

З.Ы. Вот хорошая книга про внутренности сетевого стека Linux (местами гик порн): www.amazon.com/TCP-Architecture-Design-Implementation-Linux/dp/0470147733
Отсылка ACK задерживается и так, согласно встроенному в стек delayed ACK алгоритму. TCP_QUICKACK нужен именно для того чтобы этот алгоритм отменить на время. man -s7 tcp:
TCP_QUICKACK (since Linux 2.4.4)
Enable quickack mode if set or disable quickack mode if cleared.
In quickack mode, acks are sent immediately, rather than delayed
if needed in accordance to normal TCP operation.

За книгу спасибо, будет первой в списке
Про потоки и дескрипторы — не верю.

Проверка:
int main()
{
    int fd = ::open("/dev/zero", O_RDONLY);
    char buf[2];
    cout << ::read(fd, buf, 2) << endl;
    std::thread([&]() { ::close(fd); }).join();
    cout << ::read(fd, buf, 2) << endl;
    return 0;
}

Выводит:
2
-1


Т.е. дескриптор текущего потока, закрытый в другом потоке, закрыт и в текущем.

Так вы про потоки, а в статье про процессы. Повторите тоже самое, только с fork()
Нет. В статье именно про потоки.
Если один из потоков решает закрыть свой сокет, другие потоки об этом ничего не знают, поскольку они на самом деле отдельные процессы и у них свои собственные копии этого сокета, и продолжают работать.

И процитирванное совершенно не соответствует действительности.
Хм, действительно, прочитал не внимательно. Ок.
Подозреваю, что сейчас это поведение регулируется флагами вызова clone(). Надо почитать…
Попробуйте то же самое с сокетами:
void reader(int fd)
{
	char buf[2];
	auto l=read(fd, buf, sizeof(buf));
	std::cout<<l<<std::endl;
	if(l > 0) close(fd);
}

int s[2];
socketpair(AF_LOCAL, SOCK_STREAM, 0, s)

char buf[2];
write(s[1], buf, sizeof buf);

std::thread t1{[=]{ reader(s[0]); }};
std::thread t2{[=]{ reader(s[0]); }};
t1.join();
t2.join();

выводит 2 и подвисает как и описано в посте.
Я не знал что простые файлы ведут себя по другому, спасибо. Надо будет выяснить что там внутри происходит.

Он подвисает не потому что закрытие сокета в одном из потоков не видно в другом потоке, а потому что конкуретные операции из разных потоков на одном сокете в линуксе сериализуются мьютексом, и сама операция read достаточно длительная, чтобы оба потока успели вызвать read до того как первый из них вызовет close.

Уберите конкуренцию потоков и увидите что с сокетами все точно также как и с файлами:
    std::thread t1{[=]{ reader(s[0]); }};
    t1.join();
    std::thread t2{[=]{ reader(s[0]); }};
    t2.join();
У-у как интересно. Получается сокеты синхронизируются некоей внешней силой (ядром?) в частности std::thread::join() однозначно синхронизирует состояния сокетов. Однако, если поток успел войти в read(), он никогда не увидит нового состояния и подвиснет. Более того, «настоящий» сокет тоже остается открытым, поскольку другой конец соединения продолжает его видеть. Судя по косвенным признакам это относится так же и к простым дескрипторам, просто там такая ситуация никогда не возникает.
Да не клонируются сокеты/файлы для потоков.
Это один сокет. А то что вы наблюдаете — обычный race condition.
Вообще насколько я помню нигде не гарантируется что операции над дескрипторами потокобезопасны.
Так что так как у вас в первом примере делать вообще нельзя.
А пруф где?
Вот мое утверждение #1: Если в Linux один поток висит в read() на сокете, а в другом потоке этот сокет закрывается, то ни первый поток, ни сокет на другом конце никогда об этом не узнают. Проверяется непосредственно.
утверждение #2: во всех не-Linux'ах в такой ситуации второй поток и другой конец видят закрытие сокета, проверяется.
Далее идут мои домыслы, однако домыслы обоснованные.
— при форке процесса сокеты клонируются
— поведение при клонировании сокета идентично тому что мы наблюдаем
— потоки под Linux создаются через clone()
=> можно предположить что clone() подобно fork() тоже клонирует сокеты, внутренне непротиворечиво.
race condition естественно присутствует, но он существует параллельно клонированию, это два несвязанных механизма
А пруф где?

Пруф я уже приводил:
    std::thread t1{[=]{ reader(s[0]); }};
    t1.join();
    std::thread t2{[=]{ reader(s[0]); }};
    t2.join();


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

Все что мой код выше показывает — это то что если сокеты применять потокобезопасно (как и следует), то никаких таких «эффектов», которые вы себе придумали — нет.
А когда нет потокобезопасности (как в вашем первом примере) — это обычный undefined behaviour. Утверждать что либо для такого кода бессмысленно.
Что процессы/потоки используют совместно, а что нет, управляется отдельными флагами вызова clone.
Из man clone:

       CLONE_FILES (since Linux 2.0)
              If  CLONE_FILES is set, the calling process and the child process share the same file descriptor table.
              Any file descriptor created by the calling process or by the child process is also valid in  the  other
              process.   Similarly, if one of the processes closes a file descriptor, or changes its associated flags
              (using the fcntl(2) F_SETFD operation), the other process is also affected.

              If CLONE_FILES is not set, the child process inherits a copy of all  file  descriptors  opened  in  the
              calling  process  at  the  time of clone().  (The duplicated file descriptors in the child refer to the
              same open file descriptions (see  open(2))  as  the  corresponding  file  descriptors  in  the  calling
              process.)   Subsequent operations that open or close file descriptors, or change file descriptor flags,
              performed by either the calling process or the child process do not affect the other process.


Для примера, в glibc в функции create_thread используестся такой набор флагов
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
    | CLONE_SIGHAND | CLONE_THREAD
    | CLONE_SETTLS | CLONE_PARENT_SETTID
    | CLONE_CHILD_CLEARTID
    | 0);

а обычный fork реализуется как вызов clone без каких-либо флагов.
Замечательно, по моему нас всех скоро осветит свет истинного понимания. Не будь я так занят ленив, сам бы мог найти. А можете обьяснить как работает код выше по ветке? вот этот
Почему один поток закрывает сокет а второй этого не видит?
Я, в свое время, даже тест написал для отлова этого эффекта tty системы. Теперь ваша очередь для сокет.
lkml.org/lkml/2012/12/18/368
Смысл в следующем:
Если один поток находится в системном вызове read, например, то второй, может войти в системный вызов close (все это для одного и того же последнего, открытого дескриптора файла). По хорошему, второй должен пометить дескриптор как закрытый, для предотвращения дальнейших попыток чтения/записи, дождаться завершения, а лучше прервать, начатый другим потоком read, и вернутся нормально в user space после read, но как обычно есть нюансы. Бывает, как было с tty, close отработал, а read все пытается читать в уже освобожденную память закрытого файла. В вашем случае, видимо, что-то помягче.
Потому что у вас в коде ошибка. Каждый поток читает по 2 байта, но в сокете всего 2. Соответственно, второй read, если он начался до того, как первый поток закрыл сокет, будет ждать своих 2 байта вечно. Если изменить ваш код так, чтобы каждый поток читал по одному байту, то все происходит так как ожидается: либо оба получают по 1 байту, либо второй read видит уже закрытый сокет.

Код
#include <thread>
#include <unistd.h>
#include <iostream>
#include <mutex>
#include <sys/types.h>
#include <sys/socket.h>

void reader(int t_id, int fd)
{
    char buf[2];
    std::cout << t_id << ": Started" << std::endl;
    auto l = read(fd, buf, 1);
    std::cout << t_id << ": Received " << l << std::endl;
    if (l > 0)
        close(fd);
    std::cout << t_id << ": Closed" << std::endl;
}


int main()
{
    int s[2];
    socketpair(AF_LOCAL, SOCK_STREAM, 0, s);

    char buf[2] = {};
    write(s[1], buf, sizeof(buf));

    std::thread t1{[=]{ reader(1, s[0]); }};
    std::thread t2{[=]{ reader(2, s[0]); }};
    t1.join();
    t2.join();
}



Примеры вывода
2: Started
2: Received 1
2: Closed
1: Started
1: Received -1
1: Closed

2: Started1: Started
1
: Received 1
1: Closed
2: Received 1
2: Closed

Возможно я вас неправильно понял и вы специально сделали чтение по 2 байта, чтобы второй read заблокировался.

Тогда не совсем понятно, что вы бы ожидали получить? Чтобы второй read прервался в тот момент, когда первый поток закрывает сокет?
Да, разумеется, и еще чтобы другой конец сессии видел закрытый сокет. А как еще он должен себя вести?
Нет, это не ошибка. Второй сокет специально подвешивается на read(), вопрос почему он не видит закрытия сокета в другом потоке.
То, что вы говорите, это в высшей степени возмутительно! Ибо опреации на сокетах, как и другие системные вызовы, являются функциями класса async signal safe [1], то есть разрешенными к использованию в обработчиках сигналов. Рассказывать, почему функция, которая использует мьютекс не может являться async signal safe?

[1] man 7 signal
Там не юзерский мьютекс, а ядерный, специально спроектированный для работы с сокетом.

А вообще, да, расскажите, что происходит, если из обработчика сигнала вызвать read, а данных еще нет :)
Подождет пока к нему придут данные. Вместе с ним подождет и прерванный поток. Большого смысле в этом, наверное, нет, но если они таки придут, то ничего страшного не случится. Так, например, write в обработчиках используется и в хвост, и в гриву. А вы в своём каменте обощили до всех операций над сокетом, а теперь пытаетесь доказать, что смысла в таком использовании каких-то конкретных функций нет.

Ниже вашего камента, коллега ошибочно показал на список согласно стандарта thread safe functions, который на самом деле является списком non thread safe functions. Так вот ни read, ни close, ни какой-либо другой функции там нет.
Касательно ядренного мутекса. Допустим, что он там есть. Каким образом, он может спровоцировать описываемое поведение да ещё так, чтобы оно считалось корректным с т.з. стандарта?

Если вы говорите, что там race condition, то мьюеткс как бы должен как раз помогать избегать race condition.
Если мы говорим о зависании, то это уже dead lock, но для возможности dead lock необходимо наличие нескольких мутексов.
Вызов из двух потоков некорректен.
А мьютекс там нужен, чтобы ядро могло защититься от дураков и сохранить внутренние структуры в консистентном состоянии.
Совсем не обязательно что после такой защиты вы получите хоть какой-то полезный или повторяемый результат.
Мне кажется, вы не прочитали то, что предшествует непосредственно этому списку функций:
POSIX.1-2001 and POSIX.1-2008 require that all functions specified in the standard shall be thread-safe, except for the following functions
Вы неверно понимаете что имеется в виду под thread-safe в POSIX.

Например memcpy потокобезопасна с точки зрения POSIX.
То есть ее ВООБЩЕ можно вызывать из нескольких потоков одновременно.

Однако вызов из двух потоков с одним и тем же приемным буфером не является потокобезопасным.

Точно по той же причине, одновременный вызов read для одного и того же дескриптора из разных потоков тоже не является безопасным, хотя read сама по себе потокобезопасна.
А это где-то описано?
Я не знаю где описано.
Поэтому привел простой и понятный пример (memcpy) подтверждающий мои слова.
Если thread-safe, значит можно как хочешь, хоть с одним дескриптором, хоть с дестью.
А пример с буфером плохой. Причем тут ядро? С его точки зрения все нормуль и его поведение не меняется.
А в нашем случае меняется — BUG.

Действительно. Причем тут ядро? Где у меня про ядро в комменте про memcpy?
При всей силе аргументов, которые приводит уважаемый amosk то, что он пытается приподать под соусом thread safe, является на самом деле async signal safe. С этой точки зрения memcpy не является async signal safe функций, а все сокетные операции — являются. При этом и те, и другие являются thread safe.
Ваш пример подверждает только ваш пример, ввиду фундаментальности ограничений с данной опеацией. Вы же пытаетесь распространить это на весь стандарт POSIX. Это нелогично и незаконно.
Нет. Наоборот.
Мой пример доказывает, что потокобезопасность по POSIX не гарантирует отсутствие race condition.
А именно race condition и является причиной наблюдаемого поведения.
В случае с memcpy нету race condition. У вас просто будет data inconsistency. А memcpy успешно отработает и не зависнет.
A race condition or race hazard is the behavior of an electronic or software system where the output is dependent on the sequence or timing of other uncontrollable events.


Т.е. буквально то что происходит при копировании в один буфер из нескольких потоков.

Согласитесь, что если бы это было так, то сокет был бы сделан без упомянутого вами мутекса, так как синхронизация доступа к сокета — это был бы гемор программиста.

Кроме того, если вы беретесь говорить за весь POSIX, то не сочтите за труд сюда ссылку или цитату в качестве пруфа.
У вас есть возражения против того что оба эти условия выполняются?
1) memcpy потокобезопасна по POSIX
2) memcpy не потокобезопасна при передаче одного и того же буфера из разных потоков
То, что мы обсуждаем, это именно то, что гарантирует async signal safe. memcpy не является async signal safe функцией именно по пункту 2. A все операции с файловыми дескрипторами — являются.

Описанный автором поста случай ничем не отличается от случая, когда мы имеем однопотоковое приложение, которое делает в основном потоке close, в этот момент случается какой-то сигнал и в нём оно делает read или write на данном дескрипторе. Этот же race condition (если он существует), приведет к такому же подвисанию, а значит это — bug.
Касательно мьютекса в сокете — почитайте исходники линукса.
Вызов read() из разных потоков совершенно безопасен и законен. Другое дело что невозможно предсказать какие данные будут прочитаны каким из потоков, но это не является ограничением на потокобезопасность. Например, я могу интерпретировать поток данных как набор независимых однобайтных команд, или вообще не интересоваться содержимым а использовать read() для синхронизации. Но сам вызов совершенно потокобезопасен.
Точно, тогда open, close, read, write — thread-safe.
И их поведение не должно зависеть от количества threads их вызывающих, значит BUG.
А вот в баги кернела я не верю. В 99.999% случаев ими прикрывают незнание системы. Давайте лучше разберемся, истина где-то рядом.
Да нет, я просто хотел сказать что баги в кернеле встречаются исключительно редко, как правило ими прикрывают свое незнание.
В любом случае, конкретно эта фишка прекрасно известна всем кто переходил на Linux с любого другого Unix'a в середине 90х, так что для бага она слишком стара. Если бы найти тьюториалы по сетевому программированию за тот период, можно было бы найти вполне авторитетные источники прямо предлагающие паттерны которые на Linux не работают.
Сделате приложению kill -3 в таком состоянии. Давайте посмотрим в корку.
Хорошая идея, только ничего интересного я не увидел:
(gdb) info thr
  Id   Target Id         Frame 
  2    Thread 0x7fe8c11e7700 (LWP 16403) 0x00007fe8c25dd3bd in read ()
    at ../sysdeps/unix/syscall-template.S:81
* 1    Thread 0x7fe8c29e5780 (LWP 16401) 0x00007fe8c25d766b in pthread_join (
    threadid=140637649139456, thread_return=0x0) at pthread_join.c:92
(gdb) 

Хотите, дамп выложу куда-нибудь
По крайней мере, мы точно знаем, что повисло в системном вызове, а не в обертке libc.
Товарищи amosk encyclopedist izyk degs
Огромная просьба от меня и других, кто следит за комментариями к этой статье, — опубликуйте результаты просветления про потоки и дескрипторы в виде отдельной статьи (если просветление всё-таки наступит и все с ним согласятся).
Тут нет инфы достойной отдельной статьи.

Как выше написал encyclopedist:
1) If CLONE_FILES is set, the calling process and the child process share the same file descriptor table
2) в glibc в функции create_thread используестся такой набор флагов ...CLONE_FILES…

Т.е. ничего не клонируется.
Вопрос закрыт.
Обязательно, если просветление все-таки наступит и мы не поубиваем друг друга в пылу дискуссии.
Sign up to leave a comment.

Articles