Linux中的多线程
程序执行单位
程序:静态代码
进程:对操作系统正在运行的程序的一种抽象。进程是资源分配的最基本的单位,运行一个程序会创建一个或多个进程
线程:一个进程由多个线程组成,每个线程都在进程的上下文中,共享同样代码和全局数据。
多线程比多进程更容易共享数据,线程更高效
协程:是⼀种⽐线程更加轻量级的存在。正如⼀个进程可以拥有多个线程⼀样,⼀个线程也可以拥有多个协程。
- 最重要的是,协程不是被操作系统内核所管理,⽽完全是由程序所控制(也就是在用户态中执⾏)。
进程是资源管理的最小单位,而线程是程序执行的最小单位
进程机制详解
进程(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
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 |
|
或者使用 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
5CLONE_VM // 共享内存空间
CLONE_FS // 共享文件系统信息(cwd 等)
CLONE_FILES // 共享文件描述符
CLONE_SIGHAND // 共享信号处理器
CLONE_THREAD // 加入父线程所在的线程组