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

  • 相关阅读:
    .Net Core部署到CentOS
    Docker容器中开始.Net Core之路
    自我总结和学习表单提交的几种方式 (二)
    自我总结和学习表单提交的几种方式 (一)
    Asp.Net MVC下自定义错误页和展示错误页的几种方式
    .Net下发送邮件遇到问题及解决方案
    Asp.Net MVC CodeFirst模式数据库迁移步骤
    利用微软认知服务实现语音识别功能
    Asp.Net MVC路由生成URL过程
    针对于多线程概念的理解
  • 原文地址:https://www.cnblogs.com/cdwodm/p/5294518.html
Copyright © 2011-2022 走看看