初識Linux · 線程概念

前言:

linux的學習從開始到現在,我們已經經歷了許多大boss,從一開始的熟悉指令,到第一次在gcc環境下編譯c語言的代碼,到理解文件系統,比如理解了文件的權限,萬物皆文件的概念,此時,是我們經歷的第一次大boss,文件系統。

之后,我們從shell開始慢慢理解linux的系統內核部分,最典型的是我們慢慢開始理解了什么是進程,從pcb->task_Struct到mm_struct地址空間,到頁表部分,最后我們從物理內存出發,一步一步的理解了程序的地址,之后,我們學習了進程的狀態,學習了進程的控制,學習了進程等待,學習了進程終止,以及進程的各種信息,比如Pid,這是第二次大boss。

今天,我們學習的是Linux中的第3個大boss,線程。線程我們同樣,從概念入手,再到線程的控制,線程同步,線程互斥等,和前兩個一樣,都是需要我們反反復復學習的知識點。

那么,話不多說,本文作為線程的概念篇,主要是解釋線程中的概念,并且結合少許的代碼。

進入第一個主題吧!線程的概念。


線程的背景

介紹線程概念我們打算從地址空間入手,在地址空間篇章,我們就提及到過,地址空間我們是要多次介紹的,一共要介紹4次左右,今天就是第四次。

我們知道,之前對于地址空間的理解就是:

初識Linux · 線程概念

這個圖我們已經十分熟絡了,通過頁表和MMU可以將虛擬內存和物理內存建立某種聯系。可是,今天我們對于頁表要理解的更深層一點。

假設在磁盤中有一個hello.o,它是文件吧?那么既然他是文件,它就應該有自己的inode,它也應該存在于物理內存中。既然是和物理內存掛鉤,在前文介紹的磁盤管理中,分為扇區等區域,并且是以4kb的大小進行管理。此時,真實的數據塊加載到了物理內存里面,通過某種方式,和虛擬內存建立了聯系。

那么,我們不妨來解析一下虛擬內存,我們將虛擬內存分為三部分:0000 0000 00,前10位是第一部分,00 0000 0000,中間10位,是第二部分,0000 0000 0000是第三部分。

好吧,在此之前我們其實還是應該再深度解析一下物理內存。

磁盤中的文件的數據是以4kb的方式存在的,我們將每個物理內存叫做頁框,每個文件的數據塊叫做頁幀,其實現在操作系統的書已經沒有區分的那么多了。

對于IO來說,IO的基本單位是4kb,OS要進行內存管理的話,不是以字節的方式管理的,而是以內存塊的方式管理的,所以文件的數據塊,物理內存的頁框都是內存管理的對象

那么OS應該如何管理頁框頁幀呢?

當然是先描述再組織。在Linux中有一種結構體是struct page,里面存的就是頁框的信息。

在Linux的源碼里應該如何管理呢?已經描述好了,那么使用一個數組就管理起來了。

現在我們在來刨析虛擬內存,前10個比特位,其實是指向的頁目錄,我們是拿32位機器舉例,所以頁目錄實際上是由1024個,那么一個一個的頁目錄,指向了一個一個的頁表,對于虛擬地址的中間10位,指向的就是頁表,對于后12位的虛擬地址,就是先定位到了頁目錄之后,才好定位到頁框的位置,這樣,就能通過虛擬地址找到物理內存了。

那么,什么是函數呢?或者說,虛擬地址的本質是什么呢?

其實在mm_struct中,正文的代碼本來就是由一個一個的地址構成的,對于函數來說,不過是一連串的地址而已。所以對于代碼數據劃分的本質,不過是一種對于資源的劃分!


線程的概念和Linux中的線程實現

上面其實是對于頁表的一種重新理解,可能有人覺得和今天的主題線程沒有關系,實則不然,因為今天實際上會對之前進程的理解有一個顛覆性的理解。

在深入了解操作系統這本書里面說的好,進程是一種抽象。我們之前認為的進程是task_struct + mm_struct + 頁表集合等。認為進程 = 內核數據結構 + 自己的代碼和數據。

