浅析Linux线程的创建

    本文首先使用了接口pthread_create创建一个线程,并用strace命令追踪了接口pthread_create创建线程的步骤以及涉及到的系统调用,然后讨论了Linux中线程与进程关系,最后概述了为了实现POSIX线程,Linux内核所做的修改。

  使用pthread_create创建线程

  在Linux下可以使用pthread_create来创建线程,该接口声明如下:

#include 
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 
#include 
#include 
#include 
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表示父进程和子进程共享内存空间;也就是说,任何一个进程在内存中修改,也会影响其他进程,包括进程中执行mmapmunmap操作,也会影响其他进程。值得一提的是,fork也是调用clone来创建子进程的,它也不会设置CLONE_VM标记。

CLONE_FS表示父进程和子进程共享文件系统信息,包括文件系统根目录、当前工作目录和umask。在父进程或子进程中调用chrootchdirumask也会影响其他进程。

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线程库对应的实现是一个名叫linuxthreadslib,但是该库并没有满足POSIX提出的那些要求,它是在用户空间实现的,在信号处理、进程调度(每个进程需要一个额外的调度线程)及多线程之间同步共享资源等方面存在一定问题。

    到了Linux2.4后,phtread线程库对应的实现是NPTL(Native POSIX Thread Library)NPTL实现满足了POSIX要求。NPTL是一个1×1的线程模型,即一个线程对于一个操作系统的调度进程。NPTL的实现依赖于linux内核的修改。内核有以下相关修改:

    1)在kernel增加了futex(fast userspace mutex)支持用于处理线程之间的sleepwakefutex是一种高效的对共享资源互斥访问的算法。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_killpthread库的接口对应的系统调用中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

你可能感兴趣的:(Linux,线程,进程,Linux,创建,实现)