Меню Рубрики

Linux thread local storage

Thread Local Storage

Thread Local Storage (TLS) are per-thread global variables. Compilers such as GCC provide a __thread keyword to mark global variables as per-thread. Support is required in the program loader and thread creator.

A x86-64 System V ABI compiler would compile this code into assembly like this:

The errno global variable is put into a special thread local bss section (.tbss) (or .tdata if initialized) and special actions occur at program link time and program load time. A per-thread allocation (containing the thread local storage, the user-space thread structure and perhaps other things) is made when the thread is created. Each per-thread variable is located at a fixed offset inside this allocation. In the above example, the %fs segment starts at the thread’s user-space thread structure (%fs thus works as an extra register), and the special errno@tpoff linker symbol is the offset from the thread’s userspace thread structure to the per-thread errno value.

Contents

Design

Master TLS Copy

The program contains a master copy of its thread local storage (with its initialized-at-compile-time values) which is used when creating threads. This special segment is created by the linker from the .tdata (initialized tls) and .tbss (zero-initialized tls) sections. You can find it by searching the ELF program headers for a segment with type PT_TLS (decimal value 7) (as opposed to the normal PT_LOAD).

The virtual address of the thread local storage master segment is meaningless as it isn’t loaded anywhere specific, you decide where you wish to load it. Mind that the segment does have alignment constraints like normal segments (but the linker placed those for you). Besides deciding yourself where to load the segment, you load this segment like a normal PT_LOAD segment.

Per-thread allocation

Each thread has a memory allocation associated with it. It contains the user-space thread structure, the thread local storage, and potentially other things. Each thread has a thread-self-pointer register that points to the thread’s user-space thread structure, which is used to quickly determine which thread the current is, as well as providing quick access to the thread local storage. The exact semantics of the per-thread depends on the architecture and its ABI, as well as whether the executable is statically or dynamically linked.

The layout of the user-space thread structure is partially mandated by the ABI. On some platforms (i386 and x86-64), there must be a pointer at the start of it that points to itself. Besides those mandatory parts, the rest of the structure is up to you, it is useful for many things such as remembering allocations that must be deallocated when the thread terminates.

The thread local storage is located at a fixed offset from the user-space thread structure, therefore each variable in the thread local storage is also at a fixed offset. This offset is determined at link time and accessible through the special foo@tpoff linker symbols. Locating a particular thread local variable is as simple as getting the thread-self-pointer and adding a fixed offset.

This is a summary of the actual details in the System V ABI.

The thread-self-pointer register is the base of the %gs segment. It is set to the address of the current thread’s user-space thread structure. When you switch the thread on the current CPU, you change the base of the gs segment of that CPU’s GDT and reload the gs register. A pointer to the user-space thread structure itself is the first member of the user-space thread structure.

The thread local storage (after having its size rounded up to its alignment) is located immediately prior to the user-space thread structure. The offsets are negative. To place the user-space thread structure and the thread local storage, do this:

Do note that the thread local structure might not be at the beginning of the per-thread allocation if it its alignment is less of that than struct uthread. It is crucial that both the thread local storage and the user-space thread structure are properly aligned. You then initialize the user-space thread structure’s self pointer and the thread local storage:

x86-64

The thread-self-pointer register is the base of the %fs segment. It is set to the address of the current thread’s user-space thread structure. When you switch thread, you set the FSBASE MSR (0xC0000100). A pointer to the user-space thread structure itself is the first member of the user-space thread structure.

The per-thread allocation is arranged and placed as described under #i386.

Other

See the tls.pdf document below and please document the specifics here afterwards. 🙂

Implementation

Your kernel should bootstrap the thread local storage for the main thread after having loaded a program:

  1. During program load, locate the thread local storage segment among the program headers, and load it somewhere.
  2. Create the per-thread allocation for the main thread.
  3. Copy the master thread local storage to the thread local storage of main thread.
  4. In the main thread’s user-space thread structure, store: The location/size of the per-thread allocation, master thread local storage location/size/alignment, main thread’s thread local storage location/size, the main thread’s stack location/size, and so on. This allows the thread to make new threads and to clean up after itself.
  5. Set the thread-self-pointer register of the main thread to the main thread’s user-space thread structure.

This approach allows thread local storage to be operational immediately when a program is loaded and makes it simple to create new thread.

Setting up thread local storage for a new thread is simple:

  1. Create a per-thread allocation for the new thread.
  2. Copy the master thread local storage to the thread local storage of new thread.
  3. Initialize the user-space thread structure for the new thread.
  4. Set the thread-self-pointer register of the new thread to the new thread’s user-space thread structure.

Some Unix kernels such as Linux actually doesn’t set up the thread local storage for the main thread. The libc is required to parse the ELF executable of the program to locate and load the master thread local storage copy itself and bootstrap the main thread. This has the obvious disadvantages of having early times where language features doesn’t work and that every executable gets linked in an ELF loader (in case it uses thread local storage).

Источник

Программерские типсы и триксы: Локальное хранилище потока, или что такое TLS

