Transcript Linux

进程创建
0420080235 俞斌
Linux 进程创建
Linux将进程的创建与目标程序的执行分成两步
 第一步是从已经存在的“父进程”中像细胞分裂
一样复制一个“子进程”。复制出来的子进程有
自己的task_struct结构和系统空间堆栈,但与父
进程共享其他所有的资源。
 第二步是目标程序的执行。一般来说,创建一个
新的进程是因为有不同的目标程序要让新的程序
去执行,所以,复制完以后,子进程通常要与父
进程分道扬镳,走自己的路。Linux提供了一个
系统调用execce(),让一个进程执行以文本形
式存在的一个可执行程序的映像。

系统调用fork()、vfork()与clone()

Linux提供了3种进程的创建的方式,也就是提供
了3种进程创建的系统调用。它们是:
fork,clone,vfork.
(1)fork用于普通进程的创建。对应的实现函
数是sys_fork.
(2)vfork是完全共享的创建,新老进程共享
同样的资源,完全没有拷贝,对应的实现函数是
sys_vfork.
(3)clone是由用户指定创建的方式(需要共
享什么,需要拷贝什么)。因此也是最灵活的创
建方式。对应的实现函数是sys_clone.
系统调用fork()、vfork()与clone()
fork()与clone()的区别:
- 两者的区别在于,fork()是全部复制,父进
程所有的资源全部通过数据结构的复制“遗传”
给子进程。而clone()则可以将资源有选择的复
制给子进程,而没有复制的数据结构则通过指针
的复制让子进程共享。
 vfork():
- 没有参数,除task_struct结构和系统空间堆栈
以外的资源全都通过数据结构指针的复制“遗
传”。

几个系统调用的代码
 详见代码
 可见,三个系统调用的实现都是通过
do_fork()来完成的,不同的是对
do_fork()的调用参数。关于这些参数所起
的作用,读了do_fork()的代码以后就会很
清楚
3个系统调用的参数
创建标志
新用户栈指针 寄存器结构
sys_fork
sigchld
regs.esp
regs
sys_clone
用户指定通过ebx传入
用户指定通过ecx传入
regs
regs.esp
regs
sys_vfork clone_vfor|clone_vm|sigchld
函数do_fork完成真正的进程创建,其
定义如下:
int do_fork(unsigned long clone_flags,
unsigned long stack_start, struct pt_regs
*regs, unsigned long stack_size)
其中:clone_flags是进程创建标志,指示
该函数如何创建新进程,tack_start是新进程用
户堆栈的栈顶指针,当新进程开始运行,从内
核返回用户态时,它指示新用户堆栈的栈顶位
置,regs为系统堆栈的栈顶布局。size为堆栈的
大小。
流程图
申请8KB的物理内存
将current结构拷贝到新进程中
详见代码
分配进程标识符get_pid
在task[]中,为进程找一个空的槽位
拷贝文件copy_files
接后
拷贝信号处理信息copy_sighand
拷贝内存信息copy_mm
详见代码
将新进程加到各个队列中
唤醒新进程wake_up_process
承前
子进程file,fs,sighand,mm,thread五
方面信息的复制
这些信息的拷贝在do_fork()函数中是通过调用子函
数实现的。代码如下:
if (copy_files(clone_flags, p))
goto bad_fork_cleanup;
if (copy_fs(clone_flags, p))
goto bad_fork_cleanup_files;
if (copy_sighand(clone_flags, p))
goto bad_fork_cleanup_fs;
if (copy_mm(clone_flags, p))
goto bad_fork_cleanup_sighand;
copy_thread(nr, clone_flags, usp, p, regs);
详细分析
系统调用execve()



Unix系统提供了一个函数家族,这些函数能用可执行文
件所描述的新上下文代替进程的上下文,而调用前后进程
ID并不改变。这样的函数名以前缀exec开始,后跟一个
或两个字母,因此,家族中的一个普通函数被当作exec
类函数来引用。
用fork子函数创建进程后,子进程往往调用一种exec函
数执行另一个程序。此时,该子进程完全由新程序代换,
而新程序从其main函数开始执行。
Linux也提供了一个系统调用execve(),而在c语言
的程序库中则又在此基础上向应用程序提供了一整套的库
函数,包括execl()、execlp()、execve()、
execle()、execv()和execvp()。
Linux的执行格式
 Linux支持很多种不同格式的可执行文件,如
ELF、a.out、Unix BSD的COFF、MSDOS的EXE程序等。还有几种与平台相关的
可执行格式以及Linux下的各种脚本程序,如
Java、bash、perl、python等。
 Linux正式的可执行格式是ELF
(Executable and Linking Format),
旧版的Linux支持a.out的格式。
Linux的执行格式(续)
 Linux系统中对程序的执行格式的区分方
法就是识别可执行文件头部的签名
(magic number)。一般Linux可执行
文件的前32位在逻辑上分成两个部分:
 高16位是一个代表目标CPU类型的代码,
如objdump命令输出的:
architecture: i386。
 低16位即是magic number,它标记了
可执行文件的类型。
Linux的执行格式(续)
 Linux为每一个可执行的类型设计了一个
linux_binfmt结构,具体代码见
include/linux/binfmt.h
 Linux系统中使用format头指针将所有
