Linux系統(tǒng)的線程入門:基本概念、虛擬內(nèi)存、Linux內(nèi)核線程、線程應(yīng)用
大家好,本篇是Linux系統(tǒng)編程系列的線程入門,我會結(jié)合底層原理,把線程是什么、為什么需要虛擬內(nèi)存、Linux 線程本質(zhì)、優(yōu)缺點與用途一次性講透,幫你從內(nèi)核視角真正理解線程。
一. 什么是線程?一句話抓住本質(zhì)
線程 = 進程內(nèi)部的一條執(zhí)行流 / 控制序列
- 一個進程至少有 1 個線程(主線程)
- 線程在進程虛擬地址空間內(nèi)運行,共享進程大部分資源
- Linux 內(nèi)核沒有專門的 “線程結(jié)構(gòu)體”,線程本質(zhì)是輕量級進程 LWP,用
task_struct描述,只是比普通進程更 “輕量化”
一句話區(qū)分進程與線程:進程是資源分配的基本單位;線程是 CPU 調(diào)度的基本單位



?? 不過:
- 僅僅有上面的理解,是不夠的
- 要真正理解線程,就必須搞清楚,內(nèi)核是如何進行資源劃分的,尤其是代碼
二. 必須先懂:虛擬地址空間與分頁機制
線程之所以能 “共享、輕量化”,完全依賴虛擬地址空間。這部分是理解線程的地基。
2.1 沒有虛擬內(nèi)存會怎樣?
早期操作系統(tǒng)沒有虛擬內(nèi)存:
- 程序直接占用連續(xù)物理內(nèi)存
- 程序大小不一,退出后留下大量內(nèi)存碎片
- 多程序容易地址沖突、越界、崩潰

我們希望:
- 用戶視角:地址連續(xù)、用起來方便
- 內(nèi)核視角:物理內(nèi)存離散、無碎片、可管理