Содержание статьи

Программисты, которые впервые сталкиваются с многопоточным программированием, очень часто совершают ошибки, связанные с нарушением общего доступа. Разные потоки могут обратиться к одной и той же переменной и одновременно попытаться изменить значение. Таких проблем можно избежать, используя старые добрые локальные переменные или. локальное хранилище потока.

Представим многопоточное приложение, которое в своем коде использует глобальные или статические переменные. Трудно представить? А вот и нет. Наглядным примером такой ситуации может служить функция strtok стандартной библиотеки C++. Точнее, в качестве такого примера она могла выступить раньше, сейчас ее уже переписали и сделали «правильной». Но не это сейчас важно. Главное то, что при первом вызове функция strtok запоминала указатель на строку, передаваемый ей в свою собственную статическую переменную. Вполне вероятна была ситуация, что эту функцию практически одновременно могли вызвать сразу два потока, вследствие чего указатель на строку успешно менялся и один из потоков получал доступ к неправильным данным.

Такие ошибки очень трудно отследить. Использование локальных переменных и параметров функций в значительной степени решают эту проблему, но иногда просто невозможно обойтись без статических или глобальных значений. Для наглядности можно взглянуть на код ниже и понять, как же все это работает.

Глобальные переменные в многопоточном приложении

// глобальные переменные
int tls_i;
char tls_char[25];

// Потоковая функция
DWORD WINAPI ThreadFunc( LPVOID lpParam )
<
// использовать глобальные переменные в потоках — плохая идея
tls_i = (int)lpParam;
lstrcpy(tls_char,”array of char”);
char szMsg[80];

wsprintf( szMsg, “Parameter = %d.”, tls_i );
MessageBox( NULL, szMsg, “ThreadFunc”, MB_OK );

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
intnCmdShow)
<
DWORD dwThreadId;

CreateThread(NULL, 0, ThreadFunc, (LPVOID)1, 0, &dwThreadId);
CreateThread(NULL, 0, ThreadFunc, (LPVOID)2, 0, &dwThreadId);

Sleep(10000); // Наслаждаемся результатом 10 секунд

Мы создаем два потока, в которых работает один и тот же код. Этот код обращается к глобальным переменным с операциями чтения/записи. Такой подход совершенно небезопасен и может быть реализован только абсолютным новичком в программировании или полным дилетантом. Если от многопоточности никуда не деться, а глобальные переменные очень нужны, и их нельзя ничем заменить, то в этом случае нам на помощь придет локальная память потока, или thread-local storage (TLS).

Что такое thread-local storage?

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

Обычно локальная память потока более полезна при разработке DLL, так как в этом случае нам неизвестна структура программы, с которой они будут связаны. В обычных приложениях эта технология тоже вполне работоспособна.

TLS бывает двух типов: статическая и динамическая. В общем и целом оба типа локальной памяти потока преследуют одну и ту же цель — безопасное хранение данных. Однако реализация и метод их использования сильно отличаются друг от друга.

Динамическая локальная память потока

Динамический thread-local storage реализован в Windows с помощью системных API. Их всего четыре: TlsAlloc, TlsGetValue, TlsSetValue и TlsFree. Но прежде чем познакомиться с ними поближе, нужно изучить, как устроена динамическая TLS изнутри.

Каждый флаг выполняемого в системе процесса может находиться в состоянии FREE или INUSE, указывая, свободна или занята данная область локальной памяти потока. Значение TLS_MINIMUM_AVAILABLE изменяется в зависимости от версии ОС. Например, в Windows 98/Me это число было равно 80, а в Windows 2000/XP – уже 1088. С каждым потоком сопоставлен массив длиной TLS_MINIMUM_AVAILABLE с элементами типа PVOID.

Функция TlsAlloc служит для резервирования блока в массиве принадлежащему вызвавшему ее потоку. Грубо говоря, она ищет ячейку с флагом FREE и возвращает ее индекс. Прототип TlsAlloc выглядит так: DWORD WINAPI TlsAlloc(void). Если функция завершилась неудачей, то возвращается TLS_OUT_OF_INDEXES.

TlsSetValue, как видно из названия, служит для занесения в зарезервированную ячейку локальной памяти потока некоторого значения. Первым передаваемым функции параметром служит результат вызова TlsAlloc, а вторым является непосредственно значение переменной, которую нужно сохранить в TLS. Обращаясь к TlsSetValue, поток изменяет только свой PVOID-массив. Он не может что-то изменить в локальной памяти другого потока.

Прототип функции TlsSetValue

BOOL WINAPI TlsSetValue(
__in DWORD dwTlsIndex,
__in LPVOID lpTlsValue
);

В отличие от предыдущей, эта функция TlsGetValue возвращает значение, содержащееся в ячейке массива с заданным индексом. Ее описание выглядит так: PVOID TlsGetValue(DWORD dwTlsIndex). Как и TlsSetValue, TlsGetValue в параметре dwTlsIndex принимает значение, полученное от TlsAlloc.