linux_binfmt结构链接到一起,其定义在
linux/fs/exec.c中:
static struct linux_binfmt
*format;
使用register_binfmt和
unregister_binfmt从链表中加入或删
除一个可执行类型。
execve系统调用
在六个exec函数中,只有execve是系统调用,
其它的五个函数均调用该函数。
 sys_execve()服务例程接受下列参数:
1. 可执行文件名的地址(在用户地址空间中)
2. 以NULL结束的字符串数组的地址(数组和字
符串均在用户态地址空间)。每个字符串表示一个
命令行参数。
3. 以NULL结束的字符串数组。每个字符串以
NAME=value形式表示一个环境变量。

execve系统调用(续)
 sys_execve()把可执行文件路径名拷贝到
一个新分配的页框。然后调用do_execve()
函数,并将指向这个页框的指针、指针数
组的指针以及用户态寄存器的内容传递给
do_execve()函数。
 具体代码位于
arch/i386/kernel/process.c。
do_execve函数
 do_execve函数依次执行下列操作:
1.静态地分配一个linux_binprm结构,并
用新的可执行文件的数据填充这个结构。
该结构是内核为了将运行一个可执行文件
时所需要的信息组织到一起而定义的,其
具体代码在include/linux/binfmts.h
中。
do_execve代码在fs/exec.c中。
2.调用open_exec()获得与这个可执行文
件相关的文件对象指针。
do_execve函数(续)
3.设置linux_binprm结构中的各变量,调用
prepare_binprm()进行文件权能检查并将可
执行文件的前128字节读入缓冲区。
4.把文件路径名、命令行参数及环境变量拷贝到一
个或多个新分配的页框中(最终这些页框将被分配
给用户态地址空间)。
5.调用search_binary_handler()函数对
formats链表进行扫描,并尽力应用每个元素的
load_binary方法,如果得到成功应答,则终
止扫描。
do_execve函数(续)
6.如果可执行文件格式不在formats链表中,
这时,如果内核支持动态安装模块,就根
据目标文件的第2和第3个字节生成
binfmt模块名,通过request_module
装入模块。再进行一次对链表的扫描。
7.如果所有元素的load_binary方法均返
回-ENOEXEC,则释放所有分配的页框返
回。
8.否则,返回从这个文件可执行格式的
load_binary方法中获得的代码。
a.out格式文件的装载运行

由以上的分析可以看出,可执行文件装入内存及运行的关
键操作为各linux_binfmts对象中定义的
load_binary方法。回忆一下,在调用load_binary
方法前,binprm结构中已经具有了从用户态空间中拷贝
过来的可执行文件名、命令行参数以及环境变量,还拥有
了从文件中读入的前128个字节。下面,我们集中讨论
a.out格式的装载和运行。
a.out的数据结构为:
static struct linux_binfmt aout_format = {
NULL, THIS_MODULE, load_aout_binary,
load_aout_library, aout_core_dump,
PAGE_SIZE
};
可见,装载a.out格式的文件的函数为
load_aout_binary。

a.out格式文件的装载运行(续)
 正如前面提及的一样,
load_aout_binary函数首先将对a.out
格式的文件的开头进行检查。所有的a.out
格式的可执行文件(二进制代码)的开头
都是一个exec数据结构,这是在
include/asm-i386/a.out.h中定义的。
而针对其magic number的宏操作的定
义在include/linux/a.out.h。
a.out格式文件的装载运行(续)
详细代码见fs/binfmt_aout.c
1.检查文件前128字节中的magic number以确定可执行
格式,不匹配返回-ENOEXEC.
2.利用程序解释器的类型和当前可执行文件首部提供的信息
做一些一致性检查.
3.调用flush_old_exec 释放进程前一个计算(例如调用
fork继承自父进程的)的几乎所有资源.
4.根据可执行文件的首部,设置当前进程的内存描述符,如
代码段、数据段、bss段等的起止虚拟地址、大小等。
5.计算进程执行的权能。
6.调用do_brk为代码段和数据段一起分配物理页面。
7.调用do_mmap分别为代码段和数据段的虚拟地址和物
理页面建立映射,同时分别从文件中读入代码和数据段的
内容。
a.out格式文件的装载运行(续)
8.调用do_brk为bss段分配物理页面并建
立其与虚拟地址的映射。
9.将以前分配的包含命令行参数和环境变量
的页面放到进程地址空间中并为其建立页
表。同时确定用户堆栈段空间。
10.在用户堆栈段空间中建立main函数的参
数表。
11.设置好寄存器的内容后返回。
a.out格式文件的装载运行(续)
当execve系统调用返回并调用进程重新恢复它
在用户态的执行时,执行上下文被彻底地改变,
调用系统调用的代码不再存在,新程序已经被映
射到进程的地址空间。
 但是,新程序还不能执行,因为程序解释器还要
照顾到共享库的装载。
 程序解释器的第一个工作就是从内核保存在用户
态堆栈的信息开始,为自己建立一个基本的执行
上下文。然后,它检查被执行的程序,识别哪些
共享库的哪些函数被有效装入。接着,解释器发
布几个mmap调用创建虚拟地址区域对共享库代
码进行映射,并更新对共享库符号的所有引用。
最后,跳转到被执行程序的入口而终止解释器的
执行。

The End
thanks