1. 線程
1. 線程是一個進程內部的控制序列
2. 線程在進程內部運行,本質是在進程地址空間內運行
3. 進程:承擔分配系統資源的基本實體
? ? 線程:CPU調度的基本單位
4. 線程在進程地址空間內運行
? ? 進程訪問的大部分資源都是通過地址空間訪問的
5. 在硬件CPU視角:線程是輕量級進程
? ? ?Linux操作系統視角:執行流
? ? ?執行流 <= 進程
6. 將資源合理分配給每一個執行流,就形成了線程執行流
7. 一切進程至少都有一個執行線程
總結:
1. 線程可以采用進程來模擬
2. 對資源的劃分本質是對地址空間虛擬地址范圍的劃分。虛擬地址就是資源的代表
3. 函數就是虛擬地址(邏輯地址)空間的集合,就是讓線程未來執行ELF程序的不同函數
4. linux的線程就是輕量級進程,或者用輕量級進程模擬實現的
5. 如果把家庭比作進程,那么家庭的每個成員就都是線程
進程強調獨占,部分共享(通信的時候)
線程強調共享,部分獨占
4KB內存與頁框:
物理內存以4KB為單位被劃分成一個一個的頁框
在進行I/O操作時,數據也是以4KB為單位在內存和磁盤間交換(程序需要讀取磁盤數據時也是以4KB大小的塊來讀取)
要管理這些4KB的頁框,也是先描述再組織
申請物理內存是在做什么?
1.查數組,改page(頁)
2.建立內核數據結構的對應關系
struck page是一個自定義描述物理內存中頁的結構體
struct page mem[1048576];
聲明了一個包含1048576個類型為struct page的元素的mem數組,每個page都有下標
4GB=4*1024*1024 KB
4*1024*1024KB/4KB = 1048576
每個page的起始物理地址就在獨立,具體物理地址=起始物理地址+頁(4KB)內偏移
沒有使用的page標志位為0
劃分地址空間本質就是劃分虛擬地址
在cpu視角全部都是輕量級進程
OS管理的基本單位是4KB
頁表(本質是一張虛擬到物理的地圖)的地址轉換
虛擬地址(邏輯地址) 轉化為物理地址
32位的數字
0000000000 0000000000 000000000000? ? ? ? ? ? ? ? ? ? ? ??
[0,1024)? ? ? ? [0,1024)? ? ? ? ? ? ?[0,4096]
CR3寄存器讀取頁目錄起始位置,根據一級頁號查頁目錄表
前10個bit位的縮影查到頁目錄
頁目錄里存儲的是下一級頁表的地址,每一項對應一個二級頁表,定位到下一級頁表的位置
頁目錄中的項可以理解為一種指針
二級頁表里面存儲的是物理頁框的地址,用于實現虛擬地址和物理地址間映射的關鍵
低12位為頁內偏移
4KB頁面大小意味著每個頁面有4096個字節單位,12位二進制數可表示為2^12 = 4096 個不同地址,可以用低12位去充分覆蓋一個頁框的整個范圍,可唯一標識頁面內的每個字節單元
先查到虛擬地址對應的頁框,根據虛擬地址的低12位作為頁內偏移訪問具體字節
一些細節:
1. 內存申請->查找數組->找到沒有被使用的page(標志位為0)->page
index(索引)->物理頁框地址
2. 寫實拷貝,缺頁中斷,內存申請等,背后都可能要重新建立新的頁表和建立映射關系的操作
3. 進程,一張頁目錄+n張頁表構建的映射關系,虛擬地址是索引,物理地址頁框是目標
物理地址=頁框地址+虛擬地址(低12位)
線程的深刻理解
執行流看到的資源是在合法情況下擁有的合法虛擬地址,虛擬地址就是資源的代表
虛擬地址空間本質:進行資源的統計數據還和整體數據
資源劃分:本質就是地址空間劃分
資源共享:本質就是虛擬地址的共享
線程進行資源劃分:本質是劃分地址空間,獲得一定范圍的合法虛擬地址,在本質,就是劃分頁表
線程進行資源共享:本質是對地址空間的共享,在本質就是對頁表條目的共享
申請內存也就是申請地址空間
越界不一定報錯
優點:
線程切換:
線程之間的切換需要OS做的工作比進程要少很多
線程切換虛擬地址空間依然是相同的
線程切換時不用對CR3寄存器進行保存
線程切換不會導致緩存失效
進程切換:
指針指向我們選中的進程,OS想知道當前進程是誰,找到該指針,優化到cpu寄存器中
進程切換=>cpu硬件上下文切換
會導致TLB和Cache失效,下次運行,需要重新緩存
線程占用的資源比進程少?線程拿到的資源本身就是進程的一部分,線程是更輕量化的
2. 進程VS線程
進程是資源分配的基本單位
線程是調度的基本單位
線程共享進程數據,但也擁有自己的一部分數據
1.線程ID
2.一組寄存器,線程的上下文數據
3.棧
4.erno
5.信號屏蔽字
6.調度優先級
3. linux 線程控制
創建線程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);//thread :返回線程id
//attr:設置線程屬性
//start_routine:是個函數地址,線程啟動后要執行的函數
//arg:傳給線程啟動函數的參數
線程創建好之后,新線程要被主線程等待(類似僵尸進程的問題,內存泄漏)
代碼:
?PID:進程ID
LWP:? 輕量級進程ID
這意味著進程內有多個線程,每個線程對應一個LWP號
CPU調度的時候,看輕量級進程lwp
1.關于調度的時間片問題:時間等分給不同的線程
2.任何一個線程崩潰,都會導致整個進程崩潰
線程tid:不直接暴露lwp概念
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <pthread.h>void showtid(pthread_t &tid)
{printf("tid: 0x%lx\n",tid);
}
std::string FormatId(const pthread_t &tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}
void *routine(void *args)
{std::string name = static_cast<const char*>(args);pthread_t tid = pthread_self();int cnt = 3;while(cnt){std::cout << "我是一個新線程: my name: main thread " << " 我的Id: " << FormatId(tid) << std::endl;sleep(1);cnt--; }return nullptr;
}int main()
{pthread_t tid;//tid變量用于存儲新創建進程的標識符int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");(void)n;showtid(tid);int cnt = 3;while(cnt){std::cout << "我是main線程: my name: main thread " << " 我的Id: " << FormatId(pthread_self()) << std::endl;sleep(1);cnt--; }pthread_join(tid,nullptr);//等待進程結束return 0;
}
main函數也有自己的線程id
庫
pthread庫,把創建輕量級進程封裝起來,給用戶提供一批創建線程的接口
linux線程實現是在用戶層實現的,我們稱之為用戶級線程
pthread:原生線程庫
C++的多線程,在linux下,本質是封裝了pthread庫,在windows下封裝windows創建線程的接口
linux系統,不存在真正意義上的線程,他所謂的概念,使用輕量級進程模擬的,但OS中,只有輕量級進程,所謂的模擬線程是我們的說法,linux只會給我們提供創建輕量級進程的系統調用
pthread_exit函數
線程終止
pthread_cancel
取消一個執行中的線程
取消的時候一定要保證線程已經啟動
pthread_join
等待線程結束
資源回收,線程終止時,系統不會自動回收線程資源,直到有其他線程調用該函數,目標線程的資源會被徹底釋放。
//thread:要等待的線程id
//retval:二級指針,用于存儲目標線程的返回值
pthread_join() 函數必須由其他線程調用,用于回收目標終止線程的資源
只能由當前線程以外的其他線程調用。例如:
主線程可以調用 pthread_join() 回收子線程的資源
子線程 A 可以調用 pthread_join() 回收子線程 B 的資源
通過函數參數指定要回收的目標線程 ID(tid)
線程分離
線程分離是一種管理線程資源的機制,當線程被設置為分離狀態時,它終止后會自動釋放所有資源,不需要有其他線程調用pthread_join來回收資源
線程的狀態:1.Joinable 可結合的 新創建的線程是可結合的,需要對其進行pthread_join操作來回? ? ? ? ? ? ? ? ? ? ? ? ? ?收資源,避免資源泄露
? ? ? ? ? ? ? ? ? ? ? 2.Detached 分離的
------------------------------------------------------------------------------------------------------------------------------------------------------------
linux沒有真正的線程,他是用輕量級進程模擬的
os提供的接口,不會直接提供線程接口
在用戶層,封裝輕量級進程形成原生線程庫(用戶級別的庫)
linux所有線程,都在庫中
線程的概念是在庫中維護的,在庫內部就一定會存在多個被創建好的線程,庫管理線程也是先描述再組織
? ? ? ? ? ? ? ? ? ?pthread_create()
struct tcb
{
? ? ? ? //線程應該有的屬性
? ? ? ? 線程狀態
? ? ? ? 線程id
? ? ? ? 線程獨立的棧結構
? ? ? ? 線程棧大小
}
線程自己的代碼區可以訪問到pthread庫內部的函數或數據
linux所有線程都在庫中
顯示器文件本身就是共享資源
拿到新線程的退出信息
線程測試
線程能夠執行進程的一部分
創建多線程
為什么是9?給每個線程第一傳id,大家的地址都一樣,所有線程參數指向的空間都是同一個,每創建一個進程都要覆蓋式改這個id,創建的這個id會讓所有線程都看到,拿到的都是同一個地址,指向的是同一個64位的空間,所以進行對應的寫入時就把上一次的覆蓋了
每一次循環要給每一個線程申請一段堆空間,這個堆空間雖然也是共享的,但只有改=該線程知道空間的起始地址
創建訪問線程的本質就是訪問用戶級別的庫
描述線程的管理塊
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? pthread庫在內存中
只需要描述我們線程有關的id信息
創建一個描述線程的管理塊,有三部分構成,返回時,返回的id地址就是這個塊的起始地址
需要jion,因為線程結束時,只是函數結束了,但是在庫中線程管理塊并沒有結束
由tid(退出的線程管理塊的起始地址/線程在庫中,對應的管理快塊的虛擬地址)和ret(曾經線程退出時的結果)就拿到整個線程退出時的退出信息,再把該線程的管理塊全部釋放,得到返回結果同時解決內存泄漏問題
在自己的代碼區里調create(),其實是在動態庫內部創建描述該線程的管理塊,管理塊的開頭是線程tcb,里面包含了線程的相關信息,tcb里包含了void *ret字段。當當前線程運行的時候,運行結束會把返回值拷貝到自己線程控制塊的void *ret。新主線程都共享地址空間,只要拿到起始虛擬地址,就可以拿到退出線程的控制塊
每個線程都有自己獨立的棧空間
clone是用于創建進程/線程的函數,可以看作是fork的升級版
#define _GNU_SOURCE#include <sched.h>int clone(int (*fn)(void *), void *stack, int flags, void *arg, .../* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
fn:子進程/線程的入口函數
linux所有線nn
linux用戶級線程:內核級LWP = 1:1
主線程和新線程誰先運行是不確定的