Linux線程 之 線程 線程組 進程 輕量級進程(LWP)


Thread Local Storage,線程本地存儲,大神Ulrich Drepper有篇PDF文檔是講TLS的,我曾經努力過三次嘗試搞清楚TLS的原理,均沒有徹底搞清楚。這一次是第三次,我沉浸glibc的源碼和 kernel的源碼中,做了一些實驗,也有所得。對Linux的線程有了進一步的理解。
   線程是有棧的,我們知道,普通的一個進程,它的棧空間是8M,我們可以通過ulmit -a查看:

  1. stack size (kbytes, -s) 8192

   線程也不例外,線程也是需要棧空間的這句話是廢話,呵呵。對於屬於同一個進程(或者說是線程組)的多個線程他們是共享一份虛擬內存地址的,如下圖所示。 這也就決定了,你不能無限制創建線,因為縱然你什么都不做,每個線程默認耗費8M的空間(事實上還不止,還有管理結構,后面陳述)。Ulrich Drepper大神有篇文章《Thread numbers and stacks》,分析了線程棧空間方面的計算。如果我們真的需要很多個線程的話,幸好我們還是可以做一些事情。我們可以通過 pthread_attr_setstacksize,設定好stack size屬性然后在pthread_create.

  1. int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

  2. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
  3.                    void *(*start_routine) (void *), void *arg);

    
   線程棧如上圖所示,共享進程(或者稱之為線程組)的虛擬地址空間。既然多個線程聚集在一起,我怎么知道我要操作的那個線程棧的地址呢。要解決這個問題,必須要領會線程和進程以及線程組的概念。我不想寫一堆片湯話,下面我運行我的測試程序,然后結合現象分析原因:

  1. #include <stdio.h>
  2. #include <pthread.h>
  3. #include <sys/syscall.h>
  4. #include <assert.h>

  5. #define gettid() syscall(__NR_gettid)

  6. pthread_key_t key;
  7. __thread int count = 2222;
  8. __thread unsigned long long count2 ;
  9. static __thread int count3;
  10. void echomsg(int t)
  11. {
  12.     printf("destructor excuted in thread %x,param=%x\n",pthread_self(),t);
  13. }

  14. void * child1(void *arg)
  15. {
  16.     int b;
  17.     int tid=pthread_self();

  18.     printf("I am the child1 pthread_self return %p gettid return %d\n",tid,gettid());

  19.     char* key_content = malloc(8);
  20.     if(key_content != NULL)
  21.     {
  22.         strcpy(key_content,"ACACACA");
  23.     }
  24.     pthread_setspecific(key,(void *)key_content);
  25.     
  26.     count=666666;
  27.     count2=1023;
  28.     count3=2048;
  29.     printf("I am child1 , tid=%x ,count (%p) = %10d,count2(%p) = %10llu,count3(%p) = %6d\n",tid,&count,count,&count2,count2,&count3,count3);
  30.     asm volatile("movl %%gs:0, %0;"
  31.             :"=r"(b) /* output */
  32.             );

  33.     printf("I am child1 , GS address %x\n",b);
  34.     
  35.     sleep(2);
  36.     printf("thread %x returns %x\n",tid,pthread_getspecific(key));
  37.     sleep(50);
  38. }

  39. void * child2(void *arg)
  40. {
  41.     int b;
  42.     int tid=pthread_self();

  43.     printf("I am the child2 pthread_self return %p gettid return %d\n",tid,gettid());

  44.     char* key_content = malloc(8);
  45.     if(key_content != NULL)
  46.     {
  47.         strcpy(key_content,"ABCDEFG");
  48.     }
  49.     pthread_setspecific(key,(void *)key_content);
  50.     count=88888888;
  51.     count2=1024;
  52.     count3=2047;
  53.     printf("I am child2 , tid=%x ,count (%p) = %10d,count2(%p) = %10llu,count3(%p) = %6d\n",tid,&count,count,&count2,count2,&count3,count3);
  54.     
  55.     
  56.     asm volatile("movl %%gs:0, %0;"
  57.             :"=r"(b) /* output */
  58.             );

  59.     printf("I am child2 , GS address %x\n",b);
  60.     
  61.     sleep(1);
  62.     printf("thread %x returns %x\n",tid,pthread_getspecific(key));
  63.     sleep(50);
  64. }


  65. int main(void)
  66. {
  67.     int b;
  68.     pthread_t tid1,tid2;
  69.     printf("hello\n");

  70.     
  71.     pthread_key_create(&key,echomsg);

  72.     asm volatile("movl %%gs:0, %0;"
  73.             :"=r"(b) /* output */
  74.             );

  75.     printf("I am the main , GS address %x\n",b);
  76.     
  77.     pthread_create(&tid1,NULL,child1,NULL);
  78.     pthread_create(&tid2,NULL,child2,NULL);

  79.     printf("pthread_create tid1 = %p\n",tid1);
  80.     printf("pthread_create tid2 = %p\n",tid2);

  81.     sleep(60);
  82.     pthread_key_delete(key);
  83.     printf("main thread exit\n");
  84.     return 0;
  85. }

    這是一個比較綜合的程序,因為我下面要多次從不同的側面分析。對於現在,我們要展示的是進程 線程 線程組的關系。在一個終端運行編譯出來的test2程序,顯示的信息如下:

     另一個終端看ps信息,ps顯示的信息如下:

    直接ps,是看不到我們創建的線程的。只有3658一個進程。當我們采用ps -eLf的時候,我們看到了三個線程3658/3659/3660,或者稱之為輕量級進程(LWP)。Linux到底是怎么看待這三者的關系的呢:
    Linux下多線程程序,一般都是有一個主進程通過調用pthread_create創建了一個或者多個子線程,如同我們的程序,主進程在main中創建了兩個子進程。那么Linux到底是怎么看待這些事情的呢?

  1.     pid_t pid;
  2.     pid_t tgid;
  3.      ...
  4.     struct task_struct *group_leader; /* threadgroup leader */

   上面三個變量是進程描述符的三個成員變量。pid字面意思是process id,其實叫thread id會更合適。tgid 字面含義是thread group ID。對於存在多個線程的程序而言,每個線程都有自己的pid,沒錯pid,如同我們例子中的3658/3659/3660,但是都有個共同的線程組ID (TGID):3658 。
   好吧,我們再重新說一遍,對於普通進程而言,我們可以稱之為只有一個LWP的線程組,pid是它自己的pid,tgid還是它自己,線程組里面只有他自 己一個光桿司令,自然group_leader也是它自己。但是多線程的進程(線程組更恰當)則不然。開天辟地的main函數所在的進程會有自己的 PID,也會有也TGID,group_leader,都是他自己。注意,它自己也是LWP。后面他使用ptherad_create創建了2個線程,或 者LWP,這兩個新創建的線程會有自己的PID,但是TGID會沿用創建自己的那個進程的TGID,group_leader也會尊創建自己的進程的進程 描述符(task_struct)為自己的group_leader。copy_process函數中有如下代碼:

  1.     p->pid = pid_nr(pid);
  2.     p->tgid = p->pid;//普通進程
  3.     if (clone_flags & CLONE_THREAD)
  4.         p->tgid = current->tgid;//線程選擇叫起它的進程的tgid作為自己的tgid
  5.     ....
  6.     p->group_leader = p;//普通進程
        INIT_LIST_HEAD(&p->thread_group);
  7.     ...
  8.     if (clone_flags & CLONE_THREAD) {
           current->signal->nr_threads++;
           atomic_inc(¤t->signal->live);
           atomic_inc(¤t->signal->sigcnt);
           p->group_leader = current->group_leader;//線程選擇叫起它的進程作為它的group_leader
           list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
    }

   OK,ps -eLf中有個字段叫NLWP,就是線程組中LWP的個數,對於我們的例子,main函數所在LWP+兩個線程 = 3.
   我們傳說的getpid函數,本質取得是進程描述符的TGID,而gettid系統調用,取得才是每個LWP各自的PID。請看上面的圖片輸出,上面連個線程gettid返回的是3873和3874,是自己的PID。稍微有點毀三觀
   除此外,需要指出的是用戶態pthread_create出來的線程,在內核態,也擁有自己的進程描述符 task_struct(copy_process里面調用dup_task_struct創建)。這是什么意思呢。意思是我們用戶態所說的線程,一樣是 內核進程調度的實體。進程調度,嚴格意義上說應該叫LWP調度,進程調度,不是以前面提到的線程組為單位調度的,本質是以LWP為單位調度的。這個結論乍 一看驚世駭俗,細細一想,其是很合理。我們為什么多線程?因為多CPU,多核,我們要充分利用多核,同一個線程組的不同LWP是可以同時跑在不同的CPU 之上的,因為這個並發,所以我們有線程鎖的設計,這從側面證明了,LWP是調度的實體。
   我們用systemtap去觀察下test2程序相關的調度:systemtap腳本如下:

  1. #! /usr/bin/env stap
  2. #
  3. #
  4. global time_offset

  5. probe begin
  6. {
  7.     time_offset = gettimeofday_us()
  8.     printf("monitor begin==========\n")
  9. }
  10. probe scheduler.cpu_off
  11. {
  12.    if(task_execname(task_next)=="test2")
  13.    {
  14.        t = gettimeofday_us();
  15.        printf("%9d : %20s(%6d)->%10s(%6d:%6d)\n",
  16.             t-time_offset,
  17.             task_execname(task_prev),
  18.             task_pid(task_prev),
  19.             task_execname(task_next),
  20.             task_pid(task_next),   #返回的是內核中的TGID
  21.             task_tid(task_next))   #返回的內核中的PID 
  22.    }
  23. }

    我們的二進制可執行程序叫做 test2, 一個終端叫起systemtap,另一個終端叫起test2,查看下輸出:

            
             
 
    上面三個LWP都是CPU友好型的,如果同屬一個線程組的多個線程(或者稱之為LWP)都是CPU消耗型,你可以看到激烈的爭奪CPU資源。
   本想繼續寫下去,無奈太長了,不想變成滾輪殺手,在下一篇寫其他內容吧。參考文獻提到的文章,非常的好,甚至提到了線程組里面信號的處理,信號不是我這篇博文的重點,所以我略過不提了。

參考文獻
1 Linux 2.6 內核中的線程組初探 (好文章,強烈推薦

轉自: http://blog.chinaunix.net/uid-24774106-id-3650136.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM