尧图精选

Linux系统篇42——线程(七) pthread库管理线程的工作流,从创建到回收

🕒 发布时间:2026/10/1 17:08:53 📁 来源:尧图网络
本文收录于「流浪」的系列专栏Linux系统⚙️C数据结构与算法PythonLangChain LangGraph️MySQL 数据库Git 工具计算机网络AI大厂面试、八股学习筑基专栏 博客主页流浪 原创首发于 CSDN前言线程六讲完退出和隔离线程控制的面儿全了。但还有个更底层的问题没拆开——pthread 库到底是怎么把一个线程从无到有管起来的。本篇拆这条工作流库怎么描述线程内核怎么造执行流两者怎么联动join 怎么收割。一、Linux 没有真正的线程线程库是用户层的封装1.1 内核眼里只有轻量级进程篇36 讲过一个底层事实Linux 没有真正的线程只是用轻量级进程LWP来模拟的。内核里只有 task_struct 这一种执行流描述符线程和进程在内核眼里是同一种东西区别只在共享了哪些资源。由此推出一个关键结论OS 不提供线程接口。既然内核不认识「线程」这个概念自然不会有create_thread()这样的系统调用给你用。1.2 用户层自己封装造出原生线程库内核不提供应用又需要这个缺口只能由用户层来补——在用户层封装轻量级进程形成原生线程库。Linux 下这就是NPTLNative POSIX Threads Library原生 POSIX 线程库从 glibc 2.3.2 起成为标准实现。按照 man 手册 pthreads(7) 的说法它是1:1 实现——你每创建一个用户线程库就在内核里造一个对应的调度实体一个对一个。1:1 意味着每个线程在内核眼里都是一个独立可调度的任务多核机器上可以真正并行着跑ps -eLf里能看到每个线程自己的 LWP 号。更早的实现 LinuxThreads 还要在进程里塞一个 manager 线程替库打杂NPTL 把它干掉了线程就是普通任务没有中间商。我们前面所有篇章用的 pthread_create、pthread_join全是这个库提供的函数。1.3 这个库是动态库映射进进程地址空间pthread 库本身是个动态库。动态库和你的可执行程序是同一种 ELF 格式只是文件类型字段标的是共享对象ET_DYN而不是可执行ET_EXEC——这是 ELF 规范里 e_type 字段的两种取值本质是一家人。程序启动时会发生什么可执行程序被加载形成进程经过动态链接和动态地址重定向动态库也被加载到内存映射到当前进程的地址空间。这一步是后面一切的前提库被映射进了进程地址空间进程自己的代码才可以访问 pthread 库内部的相关数据。管理线程用的数据结构、栈、属性全都放在这块映射出来的区域里跟你的全局变量、堆栈处在同一个地址空间中互相看得见。二、库要管理线程先描述再组织线程的概念是在库中维护的。库内部一定会存在多个被创建好的线程那库要不要管理它们要。管理的套路还是那句老话——先描述再组织。2.1 先描述管理块里记什么描述线程就是给每个线程建一个结构体。线程控制块TCB是这类结构的通用叫法glibc 源码里的实名是struct pthread定义在nptl/descr.h中。本篇后面统一用管理块称呼它。核心字段长这样简化自 glibc nptl/descr.hstructpthread{pid_ttid;// 内核侧的轻量级进程 IDvoid*result;// 线程的返回值void*(*start_routine)(void*);// 线程要执行的函数void*arg;// 传给函数的参数// 线程状态、独立栈结构、栈大小、取消状态...};线程从创建到组织起来要记的参数就落在这几个字段上内核 ID、返回值、执行方法和参数、栈信息、状态标志。前两篇埋的伏笔在这里收口——篇40 拆过 pthread_t 的本质是地址那个地址指向的就是这个管理块篇39 讲过 create 的第三参数是返回值通道函数指针和参数先存进管理块就是为执行那天做准备。2.2 调度属性不归库管归内核有一批属性在描述时被刻意排除了优先级、时间片、上下文。这三样不进管理块它们走另一条路——LWP → PCB在内核中维护。道理很简单这三样都是调度概念而调度是内核的活。库在用户态管不着 CPU 给谁时间片。于是线程的相关概念被劈成了两半对比项用户层pthread 库内核LWP → PCB管理块struct pthreadtask_struct记什么线程状态、线程 id、独立栈、栈大小、返回值、函数指针优先级、时间片、上下文谁在用pthread 系列库函数内核调度器线程的相关概念一部分在用户层一部分在内核维护。这张表就是本篇的中心结论后面三章全是围绕它展开的工作流。2.3 再组织管理块挂在链表上光描述不够多个管理块得串起来。glibc 的做法是把每个 struct pthread 用 list 字段挂进库维护的全局链表源码注释写明挂在 dl_stack_used 或 dl_stack_user 链上。库想遍历所有线程、找一个线程、回收一个线程顺着链表走就行。内核侧同样是组织好的——所有 task_struct 挂在内核自己的调度数据结构里等着调度器挑。用户态一条链内核态一条链两边的组织各管各的靠管理块里的 tid 字段把同一个线程在两边对上号。先描述、再组织进程管理那套方法论在线程库身上原样重演了一遍。三、pthread_create 背后创建线程的完整工作流铺垫到位看主戏。用户线程和 LWP 是如何关联的先看一个前提每个线程都有自己独立的栈空间——主线程用地址空间里的栈区新线程用库 mmap 出来的线程栈这点篇38 已经拆过不重讲。带着这个前提看一个线程是怎么被创建的。代码区里就一行pthread_create(id,...,routine,arg);这一行背后是两个步骤。1. 库里先建管理块起始地址写进 id调用 create 时pthread 库会先在库中创建一个 struct pthread 管理块把 routine、arg、栈信息、状态这些字段填好再将管理块的起始地址写入 id 中。这就是篇40 那个结论的诞生现场——pthread_t 为什么是个地址因为 create 压根没给你什么编号它直接把库内部数据结构的地址交了出来。之后你调 pthread_join、pthread_detach库拿着这个地址一步到位找到管理块中间不需要任何映射表。2. clone 系统调用在内核里建 LWPcreate 还要在内核中创建轻量级进程。这一步走的是系统调用clone——既然线程在内核眼里就是 LWP那创建线程就是让内核建一个 task_struct。man 手册里 clone 的 wrapper 原型长这样intclone(int(*fn)(void*),void*child_stack,intflags,void*arg,...);注意前两个参数fn执行方法child_stack栈在哪里。内核要造执行流必须知道两件事——这个执行流从哪个函数开始跑、用什么栈跑。栈由库提前 mmap 好把栈顶地址递过去fn 传的是库自己的启动函数用户的方法存在管理块里由它中转。flags 则告诉内核共享什么CLONE_VM 共享地址空间、CLONE_FILES 共享文件描述符表、CLONE_THREAD 归入同一线程组……共享得越多新 task 越像线程这正是篇36 讲过的「用进程模拟线程」在系统调用层面的落地。四、内核和用户层就这样联动起来clone 返回LWP 造好了接下来轮到 CPU。4.1 新线程怎么跑起来CPU 在进行调度时会自动执行对应的方法——调度器选中这个新 LWP执行流从库的启动函数进入启动函数从管理块里取出 start_routine 和 arg去执行自己定义的方法再把线程私有的栈结构地址交给它。「把栈地址交给它」落到硬件层面就是把栈寄存器指向那块 mmap 出来的空间。此后函数运行形成的临时数据会自动压进线程栈——局部变量、函数调用帧全落在线程自己的私有地盘上谁也踩不着谁。4.2 用户线程和 LWP 的关联闭环到这里整条链路闭环了内核和用户层在一定程度联动起来用户层的库出数据结构管理块和栈内核出执行流LWP/task_struct和调度管理块的 tid 字段记着内核给的 LWP ID库借它跟内核对话pthread_t 指向管理块LWP 在内核调度——一个用户线程就这样和 LWP 一对一关联。你写的routine函数跑在库准备的栈上、被内核当成 LWP 调度、被库的数据结构记录——三方合资才凑出一个「线程」。五、线程的回收join 从管理块里拿返回值创建讲完讲回收。站在 struct_pthread 和线程返回值的角度回收过程值得单独拆一章。5.1 返回值写进管理块线程结束后返回值被写进 struct_pthread的 result 字段。篇39 讲过 return 的值通过 void* 通道传出来、join 用二级指针接住——那个值最终落在哪就落在管理块里。5.2 线程被销毁管理块没销毁关键一句线程被销毁但是 struct_pthread 没有被销毁通过 struct_pthread 进行 join。「线程被销毁」销的是内核侧的执行流——LWP 不复存在调度队列里没有它了。「没被销毁」的是用户侧的数据——管理块还挂在库的链表上result 字段原封不动地存着退出信息。它处没有想拿线程的返回值只能通过这个管理块。两层销毁时机不同恰好对应 2.2 那张表的左右两半内核的跟着调度实体走说没就没用户层的跟着数据走等人来收。5.3 join 的收割动作站在管理块和返回值的角度join 的回收过程三步走先等——通过 futex 阻塞在目标线程的结束事件上再取——目标线程结束后从它的管理块里把 result 读出来交给 join 的第二个参数后释放——返回值拿走了管理块连同 mmap 出来的线程栈才真正被库回收。回收下来的栈也不会马上还给系统。篇38 讲过 glibc 有栈缓存——回收的栈先进缓存池等下一个 pthread_create 来了直接复用省去又一次 mmap。所以「释放」释放的是库的占用不是内存的消失。线程六讲过 detach——分离的线程没人 join库就在它结束时自己走完取值和释放这套动作管理块直接回收。所以分离线程的返回值你拿不到不是库小气是收割流程压根没给你留入口。六、线程的局部存储__thread 让全局变量每线程一份工作流讲完补一个库挂在线程身上的私有能力。篇38 拆 errno 时点过一句——错误码能线程私有靠的就是线程局部存储TLSThread-Local Storage。6.1 变量前加 __thread一个变量各线程一份用法就一个动作全局或静态变量前面加 __thread__threadintg_val10;// 加 __thread全局变量按线程私有一份变量前的__thread 起到引导编译器的作用。没有它g_val 全进程一份定义在已初始化数据段共享加上它编译器不再按全局共享的方式安置这个变量而是给每个线程准备一份私有副本放进各自线程的局部存储空间——大家的变量名相同底层虚拟地址不同。demointa10;std::stringAddr(intnum){chara[64];snprintf(a,sizeof(a),%p,num);returna;}void*routine1(void*mes){while(true){std::cout我是线程1a的值为:a a的地址Addr(a)std::endl;sleep(1);}returnnullptr;}void*routine2(void*mes){while(true){std::cout我是线程2a的值为:a a的地址Addr(a)std::endl;sleep(1);}returnnullptr;}intmain(){pthread_t tid1,tid2;pthread_create(tid1,nullptr,routine1,nullptr);pthread_create(tid2,nullptr,routine2,nullptr);pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);return0;}加上__thread之后容易联想到父子进程的写时拷贝像但机制不一样写时拷贝先共享一份、写到才复制局部存储是线程的存储空间里本来就备好独立一份没有「触发拷贝」这个动作。共性只有一个——各有一份互不干扰。6.2 它解决什么问题有一个全局变量但又不想让这个全局变量被其他线程看到。全局的写法省事共享的副作用全不想要——局部存储就是为这种情况准备的访问零成本副作用隔离在各自线程里。errno 是现成的招牌出错码放全局一个线程刚查完就被另一个覆盖错误码全乱套。篇38 那句「每线程一份」到这里才算把用法补全。6.3 两个边界外加名字存哪的甄别1. 类型受限只能存内置类型和部分指针局部存储装不下复杂对象。GCC 手册要求 C 的初始化必须是常量表达式——带自定义构造、析构的类对象放不进来。日常用法就剩整型、指针这类「内置类型和部分指针」说的就是这条边界。2. setname_np 设置的名字每线程一份但不放局部存储pthread_setname_np / pthread_getname_np 给线程起名字名字限 16 字节含结尾 ‘\0’超了报 ERANGE。名字每线程一份各写各的不存在互相覆盖的并发问题。但 man 手册注记写明 setname_np 内部写的是 /proc/self/task/tid/comm——名字落在内核 task_struct 的 comm 字段不在用户层的局部存储里。「无并发问题」取的是每线程一份的私有语义位置上面试别说错。七、全篇总结Linux 没有真正的线程内核用 LWP 模拟OS 不提供线程接口用户层封装出原生线程库 NPTL1:1 映射。库是动态库ELF 格式映射进进程地址空间进程代码才能访问库内部数据。先描述再组织——描述是 struct pthread 管理块记 tid、result、start_routine、栈信息组织是挂在库的全局链表上优先级、时间片、上下文归内核走 LWP → PCB。创建两步走——库里建管理块、起始地址写进 idclone 在内核建 LWP前两个参数是执行方法和栈位置。执行靠联动——CPU 调度 LWP从库的启动函数进入转去执行用户方法临时数据自动入线程栈。回收看两层——线程销毁的是 LWP管理块留着join 从 result 取值后才释放管理块和栈。线程局部存储——全局变量加 __thread 每线程一份变量名相同地址不同只能放内置类型和部分指针errno 是招牌应用。八、源码框架–线程创建到销毁全链路骨架/* * LWP-Life 框架 —— 线程从创建到销毁的全链路骨架glibc nptl * 闭环申请空间 - 克隆 - 运行 - 退出 - 等待/回收 - 归还缓存 - 再被申请 *//* 一、线程描述符 TCBstruct pthread 关键成员 */structpthread{pid_t tid;// 线程 ID兼作该描述符(栈)是否在被使用的标志// 0 在用0 内核已把它清零(线程真走了)0 已被自行 detach/joinpid_t pid;// 进程 ID内核里叫线程组 ID(tgid)booluser_stack;// 栈是否由用户提供否则由库 mmap 而来才可进缓存复用void*result;// 线程函数的返回值 void* 放在这里// pthread_join 拿退出信息读的就是它// 也解释了执行流可以退出TCB 可以暂时保留structsched_paramschedparam;// 调度参数intschedpolicy;// 调度策略void*(*start_routine)(void*);// 用户指定的线程函数void*arg;// 用户指定的参数void*stackblock;// 线程整块空间的起始地址size_t stackblock_size;// 整块空间的大小};/* 二、pthread_create用户层入口创建链路起点 */int__pthread_create_2_1(pthread_t*newthread,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg){// 线程属性没传就退回默认属性conststructpthread_attr*iattr(structpthread_attr*)attr;if(iattrNULL)iattrdefault_attr;structpthread*pdNULL;// 待填充的线程描述符// 申请一大块线程私有空间struct pthread 落在空间开头见第三阶段interrALLOCATE_STACK(iattr,pd);// 把要执行的函数和参数登记进 TCB稍后由新线程自己取出来跑pd-start_routinestart_routine;pd-argarg;// 把 TCB 地址作为线程 ID 交给上层 —— pthread_t 本质是一个虚拟地址*newthread(pthread_t)pd;boolis_detachedIS_DETACHED(pd);// 是否分离决定退出时谁释放资源errcreate_thread(pd,iattr,STACK_VARIABLES_ARGS);// 真正创建第四阶段if(err!0)__deallocate_stack(pd);// 创建失败空间直接还回去returnerr;}/* 三、allocate_stack安家 —— 申请空间并确定 TCB 位置 */staticintallocate_stack(conststructpthread_attr*attr,structpthread**pdp,ALLOCATE_STACK_PARMS){// 栈大小用户设置了就用用户的否则用默认值sizeattr-stacksize?:__default_stacksize;// 先尝试从栈缓存申请缓存里挂的是已退出线程的旧栈// 判定能否复用的条件是 FREE_P(curr) (curr-tid 0)// —— 内核只有在线程真正退出后才会把 tid 清零这就是闭环的连接点pdget_cached_stack(size,mem);if(pdNULL)// 缓存没有合适的 - mmap 申请私有匿名内存作用类似 malloc;// 在申请到的空间中确定 TCB 地址放在这块空间的末尾pd(structpthread*)((char*)memsize-coloring)-1;// 记录整块空间的地址和大小回收时按它归还/复用pd-stackblockmem;pd-stackblock_sizesize;// 线程即轻量级进程记录所属进程的 pidpd-pidTHREAD_GETMEM(THREAD_SELF,pid);return0;}/* 四、create_thread向内核下创建订单成功后放行新线程 */staticintcreate_thread(structpthread*pd,conststructpthread_attr*attr,STACK_VARIABLES_DECL){// 最后一个参数控制新线程是否先挂起等父线程登记完信息再放行intresdo_clone(pd,attr,clone_flags,start_thread,STACK_VARIABLES_ARGS,stopped);if(res0){// 登记 TD_CREATE 调试事件挂进事件链表并通知调试器pd-eventbuf.eventnumTD_CREATE;pd-eventbuf.eventdatapd;// ... CAS 挂链表 __nptl_create_event() ...lll_unlock(pd-lock);// 解锁放行新线程这才开始真正跑}returnres;}/* 4.1 do_clone 内部打包参数真正陷入内核 */// ARCH_CLONE 把入口函数 fct、参数 pd、TLS、tid 地址打包交给内核返回 -1 即失败if(ARCH_CLONE(fct,STACK_VARIABLES_ARGS,clone_flags,pd,pd-tid,TLS_VALUE,pd-tid)-1)/* 失败释放栈返回错误码 */;movl $SYS_ify(clone),%eax/* 系统调用号进 eax */syscall/* 陷入内核(x86_32 是 int 80)要求内核创建轻量级进程 *//* 内核从此担起一个约定clone 带了 CLONE_CHILD_CLEARTID 线程死时会把 *(pd-tid) 清零并 futex 唤醒等待者 —— 后面 join 全靠它 *//* 五、start_thread新线程的起点也是退出的起点 */staticintstart_thread(void*arg){structpthread*pdarg;// 若被要求挂起启动抢到 pd-lock 才算拿到 TCB 所有权随即释放// 这一步和上面 create_thread 的解锁正好对上保证父子交接完成if(pd-stopped_start){lll_lock(pd-lock,LLL_PRIVATE);lll_unlock(pd-lock,LLL_PRIVATE);}// setjmp 埋点pthread_exit / 取消时的清理回滚就落在 unwind_buf 上intnot_first_callsetjmp(unwind_buf.cancel_jmp_buf);if(__glibc_likely(!not_first_call)){THREAD_SETMEM(pd,cleanup_jmp_buf,unwind_buf);void*retpd-start_routine(pd-arg);// 跑用户的线程函数THREAD_SETMEM(pd,result,ret);// 返回值落进 TCB}call_function_static_weak(__call_tls_dtors);// 析构 thread_local 变量__nptl_deallocate_tsd();// 清理线程局部数据__libc_thread_freeres();// 清理 libc 的线程状态atomic_fetch_or_relaxed(pd-cancelhandling,EXITING_BITMASK);// 标记正在退出// 已经没人能 join分离线程自己把资源清干净if(IS_DETACHED(pd))__free_tcb(pd);// 不能 _exit会杀整个进程只退出当前执行流// 内核按 CLONE_CHILD_CLEARTID 的约定把 pd-tid 清零并 futex 唤醒 join 者// tid 归零后这块栈才可能被 get_cached_stack 挑走复用while(1)INTERNAL_SYSCALL_CALL(exit,0);}/* 六、pthread_join等待线程死亡并取回结果非分离线程由这里收尾 */int__pthread_join(pthread_t threadid,void**thread_return){structpthread*pd(structpthread*)threadid;// pthread_t 就是 TCB 地址if(INVALID_NOT_TERMINATED_TD_P(pd))returnESRCH;// 句柄已失效if(IS_DETACHED(pd))returnEINVAL;// 分离线程不可 joinif(pdTHREAD_SELF)returnEDEADLK;// 自己 join 自己死锁atomic_exchange(pd-joinid,self);// 抢占 join 权只能有一个等待者// 在 pd-tid 上 futex 阻塞非 0 就睡内核清零时自动唤醒lll_wait_tid(pd-tid);pd-tid-1;// 标记线程已终止且已被 joinif(thread_return!NULL)*thread_returnpd-result;// 取回返回值__free_tcb(pd);// 未分离线程的资源由 join 者释放return0;}/* 七、__free_tcb - __deallocate_stack资源归还闭环回到第三阶段 */void__free_tcb(structpthread*pd){// 首次置上 TERMINATED 位的那个线程负责回收分离线程是自己join 情形是调用者if(__builtin_expect(atomic_bit_test_set(pd-cancelhandling,TERMINATED_BIT)0,1))__deallocate_stack(pd);}void__deallocate_stack(structpthread*pd){lll_lock(stack_cache_lock,LLL_PRIVATE);stack_list_del(pd-list);// 先移出在用链表if(__glibc_likely(!pd-user_stack))queue_stack(pd);// 库申请的栈进缓存留给下一个线程复用else_dl_deallocate_tls(TLS_TPADJ(pd),false);// 用户自己的栈不能动只释放 TLSlll_unlock(stack_cache_lock,LLL_PRIVATE);}staticinlinevoidqueue_stack(structpthread*stack){// 无条件挂进缓存这块内存的 tid 还没被内核清零前不会被选中复用stack_list_add(stack-list,stack_cache);stack_cache_actsizestack-stackblock_size;if(__glibc_unlikely(stack_cache_actsizestack_cache_maxsize))__free_stacks(stack_cache_maxsize);// 缓存超限挑旧的真正 munmap 归还}九、文末面试题9.1 推导题1. 为什么内核不提供线程接口线程还能被使用答推导内核虽然不认识「线程」但提供了 clone 这个可控共享的系统调用。用户层库把 clone 封装起来配上自己的数据结构管理块、线程栈在用户态拼出了线程的完整语义。OS 不提供的是「线程」这个名字和接口不提供不了的是共享执行流的能力。2. clone 的 fn 参数和用户传给 pthread_create 的 start_routine 是同一个函数吗答推导不是。clone 的 fn 传的是库内部的启动函数用户的方法 start_routine 和参数 arg 先存进管理块。新 LWP 被调度时先执行库的启动函数再由它从管理块取出 start_routine 转调。多做这一层中转库才能在执行前后插手——填 tid、收返回值、标记状态全靠这个中转站。3. 线程结束后内核的 task_struct 和库里的管理块谁先消失答推导task_struct 先消失。线程退出即执行流终结内核侧的 LWP 不复存在管理块是库的数据结构要等 join或 detach 的自动回收取走 result 后才释放。所以「线程死了但返回值还在」拿返回值靠的是管理块。4. 为什么 create 直接把管理块地址当线程 ID 返回给用户答推导后续所有库函数join、detach、cancel都需要找到目标线程的管理块。直接把地址当 ID库函数拿到参数就能访问数据省去一次 ID 到内存块的映射查找地址在进程地址空间内天然唯一也不会和别的线程混淆。代价是它只在进程内有意义内核不认这个值。5. 主线程有没有管理块和线程栈答推导主线程同样被库管理pthread_self 在 main 里一样能拿到 ID。区别在栈——主线程用的是进程地址空间里的栈区随进程创建就有新线程的栈才是库 mmap 出来的。所以主线程和子线程在库眼里都是线程差异只在栈的来源。6. 库把线程概念劈成用户层一半、内核一半为什么不干脆全放一边答推导全放内核就得给内核加线程接口改动内核且违背 Linux「一切皆进程」的简化设计全放用户层调度属性优先级、时间片、上下文没人管——调度是内核的职权用户态碰不到 CPU 分配。两半的划分不是设计者的偏好是职权边界数据归看得见它的库调度归管得了 CPU 的内核。9.2 真题1. 在 Linux 中有一个线程被创建出来会发生什么【真题·转述自掘金面经复盘帖《面试复盘谈谈 Linux 在创建线程时做了什么工作》】答推导 · 已对照面经转述按本篇工作流作答——库先在用户态建管理块、mmap 线程栈把 pthread_t管理块地址交给调用者再走 clone 系统调用内核创建 task_structLWP按 flags 共享地址空间和文件表得到自己的 TID新执行流进调度队列被调度后从库的启动函数进入转去执行用户方法临时数据压线程栈。面经里面试官还期待你能点出 CLONE_VM 这类 flags 的作用本篇第三章正好补齐。2. pthread_create 是系统调用吗它和 clone 是什么关系【真题·转述自 CSDN 技术博客《Linux 线程机制》面试高频总结iies.in 面试题汇编同题互证】答推导 · 已对照面经转述不是。pthread_create 是用户态库函数clone 才是系统调用。前者封装后者建管理块、备栈然后带上一串 CLONE_ 标志调 clone 造出 LWP。内核从头到尾不知道「线程」的存在它只是照要求建了一个共享资源的 task_struct。结语到这里线程系列从概念、控制、退出一路拆到了库和内核的交界处。一个 pthread_create 背后站着两个世界看懂这条工作流前面几篇的所有结论pthread_t 是地址、返回值通道、栈私有就全部串成了一张网。哪一步最出乎你的意料评论区聊聊。如果这篇对你有帮助点个赞再走关注流浪Linux 系统篇持续更新。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →