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

时间:2021-08-02 16:46:39

1. 前言

    该博客内容分析自操作系统linux-2.6.11,为个人学习分享笔记,若有不足之处请指出。

    linux-2.6.11源码下载地址:https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/

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

 

 

 2. Linux进程:

  2.1 Linux进程的作用

    ①它是一个独立且可调度的活动。

    ②它是一个抽象实体,执行任务时分配和释放资源。

    ③它是一个能够并发运行的单位。

  2.2 Linux进程关系

      如果一个进程由另一个进程创建,则它们之间具有父子关系。

      如果一个进程创建多个子进程,则子进程之间有兄弟关系。

      如下图所示:

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

 

  2.3 Linux进程的几种主要状态:

    ①可运行状态        ---TASK_RUNNING

    ②可中断睡眠状态      ---TASK_INTERRUPTIBLE

    ③不可中断睡眠状态     ---TASK_UNINTERRUPTIBLE

    ④退出(僵死)状态      ---TASK_ZOMBIE / EXIT_ZOMBIE

    ⑤退出(销毁)状态      ---TASK_DEAD / EXIT_DEAD

    ⑥暂停(跟踪)状态      ---TASK_STOPPED

    

    可以总的归类为以下三种状态:

     运行态:进程占在CPU上运行,占用CPU。

     就绪态:进程具备运行条件,但未分配CPU。

          阻塞态:进程因等待某件事发生暂时不能运行。

     2.3.1 状态分析:

    ①可运行状态        ---TASK_RUNNING

      程序正在运行或者具备了运行条件准备运行,处于运行态或就绪态。

    ②可中断睡眠状态      ---TASK_INTERRUPTIBLE

      该状态也称浅度睡眠状态,处于阻塞态,在等待资源(可以被中断唤醒),如果有了资源则会被唤醒进入就绪态,准备好运行。

    ③不可中断睡眠状态     ---TASK_UNINTERRUPTIBLE

      该状态也称深度睡眠状态,处于阻塞态,在等待资源(不可被中断唤醒),如果有了资源则会被唤醒进入就绪态,准备好运行。

    ④退出(僵死)状态       ---TASK_ZOMBIE / EXIT_ZOMBIE

      进程已经结束,进程资源用户空间被释放,但还未释放进程控制块PCB,在等待父进程回收,处于阻塞态。

    ⑤退出(销毁)状态       ---TASK_DEAD / EXIT_DEAD

      进程运行完毕或者因为触发系统异常而退出,释放掉进程用户态使用的相关的物理内存,处于阻塞态。

      <linux-2.6.11\kernel\exit.c>

            if(state == EXIT_DEAD)
                release_task(tsk);

         

            state = xchg(&p->exit_state, EXIT_DEAD);
                if (state != EXIT_ZOMBIE) {
                    BUG_ON(state != EXIT_DEAD);
                    return 0;
              }

 

           父进程如果是一个EXIT_ZOMBIE状态的进程。则会尝试把它exit_state设置为EXIT_DEAD,所以父进程会把子进程由EXIT_ZOMBIE设置为EXIT_DEAD。

    ⑥暂停(跟踪)状态       ---TASK_STOPPED

       该状态处于就绪态,进程被外部程序暂停接受某种处理,当再次允许时继续执行进入运行态。

 2.4 进程状态的转换

      2.4.1 主要状态转换

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

      2.4.2 总的状态转换

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

    2.5 进程的组织

      2.5.1  0号进程

       首先,在整个系统启动期间,会初始化系统的第一个进程init_task(进程0),以下是init_task的核心代码: 

       <linux-2.6.11/arch/x86_64/kernel/init_task.c>

static struct fs_struct init_fs = INIT_FS;
static struct files_struct init_files = INIT_FILES;
static struct signal_struct init_signals = INIT_SIGNALS(init_signals);
static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand);
struct mm_struct init_mm = INIT_MM(init_mm);

EXPORT_SYMBOL(init_mm);

union thread_union init_thread_union 
    __attribute__((__section__(".data.init_task"))) =
        { INIT_THREAD_INFO(init_task) };

struct task_struct init_task = INIT_TASK(init_task);

EXPORT_SYMBOL(init_task);
DEFINE_PER_CPU(struct tss_struct, init_tss) ____cacheline_maxaligned_in_smp;

