xml地图|网站地图|网站标签 [设为首页] [加入收藏]
来自 关于我们 2019-09-28 16:43 的文章
当前位置: 新濠国际登录平台 > 关于我们 > 正文

新濠国际登录平台每个线程都拥有独立的程序计

《奔跑吧linux内核》3.1笔记,不足之处还望大家批评指正

定义

进程就是处于执行期的程序。实际上,进程就是正在执行代码的实际结果。
线程是在进程中活动的对象,每个线程都拥有独立的程序计数器,进程栈以及一组进程寄存器。内核的调度对象是线程,而不是
进程。

linux 新进程的创建

转自:

进程是Linux内核最基本的抽象之一,它是处于执行期的程序。它不仅局限于一段可执行代码(代码段),还包括进程需要的其他资源。在Linux内核中常被称作任务。

进程的两种虚拟机制

  1. 虚拟处理器:每个线程独有,不能共享
  2. 虚拟内存:同一个进程中的线程可以共享

一、背景知识:

1、进程与程序的关系:

进程是动态的,而程序是静态的;从结构上看,每个进程的实体都是由代码断和相应的数据段两部分组成的,这与程序的含义很相近;一个进程可以涉及多个程序的执行,一个程序也可以对应多个进程,即一个程序段可在不同数据集合上运行,构成不同的进程;并发性;进程具有创建其他进程的功能;操作系统中的每一个进程都是在一个进程现场中运行的。

linux中用户进程是由fork系统调用创建的。计算机的内存、CPU 等资源都是由操作系统来分配的,而操作系统在分配资源时,大多数情况下是以进程为个体的。

每一个进程只有一个父进程,但是一个父进程却可以有多个子进程,当进程创建时,操作系统会给子进程创建新的地址空间,并把父进程的地址空间的映射复制到子进程的地址空间去;父进程和子进程共享只读数据和代码段,但是堆栈和堆是分离的。

2、进程的组成:

进程控制块代码数据

进程的代码和数据由程序提供,而进程控制块则是由操作系统提供。

3、进程控制块的组成:

进程标识符进程上下文环境进程调度信息进程控制信息

进程标识符:

进程ID进程名进程家族关系拥有该进程的用户标识

进程的上下文环境:(主要指进程运行时CPU的各寄存器的内容)

通用寄存器程序状态在寄存器堆栈指针寄存器指令指针寄存器标志寄存器等

进程调度信息:

进程的状态进程的调度策略进程的优先级进程的运行睡眠时间进程的阻塞原因进程的队列指针等

当进程处于不同的状态时,会被放到不同的队列中。

进程控制信息:

进程的代码、数据、堆栈的起始地址进程的资源控制(进程的内存描述符、文件描述符、信号描述符、IPC描述符等)

进程使用的所有资源都会在PCB中描述。

进程创建时,内核为其分配PCB块,当进程请求资源时内核会将相应的资源描述信息加入到进程的PCB中,进程退出时内核会释放PCB块。通常来说进程退出时应该释放它申请的资源,如文件描述符等。为了防止进程遗忘某些资源(或是某些恶意进程)从而导致资源泄漏,内核通常会根据PCB中的信息回收进程使用过的资源。

4、task_struct 在内存中的存储:

在linux中进程控制块定义为task_struct, 下图为task_struct的主要成员:

新濠国际登录平台 1

在2.6以前的内核中,各个进程的task_struct存放在他们内核栈的尾端。这样做是为了让那些像X86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器来专门记录。由于现在使用slab分配器动态生成task_struct,所以只需在栈底或栈顶创建一个新的结果struct thread_info(在文件 asm/thread_info.h中定义)
struct thread_info{
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};

新濠国际登录平台 2

5、fork()、vfork()的联系:

