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


    一、fork和exec系统调用在内核中的执行过程
      1)fork()函数用以创建普通进程:
      fork()函数是glibc中的API,通过软中断陷入内核,利用系统调用服务例程创建进程。fork()函数中封装了软中断指令int $0x80,执行这条汇编指令会跳转到预设的内核空间地址,该处为系统调用总处理程序system_call(),该函数作为系统调用服务例程执行前的引导。因为内核实现了很多不同的系统调用,所以进程必须指明需要哪个系统调用,这需要传递系统调用号作为函数的参数。所以,在执行int 0x80时,会将系统调用号传入eax寄存器来完成系统调用传参。
      接着,system_call()读取eax传递的系统调用号作为索引,通过存放在sys_call_table数组的系统调用分派表(dispatch table),找到该系统调用对应的服务例程并依次执行。相应的,该系统调用的服务例程为sys_fork(),而sys_fork()会调用do_fork()函数执行创建进程的操作(见附录一),具体的: 
    • 新的进程通过复制旧进程(即父进程)而建立。为了创建新进程,在系统物理内存中为新进程创建一个task_struct结构(使用 kmalloc 函数,以便得到一个连续的区域),将旧进程的 task_struct  结构内容复制到其中,再修改部分数据。
    • 为新进程分配新的核心堆栈页,分配新的进程标识符 pid。
    • 将这个新 task_struct 结构的地址填到task数组中,并调整进程链关系插入运行队列。于是这个新进程便可以在下次度时被选择执行。此时,由于父进程的上下文TSS结构复制到了子进程的 TSS结构中,通过改变其中的部分数据,便可以使子进程执行效果与父一致,都是从系统调用退出。
    • 系统调用返回:子进程将得到与父不同的返回值(返回父进程的是子进程的pid,而返回子进程的是 0)。

      另外,do_fork()中使用SIGCHLD标志作为参数,该克隆标志在处理后形成exit_signal,将在子进程退出时作为信号发送给父进程

      2)exec()函数将当前进程映像替换成新的程序文件:
      程序以可执行文件的形式存放在磁盘上,可执行文件是一个普通的文件,它描述了如何初始化一个新的进程上下文。与fork()相同,execl()也是glibc库中的API,用一个指定的可执行文件所描述的上下文代替进程的上下文。类似的,库函数
    execlp,execle,execv,execve和execvp也执行相同的功能。
     
      对应于execl()的系统调用服务例程为sys_execve()函数,而sys_execve()会调用do_execve()创建进程上下文的操作(见附录二),具体的:
     

    • 删除已存在用户区域  
    • 映射私有区域 
    • 映射共享区域
    • 设置当前进程上下文的程序计数器,使之指向程序入口点
    二、编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体) -> running program

    见附录三

     三、可执行程序的加载

      在object文件中有三种主要的类型:

     1) 可重定位(relocatable)文件:保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
     2) 一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec()如何来创建程序进程映象。
     3)共享object文件:保存着代码和合适的数据,用来被链接器链接。被连接编辑器链接时,可以和其他的可重定位和共享object文件来创建其他的object;被动态链接器链接时,联合一个可执行文件和其他的共享object文件来创建一个进程映象。

      典型的ELF文件有如下图所示的格式:

      当加载器将可执行程序加载到内存中运行时,会根据ELF的段头部表,将可执行文件的代码和数据拷贝到进程的线性空间中,然后跳转到代码的第一条指令处执行。其中,创建可执行文件时,链接器会拷贝一些重定位和符号信息。这样,在执行程序时,加载器会根据.interp节的动态链接器的路径名,加载和运行相应的动态链接器,完成共享库的重定位。之后,将控制传递给应用程序,就可以执行共享库的代码了。通常,linux32的系统中,进程具有如下图所示的地址空间:

    代码段中存放:全局常量(const)、字符串常量、函数以及编译时可决定的某些东西

    数据段(初始化)中存放:初始化的全局变量、初始化的静态变量(全局的和局部的)

    数据段(未初始化)(BSS)中存放:未初始化的全局变量、未初始化的静态变量(全局的和局部的)

    堆中存放:动态分配的区域

    栈中存放:局部变量(初始化以及未初始化的,但不包含静态变量)、局部常量(const)

    附录:

    一、linux 3.3x内核中fork()封装的系统调用:

    arch/x86/syscalls/systcall_32.tbl

    #
    # 32-bit system call numbers and entry vectors
    #
    # The format is:
    # <number> <abi> <name> <entry point> <compat entry point>
    #
    # The abi is always "i386" for this file.
    #
    0       i386    restart_syscall         sys_restart_syscall
    1       i386    exit                    sys_exit
    2       i386    fork                    sys_fork                 stub32_fork

     include/linux/syscalls.h

    193#define SYSCALL_DEFINE0(name)      asmlinkage long sys_##name(void)
    
    855asmlinkage long sys_fork(void);

    kernel/fork.c

    1641
    1642#ifdef __ARCH_WANT_SYS_FORK
    1643SYSCALL_DEFINE0(fork)
    1644{
    1645#ifdef CONFIG_MMU
    1646        return do_fork(SIGCHLD, 0, 0, NULL, NULL);
    1647#else
    1648        /* can not support in nommu mode */
    1649        return(-EINVAL);
    1650#endif
    1651}
    1652#endif

     二、linux 3.3x内核中exec()封装的系统调用

    arch/x86/syscalls/syscall_32.tbl

      2011      i386    execve                  sys_execve                      stub32_execve

    include/linux/syscalls.h

     865asmlinkage long sys_execve(const char __user *filename,
     866                const char __user *const __user *argv,
     867                const char __user *const __user *envp);

    arch/x86/kernel/process.c

    /*
     * sys_execve() executes a new program.
     */
    long sys_execve(const char __user *name,
            const char __user *const __user *argv,
            const char __user *const __user *envp, struct pt_regs *regs)
    {
        long error;
        char *filename;
    
        filename = getname(name);
        error = PTR_ERR(filename);
        if (IS_ERR(filename))
            return error;
        error = do_execve(filename, argv, envp, regs);
    
    #ifdef CONFIG_X86_32
        if (error == 0) {
            /* Make sure we don't return using sysenter.. */
                    set_thread_flag(TIF_IRET);
            }
    #endif
    
        putname(filename);
        return error;
    }

     三、编程实现shell

    1 #include <stdio.h>
      2 #include <sys/types.h>
      3 #include <unistd.h>
      4 #include <stdlib.h>
      5 #include <sys/wait.h>
      6 #include <string.h>
      7 int main(){
      8         pid_t pid;
      9         char path[20];
     10         while(1){               
     11                 printf("print path(q to quit):\n");
     12                 scanf("%s",path);
     13                 if(!strcmp(path,"q"))
     14                         break;
     15                 pid=fork();
     16                 if(pid==0){
     17                         execl(path,NULL);
     18                         printf("child error end\n");
     19                         exit(0);
     20                 }       
     21                 else if(pid>0)
     22                         wait(NULL);
     23                 else{   
     24                         printf("ERROR");
     25                         exit(-1);
     26                 }
     27         } 
     28 } 
  • 相关阅读:
    Synchronized和Lock的实现原理和锁升级
    如何禁止CPU指令重排
    MESI缓存一致性
    归并排序
    强软弱虚四种引用和ThreadLocal内存泄露
    VINS-Mono代码分析与总结(完整版)
    IMU误差模型与校准
    小感
    K8S conul部署
    Centos Consul集群及Acl配置
  • 原文地址:https://www.cnblogs.com/wenxuanguan/p/3105705.html
Copyright © 2020-2023  润新知