linux下的多進程,多線程編程

關於多進程和多線程,一直想寫點什麼來進行一次總結,今天終於提筆了,若有講解錯誤之處,希望廣大讀者能給予指正。,我想從以下幾個方面進行一次詳解劃分.第一,運用。第二,同步。第三,通信。第四,選擇。

那麼閒話少說,開始第一個,關於線程和進程之間的運用。

什麼是進程?

有一個很官方的說法:進程是程序在計算機上的一次執行活動。但我覺得,可能這句話有點不對,應該換成進程加線程是程序在計算機上的一次執行活動才更加的合理。因爲進程是資源分配的最小單位,線程是CPU調度的最小單位。所以我們看到進程的時候,應該聯想到資源,看到線程的時候聯想到程序的執行,程序真正再跑的只是線程,例如程序開始的時候,就是一個線程利用進程的資源再跑。

打印出進程ID和主線程ID。

 

 

 

 

如何創建進程?

進程的創建,我們最容易想象到的兩個函數是fork和vfork。

fork在創建的子進程複製了父進程的資源,新舊進程使用同一個代碼段,複製了數據段和堆棧段,但是當進程開始運行的時候,新舊的地址空間在物理內存上開始劃分開來,這裏使用了copy_on_write技術來達到真正意義上的分開。兩者獨立運行,也就會在數據段和堆棧段上的數據不會有衝突。fork的優點取決父子進程完全獨立,這樣具有很好的併發性。

vfork在創建子進程的時候子進程是完全運行在父進程的地址空間上,使用vfork創建的子進程,筆者在這強烈建議使用exec函數族,因爲使用exec函數族,執行了另一個程序,子進程不會對父進程的地址空間有任何引用,若需求上面不能執行exec函數族,那麼子進程必須使用_exit()函數進行退出,也不能是執行exit()函數,因爲exit函數會將標準輸入輸出流進行刷新,釋放所佔有的資源以及清空緩衝區,而_exit()沒有刷新緩衝區功能,這裏是考慮到vfork父子進程使用同一個內存空間,不然程序會出現意料之外的結果,比如如下圖所示程序就出現三個進程,兩個子進程和一個父進程,只有當子進程執行了_exit(),exec函數族等函數的時候,父進程纔會正常執行。所以在此建議讀者,如果需要使用exec函數族的時候,就用vfork()創建,不然就用fork()創建子進程.

 

 

 

 

 

 

 

 

 

 

 

 運行結果是

num=0
子進程pid=29356
num=7917675
父進程pid=29355
num=0
子進程pid=29360

進程的等待,父進程可以用 wait和waitpid函數等待子進程的結束,wait是等待 任意一個子進程結束,waitpid 可以對等待的方式進行設置。如果沒有 子進程,直接返回。如果沒有結束的子進程, 父進程將被 阻塞(waitpid可以設置非阻塞)。wait、waitpid都可以取得 子進程的結束狀態。pid_t wait(int *status),返回結束的子進程id,參數用於帶出結束狀態,pid_t waitpid(pid_t pid,int* status,int options),返回結束的子進程id,參數status用於 帶出結束狀態,options可以指定非阻塞,一般用0代表阻塞。結束狀態 status可以用 宏做一些判斷:WIFEXITED(status) 可以判斷子進程是否正常結束。

   #include<pthread.h>
   #include<stdio.h>
   #include<stdlib.h>
   #include<signal.h>
   int main(int argc,char* argv[])
   {
       int status=0;
       pid_t pid=fork();
       if(pid<0)
       {
           perror("fork");
           exit(1);
       }
       else if(pid==0)
       {
           sleep(1);//讓進程進行休眠狀態,保證父進程先執行
           printf("子進程pid=%d\n",getpid());
           exit(5);
       }
       else
       {
           wait(&status);//程序進行阻塞狀態,看其是否是等待子進程執行完畢
           printf("父進程pid=%d,%d\n",getpid(),status);
           if(WIFEXITED(status))
           {
               printf("the child process exit normally\n");
           }
           else
           {
               printf("the child process exit abnormally\n");
           }
       }
   }