Fork() 在2.6版本的内核中Linux通过clone()系统调用实现fork()。这个系统调用通过一系列的参数标志来指明父、子进程需要共享的资源。Fork()、vfork()和库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork().
do_fork()完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后进程开始运行。Copy_process()函数完成的工作很有意思:
1)、调用dup_task_struct()为新进程创建一个内核堆栈、thread_info结构和task_struct结构,这些值与当前进程的值完全相同。此时子进程和父进程的描述符是完全相同的。
2)、检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
3)、子进程着手是自己与父进程区别开来。进程描述符内的许多成员变量都要被清零或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。Task_struc中的大多数据都依然未被修改。
4)、子进程的状态被设置为TASK_UNINTRRUPTIBLE,以保证它不会被投入运行。
5)、copy_process()调用copy_flags()以更新task_struct 的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
6)、调用alloc_pid()为新进程分配一个有效的PID。
7)、根据传递给clone() 的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的因此被拷贝到这里。
8)、最后copy_process()做扫尾工作并返回一个指向子进程的指针。
在回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行(虽然总是想子进程先运行,但是并非总能如此)。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝(copy-on-write)的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。
Vfork() 除了不拷贝父进程的页表项外vfork()和fork()的功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入(在没有实现写时拷贝的linux版本中,这一优化是很有用的)。

do_fork() --> clone() --> fork() 、vfork() 、__clone() ----->exec()

clone()函数的参数及其意思如下:

CLONE_FILES 父子进程共享打开的文件
CLONE_FS 父子进程共享文件系统信息
CLONE_IDLETASK 将PID设置为0(只供idle进程使用)
CLONE_NEWNS 为子进程创建新的命名空间
CLONE_PARENT 指定子进程与父进程拥有同一个父进程
CLONE_PTRACE 继续调试子进程
CLONE_SETTID 将TID写回到用户空间
CLONE_SETTLS 为子进程创建新的TLS
CLONE_SIGHAND 父子进程共享信号处理函数以及被阻断的信号
CLONE_SYSVSEM 父子进程共享System V SEM_UNDO语义
CLONE_THREAD 父子进程放进相同的进程组
CLONE_VFORK 调用Vfork(),所以父进程准备睡眠等待子进程将其唤醒
CLONE_UNTRACED 防止跟踪进程在子进程上强制执行CLONE_PTRACE
CLONE_STOP 以TASK_SROPPED状态开始执行
CLONE_SETTLS 为子进程创建新的TLS(thread-local storage)
CLONE_CHILD_CLEARTID 清除子进程的TID
CLONE_CHILD_SETTID 设置子进程的TID
CLONE_PARENT_SETTID 设置父进程的TID

CLONE_VM 父子进程共享地址空间

二、GDB追踪fork()系统调用。

GDB 调试的相关内容可以参考:GDB追踪内核启动 篇 这里不再占用过多篇幅赘述。下面先直接上图,在详细分析代码的运行过程。

新濠国际登录平台 3

启动GDB后分别在sys_clone、do_fork、copy_process、copy_thread、ret_from_fork、syscall_exit等位置设置好断点,见证fork()函数的执行过程(运行环境与GDB追踪内核启动 篇完全一致)

新濠国际登录平台 4

可以看到,当我们在menuos中运行fork 命令的时候,内核会先调用clone,在sys_clone 断点处停下来了。

新濠国际登录平台 5

在调用sys_clone() 后,内核根据不同的参数去调用do_fork()系统调用。进入do_fork()后就去又运行了copy_process().

新濠国际登录平台 6

新濠国际登录平台 7

在copy_process() 中又运行了copy_thread(),然后跳转到了ret_from_fork 处运行一段汇编代码,再然后就跳到了syscall_exit(这是在arch/x86/kernel/entry_32.S中的一个标号,是执行系统调用后用于退出内核空间的汇编程序。),

新濠国际登录平台 8

可以看到,GDB追踪到syscall_exit 后就无法继续追踪了.................

三、代码分析(3.18.6版本的内核)

在3.18.6版本的内核 kernel/fork.c文件中:

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL);
}
#endif
#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