Ну и наконец, функция TlsFree. Единственным ее параметром, о чем несложно догадаться, является индекс, полученный в результате вызова TlsAlloc. API освобождает зарезервированный блок, занятый ранее.

Использование динамической TLS

Теперь давай попробуем воспользоваться полученными знаниями и изменим программу, код которой был приведен в начале статьи. Для этого в функции WinMain мы два раза вызовем TlsAlloc, тем самым зарезервировав в локальной памяти потока две ячейки под переменные типа PVOID. Затем мы создаем два треда, каждый из которых будет обращаться к своей ячейке в TLS массиве, и, следовательно, выводить свое сообщение на экран. По завершению программы мы освободим занятую нами память с помощью вызова TlsFree.

Использование динамической TLS в многопоточном приложении

// TLS индексы
DWORD tls_i;
DWORD tls_char;

// Потоковая функция
DWORD WINAPI ThreadFunc( LPVOID lpParam )
<
TlsSetValue(tls_i, lpParam);

char *char_buf = new char[25];
lstrcpy(char_buf,”array of char”);
TlsSetValue(tls_char, char_buf);

int i = TlsGetValue(tls_i);
wsprintf( szMsg, “Parameter = %d.”, i );
MessageBox( NULL, szMsg, “ThreadFunc”, MB_OK );
delete[] char_buf;

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
intnCmdShow)
<
DWORD dwThreadId;

tls_i = TlsAlloc();
tls_char = TlsAlloc();

CreateThread(NULL, 0, ThreadFunc, (LPVOID)1, 0, &dwThreadId);
CreateThread(NULL, 0, ThreadFunc, (LPVOID)2, 0, &dwThreadId);

Sleep(10000); // Наслаждаемся результатом 10 секунд

Как видно, каждый из потоков обращается к элементу с одним и тем же индексом, но благодаря тому, что у каждого треда имеется свой массив локальной памяти, не происходит ничего неприятного. Оба потока спокойно уживаются друг с другом и не портят свои TLS-переменные.

Статическая thread-local storage

Главное отличие между статической и динамической TLS состоит в простоте использования первой – достаточно лишь использовать специальную директиву компилятора. Основную работу с thread-local storage берет на себя операционная система. Линкер генерирует специальные структуры в PE-файле, а также секцию с именем .tls (как правило), в которых хранятся все нужные данные для того, чтобы загрузчик модуля правильно инициализировал локальную память потоков.

Производительность при использовании статической TLS, конечно, страдает, но зато выигрывает программист. Не надо больше выделять блоки памяти и затем их освобождать, не надо вызывать специальные API для чтения и записи в thread-local storage, все делается нативными средствами языка C++.

Давай еще раз подправим нашу тестовую программу и попробуем воспользоваться механизмом статической локальной памяти потока.

Использование статической TLS в многопоточном приложении

// TLS-переменные
__declspec( thread ) int tls_i;
__declspec( thread ) char tls_char[25];

// Потоковая функция
DWORD WINAPI ThreadFunc( LPVOID lpParam )
<
tls_i = (int)lpParam;
lstrcpy(tls_char,”array of char”);
char szMsg[80];

wsprintf( szMsg, “Parameter = %d.”, tls_i );
MessageBox( NULL, szMsg, “ThreadFunc”, MB_OK );

int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
intnCmdShow)
<
DWORD dwThreadId;

CreateThread(NULL, 0, ThreadFunc, (LPVOID)1, 0, &dwThreadId);
CreateThread(NULL, 0, ThreadFunc, (LPVOID)2, 0, &dwThreadId);

Sleep(10000); // Наслаждаемся результатом 10 секунд

Используя директиву __declspec( thread ), мы объявили две TLS-переменные. Код приложения делает в точности все то же самое, что и в прошлом примере, с той лишь разницей, что его реализация получилась значительно проще за счет отказа от WinAPI.

Однако, тут следует обратить внимание на одну маленькую особенность. Переменная tls_char – это не указатель на блок памяти из кучи, как код с динамической TLS, а целый массив с элементами типа CHAR. Мы помним, что размер локальной памяти потока ограничен (1088 блоков в Windows XP), и, объявляя tls_char как массив, мы занимаем сразу 25 ячеек thread-local storage. Это очень и очень плохо, так как программа может обращаться к dll, которые тоже, в свою очередь, используют TLS. В итоге может случиться так, что памяти на всех не хватит, и мы получим нерабочее приложение. Помещение в TLS указателя на память, а не самого блока памяти – гораздо более рациональное решение.

Заключение

Многопоточное программирование – очень тонкая штука, и механизмы TLS помогают нам быстрее адаптироваться в мире тредов и разделяемых ресурсов. Если в коде используются глобальные или статические переменные, то при переходе к многопоточности thread-local storage просто незаменима.

Источник

Популярные записи

Как установить itunes на linux ubuntu
Linux mint tv maxe
Linux deploy как пользоваться
No mans sky linux
Консоль linux текстовый редактор
Linux debian настройка wifi

Adblock
detector