Bootstrap

Linux进程线程源码浅析

内核版本3.13

概述

       Linux内核中,进程通过数据结构task_struct(也称为进程描述符) 被表示成任务(task),不像其他的操作系统会区别进程、轻量级进程和线程(下边就统称进程吧),Linux系统用 task_struct 数据结构来表示所有的执行上下文。对于每一个进程,一个类型为task_struct的进程描述符始终存在于内存中。它包含了内核管理全部进程所需的重要信息,如调度参数、已打开的文件描述符列表等。进程描述符从进程被创建开始就一直存在于内核堆栈之中。 
       Linu对进程标识符(PID) 和任务标识符(TID)进行了区分。这两个分量都存储在任务数据结构task_struct 中。当调用clone函数创建一个新进程而不需要和旧进程共享任何信息时,会设置一个新的PID,否则,任务得到一个新的任务标识符TID,但是PID不变。这样一来,一个进程中所有的线程都会拥有与该进程中第一个线程相同的PID。

创建过程介绍

        创建一个新进程会为其创建要给新的进程描述符和用户空间,然后从父进程复制大量的内容,如子进程被赋予一个PID,并建立它的内存映射,同时它也被赋予了访问属于父进程文件的权利。然后,它的寄存器内容被初始化并准备运行。 
       当系统调用fork执行的时候,调用fork函数的进程陷入内核并且创建一个task_struct结构和其他相关的数据结构,如内核堆栈和thread_info结构。这个结构位于进程堆栈栈底固定偏移量的地方,包含一些进程参数,以及进程描述符的地址。把进程描述符的地址存储在一个固定的地方,使得Linux系统只需要进行很少的有效操作就可以找到一个运行中进程的task_struct。 
       进程描述符的主要内容根据父进程的进程描述符来填充。Linux系统只需要寻找一个可用的PID,更新进程标识符散列表的表项使之指向新的任务数据结构即可。如果散列表发生冲突,相同键值的进程描述符会被组成链表 。它会把task_struct 结构中的一些分量设置为指向任务数组中相应进程的前一/后一进程的指针。 
       理论上,现在就应该为子进程分配数据段、堆栈段,并且对父进程的段进行复制,因为fork函数意味着父、子进程之间不共享内存。其中如果代码段是只读的,可以复制也可以共享。然后,子进程就可以运行了。 但是,实际上复制内存的代价相当的昂贵,所以现代Linux系统都使用了欺骗手段。在最开始主要依赖于父进程来创建子进程用户空间,在创建的过程中所做的工作仅仅是建立mm_struct结构、vm_area_struct结构以及页目录和页表,并没有真正地复制一个物理页面。它们赋予子进程属于它自己的页表,但是这些页表都指向父进程的页面,同时把这些页面标记成只读。当子进程试图向某一页面中写入数据的时候,它会收到写保护的错误。内核发现子进程的写入行为之后,会为子进程分配一个该页面的新副本,并将这个副本标记为可读、可写,即就是为子进程分配一个对应的物理页面。通过这种方式,使得只有需要写入数据的页面才会被复制。这种机制称为写时复制机制(Copy-on-Write)。它所带来的好处就是不需要在内存中维护同一个程序的两个副本,从而节省了内存RAM。

一步一步源码分析

linux中创建进程和线程一般都是使用fork()和pthread_create(),接下来就可以对其分别使用strace命令进行追踪,确定其系统调用函数。

 
 
 
//fork创建进程strace追踪
tiany@tiany-desktop:~/program/C/pthread$ strace ./fork.o
......
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,child_tidptr=0x7fb1e8fe7a10) = 3599
 
 
//pthread_create创建线程
tiany@tiany-desktop:~/program/C/pthread$ strace ./pthread_create.o
……
clone(xchild_stack=0x7f683d393fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND| CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f683d3949d0, tls=0x7f683d394700, child_tidptr=0x7f683d3949d0) = 3878

       由strace结果可以看到,无论是fork创建进程还是pthread_create创建线程,最终都是使用系统调用clone来实现的。两者主要就是参数不一致,特别是clone_flags标志。接下来就进入内核进行深入的分析吧。先看下刚刚提到的clone_flags标志,如下。 

 
 
 
/*
* cloning flags:
*/
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD 0x00010000 /* Same thread group? */
#define CLONE_NEWNS 0x00020000 /* New namespace group? */
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED 0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
/* 0x02000000 was previously the unused CLONE_STOPPED (Start in stopped state)
and is now available for re-use. */
#define CLONE_NEWUTS 0x04000000 /* New utsname group? */
#define CLONE_NEWIPC 0x08000000 /* New ipcs */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
#define CLONE_IO 0x80000000 /* Clone io context */

 
这些flag在创建进程线程时是非常重要的,通过这些标志一般就基本上可以确定创建的是进程还是线程。接下来就真正进入内核,看看fork、vfork、clone等函数的实现,如下:

 
 
 
//fork.c  
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif

do_fork函数

实际上边的那些函数最终都是调用do_fork()函数来实现的。

 
 
 
/*
这是fork的主程序。复制进程,如果需要的话,并等待它使用VM完成
@clone_flags: 低字节指定子进程结束时发送到父进程的信号代码(通常是SIGCHILD),高位保存了其他的标志flags,如CLONE_VM
@stack_start: 用户态下,栈的起始地址
@stack_size: 为未使用(被设置为0)
@stack_parent_tidptr: 用户态下父进程的TID指针
@stack_child_tidptr: 用户态下子进程的TID指针
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
 
/*
* 确定是否以及哪些事件向追踪者报告。 当从kernel_thread或CLONE_UNTRACED被调用被显式请求时,没有事件被报告; 否则,报告是否启用了分支类型的事件
* 下边的if语句部分主要是对参数clone_flag组合的正确性进行检查,因为标志需要遵循一定的规则,若不符合,则返回错误代码
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
 
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
 
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
;