浅析Linux线程的创建

本文首先使用了接口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

时间: 2024-12-24 22:27:13

浅析Linux线程的创建的相关文章

浅析Linux线程中数据

本文首先概述了线程中有哪些数据私有的,以及进程中哪些数据线程是共享的,然后详细分析了线程在用户空间中的数据,最后通过一个多线程程序来分析线程中的数据分布. 概述 线程包含了表示进程内执行环境必需的信息,其中包括进程中标识的线程ID.一组寄存器值.栈.调度优先级和策略.信号屏蔽字(每个线程有自己的信号屏蔽字,但对某个信号的处理方式是进程中所有线程共享的).errno变量(每个线程都自己的局部errno)以及线程私有数据.进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本.程序的全局内

Linux多线程程序设计------创建线程

1.创建线程 #include<pthread.h> int pthread_create(pthread_t* tidp,const pthread_attr_t *attr,void*(*start_rtn)(void),void*arg) tidp:线程id attr:线程属性(通常为空) start_rtn:线程要执行的函数 arg:start_rtn的参数 Linux多线程程序设计------创建线程,布布扣,bubuko.com

Linux C线程的创建和使用 [转]

1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中 去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多 线程就意味着多进程.现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux. 为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题. 使用多线程的理由之一是和进程相比

Linux 线程浅析

进程和线程的区别与联系 在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体. 为了让进程完成一定的工作,进程必须至少包含一个线程. 进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统.操作系统会以进程为单位,分配系统资源,所以我们也说,进程是资源分配的最小单位. 线程存在与进程当中,是操作系统调度执行的最小单位.说通俗点

C++ Linux 多线程之创建、管理线程

线程就是,在同一程序同一时间内允许执行不同函数的离散处理队列. 这使得一个长时间去进行某种特殊运算的函数在执行时不阻碍其他的函数变得十分重要. 线程实际上允许同时执行两种函数,而这两个函数不必相互等待. 一旦一个应用程序启动,它仅包含一个默认线程. 此线程执行 main() 函数. 在 main()中被调用的函数则按这个线程的上下文顺序地执行. 这样的程序称为单线程程序. 反之,那些创建新的线程的程序就是多线程程序. 他们不仅可以在同一时间执行多个函数,而且这在如今多核盛行的时代显得尤为重要.

linux内核线程的创建与销毁

linux将创建内核线程的工作交给了一个专门的内核线程kthreadd来完成,该线程会检查全局链表kthread_create_list,如果为NULL,就会调schedule()放弃cpu进入睡眠状态,否则就取下该链表中的一项创建对应的线程.本文就从khtreadd内核线程的创建开始来展示一下内核线程的创建过程.1 kthreaddlinux2.6.30,创建内核线程是通过kethradd内核守护线程来完成的,虽然机制上有所变化,但是最终还是调用do_fork来完成线程的创建.Kthreadd

linux线程创建、取消、属性设置

创建线程: #include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) 参数:第一个参数为指向线程标识符的指针. 第二个参数用来设置线程属性. 第三个参数是线程运行函数的起始地址. 第四个参数是线程运行函数的参数. int pthread_join(pthread_t thread, void

辛星浅析Linux中的线程和进程

进程和线程都是由操作系统的程序运行的基本单元,系统利用该单元实现系统对应用的并发性.一个进程至少有一个线程,一个线程也至少属于一个进程.进程和线程的区别在于:线程的划分尺度小于进程,使得多线程程序的并发性更高. 而且进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率.线程在执行过程中与进程还是有区别的.每个独立的线程有一个程序运行的入口.顺序执行序列和程序的出口. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行,但是操作系统并没有将

浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程【转】

本文转载自:http://www.cnblogs.com/qingchen1984/p/7007631.html 本篇文章主要介绍了"浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程",主要涉及到浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程方面的内容,对于浅析 Linux 中的时间编程和实现原理一—— Linux 应用层的时间编程感兴趣的同学可以参考一下. 简介: 本文试图完整地描述 Linux 系统中 C 语言编程中的时间问