27人参与 • 2026-04-26 • Linux
大家好,本篇是linux系统编程系列的线程入门,我会结合底层原理,把线程是什么、为什么需要虚拟内存、linux 线程本质、优缺点与用途一次性讲透,帮你从内核视角真正理解线程。
线程 = 进程内部的一条执行流 / 控制序列
task_struct描述,只是比普通进程更 “轻量化”一句话区分进程与线程:进程是资源分配的基本单位;线程是 cpu 调度的基本单位



📌 不过:
- 仅仅有上面的理解,是不够的
- 要真正理解线程,就必须搞清楚,内核是如何进行资源划分的,尤其是代码
线程之所以能 “共享、轻量化”,完全依赖虚拟地址空间。这部分是理解线程的地基。
早期操作系统没有虚拟内存:

我们希望:

于是:虚拟地址空间 + 分页 + 页表 诞生。
机制:
以 4gb 物理内存、4kb 页框为例:总页数 = 4gb / 4kb = 1048576 个页框。
内核用 struct page 表示系统中的每个物理页。为节省内存,struct page 大量使用 union(联合体)。
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; /* 复用 lru */
struct { /* partial pages */
struct page *next;
#ifdef config_64bit
int pages; /* 剩余页数 */
int pobjects; /* 近似对象计数 */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* 不用于 slob */
/* 双字边界对齐 */
void *freelist; /* 第一个空闲对象 */
union {
void *s_mem; /* slab: 第一个对象 */
unsigned long counters; /* slub: 计数器 */
struct { /* slub 专用 */
unsigned inuse : 16; /* 已使用的对象数 */
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
内存开销计算:struct page 约占 40 字节。4gb 内存共 1048576 个 page:总消耗 = 1048576 * 40b ≈ 40mb。相对于 4gb 内存可以忽略。
页大小的权衡:


页表中每一个表项,指向一个物理页的起始地址。
32 位系统 4gb 虚拟空间:总表项 = 4gb / 4kb = 1048576 项,每项 4 字节 → 页表总大小 4mb。
问题:单级页表需要连续 1024 个物理页框存储。我们用分页解决物理连续,结果页表自己又要连续内存。
同时,根据局部性原理,进程只使用少量页,不需要全量页表。

解决思路:把页表再分页。
结构:
虚拟地址划分(32 位、4kb 页):10 位页目录 | 10 位页表 | 12 位页内偏移


地址转换流程


多级页表虽然省内存,但访问变慢(多次访存)。
解决方案:mmu 集成tlb 缓存,流程如下:
tlb 命中率极高,极大加速地址翻译。

当虚拟地址在 tlb 与页表中都找不到物理页时,触发缺页异常。这是硬件中断,可由软件修复。
缺页异常分为三类:
segment fault,内核直接终止进程。
linux 内核没有专门的线程结构体!

测试样例用到的代码:
// #include <iostream>
// #include <thread>
// #include <unistd.h>
// // c++中的线程
// void hello()
// {
// while(true)
// {
// std::cout << "我是新进程..., pid: " << getpid() << std::endl;
// sleep(1);
// }
// }
// int main()
// {
// std::thread t(hello);
// while(true)
// {
// std::cout << "我是主线程..., pid: " << getpid() << std::endl;
// sleep(1);
// }
// t.join();
// return 0;
// }
#include <iostream>
#include <pthread.h>
#include <unistd.h>
// linux中封装的线程 -- 其实linux是只有轻量级进程的概念的
void *hello(void *args)
{
while(true)
{
const char *name = (const char*)args;
std::cout << "我是新线程..., pid: " << getpid() << " name is : "<< name <<std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, hello, (void*)"new-thread");
while(true)
{
std::cout << "我是主线程..., pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}

同一进程内所有线程共享:
每个线程独立拥有:


创建一个新线程的代价要比创建一个新进程小的多
与进程之前的切换相比,线程之间的切换需要os做的工作要少很多
tlb(快表)会被全部刷新,这将导致内存的访问在一段时间相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。线程占用的资源要比进程少
能充分利用多处理器的并行数量
在等待慢速i/o操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能够在多处理器系统上运行,将计算分解到多个线程中实现
i/o密集型应用,为了提高性能,将i/o操作重叠。线程可以同时等待不同d1


进一步图示解析

关于cache和tlb
性能损失
健壮性降低
缺乏访问控制
编程难度提高

int g_val = 100;
int *p = nullptr;
void hello(const std::string &name) {
printf("haha, i am common function!, %s\n", name.c_str());
sleep(5);
}
void *threaddrun1(void *args)
{
p = (int*)malloc(sizeof(int) * 10);
std::string threadname = static_cast<const char*>(args);
while(true)
{
printf("%s is running, g_val: %d, &g_val: %p\n", threadname.c_str(), g_val, &g_val);
sleep(1);
hello(threadname);
}
}
void* threaddrun2(void *args)
{
std::string threadname = static_cast<const char*>(args);
while(true)
{
printf("%s is running, g_val: %d, &g_val: %p\n", threadname.c_str(), g_val, &g_val);
sleep(1);
g_val++;
hello(threadname);
}
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, nullptr, threaddrun1, (void*)"thread-1");
pthread_create(&t2, nullptr, threaddrun2, (void*)"thread-2");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}



异常:
用途:
• 合理的使用多线程,能提高cpu密集型程序的执行效率
• 合理的使用多线程,能提高io密集型程序的用户体验(如生活中我们⼀边写代码一边下载开发工具,就是多线程运行的⼀种表现)
结语:线程是 linux 并发编程的核心基石,理解其内核本质、与进程的核心区别、优缺点和适用场景,是后续掌握线程控制、同步互斥、线程安全的关键。希望这篇博客能帮你吃透线程的基础概念,后续我也会继续分享线程控制、地址空间布局等进阶内容,欢迎点赞收藏,一起交流学习~
到此这篇关于linux系统的线程入门:基本概念、虚拟内存、linux内核线程、线程应用的文章就介绍到这了,更多相关从内核视角理解linux线程内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论