Главной сложностью дизайна было принятие решения о том, где именно хранить идентифицирующую информацию. Очевидно, что она должна была храниться для каждого потока индивидуально, что в свою очередь означало или изощренное применение глобальных переменных или использование существующей в Neutrino поддержки потоковых областей данных.
Я исключил применение глобальных переменных в самом начале работы над дизайном, в основном из-за того, что глобальные переменные потребуют защиты с помощью мьютекса при обращении к данным (глобальные данные и потоки обычно не уживаются вместе). На уровне программирования потока это было бы легко: захватить мьютекс, обновить данные, освободить мьютекс. Тем не менее, на уровне программирования утилиты чтения это было бы гораздо сложнее — прежде всего, нам бы был нужен механизм отыскания области глобальных данных (так как мы предполагаем, что утилита не будет иметь возможности использовать map файл для каждого процесса при запуске) и мы бы должны были каким-то образом использовать внутренние мьютексы потоков для синхронизации.
Как оказалось, синхронизация с использованием внутреннего мьютекса потоками не была значительным препятствием; при надлежащем кодировании я мог бы сделать так, что попытка синхронизации безопасно завершалась, если шёл процесс изменения данных. Главная проблема заключается в нахождении области глобальных данных. Другая проблема, о которой я не подумал в то время, но которая в последствии была решена выбранной реализацией без дополнительных затрат, это завершение потоков. Когда поток завершается, нам хотелось, чтобы информация идентификации потока автоматически удалялась. Кроме установки обработки atexit() для каждого потока не было простого и понятного пути как это сделать. Как вы помните, мы хотели добиться результата с минимальным вмешательством, так что установка дополнительного обработчика являлась еще одним аргументов против такого решения.
В итоге я решил остановиться на подходе с применением реализованных в Neutrino потоковых областей данных по стандарту POSIX. Если вы исследуете заголовочный файл <sys/storage.h>, вы обнаружите следующую структуру:
/*
* Локальные данные потока.
* Эти данные расположены вверху
* стека каждого потока.
*/
struct _thread_local_storage {
void (*exitfunc)(void *);
void *arg;
int *errptr;
int errval;
unsigned flags;
int pid;
int tid;
unsigned owner;
void *stackaddr;
unsigned reserved1;
unsigned numkeys;
void **keydata; // Индексировано с использованием pthread_key_t
void *cleanup;
void *fpuemu_data;
void *reserved2 [2];
};
Как только я увидел «вверху стека каждого потока» я понял что я при деле. Я считал, что должна быть возможность найти верх стека любого потока, и действительно, беглый анализ заголовочного файла описывающего структуру /proc дал мне всю информацию, которая была нужна.
Так что следующим вопросом был «Где же и как я припрячу свои данные»? Глядя на поля приведенной выше структуры, становится ясно, что почти все из них не подходят для этого. Также я не собирался использовать поле reserved2 — хотя оно и зарезервировано, я не думал, что оно зарезервировано специально для меня! 🙂
Поэтому единственным полем, которое давало надежду на использование, было поле keydata. Я обратил внимание на POSIX функции pthread_key_create(), pthread_getspecific() и pthread_setspecific(). К счастью, их реализация стала довольно понятна после быстрого просмотра заголовочного файла :
typedef int pthread_key_t;
…
#define pthread_getspecific(key) ((key) < __tls()->numkeys ? __tls()->keydata[key] : 0)
Вспомните, что в определении структуры struct _thread_local_storage для keydata есть комментарий «индексировано с помощью pthread_key_t». Это наводит на мысль, что keydata это массив, который предназначен, исходя из его определения, для хранения указателей типа void *. Из документации по pthread_getspecific() и pthread_setspecific() мы знаем, что область данных, которая может быть ассоциирована с потоком, тоже является void *. Несколько маленьких экспериментов подтвердили, что простой проход по массиву keydata позволяет найти все значения привязанных к потоку данных. Отсюда понятно, что numkeys определяет размер массива. И хотя это неправильно — получать доступ к членам массива keydata напрямую вместо использования POSIX функций для работы с ключами, у нас не было другого выбора, когда речь шла об утилите командной строки — только прямой доступ позволяет получить данные из другого процесса.