2018-2019-1 20189201 《LInux内核原理与分析》第七周作业

我的愿望是 好好学习Linux

图片名称

一、书本第六章知识总结【进程的描述和进程的创建】

基础知识1

  • 操作系统内核实现操作系统的三大管理功能,即进程管理功能,内存管理和文件系统。对应的三个抽象的概念是进程,虚拟内存和文件。
    其中,操作系统最核心的功能是进程管理。
  • 进程标识值:内核通过唯一的PID来标识每个进程。
  • 进程状态:进程描述符中state域描述了进程的当前状态。
  • iret与int 0x80指令对应,一个是离开系统调用弹出寄存器值,一个是进入系统调用压入寄存器的值。
  • fork()函数最大的特点就是被调用一次,返回两次,在父进程中返回新创建子进程的 pid;在子进程中返回 0。
  • 在Linux中,fork,vfork和clone这3个系统调用都通过do_fork来实现进程的创建。
  • 在Linux中1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先。

基础知识2

  • 在操作系统原理中,通过进程控制块PCB描述进程;在Linux内核中,通过一个数据结构struct task_struct来描述进程。
  • 在操作系统原理中,进程有就绪态、运行态和阻塞态;在Linux内核中,就绪态和运行态都是相同的TASK_RUNNING状态另加上一个阻
    塞态。在Linux内核中,当进程是TASK_RUNNING状态时,它是可运行的,就是就绪态,是否在运行取决于它有没有获得CPU的控制权。
  • 对于一个正在运行的进程,调用用户态库函数exit()会陷入内核执行该内核函数do_exit(),进程会进入TASK_ZOMBIE状态,即中止状态,
    Linux内核会在适当的时候把该进程处理掉,后释放进程描述符。一个正在运行的进程在等待特定事件或资源时会进入阻塞态,阻塞态分为两
    种:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE。前者可以被信号和wake_up()唤醒,后者只能被wake_up()唤醒。总结来说分这四种:
  • TASK_RUNNING:包括两种状态:进程就绪且没有运行、进程正在运行。这两种状态的区分取决于进程有没有获得CPU的分配权。
  • TASK_ZOMBIE:进程的终止状态,此状态的进程被称作僵尸进程,在此状态下的进程会被Linux内核在适当的时候处理掉,同时进程描述符也将被释放。
  • TASK_INTERRUPTIBLE :可以被信号或者是wake_up()唤醒。信号来临时,进程会被设置为TASK_RUNNING(仅仅是就绪状态而没有执行)
  • TASK_UNINTERRUPTIBLE:只能被wake_up()唤醒
    进程状态转换图如下图所示:

图片名称

  • 进程控制块PCB——task_struct,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。
struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped   进程状态,-1表示不可执行,0表示可执行,大于1表示停止*/
    void *stack; //内核堆栈
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below  进程标识符 * /
    unsigned int ptrace;

如下图:
1206891-20181125203441637-1087875943.jpg

进程的创建

  • start_ kernel,rest_init,kernel_ init kthreadd;
    start_ kernel创建了rest_init,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;
    另一个是kthreadd内核线程是所有内核线程的祖先,负责管理所有内核线程。
    0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的。
创建进程的三个函数
  • fork,创建子进程。
  • vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。
  • clone,主要用于创建线程。

Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根
据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。

进程创建过程中的四个函数
  • do_fork():创建进程;
  • copy_process():创建进程内容(调用dup_task_struct、信息检查、初始化、更改进程状态、复制其他进程资源、调用copy_thread初始化子进
    程内核栈、设置子进程pid等);
  • dup_task_struct():复制当前进程(父进程)描述符task_struct,分配子进程内核栈;
  • copy_thread():内核栈关键信息初始化;
解析do_fork()

fork、vfork和clone这三个函数最终都是通过do_fork函数实现的。
do_fork的步骤:

  1. 调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
  2. 初始化vfork的完成处理信息(如果是vfork调用)
  3. 调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
  4. 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。
    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;
}

关于copy_process函数
  1. 创建进程描述符以及子进程所需要的其他所有数据结构,为子进程准备运行环境。
  2. 调用dup_task_struct复制一份task_struct结构体,作为子进程的进程描述符。
  3. 复制所有的进程信息。
  4. 调用copy_thread,设置子进程的堆栈信息,为子进程分配一个pid。
关于dup_task_struct
  1. 先调用alloc_task_struct_node分配一个task_struct结构体。
  2. 调用alloc_thread_info_node,分配了一个union。这里分配了一个thread_info结构体,还分配了一个stack数组。
    返回值为ti,实际上就是栈底。
  3. tsk->stack = ti将栈底的地址赋给task的stack变量。
  4. 最后为子进程分配了内核栈空间。
  5. 执行完dup_task_struct之后,子进程和父进程的task结构体,除了stack指针之外,完全相同。
关于copy_thread函数
  1. 获取子进程寄存器信息的存放位置
  2. 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
  3. 如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread, - 将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出
  4. 将父进程的寄存器信息复制给子进程。
  5. 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.
  6. 子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。
新的进程从ret_from_fork处开始执行
  1. dup_task_struct中为其分配了新的堆栈。
  2. copy_process中调用了sched_fork,将其置为TASK_RUNNING。
  3. copy_thread中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。
  4. 将ret_from_fork的地址设置为eip寄存器的值,这是子进程的第一条指令。

整个详细过程:
1206891-20181125203631587-823190379.jpg

二、实验部分【跟踪分析进程创建的过程】

(1)给操作系统MenuOS添加命令(fork功能)

1206891-20181125204610649-890473926.png

1206891-20181125204627270-739771819.png

(2)使用gdb调试跟踪

回到LinuxKernel目录下,fork指令实际上执行的就是sys_clone,我们可以在sys_clone、do_fork、dup_task_struct、copy_process、
copy_thread、ret_from_fork处设置断点,如下图所示:

图片名称
*********************************************
图片名称
*********************************************
图片名称
*********************************************
图片名称
*********************************************
图片名称
最后通过函数syscall_exit退出;

三、实验收获

1. 小总结1

fork,vfork,clone都是linux的系统调用,这三个函数分别调用了sys_fork、sys_vfork、sys_clone,最终都调用了do_fork函数,差别在于参数
的传递和一些基本的准备工作不同,主要用来linux创建新的子进程或线程(vfork创造出来的是线程)。

  • fork()函数调用成功:返回两个值; 父进程:返回子进程的PID;子进程:返回0;失败:返回-1;
    fork 创造的子进程复制了父亲进程的资源(写时复制技术),包括内存的内容task_struct内容(2个进程的pid不同)。
    这里是资源的复制不是指针的复制。

  • vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。
    故vfork创建出来的不是真正意义上的进程,而是一个线程,因为它缺少独立的内存资源。

  • int clone(int (fn)(void ), void child_stack, int flags, void arg)
    • clone和fork的区别:
      1. clone和fork的调用方式很不相同,clone调用需要传入一个函数,该函数在子进程中执行。
      2. clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。 (void *child_stack,)也就是第二个参数,需要分配栈指针
        的空间大小,所以它不再是继承或者复制,而是全新的创造。

2. 小总结2

进程在创建时具有父子关系,通过调用fork()来创建一个新进程。创建的新进程是从return_from_fork开始执行的,复制内核堆栈只复制了一部分,int
指令和save_all压到内核栈的内容。参数,系统调用号等都进行压栈。fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork
来实现进程的创建。

转载于:https://www.cnblogs.com/keady/p/10016815.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值