程序执行单位

程序:静态代码
进程:对操作系统正在运行的程序的一种抽象。进程是资源分配的最基本的单位,运行一个程序会创建一个或多个进程

线程:一个进程由多个线程组成,每个线程都在进程的上下文中,共享同样代码和全局数据。

  • 多线程比多进程更容易共享数据,线程更高效

    协程:是⼀种⽐线程更加轻量级的存在。正如⼀个进程可以拥有多个线程⼀样,⼀个线程也可以拥有多个协程。

    • 最重要的是,协程不是被操作系统内核所管理,⽽完全是由程序所控制(也就是在用户态中执⾏)。

进程是资源管理的最小单位,而线程是程序执行的最小单位

进程机制详解

进程(Process)

  • 资源分配的基本单位。
  • 每个进程拥有自己独立的地址空间、代码段、数据段、文件描述符等。
  • 进程之间不能直接访问彼此的内存空间(除非共享内存等 IPC 手段)。

PCB(Process Control Block)

每一个进程在内核中都有一个对应的数据结构来描述它的状态,称为 进程控制块(PCB,Process Control Block),在 Linux 中是 task_struct 结构体。

task_struct 中包含以下核心内容:

  • pid: 当前进程的进程 ID。
  • ppid: 父进程的 ID。
  • state: 进程状态(运行中、就绪、阻塞、僵尸等)。
  • mm: 内存描述符,包含代码段、数据段、堆、栈等信息。
  • files: 文件描述符表。
  • sched: 调度信息。
  • signal: 信号处理表。
  • children: 子进程列表。

可以通过 ps, top, htop, /proc/[pid] 等接口查看这些信息。

fork() 创建新进程

1
pid_t pid = fork();

调用 fork() 后,系统会创建一个与当前进程几乎一模一样的子进程,它们拥有独立的地址空间,但初始内容一致。

区分方式:

  • 父进程:fork() 返回子进程的 PID。
  • 子进程:fork() 返回 0。

注意:父子进程地址空间是复制的,但彼此独立(写时复制机制COW)。

当多线程进程调用fork()时,仅会将发起调用的线程复制到子进程中。子进程中该线程的线程ID与父进程中发起fork()调用线程的线程ID相一致。
其他线程均在子进程中消失,也不会为这些线程调用清理函数以及针对线程特有数据的解构函数。

fork的两个典型用法
(1) 一个进程创建一个自身的副本

  • 网络服务器的典型用法。

(2) 一个进程想要执行另一个程序。首先调用fork创建一个自身的副本,然后其中一个子进程调用exec把自身替换成新的程序。

  • shell之类程序的典型用法。

pid 与 ppid

pid(Process ID):进程的唯一标识,由内核分配

ppid(Parent PID):父进程的 pid,在 task_struct 中通过 real_parent 字段引用。

当父进程结束,子进程的 ppid 会被自动改为 1,即 init(或 systemd)接管,防止出现孤儿进程。

exec():用新程序替换当前进程映像

fork() 复制一个进程,而 exec() 则是替换当前进程所运行的程序,保留 PID 等属性

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>  

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ..., char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

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

wait() 与 waitpid()

父进程需要等待子进程执行完毕,以回收其资源(否则子进程可能变成僵尸进程,Z状态)。

1
2
#include <sys/wait.h>
wait(NULL);

或者使用 waitpid(pid, &status, options) 精确等待某个子进程。

僵尸进程出现的原因:

  • 子进程退出后,仍保留其 task_struct 中的一部分(如退出码)供父进程读取。
  • 如果父进程从不调用 wait(),子进程退出后就会一直残留在 Z 状态。

Linux 内核会通过 do_exit() → release_task() 来清理资源。

线程机制详解

线程是 CPU 调度的基本单位,而进程是资源分配的单位。
虽然我们通常把“多线程”理解为一个进程内部并发执行的多个控制流,但在 Linux 的实现中,线程和进程的本质其实非常接近,它们都由统一的内核结构 task_struct 表示。

线程(Thread)

  • CPU 调度的最小单位。
  • 属于进程的一部分,多个线程共享同一个进程的资源(比如内存空间、文件等),但每个线程有自己的栈(stack)和寄存器(register context)。
  • 使用线程可以更轻量地实现并发

在 Linux 中,线程并不是一个特殊的概念。线程就是一种特殊形式的进程,我们称之为 轻量级进程(LWP,Light Weight Process)。

  • 每个线程在内核中都由 task_struct 表示。
  • 所有线程共享相同的进程资源(内存空间、文件描述符、信号处理器等)。
  • 多个线程组成一个“线程组”,由 主线程(Group Leader) 管理。

协程(Coroutine)

  • 用户态实现的“轻量线程”,切换时不需要进入内核态。
  • 非抢占,靠用户调度。
  • Linux 用户空间实现:ucontext、libco、libuv、boost::coroutine、Python asyncio 等。

线程与进程的本质区别

特性 进程(Process) 线程(Thread)
地址空间 不共享(独立) 共享(同一进程)
文件描述符表 独立 通常共享
栈空间 独立 独立
调度 独立调度单元 也是独立调度单元
通信方式 需要 IPC(管道、信号等) 直接通过共享内存

每个线程和进程都对应一个 task_struct,只是线程共享了更多资源。

clone() 线程的创建机制

Linux 中线程和进程的创建都通过一个底层的系统调用 clone()。

函数原型(简化):

1
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

clone() 的关键点:

  • flags 参数决定子进程与父进程共享哪些资源。
  • 创建“线程”的关键是设置合适的 flags,例如:
    1
    2
    3
    4
    5
    CLONE_VM        // 共享内存空间
    CLONE_FS // 共享文件系统信息(cwd 等)
    CLONE_FILES // 共享文件描述符
    CLONE_SIGHAND // 共享信号处理器
    CLONE_THREAD // 加入父线程所在的线程组
    使用这些标志组合,就可以创建一个“线程”而非独立进程。