第一次作业:深入Linux源码分析进程模型

时间:2022-01-19 07:29:54

 

一.进程的概念

     第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

     第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程

其中在Linux内核中赋予它更通用的名称----任务(task)

  进程在整个内核中的功能位置:

第一次作业:深入Linux源码分析进程模型

  我们还可以分两个层次对操作系统进程进行讨论。 

  在较高的层次上,“进程”是一个重要的组织概念,用其说明一个计算机系统作为一个整体的活动。将计算机系统视作若干进程的组合活动是适合的,每一个进程与一道特定的程序相结合。例如“shell”或者“vi”编辑程序。在这一层次上,进程本身被视作系统中的活动实体,而真正的活动部件本体,即处理机和外部设备则被消隐,不引起人们的注意。进程诞生、生长,然后死亡;它们存在的数量在不断变化;它们可以获得并释放资源;它们可以交互作用、合作、冲突、共享资源等等。 

  在较低的层次上,进程是不活动的实体,它们依靠活动的实体,例如处理机才起作用。借助于频繁地使用处理机从一个进程映像的执行切换到另一个,就可以产生一种印象:每一个进程映像都连续发生变化,这就导致较高层次上的解释。

       Linux进程的四个要素:

  1.有一段程序供其执行,这段程序不一定是某个进程所专有的,可以与其他进程共用。

  2.有进程专用的内核空间堆栈。

  3.在内核中有一个task_struct数据结构,即通常所说的“进程控制块”。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。同时,这个结构还记录着进程所占用的各项资源。

  4.有独立的存储空间,这意味着拥有专有的用户空间;进一步,还意味着除前述的内核空间堆栈外还有其专用的用户空间堆栈。有一点必须指出,内核空间是不能独立的,任何进程都不可能直接(不通过系统调用)改变内核空间的内容(除其本身的内核空间堆栈以外)。

二.进程的组织:

进程控制块

进程创建时,操作系统就新建一个PCB结构,它之后就常驻内存,任一时刻可以存取, 在进程结束时删除。PCB是进程实体的一部分,是进程存在的唯一标志。

当创建一个进程时,系统为该进程建立一个PCB;当进程执行时,系统通过其PCB 了 解进程的现行状态信息,以便对其进行控制和管理;当进程结束时,系统收回其PCB,该进 程随之消亡。操作系统通过PCB表来管理和控制进程。

表2-1 PCB通常包含的内容
进程描述信息 进程控制和管理信息 资源分配清单 处理机相关信息
进程标识符(PID) 进程当前状态 代码段指针 通用寄存器值
用户标识符(UID) 进程优先级 数据段指针 地址寄存器值
  代码运行入口地址 堆栈段指针 控制寄存器值
  程序的外存地址 文件描述符 标志寄存器值
  进入内存时间 键盘 状态字
  处理机占用时间 鼠标  
  信号量使用    

表2-1是一个PCB的实例,PCB主要包括进程描述信息、进程控制和管理信息、资源 分配清单和处理机相关信息等。各部分的主要说明如下:

1) 进程描述信息
进程标识符:标志各个进程,每个进程都有一个并且是唯一的标识号。
用户标识符:进程归属的用户,用户标识符主要为共享和保护服务。

2) 进程控制和管理信息
进程当前状态:描述进程的状态信息,作为处理机分配调度的依据。
进程优先级:描述进程抢占处理机的优先级,优先级高的进程可以优先获得处理机。

3) 资源分配清单,用于说明有关内存地址空间或虚拟地址空间的状况;所打开文件的 列表和所使用的输入/输出设备信息。

4) 处理机相关信息,主要指处理机中各寄存器值,当进程被切换时,处理机状态信息 都必须保存在相应的PCB中,以便在该进程重新执行时,能再从断点继续执行。

在一个系统中,通常存在着许多进程,有的处于就绪状态,有的处于阻塞状态,而且阻塞的原因各不相同。为了方便进程的调度和管理,需要将各进程的PCB用适当的方法组织起来。目前,常用的组织方式有链接方式和索引方式两种。链接方式将同一状态的PCB链接成一个队列,不同状态对应不同的队列,也可以把处于阻塞状态的进程的PCB,根据其阻塞原因的不同,排成多个阻塞队列。索引方式是将同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB,不同状态对应不同的索引表,如就绪索引表和阻塞索引表等。
大量的进程是如何组织的:
第一次作业:深入Linux源码分析进程模型
/* wq为某个等待队列的队列头 */
void sleep_on (wait_queue_head_t *wq)
{
    /* 声明一个等待队列结点 */
    wait_queue_t wait;

    /* 用当前进程初始化这个等待队列结点 */
    init_waitqueue_entry (&wait, current);

    /* 设置当前进程状态为TASK_UNINTERRUPTIBLE */
    current->state = TASK_UNINTERRUPTIBLE;

    /* 将这个代表着当前进程的等待队列结点加入到wq这个等待队列 */
    add_wait_queue (wq, &wait);

    /* 请求调度器进行调度,执行完schedule后进程会被移除CPU运行队列,只有等待队列唤醒后才会重新回到CPU运行队列 */
    schedule ();

    /* 这里进程已经被等待队列唤醒,重新移到CPU运行队列,也就是等待的条件已经为真,唤醒后第一件事就是将自己从等待队列wq中移除 */
    remove_wait_queue (wq, &wait);  
}
           
     在Linux操作系统中,用户创建一个新进程的一个方法是调用系统调用fork。调用fork的进程是父进程(parent process),而新创建的进程是子进程(child  process)。在系统中调用fork返回时,子进程是父进程的一个拷贝,两个进程除了返回PID(Process ID)不同外,具有完全一样的变量值,它们打开的文件都相同。而在系统初启时由内核内部地创建的#0进程(idle进程)是唯一不通过fork而创建的进程;#1进程是系统创建的init进而在系统初启时由内核内部地创建的#0进程(idle进程)是唯一不通过fork而创建的进程;#1进程是系统创建的init进程,是系统其他每个进程的父进程。在Linux中,fork和_clone函数的具体实现是通过do_fork函数来实现的。
do_fork的算法如下:
int do_fork(unsigned long clone_flags,unsigned long usp,struct pt_regs *regs)
{
   取一个空闲的task数组表项和唯一的PID号;
    根据clone_flags参数的值将父进程的进程表现拷贝到子进程表项中或设置为共享;
   把进程加入进程图表设置跟踪进程的数据结构
   调用hash_pid把新进程置入pidhash表中;
   调用wake_up_process设进程为TASK_RUNNING并置入运行队列;
 return(p->pid)
}

在创建新进程后,我们需要它来处理其他实际的工作,通过调用exec来执行别的实际程序,就能够变成独立于其他进程的进程了,因此,创建一个真正的进程--与其祖先不同的程序镜像,要分为两步,一步是fork,另一步是exec.下面是C代码描述:

/*实验fork和exec*/
if((result=fork()==0)
{
/*child code*/
 if(exec(  "new_program")<0)
    perror("exec failed");
    exit(1);
}
else if(result<0)
{
perror ("fork failde");
}

三.进程状态转换

在一个给定时刻内,进程处于下面六种状态之一,进程的当前状态被记录在struct task_struct结构中的state成员中

struct task_struct { 
 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */    …… }; 

/include/linux/sched.h定义的进程状态:

#define TASK_RUNNING 0 /* 进程准备好运行 */
#define TASK_INTERRUPTIBLE   1  /* 等待特定事件,可以被信号中断  */ 
#define TASK_UNINTERRUPTIBLE  2  /* 等待特定硬件条件,不可以被信号中断*/ 
#define TASK_ZOMBIE        4  /* 进程已经退出  */ 
#define TASK_STOPPED        8  /* 进程已经停止运行  */ 
#define TASK_SWAPPING       16  /* 进程正在执行磁盘交换工作  */ 

第一次作业:深入Linux源码分析进程模型

何时刻一个处理机仅能执行一个进程,而可能不止一个进程处于TASK_RUNNING状态。TASK_RUNNING并不意味着该进程可以立即获得CPU(虽然有时候是这样),而是仅仅说明只要CPU一旦可用,进程就可以立即准备执行了。进程处于TASK_ZOMBIE状态,意味进程已经退出了(或已经被杀掉了),但是其相关的struct task_struct结构并没有被删除。这样即使子进程已经退出,也允许父进程对已经死去的子孙进程进行查询。父进程通过wait来获取TASK_ZOMBIE进程的信息,同时释放它占用的struct task_struct结构。

第一次作业:深入Linux源码分析进程模型

 

四.task_struct数据结构

第一次作业:深入Linux源码分析进程模型

 

 

struct task_struct
{
    volatile long state;                                                    /*state成员的可能取值如下
                                         #define TASK_RUNNING 0
                                         
#define TASK_INTERRUPTIBLE 1
                                       #define TASK_UNINTERRUPTIBLE 2
                                       #define TASK_STOPPED 4
                                       
#define TASK_TRACED 8
                                       #define EXIT_DEAD 16                                        #define EXIT_ZOMBIE 32                                        #define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
                             
#define TASK_DEAD 64                                        #define TASK_WAKEKILL 128                                        #define TASK_WAKING 256                                        #define TASK_PARKED 512                                        #define TASK_NOLOAD 1024                                        #define TASK_STATE_MAX 2048                                        #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)                                        #define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)                                        #define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)*/
struct list_head run_list; struct task_struct * next_task,*prev_task; pid_t pid; struct task_struct*p_opptr,*P_pptr;/*1.p_opptr指向进程的原始祖先 2.p_pptr指向进程的当前祖先 3.p_cptr指向进程的最年轻子孙 4.p_ysptr指向进程的下一个最年轻兄弟 5.p_osptr指向进程的下一个最古老兄弟*/ *p_cptr,*p_ysptr,*p_osptr; struct task_struct*pidhash_next; struct task_struct**pidhash_pprev; }

进程标识符:

 pid_t pid; //进程的标识符

pid_t tgid; //线程组标识符

进程标记符:

unsigned int flags; /* per process flags, defined below */ 

 五.Linux的调度

调度器介绍

  随着时代的发展,linux也从其初始版本稳步发展到今天,从2.4的非抢占内核发展到今天的可抢占内核,调度器无论从代码结构还是设计思想上也都发生了翻天覆地的变化,其普通进程的调度算法也从O(1)到现在的CFS,一个好的调度算法应当考虑以下几个方面:
  • 公平:保证每个进程得到合理的CPU时间。
  • 高效:使CPU保持忙碌状态,即总是有进程在CPU上运行。
  • 响应时间:使交互用户的响应时间尽可能短。
  • 周转时间:使批处理用户等待输出的时间尽可能短。
  • 吞吐量:使单位时间内处理的进程数量尽可能多。
  • 负载均衡:在多核多处理器系统中提供更高的性能
  而整个调度系统至少包含两种调度算法,是分别针对 实时 进程普通进程,所以在整个linux内核中,实时进程和普通进程是并存的,但它们使用的调度算法并不相同,普通进程使用的是CFS调度算法(红黑树调度)。之后会介绍调度器是怎么调度这两种进程。
在linux中,进程主要分为两种,一种为实时进程,一种为普通进程
  • 实时进程:对系统的响应时间要求很高,它们需要短的响应时间,并且这个时间的变化非常小,典型的实时进程有音乐播放器,视频播放器等。
  • 普通进程:包括交互进程和非交互进程,交互进程如文本编辑器,它会不断的休眠,又不断地通过鼠标键盘进行唤醒,而非交互进程就如后台维护进程,他们对IO,响应时间没有很高的要求,比如编译器。
  它们在linux内核运行时是共存的,实时进程的优先级为0~99,实时进程优先级不会在运行期间改变(静态优先级),而普通进程的优先级为100~139,普通进程的优先级会在内核运行期间进行相应的改变(动态优先级)。

调度策略

  在linux系统中,调度策略分为
  • SCHED_NORMAL:普通进程使用的调度策略,现在此调度策略使用的是CFS调度器。
  • SCHED_FIFO:实时进程使用的调度策略,此调度策略的进程一旦使用CPU则一直运行,直到有比其更高优先级的实时进程进入队列,或者其自动放弃CPU,适用于时间性要求比较高,但每次运行时间比较短的进程。
  • SCHED_RR:实时进程使用的时间片轮转法策略,实时进程的时间片用完后,调度器将其放到队列末尾,这样每个实时进程都可以执行一段时间。适用于每次运行时间比较长的实时进程

  

调度

  首先,我们需要清楚,什么样的进程会进入调度器进行选择,就是处于TASK_RUNNING状态的进程,而其他状态下的进程都不会进入调度器进行调度。系统发生调度的时机如下
  • 调用cond_resched()时
  • 显式调用schedule()时
  • 从系统调用或者异常中断返回用户空间时
  • 从中断上下文返回用户空间

管理组调度,内核引进了struct task_group结构,如下:

 /* 进程组,用于实现组调度 */
  struct task_group {
     /* 用于进程找到其所属进程组结构 */
     struct cgroup_subsys_state css;
  
  #ifdef CONFIG_FAIR_GROUP_SCHED
     /* CFS调度器的进程组变量,在 alloc_fair_sched_group() 中进程初始化及分配内存 */
     /* 该进程组在每个CPU上都有对应的一个调度实体,因为有可能此进程组同时在两个CPU上运行(它的A进程在CPU0上运行,B进程在CPU1上运行) */
     struct sched_entity **se;
    /* 进程组在每个CPU上都有一个CFS运行队列(为什么需要,稍后解释) */
     struct cfs_rq **cfs_rq;
    /* 用于保存优先级默认为NICE 0的优先级 */
     unsigned long shares;

 #ifdef    CONFIG_SMP
     atomic_long_t load_avg;
     atomic_t runnable_avg;
 #endif
 #endif
 
 #ifdef CONFIG_RT_GROUP_SCHED
     /* 实时进程调度器的进程组变量,同 CFS */
     struct sched_rt_entity **rt_se;
     struct rt_rq **rt_rq;
 
     struct rt_bandwidth rt_bandwidth;
 #endif
 
     struct rcu_head rcu;
     /* 用于建立进程链表(属于此调度组的进程链表) */
     struct list_head list;
 
     /* 指向其上层的进程组,每一层的进程组都是它上一层进程组的运行队列的一个调度实体,在同一层中,进程组和进程被同等对待 */
     struct task_group *parent;
     /* 进程组的兄弟结点链表 */
    struct list_head siblings;
     /* 进程组的儿子结点链表 */
     struct list_head children;

 #ifdef CONFIG_SCHED_AUTOGROUP
     struct autogroup *autogroup;
 #endif

     struct cfs_bandwidth cfs_bandwidth;
 };

 

 

调度实体(struct sched_entity)

 1 /* 一个调度实体(红黑树的一个结点),其包含一组或一个指定的进程,包含一个自己的运行队列,一个父亲指针,一个指向需要调度的运行队列指针 */
 2 struct sched_entity {
 3     /* 权重,在数组prio_to_weight[]包含优先级转权重的数值 */
 4     struct load_weight    load;        /* for load-balancing */
 5     /* 实体在红黑树对应的结点信息 */
 6     struct rb_node        run_node;    
 7     /* 实体所在的进程组 */
 8     struct list_head    group_node;
 9     /* 实体是否处于红黑树运行队列中 */
10     unsigned int        on_rq;
11 
12     /* 开始运行时间 */
13     u64            exec_start;
14     /* 总运行时间 */
15     u64            sum_exec_runtime;
16     /* 虚拟运行时间,在时间中断或者任务状态发生改变时会更新
17      * 其会不停增长,增长速度与load权重成反比,load越高,增长速度越慢,就越可能处于红黑树最左边被调度
18      * 每次时钟中断都会修改其值
19      * 具体见calc_delta_fair()函数
20      */
21     u64            vruntime;
22     /* 进程在切换进CPU时的sum_exec_runtime值 */
23     u64            prev_sum_exec_runtime;
24 
25     /* 此调度实体中进程移到其他CPU组的数量 */
26     u64            nr_migrations;
27 
28 #ifdef CONFIG_SCHEDSTATS
29     /* 用于统计一些数据 */
30     struct sched_statistics statistics;
31 #endif
32 
33 #ifdef CONFIG_FAIR_GROUP_SCHED
34     /* 代表此进程组的深度,每个进程组都比其parent调度组深度大1 */
35     int            depth;
36     /* 父亲调度实体指针,如果是进程则指向其运行队列的调度实体,如果是进程组则指向其上一个进程组的调度实体
37      * 在 set_task_rq 函数中设置
38      */
39     struct sched_entity    *parent;
40     /* 实体所处红黑树运行队列 */
41     struct cfs_rq        *cfs_rq;        
42     /* 实体的红黑树运行队列,如果为NULL表明其是一个进程,若非NULL表明其是调度组 */
43     struct cfs_rq        *my_q;
44 #endif
45 
46 #ifdef CONFIG_SMP
47     /* Per-entity load-tracking */
48     struct sched_avg    avg;
49 #endif
50 };

 

实际上,红黑树是根据 struct rb_node 建立起关系的,不过 struct rb_node 与 struct sched_entity 是一一对应关系,也可以简单看为一个红黑树结点就是一个调度实体。可以看出,在 struct sched_entity 结构中,包含了一个进程(或进程组)调度的全部数据,其被包含在 struct task_struct 结构中的se中,如下:
 1 struct task_struct {
 2     ........
 3     /* 表示是否在运行队列 */
 4     int on_rq;
 5 
 6     /* 进程优先级 
 7      * prio: 动态优先级,范围为100~139,与静态优先级和补偿(bonus)有关
 8      * static_prio: 静态优先级,static_prio = 100 + nice + 20 (nice值为-20~19,所以static_prio值为100~139)
 9      * normal_prio: 没有受优先级继承影响的常规优先级,具体见normal_prio函数,跟属于什么类型的进程有关
10      */
11     int prio, static_prio, normal_prio;
12     /* 实时进程优先级 */
13     unsigned int rt_priority;
14     /* 调度类,调度处理函数类 */
15     const struct sched_class *sched_class;
16     /* 调度实体(红黑树的一个结点) */
17     struct sched_entity se;
18     /* 调度实体(实时调度使用) */
19     struct sched_rt_entity rt;
20 #ifdef CONFIG_CGROUP_SCHED
21     /* 指向其所在进程组 */
22     struct task_group *sched_task_group;
23 #endif
24     ........
25 }

 

而在 struct task_struct 结构中,我们注意到有个调度类,里面包含的是调度处理函数,它具体如下:
 1 struct sched_class {
 2     /* 下一优先级的调度类
 3      * 调度类优先级顺序: stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
 4      */
 5     const struct sched_class *next;
 6 
 7     /* 将进程加入到运行队列中,即将调度实体(进程)放入红黑树中,并对 nr_running 变量加1 */
 8     void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
 9     /* 从运行队列中删除进程,并对 nr_running 变量中减1 */
10     void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
11     /* 放弃CPU,在 compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下,它将调度实体放在红黑树的最右端 */
12     void (*yield_task) (struct rq *rq);
13     bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
14 
15     /* 检查当前进程是否可被新进程抢占 */
16     void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
17 
18     /*
19      * It is the responsibility of the pick_next_task() method that will
20      * return the next task to call put_prev_task() on the @prev task or
21      * something equivalent.
22      *
23      * May return RETRY_TASK when it finds a higher prio class has runnable
24      * tasks.
25      */
26     /* 选择下一个应该要运行的进程运行 */
27     struct task_struct * (*pick_next_task) (struct rq *rq,
28                         struct task_struct *prev);
29     /* 将进程放回运行队列 */
30     void (*put_prev_task) (struct rq *rq, struct task_struct *p);
31 
32 #ifdef CONFIG_SMP
33     /* 为进程选择一个合适的CPU */
34     int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
35     /* 迁移任务到另一个CPU */
36     void (*migrate_task_rq)(struct task_struct *p, int next_cpu);
37     /* 用于上下文切换后 */
38     void (*post_schedule) (struct rq *this_rq);
39     /* 用于进程唤醒 */
40     void (*task_waking) (struct task_struct *task);
41     void (*task_woken) (struct rq *this_rq, struct task_struct *task);
42     /* 修改进程的CPU亲和力(affinity) */
43     void (*set_cpus_allowed)(struct task_struct *p,
44                  const struct cpumask *newmask);
45     /* 启动运行队列 */
46     void (*rq_online)(struct rq *rq);
47     /* 禁止运行队列 */
48     void (*rq_offline)(struct rq *rq);
49 #endif
50     /* 当进程改变它的调度类或进程组时被调用 */
51     void (*set_curr_task) (struct rq *rq);
52     /* 该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占 */
53     void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
54     /* 在进程创建时调用,不同调度策略的进程初始化不一样 */
55     void (*task_fork) (struct task_struct *p);
56     /* 在进程退出时会使用 */
57     void (*task_dead) (struct task_struct *p);
58 
59     /* 用于进程切换 */
60     void (*switched_from) (struct rq *this_rq, struct task_struct *task);
61     void (*switched_to) (struct rq *this_rq, struct task_struct *task);
62     /* 改变优先级 */
63     void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
64              int oldprio);
65 
66     unsigned int (*get_rr_interval) (struct rq *rq,
67                      struct task_struct *task);
68 
69     void (*update_curr) (struct rq *rq);
70 
71 #ifdef CONFIG_FAIR_GROUP_SCHED
72     void (*task_move_group) (struct task_struct *p, int on_rq);
73 #endif
74 };

 

这个调度类具体有什么用呢,实际上在内核中不同的调度算法它们的操作都不相同,为了方便修六改、替换调度算法,使用了调度类,每个调度算法只需要实现自己的调度类就可以了,CFS算法有它的调度类,SCHED_FIFO也有它自己的调度类,当一个进程创建时,用什么调度算法就将其 task_struct->sched_class 指向其相应的调度类,调度器每次调度处理时,就通过当前进程的调度类函数进程操作,大大提高了可移植性和易修改性。

 

 六.对操作系统进程模型的看法

   操作系统Operating System,简称OS)是管理和控制计算机硬件软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。

操作系统是 用户计算机接口,同时也是计算机 硬件和其他 软件的接口。 操作系统的功能包括管理 计算机系统硬件、软件及数据资源, 控制程序运行,改善 人机界面,为其它 应用软件提供支持,让 计算机系统所有资源最大限度地发挥作用,提供各种形式的 用户界面,使用户有一个好的工作环境,为其它软件的开发提供必要的服务和相应的接口等。实际上,用户是不用接触操作系统的,操作系统管理着 计算机硬件资源,同时按照 应用程序的资源请求,分配资源,如:划分 CPU时间, 内存空间的开辟,调用 打印机等。
在进程模型中, 计算机上所有可运行的 软件,通常也包括 操作系统,被组织成若干顺序进程(sequential process),简称 进程(process)。操作系统中最核心的概念是进程, 进程也是 并发程序设计中的一个最重要、 最基本的概念。进程是一个动态的过程, 即进程有生命周期, 它拥有资源, 是程序的执行过程, 其状态是变化的。  Windows、  unixLinux是目前最流行的几个操作系统。

七.参考资料

https://wenku.baidu.com/view/64179a4bcf84b9d528ea7a0a.html
https://www.doc88.com/p-7019532024389.html
https://blog.csdn.net/bit_clearoff/article/details/54292300

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

 

第一次作业:深入Linux源码分析进程模型