前言:
? ? ? ? 上文我們講到了進程間信號的話題【Linux系統】萬字解析,進程間的信號-CSDN博客
? ? ? ? 本文我們再來認識一下:線程!
Linux線程概念
什么是線程
概念定義:
? ? ? ? 進程=內核數據結構+代碼和數據(執行流)
? ? ? ? 線程=是進程內部的一個執行分支(執行流)
內核與資源角度:
? ? ? ? 進程=分配系統資源的基本實體。
? ? ? ? 線程=CUP調度的基本單位。
初步理解線程:
? ? ? ? 在之前我們講過,進程=PCB(task_struct)+代碼和數據,如上圖所示。
? ? ? ? 而線程是什么呢?
? ? ? ? 線程是進程的一個個分支!一個線程 = 一個PCB+一份自己需要執行的代碼和數據!不同的線程執行進程中不同的代碼,各司其職。
? ? ? ? 一個線程執行一部分代碼,多個線程同時執行,讓進程整體效率提升!
? ? ? ? 而我們之前所講的進程其實是:內部只有一個線程的進程!(單線程)
結論:
? ? ? ? 1.線程也采用PCB結構體來描述的
? ? ? ? 2.對資源的劃分,本質是對虛擬地址的劃分。也就是說,虛擬地址就是資源的代表。
? ? ? ? 3線程對進程代碼的“劃分”,不需要我們人為的去“劃分”!因為進程所要執行的代碼,其本質都是由一個個函數組成的!這本就是天然的“劃分”好了的狀態,所以線程對函數“劃分”即可(獲得函數的入口地址即可)!
? ? ? ? 4.線程其實不會對資源進行劃分,進程內的大部分資源都是共享的,不存在說這個資源是線程a的誰都不可以訪問!對代碼的“劃分”也不是真正的劃分,僅僅是表示對任務的分配。一個線程負責執行一部分代碼,讓進程的代碼同時被多個線程推進!
? ? ? ? 5.Linux的線程就是輕量級的進程(單線程進程)!
? ? ? ? 6.進程強調獨占,部分共享(如進程間的通信)
? ? ? ? ? ?線程強調共享,部分獨占
補充:
? ? ? ? windows下的線程設計,與Linux的并不相同!Linux的線程都是使用PCB結構體描述的,但是windows下的線程是采用新設計的結構體:TCB來描述的。
? ? ? ? 越復雜的代碼可維護性、健壯性越不好,所以Linux在這一方面采用復用的方式,設計的更好!
分頁式存儲管理
進一步理解線程:內核資源的劃分
物理內存管理
? ? ? ? 物理內存最小管理與分配單位:頁框/頁幀,大小為4KB。當然虛擬內存是與物理內存一一對應的,虛擬內存也是以4KB為基本單位進行分配(是分配噢,不是讀寫)。
? ? ? ? 之前我們在文件系統中也講過:磁盤數據的分配讀寫(磁盤是例外),是以4KB為單位進行的。【Linux系統】詳解Ext2,文件系統-CSDN博客
????????????????????????????????虛擬頁面(4KB) ? 物理頁框(4KB) ? 磁盤塊(4KB)
????????當然不是真的劃分為一個個4KB的空間,實際上是一個整體,只是OS在邏輯上進行了劃分。
? ? ? ? OS采用結構體:page,進行描述!
? ? ? ? page描述了頁框的各種信號,其中包含了頁框的狀態:是否被使用,是否被鎖定等等。
? ? ? ? 并采用數組:struct page mem[1048576],進行組織!
? ? ? ? 所以每一個page都會對應一個數組下標!而我們讓數組下標 * 4KB就可以得到page的起始首物理地址了!
? ? ? ? 起始首地址+頁框中的偏移量=真實的物理地址。
????????
? ? ? ? 有了以上的梳理,我可以知道,當線程或進程申請物理內存時:
? ? ? ? 1.查數組,修改page? ? ? ? 2.建立page與內核數據結構的映射關系
頁表
重新認識頁表
? ? ? ? 在此之前,我們認識頁表就如圖所示:一張表保存虛擬地址與物理地址映射關系。
? ? ? ? 思考一個問題:
? ? ? ? ? ? ? ? 如果一張頁表將虛擬地址與物理地址的映射關系全部保存,(以32位機器為例)一個地址是4字節,那么頁表中一排就要保存8字節數據。那么一共有多少地址需要我們保存呢?4GB!這也就意味著頁表的大小將會來到:8字節 * 4GB = 32GB!這是不現實的!所以頁表是絕不可能僅用一張表來保存映射關系的。
頁表真正的保存方式:
? ? ? ? 真正的頁表由兩部分組成:頁目錄、頁表。
?虛擬地址的轉化:
????????首先將一個虛擬地址劃分位3部分:以10位、10位、12位為3組(32位下)??
? ? ? ? 前10位:表示指向頁目錄的地址,其中頁目錄中保存的是頁表的地址。
? ? ? ? 中間10位:表示指向頁表的地址,其中頁表中保存的是頁地址(起始地址)。
? ? ? ? 最后12位:表示頁中的偏移量,前面的地址找到了具體的頁框,最后加上偏移量,就得到了真正的物理地址了!
細節:
? ? ? ? 1.一張頁目錄+n張頁表構成了映射體系,物理頁框是映射目標。最后12位地址+頁框地址=真實的物理地址。
? ? ? ? 2.虛擬地址的轉化其實是有CPU中的硬件:MMU自動完成的
? ? ? ? 3.申請物理內存:查找數組,找到沒有使用的page,修改page,通過page下標得到物理地址,以頁框位最小單位獲得到申請的內存。
? ? ? ? 4.寫時拷貝,缺頁中斷,內存申請等等,背后都可能要重新建立新的頁表與新的映射關系。????????
? ? ? ? 5.為什么要用最后12位,最為頁內偏移量?
? ? ? ? ? ? ? ? 12位:2^12,且一個地址的存儲空間為1字節,剛好為4KB(與頁框大小一致,可以覆蓋整個頁框的偏移)
? ? ? ? ????????最后12位:前20位的數據是一致的,這可以保證查找到數據屬于同一個4KB的頁框。
深刻理解線程
????????1.線程進行資源的劃分:本質是劃分地址空間,得到一定合法范圍的虛擬地址空間,本質就是,對頁表的劃分!
????????2. 線程對資源的共享:本質就是地址空間的共享,本質就是對頁表條目的共享!
????????3.線程是輕量化進程,顧名思義:線程的開銷比進程更低,尤其在線程的切換方面!
切換方面解釋:
? ? ? ? 為了提高轉化地址的效率,MMU引入了TLB(Translation Lookaside Buffer,緩存),其中存儲最近頻繁使用的映射關系!MMU做虛擬地址與物理地址的轉化時,先去TLB中查詢,若沒有,則再去頁表中查詢!
? ? ? ? 對于線程:線程不論如何切換,都是在同一個進程中的!在同一個虛擬地址空間中!
? ? ? ? 對于進程:進程一旦切換,新的進程是對應新的虛擬地址空間的!
? ? ? ? 也就是說,線程切換,虛擬地址空間不會切換,TLB正常使用!但進程切換,虛擬地址空間也切換,TLB中保存的映射關系全部報廢!需要全部將其刷新!
? ? ? ? 所以這也就是為什么線程的切換開銷更小!
Linux線程控制
引入pthread庫
這個一個關于線程的庫
? ? ? ? 首先,prhread庫是Linux系統下C/C++實現的線程庫!
? ? ? ? 其次,Linux系統中其實并沒有真正的線程!都是輕量級進程!Linux 內核中沒有獨立的 “線程” 數據結構,而是通過 “輕量級進程(Lightweight Process, LWP)” 來實現線程功能!
? ? ? ? 但對于用戶來說,用戶需要使用線程的概念以及方法!所以為什么保證用戶的正常使用,C/C++實現了pthread庫,封裝了LWP,來實現“線程”的概念以及方法!
? ? ? ? 所以Linux線程的實現是在用戶層的,我們也將其稱為:用戶級線程。
? ? ? ? 注:使用pthread庫,在編譯器時需要加上 -l pthread選項(因為pthread庫不是被默認鏈接的)
pthread庫接口
1.線程創建
pthread_create
功能:創建線程
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);thread:輸出參數,用于存儲新線程的 ID(pthread_t 類型)
attr:線程屬性(如棧大小、分離狀態等),NULL 表示使用默認屬性
start_routine:線程入口函數(函數指針),格式為 void* (*)(void*),線程啟動后會執行該函數
arg:傳遞給 start_routine 的參數(無參數時傳 NULL)返回值:0:成功;非 0:錯誤碼
演示:
#include <pthread.h> //線程庫
#include <iostream>
using namespace std;void *routine(void *args)
{string name = static_cast<char *>(args);cout << "新線程:" << name << endl;while (true){}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void *)"thread -1");cout << "主線程" << endl;while (true){}
}hyc@hyc-alicloud:~/linux/線程dome$ ./test
主線程
新線程:thread -1
? ? ? ? 可以看到,其實創建了線程!我們也可以通過指令:ps -aL來查看:
hyc@hyc-alicloud:~/linux/線程dome$ ps -aLPID LWP TTY TIME CMD94651 94651 pts/0 00:00:21 test94651 94652 pts/0 00:00:21 test
? ? ? ? PID:我們可以看到PID都是一樣的!這說明都屬于同一個進程!
? ? ? ? LWP:LWP不一樣,這正好說明了創建了新的線程!
補充:
函數:pthread_create(創建線程),其底層其實封裝了系統調用:clone(創建輕量級進程)#include <sched.h>int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...,* pid_t *parent_tid,void *tls, pid_t *child_tid */ );
線程運行問題
創建新線程后,是先執行主線程還是先執行新線程的代碼?這個是不確定的,取決于OS的調用機制! |
?CPU在調度的時候,是調度進程還是線程?線程!線程是CPU調度的基本單位! |
?一個進程有多個線程,那么時間片如何分配?平均分配! |
?線程運行時如果出現異常,整個進程都會被OS直接終止掉!這也就導致了多線程程序的健壯性低。 |
2.線程終止
?pthread_exit
功能:終止線程
#include <pthread.h>void pthread_exit(void *retval);retval:一個 void* 類型的指針,表示線程退出的返回值return也可以終止線程,推薦使用:return
區別:在主線程中使用return,回讓整個進程全部退出!但pthread_exit只會退出主線程,其他子線程照常運行注:線程中萬不可用exit()退出!因為exit()是進程退出的接口!
pthread_cancel
功能:取消線程
#include <pthread.h>int pthread_cancel(pthread_t thread);線程取消后,退出結果是-1【PTHREAD_CANCELED】thread:目標線程的id(由pthread_create得到)
返回值:成功返回 0;失敗返回非 0 的錯誤碼(如 ESRCH 表示目標線程不存在)
注意:該函數只是 “請求” 取消,而非強制終止。目標線程是否以及何時終止,取決于其自身的取消配置
演示:
#include <pthread.h> //線程庫
#include <iostream>
using namespace std;void *routine(void *args)
{string name = static_cast<char *>(args);cout << "新線程:" << name << endl;while (true){}// 不應該看見cout << "線程取消失敗!" << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void *)"thread -1");// 取消線程pthread_cancel(tid);cout << "主線程" << endl;
}hyc@hyc-alicloud:~/linux/線程dome$ ./test
主線程
hyc@hyc-alicloud:~/linux/線程dome$
? ? ? ? 按道理來講,主線程也可以被取消,但并不建議這么做!
3.線程等待
pthread_join
功能:等待線程
其目的與進程的等待一致,都是為了獲得線程的返回值,并回收資源!若不回收將回出現:內存泄漏!
#include <pthread.h>int pthread_join(pthread_t thread, void **retval);thread:需要等待的目標線程的 ID(由 pthread_create 函數返回)
retval:二級指針(void**),用于接收目標線程的退出狀態(即線程通過 pthread_exit(retval) 或 return retval 返回的值)
若不需要獲取退出狀態,可傳入 NULL
若需要獲取,則需提前定義一個 void* 指針,再將其地址傳給 retval返回值:0表示等待成功,非0表示不成功!
? ? ? ? 值得一提的是,此接口的等待方式的阻塞等待!
演示:
#include <pthread.h> //線程庫
#include <iostream>
#include <unistd.h>
using namespace std;// 線程等待void *routine(void *agrs)
{string name = static_cast<char *>(agrs);cout << "新線程執行完方法,返回" << endl;return (void *)1;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void *)"thread");// 阻塞等待void *ret;pthread_join(tid, &ret);cout << "等待成功:" << (long long)ret << endl; // long long防止在64位下進度丟失int cnt = 5;while (cnt--){cout << "主線程運行中" << endl;sleep(1);}
}hyc@hyc-alicloud:~/linux/線程dome$ ./test
新線程執行完方法,返回
等待成功:1
主線程運行中
主線程運行中
主線程運行中
主線程運行中
主線程運行中
hyc@hyc-alicloud:~/linux/線程dome$
4.線程分離
pthread_detach
功能:讓新線程與主線程分離,分離主線程不再阻塞等待新線程了,新線程執行完畢后會自動的回收空間
? ? ? ? 當我們不關心新線程的返回值時,可以讓線程分離,這樣的好處是主線程不用阻塞的等待新線程,可以執行自己的代碼。
#include <pthread.h>int pthread_detach(pthread_t thread);thread 是需要分離的線程 ID(由 pthread_create 創建線程時返回)
成功返回 0;失敗返回非零錯誤碼注:即使線程分離了,分離的線程仍然都在同一個進程的地址空間中,所有的資源依舊可以訪問!分離的線程,不用被主線程join,也不能被主線程join(會失敗)!
演示:
#include <pthread.h> //線程庫
#include <iostream>
#include <unistd.h>
using namespace std;// 線程分離void *routine(void *agrs)
{int cnt = 5;while (cnt--){cout << "新線程運行" << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void *)"thread");cout << "運行主線程" << endl;// 分離pthread_detach(tid);// 等待失敗!!!int ret = pthread_join(tid, nullptr);if (ret != 0)cout << "等待失敗!" << endl;
}hyc@hyc-alicloud:~/linux/線程dome$ make
g++ -o test test.cc -l pthread
hyc@hyc-alicloud:~/linux/線程dome$ ./test
運行主線程
等待失敗!
hyc@hyc-alicloud:~/linux/線程dome$
創建多線程演示
#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;// 創建多線程void *routine(void *agrs)
{string name = static_cast<char *>(agrs);cout << "創建線程:" << name << endl;return nullptr;
}int main()
{for (int i = 0; i < 5; i++){pthread_t tid;char str[10];snprintf(str, sizeof(str), "%s%d", "thread-", i);pthread_create(&tid, nullptr, routine, (void *)str);}while (true){}
}hyc@hyc-alicloud:~/linux/多線程dome$ make
g++ -o test test.cc -l pthread
hyc@hyc-alicloud:~/linux/多線程dome$ ./test
創建線程:thread-1
創建線程:thread-4
創建線程:thread-4
創建線程:thread-4
創建線程:thread-4
? ? ? ? 此時,我們可以看見結果不太對,這是因為for循環的速度與新鍵線程的速度并不一致,導致開沒有開始創建對應的線程時,str里面的內容又被刷新了!
? ? ? ? 處理辦法:開辟獨立的空間,避免被覆蓋!
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
using namespace std;// 創建多線程void *routine(void *agrs)
{string *name = static_cast<string *>(agrs);cout << "創建線程:" << *name << endl;return nullptr;
}int main()
{vector<pthread_t> arr;for (int i = 0; i < 5; i++){pthread_t tid;string *name = new string("thread-" + to_string(i));pthread_create(&tid, nullptr, routine, name);arr.push_back(tid);}for (int i = 0; i < 5; i++){int ret = pthread_join(arr[i], nullptr);if (ret == 0)cout << "等待成功" << endl;}
}hyc@hyc-alicloud:~/linux/多線程dome$ ./test
創建線程:thread-0
創建線程:thread-1
創建線程:thread-3
創建線程:thread-2
創建線程:thread-4
等待成功
等待成功
等待成功
等待成功
等待成功
hyc@hyc-alicloud:~/linux/多線程dome$
線程ID與進程地址空間布局
線程ID
hyc@hyc-alicloud:~$ ps -aLPID LWP TTY TIME CMD103519 103519 pts/3 00:00:00 test103519 103520 pts/3 00:00:04 test103519 103521 pts/3 00:00:05 test103519 103522 pts/3 00:00:04 test103519 103523 pts/3 00:00:04 test103519 103524 pts/3 00:00:04 test
? ? ? ? 首先,我們要區分LWP號與線程ID的區別。
????????LWP號是輕量級線程(LWP)的編號,但為了給用戶提供線程的概念,LWP號肯定不能提供給用戶,于是線程庫提供了標識號:線程ID!
? ? ? ? 那這個線程ID本質是什么東西呢?接著往下看!
進程地址空間分布
? ? ? ? Linux下的線程是由線程庫提供的,而庫是滿足EIF文件格式,動態庫會加載到物理內存空間中,然后再映射到需要的虛擬地址空間中共享區!
? ? ? ? 最后通過起始地址+偏移量的方式,就可以訪問到線程庫中的方法與數據了!
? ? ? ? 線程的概念是在pthread庫中被維護的!那這也意味著庫中一定有大量的被創建的線程!
? ? ? ? 庫一定會管理這些線程,如何管理?先描述,再組織!
描述:
????????庫中存在結構體,TCB用于描述線程對應屬性!
strcut TCB
{線程狀態線程ID線程獨立的棧結構線程棧的大小.....
}注:TCB中并沒有關于線程運行的屬性,如:優先級、時間片、上下文等等
組織:
? ? ? ? 通過數組進行組織!
? ? ? ? TCB分為3大部分:struct pthread、線程局部存儲、線程棧。其中每一個線程都必須有對應的線程棧!因為線程棧主要用于存儲代碼的臨時數據。注:主線程的棧空間并不在線程庫中!
? ? ? ? 線程ID:線程ID其實就是對應的TCB地址!
? ? ? ? 返回值:線程返回值,其實是寫入了struct pthread中的void* ret中,線程等待接口參數的變量之所以是void **,是為了拿到void *ret的數據!
? ? ? ? 線程等待:等待釋放資源,就是為了釋放TCB這個資源!
見一見線程ID:創建線程:thread-0
線程ID:140610093467200
線程ID:140610085074496
線程ID:140610076681792
線程ID:140610068289088
線程ID:140610059896384
? ? ? ? 而strcut pthread中之所有沒有關于線程執行的屬性,如:時間片、優先級。是因為線程的執行工作是交給了底層的系統調用clone!由clone去執行并返回結果!
函數:pthread_create(創建線程),其底層其實封裝了系統調用:clone(創建輕量級進程)#include <sched.h>int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...,* pid_t *parent_tid,void *tls, pid_t *child_tid */ );int clone(int (*fn)(void *), // 1. 子進程執行的函數void *stack, // 2. 子進程的棧指針int flags, // 3. 核心控制標志(位掩碼)void *arg, // 4. 傳遞給fn的參數... /* 可選參數,順序固定 */pid_t *parent_tid, // 5. 父進程中存儲子進程TID的地址void *tls, // 6. 線程本地存儲(TLS)結構地址pid_t *child_tid); // 7. 子進程中存儲自身TID的地址
????????所以,調用pthread_create方法會執行兩大步:
? ? ? ? 1.在庫中創建線程的控制管理塊,TCB
? ? ? ? 2.調用系統調用clone,在內核中創建輕量級進程,并傳入執行方法,讓其執行!
????????總結來說,用戶態管理線程的邏輯信息,內核態負責實際的調度執行,兩者通過系統調用協作,實現線程的創建與運行。
值得一提:Linux中線程(用戶級)與內核LWP是一對一的關系!
線程棧
? ? ? ? 首先,主線程的棧與子線程的棧是不一樣的!
? ? ? ? 主線程的棧大小不固定,可以向下增長!但子線程的棧是固定的,用完就完了,不會增長!
? ? ? ? 對于子線程的棧空間,原則上是線程私有的!但是其他線程想要訪問還是可以訪問的,沒有特殊的限制。
線程局部存儲
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;int num = 0;void *run1(void *args)
{while (1){cout << "修改num:" << num++ << endl;sleep(1);}
}void *run2(void *args)
{while (1){cout << "num:" << num << endl;sleep(1);}
}int main()
{pthread_t tid1, tid2;pthread_create(&tid1, nullptr, run1, nullptr);pthread_create(&tid2, nullptr, run2, nullptr);while (1){}
}hyc@hyc-alicloud:~/linux/線程局部存儲$ ./test
修改num:0
num:1
修改num:1
num:2
修改num:2
num:3
修改num:3
num:4
修改num:4
num:5
修改num:5
num:6
修改num:6
num:7
修改num:7
num:8
修改num:8
? ? ? ? 我們可以看到,兩個線程訪問了同一個全局變量,這也符合我們的預期。
? ? ? ? 下面來看看,線程局部存儲的效果。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;// __thread聲明線程局部存儲變量
__thread int num = 0;void *run1(void *args)
{while (1){cout << "修改num:" << num++ << endl;sleep(1);}
}void *run2(void *args)
{while (1){cout << "num:" << num << endl;sleep(1);}
}int main()
{pthread_t tid1, tid2;pthread_create(&tid1, nullptr, run1, nullptr);pthread_create(&tid2, nullptr, run2, nullptr);while (1){}
}hyc@hyc-alicloud:~/linux/線程局部存儲$ ./test
修改num:0
num:0
修改num:1
num:0
修改num:2
num:0
修改num:3
num:0
修改num:4
num:0
修改num:5
num:0
修改num:6
num:0
? ? ? ? 由運行結果我們可以知道,線程局部存儲就是為每個線程創建該變量的獨立副本,不同線程之間變量互不干擾。只是名字都是一樣的(理解為類似寫時拷貝的效果)
? ? ? ? 作用:當我們想要使用全局變量,但又不想被其他線程干擾。這時就可以聲明線程局部存儲!
? ? ? ? 限制:線程局部存儲只能申明內置類型、部分指針