Кто создает и владеет стеком вызовов и как стек вызовов работает в многопоточности? - PullRequest
0 голосов
/ 16 октября 2019

Я знаю, что каждый поток обычно имеет один стек вызовов, который является просто частью памяти и управляется с помощью esp и ebp.

1, как создаются эти стеки вызовов и кто за это отвечает? Я предполагаю, что время выполнения, например, Swift Runtime для приложения iOS. И действительно ли поток напрямую обращается к своему стеку вызовов по esp и ebp или через время выполнения?

2, для каждого стека вызовов они должны работать с регистрами esp и ebb cpu, если у меня есть процессорс двумя ядрами и четырьмя потоками, скажем, у него есть четыре ядра (наборы инструкций). Значит ли это, что каждый стек вызовов будет работать с этими регистрами только в конкретном ядре?

Ответы [ 2 ]

2 голосов
/ 16 октября 2019

Ядро XNU делает это. Swift - это нити POSIX Mach . Во время запуска программы ядро ​​XNU анализирует исполняемый формат Mach-O и обрабатывает либо современную LC_MAIN, либо устаревшую команду LC_UNIXTHREAD load (среди прочих). Это выполняется в функциях ядра:

static
load_return_t
load_main(
        struct entry_point_command  *epc,
        thread_t        thread,
        int64_t             slide,
        load_result_t       *result
    )

&

static
load_return_t
load_unixthread(
    struct thread_command   *tcp,
    thread_t        thread,
    int64_t             slide,
    load_result_t       *result
)

, которые оказываются открытым исходным кодом

LC_MAIN инициализируетстек через thread_userstackdefault

LC_UNIXTHREAD - load_threadstack.

Как @PeterCordes упоминает в комментариях, только когда ядро ​​создает основной поток, сам запущенный процесс может порождать дочерние потоки из своих собственных. Основной поток либо через некоторые API, такие как GCD, либо непосредственно через системный вызов (bsdthread_create, не уверен, если есть другие). Системный вызов имеет user_addr_t stack в качестве третьего аргумента (т. Е. rdx в ядре x86-64 System V ABI, используемом MacOS). Справочник по системным вызовам MacOS
Я не подробно исследовал детали этого конкретного аргумента стека, но я предположил бы, что он похож на thread_userstackdefault / load_threadstack подход.

Я делаюПолагайте, что ваши сомнения относительно ответственности Swift во время выполнения могут возникнуть из-за частых упоминаний о структурах данных (например, Swift struct - без каламбура), хранящихся в стеке (что является подробностью реализации, а не гарантированной особенностью среды выполнения).

Обновление :
Он пример main.swift программы командной строки, иллюстрирующей идею.

import Foundation

struct testStruct {
    var a: Int
}

class testClass {
}

func testLocalVariables() {
    print("main thread function with local varablies")
    var struct1 = testStruct(a: 5)
    withUnsafeBytes(of: &struct1) { print($0) }
    var classInstance = testClass()
    print(NSString(format: "%p", unsafeBitCast(classInstance, to: Int.self)))
}
testLocalVariables()

print("Main thread", Thread.isMainThread)
var struct1 = testStruct(a: 5)
var struct1Copy = struct1

withUnsafeBytes(of: &struct1) { print($0) }
withUnsafeBytes(of: &struct1Copy) { print($0) }

var string = "testString"
var stringCopy = string

withUnsafeBytes(of: &string) { print($0) }
withUnsafeBytes(of: &stringCopy) { print($0) }

var classInstance = testClass()
var classInstanceAssignment = classInstance
var classInstance2 = testClass()

print(NSString(format: "%p", unsafeBitCast(classInstance, to: Int.self)))
print(NSString(format: "%p", unsafeBitCast(classInstanceAssignment, to: Int.self)))
print(NSString(format: "%p", unsafeBitCast(classInstance2, to: Int.self)))

DispatchQueue.global(qos: .background).async {
    print("Child thread", Thread.isMainThread)
    withUnsafeBytes(of: &struct1) { print($0) }
    withUnsafeBytes(of: &struct1Copy) { print($0) }
    withUnsafeBytes(of: &string) { print($0) }
    withUnsafeBytes(of: &stringCopy) { print($0) }
    print(NSString(format: "%p", unsafeBitCast(classInstance, to: Int.self)))
    print(NSString(format: "%p", unsafeBitCast(classInstanceAssignment, to: Int.self)))
    print(NSString(format: "%p", unsafeBitCast(classInstance2, to: Int.self)))
}

//Keep main thread alive indefinitely so that process doesn't exit
CFRunLoopRun()

Мой вывод выглядит так:

main thread function with local varablies
UnsafeRawBufferPointer(start: 0x00007ffeefbfeff8, count: 8)
0x7fcd0940cd30
Main thread true
UnsafeRawBufferPointer(start: 0x000000010058a6f0, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a6f8, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a700, count: 16)
UnsafeRawBufferPointer(start: 0x000000010058a710, count: 16)
0x7fcd0940cd40
0x7fcd0940cd40
0x7fcd0940c900
Child thread false
UnsafeRawBufferPointer(start: 0x000000010058a6f0, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a6f8, count: 8)
UnsafeRawBufferPointer(start: 0x000000010058a700, count: 16)
UnsafeRawBufferPointer(start: 0x000000010058a710, count: 16)
0x7fcd0940cd40
0x7fcd0940cd40
0x7fcd0940c900

Теперь мы можем наблюдать пару интересных вещей:

  1. Class экземпляры явно занимают другую часть памяти, чем Structs
  2. При назначении структуры новой переменной получаетсякопировать на новый адрес памяти
  3. При назначении экземпляра класса просто копируется указатель.
  4. И основной поток, и дочерний поток при обращении к глобальному Structs указывают на точно такая же память
  5. Строки имеют контейнер Struct.

Update2 - подтверждение 4 ^ Мы можем на самом деле проверить память под ней:

x 0x10058a6f0 -c 8
0x10058a6f0: 05 00 00 00 00 00 00 00                          ........
x 0x10058a6f8 -c 8
0x10058a6f8: 05 00 00 00 00 00 00 00                          ........

Так что это определенно фактические данные структуры сырой, т.е. сама структура .

Обновление 3

Я добавил функцию testLocalVariables(), чтобы различать Swift Struct, определенный как глобальные и локальные переменные. В этом случае

x 0x00007ffeefbfeff8 -c 8
0x7ffeefbfeff8: 05 00 00 00 00 00 00 00                          ........

явно живет в стеке .

Последнее, но не менее важное, когда в lldb я делаю:

re read rsp
rsp = 0x00007ffeefbfefc0  from main thread
re read rsp
rsp = 0x000070000291ea40  from child thread

это дает различное значение для каждого потока, поэтому стеки потока четко различаются.

Копаем дальше
Есть удобная область памяти Команда lldb, которая проливает свет на происходящее.

memory region 0x000000010058a6f0
[0x000000010053d000-0x000000010058b000) rw- __DATA

Так глобально Structs сидеть на предварительно выделенной исполняемой для записи __DATA странице памяти (той же самой, где живут ваши глобальные переменные). Та же команда для адреса класса 0x7fcd0940cd40 не столь впечатляющая (я считаю, что это динамически размещаемая куча). Аналогично для адреса стека потока 0x7ffeefbfefc0, который явно не является областью памяти процесса.

К счастью, есть еще один последний инструмент для дальнейшего продвижения по кроличьей норе.
vmmap -v -purge pid , который подтверждает, что классы располагаются в куче MALLOC_ ed и также в потокестек (по крайней мере, для основного потока) может иметь перекрестные ссылки на Stack.

В некоторой степени связанный с этим вопрос также здесь .

HTH

2 голосов
/ 16 октября 2019

(Я предполагаю, что многопоточность Swift похожа на потоки в других языках. На самом деле хороших вариантов не так много, будь то обычные потоки на уровне ОС или «зеленые потоки» в пространстве пользователя, или их сочетание. Разницапроисходит только тогда, когда происходят переключения контекста; основные концепции остаются прежними)


Каждый поток имеет свой собственный стек, выделенный в адресном пространстве процесса как mmap или чем-то родительским потоком, или, возможно,тот же системный вызов, который создает поток. Системные вызовы IDK iOS. В Linux вы должны передать void *child_stack системному вызову , специфичному для Linux clone(2), который фактически создает новый поток. Очень редко можно напрямую использовать системные вызовы низкого уровня для конкретной ОС;среда выполнения языка, вероятно, будет выполнять многопоточность поверх функций pthreads, таких как pthread_create, и эта библиотека pthreads будет обрабатывать специфичные для ОС детали.


И да, каждый программный поток имеет свое собственное архитектурное состояние, включаяRSP на x86-64 или sp на AArch64 . (Или ESP, если вы делаете устаревший 32-битный код x86). Я предполагаю, что указатели фреймов необязательны для swift.

И да каждое логическое ядро ​​имеет свое собственное архитектурное состояние (регистры, включая указатель стека) ;программный поток работает на логическом ядре, и контекст переключается между программными потоками для сохранения / восстановления регистров. Возможно, дубликат Какие ресурсы распределены между потоками? .

Программные потоки используют одни и те же таблицы страниц (виртуальное адресное пространство), но регистры не .

...