于是:虛擬地址空間 + 分頁 + 頁表 誕生。
2.2 分頁基本概念
- 物理內(nèi)存按固定大小分割:頁框(Page Frame)
- 虛擬地址按同樣大小分割:頁(Page)
- 常見頁大?。?strong>32 位系統(tǒng) 4KB,64 位系統(tǒng) 8KB
- 作用:把連續(xù)的虛擬地址,映射到不連續(xù)的物理內(nèi)存頁,解決內(nèi)存碎片問題。
機制:
- CPU不直接訪問物理內(nèi)存,而是通過虛擬地址空間間接訪問。
- 操作系統(tǒng)為每個執(zhí)行中的進程分配邏輯地址空間。32 位機:范圍 0 ~ 4G-1。
- 通過頁表建立虛擬地址 ↔ 物理地址的映射。
2.3 物理內(nèi)存管理(重點看圖)
以 4GB 物理內(nèi)存、4KB 頁框為例:總頁數(shù) = 4GB / 4KB = 1048576 個頁框。
內(nèi)核用 struct page 表示系統(tǒng)中的每個物理頁。為節(jié)省內(nèi)存,struct page 大量使用 union(聯(lián)合體)。
struct page {
/* 原子標(biāo)志,有些情況下會異步更新 */
unsigned long flags;
union {
struct {
/* 換出頁列表,例如由 zone->lru_lock 保護的 active_list */
struct list_head lru;
/* 如果最低位為 0,則指向 inode 的 address_space,或為 NULL;
* 如果頁映射為匿名內(nèi)存,最低位置位,且該指針指向 anon_vma 對象
*/
struct address_space *mapping;
/* 在映射內(nèi)的偏移量 */
pgoff_t index;
/*
* 由映射私有,不透明數(shù)據(jù)
* - 如果設(shè)置了 PagePrivate,通常用于 buffer_heads
* - 如果設(shè)置了 PageSwapCache,則用于 swp_entry_t
* - 如果設(shè)置了 PG_buddy,則用于表示伙伴系統(tǒng)中的階
*/
unsigned long private;
};
struct { /* slab, slob and slub */
union {
struct list_head slab_list; /* 復(fù)用 lru */
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* 剩余頁數(shù) */
int pobjects; /* 近似對象計數(shù) */
#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: 計數(shù)器 */
struct { /* SLUB 專用 */
unsigned inuse : 16; /* 已使用的對象數(shù) */
unsigned objects : 15; /* 總對象數(shù) */
unsigned frozen : 1; /* 是否凍結(jié) */
};
};
};
/* 其他可能的聯(lián)合成員(如用于文件系統(tǒng)等) */
...
};
union {
/* 內(nèi)存管理子系統(tǒng)中映射的頁表項計數(shù),用于表示頁是否已經(jīng)映射,
* 還用于限制逆向映射搜索 */
atomic_t _mapcount;
unsigned int page_type;
unsigned int active; /* SLAB */
int units; /* SLOB */
};
/* 其余字段(如引用計數(shù)、私有用例等) */
...
#if defined(WANT_PAGE_VIRTUAL)
/* 內(nèi)核虛擬地址(如果沒有映射則為 NULL,即高端內(nèi)存) */
void *virtual;
#endif /* WANT_PAGE_VIRTUAL */
/* 后續(xù)可能還有其他成員,取決于內(nèi)核配置 */
...
};
關(guān)鍵成員:
flags
- 存放頁的狀態(tài):是否鎖定、是否臟頁、是否在緩存、是否空閑等。每一位表示一種狀態(tài),最多可表示 32 種狀態(tài)。重要標(biāo)志:
- PG_locked:頁是否被鎖定
- PG_uptodate:頁數(shù)據(jù)是否有效
- PG_dirty:頁是否被修改(臟頁)
_mapcount
- 表示有多少頁表項指向該頁,即被引用計數(shù)。當(dāng)值為 -1 時,表示該頁空閑可分配。
virtual
- 頁的內(nèi)核虛擬地址。高端內(nèi)存不永久映射,此時 virtual 為 NULL,需要動態(tài)映射。
內(nèi)存開銷計算:struct page 約占 40 字節(jié)。4GB 內(nèi)存共 1048576 個 page:總消耗 = 1048576 * 40B ≈ 40MB。相對于 4GB 內(nèi)存可以忽略。
頁大小的權(quán)衡:
- 頁太大:頁內(nèi)碎片大
- 頁太?。?strong>頁表過長、切換開銷大
- Linux/Windows 默認(rèn):4KB。


2.4 頁表(單級頁表)
頁表中每一個表項,指向一個物理頁的起始地址。
32 位系統(tǒng) 4GB 虛擬空間:總表項 = 4GB / 4KB = 1048576 項,每項 4 字節(jié) → 頁表總大小 4MB。
問題:單級頁表需要連續(xù) 1024 個物理頁框存儲。我們用分頁解決物理連續(xù),結(jié)果頁表自己又要連續(xù)內(nèi)存。
同時,根據(jù)局部性原理,進程只使用少量頁,不需要全量頁表。

2.5 多級頁表(二級頁表)&& 地址轉(zhuǎn)換
解決思路:把頁表再分頁。
結(jié)構(gòu):
- 頁目錄表(PGD):1024 項
- 頁表(PT):每個 1024 項
- 總覆蓋:1024 * 1024 = 1048576 項,依然覆蓋 4GB。
虛擬地址劃分(32 位、4KB 頁):10 位頁目錄 | 10 位頁表 | 12 位頁內(nèi)偏移
- 頁目錄與頁表可以離散存儲
- 進程只加載用到的頁表,大幅節(jié)省內(nèi)存
- 支持大地址空間
- 示例:10MB 程序 → 對齊到 12MB → 需要 3 個頁表 即可。


地址轉(zhuǎn)換流程
- CR3 寄存器存放頁目錄物理地址
- 用虛擬地址高 10 位查頁目錄 → 找到頁表
- 用中間 10 位查頁表 → 找到物理頁框
- 低 12 位偏移 → 最終物理地址
- 整個過程由 MMU(內(nèi)存管理單元) 硬件完成。


