Linux内核中进程创建、可执行文件的加载和进程执行进程切换的分析

274

原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/

一、进程创建的分析

Linux提供了几个系统调用来创建和终止进程,以及执行新程序,他们分别是Fork,vfork,clone和exec,exit;其中clone用来创建轻量级进程,必须制定要共享的资源,exec系统调用执行一个新程序,exit系统调用终止进程。不论是fork,vfork还是clone,在内核中最终都是调用了do_fork来实现进程的创建。do_fork函数的主要作用就是复制原来的进程生成另一个新的进程,它完成了整个进程创建中的大部分工作。

do_fork分析:

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;
	long nr;

	if (!(clone_flags & CLONE_UNTRACED)) {
	        if (clone_flags & CLONE_VFORK)
		        trace = PTRACE_EVENT_VFORK;
	        else if ((clone_flags & CSIGNAL) != SIGCHLD)
		        trace = PTRACE_EVENT_CLONE;
	        else
		        trace = PTRACE_EVENT_FORK;

	        if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}
        
        //通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);
        
	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);

		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);

		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);

                /* 如果使用的是vfork来创建子进程,那么子进程先执行,当子进程调用exec函数或退出
                时向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。*/
		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}
                //将子进程添加到调度器的队列
		wake_up_new_task(p);

		if (unlikely(trace))
			ptrace_event_pid(trace, pid);
                /*如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函
                数或者退出。*/
		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}

		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}

 二、使用gdb跟踪分析一个fork系统调用内核处理函数do_fork

1、通过qemu启动内核

cd Linux-5.0.1   
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs

 

使用gdb进行跟踪调试

 qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s 

 

三、编译链接的过程和ELF可执行文件格式

源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。

  Executable and Linking Format(ELF)文件是x86 Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:

 适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。

 适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。

 共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度,如图所示。

ELF header在文件开始处描述了整个文件的组织,Section提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等),Program header table指出怎样创建进程映像,含有每个program header的入口,section header table包含每一个section的入口,给出名字、大小等信息。

ELF的加载过程:

从编译/链接和运行的角度看,应用程序和库程序的连接有两种方式。一种是固定的、静态的连接,就是把需要用到的库函数的目标代码(二进制)代码从程序库中抽取出来,链接进应用软件的目标映像中;另一种是动态链接,是指库函数的代码并不进入应用软件的目标映像,应用软件在编译/链接阶段并不完成跟库函数的链接,而是把函数库的映像也交给用户,到启动应用软件目标映像运行时才把程序库的映像也装入用户空间(并加以定位),再完成应用软件与库函数的连接。

       这样,就有了两种不同的ELF格式映像。一种是静态链接的,在装入/启动其运行时无需装入函数库映像、也无需进行动态连接。另一种是动态连接,需要在装入/启动其运行时同时装入函数库映像并进行动态链接。Linux内核既支持静态链接的ELF映像,也支持动态链接的ELF映像,而且装入/启动ELF映像必需由内核完成,而动态连接的实现则既可以在内核中完成,也可在用户空间完成。因此,GNU把对于动态链接ELF映像的支持作了分工:把ELF映像的装入/启动入在Linux内核中;而把动态链接的实现放在用户空间(glibc),并为此提供一个称为“解释器”(ld-linux.so.2)的工具软件,而解释器的装入/启动也由内核负责

内核空间的加载过程:

内核中实际执行execv()或execve()系统调用的程序是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(第一个字节开始)读入若干(当前Linux内核中是128)字节(实际上就是填充ELF文件头,下面的分析可以看到),然后调用另一个函数search_binary_handler(),在此函数里面,它会搜索我们上面提到的Linux支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理。如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件。在ELF文件格式中,处理函数是load_elf_binary函数。

ELF文件加载和链接的简要总结:

用户通过shell执行程序,shell通过exceve进入系统调用。(User-Mode)

sys_execve经过一系列过程,并最终通过ELF文件的处理函数load_elf_binary将用户程序和ELF解释器加载进内存,并将控制权交给解释器。(Kernel-Mode)

       ELF解释器进行相关库的加载,并最终把控制权交给用户程序。由解释器处理用户程序运行过程中符号的动态解析。(User-Mode)。

四、总结

Linux系统的执行过程理解:

最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程

1、正在运行的用户态进程X

2、发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

3、SAVE_ALL //保存现场

4、中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

5、标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

6、restore_all //恢复现场

7、iret - pop cs:eip/ss:esp/eflags from kernel stack

8、继续运行用户态进程Y

几个特殊情况:

1、通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;

2、内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换;

3、创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork; 加载一个新的可执行程序后返回到用户态的情况,如execve。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值