《Linux内核分析》第六周学习笔记

《Linux内核分析》第六周学习笔记 进程的描述和创建

郭垚 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

【学习视频时间:1小时 撰写博客时间:2小时】

【学习内容:进程创建的过程、使用gdb跟踪分析内核处理函数sys_clone】

一、进程的描述

1.1 进程描述符task_struct数据结构(一)

1. 进程控制块PCB——task_struct

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

struct task_struct{
    volatile long state; //进程状态,-1表示不可执行,0表示可执行,大于1表示停止
    void *stack; //内核堆栈
    atomic_t usage;
    unsigned int flags; //进程标识符
    unsigned int ptrace;
    ……
}

struct task_ struct数据结构很庞大

  • Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_ RUNNING,为什么呢?
  • 进程的标示pid
  • 所有进程链表struct list_ head tasks;
    • 内核的双向循环链表的实现方法
    • 一个更简略的双向循环链表
  • 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
  • Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_ info和进程的内核堆栈
    • 进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?
    • 内核控制路径所用的堆栈很少,因此对栈和Thread_ info来说,8KB足够了
  • struct thread_ struct thread; //CPU-specific state of this task
  • 文件系统和文件描述符
  • 内存管理——进程的地址空间

Linux进程状态转换图

1.2 进程描述符task_struct数据结构(二)

1242#ifdef CONFIG_SMP //条件编译,多处理器会用到
1243    struct llist_node wake_entry;
1244    int on_cpu;
1245    struct task_struct *last_wakee;
1246    unsigned long wakee_flips;
1247    unsigned long wakee_flip_decay_ts;
1248
1249    int wake_cpu;
1250#endif
1251    int on_rq;
1252
1253    int prio, static_prio, normal_prio;
1254    unsigned int rt_priority; //与优先级相关
1255    const struct sched_class *sched_class;
1256    struct sched_entity se;
1257    struct sched_rt_entity rt;

……

1295    struct list_head tasks; //进程链表
1296#ifdef CONFIG_SMP
1297    struct plist_node pushable_tasks;
1298    struct rb_node pushable_dl_tasks;
1299#endif

Linux的状态图

1301    struct mm_struct *mm, *active_mm; //内存管理进程的地址空间
1302#ifdef CONFIG_COMPAT_BRK
1303    unsigned brk_randomized:1;
1304#endif
1305    /* per-thread vma caching */
1306    u32 vmacache_seqnum;
1307    struct vm_area_struct *vmacache[VMACACHE_SIZE];
1308#if defined(SPLIT_RSS_COUNTING)
1309    struct task_rss_stat    rss_stat;
1310#endif
1311/* task state */
1312    int exit_state;  //任务的状态
1313    int exit_code, exit_signal;
1314    int pdeath_signal;  /*  The signal sent when the parent dies  */
1315    unsigned int jobctl;    /* JOBCTL_*, siglock protected */
1316
1317    /* Used for emulating ABI behavior of previous Linux versions */
1318    unsigned int personality;
1319
1320    unsigned in_execve:1;   /* Tell the LSMs that the process is doing an
1321                 * execve */
1322    unsigned in_iowait:1;

……

1337    //进程的父子关系
1338     * pointers to (original) parent process, youngest child, younger sibling,
1339     * older sibling, respectively.  (p->father can be replaced with
1340     * p->real_parent->pid)
1341     */
1342    struct task_struct __rcu *real_parent; /* real parent process */
1343    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
1344    /*
1345     * children/sibling forms the list of my natural children
1346     */
1347    struct list_head children;  /* list of my children */
1348    struct list_head sibling;   /* linkage in my parent‘s children list */
1349    struct task_struct *group_leader;   /* threadgroup leader */

1356    struct list_head ptraced; //调试
1357    struct list_head ptrace_entry;
1358
1359    /* PID/PID hash table linkage. */
1360    struct pid_link pids[PIDTYPE_MAX];
1361    struct list_head thread_group;
1362    struct list_head thread_node; //将进程链表连接起来

……

1411/* 与当前任务CPU状态相关,对进程上下文切换有关键性作用 */
1412    struct thread_struct thread;
1413/* filesystem information */
1414    struct fs_struct *fs;
1415/* 打开文件描述符列表 */
1416    struct files_struct *files;
1417/* namespaces */
1418    struct nsproxy *nsproxy;
1419/* 信号处理 */
1420    struct signal_struct *signal;
1421    struct sighand_struct *sighand;
1422
1423    sigset_t blocked, real_blocked;
1424    sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
1425    struct sigpending pending;
……
1598    struct pipe_inode_info *splice_pipe;
1599    //与管道相关
1600    struct page_frag task_frag;
1601
1602#ifdef  CONFIG_TASK_DELAY_ACCT
1603    struct task_delay_info *delays;
1604#endif
1605#ifdef CONFIG_FAULT_INJECTION
1606    int make_it_fail;
1607#endif