2.6 TLB 快表(Translation Lookaside Buffer)
多級頁表雖然省內(nèi)存,但訪問變慢(多次訪存)。
解決方案:MMU 集成TLB 緩存,流程如下:
- CPU 給出虛擬地址
- MMU 先查 TLB
- 命中:直接得到物理地址
- 不命中:查頁表,并把映射寫入 TLB
TLB 命中率極高,極大加速地址翻譯。

2.8 缺頁異常 Page Fault
當(dāng)虛擬地址在 TLB 與頁表中都找不到物理頁時,觸發(fā)缺頁異常。這是硬件中斷,可由軟件修復(fù)。
缺頁異常分為三類:
- 硬缺頁(Major Page Fault)物理內(nèi)存中沒有該頁,必須從磁盤讀取到內(nèi)存,再建立映射。
- 軟缺頁(Minor Page Fault)物理內(nèi)存已有該頁,只是當(dāng)前進程未建立映射,直接映射即可。常見于共享內(nèi)存、父子進程、多線程。
- 無效缺頁(Invalid Page Fault) 地址非法:越界、空指針、非法權(quán)限。觸發(fā)
Segment Fault,內(nèi)核直接終止進程。

三. Linux 線程本質(zhì):LWP 輕量級進程
Linux 內(nèi)核沒有專門的線程結(jié)構(gòu)體!
- 進程、線程都用 task_struct 描述
- 線程 = 輕量級進程 LWP
- 線程共享 mm_struct(虛擬地址空間)
- 線程只私有少量執(zhí)行上下文

測試樣例用到的代碼:
// #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;
}

3.1 線程資源共享 && 線程私有資源
同一進程內(nèi)所有線程共享:
- 虛擬地址空間(代碼段、數(shù)據(jù)段、bss、堆、共享區(qū))
- 文件描述符表
- 信號處理方式(SIG_IGN、SIG_DFL、自定義 handler)
- 當(dāng)前工作目錄
- 用戶 ID、組 ID,大部分內(nèi)存資源
每個線程獨立擁有:
- 線程 ID(LWP、pthread_t)
- 一組寄存器(上下文)
- 獨立??臻g
- errno 變量
- 信號屏蔽字
- 調(diào)度優(yōu)先級

3.2 線程棧位置 && Linux中線程的理解圖示
- 主線程棧:在進程默認(rèn)棧區(qū),可動態(tài)增長
- 其他線程棧:在共享區(qū)(mmap 區(qū)域)
- 由 pthread 庫通過 mmap 分配
- 默認(rèn)大?。?strong>8MB,固定不可動態(tài)增長

四. 線程的優(yōu)缺點和異常與用途
4.1 線程的優(yōu)點
創(chuàng)建一個新線程的代價要比創(chuàng)建一個新進程小的多
與進程之前的切換相比,線程之間的切換需要OS做的工作要少很多
- 最主要的區(qū)別是線程的切換虛擬內(nèi)存空間依然是相同的,但是進程切換是不同的。這兩種上下文切換的處理都是通過操作系統(tǒng)內(nèi)核來完成的。內(nèi)核的這種切換過程伴隨的最顯著的性能損耗是將寄存器中的內(nèi)容切換出。
- 另外一個隱藏的損耗是上下文的切換會擾亂處理器的緩存機制。簡單的說,一旦去切換上下文,處理器中所有已經(jīng)緩存的內(nèi)存地址一瞬間都作廢了。還有一個顯著的區(qū)別是當(dāng)你改變虛擬內(nèi)存空間的時候,處理的頁表緩沖
TLB(快表)會被全部刷新,這將導(dǎo)致內(nèi)存的訪問在一段時間相當(dāng)?shù)牡托?。但是在線程的切換中,不會出現(xiàn)這個問題,當(dāng)然還有硬件cache。
線程占用的資源要比進程少
能充分利用多處理器的并行數(shù)量
在等待慢速I/O操作結(jié)束的同時,程序可執(zhí)行其他的計算任務(wù)
計算密集型應(yīng)用,為了能夠在多處理器系統(tǒng)上運行,將計算分解到多個線程中實現(xiàn)
I/O密集型應(yīng)用,為了提高性能,將I/O操作重疊。線程可以同時等待不同d1