<span style="font-family:KaiTi_GB2312;"><span style="font-size:18px;">
</span></span>

 

進程的結束,進程的結束可以分程序執行完畢,自然結束,也可以由int kill(pid_t pid, int sig);向指定的進程號發送信號,如果發送9信號,默認是殺掉指定的進程,因爲考慮到資源問題,擔心一些動態內存在進程裏面申請後,正準備釋放之前該進程被其它進程kill了,導致出現內存泄露,所以我們儘量避免使用kill這樣暴力的函數

父子進程之間的執行,注在線程裏面沒有父子線程關係,只有主線程和其它線程.父進程啓動子進程後,父子進程 同時運行。如果子進程先結束,子進程 給父進程發信號,父進程 負責回收子進程的資源。 父進程啓動子進程後,父子進程 同時運行。如果父進程先結束,子進程成 孤兒進程,子進程 認 init進程(進程1)做新的父進程,init進程 也叫 孤兒院。父進程啓動子進程後,父子進程 同時運行。如果子進程先結束,子進程 給父進程發信號,但父進程有可能沒收到信號,或者收到了沒處理,子進程 會變成 殭屍進程

 

 

線程的創建,使用函數int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg);第一個參數是指向線程標示符的指針,線程標示符,一個無符號的長整型的類型,在頭文件裏面我們可以看到它的定義typedef unsigned long int pthread_t;第二個參數是指向線程屬性的指針,這個如若不設置屬性,那麼設置爲空,一切就以默認屬性,筆者覺得這個屬性比較重要,將在後面詳細講解.第三個參數,顧名思義是一個函數指針,函數的返回值類型是無符號的指針類型,第四個參數是函數指針函數的傳遞參數,如若沒有參數,則可以設置空,如果需要傳遞多個參數,可定義結構體進行傳遞,不過傳遞的時候要強制轉換成void*類型。

線程的等待,使用函數pthread_join(),該函數是當線程結束的時候,清收線程的資源,然後函數返回,這裏可以保證不會存在主線程結束了,開啓的線程還麼開始執行,或者只是執行了一半,有讀者可能會有疑問,不是說線程是執行單位,進程纔是資源單位,爲什麼還有清理資源工作,這裏我們需要瞭解到其實在Linux中,新建的線程並不是在原先的進程中,而是系統通過一個系統調用函數clone()。該系統調用copy了一個和原先進程完全一樣的進程,並在這個進程中執行線程函數。不過這個copy過程和fork不一樣。 copy後的進程和原先的進程共享了所有的變量(有點像是vfork的功能),運行環境。這樣既可以做到多線程之間共享全局變量。所以pthread_join函數保證了線程的全部執行和資源清理工作。

線程的屬性,也就是在創建線程函數pthread_create裏面的第三個參數pthread_attr_t *attr,首先我們要知道,pthread_attr_t是一個結構體類型,如下所示

                    typedef struct
                     {
                        int                          detachstate;     線程的分離狀態
                       int                          schedpolicy;   線程調度策略
                       struct sched_param              schedparam;   線程的調度參數
                       int                          inheritsched;    線程的繼承性
                       int                          scope;          線程的作用域
                       size_t                        guardsize; 線程棧末尾的警戒緩衝區大小
                       int                          stackaddr_set;
                        void *                        stackaddr;      線程棧的位置
                       size_t                        stacksize;       線程棧的大小
                }pthread_attr_t;

