linux 用户态栈 fork,fork, vfork, clone,pthread_create,kernel_thread

gon1.4 线程创建的Linux实现

我们知道,Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和fork

(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行的,因

此,do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文

件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统

调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用pthread_create()来创建线程时,则最终设置了所

有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的"进程"拥有共享的运行环境,只有栈是独立的,由

__clone()传入。

Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。

pthread

库使用一个管理线程(__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号

(比如Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。

fork, vfork,

clone,pthread_create,kernel_thread

fork,vfork,clone,都是系统调用,以前还以为是前面两个是clone的封装,实际上前三个都是系统调用,pthread_create是对clone的封装,kernel_thread用于创建内核线程

fork 在内核中调用

do_fork(SIGCHLD, regs.esp, &regs,

0, NULL, NULL)

vfork:

do_fork(CLONE_VFORK |

CLONE_VM | SIGCHLD, regs.esp, &regs, 0, NULL, NULL)

clone:

do_fork(clone_flags, newsp,

&regs, 0, parent_tidptr, child_tidptr)

其中 clone_flags = regs.ebx;

newsp = regs.ecx;

parent_tidptr = (int __user *)regs.edx;

child_tidptr = (int __user *)regs.edi;

pthread_create:

pid =

__clone(__pthread_manager_event,

(void **) __pthread_manager_thread_tos,

CLONE_VM | CLONE_FS | CLONE_FILES |

CLONE_SIGHAND,

(void *)(long)manager_pipe[0]);

kernel_thread:

do_fork(flags |

CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL,

NULL)

在执行这个函数之前,在栈中对regs作了初始化,把自己的参数塞进起,并设regs.eip=kernel_thread_helper,具体流程参见前一篇文章。

可见他们的不同主要在于flag的不同,为什么前两个都有SIGCHLD,把保护用户态寄存器的地址传递是为什么?进程的到底是怎么切换的?

先看do_fork,首先分配一个PID,调用copy_process()进行具体的拷贝,如果flag里有CLONE_VFORK,将父进程放入一个等待队列,所以子进程先运行。

copy_process()

This creates a new process as a copy of the old

one, but does not actually start it yet.It copies the registers,

and all the appropriate parts of the process environment.

p = dup_task_struct(current);  为子进程分配task_struct和内核stack.

然后copy_files(),copy_fs()进行拷贝。

copy_thread,把用户传进来的通用寄存器进行拷贝给子进程并设置,这儿很关键

childregs = task_pt_regs(p);

*childregs = *regs;

childregs->eax = 0;

childregs->esp = esp;

p->thread.esp = (unsigned long) childregs;

p->thread.esp0 = (unsigned long) (childregs+1);

p->thread.eip = (unsigned long) ret_from_fork;

可见传入的用户态通用寄存器附给了子进程,并置eax=0所以返回的pid是0,估计是pt_regs里没有esp,所以单独赋值了。task_struct的thread字段记录了进程特定于cpu的信息,但切换到子进程的时候,就把thread中esp,eip弹出,所以子进程就可以通过ret_from_fork返回运行了。

而ret_from_fork

asmlinkage void ret_from_fork(void)

__asm__("ret_from_fork");

不明白。

至于前面说道的SIGCHLD,估计是设置子进程挂了的时候给父进程发个SIGCHLD信号。也没看到在哪儿设。

所谓创建一个进程,就是创建task_struct和内核stack,并把stack里的pt_regs设置下,返回的时候好恢复到调用点,并选择性的继承父进程的资源。

内核线程

内核线程和用户级线程的区别在于内核线程没有独立的地址空间(实际上它的mm指针被设置为NULL).它也可以被调度也可以被抢占。内核线程也只能由其他内核线程创建。方法如下:int

kernel_thread(int (*fn)(void *), void *arg, unsigned long

flags).新的任务也是通过像普通的clone()系统调用传递特定的flags参数而创建的。上面函数返回时,父进程退出,并返回一个子线程task_struct的指针。子进程开始运行fn指向的函数,arg是运行时需要用到的参数。一个特殊的clone标志CLONE_KERNEL定义了内核线程常用到参数标志:CLONE_FS,

CLONE_FILES, CLONE_SIGHAND.大部分的内核线程把这个标志传递给它们的flags参数。

针对线程模型的两大意义,分别开发出了核心级线程和用户级线程两种线程模型,分类的标准主要是线程的调度者在核内还是在核外。前者更利于并发使用多处理器的资源,而后者则更多考虑的是上下文切换开销。在目前的商用系统中,通常都将两者结合起来使用,既提供核心线程以满足smp系统的需要,也支持用线程库的方式在用户态实现另一套线程机制,此时一个核心线程同时成为多个用户态线程的调度者。正如很多技术一样,"混合"通常都能带来更高的效率,但同时也带来更大的实现难度,出于"简单"的设计思路,Linux从一开始就没有实现混合模型的计划,但它在实现上采用了另一种思路的"混合"。

在线程机制的具体实现上,可以在操作系统内核上实现线程,也可以在核外实现,后者显然要求核内至少实现了进程,而前者则一般要求在核内同时也支持进程。核心级线程模型显然要求前者的支持,而用户级线程模型则不一定基于后者实现。这种差异,正如前所述,是两种分类方式的标准不同带来的。

当核内既支持进程也支持线程时,就可以实现线程-进程的"多对多"模型,即一个进程的某个线程由核内调度,而同时它也可以作为用户级线程池的调度者,选择合适的用户级线程在其空间中运行。这就是前面提到的"混合"线程模型,既可满足多处理机系统的需要,也可以最大限度的减小调度开销。绝大多数商业操作系统(如Digital

Unix、Solaris、Irix)都采用的这种能够完全实现POSIX1003.1c标准的线程模型。在核外实现的线程又可以分为"一对一"、"多对一"两种模型,前者用一个核心进程(也许是轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成,而后者则完全在核外实现多线程,调度也在用户态完成。后者就是前面提到的单纯的用户级线程模型的实现方式,显然,这种核外的线程调度器实际上只需要完成线程运行栈的切换,调度开销非常小,但同时因为核心信号(无论是同步的还是异步的)都是以进程为单位的,因而无法定位到线程,所以这种实现方式不能用于多处理器系统,而这个需求正变得越来越大,因此,在现实中,纯用户级线程的实现,除算法研究目的以外,几乎已经消失了。

一.linux进程,轻量级进程,线程

最初的进程定义都包含程序、资源及其执行三部分,其中程序通常指代码,资源在操作系统层面上通常包括内存资源、IO资源、信号处理等部分,而程序的执行通常理解为执行上下文,包括对cpu的占用,后来发展为线程。在线程概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,数据内存,甚至代码,这就发展出轻量进程的概念。

Linux 线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行,

而调度交给内核处理。轻量级进程和普通进程的区别在于:前者没有独立的用户空间(内核态线程无用户空间,用户态线程共享用户空间),而普通进程有独立的内存空间

二.实现

Linux的进程,线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了

三个系统调用__clone()和fork()以及vfort(),最终都用不同的参数调用do_fork()核内API。

do_fork()

提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、

CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。

Linux下不管是多线程还是多进程编程,linux通过clone()系统调用实现产生进程或者线程。无论是fork(),还是vfork(),__clone()最后都根据各自需要的参数标志去调用clone().然后有clone()去调用do_fork().这样一说,最终都是用do_fork实现的多进程编程,只是进程创建时的参数不同,从而导致有不同的共享环境。

Clone()

clone函数功能强大,带了众多参数。clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。先有必要说下这个函数的结构

int clone(int (*fn)(void *), void *child_stack,

int flags, void *arg);

这里fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是所谓的“剧本",

child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源,

arg就是传给子进程的参数)。下面是flags可以取的值

CLONE_PARENT

创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”

CLONE_FS子进程与父进程共享相同的文件系统,包括root、当前目录、umask

CLONE_FILES子进程与父进程共享相同的文件描述符(file

descriptor)表

CLONE_NEWNS

在新的namespace启动子进程,namespace描述了进程的文件hierarchy

CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal

handler)表

CLONE_PTRACE 若父进程被trace,子进程也被trace

CLONE_VFORK父进程被挂起,直至子进程释放虚拟内存资源

CLONE_VM子进程与父进程运行于相同的内存空间

CLONE_PID 子进程在创建时PID与父进程一致

CLONE_THREAD Linux

2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

fork(),vfork(),_clone(),exec()

1.fork()

fork

创造的子进程复制了父亲进程的资源,子进程是父进程的一个拷贝。具体说,子进程从父进程那得到了数据段和堆栈段,但不是与父进程共享而是单独分配内存.

在Linux下使用了一种叫做写时拷贝(copy-on-write)页实现。这种技术原理是:内存并不复制整个进程地址空间,而是让父进程和子进程共享同一拷贝,只有在需要写入的时候,数据才会被复制当使用fork系统调用产生多进程时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境。

调用clone时候flag指定为SIGCHLD和清除所有CLONE_XX标志调用clone().然后有clone()去调用do_fork().这样一说,我想大家明白我的意思了,问题的关键纠结于do_fork(),它定义在kernel/fork.c中,完成了大部分工作,该函数调用copy_process()函数,然后让进城开始运行,copy_precess()函数完成的工作很有意思:

1.调用dup_task_struct()为新进程创建一个内核栈,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数。然后让进程开始运行。从函数的名字dup就可知,此时,子进程和父进程的描述符是完全相同的。

2.检查这个新创建的的子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制。

3.现在,子进程开始使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。

4.接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。

5.调用copy_flags()以更新task_struct的flags成员,表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec函数的PF_FORKNOEXEC标志。

6.调用get_pid()为新进程获取一个有效的PID.

7.根据传递给clone()的参数标志,拷贝或共享打开的文件,文件系统信息,信号处理函数。进程地址空间和命名空间等。一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里.

8.让父进程和子进程平分剩余的时间片

9.最后,作扫尾工作并返回一个指向子进程的指针。

经过上面的操作,再回到do_fork()函数,如果copy_process()函数成功返回。新创建的子进程被唤醒并让其投入运行。内核有意选择子进程先运行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。如果父进程首先执行的话,有可能会开始向地址空间写入。

2.vfork()

vfork创建新进程的主要目的在于用exec函数执行另外的程序,实际上,在没调用exec或exit之前子进程的运行中是与父进程共享数据段的。在vfork调用中,子进程先运行,父进程挂起,直到子进程调用exec或exit,在这以后,父子进程的执行顺序不再有限制。调用clone时候flag指定为SIGCHLD和CLONE_VM,

CLONE_VFORK。

vfork()不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec(),子进程不能向地址空间写入。按照刚才的方法,分析一下vfork(),它是通过向clone()系统调用传递一个特殊标志来进行的,过程如下:

1.在调用copy_process时,task_struct的vfor_done成员被设置为NULL

2.在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址。

3.子进程开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done指针向它发送信号。

4.在调用mm_release()时,该函数用于进程退出内存地址空间,如果vfork_done不为空,会向父进程发送信号。

5.回到do_fork(),父进程醒来并返回。

3.__clone()

当使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的"进程"拥有共享的运行环境,只有栈是独立的,由

传入。我们说在linux中,线程仅仅是一个使用共享资源的轻量级进程。调用clone时候flag指定为CLONE_VM

| CLONE_FS | CLONE_FILES | CLONE_SIGHAND。

4.__clone()

该函数从一个bin文件加载一个app,它不创建新的进程,只是把当前替换成新的bin程序文件,并执行它。因为调用e

x e c并不创建新进程,所以前后的进程I D并未改变。e x e c只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

Linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,一定程度上也弥补了这一缺陷。目前最流行的线程机制LinuxThreads所采用的就是线程-进程"一对一"模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。Linux-LinuxThreads的运行机制正是本文的描述重点

浅析linux中fork函数

作者:吴新武,华清远见嵌入式学院讲师。

Linux通过clone()系统调用实现fork()、vfork()和__clone()库函数创建新的进程,这个调用通过一系列的参数标志来指明父子进程的共享资源,最终将各自的参数标志位传递给clone,由clone()去调用do_fork()来实现创建新的进程的目的。

do_fork的实现源码在kernel/fork.c文件中,其主要的作用就是复制原来的进程成为另一个新的进程,它完成了整个进程的创建过程。do_fork()的实现主要由以下5个步骤,在分析代码之前,先了解以下do_fork()函数的参数的含义,其参数的含义如下。

clone_flags:该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合。通过clone标志可以有选择的对父进程的资源进行复制。例如CLONE_VM表示共享内存描述符合所有的页表;

CLONE_FS共享根目录和当前工作目录所在的表以及权限掩码。

statck_start:子进程用户态堆栈的地址;

regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中;

stack_size:未被使用,通常被赋值为0;

parent_tidptr:父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义;

child_tidptr:子进程在用户态下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义。

函数原型及实现为:

long do_fork(unsigned long clone_flags,unsigned

long stack_start, struct pt_regs *regs,unsigned long stack_size,int

__user *parent_tidptr, int __user *child_tidptr)

{

struct

task_struct *p;

p

= copy_process(clone_flags, stack_start, regs, stack_size,

child_tidptr,

NULL, trace); (1)

if

(!IS_ERR(p)) {

if

(clone_flags & CLONE_VFORK) {

p->vfork_done

= &vfork;

init_completion(&vfork);

}

……

}

一、首先调用copy_process()函数

copy_process()函数实现了进程的大部分拷贝工作。

static struct task_struct *copy_process(unsigned

long clone_flags, unsigned long stack_start,struct pt_regs

*regs,unsigned long stack_size, int __user *child_tidptr, struct

pid *pid,int trace)

{

//对传入的clone_flag进行检查

//为新进程创建一个内核栈、thread_info结构和task_struct;其值域当前进程的值完全相同(父子进程的描述符此时也相同)

p = dup_task_struct(current);

//判断是否超出进城用户可以拥有的总进城数量,检查是否有权对指定的资源进行操作

if

(atomic_read(&p->real_cred->user->processes)

>=

task_rlimit(p,

RLIMIT_NPROC)) {

if

(!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE)

&&

p->real_cred->user

!= INIT_USER)

goto

bad_fork_free;

}

//在task_struct结构中有一个指针user,该指针指向一个user_struct结构,一个用户的多个进程可以通过user指针共享该用户的资源信息,该结构定义在include/linux/sched.h中,

retval = copy_creds(p, clone_flags);

//copy_creds函数中调用:

//检查创建的进程是否超过了系统进程总量

if (nr_threads >= max_threads)

goto

bad_fork_cleanup_count;

//获得进程执行域

if

(!try_module_get(task_thread_info(p)->exec_domain->module))

goto

bad_fork_cleanup_count;

//调用copy_flags函数更新task_struct结构中flags成员。表明进程是否拥有超级用户权限的PF_SUPERPPRIV标志被清除,表明进程还没有exec()的PF_FORKNOEXEC被设置

copy_flags(clone_flags, p);

//根据clone的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间,代码如下图所示。

//为新进程获取一个有效的PID,调用pid =

alloc_pidmap();紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理,用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。

pid = alloc_pid(p->nsproxy->pid_ns);

//父子进程平分共享的时间片

sched_fork(p, clone_flags);

//返回子进程的指针。

return p;

}

再回到do_fork函数,如果copy_process函数成功返回,新创建的子进程被唤醒并投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能开始向地址空间写入。

二、init_completion(&vfork);

如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。完成量的作用是,直到任务A发出信号通知任务B发生了某个特定事件时,任务B才会开始执行;否则任务B一直等待。我们知道,如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。究其原因就是此处vfork完成量所起到的作用:当子进程调用exec函数或退出时就向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。此处的代码只是对完成量进行初始化,具体的阻塞语句则在后面的代码中有所体现。

三、检查子进程是否设置了CLONE_STOPPED标志。

设置了CLONE_STOPPED标志通过sigaddset函数为子进程增加挂起信号。signal对应一个unsigned

long类型的变量,该变量的每个位分别对应一种信号。具体的操作是,将SIGSTOP信号所对应的那一位置1。

如果子进程并未设置CLONE_STOPPED标志,那么通过wake_up_new_task函数使得父子进程之一优先运行;否则,将子进程的状态设置为TASK_STOPPED。

四、检查CLONE_VFORK标志被设置

如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。

五、返回pid

return nr; //其中nr最后一次赋值为:nr =

task_pid_vnr(p);即子进程的pid号。

至此,fork函数的系统调用过程结束,子进程和父进程各返回一次,子进程返回值为0,父进程返回值为子进程的pid号。应用程序可通过fork的返回值来判断是在子进程中还是父进程中,从而实现多进程程序的编写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值