iOSjava线程栈有没有自己的栈

I have a C# .NET 4.5 application heavily using the Task Parallel Library that eventually ends up starved for threads after days of operation.
When I grab a HANG dump from AdPlus and look at the threads via Visual Studio, I see 43 threads with no apparent origin in my code:
ntdll.dll!_NtWaitForSingleObject@12()
+ 0x15 bytes
ntdll.dll!_NtWaitForSingleObject@12()
+ 0x15 bytes
kernel32.dll!@BaseThreadInitThunk@12()
+ 0x12 bytes
ntdll.dll!___RtlUserThreadStart@8()
+ 0x27 bytes
ntdll.dll!__RtlUserThreadStart@8()
+ 0x1b bytes
Why do these threads show no managed origin in their stack trace?
解决方案 All threads in a given process, even TPL threads have this startup procedure.
When you start a thread running, eventually the CLR calls the OS to start a thread.
What you're looking at is the functions that the thread executes at startup.
If you suspend any managed process, you'll see that at the bottom of the stack there are unmanaged calls.
The reason you don't see the managed start procedure, is that each thread gets it's own stack, created by the OS when it creates the thread.
For example, running the following:
for (int i = 0; i & 10; i++)
Thread t = new Thread(new ThreadStart(()=&Thread.Sleep(100000)));
t.Start();
Console.ReadKey();
then breaking into the process using WinDbg, and looking at one of the sleeping threads, gives a call stack that looks like this (All of the threads have the same two functions at the bottom, I'm just dumping one for this exercise.):
0:012& !dumpstack
OS Thread Id: 0x3694 (12)
Current frame: ntdll!ZwDelayExecution+0xa
Caller, Callee
dc8ea70 000007fefd1c1203 KERNELBASE!SleepEx+0xab, calling ntdll!NtDelayExecution
dc8eae0 000007fefd1c38fb KERNELBASE!SleepEx+0x12d, calling ntdll!RtlActivateActivationContextUnsafeFast
dc8eb10 000007fed860a888 clr!CExecutionEngine::ClrSleepEx+0x29, calling KERNEL32!SleepExStub
dc8eb40 000007fed874d483 clr!Thread::UserSleep+0x7c, calling clr!ClrSleepEx
dc8eba0 000007fed874d597 clr!ThreadNative::Sleep+0xb7, calling clr!Thread::UserSleep
[... removed some frames for clarity ...]
dc8f6f0 000007fed874fcb6 clr!Thread::intermediateThreadProc+0x7d
dc8faf0 000007fed874fc9f clr!Thread::intermediateThreadProc+0x66, calling clr!alloca_probe
dc8fb30 5a4d KERNEL32!BaseThreadInitThunk+0xd
dc8fb60 cb831 ntdll!RtlUserThreadStart+0x1d
For reference, this is the Thread object wrapping the thread that we dumped the stack of:
0:012& !do 2a23e08
System.Threading.Thread
MethodTable: 000007fed76522f8
000007fed7038200
96(0x60) bytes
C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c\mscorlib.dll
Value Name
000007fed763eca8
8 ....Contexts.Context
0 instance 0000 m_Context
000007fed765a958
10 ....ExecutionContext
0 instance 0000 m_ExecutionContext
000007fed00767
System.String
0 instance 0000 m_Name
000007fed00768
System.Delegate
0 instance 0000 m_Delegate
000007fed00769
28 ...ation.CultureInfo
0 instance 0000 m_CurrentCulture
000007fed0076a
30 ...ation.CultureInfo
0 instance 0000 m_CurrentUICulture
000007fed0076b
System.Object
0 instance 0000 m_ThreadStartArg
000007fed0076c
System.IntPtr
1 instance
24a5ed0 DONT_USE_InternalThread
000007fed0076d
System.Int32
1 instance
2 m_Priority
000007fed0076e
System.Int32
1 instance
12 m_ManagedThreadId
000007fed0076f
System.Boolean
1 instance
1 m_ExecutionContextBelongsToOuterScope
000007fed00770
378 ...LocalDataStoreMgr
static s_LocalDataStoreMgr
&& Domain:Value
f40b0:NotInit
000007fed00771
8 ...alDataStoreHolder
TLstatic s_LocalDataStore
&& Thread:Value &&
The System.IntPtr
named DONT_USE_InternalThread holds the pointer to the OS thread. (My guess is that it's probably the handle from CreateThread, but I didn't investigate it too much.)
(Note to editors: brillant is spelled that way intentionally.
Please don't 'fix' it)
本文地址: &
我有一个C#.NET 4.5应用程序大量使用任务并行库,最终结束了操作天后饿死线程。
当我抓住从ADPlus的一个挂起转储,并期待在通过Visual Studio中的线程,我看到在我的code 43线程,无明显产地:
ntdll.dll!_NtWaitForSingleObject@12()+ 0x15字节
ntdll.dll!_NtWaitForSingleObject@12()+ 0x15字节
KERNEL32.DLL!@ BaseThreadInitThunk @ 12()+ 0×12字节
ntdll.dll中!___ RtlUserThreadStart @ 8()+ 0x27字节
ntdll.dll中!__ RtlUserThreadStart @ 8()+ 0x1b字节
为什么要在自己的堆栈跟踪做这些线程显示没有管理的起源?
解决方案 在一个给定的进程中的所有线程,甚至TPL线程有这个启动过程。当您启动一个线程在运行,最终CLR调用OS启动一个线程。你在寻找什么是该线程在启动时执行的功能。如果您暂停任何管理的过程中,你会看到在堆的底部也有非托管的呼叫。你看不到管理启动程序的原因,是每个线程得到它自己的堆栈,由OS创建时创建线程。
例如,运行以下内容:
的for(int i = 0;我小于10;我++)
线程t =新主题(新的ThreadStart(()=> Thread.sleep代码(100000)));
t.Start();
Console.ReadKey();
再破入过程中使用的WinDbg,看着熟睡的一个线程,给出了一个调用栈看起来像这样(所有线程都在底部的两个相同的功能,我只是倾销一个用于此运动):
0:012> !dumpstack
操作系统线程ID:0x3694(12)
当前帧:NTDLL ZwDelayExecution +是0xA
儿童-SP RetAddr主叫,被叫
dc8ea70 000007fefd1c1203 KERNELBASE!SleepEx +是0xAB,叫NTDLL!NtDelayExecution
dc8eae0 000007fefd1c38fb KERNELBASE!SleepEx + 0x12d,叫NTDLL!RtlActivateActivationContextUnsafeFast
dc8eb10 000007fed860a888 CLR!CExecutionEngine :: ClrSleepEx + 0x29,调用KERNEL32!SleepExStub
dc8eb40 000007fed874d483 CLR!主题:: UserSleep + 0x7c,调用CLR!ClrSleepEx
dc8eba0 000007fed874d597 CLR!ThreadNative ::休眠+ 0xb7,调用CLR!主题:: UserSleep
[...为了清楚而移除了一些帧...]
dc8f6f0 000007fed874fcb6 CLR!主题:: intermediateThreadProc + 0x7d
dc8faf0 000007fed874fc9f CLR!主题:: intermediateThreadProc + 0x66,调用CLR!alloca_probe
dc8fb30 5a4d KERNEL32!BaseThreadInitThunk + 0xd中
dc8fb60 cb831 NTDLL!RtlUserThreadStart + 0x1d
作为参考,这是发对象包装,我们倾倒的堆栈螺纹:
0:012> !做2a23e08
名称:System.Threading.Thread
方法表:000007fed76522f8
EEClass:000007fed7038200
尺寸:96(地址0x60)字节
文件:C:\ WINDOWS \ Microsoft.Net \组装\ GAC_64 \ mscorlib程序\ v4.0_4.0.0.0__b77a5c \ mscorlib.dll中
MT字段偏移类型VT的Attr值名称
000007fed763eca8
.... Contexts.Context 0例如0000 m_Context
000007fed765a958
.... 0的ExecutionContext例如0000 m_ExecutionContext
000007fed0767 18 System.String 0例如0000 m_Name
000007fed0768 20 System.Delegate 0例如0000 m_Delegate
000007fed0769 28 ... ation.CultureInfo 0例如0000 m_CurrentCulture
000007fed076a 30 ... ation.CultureInfo 0例如0000 m_CurrentUICulture
000007fed076b 38 System.Object的0例如0000 m_ThreadStartArg
000007fed076c 40 System.IntPtr 1实例24a5ed0 DONT_USE_InternalThread
000007fed076d 48 System.Int32的1个实例2 m_Priority
000007fed076e 4C System.Int32的1个实例12 m_ManagedThreadId
000007fed076f 50可选System.Boolean 1实例1 m_ExecutionContextBelongsToOuterScope
000007fed ...... LocalDataStoreMgr 0共享静态s_LocalDataStoreMgr
>>域名:值f40b0:NotInit<<
000007fed0771 8 ... alDataStoreHolder 0共享TLstatic s_LocalDataStore
>>主题:值小于;<
在 System.IntPtr
的名为 DONT_USE_InternalThread 持有的指针操作系统线程。 (我的猜测是,它可能是从的CreateThread 手柄,但我没有调查就太多了。)
(编者注:高明拼写这种方式有意请不要'修复'吧)的
本文地址: &
扫一扫关注IT屋
微信公众号搜索 “ IT屋 ” ,选择关注
与百万开发者在一起
(window.slotbydup = window.slotbydup || []).push({
id: '5828425',
container: s,
size: '300,250',
display: 'inlay-fix'我的 linux 中 进程栈的大小是 8M,请问当我 pthread_create 一个子线程后,这个子线程栈是多大?
该问题被发起重新开启投票
投票剩余时间:
之前被关闭原因:
该问题被发起删除投票
投票剩余时间:
距离悬赏到期还有:
参与关闭投票者:
关闭原因:
该问题已经被锁定
锁定原因:()
保护原因:避免来自新用户不合宜或无意义的致谢、跟帖答案。
该问题已成功删除,仅对您可见,其他人不能够查看。
Linux的栈由RLIMIT_STACK这个limit参数决定。
通过getrlimit系统调用或者ulimit(bash)命令可以查看。
getrlimit(RLIMIT_STACK, {rlim_cur=, rlim_max=RLIM_INFINITY}) = 0
yalung@yalung:/tmp$ ulimit -acore file size
(blocks, -c) 0data seg size
(kbytes, -d) unlimitedscheduling priority
(-e) 0file size
(blocks, -f) unlimitedpending signals
(-i) 7824max locked memory
(kbytes, -l) 64max memory size
(kbytes, -m) unlimitedopen files
(-n) 1024pipe size
(512 bytes, -p) 8POSIX message queues
(bytes, -q) 819200real-time priority
(-r) 0**stack size
(kbytes, -s) 8192**cpu time
(seconds, -t) unlimitedmax user processes
(-u) 7824virtual memory
(kbytes, -v) unlimitedfile locks
(-x) unlimitedyalung@yalung:/tmp$
我的环境上默认是8M。ulimit的作用机制是子进程继承父进程。ulimit命令实际上显示的是当前shell进程bash的栈,在这个shell下启动一个程序,程序就是shell的子进程(即使后来过继给1号,最初的基因也是从启动它的shell进程继承的)。所以程序的栈就是8M。
换一个角度讲,你要是想改变一个程序的默认栈,除了在main函数开始调用setrlimit外,也可以在程序启动脚本内,启动程序之前执行ulimit。
而pthread_create创建的线程,在linux上实际上就是一个进程,只不过和主进程共享了地址空间和内存而已。所以pthread_create默认就是进程的值。
通过man页:
implementation,
RLIMIT_STACK soft resource limit at the time the program started has
any value other than "unlimited", then
determines
size of new threads.
Using pthread_attr_setstacksize(3), the
stack size attribute can be explicitly set in the attr argument used
a thread, in order to obtain a stack size other than the
以及代码测试:
yalung@yalung:/tmp$ cat test.c#include &stdio.h&#include &stdlib.h&#include &unistd.h&#include &pthread.h&int main(int argc,char **argv){
pthread_attr_
pthread_attr_getstacksize(&attr,&size);
printf("size = %ld\n", size / 1024);}yalung@yalung:/tmp$ ./a.outsize = 8192yalung@yalung:/tmp$
可以验证这一点。
至于_SC_THREAD_STACK_MIN我记得那应该是《UNIX高级编程》这本书上的东西吧。书上的东西参考一下就可以了。关键还是实测和读代码。
#include &stdio.h&#include &stdlib.h&#include &unistd.h&int main(int argc,char **argv){
int thread_stack_
thread_stack_size = sysconf(_SC_THREAD_STACK_MIN);
printf("size = %d\n",thread_stack_size);}
在我的ubuntu下的结果是:size = 16384
德问是一个专业的编程问答社区,请
后再提交答案
关注该问题的人
共被浏览 (3487) 次Stack 增长很多人知道编译器有个设置选项,里面可以设置线程栈的大小,有两个值可以设置:l
Stack Reserve Size表示在虚拟内存中保留(Reserve)给栈的虚拟空间大小,Stack增长不能超过这个界限,如果不设置,默认是1M。l
Stack Commit Size表示线程初始化时在为其保留的虚拟空间内提交(Commit)的内存大小,如果不设置,默认仅提交一个页,即4K。另外在调用CreateThread创建线程时也可以动态修改初始化提交的页面大小。这两个数值具体放置在PE文件头的 IMAGE_OPTIONAL_HEADER 内的变量 SizeOfStackReserve 和 SizeOfStackCommit 。 为什么不一次性提交全部保留大小,这样设计当然是为了节省物理内存。那么接下来的关键问题是:stack的Commit区域是如何增长的?
神秘函数 _chkstk如果一个函数中在栈中分配了超过一个页大小(4096字节),查看汇编代码,在函数头部编译器会帮你插入调用_chkstk的指令:mov
//这里的eax作为唯一的函数参数,表示这个函数将从栈中分配的字节数call
_chkstk函数_chkstk在vs2005下实现代码如下:
计算栈新的栈顶位置( TOS )
ecx, [esp +
进入此函数前的栈顶位置 + 存储函数返回地址所占的4字节)
栈新的栈顶位置;
注意前面指令中的ecx可能小于eax,下面处理这种情况——如果出现ecx小于eax,就设置ecx为0
eax, 0 if CF==0, ~0 if CF==1
~0 if TOS did not wrapped around, 0 otherwise
ecx, set to 0 if wraparound; 下面指令从当前栈顶位置开始,按顺序逐页逐页的walk,直到新的栈顶位置
当前栈顶位置
0xFFFF000 Round down to current page boundarycs10:
比较是否已经到达了新的栈顶位置
short cs20
修改esp为新的栈顶位置
eax, dword ptr [eax] get return address
dword ptr [esp], and put it at new TOS
ret; Find next lower page and probecs20:
eax, _PAGESIZE_ decrease by PAGESIZE
dword ptr [eax], probe page.
short cs10 从上面代码可以清楚的看出_chkstk的做了什么工作:(1) 计算栈新的栈顶位置(2) 从当前栈顶位置开始,按顺序逐页逐页的walk,直到新的栈顶位置(3) 修改esp指向新的位置,即分配栈空间这里的关键的动作只有一行代码,在第(2)步: test
dword ptr [eax],eax 。 可是这行代码仅仅是读了一下eax指向的内存,难道这里隐藏着什么东西?没错,因为这里的读操作将触发一个STATUS_GUARD_PAGE异常,内核通过捕获这个异常,从而知道你的线程已经越过了栈中已提交内存区域的边界,这时应该增加新的页了。 操作系统规定栈中的页commit必须逐页提交,具体的实现是,对已提交的内存区域的最后一个页设置PAGE_GUARD属 性 ,当这个页发生 STATUS_GUARD_PAGE异常时(这个异常会自动清除其 PAGE_GUARD属性) ,再commit下一个页,同时设置其 PAGE_GUARD属 性。
获取Stack 中Commit内存区域边界1. 通过TEB(Thread Environment Block,线程环境块)获取系 统在此TEB中保存线程频繁使用的相关数据。位于用户地址空间。进程中的每个线程都有自己的一个TEB。一个进程的所有TEB都存放在从 0x7FFDE000开始的线性内存中,每4KB为一个完整的TEB,不过该内存区域是向低地址扩展的。在用户模式下,每个线程的TEB位于独立的4KB 段,可通过CPU的FS段寄存器来访问该段,一般存储在[FS:0]。在用户态下WinDbg中可用命令$thread取得TEB地址。TEB最前面的结构是TIB,定义如下:typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionL
PVOID StackB
PVOID StackL
PVOID SubSystemT
PVOID FiberD
PVOID ArbitraryUserP
struct _NT_TIB *S} NT_TIB; 栈提交区域上下边界值各位于偏移4和8字节处。因此,可以通过下面方法获取:LPVOID pStackHigh, pStackL__asm{mov eax,fs:[4];mov pStackHigh,mov eax,fs:[8];mov pStackLow,}
2. 通过系统未公开的API获取当线程切换时,FS段寄存器也会发生切换,这样导致一个问题,实际上一个线程不能通过FS段访问其它线程的TEB,它只能访问到它自己的TEB。通过ntdll.dll未公开函数 NtQueryInformationThread 可以方便访问其它线程TEB。typedef struct _THREAD_BASIC_INFORMATION {
CLIENT_ID ClientId;
KAFFINITY AffinityM
KPRIORITY P
KPRIORITY BaseP} THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;typedef NTSTATUS (__stdcall *NtQueryInformationThread_Type) (IN HANDLE ThreadHandle,IN THREADINFOCLASS ThreadInformationClass,OUT PVOID ThreadInformation,IN ULONG ThreadInformationLength,OUT PULONG ReturnLength OPTIONAL
); 相对代码如下:NtQueryInformationThread_Type
pNtQueryInformationThread = (NtQueryInformationThread_Type)
GetProcAddress(
GetModuleHandle(_T("ntdll.dll")
), "NtQueryInformationThread"); THREAD_BASIC_INFORMATIONNTSTATUS Status = pNtQueryInformationThread(hThread, ThreadBasicInformation, &tbi, sizeof(tbi), NULL);if (NT_SUCCESS(Status)){stackLow = tbi.TebBaseAddress-&pvStackLstackHigh = tbi.TebBaseAddress-&pvStackB} Stack Overflow (栈溢出)当 访问地址超过栈的保留内存区域时,就会发生栈溢出,windows操作系统会产生EXCEPTION_STACK_OVERFLOW异常。因为发生异常 后,异常处理代码也在同一个线程运行,实际上,当你开始访问倒数第三个页时,windows就会发出这个异常,从而给异常处理函数留下两个页的栈空间。这 里最后一个页永远是Reserver状态,windows这样做的理由是如果异常处理代码超出为它预留的2个页,就会引发访问异常。为了观察栈访问到哪儿引发EXCEPTION_STACK_OVERFLOW异常,我用了下面的一个测试函数,通过无限梯归调用制造栈溢出。void _declspec(naked) test_stack(){_asm{push ebpmov ebp, espcall test_pop ebpret}}然后通过Vector 异常处理函数捕获栈溢出异常,并调用下面代码,观察访问到哪儿会引发访问异常。__asm{mov ebx, espLOOP_BEGIN:mov eax, dword ptr[ebx]sub ebx, 4jmp LOOP_BEGIN} (1)如何捕获栈溢出异常l
通过SetUnhandledExceptionFilter设置异常钩子l
通过 AddVectoredExceptionHandle (需要 xp或以上操作系统支持)后一种方法在某些情况下更有效,它能有够有机会在任何基于栈的异常处理之前被调用。例如你的代码中可能存在一些自己的结构化异常处理:__try{......}__except(EXCEPTION_EXECUTE_HANDLER){......}如 果上面的情况下使用的是SetUnhandledExceptionFilter话,这里的栈溢出异常首先被__try块捕获。如果它并不能真正处理好栈 溢出异常,接下来的栈溢出(因为只有2个页面了,这时很容易发生溢出)只会引发访问异常,不再是栈溢出异常,并被 SetUnhandledExceptionFilter捕获到,这当然不是你所希望的。如果想在第一时间捕获栈溢出,应该使用 AddVectoredExceptionHandle 。 (2)处理栈溢出栈 溢出是非常严重的bug,大多数时候都没有恢复的必要性,唯一能做的就是及时记录crash现场信息(比如生成dump文件),因为当发生栈溢出,只剩下 2个页面的栈空间,在异常处理代码中需要小心翼翼的处理,确保对栈的使用不超出范围。但是有些系统函数,例如dbghelp的 MiniDumpWriteDump 栈空间使用超过2个页面。 一个最简单有效的方法就是创建新的线程,然后马上Sleep(INFINITE),由新线程来处理这些crash信息收集工作。
没有更多推荐了,栈公有,堆私有
栈公有,堆公有
栈私有,堆公有
栈私有,堆私有
在多线程环境下,每个线程拥有一个栈和一个程序计数器。栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。其他的资源(比如堆、地址空间、全局变量)是由同一个进程内的多个线程共享
堆主要是动态静态分配内存空间,内存空间在内部环境是统一编址的,不会因为多了一个对象而复制另一块独立的内存空间给实例对象,而栈是存储临时变量等的,有一定生命周期,是多线程独立的。
可以理解为堆是全局性的,栈是局部性的
堆在一起的东西,肯定是公用(公有)的,你占(栈)有的东西,肯定是你自己私有的。
上面纯属娱乐,不过容易记忆。
线程拥有的少量资源:程序计数器、寄存器和栈
可以结合stringBuilder来理解
太模糊了这题,全是背板的,用户线程栈都在一个地址空间,相互访问当然可以,只是地址的获取比较困难,如果这个叫私有的话 那么windows下某线程HeapCreate创建的堆,其余线程也需要一定方法才能获取句柄,那么这个堆是不是可以说是私有的? 这题我真给不出答案
在多线程环境下,每个线程拥有一个栈和一个程序计数器。栈和程序计数器用来保存线程的执行历史和线程的执行状态,是线程私有的资源。其他的资源(比如堆、地址空间、全局变量)是由同一个进程内的多个线程共享
这道题你会答吗?花几分钟告诉大家答案吧!
扫描二维码,关注牛客网
下载牛客APP,随时随地刷题
京ICP备号-4
扫一扫,把题目装进口袋BSBacktraceLogger 是一个轻量级的框架,可以获取任意线程的调用栈,开源在我的 ,建议下载下来结合本文阅读。
我们知道 NSThread 有一个类方法 callstackSymbols 可以获取调用栈,但是它输出的是当前线程的调用栈。在利用 Runloop 检测卡顿时,子线程检测到了主线程发生卡顿,需要通过主线程的调用栈来分析具体是哪个方法导致了阻塞,这时系统提供的方法就无能为力了。
最简单、自然的想法就是利用 dispatch_async 或 performSelectorOnMainThread 等方法,回到主线程并获取调用栈。不用说也能猜到这种想法并不可行,否则就没有写作本文的必要了。
这篇文章的重点不是介绍获取调用栈的细节,而是在实现过程中的遇到的诸多问题和尝试过的解决方案。有的方案也许不能解决问题,但在思考的过程中能够把知识点串联起来,在我看来这才是本文最大的价值。
在介绍后续知识之前,有必要介绍一下调用栈的相关背景知识。
首先聊聊栈,它是每个线程独享的一种数据结构。借用维基百科上的一张图片:
上图表示了一个栈,它分为若干栈帧(frame),每个栈帧对应一个函数调用,比如蓝色的部分是 DrawSquare 函数的栈帧,它在执行的过程中调用了 DrawLine 函数,栈帧用绿色表示。
可以看到栈帧由三部分组成:函数参数,返回地址,帧内的变量。举个例子,在调用 DrawLine 函数时首先把函数的参数入栈,这是第一部分;随后将返回地址入栈,这表示当前函数执行完后回到哪里继续执行;在函数内部定义的变量则属于第三部分。
Stack Pointer(栈指针)表示当前栈的顶部,由于大部分操作系统的栈向下生长,它其实是栈地址的最小值。根据之前的解释,Frame Pointer 指向的地址中,存储了上一次 Stack Pointer 的值,也就是返回地址。
在大多数操作系统中,每个栈帧还保存了上一个栈帧的 Frame Pointer,因此只要知道当前栈帧的 Stack Pointer 和 Frame Pointer,就能知道上一个栈帧的 Stack Pointer 和 Frame Pointer,从而递归的获取栈底的帧。
显然当一个函数调用结束时,它的栈帧就不存在了。
因此,调用栈其实是栈的一种抽象概念,它表示了方法之间的调用关系,一般来说从栈中可以解析出调用栈。
失败的传统方法
最初的想法很简单,既然 callstackSymbols 只能获取当前线程的调用栈,那在目标线程调用就可以了。比如 dispatch_async 到主队列,或者 performSelector 系列,更不用说还可以用 Block 或者代理等方法。
我们以 UIViewController 的viewDidLoad 方法为例,推测它底层都发生了什么。
首先主线程也是线程,就得按照线程基本法来办事。线程基本法说的是首先要把线程运行起来,然后(如果有必要,比如主线程)启动 runloop 进行保活。我们知道 runloop 的本质就是一个死循环,在循环中调用多个函数,分别判断 source0、source1、timer、dispatch_queue 等事件源有没有要处理的内容。
和 UI 相关的事件都是 source0,因此会执行 __CFRunLoopDoSources0,最终一步步走到 viewDidLoad。当事件处理完后 runloop 进入休眠状态。
假设我们使用 dispatch_async,它会唤醒 runloop 并处理事件,但此时 __CFRunLoopDoSources0 已经执行完毕,不可能获取到 viewDidLoad 的调用栈。
performSelector 系列方法的底层也依赖于 runloop,因此它只是像当前的 runloop 提交了一个任务,但是依然要等待现有任务完成以后才能执行,所以拿不到实时的调用栈。
总而言之,一切涉及到 runloop,或者需要等待 viewDidLoad 执行完的方案都不可能成功。
要想不依赖于 viewDidLoad 完成,并在主线程执行代码,只能从操作系统层面入手。我尝试了使用信号(Signal)来实现,
信号其实是一种软中断,也是由系统的中断处理程序负责处理。在处理信号时,操作系统会保存正在执行的上下文,比如寄存器的值,当前指令等,然后处理信号,处理完成后再恢复执行上下文。
因此从理论上来说,信号可以强制让目标线程停下,处理信号再恢复。一般情况下发送信号是针对整个进程的,任何线程都可以接受并处理,也可以用 pthread_kill() 向指定线程发送某个信号。
信号的处理可以用 signal 或者 sigaction 来实现,前者比较简单,后者功能更加强大。
比如我们运行程序后按下 Ctrl + C 实际上就是发出了 SIGINT 信号,以下代码可以在按下 Ctrl + C 时做一些输出并避免程序退出:
void sig_handler(int signum) {
printf("Received signal %d\n", signum);
void main() {
signal(SIGINT, sig_handler);
遗憾的是,使用pthread_kill() 发出的信号似乎无法被上述方法正确处理,查阅各种资料无果后放弃此思路。但至今任然觉得这是可行的,如果有人知道还望指正。
Mach_thread
回忆之前对栈的介绍,只要知道 StackPointer 和 FramePointer 就可以完全确定一个栈的信息,那有没有办法拿到所有线程的 StackPointer 和 FramePointer 呢?
答案是肯定的,首先系统提供了 task_threads 方法,可以获取到所有的线程,注意这里的线程是最底层的 mach 线程,它和 NSThread 的关系稍后会详细阐述。
对于每一个线程,可以用 thread_get_state 方法获取它的所有信息,信息填充在 _STRUCT_MCONTEXT 类型的参数中。这个方法中有两个参数随着 CPU 架构的不同而改变,因此我定义了 BS_THREAD_STATE_COUNT 和 BS_THREAD_STATE 这两个宏用于屏蔽不同 CPU 之间的区别。
在 _STRUCT_MCONTEXT 类型的结构体中,存储了当前线程的 Stack Pointer 和最顶部栈帧的 Frame Pointer,从而获取到了整个线程的调用栈。
在项目中,调用栈存储在 backtraceBuffer 数组中,其中每一个指针对应了一个栈帧,每个栈帧又对应一个函数调用,并且每个函数都有自己的符号名。
接下来的任务就是根据栈帧的 Frame Pointer 获取到这个函数调用的符号名。
就像 “把大象关进冰箱需要几步” 一样,获取 Frame Pointer 对应的符号名也可以分为以下几步:
根据 Frame Pointer 找到函数调用的地址
找到 Frame Pointer 属于哪个镜像文件
找到镜像文件的符号表
在符号表中找到函数调用地址对应的符号名
这实际上都是 C 语言编程问题,我没有相关经验,不过好在有前人的研究成果可以借鉴。感兴趣的读者可以直接阅读源码。
揭秘 NSThread
根据上述分析,我们可以获取到所有线程以及他们的调用堆栈,但如果想单独获取某个线程的堆栈呢?问题在于,如何建立 NSThread 线程和内核线程之间的联系。
再次 Google 无果后,我找到了 ,下载了 1.24.9 版本,其中包含了 Foundation 库的源码,我不能确保现在的 NSThread 完全采用这里的实现,但至少可以从 NSThread.m 类中挖掘出很多有用信息。
NSThread 的封装层级
很多文章都提到了 NSThread 是 pthread 的封装,这就涉及两个问题:
pthread 是什么
NSThread 如何封装 pthread
pthread 中的字母 p 是 POSIX 的简写,POSIX 表示 “可移植操作系统接口(Portable Operating System Interface)”。
每个操作系统都有自己的线程模型,不同操作系统提供的,操作线程的 API 也不一样,这就给跨平台的线程管理带来了问题,而 POSIX 的目的就是提供抽象的 pthread 以及相关 API,这些 API 在不同操作系统中有不同的实现,但是完成的功能一致。
Unix 系统提供的 thread_get_state 和 task_threads 等方法,操作的都是内核线程,每个内核线程由 thread_t 类型的 id 来唯一标识,pthread 的唯一标识是 pthread_t 类型。
内核线程和 pthread 的转换(也即是 thread_t 和 pthread_t 互转)很容易,因为 pthread 诞生的目的就是为了抽象内核线程。
说 NSThread 封装了 pthread 并不是很准确,NSThread 内部只有很少的地方用到了 pthread。NSThread 的 start 方法简化版实现如下:
- (void) start {
pthread_attr_
errno = 0;
pthread_attr_init(&attr);
if (pthread_create(&thr, &attr, nsthreadLauncher, self)) {
甚至于 NSThread 都没有存储新建 pthread 的 pthread_t 标识。
另一处用到 pthread 的地方就是 NSThread 在退出时,调用了 pthread_exit()。除此以外就很少感受到 pthread 的存在感了,因此个人认为 “NSThread 是对 pthread 的封装” 这种说法并不准确。
PerformSelectorOn
实际上所有的 performSelector系列最终都会走到下面这个全能函数:
- (void) performSelector: (SEL)aSelector
onThread: (NSThread*)aThread
withObject: (id)anObject
waitUntilDone: (BOOL)aFlag
modes: (NSArray*)anA
而它仅仅是一个封装,根据线程获取到 runloop,真正调用的还是 NSRunloop 的方法:
- (void) performSelector: (SEL)aSelector
target: (id)target
argument: (id)argument
order: (NSUInteger)order
modes: (NSArray*)modes{}
这些信息将组成一个 Performer 对象放进 runloop 等待执行。
NSThread 转内核 thread
由于系统没有提供相应的转换方法,而且 NSThread 没有保留线程的 pthread_t,所以常规手段无法满足需求。
一种思路是利用 performSelector 方法在指定线程执行代码并记录 thread_t,执行代码的时机不能太晚,如果在打印调用栈时才执行就会破坏调用栈。最好的方法是在线程创建时执行,上文提到了利用 pthread_create 方法创建线程,它的回调函数 nsthreadLauncher 实现如下:
static void *nsthreadLauncher(void* thread)
NSThread *t = (NSThread*)
[nc postNotificationName: NSThreadDidStartNotification object:t userInfo: nil];
[t _setName: [t name]];
[NSThread exit];
return NULL;
很神奇的发现系统居然会发送一个通知,通知名不对外提供,但是可以通过监听所有通知名的方法得知它的名字: @"_NSThreadDidStartNotification",于是我们可以监听这个通知并调用 performSelector 方法。
一般 NSThread 使用 initWithTarget:Selector:object 方法创建。在 main 方法中 selector 会被执行,main 方法执行结束后线程就会退出。如果想做线程保活,需要在传入的 selector 中开启 runloop,详见我的这篇文章: 。
可见,这种方案并不现实,因为之前已经解释过,performSelector 依赖于 runloop 开启,而 runloop 直到 main 方法才有可能开启。
回顾问题发现,我们需要的是一个联系 NSThread 对象和内核 thread 的纽带,也就是说要找到 NSThread 对象的某个唯一值,而且内核 thread 也具有这个唯一值。
观察一下 NSThread,它的唯一值只有对象地址,对象序列号(Sequence Number) 和线程名称:
&NSThread: 0x144d095e0&{number = 1, name = main}
地址分配在堆上,没有使用意义,序列号的计算没有看懂,因此只剩下 name。幸运的是 pthread 也提供了一个方法 pthread_getname_np 来获取线程的名字,两者是一致的,感兴趣的读者可以自行阅读 setName 方法的实现,它调用的就是 pthread 提供的接口。
这里的 np 表示 not POSIX,也就是说它并不能跨平台使用。
于是解决方案就很简单了,对于 NSThread 参数,把它的名字改为某个随机数(我选择了时间戳),然后遍历 pthread 并检查有没有匹配的名字。查找完成后把参数的名字恢复即可。
主线程转内核 thread
本来以为问题已经圆满解决,不料还有一个坑,主线程设置 name 后无法用 pthread_getname_np 读取到。
好在我们还可以迂回解决问题: 事先获得主线程的 thread_t,然后进行比对。
上述方案要求我们在主线程中执行代码从而获得 thread_t,显然最好的方案是在 load 方法里:
static mach_port_t main_thread_
+ (void)load {
main_thread_id = mach_thread_self();
以上就是 BSBacktraceLogger 的全部分析,它只有一个类,400行代码,因此还算是比较简单。然而 NSThread、NSRunloop 以及 GCD 的源码着实值得反复研究、阅读。
完成一个技术项目往往最大的收获不是最后的结果,而是实现过程中的思考。这些走过的弯路加深了对知识体系的理解。
关注与订阅
搜索 “iOSZhaZha” 关注微信公众号,第一时间获得更新
iOS中线程Call Stack的捕获和解析(一)
获取当前空闲CPU比较准确的方法
监控线程的 Mach 异常
获取iOS任意线程调用堆栈(一)获取任意线程的调用栈地址列表
没有更多推荐了,}

我要回帖

更多关于 线程栈大小 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信