线程模型的综述

ElvinMontgo 3年前
   <p>本文首先介绍了一些线程基础,比如并发、并行、内存分配、系统调用、POSIX线程。接着通过strace分析了线程与进程的区别。最后以Android、Golang等线程模型进行了分析。</p>    <h2>基础</h2>    <h3>1. 什么是并发(Concurrent),什么是并行(Parallels)?</h3>    <p>并发指同时进行多个计算任务。</p>    <p><img src="http://static.open-open.com/lib/uploadImg/20160624/20160624094214_933.svg"></p>    <p> </p>    <p>并行指通过切换时间片模拟进行多个计算任务。</p>    <p><img src="http://static.open-open.com/lib/uploadImg/20160624/20160624094215_224.svg"></p>    <p> </p>    <blockquote>     <p>详细可以参考<a href="/misc/goto?guid=4959674789586747541">Difference between concurrent programming and parallel programming - stackoverflow</a></p>    </blockquote>    <h3>2. OS下的内存分配、用户区与内核区</h3>    <p>在32位的Linux操作系统中,当一个进程启动后,将被分配4G的虚拟内存。内存可以分为两个空间,一个是用户空间(0~3G),另一个是内核空间(3G~4G)。其中用户空间就是代码运行的空间,比如堆栈、BSS(未初始化数据段)、DATA(已经初始化数据段)、TEXT(代码二进制段);而在内核空间中,是OS内核的映射,只有在执行syscall系统调用时,才能进行重写。</p>    <p><img alt="线程模型的综述" src="https://simg.open-open.com/show/a1dd65185ea08011df8bbfad6711faf8.jpg"></p>    <p>32 Bit OS Virtual Memory</p>    <p>在用户态中,执行用户代码,比如直接运行C程序、或者运行JVM虚拟机等。</p>    <p>在内核中,主要负责I/O(显示,层三以下的网络,FS),Memory(虚拟内存,页面替换/缓存), Process(信号、线程/进程管理,CPU调度)的管理,直接控制CPU、内存等硬件,权限(privilege)非常大;</p>    <h3>3. 系统调用中断(SCI)</h3>    <p>系统调用是用户与内核间的一个桩(stub),当在用户态执行高权限任务,需要通过系统调用切换入内核态去执行最底层任务。比如在C语言中调用<code>getTime()</code>时,大致流程如下</p>    <pre>  <code class="language-cpp">1. app method(User Application)      |      |调用stdlibc标准库      |  2. systemcall_stub(std libc)      |      |系统调用,进入内核态      |  3. system_call_table[call_number](Kernel)      |      |通过查表调用硬件函数      |  4. hardware_call(Kernel)</code></pre>    <ol>     <li>在App层面,开发者不需要自己写系统调用,系统会提供相关C标准库的SDK供开发者使用,比如开发者调用<code>getTime()</code>时,实际是使用了标准库的<code>time.h</code>头文件。</li>     <li>代码在执行时,OS自动加载标准库。比如在android的bionic库中,实际执行getTime的系统调用是<a href="/misc/goto?guid=4959674789675761653">这里</a>的平台相关的汇编代码,将系统调用的ID、参数传入内核。</li>     <li>内核通过系统调用ID进行表的索引,寻找真正的硬件调用函数</li>     <li>进行硬件相关的调用</li>    </ol>    <blockquote>     <p>在Mac下打开ActivityManager或者在Terminal中运行top,就可以显示地看到用户与系统的CPU占用</p>     <img alt="线程模型的综述" src="https://simg.open-open.com/show/3ed8868cc977deae91979a4112a8e10f.png">     <p>User and Kernel CPU usage</p>    </blockquote>    <h3>4. POSIX线程模型</h3>    <p>POSIX是IEEE P1003.1中的线程标准,目前所有的系统,甚至windows都支持POSIX。它提供了用户态下的线程编程接口,开发者在进行线程开发时,只用引用<code>pthread.h</code>头文件调用即可。程序在运行时通过系统调用,在内核中进行线程的实现。它有很多函数,比如create, exit, join, yield等,具体可以去各个平台下的libc源码/sdk中去看Header文件中方法的定义,比如android中使用biolibc中pthread.h的代码在<a href="/misc/goto?guid=4959674789757752425">这里</a>,这里的头文件是对内核线程的包装。</p>    <h2>线程与进程的区别</h2>    <p>以下特指32位下使用glibc的Linux系统中POSIX模型,即用户面模型</p>    <p>本测试基于Ubuntu 14.04 i386</p>    <h3>1. 测试代码设计</h3>    <p>1.1. 线程测试代码</p>    <pre>  <code class="language-cpp">//modified from https://computing.llnl.gov/tutorials/pthreads/samples/hello.c    //todo run:  //clang -Wall -g pthread.c -o pthread.out -lpthread  //strace -Cfo ./pthread.strace.log ./pthread.out    #include <stdio.h>  #include <stdlib.h>  #include <pthread.h>    void*  PrintHello(void *threadid)  {     long tid;     tid = (long)threadid;     printf("Hello World! It's me, thread #%ld!\n", tid);     pthread_exit(NULL);  }    int   main(int argc, char *argv[]){     pthread_t thread;     int rc = 0;     long t = 0;     printf("In main: creating thread %ld\n", t);     rc = pthread_create(&thread, NULL, PrintHello, (void *)t);     if (rc){       exit(-1);     }  }</code></pre>    <p>1.2. 进程测试代码</p>    <pre>  <code class="language-cpp">//todo run:  //clang -Wall -g fork.c -o fork.out  //strace -Cfo ./fork.strace.log ./fork.out    #include <unistd.h>    int   main(int argc, char *argv[])  {    pid_t pid;    pid = fork();    if(pid < 0){      return -1;    }        return 0;  }</code></pre>    <h3>2. 测试结果</h3>    <p>调用<code>strace</code>命令后,结果如下</p>    <p>2.1. 进程的strace路线如下</p>    <pre>  <code class="language-cpp">19948 execve("./fork.out", ["./fork.out"], [/* 68 vars */]) = 0  19948 brk(0)                            = 0x9bc000  19948 open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3  19948 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832  .....  19948 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f5adac4ca10) = 19949  ....  19949 +++ exited with 0 +++</code></pre>    <p>2.2. 线程的strace路线如下</p>    <pre>  <code class="language-cpp">21958 execve("./pthread.out", ["./pthread.out"], [/* 68 vars */]) = 0  21958 open("/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3  ....  21958 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)  21958 open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3  21958 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832  21958 fstat(3, {st_mode=S_IFREG|0755, st_size=1845024, ...}) = 0  21958 mmap(NULL, 3953344, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f34229e4000  ....  21958 clone(child_stack=0x7f34229e2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f34229e39d0, tls=0x7f34229e3700, child_tidptr=0x7f34229e39d0) = 21959  ....  21958 +++ exited with 0 +++</code></pre>    <h3>3. 测试结论</h3>    <p>通过上述的调用栈分析,可以得知均是通过调用<code>x86_64-linux-gnu</code>下的libc库,接着通过systemcall函数<code>clone()</code>实现对内核Process的控制,主要区别在函数参数中FLAG上有所不同,clone_flag指定了可以共享的资源</p>    <pre>  <code class="language-cpp">//clone flag between thread and process  //⚠️: 省略了`CLONE_`前缀    //进程的FLAG参数  flags=CHILD_CLEARTID|CHILD_SETTID|SIGCHLD    //线程的FLAG参数  flags=VM|FS|FILES|SIGHAND|THREAD|SYSVSEM|SETTLS|PARENT_SETTID|CHILD_CLEARTID</code></pre>    <p>通过对<code>clone</code>进行man<a href="/misc/goto?guid=4959674789838065675">查询</a>,</p>    <p>进程的参数解释:</p>    <ul>     <li><code>CLONE_CHILD_CLEARTID</code>: Erase child thread ID at location ctid in child memory when the child exits, and do a wakeup on the futex at that address。</li>     <li><code>CLONE_SETTLS</code>: thread local storage (TLS) area,注意这个不可移植</li>     <li><code>CLONE_SIGHAND</code>: 共享signal handlers</li>    </ul>    <p>线程的一些参数解释:</p>    <ul>     <li><code>CLONE_VM</code>: the calling process and the child process run in the same memory space. (注意这里说的是<code>memory space</code>,指通过mmap()分配的内存。再多说一点,线程中的栈内存由<code>pthread_attr_t</code>属性中的<code>pthread_attr_setstacksize</code>函数实现,默认可能为8MB,当然在实际中我们使用栈内存大多都是几KB而已;堆内存是共享的,这里不讨论)</li>     <li><code>CLONE_FS</code>: 共享文件系统,如下函数chroot(2), chdir(2), or umask(2)会被影响。</li>     <li><code>CLONE_FILES</code>: 共享file descriptor table</li>     <li><code>CLONE_SIGHAND</code>: 共享signal handlers</li>     <li><code>CLONE_THREAD</code>: 共享thread group,即有相同的PID,独立的TID;</li>     <li><code>CLONE_SYSVSEM</code>: 共享System V semaphore undo values列表,俺表示目前还不懂。</li>     <li><code>CLONE_SETTLS</code>: thread local storage (TLS) area,注意这个不可移植</li>     <li><code>CLONE_PARENT_SETTID</code>: Store child thread ID at location ptid in parent and child memory.</li>     <li><code>CLONE_CHILD_CLEARTID</code>: Erase child thread ID at location ctid in child memory when the child exits, and do a wakeup on the futex at that address。</li>    </ul>    <p>接着结合一些教科书,可以得知</p>    <table>     <thead>      <tr>       <th> </th>       <th>进程</th>       <th>线程</th>      </tr>     </thead>     <tbody>      <tr>       <td>用户层函数</td>       <td>fork()</td>       <td>pthread_create()</td>      </tr>      <tr>       <td>内核实现</td>       <td>clone()</td>       <td>clone()</td>      </tr>      <tr>       <td>内存</td>       <td>新复制的内存(Copy-on-Write),独立4G(1G+3G)</td>       <td>共享4G内存:其中8M左右的栈内存是私有的,可以通过参数决定;共享堆内存</td>      </tr>      <tr>       <td>创建耗时</td>       <td>复制的flag少,所以耗时多</td>       <td>低</td>      </tr>      <tr>       <td>上下文切换耗时</td>       <td>switching the memory address</td>       <td>几乎只有进出内核的损失</td>      </tr>      <tr>       <td>内部通信</td>       <td>IPC</td>       <td>共享的内存区(更简单)</td>      </tr>     </tbody>    </table>    <h2>高级语言对内核线程的封装实现</h2>    <p>除了通过POSIX标准外,高级语言也可以自己通过系统调用对内核的线程进行实现,主要有如下三种。</p>    <h3>1. 纯内核线程实现(1:1)</h3>    <p>此线程模型将内核线程与App线程一一对应,可以看作为一种简单的映射关系,这里的代表有POSIX线程模型(pthread),以及依赖pThread标准库的Java与Ruby(1.9+)线程模型。</p>    <p>以在Android/ArtJvm下创建线程为例,具体实现调用栈如下</p>    <pre>  <code class="language-cpp">java.lang.Thread      |  POSIX thread(user mode){      0. art.runtime.Thread::CreateNativeThread(cpp, in jvm)      1. pthread_create(pthread.h,标准库头文件)      2. bionic标准库下的so文件,进行SystemCall(libc)      3. 用户态陷入内核态  }      |  Kernal thread(kernal mode)</code></pre>    <p>可以看出,在JVM下的实现主要是对POSIX线程的包装与映射,自己本身只是做了点微小的工作,特点如下:</p>    <ol>     <li>移植性较差,需要适配各种libc库,但是由于被OS直接管理,因此在分配任务上可以充分借用内核的高效调度,能够高效利用物理核并实现真正的并行。</li>     <li>用户态与内核态切换有一定的消耗损失</li>    </ol>    <h3>2. 纯用户态实现(1:N)</h3>    <p>将线程的调度在用户态实现,也称<code>green thread</code>,自己写调度算法,可以将一个native线程映射为多个app thread(这里也可以叫做线程包),这里的代表有Ruby(1.8-),Java等老版本,特点如下:</p>    <ol>     <li>移植性好,没有切换、映射到内核的损失</li>     <li>需要自己维护Scheduler</li>     <li>由于内核并不了解调度细节,很难进行多核利用</li>    </ol>    <h3>3. 混合实现(M:N)</h3>    <p>可以同时运行M个kernel线程下管理N个app线程,比如golang。通过设置<code>GOMAXPROCS</code>个native线程,然后通过<code>go</code>关键词创建app线程,它的特点如下:</p>    <ol>     <li>调度器实现比较困难</li>     <li>通过语法糖与管道简化了并发编程,切换损失低</li>     <li>部分调度需要自己主动释放时间片</li>    </ol>    <pre>  <code class="language-cpp">golang threading model(N)      ↓      ↓ goroutine      ↓  Kernal thread model(M)</code></pre>    <blockquote>     <p>详见<a href="/misc/goto?guid=4959674789974247285">libtask</a>与许式伟的《go语言编程》</p>    </blockquote>    <h2>总结</h2>    <ol>     <li>Concurrent是多个任务同时进行,而Parallels是分享时间片</li>     <li>在启动一个程序后,将分配用户态与内核态任务,通过系统调用执行内核中的高权限任务</li>     <li>POSIX是一种线程标准,或者是一种接口,由libc库实现</li>     <li>线程与进程最大的区别在于<code>clone</code>操作时的flag不同,导致共享资源不同。最终创建、切换耗时不同;以及内存分配、内部通信复杂度不同。</li>     <li>在Java中,<code>java.lang.Thread</code>与内核线程一一对应;在某些旧版语言中,实现了一个内核线程对应多个高层线程;在golang中,通过<code>goroutine</code>实现M个内核线程对应N个高层线程;</li>    </ol>    <h2>Ref</h2>    <ol>     <li><a href="/misc/goto?guid=4959674790048349009">https://www.zhihu.com/question/21461752</a></li>     <li><a href="/misc/goto?guid=4959674790131138565">https://blog.codinghorror.com/understanding-user-and-kernel-mode/</a></li>     <li><a href="/misc/goto?guid=4959674790209202714">http://stackoverflow.com/questions/1311402/differences-between-user-and-kernel-modes</a></li>     <li><a href="/misc/goto?guid=4959674790295038326">https://zh.wikipedia.org/wiki/%E5%BF%99%E7%A2%8C%E7%AD%89%E5%BE%85</a></li>     <li><a href="/misc/goto?guid=4959674790374813084">https://www.ibm.com/developerworks/cn/linux/l-system-calls/</a></li>    </ol>    <p><br>  </p>    <p>文/<a href="/misc/goto?guid=4959674790453950938">BlackSwift</a>(简书)<br>  </p>    <p> </p>