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. count=666666;
  26. count2=1023;
  27. count3=2048;
  28. printf("I am child1 , tid=%x ,count (%p) = %10d,count2(%p) = %10llu,count3(%p) = %6d\n",tid,&count,count,&count2,count2,&count3,count3);
  29. asm volatile("movl %%gs:0, %0;"
  30. :"=r"(b) /* output */
  31. );
  32. printf("I am child1 , GS address %x\n",b);
  33. sleep(2);
  34. printf("thread %x returns %x\n",tid,pthread_getspecific(key));
  35. sleep(50);
  36. }
  37. void * child2(void *arg)
  38. {
  39. int b;
  40. int tid=pthread_self();
  41. printf("I am the child2 pthread_self return %p gettid return %d\n",tid,gettid());
  42. char* key_content = malloc(8);
  43. if(key_content != NULL)
  44. {
  45. strcpy(key_content,"ABCDEFG");
  46. }
  47. pthread_setspecific(key,(void *)key_content);
  48. count=88888888;
  49. count2=1024;
  50. count3=2047;
  51. printf("I am child2 , tid=%x ,count (%p) = %10d,count2(%p) = %10llu,count3(%p) = %6d\n",tid,&count,count,&count2,count2,&count3,count3);
  52. asm volatile("movl %%gs:0, %0;"
  53. :"=r"(b) /* output */
  54. );
  55. printf("I am child2 , GS address %x\n",b);
  56. sleep(1);
  57. printf("thread %x returns %x\n",tid,pthread_getspecific(key));
  58. sleep(50);
  59. }
  60. int main(void)
  61. {
  62. int b;
  63. pthread_t tid1,tid2;
  64. printf("hello\n");
  65. pthread_key_create(&key,echomsg);
  66. asm volatile("movl %%gs:0, %0;"
  67. :"=r"(b) /* output */
  68. );
  69. printf("I am the main , GS address %x\n",b);
  70. pthread_create(&tid1,NULL,child1,NULL);
  71. pthread_create(&tid2,NULL,child2,NULL);
  72. printf("pthread_create tid1 = %p\n",tid1);
  73. printf("pthread_create tid2 = %p\n",tid2);
  74. sleep(60);
  75. pthread_key_delete(key);
  76. printf("main thread exit\n");
  77. return 0;
  78. }

这是一个比较综合的程序,因为我下面要多次从不同的侧面分析。对于现在,我们要展示的是进程 线程 线程组的关系。在一个终端运行编译出来的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

