进程的创建和调度分析

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

关于task_struct数据结构

每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。task_struct是Linux内核的一种数据结构,它会被装载到RAM中并且包含着进程的信息。每个进程都把它的信息放在 task_struct 这个数据结构体,task_struct 部分内容如下: 

 struct task_struct{
    pid_t pid;                  //进程id
    uid_t uid,euid;
    gid_t gid,egid;
    volatile long state;        //进程状态,0 running(运行/就绪);1/2  均等待态,分别响应/不响应异步信号;
//4 僵尸态,Linux特有,为生命周期已终止,但PCB未释放;8 暂停态,可被恢复
int exit_state; //退出的状态 unsigned int rt_priority; //调度优先级 unsigned int policy; //调度策略 struct list_head tasks; struct task_struct *real_parent; struct task_struct *parent; struct list_head children,sibling; struct fs_struct *fs; //进程与文件系统管理,进程工作的目录与根目录 struct files_struct *files; //进程对所有打开文件的组织,存储指向文件的句柄们 struct mm_struct *mm; //内存管理组织,存储了进程在用户空间不同的地址空间,可能存的数据,可能代码段 struct signal_struct *signal; //进程间通信机制--信号 struct sighand_struct *sighand; //指向进程 cputime_t utime, stime; //进程在用户态、内核态下所经历的节拍数 struct timespec start_time; //进程创建时间 struct timespec real_start_time; //包括睡眠时间的创建时间 }

关于do_fork()函数调用

Linux中创建进程有三个函数fork(),vfork()和clone(),do_fork()函数对应其系统调用。调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息之后将子进程压入队列,等待被调度。以下为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;
}

 

通过gdb跟踪系统调用内核处理函数do_fork()

输入命令:

rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs

然后进行gdb调试:

b sys_clone
b _do_fork
b dup_task_struct
b copy_process

调试效果如上图所示。

理解编译链接的过程和ELF可执行文件格式

1、 编译与链接的过程: 

从源代码到可执行程序所要经历的过程概述如下图:

   源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。

2、 ELF可执行文件

ELF文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
1.可执行文件(应用程序)可执行文件包含了代码和数据,是可以直接运行的程序。
2.可重定向文件(.o)可重定向文件又称为目标文件,它包含了代码和数据(这些数据是和其他重定位文件和共享的object文件一起连接时使用的)。
.o文件参与程序的连接(创建一个程序)和程序的执行(运行一个程序),它提供了一个方便有效的方法来用并行的视角看待文件的内容,这些.o文件的活动可以反映出不同的需要。
Linux下,我们可以用gcc -c编译源文件时可将其编译成.o格式。
3.共享文件(*.so)也称为动态库文件,它包含了代码和数据(这些数据是在连接时候被连接器ld和运行时动态连接器使用的)。动态连接器可能称为ld.so.1,libc.so.1或者 ld-linux.so.1。

实验总结

所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。

 

原文地址:https://www.cnblogs.com/xiguas/p/10604320.html