進一步圖示解析


關(guān)于cache和TLB

4.2 線程的缺點
性能損失
- ?個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同?個處理器。如果計
算密集型線程的數(shù)量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指
的是增加了額外的同步和調(diào)度開銷,而可用的資源不變。
- ?個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同?個處理器。如果計
健壯性降低
- 編寫多線程需要更全面更深入的考慮,在?個多線程程序里,因時間分配上的細(xì)微偏差或者
因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護的。
- 編寫多線程需要更全面更深入的考慮,在?個多線程程序里,因時間分配上的細(xì)微偏差或者
缺乏訪問控制
- 進程是訪問控制的基本粒度,在?個線程中調(diào)用某些OS函數(shù)會對整個進程造成影響。
編程難度提高
- 編寫與調(diào)試?個多線程程序比單線程程序困難得多

- 代碼示例:我們通過代碼可以看到我們的線程是共享全局變量,函數(shù),甚至是malloc出來的空間
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;
}



4.3 線程的異常與用途(異常上面的缺點中也涉及到了)
異常:
- 單個線程如果出現(xiàn)除零,野指針問題導(dǎo)致線程崩潰,進程也會隨著崩潰
- 線程是進程的執(zhí)行分支,線程出異常,就類似進程出異常,進而觸發(fā)信號機制,終止進程,進程終止,該進程內(nèi)的所有線程也就隨即退出
用途:
• 合理的使用多線程,能提高CPU密集型程序的執(zhí)行效率
• 合理的使用多線程,能提高IO密集型程序的用戶體驗(如生活中我們?邊寫代碼一邊下載開發(fā)工具,就是多線程運行的?種表現(xiàn))
4.4 最簡單總結(jié)
- 線程:CPU 調(diào)度的基本單位
- Linux 線程 = 輕量級進程 LWP
- 線程共享地址空間、頁表、文件描述符、堆、數(shù)據(jù)段
- 線程私有:棧、寄存器、線程 ID、errno、信號屏蔽字
- 線程優(yōu)點:創(chuàng)建快、切換快、占用少、適合多核、適合 I/O
- 線程缺點:健壯性差、一崩全崩、編程復(fù)雜
- 線程用途:加速計算、提高 I/O 體驗、高并發(fā)服務(wù)
結(jié)尾
結(jié)語:線程是 Linux 并發(fā)編程的核心基石,理解其內(nèi)核本質(zhì)、與進程的核心區(qū)別、優(yōu)缺點和適用場景,是后續(xù)掌握線程控制、同步互斥、線程安全的關(guān)鍵。希望這篇博客能幫你吃透線程的基礎(chǔ)概念,后續(xù)我也會繼續(xù)分享線程控制、地址空間布局等進階內(nèi)容,歡迎點贊收藏,一起交流學(xué)習(xí)~
到此這篇關(guān)于Linux系統(tǒng)的線程入門:基本概念、虛擬內(nèi)存、Linux內(nèi)核線程、線程應(yīng)用的文章就介紹到這了,更多相關(guān)從內(nèi)核視角理解Linux線程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
linux下獲取文件的創(chuàng)建時間與實戰(zhàn)教程
這篇文章主要給大家介紹了關(guān)于linux下獲取文件的創(chuàng)建時間與實戰(zhàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用linux系統(tǒng)具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Debian 9系統(tǒng)下修改默認(rèn)網(wǎng)卡為eth0的方法
這篇文章主要給大家介紹了在Debian 9系統(tǒng)下修改默認(rèn)網(wǎng)卡為eth0的方法,文中介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-06-06
Ubuntu 14.04下安裝Eclipse的經(jīng)驗分享
在linux下配置開發(fā)環(huán)境有點麻煩,對于linux剛?cè)腴T的菜鳥來說,命令使用不熟悉,環(huán)境也是朦朦朧朧,今天整理了一下ubnutu14.04下安裝eclipse的步驟,希望對大家有用。廢話不多說,進入主題2014-09-09

