进程与线程

进程



如何找到当前进程线程的指针


进程的状态

🏃 TASK_RUNNING (运行):正在执行或排队等待 。

😴 TASK_INTERRUPTIBLE (可中断):睡眠等待条件,收到信号可唤醒 。

🪨 TASK_UNINTERRUPTIBLE (不可中断):深度睡眠,对信号完全不响应 。

🛑 TASK_TRACED / TASK_STOPPED:被调试或停止运行


进程是怎么样创建的

Linux 创建进程最经典的 fork() + execve() 两步走战略


execve()指令

int execve(const char *pathname, char *const argv[], char *const envp[]);

第一步:找文件与验明正身。 内核根据 pathname 找到磁盘上的文件,检查你有没有执行权限(x 权限)。然后读取文件的前几个字节(魔数,Magic Number),判断这是个什么类型的文件。如果是 Linux 标准的可执行文件,通常是 ELF 格式(以 \x7fELF 开头);如果是脚本,可能会看到 #!(Shebang)。


第二步:砸掉旧房子(清空内存)。 内核会找到当前进程的内存大管家 mm_struct,把旧程序之前申请的虚拟内存空间(代码段、数据段、堆、栈)全部解除映射并清空。

第三步:按新图纸装修(解析 ELF 并映射)。 内核里的 ELF 加载器(load_elf_binary 函数)登场。它会读取 ELF 文件头里的信息,在进程的虚拟地址空间里重新划出地盘:

把文件里的 .text(代码)映射到内存。

把 .data(已初始化变量)映射到内存。

在最高地址处分配一块全新的空地作为用户栈(Stack)。


第四步:搬入新家具(拷贝参数)。 把你传入的 argv(命令行参数)和 envp(环境变量)小心翼翼地拷贝到刚刚建好的那个全新用户栈的栈底。这样新程序的 main 函数一启动,就能从栈里拿到这些参数。

第五步:修改命运的指针(设置入口点)。 一切准备就绪,内核会修改保存在内核栈上的 CPU 寄存器现场(比如 ARM64 的 elr_el1,x86 的 rip)。把它强行指向 ELF 文件头里写明的**“程序入口地址”**(Entry Point)。


fork()的调用

fork()、vfork() 乃至各种线程库,底层统统调用 clone() 把 clone() 比作一个拥有无数开关的机器。开哪些开关,决定了生出来的是“普通进程”还是我们俗称的“线程 进程与线程 配图 1


进程树

在task_struct 结构体中有一个parent参数就是记录他的父进程,所有的进程都是 PID 为 1 的 init 进程的后代,也是系统启动时最先启动的进程,所以进程也可以表示为一个树,init进程时父节点

这里init进程启动时会读取系统的初始化脚本(initscript)并执行,在实际工程中也经常修改这些开机脚本来实现对应程序的自启动


孤儿进程

异常情况:亲爹(父进程)先死了怎么办? 寻父机制 (find_new_reaper): 优先在当前线程组内找一个线程当“养父” 。 如果组里没亲戚了,就甩锅给全局的 init 进程兜底 。