从以上fork()、vfork()、clone() 的定义可以看出,三者都是根据不同的情况传递不同的参数直接调用了do_fork()函数,去掉了中间环节clone()。

进入do_fork 后:

新濠国际登录平台 9

在do_fork中首先是对参数做了大量的参数检查,然后就执行就执行 copy_process将父进程的PCB复制一份到子进程,作为子进程的PCB,再然后根据copy_process的返回值P判断进程PCB复制是否成功,如果成功就先唤醒子进程,让子进程就绪准备运行。

所以在do_fork中最重要的也就是copy_process()了,它完成了子进程PCB的复制与初始化操作。下面就进入copy_process中看看内核是如何实现的:

新濠国际登录平台 10

先从整体上看一下,发现,copy_process中开头一部分的代码同样是参数的检查和根据不同的参数执行一些相关的操作,然后创建了一个任务,接着dup_task_struct(current)将当前进程的task_struct 复制了一份,并将新的task_struct地址作为指针返回!

新濠国际登录平台 11

在dup_task_struct中为子进程创建了一个task_struct结构体、一个thread_info 结构体,并进行了简单的初始化,但是这是子进程的task_struct还是空的所以接下来的中间一部显然是要将父子进程task_struct中相同的部分从父进程拷贝到子进程,然后不同的部分再在子进程中进行初始化。

最后面的一部分则是,出现各种错误后的退出口。

下面来看一下中间那部分:如何将父子进程相同的、不同的部分区别开来。

新濠国际登录平台 12

可以看到,内核先是将父进程的stask_struct中的内容不管三七二十一全都拷贝到子进程的stask_struct中了(这里面大部分的内容都是和父进程一样,只有少部分根据参数的不同稍作修改),每一个模块拷贝结束后都进行了相应的检查,看是否拷贝成功,如果失败就跳到相应的出口处执行恢复操作。最后又执行了一个copy_thread(),

新濠国际登录平台 13

在copy_thread这个函数中做了两件非常重要的事情:1、就是把子进程的 eax 赋值为 0,childregs->ax = 0,使得 fork 在子进程中返回 0;2、将子进程唤醒后执行的第一条指令定向到 ret_from_fork。所以这里可以看到子进程的执行从ret_from_fork开始。

借来继续看copy_process中的代码。拷贝完父进程中的内容后,就要对子进程进行“个性化”,

新濠国际登录平台 14

从代码也可以看出,这里是对子进程中的其他成员进程初始化操作。然后就退出了copy_process,回到了do_fork()中。

再接着看一下do_fork()中“扫尾“工作是怎么做的:

新濠国际登录平台 15

前面植根据参数做一些变量的修改,后面两个操作比较重要,如果是通过fork() 创建子进程,那么最后就直接将子进程唤醒,但是如果是通过vfork()来创建子进程,那么就要通知父进程必须等子进程运行结束才能开始运行。

总结:

综上所述:内核在创建一个新进程的时候,主要执行了一下任务:

1、父进程执行一个系统调用fork()或vfork();但最后都是通过调用do_fork()函数来操作,只不过fork(),vfork()传递给do_fork()的参数不同。

2、在do_fork()函数中,前面做参数检查,后面负责唤醒子进程(如果是vfork则让父进程等待),中间部分负责创建子进程和子进程的PCB的初始化,这些工作都在copy_process()中完成。

3、在copy_process()中先是例行的参数检查和根据参数进行配置;然后是调用大量的copy_***** 函数将父进程task_struct中的内容拷贝到子进程的task_struct中,然后对于子进程与父进程之间不同的地方,在子进程中初始化或是清零。

4、完成子进程的创建和初始化后,将子进程唤醒,优先让子进程先运行,因为如果让父进程先运行的话,由于linux的写时拷贝机制,父进程很可能会对数据进行写操作,这时就需要拷贝数据段和代码断的内容了,但如果先执行子进程的话,子进程通常都会通过exec()转去执行其他的任务,直接将新任务的数据和代码拷过来就行了,而不需要像前面那样先把父进程的数据代码拷过来,然后拷新任务的代码的时候又将其覆盖掉。

