本文首先使用了接口pthread_create创建一个线程,并用strace命令追踪了接口pthread_create创建线程的步骤以及涉及到的系统调用,然后讨论了Linux中线程与进程关系,最后概述了为了实现POSIX线程,Linux内核所做的修改。
使用pthread_create创建线程
在Linux下可以使用pthread_create来创建线程,该接口声明如下:
#include <pthread.h> int pthread_create(phtread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
可以看到,我们在创建线程时,可以指定线程的属性pthread_attr_t,比如线程的分离状态属性、线程栈的大小等属性(当然需要pthread_attr_init相关接口来操作这个属性结构体),另外也可以在创建线程时,给线程入口函数传递参数arg。注意在用改接口创建新的线程时,新创建的线程可能在pthread_create函数返回之前就运行了。下面是一个简单示例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> void* thread(void* arg) { printf("This is a pthread.\n"); sleep(5); return((void *)0); } int main(int argc,char **argv) { pthread_t id; int ret; printf("pthread_start\n"); ret = pthread_create(&id,NULL,thread,NULL); printf("pthread_end\n"); if(ret != 0) { printf("Create pthread error!\n"); exit(1); } printf("This is the main process.\n"); pthread_join(id,NULL); return 0; }
编译程序获得可执行文件:
$gcc -g -lpthread -Wall -o hack_pthread_create hack_pthread_create.c
我们可以使用命令strace来跟踪线程的创建过程:
$strace ./hack_pthread_create
其中接口pthread_create相关部分的输出如下:
mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f70b6c2f000 brk(0) = 0x1fe3000 brk(0x2004000) = 0x2004000 mprotect(0x7f70b6c2f000, 4096, PROT_NONE) = 0 clone(child_stack=0x7f70b742eff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f70b742f9d0, tls=0x7f70b742f700, child_tidptr=0x7f70b742f9d0) = 64861
有上面输出可以知道,接口pthread_create创建线程的步骤如下:
1)调用mmap在堆上分配内存,大小为8392704字节,即8196KB,也就是8M+4K,比栈的空间大了4K,这4k大小为栈的警容缓冲区大小。
2)调用mprotect()设置个内存页的保护区(大小为4K),页面起始地址为0x7f70b6c2f000,这个页面用于监测栈溢出,如果对这片内存有读写操作,那么将会触发一个SIGSEGV信号。
3)调用clone()创建线程。在Linux中,该接口用来创建进程,实质上,Linux中线程是用进程来实现,具体参照下文。调用的第一个参数是栈底的地址。栈空间的内存使用,是从高位内存开始的。其中参数flags主要标记含义说明如下:
CLONE_VM表示父进程和子进程共享内存空间;也就是说,任何一个进程在内存中修改,也会影响其他进程,包括进程中执行mmap或munmap操作,也会影响其他进程。值得一提的是,fork也是调用clone来创建子进程的,它也不会设置CLONE_VM标记。
CLONE_FS表示父进程和子进程共享文件系统信息,包括文件系统根目录、当前工作目录和umask。在父进程或子进程中调用chroot,chdir和umask也会影响其他进程。
CLONE_FILES表示父进程和子进程共享相同的文件描述符表。在父进程或子进程中打开一个新的文件,或者关闭一个文件,或者用fcntl修改相关的文件flag,也会影响其他进程。
CLONE_SIGHAND表示父进程和子进程共享相同的信号处理程序表,即父进程或子进程通过sigaction修改信号的处理方式,也会影响其他进程。但是父进程和子进程各种有独立掩码,因此一个进程通过sigprocmask来阻塞或不阻塞某个信号,是不会影响其他进程的。
CLONE_THREAD用来表示子进程与父进程在同一个线程组(thread group)中。简单的说,创建的子进程对于用户空间来说就是创建一个线程。
CLONE_SYSVSEM用来表示子进程与父进程共享相同的信号量列表。
上面说的“子进程”实质就是我们创建的线程,从这些标识也能看出,进程中各个线程之间共享了那些资源。
Linux中线程与进程关系
在Linux系统中,进程虽然定义为程序的执行实例, 它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。为了让进程完成一定的工作, 进程必须至少包含一个线程。进程所维护的是程序所包含的资源(静态资源),比如:虚拟地址空间、打开的文件描述符集合、文件系统状态和信号处理程序等;线程所维护的运行相关的资源(动态资源),比如:运行栈、调度相关的控制信息、待处理的信号集等。在Linux内核中并没有线程的概念,每一个执行实体都是一个task_struct结构, 通常称之为进程。进程是一个执行单元,维护着执行相关的动态资源。同时它又引用着程序所需的静态资源。通过系统调用clone创建子进程时,可以有选择性地让子进程共享父进程所引用的资源。这样的子进程通常称为轻量级进程。linux上的线程就是基于轻量级进程,由用户态的pthread库实现的。
使用pthread时, 在用户看来, 每一个task_struct就对应一个线程, 而一组线程以及它们所共同引用的一组资源就是一个进程,但是一组线程并不仅仅是引用同一组资源就够了,它们还必须被视为一个整体,即所谓的线程组。
总之,一个进程里面可以有多个线程,这些线程由内核自动调度,并且每个线程有它自己的线程上下文(thread context),包括线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码。而进程中其他资源是所有的线程共享的,包括虚拟地址空间(代码、数据、堆、共享库)、文件系统信息、文件描述符表和信号处理程序。
Linux中线程的实现
从接口pthread_create调用的函数知道,在Linux中线程是通过进程来实现,Linux 内核为进程创建提供一个clone()系统调用,clone参数有CLONE_VM, CLONE_FILES, CLONE_SIGHAND,CLONE_THREAD等。在创建一个线程时,通过clone()的参数,新创建的进程(也称为LWP(Lightweight process))与父进程共享内存空间,文件描述符表和信号处理程序等,从而达到创建线程相同的目的。
在Linux 2.4之前,phtread线程库对应的实现是一个名叫linuxthreads的lib,但是该库并没有满足POSIX提出的那些要求,它是在用户空间实现的,在信号处理、进程调度(每个进程需要一个额外的调度线程)及多线程之间同步共享资源等方面存在一定问题。
到了Linux2.4后,phtread线程库对应的实现是NPTL(Native POSIX Thread Library),NPTL实现满足了POSIX要求。NPTL是一个1×1的线程模型,即一个线程对于一个操作系统的调度进程。NPTL的实现依赖于linux内核的修改。内核有以下相关修改:
1)在kernel增加了futex(fast userspace mutex)支持用于处理线程之间的sleep与wake。futex是一种高效的对共享资源互斥访问的算法。kernel在里面起仲裁作用,但通常都由进程自行完成。
2)在Linux 2.4中,内核有了线程组的概念,线程组中所有的线程共享一个PID,这个PID就是所谓的线程组标识(thread group identifier (TGID)),并且在task_struct结构中增加了一个字段存放这个值。如果新创建的线程是线程组中的第一个线程,即主线程,则TGID的值就是这个线程PID的值,否则TGID的值等于进程的PID(即主线程的PID)。
如果在新创建的线程中调用getpid,则返回的值就是这个TGID,即主线程的PID(也就是通常说的进程PID),要想获得线程自身在内核的ID,即tast_struct中的PID,可以调用gettid获得。
在clone系统调用中, 传递CLONE_THREAD参数(即在创建线程时)就可以把新进程的TGID设置为父进程的TGID(否则新进程的TGID会设为其自身的PID)。类似的ID在task_struct中还有两个:task->signal->pgid保存进程组的打头进程的PID,task->signal->session保存会话打头进程的PID,通过这两个id来关联进程组和会话。有了TGID, 内核或相关的shell程序就知道某个tast_struct是代表一个进程还是代表一个线程, 也就知道在什么时候该展现它们, 什么时候不该展现(比如在ps的时候, 线程通常就不会展现了,但是加上选项-L就会显示)。
在进程中任何一个线程中执行类似的 execve的函数,除了主线程外,其他线程都会终止,并且新的程序在主线程中执行。
注意上面讨论的id与线程的pthread_t完全不相关的,并且在大多数以PID作为参数或作用在进程上的系统调用,都会把这个PID当成TGID会把操作作用到整个线程组上(进程上)。
3)为了应付"发送给进程的信号"和"发送给线程的信号", task_struct里面维护了两套signal_pending, 一套是线程组共享的, 一套是线程独有的。通过kill发送的信号被放在线程组共享的signal_pending中, 可以由任意一个线程来处理;通过pthread_kill发送的信号(pthread_kill是pthread库的接口, 对应的系统调用中tkill)被放在线程独有的signal_pending中, 只能由本线程来处理。当线程停止/继续, 或者是收到一个致命信号时, 内核会将处理动作施加到整个线程组中。
参考资料
http://my.oschina.net/sincoder/blog/132303
http://blog.csdn.net/yetyongjin/article/details/7673837
http://hi.baidu.com/_kouu/item/282b80a933ccc3a829ce9dd9
http://hi.baidu.com/_kouu/item/358716f4c5f0cd0ec6dc45d0
http://man7.org/linux/man-pages/man2/clone.2.html
http://stackoverflow.com/questions/9154671/distinction-between-processes-and-threads-in-linux