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