进程的创建与可执行程序的加载

进程的创建与可执行程序的加载

一 进程相关简介

        进程就是程序的一次执行,OS是通过进程的PCB对进程进行管理的,在Linux中task_struct结构即是进程的PCB,task_struct的结构如附录一所示,这里主要介绍跟进程创建和可执行程序加载密切相关的部分。

1.首先是next_task,pre_task。我们知道在linux中维护一个所有进程链表struct list_head tasks,这是一个循环双向链表;如下所示


        这个链表的头部为init_task进程0,next_task,pre_task两个进程指针用以维护进程在双向链表中的操作,新建的进程都要插入到这个双向链表中。

2.比较重要是structmm_struct *mm指针,记录着进程的内存分配情况。我们知道一个高级语言程序的执行,都需要进行预处理、编译、汇编、链接形成最后的可执行程序,即我们通常用的ELF格式可执行文件。可执行文件需要通过加载器来加载从而来运行。在linux中每个程序都有一个运行是存储映像,如下图所示,而我们的mm指针就是指向进程的这样的一个存储映像的内存分配情况。


      vm_start:指向这个区域的起始处

      vm_end:指向这个区域的结束处

      vm_prot:指向这个区域内所包含的的所有页的读写许可权限

      vm_flags:描述这个区域内的页面是与其他进程共享的还是本进程私有的

      vm_next:指向链表中下一个区域结构

二 fork进程创建和execve可执行程序的加载

我们通过如下代码来分析fork和execve的执行过程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    pid_t  pid;
    int status;
    pid = fork();
    if (pid<0)
    {
       fprintf(stderr,"forkfailed!");
       exit(-1);
    }
    else if (pid==0)
    {
      execlp("/bin/ls","ls",NULL);
    }
    else
    {
       wait(&status);
       printf("child complete!");
       exit(0);
    }
}

1.fork在内存中的执行过程

        在用户态下,使用fork()创建一个进程对我们来说已经不再陌生。除了这个函数,一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由C库提供,它们分别在C库中封装了与其同名的系统调用fork(),vfork()和clone()。API所封装的系统调用对编程者是隐藏的,编程者只需知道如何使用这些API即可。

上述三个系统调用所对应的系统调用号在linux/include/asm-i386/unistd.h中定义如下:

