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中, 只能由本线程来处理。当线程停止/继续, 或者是收到一个致命信号时, 内核会将处理动作施加到整个线程组中。

时间: 2024-10-12 12:57:22

Linux进程创建过程详解的相关文章

Linux进程创建过程

相关学习资料 linux内核设计与实现+原书第3版.pdf(3.3章) 深入linux内核架构(中文版).pdf 深入理解linux内核中文第三版.pdf <独辟蹊径品内核Linux内核源代码导读> http://www.yanyulin.info/pages/2013/11/linux0.html http://blog.csdn.net/ddna/article/details/4958058 http://www.cnblogs.com/coolgestar02/archive/2010

Linux进程管理工具详解:htop、glances、dstat

1.进程相关知识     1.操作系统的功能:文件系统.网络功能.进程管理.内存管理.安全功能.驱动程序(Linux是一个多任务的操作系统,而且是抢占式的多任务操作系统);.OS其实就是提供虚拟的计算机,进而能够将有限的资源借助于"保护"机制分配多个同时运行的程序,即"进程"使用,从而实现了所谓的多任务 2.程序执行环境有2种: 内核模式:cpu运行内核级指令 用户模式:cpu运行普通指令 3.应用程序运行普通指令,其实是直接运行于cpu上:应用程序运行特权指令,是

Linux 进程间通讯详解二

消息队列 --消息队列提供了本机上从一个进程向另外一个进程发送一块数据的方法 --每个数据块都被认为有一个类型,接收者进程接收的数据块可以有不同的类型值 --消息队列也有管道一样的不足,就是每个消息的最大长度是由上限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有一个上限(MSGMNI) 消息大小的三大限制 cat /proc/sys/kernel/msgmax --最大消息的长度限制(65536) cat /proc/sys/kernel/msg

深入了解linux操作系统引导过程详解

Linux操作系统的引导过程:开机自检.mbr引导.grub菜单.加载linux内核.init进程初始化. 1.开机自检,检测硬件后根据启动顺序将控制权交给本机硬盘 2.mbr引导,根据硬盘的主引导记录,将控制权转交给引导分区或者启动菜单 3.grub菜单,启动菜单提供给用户选择进入不同的操作系统(例如windows)的界面.默认进入linux将系统控制权转交给内核. 4.加载linux内核,内核负责分配调度硬件资源与系统程序,内核接过系统控制权,开始管理整个Linux系统. 5.init进程初

进程切换过程详解

/* 注:在学习内核的时候有一个困难,那就是任何一个模块都不是孤立的,比如进程的调度就设计到中断处理.信号处理还有进程上下文的切换等等.作为一个初学者,想一下子把操作系统的整个运行过程都清晰地展现在脑海是不现实的.尽管如此,每个模块还是有它所关注的焦点,我们所采取的策略是把整个操作系统分为几个大模块,比如:进程的管理.内存管理.文件系统等等.然后把这些大模块进一步分解成一个个小模块,比如进程的管理可以细分为进程的创建.进程的切换.系统调用的处理.信号的处理等等.在分析每一个模块时,先把其他的模块

Linux系统开机过程详解

从按下电源键开始,到登录成功,计算机都做了哪些事儿呢?且听我细细道来. 其过程可以总结如下: 一 BIOS 当按下计算机电源键时,计算机便会读取主板的BIOS(Basic Input/Output System)中存储的程序, BIOS允许你从软盘.光盘或者硬盘中选择一个来"存储设备"启动计算机. 二 MBR 当你选择"存储设备"后,计算机便读取其起始的512 bytes数据,即主引导记录MBR (master boot record), MBR会告诉计算机从该设备

Android Phone进程启动过程详解

之前解决一个开机搜网慢的问题时,发现由于Phone进程起来以后才会主动连接RILD,因而在一定程度上Phone进程启动的时间会影响网络状态注册的快慢.适当的将Phone进程提前,可以将网络注册时间提前一点,让状态栏中信号显示的时间提前.那么,Android中作为系统的核心进程之一,Phone进程是如何启动的了? RIL运行机制请参考: http://blog.csdn.net/jason_wzn/article/details/53232022 Telephony最开始创建的是PhoneFact

Linux进程管理命令详解

一.ps命令:只显示命令运行这一刻的进程的状态  (一) /proc/:内核中的状态信息:通过这个接口输出给用户        1. 内核参数:            可设置参数:可设置其值从而调整内核运行特性的参数:/proc/sys/            状态变量:其用于输出内核中统计信息或状态信息,仅用于查看:        2. 参数:参数被模拟成文件系统类型:  (二)进程: /proc/#:在proc目录下都有一个以进程号命名的目录,这个目录存放了进程当前状态的各种信息      

关于Linux操作系统的开机过程详解

由 于操作系统正在变得越来越复杂,所以开机引导和关机下电的过程也越来越智能化.从简单的DOS系统转移到 Windows NT系统,人们已经亲身感受到了这些变化--这已不仅仅是核心操作系统的启动引导和关闭了,还包括必须要同时启动或者关闭相当数量的服务项目.类似于 Windows NT,Linux系统启动过程需要打开的服务项目也是数量极大的. 这里,我们假设大家已经熟悉其它操作系统的引导过程,了解硬件的自检引导步骤,就只从Linux操作系统的引导加载程序(对个人电脑而言通常是LILO)开始,介绍Li