内核版本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_BACKWARDSSYSCALL_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)#elseSYSCALL_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;elsetrace = 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

本文详细介绍了Linux内核中进程和线程的创建过程,特别是通过do_fork和copy_process函数实现的写时复制(Copy-on-Write)机制,以及在创建过程中如何使用clone函数和不同的clone_flags标志来区分进程和线程。
最低0.47元/天 解锁文章
1万+

被折叠的 条评论
为什么被折叠?