5、执行完copy_process()后就回到了do_fork()中,接着父进程回到system_call中执行syscall_exit: 后面的代码,而子进程则先从ret_from_fork: 处开始执行,然后在回到system_call 中去执行syscall_exit:.

ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi %eax
call schedule_tail
GET_THREAD_INFO(%ebp)
popl_cfi %eax
pushl_cfi $0x0202 # Reset kernel eflags
popfl_cfi
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)

6、父进程和子进程最后都是通过system_call 的出口从内核空间回到用户空间,回到用户空间后,由于fork()函数对父子进程的返回值不同,所以根据返回值判断出回来的是父进程还是子进程,然后分别执行不同的操作。

新濠国际登录平台 16

新进程的创建 一、背景知识: 1、进程与程序的关系: 进程是动态的,而程序是静态的;从结构上看,每个进程的实体都是由代码断和...

前言


Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,clone(确切说vfork创造出来的是轻量级进程,也叫线程,是共享资源的进程)

系统调用 描述
fork fork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容
vfork vfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行
clone Linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone

关于用户空间使用fork, vfork和clone, 请参见

Linux中fork,vfork和clone详解(区别与联系)

fork, vfork和clone的系统调用的入口地址分别是sys_fork, sys_vfork和sys_clone, 而他们的定义是依赖于体系结构的, 因为在用户空间和内核空间之间传递参数的方法因体系结构而异

系统调用的参数传递

系统调用的实现与C库不同, 普通C函数通过将参数的值压入到进程的栈中进行参数的传递。由于系统调用是通过中断进程从用户态到内核态的一种特殊的函数调用,没有用户态或者内核态的堆栈可以被用来在调用函数和被调函数之间进行参数传递。系统调用通过CPU的寄存器来进行参数传递。在进行系统调用之前,系统调用的参数被写入CPU的寄存器,而在实际调用系统服务例程之前,内核将CPU寄存器的内容拷贝到内核堆栈中,实现参数的传递。

因此不同的体系结构可能采用不同的方式或者不同的寄存器来传递参数,而上面函数的任务就是从处理器的寄存器中提取用户空间提供的信息, 并调用体系结构无关的_do_fork(或者早期的do_fork)函数, 负责进程的复制

新濠国际登录平台,不同的体系结构可能需要采用不同的方式或者寄存器来存储函数调用的参数, 因此linux在设计系统调用的时候, 将其划分成体系结构相关的层次和体系结构无关的层次, 前者复杂提取出依赖与体系结构的特定的参数, 后者则依据参数的设置执行特定的真正操作

线程被称为轻量级进程,是操作系统调度的最小单元,通常一个进程可以拥有多个线程。

进程描述符及任务结构

  • 任务队列:存放进程列表的双向循环链表
  • task_struct:进程描述符,包含一个具体进程的所有信息。2.6以后的版本通过slab动态生成task_struct。
  • thread_info:线程描述符,

fork, vfork, clone系统调用的实现


进程和线程的区别在于进程拥有独立的资源空间,而线程则共享进程的资源空间。

PID

唯一的进程标志值。int类型,为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768,这个值最大可增加到400万。进程的PID存放在进程描述符中。

关于do_fork和_do_frok


The commit 3033f14ab78c32687 (“clone: support passing tls argument via C
rather than pt_regs magic”) introduced _do_fork() that allowed to pass
@tls parameter.

参见

linux2.5.32以后, 添加了TLS(Thread Local Storage)机制, clone的标识CLONE_SETTLS接受一个参数来设置线程的本地存储区。sys_clone也因此增加了一个int参数来传入相应的点tls_val。sys_clone通过do_fork来调用copy_process完成进程的复制,它调用特定的copy_thread和copy_thread把相应的系统调用参数从pt_regs寄存器列表中提取出来,但是会导致意外的情况。