可是,在內核的觀點里面,認為進程實際上是承擔分配系統資源的基本實體。

今天就不講故事了,我們直接說,以前認為task_struct就是進程,認為cpu調度的時候就是調度的task_strcut。

實際上不是的,一個進程可以存在多個task_struct,而對于task_struct就是Linux中的線程,為什么說是Linux中的線程呢?因為對于windows來說,windows也有自己的線程標準。

我們這樣理解吧,對于國家來說,分配資源的實體是一個一個的家庭,家庭中的許多成員就是一個一個的線程。

那么windows對于線程是提供了真實線程控制塊,對于Linux來說,是直接復用的內核代碼,不然單獨創建線程控制塊,增加管理成本,源碼還要多寫很多很多行。

所以Linux中的線程實際上是集成在進程里面的。所以之前理解的進程實際上是只有一個線程的進程,我們之后要學習的就是一個進程含有多個線程。

那么對于cpu來說,是否要區分什么是線程,什么是進程呢?因為task_strcut理解存放的是進程的信息,但是實際上進程執行任務的時候使用的是線程。

cpu是不用區分的,因為cpu看到的執行流

那么既然有了多進程,為什么還要多線程呢?

因為cpu內部調度的時候,時間片一到,進程切換需要存上下文吧?地址空間,頁表全部都要切換吧?那么這個成本是不是十分高了就?如果使用的是多線程,線程之間共享的是地址空間,頁表,切換的時候成本就很低了。所以這是多線程的優勢。

但是就和家庭一樣,一個線程如果奔潰了,其他線程也都是會崩潰的。


線程雜談

說了那么多,我們總的看看吧?

初識Linux · 線程概念

使用函數我們可以創建線程,其實第一個參數不解釋了,第二個參數我們設置為nullptr即可,對于第三個參數就是函數指針,信號那里我們也見過,第四個參數是線程的名字。

我們直接來一份代碼:

代碼語言:javascript代碼運行次數:0運行復制

int gval = 100;void *threadStart(void *args){    while (true)    {        sleep(1);        std::cout << "new thread running..." << ", pid: " << getpid() << std::endl;        // std::cout << "new thread running..." << ", pid: " << getpid()        //           << ", gval: " << gval << ", &gval: " << &gval << std::endl;    }}int main(){    srand(time(nullptr));    pthread_t tid1;    pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");    // pthread_t tid2;    // pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");    // pthread_t tid3;    // pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");        // 主線程    while (true)    {                std::cout << "main thread running..." << ", pid: " << getpid() << std::endl;        // gval++;        sleep(1);    }    return 0;}

按照平常,我們直接g++是編譯不過去的,因為它需要鏈接庫,沒想到到,重制版介紹為什么。

所以需要在Makefile文件里面加-lpthread

按照常理來說,兩個死循環是不會同時打印的,可是一個進程使用兩個線程調用不同的任務,就可以同時打印了:

初識Linux · 線程概念

像這樣。

那么我們定義一個全局變量,看看g_val的變化:

初識Linux · 線程概念

發現線程是共享數據的,也就是地址空間都是同一份,這個我們也成功驗證了。

初識Linux · 線程概念

當我們輸入ps -aL指令,我們能查看線程的部分信息,可以發現和進程Pid相等的線程實際上是主線程,其他的是非主線程了,那么提問了,對于cpu來說,調度的時候看的是pid還是lwp呢?

當然是lwp了,畢竟是線程來執行的任務。

這里提及一個非常重要的點:

線程雖然共享了許多資源,但是線程私有的部分有一組寄存器,棧。前者要存儲硬件的上下文,后者是線程運行的時候會形成各種臨時變量,肯定都會被自己的線程保存在自己的棧區里。

本文不過是對線程的非常粗略的概念理解,請各位佬不要介意沒有解釋清楚大部分知識,重制版一定非常清晰的介紹清楚了。


感謝閱讀!

? 版權聲明
THE END
喜歡就支持一下吧
點贊9 分享