Windows与Linux进程线程深度解析:从系统调用到死锁防范 概述 操作系统设计这门课程真是让我又爱又恨,从最基础的CreateProcess调用都写不对,到后来能看懂部分内核源码分析死锁踩了无数坑,今天就把Windows和Linux在进程线程管理上的区别,还有那个差点让我debug到通宵的死锁问题详细总结一下
目录
进程线程基础概念
Windows进程线程全流程
Linux进程线程全流程
死锁原理与实战分析
总结与心得体会
进程线程基础概念 进程是什么? 进程就是正在运行的程序的实例 。每个进程都有自己独立的内存空间,就像同时打开多个Chrome标签页,每个标签页其实都是独立的进程
线程又是什么? 线程是进程内的执行单元,多个线程共享进程的资源。比如在一个word文档里,一个线程处理用户输入,另一个线程自动保存,它们共享同一个文档数据,下面是解释的流程图:
graph TB
A[进程] --> B[代码段]
A --> C[数据段]
A --> D[文件句柄]
A --> E[线程1]
A --> F[线程2]
A --> G[线程3]
E --> H[栈空间]
E --> I[寄存器]
F --> J[栈空间]
F --> K[寄存器]
G --> L[栈空间]
G --> M[寄存器]
Windows进程线程的流程 进程创建流程分析 Windows的进程创建其实还挺复杂的,但是我们通过IDA Pro分析ntdll.dll可以看到底层实现:
1 2 3 4 5 6 7 8 9 10 ; Windows 10 ntdll.dll反编译结果 - NtCreateProcess系统调用 .text:000000018009EC10 4C 8B D1 mov r10, rcx ; 参数转移到r10 .text:000000018009EC13 B8 BA 00 00 00 mov eax, 0BAh ; 系统调用号0xBA .text:000000018009EC18 F6 04 25 08 03 FE 7F 01 test byte ptr ds:7FFE0308h, 1 .text:000000018009EC20 75 03 jnz short loc_18009EC25 .text:000000018009EC22 0F 05 syscall ; 快速系统调用 .text:000000018009EC24 C3 retn .text:000000018009EC25 .text:000000018009EC25 CD 2E int 2Eh ; 传统系统调用方式 .text:000000018009EC27 C3 retn
关键分析:
系统调用号 : 0xBA (十进制186) - Windows 10中NtCreateProcess的调用号
调用机制 : 检查7FFE0308标志位决定使用syscall还是int 2Eh
参数传递 : 使用r10寄存器传递参数,符合Windows x64调用约定
Windows进程创建调用栈:
1 2 3 4 5 6 7 8 9 CreateProcessW (kernel32.dll) │ └── CreateProcessInternalW (kernel32.dll) │ └── NtCreateUserProcess (ntdll.dll系统调用) ; 实际创建进程 │ └── PspCreateProcess (内核层executive) │ └── MmCreateProcessAddressSpace (内存管理器)
这个反汇编代码展示了Windows 10系统调用的两种方式:现代的syscall指令和传统的int 2Eh中断,其他版本的windows略有差异,这里附不同Windows版本的系统调用号对比:
Windows版本
NtCreateProcess调用号
调用指令
Windows 7
0xAA
int 2Eh
Windows 8
0xAF
syscall
Windows 10
0xBA
syscall
Windows 11
0xBC
syscall
Windows进程创建完整流程:
graph TD
A[CreateProcessW调用] --> B[参数验证和转换]
B --> C[创建内核对象EPROCESS]
C --> D[初始化地址空间]
D --> E[设置PEB进程环境块]
E --> F[创建初始线程ETHREAD]
F --> G[加载可执行映像]
G --> H[初始化线程上下文]
H --> I[通知Windows子系统]
I --> J[调度执行]
style A fill:#00008b
style J fill:#228b22
实际代码示例 回顾刚开始学的时候写的代码真是惨不忍睹:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 BOOL CreateProcessDemo_bad () { STARTUPINFO si = {0 }; PROCESS_INFORMATION pi; BOOL result = CreateProcess( "notepad.exe" , NULL , NULL , NULL , FALSE, 0 , NULL , NULL , &si, &pi ); if (result) { return TRUE; } return FALSE; }
后来慢慢改进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 BOOL CreateProcessDemo_fixed () { STARTUPINFO si; PROCESS_INFORMATION pi; BOOL bResult; DWORD dwError; ZeroMemory(&si, sizeof (si)); si.cb = sizeof (si); ZeroMemory(&pi, sizeof (pi)); bResult = CreateProcess( NULL , "notepad.exe" , NULL , NULL , FALSE, CREATE_NEW_CONSOLE, NULL , NULL , &si, &pi ); if (!bResult) { dwError = GetLastError(); printf ("CreateProcess failed with error %d\n" , dwError); return FALSE; } WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return TRUE; }
线程创建与同步 线程同步这块更是坑多:
graph LR
A[主线程创建子线程] --> B[子线程开始执行]
B --> C[线程同步等待]
C --> D{同步方式}
D --> E[事件Event]
D --> F[互斥量Mutex]
D --> G[信号量Semaphore]
D --> H[临界区CriticalSection]
E --> I[同步完成]
F --> I
G --> I
H --> I
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 DWORD WINAPI WorkerThread (LPVOID lpParam) { THREAD_DATA* pData = (THREAD_DATA*)lpParam; printf ("Thread %d started\n" , pData->id); WaitForSingleObject(pData->start_event, INFINITE); for (int i = 0 ; i < pData->work_count; i++) { EnterCriticalSection(&pData->cs); pData->shared_counter++; LeaveCriticalSection(&pData->cs); Sleep(100 ); } SetEvent(pData->completion_event); return 0 ; }
Linux进程线程的流程 进程创建:fork()的魔法 Linux的fork机制开始真的很难理解:
graph TD
A[父进程调用fork] --> B[创建子进程副本]
B --> C[写时复制技术]
C --> D[子进程返回0]
C --> E[父进程返回子进程PID]
D --> F[子进程执行]
E --> G[父进程继续执行]
F --> H[exec系列函数]
H --> I[加载新程序]
style A fill:#00ffff
style I fill:#228b22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int forkDemo () { pid_t pid; int x = 10 ; pid = fork(); if (pid < 0 ) { perror("fork failed" ); return 1 ; } if (pid == 0 ) { printf ("Child: x = %d\n" , x); x = 20 ; printf ("Child: modified x = %d\n" , x); exit (0 ); } else { sleep(1 ); printf ("Parent: x = %d (not changed)\n" , x); wait(NULL ); } return 0 ; }
线程创建:pthread的复杂性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 typedef struct { int thread_id; char task_name[32 ]; pthread_mutex_t * shared_lock; } thread_arg_t ; void * thread_function (void * arg) { thread_arg_t * targ = (thread_arg_t *)arg; printf ("Thread %d (%s) starting\n" , targ->thread_id, targ->task_name); pthread_mutex_lock(targ->shared_lock); for (int i = 0 ; i < 3 ; i++) { printf ("Thread %d working... %d/3\n" , targ->thread_id, i + 1 ); sleep(1 ); } pthread_mutex_unlock(targ->shared_lock); printf ("Thread %d completed\n" , targ->thread_id); return NULL ; } int main () { pthread_t threads[3 ]; thread_arg_t args[3 ]; pthread_mutex_t shared_lock; int i; pthread_mutex_init(&shared_lock, NULL ); for (i = 0 ; i < 3 ; i++) { args[i].thread_id = i + 1 ; snprintf (args[i].task_name, 32 , "Task-%d" , i + 1 ); args[i].shared_lock = &shared_lock; if (pthread_create(&threads[i], NULL , thread_function, &args[i]) != 0 ) { perror("pthread_create failed" ); return 1 ; } } for (i = 0 ; i < 3 ; i++) { pthread_join(threads[i], NULL ); } pthread_mutex_destroy(&shared_lock); return 0 ; }
接着从内核源码分析 直接阅读Linux内核源码(以5.x版本为例):
进程创建核心函数 :kernel/fork.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 long _do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, unsigned long tls) { struct task_struct *p ; int trace = 0 ; long nr; if (!clone_flags_valid(clone_flags)) return -EINVAL; p = copy_process(clone_flags, stack_start, stack_size, parent_tidptr, child_tidptr, tls, trace, NUMA_NO_NODE); if (IS_ERR(p)) return PTR_ERR(p); pid = get_task_pid(p, PIDTYPE_PID); wake_up_new_task(p); if (trace) ptrace_event_pid(trace, pid); if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); } return pid; }
Linux系统调用实际分析 clone系统调用的实际实现:
1 2 3 4 5 6 7 8 SYSCALL_DEFINE5(clone, unsigned long , clone_flags, unsigned long , newsp, int __user *, parent_tidptr, int __user *, child_tidptr, unsigned long , tls) { return _do_fork(clone_flags, newsp, 0 , parent_tidptr, child_tidptr, tls); }
系统调用层对比分析 创建机制对比
特性
Windows
Linux
进程创建系统调用
NtCreateProcess (0xBA)
fork() → clone(0x38)
线程创建系统调用
NtCreateThread (0xB9)
clone(0x38) with flags
调用指令
syscall/int 2Eh
syscall/int 0x80
参数传递
RCX,RDX,R8,R9 + 栈
RDI,RSI,RDX,R10,R8,R9
总结一下实际系统调用流程对比 Windows系统调用流程:
graph LR
A[用户模式API] --> B[ntdll.dll存根]
B --> C{检查7FFE0308标志}
C -->|1| D[syscall指令]
C -->|0| E[int 2Eh中断]
D --> F[内核模式切换]
E --> F
F --> G[KiSystemCall64]
G --> H[SSDT查找]
H --> I[执行内核函数]
Linux系统调用流程:
graph LR
A[用户模式API] --> B[libc包装器]
B --> C[syscall指令]
C --> D[内核模式切换]
D --> E[entry_SYSCALL_64]
E --> F[系统调用表查找]
F --> G[执行内核函数]
死锁原理与实战分析 首先解释一下什么是死锁
在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁 ,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁
举个例子,一个程序猿去面试,到最后一关时面试官对程序员:你解释一下什么是死锁就录用他,程序员说:你录用我我就跟你解释什么是死锁,此时程序员想要被录用就要先向面试官解释死锁,可是不录用程序员就不向面试官解释,此时如果面试官水平足够那他是能明白程序员的意思的,我们假设面试官没有反应过来,此时就需要一个外力去终止,而这个外力就是HR直接介入,如图所示:
graph TD
A[面试官] --> B[持有资源: 录用决定权]
C[程序员] --> D[持有资源: 死锁知识]
B --> E[先解释就录用]
D --> F[先录用就解释]
E --> G[等待程序员的专业知识]
F --> H[等待面试官的录用权]
G --> I[死锁发生]
H --> I
I --> J[双方陷入沉思]
J --> K[时间无限循环]
K --> L[唯一解法]
L --> M[HR进行干预]
style A fill:#1e90ff
style C fill:#ff1493
style B fill:#00bfff
style D fill:#ff69b4
style E fill:#fff3e0
style F fill:#fff3e0
style I fill:#cd5c5c
style M fill:7fff00
死锁产生的四个条件
互斥访问 :资源不能共享,一次只能一个线程用
持有并等待 :线程拿着资源A等资源B
不可剥夺 :资源不能被强行抢走
循环等待 :线程A等B的资源,线程B等A的资源
graph TD
A[线程1] -->|持有| B[锁A]
A -->|等待| C[锁B]
D[线程2] -->|持有| C
D -->|等待| B
B -->|被线程1持有| A
C -->|被线程2持有| D
style A fill:#dc143c
style D fill:#dc143c
style B fill:#228b22
style C fill:#228b22
实际死锁案例 课程设计中的文件处理器出现了这样的死锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pthread_mutex_t file_lock = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t log_lock = PTHREAD_MUTEX_INITIALIZER;void process_file () { pthread_mutex_lock(&file_lock); sleep(1 ); pthread_mutex_lock(&log_lock); printf ("File processed\n" ); pthread_mutex_unlock(&log_lock); pthread_mutex_unlock(&file_lock); } void write_log () { pthread_mutex_lock(&log_lock); pthread_mutex_lock(&file_lock); printf ("Checking file status\n" ); pthread_mutex_unlock(&file_lock); printf ("Log written\n" ); pthread_mutex_unlock(&log_lock); }
死锁解决方案 后面我试了好几种方法,最终用锁层次协议解决了:
graph TB
A[死锁问题] --> B[解决方案]
B --> C[预防策略]
B --> D[避免策略]
B --> E[检测恢复]
C --> C1[破坏互斥条件]
C --> C2[破坏持有等待]
C --> C3[破坏不可剥夺]
C --> C4[破坏循环等待]
D --> D1[锁层次协议]
D --> D2[超时机制]
D --> D3[银行家算法]
E --> E1[死锁检测]
E --> E2[资源剥夺]
E --> E3[进程回退]
style D1 fill:#228b22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 typedef enum { LOCK_LEVEL_FILE = 1 , LOCK_LEVEL_LOG = 2 } lock_level_t ; __thread int current_lock_level = 0 ; void safe_lock (pthread_mutex_t * lock, int level, const char * func_name) { if (level <= current_lock_level) { fprintf (stderr , "潜在死锁风险: %s中违反锁层次协议\n" , func_name); fprintf (stderr , "当前级别: %d, 尝试级别: %d\n" , current_lock_level, level); } pthread_mutex_lock(lock); current_lock_level = level; } void process_file_fixed () { safe_lock(&file_lock, LOCK_LEVEL_FILE, __func__); sleep(1 ); safe_lock(&log_lock, LOCK_LEVEL_LOG, __func__); printf ("File processed\n" ); pthread_mutex_unlock(&log_lock); pthread_mutex_unlock(&file_lock); current_lock_level = 0 ; } void write_log_fixed () { safe_lock(&file_lock, LOCK_LEVEL_FILE, __func__); printf ("Checking file status\n" ); safe_lock(&log_lock, LOCK_LEVEL_LOG, __func__); printf ("Log written\n" ); pthread_mutex_unlock(&log_lock); pthread_mutex_unlock(&file_lock); current_lock_level = 0 ; }
其他死锁防范技巧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <time.h> int try_lock_with_timeout (pthread_mutex_t * mutex, int timeout_sec) { struct timespec ts ; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += timeout_sec; return pthread_mutex_timedlock(mutex, &ts); } void critical_operation () { if (pthread_mutex_trylock(&some_lock) == 0 ) { pthread_mutex_unlock(&some_lock); } else { printf ("锁被占用,执行备用逻辑\n" ); } }
总结与心得体会 核心收获
进程线程的本质区别 :进程是资源分配单位,线程是执行调度单位
系统调用的底层机制 :从API调用到内核执行的完整流程
死锁的预防和避免 :锁层次协议、超时机制等实战技巧
踩坑经验总结 Windows方面:
CreateProcess参数复杂,需要仔细处理
句柄管理容易出错,记得及时关闭
线程同步要选择合适的机制
Linux方面:
fork的写时复制机制需要理解透彻
pthread的错误检查不能忽略
信号处理要小心谨慎
共同问题:
死锁防范意识需要加强
调试工具使用要熟练(差不多就行了)
代码规范很重要(根本做不到)
学习建议 给正在学习操作系统的同学几点建议:
多动手实践 :光看书不行,一定要写代码调试
善用调试工具 :GDB、IDA pro、x64dbg等
理解底层原理 :明白为什么比知道怎么做更重要
操作系统的学习过程虽然痛苦,但从对进程线程一知半解,到能解决实际并发问题,这个成长过程还是很值得的
参考资料
版本信息
Windows版本 : Windows 10 21H2
ntdll.dll版本 : 10.0.19041.1237
Linux内核版本 : 5.x
技术说明 : 本文基于实际反编译结果,系统调用号可能因Windows更新而变化。建议在实际环境中验证具体版本的系统调用表。