only one code path into copy_thread can pass the CLONE_SETTLS flag, and
that code path comes from sys_clone with its architecture-specific
argument-passing order.

前面我们说了, 在实现函数调用的时候,我iosys_clone等将特定体系结构的参数从寄存器中提取出来, 然后到达do_fork这步的时候已经应该是体系结构无关了, 但是我们sys_clone需要设置的CLONE_SETTLS的tls仍然是个依赖与体系结构的参数, 这里就会出现问题。

因此linux-4.2之后选择引入一个新的CONFIG_HAVE_COPY_THREAD_TLS,和一个新的COPY_THREAD_TLS接受TLS参数为
额外的长整型(系统调用参数大小)的争论。改变sys_clone的TLS参数unsigned long,并传递到copy_thread_tls。

/* http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L2646  */
extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long);
extern long do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *);


/* linux2.5.32以后, 添加了TLS(Thread Local Storage)机制, 
    在最新的linux-4.2中添加了对CLONE_SETTLS 的支持 
    底层的_do_fork实现了对其的支持, 
    dansh*/
#ifndef CONFIG_HAVE_COPY_THREAD_TLS
/* For compatibility with architectures that call do_fork directly rather than
 * using the syscall entry points below. */
long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr)
{
        return _do_fork(clone_flags, stack_start, stack_size,
                        parent_tidptr, child_tidptr, 0);
}
#endif

 

我们会发现,新版本的系统中clone的TLS设置标识会通过TLS参数传递, 因此_do_fork替代了老版本的do_fork。

老版本的do_fork只有在如下情况才会定义

  • 只有当系统不支持通过TLS参数通过参数传递而是使用pt_regs寄存器列表传递时

  • 未定义CONFIG_HAVE_COPY_THREAD_TLS宏

参数 描述
clone_flags 与clone()参数flags相同, 用来控制进程复制过的一些属性信息, 描述你需要从父进程继承那些资源。该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过clone标志可以有选择的对父进程的资源进行复制;
stack_start 与clone()参数stack_start相同, 子进程用户态堆栈的地址
regs 是一个指向了寄存器集合的指针, 其中以原始形式, 保存了调用的参数, 该参数使用的数据类型是特定体系结构的struct pt_regs,其中按照系统调用执行时寄存器在内核栈上的存储顺序, 保存了所有的寄存器, 即指向内核态堆栈通用寄存器值的指针,通用寄存器的值是在从用户态切换到内核态时被保存到内核态堆栈中的(指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中)
stack_size 用户状态下栈的大小, 该参数通常是不必要的, 总被设置为0
parent_tidptr 与clone的ptid参数相同, 父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义
child_tidptr 与clone的ctid参数相同, 子进程在用户太下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义

其中clone_flags如下表所示

新濠国际登录平台 17

问题一:在内核中如何获取当前进程的task_struct数据结构?

进程状态

进程描述符中的state域记录进程当前的状态,进程一共有五中状态,分别为:

  • TASK_RUNNING 运行
  • TASK_INTERRUPTIBLE 可中断
  • TASK_UNINTERRUPTIBLE 不可中断
  • __TASK_TRACED 被其他进程跟踪的进程
  • __TASK_STOPPED 进程停止执行

sys_fork的实现


不同体系结构下的fork实现sys_fork主要是通过标志集合区分, 在大多数体系结构上, 典型的fork实现方式与如下

早期实现

架构 实现
arm arch/arm/kernel/sys_arm.c, line 239
i386 arch/i386/kernel/process.c, line 710
x86_64 arch/x86_64/kernel/process.c, line 706
asmlinkage long sys_fork(struct pt_regs regs)
{
    return do_fork(SIGCHLD, regs.rsp, &regs, 0);
}

 

新版本

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
        return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
        /* can not support in nommu mode */
        return -EINVAL;