#define__NR_restart_syscall      0
#define__NR_exit                 1
#define__NR_fork                 2
…… ……
#define__NR_clone              120
…… ……
#define__NR_vfork              190
…… ……

        这里我们已fork为例来进行分析。利用fork创建一个子进程,首先系统为新的进程创建一个task_struct结构体,传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。

        内核采用写时复制技术对传统的fork函数进行了下面的优化。即子进程创建后,父子以只读的方式共享父进程的资源(并不包括父进程的页表项)。当子进程需要修改进程地址空间的某一页时,才为子进程复制该页。采用这样的技术可以避免对父进程中某些数据不必要的复制。

        fork是通过系统调用do_fork(do_fork代码见附录二所示)实现的,do_fork 函数首先调用 alloc_pidmap,该调用会分配一个新的 PID。接下来调用do_fork中最重要的两个函数。

       首先是调用copy_process(见附录二)函数,向其传递这些标志、堆栈、注册表、父进程以及最新分配的 PID。

      接着copy_process 会调用dup_task_struct 函数(见附录二)(在 ./linux/kernel/fork.c 内),这会分配一个新 task_struct 并将当前进程的描述符复制到其内。在新的线程堆栈设置好后,一些状态信息也会被初始化,并且会将控制返回给copy_process。控制回到copy_process后,除了其他几个限制和安全检查之外,还会执行一些常规管理,包括在新task_struct 上的各种初始化。之后,会调用一系列复制函数来复制此进程的各个方面,比如复制开放文件描述符(copy_files)、复制符号信息(copy_sighand和copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。

        之后,这个新任务会被指定给一个处理程序,同时对允许执行进程的处理程序进行额外的检查(cpus_allowed)。新进程的优先级从父进程的优先级继承后,执行一小部分额外的常规管理,而且控制也会被返回给 do_fork。在此时,新进程存在但尚未运行,do_fork 函数通过调用 wake_up_new_task 函数(可在./linux/kernel/sched.c 内找到)初始化某些调度程序的常规管理信息,将新进程放置在运行队列之内,然后将其唤醒以便执行。最后,一旦返回至do_fork,此 PID 值即被返回给调用程序,进程完成。

        fork的总的执行过程如下图所示:


2.execve在内核中的执行过程

  execve函数在当前进程中加载并运行包含可执行目标文件的程序。以我们代码中的可执行程序/bin/ls为例,加载并运行可执行程序主要需要以下几个步骤

(1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构

(2)隐身私有区域:为新程序的文本、数据、bss和栈区域创建新的区域结构,所有这些区域都是私有的、写实拷贝的。文本和数据区被映射为ELF可执行文件中的文本和数据区,bss区域是请求二进制零的,映射到匿名文件。栈和堆区域也是请求二进制零的,初始长度为零。

(3)映射共享区域:如果ELF文件与共享目标链接,比如标准C中的libc.so库,那么就需要动态链接,然后映射到用户虚拟地址空间中的共享区域。

(4)设置程序计数器(EIP):execve做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向文本区域的入口地址。如下图所示:


         execve函数在内核中的详细执行流程如下图所示:


(1)do_execve()

         在用户态下调用execve(),引发系统中断后,在内核态执行的相应函数是do_sys_execve(),而do_sys_execve()会调用do_execve()函数。do_execve()首先会读入可执行文件,如果可执行文件不存在,会报错。然后对可执行文件的权限进行检查。如果文件不是当前用户是可执行的,则execve()会返回-1,报permissiondenied的错误。否则继续读入运行可执行文件时所需的信息。

(2) search_binary_handler()

        接着系统调用search_binary_handler(),根据可执行文件的类型,查找到相应的处理函数。系统为每种文件类型创建了一个struct linux_binfmt,并把其串在一个链表上,执行时遍历这个链表,找到相应类型的结构。然后执行相应的load_binary()函数开始加载可执行文件。

(3) load_elf_binary()

         加载elf类型文件的handler是load_elf_binary(),它先读入ELF文件的头部,根据ELF文件的头部信息读入各种数据(header information)。再次扫描程序段描述表,找到类型为PT_LOAD的段,将其映射(elf_map())到内存的固定地址上。如果没有动态链接器的描述段,把返回的入口地址设置成应用程序入口。完成这个功能的是start_thread(),start_thread()并不启动一个线程,而只是用来修改了pt_regs中保存的PC等寄存器的值,使其指向加载的应用程序的入口。这样当内核操作结束,返回用户态的时候,接下来执行的就是应用程序了。

(4) load_elf_interp()

         如果有动态链接库的存在,那么我们还需要映射动态链接库到共享区域,还要把控制权交给动态连接器(programinterpreter,ld.so in linux)以处理动态链接的程序。内核搜寻段表,找到标记为PT_INTERP的段中所对应的动态连接器的名称,并使用load_elf_interp()加载其映像,并把返回的入口地址设置成load_elf_interp()的返回值,即动态链接器入口。当execve退出的时候动态链接器接着运行。动态连接器检查应用程序对共享连接库的依赖性,并在需要时对其进行加载,对程序的外部引用进行重定位。然后动态连接器把控制权交给应用程序,从ELF文件头部中定义的程序进入点开始执行。

附录一:task_struct结构


附录二:

do_fork代码

long do_fork(unsigned long clone_flags,
             unsigned longstack_start,
             structpt_regs *regs,
             unsigned longstack_size,
             int __user*parent_tidptr,
             int __user*child_tidptr)
 
{
    struct task_struct *p;
    ……
 
    //创建进程
    p =copy_process(clone_flags, stack_start, regs, stack_size,
                   child_tidptr,NULL, trace);
    ……
    //将进程加入到运行队列中
    wake_up_new_task(p);
}

copy_process代码

task_struct*copy_process(unsigned long clone_flags,
                         unsigned long stack_start,
                         struct pt_regs *regs,
                         unsigned long stack_size,
                         int __user *child_tidptr,
                         struct pid *pid,
                         int trace)
{
       struct task_struct*p;
       //创建进程内核栈和进程描述符
       p =dup_task_struct(current);
       //得到的进程与父进程内容完全一致,初始化新创建进程
       ……
       return p;
}

dup_task_struct 代码

static struct task_struct *dup_task_struct(struct task_struct*orig)
{
       struct task_struct*tsk;
       struct thread_info*ti;
       int node =tsk_fork_get_node(orig);
       //创建进程描述符对象
       tsk =alloc_task_struct_node(node);
       //创建进程内核栈 thread_info
       ti =alloc_thread_info_node(tsk, node);
       //使子进程描述符和父进程一致
       err =arch_dup_task_struct(tsk, orig);
       //进程描述符stack指向thread_info
       tsk->stack = ti;
       //使子进程thread_info内容与父进程一致但task指向子进程task_struct
      setup_thread_stack(tsk, orig);
       return tsk;
}

附录三 ELF文件的分析

1.ELF文件的格式


          ELF头部在文件的开始,描述文件的总体格式,保存了路线图,描述该文件的组织情况,即生成该文件系统的字的大小和字节顺序。

          段头部表用来描述ELF可执行文件与连续的存储段之间的映射关系。

          节头表包含了描述文件节区的信息,每个节区在表中都有一个项,给出节区的名称、节区大小这类心里。用于链接的目标文件(可重定向文件)必须包含节区头部表,而可执行文件可以没有。

         根据我们上面给出的程序,可以通过readelf来查看ELF可执行文件的信息。

         通过readelf –h process查看的ELF文件的头部信息如下图所示:


             通过readelf –l process查看的ELF文件的段头部表信息如下图所示:


             通过readelf –S process查看的ELF文件的节点信息如下图所示:


 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值