线程
什么是线程呢?线程和之前学习到的进程又有什么关系呢?
从概念的角度来说:
进程 = 内核数据结构 + 代码和数据(执行流)
线程 = 进程内部的一个执行分支(执行流)
在内核与资源的角度:
进程:承担分配系统资源的基本实体;
线程:
CPU
调度的基本单位;
那线程到底是什么呢?在Linux
操作系统中线程具体是什么呢?
首先,对于进程我们知道,有独立的内核数据结构(task_struct)
、进程地址空间(mm_struct
)、页表映射和物理内存中的代码和数据。

在我们之前的认知中,一个进程有一个task_struct
,和进程地址空间mm_struct
;也就是说这个task_struct
是独享这个进程地址空间的。
而线程是进程内部的一个执行分支,那是不是就可以为线程创建task_struct
,并且多个task_struct
共享进程地址空间mm_struct
;这样每一个task_struct
执行自己的那部分代码,每一个task_struct
就是一个执行流。
这里进程访问资源,大部分都是通过进程地址空间来访问的;进程地址空间就犹如一个窗口,进程通过窗口来访问资源。
所以,创建多个进程(
task_struct
),共享同一个窗口(进程地址空间),将资源分配给不同的task_struct
,这样就可以使用进程来模拟线程的。
- 在
Linux
操作系统中,线程就是用进程模拟实现的。 - 对于资源的分配,本质就是对进程地址空间的虚拟地址的划分。(虚拟地址就是资源)
- 对于代码区的划分,函数也是虚拟地址的集合;让线程执行不同的函数。
轻量级进程
说了这么多,在Linux
操作系统中其实是没有线程这一概念的,线程是用进程模拟实现的。
那在Linux
中线程和进程如何区分呢?
进程:内核数据结构 + 代码和数据;而在Linux
是没有线程的概念的;
对于一个进程,其中有一个或者多个内核数据结构;而其中的一个task_struct
又被称为 轻量级进程
所以说,在
Linux
内核中,task_struct
结构体对象又被称为轻量级进程;而进程是内核数据结构对象 + 代码和数据。在一个进程中,可能存在多个
task_struct
结构体对象;之前所了解的只是单执行流的进程。

所以,在Linux
操作系统下,并没有线程这一概念,只有轻量级进程(task_struct
);
Linux
系统的线程是使用进程来模拟实现的:
应该轻量级进程
task_struct
就相当于进程中的一个线程;线程是进程的一个执行流,而一个进程可能存在多个
task_struct
,分别执行不同的代码,也就是多个执行流。
分页式存储
在深入理解线程之前,先来简单了解一下分页式存储:
1. 虚拟地址和页表
如果没有虚拟地址和页表,为了能够保证在物理内存上找到对应的代码和数据,每一个程序在物理内存上所对应的空间就必须是连续的;
而代码和数据的长度是不一致的,这样去映射,物理内存就势必会以很多碎片的形式存在,这样内存利用率非常低。

而想要操作系统提供给用户的空间是连续的,而对应的物理内存尽量不要连续;就有了虚拟地址和页表分页:

将物理内存按照固定的长度的页框 进行分割,也称为物理页;每一个页框包含一个物理页,一个页的大小等于页框的大小。
大部分32
位体系结构支持4KB
的页,64
位体系结构一般会支持8KB
的页。
- 页框是一个存储区域
- 页是一个数据块,可以存放在任何页框或磁盘中。
有了虚拟地址和页表分页机制,
CPU
就能够通过虚拟地址空间来间接访问物理地址,而不是直接访问物理地址。虚拟地址空间:就是操作系统在位每一个正在执行的进程分配的一个物理地址,在
32
位下,范围是[0,4]GB
。操作系统将虚拟地址空间和物理地址之间建立映射关系,就是页表 ,页表中记录了每一对页和页框的映射关系;
CPU
能够根据虚拟地址,通过页表映射访问物理内存地址。
简单来说就是:
将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干个页框,通过页表将连续的虚拟内存映射到若干个不连续的物理内存页
2. 物理内存
将物理内存空间分为若干个页框;比如4GB
物理内存,每个页框的大小4KB
,那就存在``1048576`个页框;这么多物理页,操作系统肯定要将这些物理页管理起来。
在Linux
内核中,struct page
结构表示每个系统中的物理页,在struct page
中,存在非常多的联合体union
(节省空间)
c
struct page
{
/* 原⼦标志,有些情况下会异步更新 */
unsigned long flags;
union
{
struct
{
/* 换出⻚列表,例如由zone->lru_lock保护的active_list */
struct list_head lru;
/* 如果最低为为0,则指向inode * address_space,或为NULL
* 如果⻚映射为匿名内存,最低为置位 * ⽽且该指针指向anon_vma对象 */
struct address_space *mapping;
/* 在映射内的偏移量 */
pgoff_t index;
/* * 由映射私有,不透明数据 * 如果设置了PagePrivate,通常⽤于buffer_heads
* 如果设置了PageSwapCache,则⽤于swp_entry_t * 如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶 */
unsigned long private;
};
struct
{ /* slab, slob and slub */
union
{
struct list_head slab_list; /* uses lru */
struct
{ /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* not slob */
/* Double-word boundary */
void *freelist; /* first free object */
union
{
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct
{ /* SLUB */
unsigned inuse : 16; /* ⽤于SLUB分配器:对象的数⽬ */
unsigned objects : 15;
unsigned frozen : 1;
};
};
};
...
};
union
{
/* 内存管理⼦系统中映射的⻚表项计数,⽤于表⽰⻚是否已经映射,还⽤于限制逆向映射搜索*/
atomic_t _mapcount;
unsigned int page_type;
unsigned int active; /* SLAB */
int units; /* SLOB */
};
...
#if defined(WANT_PAGE_VIRTUAL)
/* 内核虚拟地址(如果没有映射则为NULL,即⾼端内存) */
void *virtual;
#endif /* WANT_PAGE_VIRTUAL */
...
}
其中存在
flags
、_mapcount
、virtual
等标志位
falgs
:存放页的状态,包括页是否是脏的,是否被锁定在内存中。falgs
的每一个比特位单独表示一种状态,至少可以同时表示32
种的状态。其中PG_locked
表示指定页是否被锁定、PG_uptodate
表示页的数据结已经存块设备读取且没有错误。_mapcount
:表示页表中有多少项指向该页,也就是该页被引用了多少次;当计数值为-1
时,表示内核并没有引用这一页,就在新分配中使用它。virtual
:页的虚拟地址,一般情况下它就是页中虚拟内存中的地址。(一些内存并不永久映射到内核地址空间中,此时该值为NULL
,在需要的时候动态映射这些页。
3. 页表
在上面描述中,通过页表将连续的虚拟地址映射到了不连续的物理地址中;那页表 是什么呢?
页表的每一个表项都指向一个物理页,在32
位系统中,虚拟地址的最大空间是4GB
,每一个用户程序都存在虚拟地址空间;让4GB
空间全部可用,在页表中就要表示出来这4GB
空间,也就需要4GB/4KB
,也就是1048576
个表项。
如果页表中直接存储物理地址,在32
位系统下,地址的大小是4
KB,这样页表就要占用1048576*4
也就是4MB
的空间,一个页表就要占用1024
个物理页;在内核中存在非常多的进程,每一个进程都有自己的页表,那是不是就占用非常多的内存资源了。
要解决大容量页表,就要把页表看做普通文件,对它进程离散分离,对页表再分页,形成多级页表

简单了解了页表,那虚拟地址又是什么呢?
CPU
是如何通过虚拟地址获取物理地址的呢?以
32
位系统为例,地址大小为4
字节,也就是32
个bit
位;这
32
bit位分为三部分:前10
位,表示页目录表中页表对应的下标、之后的10
位,表示页表中物理页对应的下标;后12
位表示页内偏移

- 页表的物理地址被页目录表项指向;
- 页目录的物理地址被
CR3
寄存器指向;CR3
寄存器保存了当前正在执行任务的页目录地址。所以说,操作系统在加载用户程序时,不仅需要为程序内容分配物理地址,还要为页目录和页表分配物理内存。

4. 缺页中断
缺页中断简单来说:
缺页中断是当程序试图访问一个当前不在物理内存(RAM)中,而是存放在硬盘(如交换空间或页面文件)中的内存页面时,由硬件(通常是内存管理单元 MMU)触发的一种特殊中断(或异常)。
为什么需要它?
缺页中断是现代操作系统实现虚拟内存 管理的关键机制。虚拟内存让程序以为自己拥有连续的、比实际物理内存大得多的地址空间。操作系统负责在物理内存和硬盘之间按需交换数据块(称为"页")。
发生缺页中断的基本流程:
- 程序访问内存: CPU 执行一条指令,需要读取或写入某个内存地址。
- MMU 检查页表: 内存管理单元根据该地址查找页表(操作系统维护的数据结构,记录虚拟页到物理页框的映射关系,以及页的状态)。
- 发现"缺页": MMU 发现该地址对应的页表项表明:
- 无效: 该虚拟地址尚未分配或无效(这会导致更严重的错误,如段错误)。
- 有效但不在内存中: 该虚拟页是合法的(已分配),但它的内容当前不在物理内存中(页表项中的"存在/有效"位被清除)。这就是"缺页"情况。
- 触发中断: MMU 检测到缺页条件,向 CPU 发出一个缺页中断信号。
- 操作系统接管: CPU 暂停当前程序的执行,保存其上下文(寄存器状态等),并切换到内核态,执行操作系统预先设置好的缺页中断处理程序。
- 中断处理程序工作:
- 查找页面位置: 根据触发中断的虚拟地址,在页表中找到对应的项,确定该页当前存放在硬盘上的具体位置(如交换空间)。
- 分配物理页框: 在物理内存中找到一个空闲的页框(物理内存块)。如果没有空闲页框,则需要使用页面置换算法(如 LRU)选择一个"牺牲"页框,将其内容写回硬盘(如果它是脏的/被修改过)。
- 调入页面: 发出 I/O 请求,将需要的页面从硬盘读入到分配好的物理页框中。这是一个相对慢速的操作(磁盘 I/O)。
- 更新页表: 修改页表项,将该虚拟页映射到新的物理页框,并将状态标记为"在内存中"(设置"存在/有效"位)。
- 可能调整: 如果需要置换页面,还需更新被换出页面的页表项(标记为不在内存)。
- 恢复执行: 中断处理程序完成后:
- 恢复被中断程序的上下文。
- 重新执行那条导致缺页中断的指令。
- 这次 MMU 再查页表时,就能找到有效的物理地址映射,指令得以正常执行。
简单来说,缺页中断就是操作系统用来"现场搬救兵"的机制:当程序要用到硬盘上的数据时,硬件喊停,操作系统赶紧去硬盘把数据搬到内存,然后让程序接着运行。这是现代操作系统内存管理高效运作的核心机制之一。
线程与进程
1. 线程的优点
- 创建与切换成本低
- 创建代价:线程共享进程资源(内存、文件描述符等),无需复制完整地址空间。
- 切换代价低 :
- 保留虚拟内存:线程切换不刷新TLB(快表)和硬件Cache,避免内存访问效率下降。
- 寄存器切换为主:仅需保存/恢复线程私有数据(寄存器、栈指针),无需切换页表等内核资源。
- 资源占用少
- 共享进程的代码段、数据段、堆、打开文件等,仅需独立维护线程私有栈、寄存器等少量资源。
- 提升并行能力
- 多线程可绑定到不同CPU核心,充分利用多处理器并行执行(尤其适合计算密集型任务)。
- I/O效率优化
- 线程可并发等待多个I/O操作(如一个线程处理用户输入,另一个线程下载文件),避免阻塞主程序。
2. 线程的缺点
- 性能损失风险
- 计算密集型场景:若线程数 > CPU核心数,频繁切换导致调度开销增大,实际吞吐量反而下降。
- 同步开销:锁竞争、信号量管理等额外操作消耗CPU时间。
- 健壮性降低
- 共享数据风险:一个线程修改共享变量可能导致其他线程逻辑错误(需严格同步)。
- 单点崩溃 :单个线程的野指针/除零错误会触发信号机制(如SIGSEGV),终止整个进程。
- 访问控制缺失
- 进程是资源分配单位,线程无权限制其他线程访问共享资源(如全局变量、文件句柄)。
- 开发复杂度高
- 需处理竞态条件、死锁、优先级反转等问题,调试难度显著增加。
3. 线程异常
线程是进程的一部分,某个线程如果出现除零、野指针等问题导致线程崩溃,进程也会崩溃。
线程是进程的执行分支,线程如果出现异常,就等同于进程出异常,从而触发信号机制,终止进程进程终止,进程中的所有线程也就随即退出。
4. 线程适用场景
场景类型 | 应用示例 | 线程作用 |
---|---|---|
CPU密集型 | 视频编码/科学计算 | 分解任务到多核并行,缩短计算时间。 |
I/O密集型 | Web服务器/文件下载工具 | 重叠I/O等待与计算,提升响应速度。 |
交互式应用 | 图形界面程序 | 后台任务不阻塞用户操作(如边渲染边响应用户输入)。 |
Linux
进程与线程
-
进程是资源分配的基本单位,线程是调度的基本单位
-
线程共享进程数据,但同时也有自己的一部分数据(线程ID 、一组寄存器、栈、调度优先级等)
线程共享进程地址空间
在Linux
操作系统中,多个轻量级进程公用一个进程地址空间,在同一个进程地址空间内,代码和数据都是共享的;
如果定义一个函数,每一个线程都可以调用这一个函数;定义一个变量,每一个线程都可以访问这个变量;
除此之外,像文件描述符表、每种信号处理方式、当前工作目录、用户id和组id等等都是共享的。

在之前学习进程的过程中,都是单线程进程;也就是只具有一个执行流的进程。
到这里本篇文章内容就结束了,感谢支持