Linux分析第六周——进程的描述和进程的创建
李雪琦+原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、知识要点:
task_struct
task_struct
结构的分配使用的是alloc_task_struct
宏,该宏就是简单地调用kmem_cache_alloc()
从task_struct_cachep
缓存中分配。接着会使用alloc_thread_info
宏分配thread_info
结构。thread_info
结构保存了特定于体系结构的汇编语言代码需要访问的那部分进程数据,包括执行域、可抢占标志和当前的CPU等信息。在分配thread_info
结构的时,实际调用的是__get_free_pages()
,64位下分配的是2个物理页(返回的是虚拟地址)。但是很显然thread_info
结构是用不了2页的,剩余的内存其实是用作子进程的内核栈(不是stack_start
参数指定的用户栈),可以通过end_of_stack()
来计算栈顶位置。在分配完必要的结构后,会调用arch_dup_task_struct()
拷贝父进程的task_struct
结构到子进程的task_struct
结构,如果父进程使用FPU或其他的CPU扩展寄存器,则要将这些寄存器的信息(在prepare_to_copy()
中保存的)拷贝到子进程的task_struct
结构中。
fork函数对应的内核处理过程sys_clone:
fork()系统调用对应的内核实现为sys_fork()
,sys_fork()
是对do_fork()
的简单封装,sys_fork()
的任务是从处理器寄存器中提取由用户空间提供的信息,do_fork()
负责进程的复制。fork()和clone()系统调用的入口点sys_vfork()
和sys_clone()
也是调用的do_fork()
。
而其中关系着进程创建的主要是copy_process
和copy_thread.copy_process()
函数有7个参数,其中我们需要关心的有clone_flags
、stack_start
和regs
。cone_flags
是一个标志集合,分为两部分:最低的字节指定了在子进程终止时发送给父进程的信号,其余的高位字节保存了各种真正的复制标志,如CLONE_FS
、CLONE_THREAD
等。在用户层调用fork()时不能指定标志,所以默认的CLONE_FLAGS
的值为SIGCHLD。
dup_task_struct:
为
task_struct
开辟内存tsk = alloc_task_struct_node(node)
;为
thread_info
开辟内存ti = alloc_thread_info_node(tsk, node)
;-
复制父进程的
task_struct
信息到新的task_struct
里err = arch_dup_task_struct(tsk, orig)
;
int arch_dup_task_struct(struct task_struct dst, struct task_struct src)
{
dst = src;
if (src->thread.xstate) {
dst->thread.xstate = kmem_cache_alloc(task_xstate_cachep,
GFP_KERNEL);
if (!dst->thread.xstate)
return -ENOMEM;
memcpy(dst->thread.xstate, src->thread.xstate, xstate_size);
}
return 0;
}
4)setup_thread_stack
设置线程栈
子进程的初始化:进程的用户栈的起始地址存储在task_struct
结构的stack_start
成员中,在初始化的时候使用的是父进程的用户栈地址,所以在子进程创建时会和父进程使用相同的用户栈,如果有任何一方修改栈的话,会重新拷贝一份,这是基于COW技术,以避免无用的复制。创建子进程为调度器类提供了调度进程的一个切入点,内核会调用sched_fork()
函数,以便使调度器有机会对新进程进行设置。sched_fork()
会初始化一些和调度相关的成员,并将进程设置为TASK_RUNNING状态。不过此时新的进程还没有放到CPU的执行队列中,所以新的进程不会被调度到。
新进程是从ret_from_fork
处开始执行的。对于fork执行处理过程来说,父子进程共享同一段代码空间,”一次调用,两次返回“,其实对于调用fork的父进程来说,如果fork出来的子进程没有得到 调度,那么父进程从fork系统调用返回,同时分析sys_fork
知道,fork返回的是子进程的id。再看fork出来的子进程,由 copy_process
函数可以看出,子进程的返回地址为ret_from_fork
(和父进程在同一个代码点上返回),返回值直接置为0。所以当子进 程得到调度的时候,也从fork返回,返回值为0。ret_from_fork()
调用schedule_tail()
函数,用存放在栈中的值再装入所有寄存器,并强迫CPU返回到用户态。这样,eax寄存器就装过两个值,一个是子进程的值0,一个是父进程的值——子进程的PID。然后在fork()、vfork()或clone()返回时,新进程将开始执行。在不同的进程中返回不同的值。
执行过程图:
二、实验过程:
1.配置环境,登录实验楼网站。
按照上次实验的基本步骤,结合老师视频所讲,完成相关实验。
cd LinuxKernel
删除menu
然后从github上克隆相应的mengning/menu.git
2.测试menuOS,测试fork直接执行结果。
3.配置调试系统,进入gdb调试,利用file linux-3.18.6/vmlinux和target remote:1234来配置加载初始调试环境.
4.在linux内核进程创建可能用到的点设置断点分别为sys_clone,do_fork,dup_task_struct,copy_thread,copy_process和ret_from_fork.
三、总结:
Linux中,fork、vfork和clone三个系统调用都是通过调用do_fork
来实现进程的创建,而fork()系统调用产生的子进程在系统调用处理过程中从ret_from_fork
处开始执行。fork会产生父子进程,在父进程中,返回值是子进程的进程号;在子进程中,返回值为0。因此可通过返回值来判断当前进程是父进程还是子进程。使用fork函数得到的子进程是父进程的一个复制品,它从父进程处复制了整个进程的地址空间,包括进程上下文,进程堆栈,内存信息,打开的文件描述符,信号控制设定,进程优先级,进程组号,当前工作目录,根目录,资源限制,控制终端等。而子进程所独有的只是它的进程号,资源使用和计时器等。可以看出,使用fork函数的代价是很大的,它复制了父进程中的代码段,数据段和堆栈段里的大部分内容,使得fork函数的执行速度并不快。