Linux实验(三):从整体上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换

学号后三位为478
原创作品转载请注明出处 : https://github.com/mengning/linuxkernel/
一、实验环境
Linux-5.0.1
VMware Workstation Pro
Ubuntu 14.04
二、实验目的
从整体上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换。
三、实验内容
1.task_struct数据结构
为了管理进程,操作系统必须对每个进程所做的事情进行清楚的描述,为此,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块PCB。关键代码和注释如下:

    volatile long state; //表示进程状态
    void *stack; //进程所属堆栈指针
    unsigned int rt_priority;//进程优先级
    int exit_state;//退出时状态
    pid_t pid;//进程号,作为进程的全局标识符
    pid_t tgid;//进程组号
    struct task_struct __rcu *real_parent;//父进程
    struct list_head children;//子进程
    struct list_head sibling;//兄弟进程
    struct task_struct *group_leader;//所属进程组的主进程

2.分析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;

    // ...

    // 复制进程描述符,返回创建的task_struct的指针
    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);

        // 取出task结构体内的pid
        pid = get_task_pid(p, PIDTYPE_PID);
        nr = pid_vnr(pid);

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

        // 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
        if (clone_flags & CLONE_VFORK) {
            p->vfork_done = &vfork;
            init_completion(&vfork);
            get_task_struct(p);
        }

        // 将子进程添加到调度器的队列,使得子进程有机会获得CPU
        wake_up_new_task(p);

        // ...

        // 如果设置了 CLONE_VFORK 则将父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间
        // 保证子进程优先于父进程运行
        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;
}

do_fork():-调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息;初始化vfork的完成处理信息(如果是vfork调用);调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行;如果是vfork调用,需要阻塞父进程,直到子进程执行exec。
copy_process():检查各种标志位(已经省略);调用dup_task_struct复制一份task_struct结构体,作为子进程的进程描述符;检查进程的数量限制;初始化定时器、信号和自旋锁; 初始化与调度有关的数据结构,调用了sched_fork,这里将子进程的state设置为TASK_RUNNING;复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等;调用copy_thread,这又是关键的一步,这里设置了子进程的堆栈信息;为子进程分配一个pid; 设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。
copy_thread():获取子进程寄存器信息的存放位置;对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值; 如果是创建内核线程,那么它的运行位置ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出;将父进程的寄存器信息复制给子进程;将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0;子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。
3.使用gdb跟踪分析一个fork系统调用内核处理函数do_fork()
(1)启动MenuOS

cd LinuxKernel

rm menu -rf

git clone https://github.com/mengning/menu.git

cd menu

mv test_fork.c test.c

make rootfs
(2)gdb调试
gdb

gdb>file LinuxKernel/linux-5.0.1/vmlinux

gdb>target remote:1234
(3)设置断点
b sys_clone

b _do_fork

b dup_task_struct

b copy_process

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
4.理解编译链接的过程和ELF可执行文件格式
(1)编译链接的过程
在这里插入图片描述
(2)ELF可执行文件格式
一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
一个共享object文件保存着代码和合适的数据,用来被不同的两个链接器链接。
5.编程使用exec*库函数加载一个可执行文件
调用链:sys_execve()->do_execve()->do_execveat_common()->__do_execve_file()->prepare_binprm()->search_binary_handler()->load_elf_binary()->start_thread()
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
6.进程调度的时机
中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()
内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
7.使用gdb跟踪分析一个schedule()函数 ,验证对Linux系统进程调度与进程切换过程的理解
首先设置断点,schedul函数选择一个新的进程来运行,并调用context_switch进行上下文的切换。context_switch首先调用switch_mm切换CR3,然后调用宏switch_to来进行硬件上的上下文切换。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
8.特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系
调用关系:schedule() --> context_switch() --> switch_to --> switch_to()
switch_to实现了进程之间的切换:
首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度。通过jmp指令(而不是call指令)转入一个函数__switch_to()恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。
四.总结
对Linux系统的执行过程的理解:
在调度时机方面,内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
schedule()函数实现进程调度,context
switch完成进程上下文切换,switch
to完成寄存器的切换。
用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值