二、进程的创建

2.1 进程的创建概览及fork一个进程的用户态代码

回顾:

  • 道生一(start_ kernel...cpu_ idle)
  • 一生二(kernel_ init和kthreadd)
  • 二生三(即前面的0、1、2三个进程)
  • 三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
  • start_ kernel创建了cpu_ idle,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的
  • iret与int 0x80指令对应,一个是弹出寄存器值,一个是压入寄存器的值
  • 如果将系统调用类比于fork();那么就相当于系统调用创建了一个子进程,然后子进程返回之后将在内核态运行,而返回到父进程后仍然在用户态运行

fork一个子进程的代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0)
    {
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    }
    else if (pid == 0) //pid == 0和下面的else都会被执行到(一个是在父进程中即pid ==0的情况,一个是在子进程中,即pid不等于0)

    {
        /* child process */
        printf("This is Child Process!\n");
    }
    else
    {
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

2.2 理解进程创建过程复杂代码的方法

Linux中创建进程一共有三个函数:

  • fork,创建子进程
  • vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。
  • clone,主要用于创建线程

  这里值得注意的是,Linux中得线程是通过模拟进程实现的,较新的内核使用的线程库一般都是NPTL。

  • 复制一个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(),函数返回了两次,即在父子进程中各返回一次。

2.3 浏览进程创建过程相关的关键代码

通过之前的学习,我们知道fork是通过触发0x80中断,陷入内核,来使用内核提供的提供调用:

SYSCALL_DEFINE0(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大概做了如下几件事:

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

抛开vfork,进程创建的大部分过程都在copy_ process函数中copy_process的代码非常复杂,这里我精简了大部分,只留下最重要的一些:

/*
    创建进程描述符以及子进程所需要的其他所有数据结构
    为子进程准备运行环境
*/
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;
}

根据这份精简代码,我总结出copy_process的大体流程

  1. 检查各种标志位(已经省略)
  2. 调用dup_ task_ struct复制一份task_ struct结构体,作为子进程的进程描述符。
  3. 检查进程的数量限制。
  4. 初始化定时器、信号和自旋锁。
  5. 初始化与调度有关的数据结构,调用了sched_ fork,这里将子进程的state设置为TASK_ RUNNING。
  6. 复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。
  7. 调用copy_ thread,这又是关键的一步,这里设置了子进程的堆栈信息。
  8. 为子进程分配一个pid
  9. 设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。

2.4 创建的新进程从哪里开始执行?

一个新创建的子进程,(当它获得CPU之后)是从哪一行代码进程执行的?

  • 与之前写过的my_ kernel相比较,kernel中是可以指定新进程开始的位置(也就是通过eip寄存器指定代码行)。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; //调度到子进程时的第一条指令地址

2.5 使用gdb跟踪创建新进程的过程(实验)

1. 更新menu,删除test_fork.c和test.c文件,重新执行make rootfs

2. 编译内核查看fork命令

3. 启动gdb调试,并对主要的函数设置断点

执行一个fork,会发现只输出一个fork的命令描述,后面并没有执行,因为它停在了sys_ clone这个位置。

4. 特别关注新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?即执行起点与内核堆栈如何保证一致。

  答:ret_ from_ fork决定了新进程的第一条指令地址。子进程从ret_ from_ fork处开始执行。因为在ret_ from_ fork之前,也就是在copy_ thread()函数中* childregs = * current_ pt_ regs();该句将父进程的regs参数赋值到子进程的内核堆栈。* childregs的类型为pt_ regs,里面存放了SAVE_ ALL中压入栈的参数,因此在之后的RESTORE ALL中能顺利执行下去。

总结

  Linux中所有的进程创建都是基于复制的方式,Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。fork()函数最大的特点就是被调用一次,返回两次。

时间: 2024-10-23 14:41:55

《Linux内核分析》第六周学习笔记的相关文章

《深入理解计算机系统》第六周学习笔记

第四章 处理器体系结构 (一)知识点总结 一.Y86指令集体系结构 1.Y86处理器状态类似于IA32,有8个程序寄存器: %eax.%ecx.%edx.%ebx.%esi.%edi.%esp.%ebp.处理器的每个程序寄存器存储一个字.%esp被入栈.出栈.调用和返回指令作为栈指针. 2.3个一位的条件吗:ZF.SF.OF,它们保存最近的算术或逻辑指令所造成影响的有关信息.程序计数器PC存放当前正在执行指令的地址. 3.程序状态的最后一个部分是状态码stat,它表明程序执行的总体状态 4.Y8

软件测试第六周学习笔记之“Win8 APP应用程序的白盒测试”

这周的学习笔记我想写点自己关于实验中碰到的问题和感想. 因为这次做的是白盒测试,所以我决定去测试一下上回测试的app的功能函数. 这次我用的是单元测试项目来做的白盒测试: 创建单元测试的步骤: 1.点击 “文件”->“添加”->“新建项目” 2.选择 windows应用程序-> 单元测试项目 3.在解决方案资源管理器中为单元测试项目下的引用上右击选择添加引用 4.选择解决方案下的项目中的用来测试的win8应用 接下来是单元测试的代码部分的编写了: 我要测试的是该项目中的一个字符串转化编码

第十六周学习笔记

本周的学习笔记主要关于MY SQL日志 1.查询日志是否开启 2.查看慢查询日志 3.查看二进制日志 4.删除所有二进制日志文件 RESET MASTER; 5.只删除部分二进制日志文件 PURGE MASTER LOGS; 原文地址:https://www.cnblogs.com/lzy35/p/8111445.html

Linux内核分析——第六周学习笔记

进程的描述和进程的创建 前言:以下笔记除了一些讲解视频中的概念记录,图示.图示中的补充文字.总结.分析.小结部分均是个人理解.如有错误观点,请多指教! PS.实验操作会在提交到MOOC网站的博客中写.

Linux内核分析——第六周学习笔记20135308

第五周 进程的描述和进程的创建 一.进程描述符task_struct数据结构 1.操作系统三大功能 进程管理 内存管理 文件系统 2.进程控制块PCB——task_struct 也叫进程描述符,为了管理进程,内核需要对每个进程进行描述,它就提供了内核所需了解的进程信息. struct task_struct数据结构很庞大,1235行~1644行 3.Linux进程状态 Linux进程的状态与操作系统原理中的描述的进程状态有所不同 操作系统状态: 就绪态 运行态 阻塞态 linux进程状态: 4.

《机电传动控制》第六周学习笔记

这门课与之前学习的课程的联系: 1.首先这门课有两门基础的课程,就是<大学物理>和<电路基础>,通过这两门课程的学习基础,才能去理解<机电传动控制>课程内的一些原理性内容.比如说对直流电动机电路的分析,以及对三相交流电机的分析 ,这在之前的课程里都有提到过,因此有十分紧密的联系. 2.这门课学习的知识可以运用到<数控技术>中,如数控机床中经常会用到步进电机和伺服电机,因此学好电机,方便对数控机床进行理解. 3.这门课的学习与<工程控制理论>这门课

《深入理解计算机系统》第四周学习笔记

一.知识点总结 1.信息存储 练习题2.4 0x503c+0x8=0x5044 0x503c-0x40=0x4ffc 0x503c+64=0x503c+0x40=0x507c 0x50ea-0x503c=0xae 1)字长:指明整数和指针数据的标称大小.一个字长为w的机器的虚拟地址范围为0~2^(w-1),程序最多访问2^w个字节 int .char 4字节,单精度float 字节,双精度double 8字节 2)小端法:最低有效字节在最前面的顺序存储 大端法:最高有效字节在最前面的顺序存储 练

深入理解计算机系统之存储器层次结构学习笔记

一.存储技术 (一)随机访问存储器 随机访问寄存器(RAM)分为静态随机访问寄存器(SRAM)和动态随机访问寄存器(DRAM).静态RAM可以作为高速缓存寄存器,动态RAM可以用作主存以及图形系统的帧缓冲区.静态RAM将每一个位存储在一个双稳态的存储器单元里,构成静态RAM 的电路可以无限期的保持在两个不同的电压配置或状态之一.动态RAM将每一个位存储为对电容的充电,所以动态RAM要比静态RAM对干扰的敏感度更高.构成动态RAM的电路被干扰后就不会恢复了. 1 传统的DRAM 常规DRAM芯片中

机电传动控制第六周学习笔记——《机电传动控制》和其他课程的联系

机电传动控制就目前学习的情况来看电学方面的东西比较多,但是与其他非电类的课程也有很多联系. 1.电机原理的部分主要是和基础物理学里面的内容有很大关系,比如电磁感应,楞次定律等等,从基础的物理原理讲起: 2.进入控制部分以后我们见到了很多电机,建模的时候有数学建模的思想,利用自定义的参数的变化得到电机随时间的运行状况仿真,并通过探讨这些参数的敏感性牺牲不敏感参数找到整体的最优解: 3.编程的思想和C语言很类似.逻辑性和面向过程编程都很重要: 4.控制时使用的继电器-接触器控制电路与电路的分析很类似