zoukankan      html  css  js  c++  java
  • 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 ",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 ",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 ",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 ",b);
    34.     
    35.     sleep(2);
    36.     printf("thread %x returns %x ",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 ",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 ",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 ",b);
    60.     
    61.     sleep(1);
    62.     printf("thread %x returns %x ",tid,pthread_getspecific(key));
    63.     sleep(50);
    64. }


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

    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 ",b);
    76.     
    77.     pthread_create(&tid1,NULL,child1,NULL);
    78.     pthread_create(&tid2,NULL,child2,NULL);

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

    81.     sleep(60);
    82.     pthread_key_delete(key);
    83.     printf("main thread exit ");
    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========== ")
    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) ",
    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

  • 相关阅读:
    HDU 1058 Humble Numbers
    HDU 1421 搬寝室
    HDU 1176 免费馅饼
    七种排序算法的实现和总结
    算法纲要
    UVa401 回文词
    UVa 10361 Automatic Poetry
    UVa 537 Artificial Intelligence?
    UVa 409 Excuses, Excuses!
    UVa 10878 Decode the tape
  • 原文地址:https://www.cnblogs.com/cdwodm/p/5294518.html
Copyright © 2011-2022 走看看