#define ALIGN_TO_4K __attribute__((section(".data.init_task")))

 

       init_task进程属于内核中的一个进程,是所有进程的祖先,属于一个比较特殊的进程,它是内核开发者人为制造出来的,init_task对象的初始化在内核代码中由该代码来完成:

struct task_struct init_task = INIT_TASK(init_task);

 

       init_task内核栈用静态方式分配,其代码为:

union thread_union init_thread_union 
  __attribute__((__section__(".data.init_task"))) =
    { INIT_THREAD_INFO(init_task) };

 

        2.5.2 内核栈

       每个进程都有自己的内核栈,当进程从用户态进入内核态时,CPU 就自动地设置该进程的内核栈。

       <linux-2.6.11/include/linux/sched.h>

struct task_struct 
{
    ......    
    volatile long state;                  //进程状态
    void *stack;                     //指向内核栈
    Struct list_head tasks;               //用于加入进程的链表
    pid_t pid;                       //进程的ID
    pid_t gpid;                      //线程组的ID
    unsigned long timestamp;              //时间戳
    unsigned long rt_priority;            //任务的实时优先级
    struct mm_struct *mm, *active_mm      //指向该进程的内存区描述符
    struct task_struct __rcu *real_parent;    //指向创建其的父进程如果其父进程不存在,则指向init进程
    struct task_struct __rcu *parent;         //指向当前的父进程
    ......
}

 

       其中pid是一个数字,按从小到大顺序进行编号,表示进程标识号,一次只能标识一个进程,所以每个进程的pid都不同,因此内核可以通过这个标识来识别不同的进程,用户程序可以通过pid对进程发号施令,ppid则是父进程标识号。

 

      2.5.3 宏

       <linux-2.6.11\include\asm-i386\current.h>

struct task_struct;
static inline struct task_struct * get_current(void)
{
    return current_thread_info()->task;
}
#define current get_current()
#endif /* !(_I386_CURRENT_H) */

       该段代码中定义了current 宏,这是一段与体系结构相关的代码,当一个进程在某个CPU 上正在执行时,内核从此处获得指向它的task_struct 的指针。

 

      2.5.4 哈希表

       哈希表可以根据进程的pid 可以快速地找到对应的进程。

       Linux 在进程中引入的哈希表叫做pidhash,定义如下:

       <linux-2.6.11/include/linux/sched.h>

#define PIDHASH_SZ (4096 >> 2)
struct list_head uidhash_list;
struct pid pids[PIDTYPE_MAX];
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))

       PIDHASH_SZ 表示表中元素的个数,表中的元素是指向task_struct 结构的指针。

       pid_hashfn 为哈希函数,把进程中的的PID 转换为表的索引。

       通过这个函数,可以把进程的PID均匀地散列在它们的域(0 到 PID_MAX-1)中。

 

       Linux 操作系统中利用链地址法来处理冲突的PID,每一表项是由冲突的PID 组成的双向链表,这种链表是由task_struct 结构中的pidhash_next 和 pidhash_pprev 域实现的,同一链表中pid 的大小由小到大排列。

      2.5.5 双向循环链表

       

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) /struct list_head name = LIST_HEAD_INIT(name)
struct list_head
{
       struct list_head *next, *prev;
};

       

       哈希表的主要作用是根据进程的pid 可以快速地找到对应的进程,但没有反映进程创建的顺序,也没有反映进程之间的亲属关系,因此引入双向循环链表,用每个进程的task_struct结构中的prev_task 和next_task 域来实现这种链表。

       链表的头和尾都为init_task,它对应的是进程0(pid 为0),也就是所谓的空进程。

      2.5.6 运行队列

       

struct work_struct {  
    atomic_long_t data;           //运行处理函数func的参数*  
    #define WORK_STRUCT_PENDING 0      
    #define WORK_STRUCT_STATIC 1       
    #define WORK_STRUCT_FLAG_MASK (3UL)  
    #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)  
    struct list_head entry;         //连接运行的指针
    work_func_t func;               //运行处理函数
    #ifdef CONFIG_LOCKDEP  
    struct lockdep_map lockdep_map;  
    #endif  
};  

 

       当内核要寻找一个新的进程在CPU 上运行时,必须只考虑处于可运行状态的进程,因为扫描整个进程链表是相当低效的,因而引入了可运行状态进程的双向循环链表,即运行队列。

        运行队列通过task_struct 结构中的两个指针work_list 链表来维持。

      2.5.7 等待队列

       主要例子:sleep_on函数

       

void sleep_on (wait_queue_head_t *wq)         //wq是某个等待队列的队列头
{
    wait_queue_t wait;                         //定义一个等待队列结点 


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


    current->state = TASK_UNINTERRUPTIBLE;   //设置进程状态为不可中断睡眠状态


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


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


    remove_wait_queue (wq, &wait);          //进程已被唤醒,重新移到CPU运行队列

}

       进程必须经常等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源或等待时间走过固定的间隔。

       等待队列实现在事件上的条件等待,也就是说,等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们,由循环链表实现。

 

      2.5.8 内核线程

struct task_struct
{
    ......
    struct mm_struct *mm;
    struct mm_struct *avtive_mm;   //指向该进程的内存区描述符
  ......
};

       内核线程主要用于实现系统后台操作,如页面对换,刷新磁盘缓存,网络连接等系统工作。

       它所执行的是内核中的函数,而普通进程只有通过系统调用才能执行内核中的函数。

 

   2.6 进程的调度

      2.6.1 调度器

          Linux调度器的一般原理是按所需分配的CPU计算能力, 向系统中每个进程提供最大的公正性。

          调度器由两个调度函数来实现,一个为主调度函数,一个为周期性调度函数。

          ①主调度器负责将CPU使用权限从一个进程切换到另一个进程。

          ②周期性调度器由中断实现,在中断过程中执行scheduler_tick()函数,执行完毕后返还CPU权限,可用于更新统计信息。

          调度器部分代码:

          <linux-2.6.11\kernel/sched.c>

asmlinkage void __sched schedule(void)
{
    struct task_struct *tsk = current;
    sched_submit_work(tsk);
    __schedule();
}

          先将当前进程加入运行队列,然后选择下个可调度进程,之后调用进程上下文切换,再开始执行下个进程。

static void __sched __schedule(void)  
{  
    struct task_struct *prev, *next;  
    unsigned long *switch_count;  
    struct rq *rq;  
    int cpu;  
    cpu = smp_processor_id();  
    rq = cpu_rq(cpu);  
    rcu_note_context_switch(cpu);  
    prev = rq->curr;            //当前队列的当前进程  
    schedule_debug(prev);  
  ...     
if (unlikely(!rq->nr_running))//当前rq无进程运行时,从其他cpu上取一个task执行           idle_balance(cpu, rq);       //选择下个可调度进程       next = pick_next_task(rq);     ...     //上下文切换      context_switch(rq, prev, next); 
  ... }  

 

      2.6.2 调度算法(CFS)

          CFS是完全公平调度算法,将所有的进程都统一对待。其中的schedule() 函数它会先抢占当前运行任务,当它开始确定下一个要调度的任务时,它会调用 pick_next_task 函数,通过调度器类调用 CFS 调度器,返回相关 的sched_entity。

          pick_next_task的具体实现:

          <linux-2.6.11/kernel/sched_fair.c>

static inline struct task_struct *  
pick_next_task(struct rq *rq)  
{  
    const struct sched_class *class;  
    struct task_struct *p;  
  /*使用的数据结构*/
if (likely(rq->nr_running == rq->cfs.nr_running)) {       //如果nr_running==cfs.nr_running,则说明当前rq队列中是没有rt任务的,       //rt任务不在cfs队列中,它的优先级的设置和cfs不一样。
p
= fair_sched_class.pick_next_task(rq); //在cfs队列中挑选即将被切换进去的进程 if (likely(p)) return p; } for_each_class(class) { p = class->pick_next_task(rq); if (p) return p; } }

 

 

3.对Linux操作系统进程模型的看法:

      Linux操作系统的进程模型是开源的,能够表示这是一个足够安全的系统,对比于windows而言,windows则需要不断的打补丁。

      它能够并发的执行任务,合理的分配CPU资源,而且整合性很强,整个操作系统的规模非常的小,但是却能够完成各方各面的任务。

       其中有的代码很简明易懂,但是也有的比较深奥,比如算法方面的内容,还需要自己加倍努力,多加学习,努力提高自己,保证自己的能力有所进步。

 

4.参考资料:

      https://blog.csdn.net/jnu_simba/article/details/11724277

      https://www.cnblogs.com/tolimit/p/4530370.html

      https://blog.csdn.net/u014089131/article/details/54585805