20155321 《信息安全系統設計》Linux多線程的深入學習
再次學習之多進程
基本概念的再次學習
- 線程是程序執行的最小單位(進程是資源管理的最小單位),線程隸屬於某個進程中
進程有自己的數據段、代碼段和堆棧段。線程通常叫做輕型的進程,每個線程共享其所附屬進程的所有資源,包括打開的文件、內存頁面、信號標識及動態分配的內存等
- 線程和進程比起來很小,因此線程花費更少的CPU資源
- 在大並發、不斷有客戶端在請求服務器端的情況下,服務端則應該使用線程
當進程退出時該進程所產生的線程都會被強制退出並清除。一個進程至少需要一個線程(主線程)作為它的指令執行體,進程管理著資源,將線程分配到某個CPU執行
- 線程的分類
- 用戶級線程(由用戶決定,主要解決上下文切換的問題)和內核級線程(由內核調度機制實現)
- 用戶級線程綁定內核級線程運行,一個進程中的內核級線程會分配到固定的時間片,用戶級線程分配的時間片以內核級線程為準
當CPU分配給線程的時間片用完後但線程沒有執行完畢,此時線程會從運行狀態返回到就緒狀態,將CPU讓給其它線程使用
在Linux中,一般采用pthread線程庫實現線程的訪問與控制,因此在編譯的時候要鏈接庫pthread,即-lpthread
- 線程的標識
- 每個線程都有自己的唯一標識(標識是pthread_t數據類型)
- 線程標識只在所屬的進程環境中有效
++與線程標識相關的函數++:
int pthread_equal(pthread_t,pthread_t)
:判斷線程是否一致;pthread_t pthread_self(void)
學習時遇到的理解問題
- 問題1:對於CPU而言,每次均有一個進程搶到CPU,而進程的運行實質上是指進程中的線程在運行。但是一個進程中有多個線程(包括主線程和主線程創建的若幹個為了滿足客戶需求的子線程),那麽在某一時刻,到底是哪個線程獲得CPU在運行呢?
- 根據網上的輔助資料顯示,具體哪個線程得到CPU並執行需要通過系統的調度,例如根據這些線程的優先級將某個線程從就緒狀態調度為running狀態。即系統是先把某個進程調度為running狀態,後再根據某些規則(例如操作系統課上提到的優先級和調度算法)將某個線程調度為running狀態。(當然這些線程之間也會根據某個算法進行切換,在進程擁有很多線程的時候,一般而言某個線程很難一直占有CPU)
- 問題2:用戶級線程與內核級線程是處於綁定運行的狀態,也就是說用戶級線程跟在內核級線程後進行運行。那麽不會出現以下這個情況嗎:某一個內核級線程與多個用戶級線程綁定在一起,當這個內核級線程被調度的時候,與它所綁定的多個用戶級線程也就被調度了,這樣不會造成CPU的疑惑嗎?(就是CPU怎麽知道要執行哪個線程呢)
- 默認情況下用戶級線程和內核級線程是一對一,例如一個內核級線程被允許執行10ns,那麽與它所綁定的這個用戶級線程也就執行10ns的時間。雖然也可以多對一,但會導致實時性比較差
線程的創建
- 調用函數:
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void*),void *restrict arg);
成功則返回0 - tidp:線程標識符指針
- attr:線程屬性指針(不要求則傳空)
- start_rtn:線程運行函數的起始地址,(需要用戶自己定義)
- arg:傳遞給線程運行函數的參數
- 新創建線程從start_rtn函數的地址開始運行
不能保證新線程和調用線程的執行順序
實踐環節
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* th_fn(void *arg)
{
int distance=(int)arg;
int i;
for(i=1;i<=distance;i++){
printf("%lx run %d\n",pthread_self(),i);
sleep(1);
}
return (void*)0;
}
int main(void)
{
int err;
pthread_t rabbit,turtle;
if((err = pthread_create(&rabbit,NULL,th_fn,(void*)50)) != 0){
printf("error!");
}
if((err = pthread_create(&turtle,NULL,th_fn,(void*)50)) != 0){
printf("error!");
}
printf("control thread id:%lx\n",pthread_self());
printf("finish!\n");
return 0;
}
運行結果如下所示:
從輸出可以看到,程序並沒有創建子線程成功,這個原因主要是因為當子線程被創建時,無法判斷是主線程先執行還是子線程先執行,從這個結果可以看到,程序在創建完子線程後執行了主線程,因此當主線程執行完畢後,因為遇到了return 0,代表了整個進程的結束,因此新創建的子進程也被強制退出了。
由此可以得知若要令子線程也被執行,則應令主線程處於等待狀態,即令主線程等待子線程執行完畢後再退出。在代碼上只需在return 0;語句前加上sleep(10)語句,令主線程處於阻塞狀態即可。
由運行結果可以看出,兩個線程正在交替運行,運行結束後才執行主線程並退出
- 問題3:從上面這個實踐可以看出,在調用pthread_create()創建子線程的時候,是通過第四個參數進行參數傳遞,但是實際應用中可能向子線程傳遞多個參數,這個時候該怎麽辦呢?
把所有要傳的參數組成一個結構體,把這個結構體傳給子線程以完成向子線程傳遞多個參數
- 關於問題3的實踐
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
char name[20];
int time;
int start;
int end;
}RaceArg;
void* th_fn(void *arg)
{
RaceArg *r = (RaceArg*)arg;
int i = r->start;
for(; i <= r->end ; i++){
printf("%s(%lx) running %d\n",r->name,pthread_self(),i);
usleep(r->time);
}
return (void*)0;
}
int main()
{
int err;
pthread_t rabbit,turtle;
RaceArg r_a = {"rabbit",10,20,50};
RaceArg t_a = {"turtle",20,10,60};
if((err = pthread_create(&rabbit,NULL,th_fn,(void*)&r_a)) != 0){
printf("error!");
}
if((err = pthread_create(&turtle,NULL,th_fn,(void*)&t_a)) != 0){
printf("error!");
}
pthread_join(rabbit,NULL);
pthread_join(turtle,NULL);
printf("control thread id:%lx\n",pthread_self());
printf("finish!\n");
return 0;
}
即定義結構體,把需要傳的參數放在結構體內,然後把指向結構體的指針傳到子線程中。
- 問題4:從上面兩次的實踐可以知道,通過線程創建函數創建了2個子線程,這些子線程在內存中是如何存儲的,以保證他們內部存儲的信息互不相幹擾又能共同共享同一個進程?
- 線程所擁有的局部變量是互不相幹擾的,以上面實踐的為例,在內存中的存儲方式如下圖所示:
但對於全局變量則是存儲在進程數據段當中,子線程則共享數據段中的數據(共享資源),但這樣好像會對安全性有一定的影響
線程的終止
- 主動終止
- 線程的執行函數中調用return語句
調用pthread_exit()
- 被動終止
被同一進程的其他線程取消:pthread_cancel(pthid),pthid為被終止的線程標識符。此函數類似進程中的kill函數
註意,若在線程中調用exit(0)這類函數時,則是進程被終止了。另外,當進程還未結束時,退出線程所占用的資源並不會隨線程結束而釋放
- pthread_join()函數
- 原型為
int pthread_join(pthread_t th,void **thread_return);
th為被等待線程的標識符,thread_return為用戶定義指針,用來存儲被等待線程的返回值。成功則返回0 調用pthread_join對資源的釋放也有一定幫助,避免內存過於擁擠
對於pthread_join()函數,我認為難點主要是第二個參數,它是一個二級指針變量,為了更好理解pthread_join()函數的第二個參數,可以使用下面這個例子(代碼功能是將傳入子線程的兩個參數相加)
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct
{
int d1;
int d2;
}Arg;
void* th_fn(void *arg)
{
Arg *r = (Arg*)arg;
return (void*)(r->d1 + r->d2);
}
int main(void)
{
int err;
pthread_t th;
Arg r = {20,50};
if((err = pthread_create(&th,NULL,th_fn,(void*)&r)) != 0){
printf("error!");}
int *result;
pthread_join(th,(void**)&result);
printf("result is %d\n",(int)result);
return 0;
}
運行結果如下:
- 類似地,如果希望pthread_join()函數能返回多個參數,就把這多個參數組成一個結構體並令線程返回指向該結構體的指針變量即可。
線程的清理和控制
- pthread_cleanup_push(void (rtn)(void ),void* arg)函數
- pthread_cleanup_pop(int execute)函數,成功則返回0
- 兩者成對出現,即
while(execute){
執行線程處理函數
}
- 觸發線程調用清理函數的動作
- 調用pthread_exit
- 響應取消請求
用非零execute參數調用pthread_cleanup_pop時
實踐
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
void clean_fun(void *arg)
{
char *s = (char*)arg;
printf("clean_func:%s\n",s);
}
void* th_fun(void *arg)
{
int execute = (int)arg;
pthread_cleanup_push(clean_fun,"first clean");
pthread_cleanup_push(clean_fun,"second clean");
printf("thread running %lx\n",pthread_self());
pthread_cleanup_pop(execute);
pthread_cleanup_pop(execute);
return (void*)0;
}
int main()
{
int err;
pthread_t th1,th2;
if((err = pthread_create(&th1,NULL,th_fun,(void*)1)) != 0){
printf("error");
}
pthread_join(th1,NULL);
printf("th1(%lx) finished\n",th1);
if((err = pthread_create(&th2,NULL,th_fun,(void*)1)) != 0){
printf("error");
}
pthread_join(th2,NULL);
printf("th2(%lx) finished\n",th2);
return 0;
}
由運行結果也可以看出,棧的特點是後入先出,即先壓入棧的先被處理。
進程和線程啟動和終止方式的比較
線程的狀態轉換
<未完>
20155321 《信息安全系統設計》Linux多線程的深入學習