linux实验六

一.task_struct数据结构

为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。

linux实验六_第1张图片
进程描述符

state表示当前进程的状态,当state为-1时表示不可执行,为0时表示可以执行,大于1时表示停止。
stack指向进程堆栈,Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_ info和进程的内核堆栈。
flags是进程标示符。

linux实验六_第2张图片
多对称编译及优先级相关变量定义.png

所有进程链表struct list_head tasks


linux实验六_第3张图片
定义进程链表

程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系。

linux实验六_第4张图片
父子关系相关
linux实验六_第5张图片
将进程链表及pid哈希表连接起来
linux实验六_第6张图片
进程上下文切换相关

二.进程创建

创建一个新进程在内核中的执行过程
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
复制一个PCB——task_struct
err = arch_dup_task_struct(tsk, orig);
要给新进程分配一个新的内核堆栈
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,父进程从系统调用中返回比较容易理解,子进程从系统调用中返回,那它在系统调用处理过程中的哪里开始执行的呢?这就涉及子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题,这是在哪里设定的?copy_thread in copy_process
*childregs = *current_pt_regs(); //复制内核堆栈
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址

进程创建相关代码

{
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
#endif
SYSCALL_DEFINE0(vfork)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL);
}

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

fork、vfork和clone这三个函数最终都是通过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;
}```

由以上代码可以看出,do_ fork大概做了如下几件事:

调用copy_ process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
初始化vfork的完成处理信息(如果是vfork调用)
调用wake_ up_ new_ task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
如果是vfork调用,需要阻塞父进程,知道子进程执行exec。上面的过程对vfork稍微做了处理,因为vfork必须保证子进程优先运行,执行exec,替换自己的地址空间。

```/*
    创建进程描述符以及子进程所需要的其他所有数据结构
    为子进程准备运行环境
*/
static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;

    // 分配一个新的task_struct,此时的p与当前进程的task,仅仅是stack地址不同
    p = dup_task_struct(current);

    // 检查该用户的进程数是否超过限制
    if (atomic_read(&p->real_cred->user->processes) >=
            task_rlimit(p, RLIMIT_NPROC)) {
        // 检查该用户是否具有相关权限,不一定是root
        if (p->real_cred->user != INIT_USER &&
            !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
            goto bad_fork_free;
    }

    retval = -EAGAIN;
    // 检查进程数量是否超过 max_threads,后者取决于内存的大小
    if (nr_threads >= max_threads)
        goto bad_fork_cleanup_count;

    // 初始化自旋锁

    // 初始化挂起信号

    // 初始化定时器

    // 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING
    retval = sched_fork(clone_flags, p);
    // .....

    // 复制所有的进程信息
    // copy_xyz

    // 初始化子进程的内核栈
    retval = copy_thread(clone_flags, stack_start, stack_size, p);
    if (retval)
        goto bad_fork_cleanup_io;

    if (pid != &init_struct_pid) {
        retval = -ENOMEM;
        // 这里为子进程分配了新的pid号
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (!pid)
            goto bad_fork_cleanup_io;
    }

    /* ok, now we should be set up.. */
    // 设置子进程的pid
    p->pid = pid_nr(pid);
    // 如果是创建线程
    if (clone_flags & CLONE_THREAD) {
        p->exit_signal = -1;
        // 线程组的leader设置为当前线程的leader
        p->group_leader = current->group_leader;
        // tgid是当前线程组的id,也就是main进程的pid
        p->tgid = current->tgid;
    } else {
        if (clone_flags & CLONE_PARENT)
            p->exit_signal = current->group_leader->exit_signal;
        else
            p->exit_signal = (clone_flags & CSIGNAL);
        // 创建的是进程,自己是一个单独的线程组
        p->group_leader = p;
        // tgid和pid相同
        p->tgid = p->pid;
    }

    if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
        // 如果是创建线程,那么同一线程组内的所有线程、进程共享parent
        p->real_parent = current->real_parent;
        p->parent_exec_id = current->parent_exec_id;
    } else {
        // 如果是创建进程,当前进程就是子进程的parent
        p->real_parent = current;
        p->parent_exec_id = current->self_exec_id;
    }

    // 将pid加入PIDTYPE_PID这个散列表
    attach_pid(p, PIDTYPE_PID);
    // 递增 nr_threads的值
    nr_threads++;

    // 返回被创建的task结构体指针
    return p;
}```

###三.实验过程
首先更新menu,然后用test_fork覆盖test.c
![更新menu](http://upload-images.jianshu.io/upload_images/5179974-59f04b3909da0f4f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
重新编译并运行
![运行](http://upload-images.jianshu.io/upload_images/5179974-9103f0b977c75ab0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

执行命令qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
使用gdb跟踪调试

![跟踪调试](http://upload-images.jianshu.io/upload_images/5179974-ad3b74894804d1d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![设置断点](http://upload-images.jianshu.io/upload_images/5179974-16a0c2d494ebcd62.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


![调试结果](http://upload-images.jianshu.io/upload_images/5179974-4d7f9b43e43822a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


![调试结果](http://upload-images.jianshu.io/upload_images/5179974-e8c03ed99b84d309.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

按上述步骤操作会卡在do_fork无法开启内核,应先在sys_time设置一个断点,进入menu以后再设置sys_clone,do_fork等断点,因为系统开始时初始化进程时要用到以上调用,所以会导致断点位置错误.

![设置sys_time断点](http://upload-images.jianshu.io/upload_images/5179974-687228c9d7d7fc27.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![设置sys_clone等断点](http://upload-images.jianshu.io/upload_images/5179974-0b8d76cc2ce93993.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![跟踪调试过程](http://upload-images.jianshu.io/upload_images/5179974-fd93e4dc13d7f270.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


![单步调试](http://upload-images.jianshu.io/upload_images/5179974-9d313f533ee30667.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

你可能感兴趣的:(linux实验六)