理解进程创建、可执行文件的加载和进程执行进程切换

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

本实验是基于实验楼的。

源码来源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235;

由源码可知,pcb包含以下内容:

进程基本信息、调度信息、文件系统信息、内存信息、I/O信息等。

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

    // ...

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

1.do_fork处理了以下内容:

(1)调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。

(2)初始化vfork的完成处理信息(如果是vfork调用)

(3)调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。

如果是vfork调用,需要阻塞父进程,知道子进程执行exec。

2.do_fork()流程

(1)首先调用copy_process()为子进程复制出一份进程信息,如果是vfork()则初始化完成处理信息;

(2)然后调用wake_up_new_task将子进程加入调度器,为之分配CPU,如果是vfork(),则父进程等待子进程完成exec替换自己的地址空间。

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

(1)启动Menu OS

cd LinuxKernel

rm menu -rf

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

cd menu

mv test_fork.c test.c

make rootfs

在qemu界面输入fork

(2)gdb调试

gdb

file linux-3.18.6/vmlinux

target remote:1234

b sys_clone

b do_fork

b dup_task_struct

b copy_process

b copy_thread

b ret_from_fork

运行后首先停在sys_clone处

然后到do_fork

再到copy_process

接着进入copy_thread

最后进入ret_form_fork。

ENTRY(ret_from_fork)
    CFI_STARTPROC
    pushl_cfi %eax
    call schedule_tail
    GET_THREAD_INFO(%ebp)
    popl_cfi %eax
    pushl_cfi $0x0202       # Reset kernel eflags
    popfl_cfi
    jmp syscall_exit
    CFI_ENDPROC
END(ret_from_fork)

观察实验结果得知:进程的建立经历了:sys_clone->do_fork->copy_process->copy_thread->ret_form_fork的过程。

二.编程使用exec*库函数加载一个可执行文件

1.编写一个用于输出的hello.c

#include<stdio.h>
int main(){
   printf("Hello World!\n");
return 0;

2.动态编译

(1)生成预处理文件hello.cpp

(2)编译成汇编代码hello.s

(3)编译成目标代码,得到二进制文件hello.o

(4)链接成可执行文件hello

(5)运行一下./hello

 3.静态编译

分析hello.static比hello大的多

4.跟踪do_execve

(1)设置断点

结果如下:

do_execve代码如下:

int do_execve(struct filename *filename,
          const char __user *const __user *__argv,
          const char __user *const __user *__envp)
      {
          struct user_arg_ptr argv = { .ptr.native = __argv };
          struct user_arg_ptr envp = { .ptr.native = __envp };
          //调用do_execve_common
          return do_execve_common(filename, argv, envp);
      }

三.理解进程调度时机并用gdb跟踪分析一个schedule函数

1.调度时机

(1)中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。

(2)内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。

(3)用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

2.gdb跟踪分析

四.实验总结

linux执行过程

1.运行用户态进程U
2.发生中断:save cs:eip/ss:eip/eflags,加载当前进程内核堆栈,跳转至中断处理程序
3.SAVE_ALL,保存现场,完成中断上下文的切换。
4.中断处理过程若调用了schedule函数,其中switch_to做进程上下文的切换。(假设由进程U到进程M)
5.$1f之后,运行用户态进程
6.restore_all,恢复现场
7.iret 从U进程内核堆栈弹出硬件完成的压栈内容,完成中断上下文的切换,即U的内核态到用户态。
8.继续运行U。

 

 

 

 

 

posted @ 2019-03-26 23:22  若我  阅读(242)  评论(0编辑  收藏  举报