Linux线程 之 线程 线程组 进程 轻量级进程(LWP)的更多相关文章

  1. Linux线程 之 线程 线程组 进程 轻量级进程(LWP) -systemtap -mysql

    http://blog.chinaunix.net/uid-24774106-id-3650136.html http://blog.itpub.net/15480802/viewspace-7627 ...

  2. linux内核——进程,轻量级进程,线程,线程组

    1.进程.轻量级进程.线程.线程组之间的关系 2.及它们的标识相关说明 一.进程.轻量级进程.线程.线程组之间的关系 借助上图说明: 进程P0有四条执行流,即线程, 主线程t0是它的第一个线程,且与进 ...

  3. Linux下的进程类别(内核线程、轻量级进程和用户进程)--Linux进程的管理与调度(四)

    本文中出现的,内核线程,轻量级进程,用户进程,用户线程等概念,如果不太熟悉, 可以参见 内核线程.轻量级进程.用户线程三种线程概念解惑(线程≠轻量级进程) Linux进程类别 虽然我们在区分Linux ...

  4. 进程、线程、轻量级进程、协程与 go 的 goroutine【转载+整理】

    本文内容 进程 线程 协程 Go 中的 goroutine 参考资料 最近,看一些文章,提到"协程"的概念,心想,进程,线程,协程,前两个很容易,任何一本关于操作系统的书都有说,开 ...

  5. 进程、线程、轻量级进程、协程和go中的Goroutine

    进程.线程.轻量级进程.协程和go中的Goroutine 那些事儿电话面试被问到go的协程,曾经的军伟也问到过我协程.虽然用python时候在Eurasia和eventlet里了解过协程,但自己对协程 ...

  6. Go语言 进程、线程、轻量级进程、协程和go中的Goroutine 那些事儿

    原文:http://www.cnblogs.com/shenguanpu/archive/2013/05/05/3060616.html 电话面试被问到go的协程,曾经的军伟也问到过我协程.虽然用py ...

  7. 进程、轻量级进程(LWP)、线程

    进程.轻量级进程(LWP).线程 进程:程序执行体,有生命期,用来分配资源的实体 线程:分配CPU的实体. 用户空间实现,一个线程阻塞,所有都阻塞. 内核实现,不会所用相关线程都阻塞.用LWP实现,用 ...

  8. 进程、线程、轻量级进程、协程与 go 的 goroutine

    本文内容 进程 线程 协程 Go 中的 goroutine 参考资料 最近,看一些文章,提到“协程”的概念,心想,进程,线程,协程,前两个很容易,任何一本关于操作系统的书都有说,开发时也经常用,但是协 ...

  9. (转)linux下进程的进程最大数、最大线程数、进程打开的文件数和ulimit命令修改硬件资源限制

    ulimit命令查看和更改系统限制 ulimit命令详解 ulimit用于shell启动进程所占用的资源,可以用来设置系统的限制 语法格式 ulimit [-acdfHlmnpsStvw] [size ...

随机推荐

  1. 【Android进阶】Android程序与JavaScript之间的简单调用

    本篇将讲解一个简单的Android与JavaScript之间的简单调用的小程序 效果图 工程结构 HTMLActivity.java代码 package com.example.javatojs; i ...

  2. Palindromes&nbsp;_easy&nbsp;version

    Time Limit: 1Sec  MemoryLimit: 64 MB Submit:165  Solved: 76 [Submit][Status][WebBoard] Description & ...

  3. 在Linux终端下使用代理访问网络(转)

    最近,需要在linux环境下使用脚本进行一些网络访问(主要是HTTP请求与文件下载),于是查阅了一些关于代理的资料. 以下是尝试的几种代理设置方法,以供参考: 一.使用wget命令进行代理访问 wge ...

  4. 不一样的味道--Html和Xml解析、格式、遍历

    很多其它内容查看官网:http://www.tinygroup.org TinyXmlParser一切以简单.有用.高速为主. 演示样例1:Xml字符串解析 比方,我们要解析一段Xml字符串,简单例如 ...

  5. ZOJ 3820 2014ACM/ICPC牡丹江司B称号

    3797714 2014 - 10 - 12 21:58 : 19 Accepted 3820 C++ 1350 70240 zz_1215 比較麻烦的一道题吧,開始的时候不停的段异常,后面知道是爆栈 ...

  6. Servlet(五岁以下儿童)web.xml一些常用的配置

    (1)lode-on-startup,这Servlet该项目启动时它将被称为(从主要的电话init办法,为了安全起见,一般不应为Servlet建立URL制图).一些数据通常被用作前处理,或使用多线程建 ...

  7. HDU 3874 离线段树

    在所有数字的统计范围,,对于重复统计只有一次 离线段树算法 排序终点坐标.然后再扫,反复交锋.把之前插入树行被删除 #include "stdio.h" #include &quo ...

  8. 【iOS7一些总结】9、与列表显示(在):列表显示UITableView

    列表显示,顾名思义它是在一个列表视图的形式显示在屏幕上的数据的内容.于ios在列表视图UITableView达到.这个类在实际应用中频繁,是很easy理解.这里将UITableView的主要使用方法总 ...

  9. 设计模式 Template Method模式 显示程序猿的一天

    转载请注明出处:http://blog.csdn.net/lmj623565791/article/details/26276093 不断设计模式~ Template Method模式 老套路,看高清 ...

  10. 无插件,直接加参数,chrome它可以模拟手机浏览器

    在目标出现,加上一些参数即可:--user-agent="mozilla/5.0 (linux; u; android 2.3.3; en-us; sdk build/ gri34) app ...