屬性pthread_attr_t主要包括scope屬性、detach屬性、堆棧地址、堆棧大小、優先級。在這裏就注重講解堆棧大小屬性的設置及其應用,至於其它屬性值,讀者若是有興趣,可以參考http://blog.csdn.net/zsf8701/article/details/7843837,這篇博客.在很多的時候,我們需要增加併發數量來完成某一項認爲,特別是在網路編程裏面服務端程序上支持多併發處理,我們需要更多的併發量去支持,我們假設使用的計算機CPU是32位數,最大的尋址範圍是4GB,1GB是所有的進程共享的內核空間,3GB是用戶空間,也就是進程虛擬內存空間,我們在linux下面用ulimit -s 可以產看當前系統上線程的堆棧空間大小是多少,一般默認是8M,這樣我們可以計算出線程的最大上限數量是: (1024*1024*1024*3) / (1024*1024*8) = 384,實際數字應該略小384,因爲還要計算程序文本、數據、共享庫等佔用的空間。當前的WEB服務器上面併發量大於384已經是很平常的事了,那麼我們如何突破這個限制,已達到更多的併發量呢,有兩種辦法,第一是通過ulimit -s 去修改堆棧的空間大小,比如 ulimit -s 1024  每個線程只分配1M的空間,那麼併發量就可以增加到384*8,可以根據自己的項目需求去更改這個限制,但是這樣做的缺點是修改了,那麼所有在本機器上面跑的進程,都會默認這個設置,就會讓一些進程因爲堆棧空間太小導致段錯誤,所以顯然這個不是一個很理智的辦法。於是我們就有了線程設置堆棧大小的屬性,保證本次受影響的線程只是設置這個屬性的線程。說了這麼多,我們就開始用實際代碼練練手。

#include<stdio.h>
#include<string.h> 
#include<limits.h>
#include<pthread.h>
void* fun(void* i);
int main(int argc,char* argv[])
  {
     pthread_t t[1000];
     pthread_attr_t attr;
     int i=0;
     
     if(pthread_attr_init(&attr))//初始化一個屬性
      {
         perror("attr");
     }
     
     if(pthread_attr_setstacksize(&attr,163840))//設置線程的堆棧大小,我們開始利用初始化的屬性進行堆棧空間大小的設置,這裏是以字節爲單位的堆棧空間大小的調整
      {
         perror("attr\n");
     }

     for(i=0;i<1000;i++)
     {
         pthread_create(&t[i],&attr,fun,NULL);//開啓一個線程
      }

      for(i=0;i<1000;i++)
     {
        pthread_join(t[i],NULL);
     }
    printf("%d\n",PTHREAD_STACK_MIN);//這裏需要牽扯到一個知識點,我們在設置堆棧空間大小的時候,不能小於linux內部定義的最小堆棧大小,可以打印這個宏看看是多少,根據操作系統不同而不同,我這裏是16384
 }
 void* fun(void*i)
 {
 }

在設置屬性值的時候,注意點是第一先利用pthread_attr_init初始化一個屬性,後續才能利用這個初始化好的屬性值。

在設置堆棧大小上,不得不提到一個重要的屬性是線程棧末尾的警戒緩衝區,int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);設置線程的堆棧保護區,第二個參數設置堆棧的保護區,一般默認是4096個字節,一旦線程棧在使用中溢出併到達了這片內存,程序可以捕獲系統內核發出的告警信號,然後使用 malloc獲取另外的內存,並通過stackaddr改變線程棧的位置,以獲得額外的棧空間,這個動態擴展棧空間辦法需要手工編程。

線程的結束,我們在啓動一個線程,強烈建議不要在外部用pthread_kill,pthread_cancel等函數強行的中端一個線程,這樣容易導致很多問題,因爲這樣的強行取消我們不清楚線程終止的地方會是在哪裏,可能會在這之前動態申請了內存,而沒有得到釋放,更嚴重的是在剛進入一個加鎖代碼中,然後被強制退出,那麼就會導致死鎖現象,所以建議讓線程能夠自己執行完,讓所有的資源能夠得到釋放,若必須使用外部干擾,那麼我們需要在線程裏面設置安全的取消點,也就是當線程接收到cancel的時候,不會馬上退出,運行到一個相對比較安全的地方,然後再退出本次線程,設置取消點上,我們需要讓本次線程能夠做到延遲取消 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);在線程設置這個屬性,然後設置一個取消點,取消點有很多的線程函數都可以,常用的是pthread_testcancel(),pthread_join()等

第二個,同步

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章