#endif
}
#endif

 

我们可以看到唯一使用的标志是SIGCHLD。这意味着在子进程终止后将发送信号SIGCHLD信号通知父进程,

由于写时复制(COW)技术, 最初父子进程的栈地址相同, 但是如果操作栈地址闭并写入数据, 则COW机制会为每个进程分别创建一个新的栈副本

如果do_fork成功, 则新建进程的pid作为系统调用的结果返回, 否则返回错误码

  内核有一个常用的常量current用于获取当前进程task_struct数据结构,它利用了内核栈的特性。首先通过sp寄存器获取当前内核栈的地址,对齐后获取struct thread_info数据结构指针,最后通过thread_info->task成员获取task_struct数据结构。图1为linux内核栈的结构图。

进程上下文

通常进程的代码在用户空间执行,当执行了系统调用或触发了某个异常时,它就陷入了内核空间。此时,我们称内核处于进程上下文中。

sys_vfork的实现


早期实现

架构 实现
arm arch/arm/kernel/sys_arm.c, line 254
i386 arch/i386/kernel/process.c, line 737
x86_64 arch/x86_64/kernel/process.c, line 728
asmlinkage long sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.rsp, &regs, 0);
}

 

新版本

#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
        return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
                        0, NULL, NULL, 0);
}
#endif

 

可以看到sys_vfork的实现与sys_fork只是略微不同, 前者使用了额外的标志CLONE_VFORK | CLONE_VM

新濠国际登录平台 18图1 内核栈

进程创建

  1. 写时拷贝,父子进程共享同一个地址空间,将页的拷贝推迟到实际发生写入时才进行。这个优化可以避免创建进程时拷贝大量不被使用的数据。
  2. 在进程中调用fork()会通过复制一个现有进程来创建一个新进程,调用fork()的进程是父进程,创建的进程是子进程。fork()函数从内核返回两次,一次是返回父进程,另一次返回子进程。Linux通过 clone(SIGCHLD, 0);系统调用实现fork()。
  3. vfork() 不拷贝父进程的页表项,其它与fork功能相同。系统实现:clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
  4. exec()这组函数可以创建新的地址空间,并把新的程序载入其中。

sys_clone的实现


早期实现

架构 实现
arm arch/arm/kernel/sys_arm.c, line 247
i386 arch/i386/kernel/process.c, line 715
x86_64 arch/x86_64/kernel/process.c, line 711

sys_clone的实现方式与上述系统调用类似, 但实际差别在于do_fork如下调用

casmlinkage int sys_clone(struct pt_regs regs)
{
    /* 注释中是i385下增加的代码, 其他体系结构无此定义
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx;*/
    if (!newsp)
        newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0);
}

 

新版本

#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 unsigned long, tls,
                 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,
                 unsigned long, tls)
#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,
                unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
                 int __user *, parent_tidptr,
                 int __user *, child_tidptr,
                 unsigned long, tls)
#endif
{
        return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif

 

我们可以看到sys_clone的标识不再是硬编码的, 而是通过各个寄存器参数传递到系统调用, 因而我们需要提取这些参数。

另外,clone也不再复制进程的栈, 而是可以指定新的栈地址, 在生成线程时, 可能需要这样做, 线程可能与父进程共享地址空间, 但是线程自身的栈可能在另外一个地址空间

另外还指令了用户空间的两个指针(parent_tidptr和child_tidptr), 用于与线程库通信

 

线程实现

在Linux内核中线程看起来就是一个普通的进程,只是和其它一些进程共享某些资源,如地址空间。

  1. 创建线程同样使用clone实现,只是需要传递一些参数标志来指明需要共享的资源:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
  2. 内核线程没有独立的地址空间,只在内核空间运行,不切换到用户空间上去,只能由内核线程创建。

创建子进程的流程


本文由新濠国际登录平台发布于关于我们,转载请注明出处:新濠国际登录平台每个线程都拥有独立的程序计

关键词: