目錄
Linux線程概念
什么是線程
分頁式存儲管理
虛擬地址和頁表的由來
物理內存管理
頁表
缺頁異常
線程的優點
線程的缺點
線程異常
Linux進程VS線程
進程與線程
進程的多個線程共享
進程與線程關系如圖
Linux線程控制
POSIX線程庫
創建線程
測試
獲取線程ID
線程終止
線程等待
測試
分離線程
測試
線程ID及進程地址空間布局
線程封裝
測試
Linux線程概念
?
什么是線程
?
1.在?個程序?的?個執行路線就叫做線程(thread)。更準確的定義是:線程是“?個進程內部
的控制序列”
2.?切進程至少都有?個執行線程
3.線程在進程內部運行,本質是在進程地址空間內運行
4.在Linux系統中,在CPU眼中,看到的PCB都要比傳統的進程更加輕量化
5.透過進程虛擬地址空間,可以看到進程的大部分資源,將進程資源合理分配給每個執行流,就形成了線程控制流
分頁式存儲管理
虛擬地址和頁表的由來
如果在沒有虛擬內存和分頁機制的情況下,每一個用戶程序在物理內存上所對應的空間必須是連續的
但是不同的程序他的代碼與數據長度都是不一樣的,并且一直會有進程退出,如果采用連續內存的方式就會導致存在很多的內存碎片。
因此,我們希望操作系統提供給用戶的空間必須是連續的,但是物理內存最好不要連續。
由此,虛擬地址與頁表就誕生了
把物理內存按照?個固定的長度的頁框進行分割,有時叫做物理頁。每個頁框包含?個物理頁
?個頁的大小等于頁框的大小。大多數 32位 體系結構支持4KB的頁,而64位體系結
構?般會支持?8KB 的頁。
頁:一個數據塊,存放于頁框或磁盤上
頁框:一個存儲區域
有了這種機制,CPU便并非是直接訪問物理內存地址,而是通過虛擬地址空間來間接的訪問物理內存地址。所謂的虛擬地址空間,是操作系統為每?個正在執行的進程分配的?個邏輯地址。
?
操作系統通過將虛擬地址空間和物理內存地址之間建立映射關系,也就是頁表,這張表上記錄了每?對頁和頁框的映射關系,能讓CPU間接的訪問物理內存地址。
總結?下,其思想是將虛擬內存下的邏輯地址空間分為若?頁,將物理內存空間分為若?頁框,通過頁表便能把連續的虛擬內存,映射到若干個不連續的物理內存頁。這樣就解決了使?連續的物理內存造成的碎片問題。
物理內存管理
?
假設?個可?的物理內存有4GB的空間。按照?個頁框的大小4KB進行劃分, 4GB的空間就是4GB/4KB = 1048576個頁框。有這么多的物理頁,操作系統肯定是要將其管理起來的,操作系統需要知道哪些頁正在被使用,哪些頁空閑等等。
內核用?struct page 結構表示系統中的每個物理頁,出于節省內存的考慮, struct page 中使
用了大量的聯合體union
注意的是 struct page 與物理頁相關,而并非與虛擬頁相關。?系統中的每個物理頁都要分配?
個這樣的結構體,讓我們來算算對所有這些頁都這么做,到底要消耗掉多少內存。
我們算一個struct page 40個字節,一個頁4kb,那么一個struct page的總數大概為頁總數的1/100。
系統4GB的空間,struct page大概占40M,相對于系統的4GB內存來說并不算多
要知道的是,頁的大小對于內存利用和系統開銷來說非常重要,頁太大,頁必然會剩余較大不能利用的空間(頁內碎片)。頁太小,雖然可以減小頁內碎片的大小,但是頁太多,會使得頁表太長而占用內存,同時系統頻繁地進行頁轉化,加重系統開銷。因此,頁的大小應該適中,通常512B -8KB ,windows系統的頁框大小為4KB。
頁表
?
頁表中的每?個表項,指向?個物理頁的開始地址。在 32 位系統中,虛擬內存的最大空間是 4GB ,這是每?個用戶程序都擁有的虛擬內存空間。既然需要讓 4GB 的虛擬內存全部可用,那么頁表中就需要能夠表示這所有的 4GB 空間,那么就一共需要 4GB/4KB = 1048576 個表項。
虛擬內存仍然是連續的,圖中的虛線只是用來表示虛擬內存單元與頁表每一個表項的映射關系
最終實現,虛擬地址上連續,物理地址上分散,并且解決了內存碎片化的問題
提問
在 32 位系統中,地址的長度是 4 個字節,那么頁表中的每?個表項就是占用?4 個字節。所以頁表占據的總空間大小就是: 1048576*4 = 4MB的大小。也就是說映射表自己本身,就要占用4MB /4KB = 1024 個物理頁。這會存在哪些問題呢?
1.我們創建頁表的目的就是為了將進程劃分為可以一個個頁,可以不用連續的存放在物理地址。
但是我們頁表自己就需要1024個連續物理頁,與我們一開始的想法沖突
2.很多時候進程都是需要訪問部分物理頁,沒有必要讓所有物理頁都一直占據內存空間
解答
解決大容量頁表的方法就是將頁表也看作文件,對頁表也進行分頁,因此,多級頁表的思想就產生了。
將一個大頁表拆成1024個小頁表(每個表1024個表項),這樣,1024(表的個數)*1024(每個表中表項)個4k的小頁表同樣可以占據4GB的物理內存空間。
從總數上看,整張大頁表仍然需要4M空間,似乎和之前沒區別,但實際上一個應用程序不可能占據4GB的所有內存空間,或許幾十個小頁表就夠了,一個程序的代碼段,數據段,棧段一共需要10M,也就是3張小頁表就夠了
缺頁異常
?
CPU給MMU的虛擬地址,在 TLB 和頁表都沒有找到對應的物理頁,該怎么辦呢?其實這就是缺頁異常 Page Fault ,它是?個由硬件中斷觸發的可以由軟件邏輯糾正的錯誤。
由于CPU沒有數據就無法進行計算,CPU罷工了,用戶進程也就出現了缺頁中斷,進程會從用戶態切換到內核態,并將缺頁中斷交給內核的Page Fault Handler處理。
缺頁中斷會交給 PageFaultHandler 處理,其根據缺頁中斷的不同類型會進行不同的處理:
?
1.Hard Page Fault 也被稱為 Major Page Fault ,翻譯為硬缺頁錯誤/主要缺頁錯誤,這
時物理內存中沒有對應的物理頁,需要CPU打開磁盤設備讀取到物理內存中,再讓MMU建立虛擬
地址和物理地址的映射。
2.Soft Page Fault 也被稱為 Minor Page Fault ,翻譯為軟缺頁錯誤/次要缺頁錯誤,這
時物理內存中是存在對應物理頁的,只不過可能是其他進程調入的,發出缺頁異常的進程不知道
而已,此時MMU只需要建立映射即可,無需從磁盤讀取寫?內存,?般出現在多進程共享內存區
域。
3.Invalid Page Fault 翻譯為無效缺頁錯誤,比如進程訪問的內存地址越界訪問,右比如對
空指針解引用內核就會報 segment fault 錯誤中斷進程直接掛掉。
線程的優點
?
1.創建一個新線程代價比創建一個新進程代價小得多
2.與進程切換相比,線程切換操作系統要做的工作也小很多
例如:1.由于線程屬于同一個進程,擁有相同的虛擬地址空間。2.進程上下文的切換會擾亂處理器的緩存機制,這將導致內存的訪問在一段時間內效率很低,在線程的切換中就沒有這個問題。
3.線程占用的資源要比進程小很多
4.能充分利用多處理器的可并行數量
5.在等待慢速IO工作完成的同時也可以執行計算任務
6.計算密集型應用為了在多處理器系統上運行,將計算分解到多線程中運行
7.IO密集型應用,為了提高性能將I/O操作重疊。線程可同時等待不同的I/O操作
線程的缺點
?
性能損失
?個很少被外部事件阻塞的計算密集型線程往往無法與其它線程共享同?個處理器。如果計
算密集型線程的數量?可?的處理器多,那么可能會有較大的性能損失,這?的性能損失指
的是增加了額外的同步和調度開銷,而可用的資源不變。
健壯性降低
編寫多線程需要更全?更深?的考慮,在?個多線程程序里,因時間分配上的細微偏差或者
因共享了不該共享的變量?造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護
的。
缺乏訪問控制
進程是訪問控制的基本粒度,在?個線程中調用某些OS函數會對整個進程造成影響。
編程難度提高
線程異常
?
一個進程中的多個線程同屬于該進程,那就意味著一旦某個進程出現異常例如野指針導致的不僅僅是那個線程退出,還是整個進程的退出。因為線程出了異常就是進程出異常。
Linux進程VS線程
?
進程與線程
1.進程是資源分配的基本單位(所有線程共享進程資源)
2.線程是調度的基本單位(這意味著每個線程分配到的時間片和進程本身無關)
3.線程共享一部分數據,同時還擁有自己的一部分數據:
線程ID、一組寄存器、棧、errno、信號屏蔽字、調度優先級
進程的多個線程共享
?
同?地址空間,因此Text Segment、Data Segment都是共享的,如果定義?個函數,在各線程中都可以調用,如果定義?個全局變量,在各線程中都可以訪問到,除此之外,各線程還共享以下進程資源和環境:
文件描述符表、每種信號的處理方式、當前工作目錄、用戶ID與組ID
進程與線程關系如圖
Linux線程控制
?
POSIX線程庫
1.與線程有關的函數構成了?個完整的系列,絕大多數函數的名字都是以“pthread_”打頭的
2.要使用這些函數庫,要引入頭文件<pthread.h>
3.連接這些函數庫需要使用編譯器命令的 -lpthread選項,也就是使用系統路徑下的libpthread.so 動態庫,頭文件與庫文件都在系統默認路徑下
創建線程
#include<pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);thread:返回線程ID
attr:設置線程屬性,設置為nullptr則采用默認配置
start_routine:是個函數地址,線程啟動后要執?的函數
arg:傳給線程啟動函數的參數返回值:成功返回0
錯誤返回錯誤碼
測試
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void* run(void* arg)
{int i=0;while(true){i++;printf("我是線程1:%d\n",i);sleep(1);}
}int main()
{pthread_t tid;int ret;if(ret=pthread_create(&tid,nullptr,run,nullptr)!=0){fprintf(stderr,"pthread create:%s",strerror(ret));exit(EXIT_FAILURE);}while(true){printf("我是主線程\n");sleep(1);}
}
獲取線程ID
#include<pthread.h>pthread_t pthread_self(void);
這個函數返回“進程級線程ID”
這個“ID”是pthread庫給每個線程定義的進程內唯?標識,是pthread庫維持的
當然了,這個ID是進程級的,操作系統并不認識
其實pthread庫也是通過內核提供的系統調?(例如clone)來創建線程的,而內核會為每個線程創建系統全局唯?的“ID”來唯?標識這個線程。
-L 選項:打印線程信息
?
我們可以看到,兩個線程擁有相同的進程ID,但是操作系統為他們分配了不同的“LWP”(即系統級線程ID)
LWP得到的是真正的線程ID。之前使?pthread_self得到的這個數實際上是?個地址,在虛擬地址空間上的?個地址,通過這個地址,可以找到關于這個線程的基本信息,包括線程ID,線程棧,寄存器等屬性。
ps -aL看到的線程ID,有?個線程ID和進程ID相同,這個線程就是主線程,主線程的棧在虛擬
地址空間的棧上,而其他線程的棧在是在共享區(堆棧之間),因為pthread系列函數都是pthread庫提供給我們的。而pthread庫是在共享區的。所以除了主線程之外的其他線程的棧都在共享區。
線程終止
如果需要只終止某個線程而不終止整個進程,可以有三種方法:
1. 從線程函數return。這種方法對主線程不適用,從main函數return相當于調?exit。
2. 線程可以調pthread_exit終止自己
3. ?個線程可以調?pthread_cancel終?同?進程中的另?個線程。
pthread_exit函數
功能:線程終止void pthread_exit(void *value_ptr);value_ptr:value_ptr不要指向?個局部變量,因為這個返回值是要交給其他線程查看的,
如果指向局部變量那么線程結束時,局部變量也會被一起銷毀
因此,value_ptr最好是全局變量的地址或者是堆上開辟的空間?返回值,跟進程?樣,線程結束的時候?法返回到它的調?者(??)
如果采用return返回,那么要求也和pthread_exit相同,不要指向一個局部變量
?
功能:取消?個執?中的線程int pthread_cancel(pthread_t thread);thread:線程ID返回值:
成功返回0
失敗返回錯誤碼
線程等待
?
為什么需要線程等待
已經退出的線程,其空間沒有被釋放,仍然在進程的地址空間內。
創建新的線程不會復用剛才退出線程的地址空間。
?
這里其實和進程等待差不多,如果不等待的話那么線程也會一直占有空間不釋放,相當于“僵尸線程”
功能:等待線程結束int pthread_join(pthread_t thread, void **value_ptr);thread:指定線程IDvalue_ptr:因為線程的返回值是一個一級指針,
所以我們就需要使用二級指針才能拿到線程退出時返回的值返回值:
成功返回0
失敗返回錯誤碼
調用該函數的線程將掛起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過
pthread_join得到的終止狀態是不同的,總結如下:
1.使用return 或pthread_exit函數退出,我們知道exit和return非常類似,所以使用這兩種方式退出時,value_ptr指針上的值就是return的返回值或時傳給pthread_exit函數的參數。
2.如果線程是被別的線程使用pthread_cancel終止掉的,value_ptr所指向的單元里存放的是
就為常數PTHREAD_CANCELED
3.如果對thread線程的終止狀態不感興趣,可以傳NULL給value_ptr參數
測試
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)//1號線程return返回,在堆上開辟空間帶出返回值
{printf("thread 1 returning ... \n");int *p = (int *)malloc(sizeof(int));*p = 1;return (void *)p;
}
void *thread2(void *arg)//2號線程pthread_exit返回,一樣在堆上開辟空間
{printf("thread 2 exiting ...\n");int *p = (int *)malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}
void *thread3(void *arg)//3號線程被主線程pthread_cancel返回,返回值默認為常數PTHREAD_CANCELED
{while (1){ //printf("thread 3 is running ...\n");sleep(1);}return NULL;
}int main(void)
{pthread_t tid;void *ret;// thread 1 returnpthread_create(&tid, NULL, thread1, NULL);pthread_join(tid, &ret);printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);free(ret);// thread 2 exitpthread_create(&tid, NULL, thread2, NULL);pthread_join(tid, &ret);printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);free(ret);// thread 3 cancel by otherpthread_create(&tid, NULL, thread3, NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED)printf("thread return, thread id %lX, return code:PTHREAD_CANCELED\n",tid);elseprintf("thread return, thread id %lX, return code:NULL\n", tid);
}
分離線程
?
默認情況下,新創建的線程是joinable的,線程退出后,需要對其進行pthread_join操作,否則
無法釋放資源,從而造成系統泄漏。
如果不關心線程的返回值,join是?種負擔,這個時候,我們可以告訴系統,當線程退出時,?
動釋放線程資源。
功能:分離線程,線程退出時自動釋放資源int pthread_detach(pthread_t thread);thread:指定線程ID返回值:
成功返回0
失敗返回錯誤碼
我們可以選擇讓線程自己分離自己,也可以選擇讓其他線程來進行分離
thread_detach(pthread_self());thread_datach(thread_id);
joinable和分離是沖突的,?個線程不能既是joinable又是分離的。
測試
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{pthread_detach(pthread_self());printf("%s\n", (char *)arg);return NULL;
}
int main(void)
{pthread_t tid;if (pthread_create(&tid, NULL, thread_run, (void*)"thread1 run...") != 0){printf("create thread error\n");return 1;}int ret = 0;sleep(1); // 很重要,要讓線程先分離,再等待if (pthread_join(tid, NULL) == 0){printf("pthread wait success\n");ret = 0;}else{printf("pthread wait failed\n");ret = 1;}return ret;
}
線程ID及進程地址空間布局
?
pthread_create函數會產生?個進程級線程ID,存放在第?個參數指向的地址中。
前面講的系統級線程ID屬于進程調度的范疇,因為線程是輕量級進程,是操作系統調度的最小單位,所以需要唯一的一個數值來表示該線程
pthread_create函數第?個參數指向?個虛擬內存單元,該內存單元的地址即為新創建線程的線
程ID,屬于NPTL線程庫的范疇。線程庫的后續操作,就是根據該線程ID來操作線程的。
?
線程庫NPTL提供了pthread_self函數,可以獲得線程自身的ID:
?
pthread_t pthread_self(void);
pthread_t 類型取決于實現,對于linux目前的NPTL而言,pthread_t類型的線程ID實際上,本質上是進程地址空間上的一個地址。
線程封裝
?
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
#include <string>
#include <cstdint>namespace ThreadModule
{std::uint32_t cnt; // 原子計數器,形成線程名稱class Thread{public:using work_t = std::function<void()>;enum class TSTATUS{THREAD_NEW,THREAD_RUNNING,THREAD_STOP};Thread(work_t work) : _status(TSTATUS::THREAD_NEW),_joined(true),_func(work){SetName();}void EnableJoined(){if (_status == TSTATUS::THREAD_NEW)_joined = true;}void EnableDetach(){if (_status == TSTATUS::THREAD_NEW)_joined = false;}bool Start(){if (_status == TSTATUS::THREAD_RUNNING)return true;int n = pthread_create(&_id, nullptr, Run, (void *)this);if (n != 0)return false;return true;}bool Join(){if (_joined){int n = pthread_join(_id, nullptr);if (n != 0)return false;return true;}return _status==TSTATUS::THREAD_STOP;}~Thread() {}private:static void *Run(void *obj){Thread *self = static_cast<Thread *>(obj);self->_status = TSTATUS::THREAD_RUNNING;pthread_setname_np(self->_id, self->_name.c_str()); // 為當前線程取名字if (self->_joined == false)pthread_detach(pthread_self());self->_func();self->_status=TSTATUS::THREAD_STOP;return nullptr;}void SetName(){_name = "Thread-" + std::to_string(cnt++);}private:std::string _name;pthread_t _id;TSTATUS _status;bool _joined;work_t _func;};
}
測試
#include"thread.hpp"
#include<vector>
void test_printf(int num)
{std::cout<<num;
}
int main()
{std::vector<ThreadModule::Thread> vec;for(int i=0;i<10;++i){auto func=std::bind(test_printf,i);vec.emplace_back(func);}for(auto& e:vec)e.Start();for(auto& e:vec)e.Join();std::cout<<std::endl;return 0;
}