XNU 内核做到了。 Swift 线程是 POSIX pthread 又名 Mach 线程。在程序启动期间,XNU 内核解析 Mach-O 可执行格式并处理现代 LC_MAIN 或旧版 LC_UNIXTHREAD 加载命令(等等)。这是在内核函数中处理的:
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
)
恰好是open source
LC_MAIN通过thread_userstackdefault初始化栈
LC_UNIXTHREAD 到 load_threadstack。
正如@PeterCordes 在 cmets 中提到的,只有当内核创建主线程时,启动的进程本身才能通过 GCD 之类的 api 或直接通过系统调用从它自己的主线程生成子线程(bsdthread_create,不确定是否有其他)。系统调用恰好有
user_addr_t stack 作为第三个参数(即 MacOS 使用的 x86-64 System V 内核 ABI 中的 rdx)。 Reference for MacOS syscalls
我还没有彻底调查这个特定堆栈参数的细节,但我想它类似于 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
现在我们可以观察到一些有趣的事情:
-
Class 实例显然占用了与 Structs 不同的内存部分
- 将结构分配给新变量会复制到新的内存地址
- 分配类实例只是复制指针。
- 当引用全局
Structs 时,主线程和子线程都指向完全相同的内存
- 字符串确实有一个结构容器。
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
它为每个线程产生不同的值,因此线程堆栈明显不同。
进一步挖掘
有一个方便的 memory region lldb 命令可以揭示发生了什么。
memory region 0x000000010058a6f0
[0x000000010053d000-0x000000010058b000) rw- __DATA
所以全局Structs 位于预分配的可执行可写__DATA 内存页(与您的全局变量所在的内存页相同)。类0x7fcd0940cd40 地址的相同命令并不那么壮观(我认为这是一个动态分配的堆)。类似于线程堆栈地址0x7ffeefbfefc0,它显然不是进程内存区域。
幸运的是,还有最后一个工具可以进一步深入兔子洞。
vmmap -v -purge pid 确实确认类位于MALLOC_ed 堆中,同样可以交叉引用线程堆栈(至少对于主线程)到Stack。
有点相关的问题也是here。
HTH