关于linux操作系统中进程相关问题的学习笔记

关于linux操作系统中进程相关问题的学习笔记

1.摘要

  进程的经典定义是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中(contest)中。上下文是由程序运行正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符合的集合。在本次学习笔记中,我是以linux为例,学习了以下三个方面的知识:操作系统如何组织进程、进程状态如何转换以及进程是如何调度的。在最后我还谈了下自己对操作系统进程模型的一些学习心得。

2.操作系统如何组织进程

  进程是由程序、数据和进程快PCB(Process Control Block)组成。进程控制块PCB是进程存在唯一标识,系统通过PCB的存在而感知进程的存在.当创建一个进程时,实际上是建立一个PCB。当进程消失时,实际上是撤销PCB。在linux中,每个进程中的PCB用一个名为task struct的结构体来表示,定义在include/linux/sched.h中。

1 struct task_struct {  
2     pid_t pid;  
3     pid_t tgid;  
4   
5     /* PID/PID hash table linkage. */  
6     struct pid_link pids[PIDTYPE_MAX];   <span style="color:#ff0000;"> </span>//一个进程ID可能是多种身份,比如Session ID,进程组ID, 进程ID,所以指向多个pid节点  
7     struct list_head thread_group;  
8 }  

linux可以运行的进程数量可达到成千上万个(用ps 命令可查看当前进程),而这些进程又可能处于不同的状态,因此需要操作系统来管理组织它们。linux采用了以下几种方式来组织进程: 

  2.1哈希表

  哈希表是进行快速查找的一种有效的组织方式。 L inux 在进程中引入的哈希表叫做pidhash,在include/linux/sched.h中,定义如下:

stru ct task stru ct*p idhash[ PIDHASH_SZ] ;

PIDHASH SZ 在inc lude /linux /sched. h 中定义, 其值为1024.

系统根据进程的进程号求得hash值, 加到hash表中:

#define p id hash fn(x)((((x) >>8)∧ (x))&(PIDHASH_SZ -1))

其中, PIDHASH_SZ 是表中元素的个数, 表中的元素是指向task_struct结构体的指针。pid_hashfn为哈希函数,将进程的pid转换为表的索引,通过该函数,可以将进程的pid均匀地散列在它们的域中。

函数代码如下:

#define pid_hashfn(nr, ns)  \  
    hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift)  
static struct hlist_head *pid_hash;  
static unsigned int pidhash_shift = 4;  
  
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)  
{  
    struct upid *pnr;  
  
    hlist_for_each_entry_rcu(pnr,  
            &pid_hash[pid_hashfn(nr, ns)], pid_chain)  
        if (pnr->nr == nr && pnr->ns == ns)  
            return container_of(pnr, struct pid,  
                    numbers[ns->level]);  
  
    return NULL;  
}  

如果知道进程号, 可以通过hash表很快地找到该进程,,查找函数如下:

struct task_struct *pid_task(struct pid *pid, enum pid_type type)  
{  
    struct task_struct *result = NULL;  
    if (pid) {  
        struct hlist_node *first;  
        first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),  
                          lockdep_tasklist_lock_is_held());  
        if (first)         //pid中的task[type] 与task_struct.pid[type].node  指向的是同一个节点  
            result = hlist_entry(first, struct task_struct, pids[(type)].node);  <span style="color:#ff0000;"> </span>//node实体在task_struct结构中,所以可以利用first指针得到task_struct结构体指针  
    }  
    return result;  
}

2.2双向循环链表

  哈希表的主要作用是根据进程的pid可以快速找到对应的进程,但它没有反映创建的顺序,也无法反映进程之间的亲属关系,而双向循环链表可以弥补这一弱势

(图1,图片来源:http://blog.chinaunix.net/uid-27033491-id-3233511.html)

    其对应的结构体是:

struct task_struct {
   ...;
    struct list_head tasks;
   ...;
};

struct list_head {
    struct list_head *next,*prev;
};

 2.3运行队列

  当内核要寻找一个新的进程在CPU运行时,一般只考虑那些处于可运行状态的进程,因为查找整个进程链表效率是很低的, 所以引入了可运行状态进程的双向循环链表, 也叫运行队列。运行队列容纳了系统中所有可以运行的进程, 它是一个双向循环队列, 该队列通过task _truc t结构中的两个指针run_list链表来维护。 队列的标志有两个:一个是“空进程” id le_task,一个是队列的长度。空进程是一个比较特殊的进程, 只有系统中没有进程可运行时它才会被执行, L inux 将它看作运行队列的头, 当调度程序遍历运行队列时, 是从idle_task开始、到idle_task结束的。

  2.4等待队列
  

  运行队列链表将所有状态为TASK_RUNNING 的进程组织在一起.在一起。 将所有状态为TASK _INTERRUPT IBLE和TASK_UNINTERRUPTIBLE的进程组织在一起而形成的远程链表称为等待队列。进程必须经常等待某些事件的发生, 等待队列实现在事件上的条件等待, 希望等待特定事件的进程将自己放进合适的等待队列, 并放弃控制权。 等待队列表示一组睡眠的进程, 当条件满足时, 由内核将它们唤醒。
等待队列由循环链表实现:

struct __wait_queue {  
    unsigned int flags;  
#define WQ_FLAG_EXCLUSIVE   0x01  
    void *private;  
    wait_queue_func_t func;  
    struct list_head task_list;  
}; 

(图2,图片来源:https://blog.csdn.net/silent123go/article/details/52599210)

3.进程状态如何转换

  

  Linux 系统中的进程有几种关键的状态,他们分别是可执行状态(TASK_RUNNING),可中断的睡眠状态(TASK_INTERRUPTIBLE),不可中断的睡眠状态(TASK_UNINTERRUPTIBLE),暂停(TASK_STOPPED),跟踪状态(TASK_TRACED),僵死状态(EXIT_ZOMBIE)和退出状态(TASK_DEAD)等。各种状态之间的关系如图3所示。这些状态主要是依据进程与CPU 之间的关系来划分的,为的是操作系统内核能对CPU 和进程进行有效地管理。

(图3,图片来源:杨兴强,刘翔鹏,刘毅.Linux进程状态演化过程的图形学表示[J].系统仿真学报,2013,25(10):2444-2448)

状态切换实在contest_switch中实现的,其函数代码如下:

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
     struct mm_struct *mm, *oldmm;
 
     prepare_task_switch(rq, prev, next);
     mm = next->mm;
     oldmm = prev->active_mm;
     /*
      * For paravirt, this is coupled with an exit in switch_to to
      * combine the page table reload and the switch backend into
      * one hypercall.
      */
     arch_enter_lazy_cpu_mode();
 
     //task->mm 为空.则是一个内核线程
     if (unlikely(!mm)) {
         //内核线程共享上一个运行进程的mm
         next->active_mm = oldmm;
         //增加引用计数
         atomic_inc(&oldmm->mm_count);
         enter_lazy_tlb(oldmm, next);
     } else
         //如果是用户进程,则切换运行空间
         switch_mm(oldmm, mm, next);
 
     //如果上一个运行进程是内核线程
     if (unlikely(!prev->mm)) {
         //赋active_mm为空.
         prev->active_mm = NULL;
         //更新运行队列的prev_mm成员
         rq->prev_mm = oldmm;
     }
     /*
      * Since the runqueue lock will be released by the next
      * task (which is an invalid locking op but in the case
      * of the scheduler it's an obvious special-case), so we
      * do an early lockdep release here:
      */
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
     spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif
 
     /* Here we just switch the register state and the stack. */
     //切换进程的执行环境
     switch_to(prev, next, prev);
 
     barrier();
     /*
      * this_rq must be evaluated again because prev may have moved
      * CPUs since it called schedule(), thus the 'rq' on its stack
      * frame will be invalid.
      */
 
     //进程切换之后的处理工作
     finish_task_switch(this_rq(), prev);
}

 4.进程如何调度
  

  Linux系统的线程是内核线程,所以Linux系统的调度是基于线程的,而不是基于进程的。

  为了实行调度,LInux系统将线程区分为三类:

  (1)实时先入先出。
    (2)实时轮转。
  (3)分时。
    实时先入先出线程具有最高优先级,它不会被其它进程抢占。实时轮转线程与实时先入先出进程基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后就可以被抢占。在系统内部,实时线程的优先级从0~99,0是实时线程的最高优先级,99是实时线程的最低优先级。传统的非实时线程形成的单独的类并由单独的算法进行调度,这样可以使非实时线程不与实时线程竞争资源。在系统内部,这些线程的优先级从100-139.也就是说,Linux系统包含140个不同优先级(包括实时和非实时任务)。就像实时轮转线程一样,Linux系统根据非实时线程的要求以及它们的优先级分配CPU时间片。

  这里主要学习两个调度算法:Linux O(1)调度器(O(1) scheduler)和完全公平调度器(Compleetely Fair Scheduler,CFS).

  4.2 Linux O(1)调度器  

  schedule()是实现进程调度的主要函数,并负责完成进程切换工作.其用于确定最高优先级进程的代码非常快捷高效,它在/kernel/sched.c中的定义如下

 1 task_t*prev,*next;
 2 runqueue_t*rq;
 3 prio_array_t*array;
 4 intidx;preempt_disable();
 5 prev=current;//Linux2.6内核支持抢占,所以在对队列操作时需要设置为不可抢占rq=this_rq();
 6 array=rq->active;
 7 if(unlikely(!array->nr_active))
 8 {rq->active=rq->expired;
 9 rq->expired=array;
10 array=rq->active;}.

这段代码的作用是执行两个数组(活动数组rq->active和过期数组rq->expired)的切换.判断活动数组中如果没有进程了,则通过指针操作来切换两个数组.之前在过期数组中的进程时间片已经被计算好了.所以在两个数组切换后,过期数组中的进程都变为活动进程,交换数组的时间就是交换指针的时间.这种交换就是O(1)调度算法的核心.O(1)调度算法不需要从头到尾一个一个地对进程进行时间片的计算,而是通过很简单的数组切换实现进程的切换,解决了之前算法中效率低下的弊端.该过程可用图4表示.

 (图4,图片来源:张永选,姚远耀.Linux2.6内核O(1)调度算法剖析[J].韶关学院学报,2009,30(06):5-9.)

有了活动数组,并且各个进程都按优先级排好队等待被调度,继而就要选择候选进程了:

1  idx=sched_find_first_bit(array->bitmap);
2  queue=array->queue+idx;
3 next=list_entry(queue->next,task_t,run_list);
4 if(unlikely(next->prio!=new_prio)){dequeue_task(next,array);
5 next->prio=new_prio;enqueue_task(next,array);}
6 elserequeue_task(next,array);

首先,要在活动数组中的索引位图里找到第一个被设置的优先级位,这里通过sched_find_first_bit函数来实现.如前所述,该函数通过汇编指令从进程优先级由高到低的方向找到第一个为1的位置idx.因为优先级的个数是个定值,所以查找时间恒定,并不受系统到底有多少可执行进程的影响.这是Linux2.6内核实现O(1)调度算法的关键之一.此外,Linux对它支持的每一种体系结构都提供了对应的快速查找算法,以保证对位图的快速查找.很多体系结构提供了find-first-set指令,这条指令对指定的字操作(在Intelx86体系结构上,这条指令叫做bsfl.在IBMPPC上,cntlzw用于此目的).在这些系统上,找到第一个要设置的位所花的时间至多是执行这条指令的两倍,这也在很大程度上提高了调度算法的效率.sched_find_first_bit函数找到第一个被设置的优先级位后,再找到该优先级对应的可运行进程队列,接着找到该队列中的第一个进程,最后把找到的进程插入运行队列中.整个过程如下图5所示.图5中的网格为140位索引位图,queue[7]为优先级为7的就绪进程链表..

1 if(likely(prev!=next)){prev=context_switch(rq,prev,next);}
2 elsespin_unlock_irq(&rq->lock);

如果候选进程不是当前运行进程,则需要进行进程切换.反之,仅仅释放之前对运行队列所加的锁.

(图5,图片来源:张永选,姚远耀.Linux2.6内核O(1)调度算法剖析[J].韶关学院学报,2009,30(06):5-9.)

4.2 CFS算法

  CFS的主要思想是使用一颗红黑树作为调度队列的数据结构。
  第一个是调度实体sched_entity,它代表一个调度单位,在组调度关闭的时候可以把他等同为进程。每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。那么所有的sched_entity怎么组织在一起呢?红黑树。所有的sched_entity以vruntime为key(实际上是以vruntime-min_vruntime为key,是为了防止溢出,反正结果是一样的)插入到红黑树中,同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。注意只有等待CPU的就绪态进程在这棵树上,睡眠进程和正在运行的进程都不在树上。

  (图6:红黑树,图片来源:https://www.cnblogs.com/tianguiyu/articles/6091378.html)

  CFS调度算法可以总结如下:该算法总是优先调度那些使用CPU时间最少的任务,通常是在树中最左边节点上的任务。CFS会周期性地根据任务已经停止运行的时间,递增它的虚拟运行时间值,并将这个值与最左边的值进行比较,如果正在运行的任务仍具有较小的虚拟运行时间值,那么它将继续运行,否则,它将插入到红黑树的适当位置,并且CPU将执行新的最左边节点上的任务。

代码如下(函数):

1 struct sched_class { /* Defined in 2.6.23:/usr/include/linux/sched.h */ struct sched_class *next; 
2 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int wakeup); 
3 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int sleep);
4  void (*yield_task) (struct rq *rq, struct task_struct *p); 
5 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p);
6  struct task_struct * (*pick_next_task) (struct rq *rq); 
7 void (*put_prev_task) (struct rq *rq, struct task_struct *p); 
8 unsigned long (*load_balance) (struct rq *this_rq, int this_cpu, struct rq *busiest, unsigned long max_nr_move, unsigned long max_load_move, struct sched_domain *sd, enum cpu_idle_type idle, int *all_pinned, int *this_best_prio);
void (*set_curr_task) (struct rq *rq); 9 void (*task_tick) (struct rq *rq, struct task_struct *p);
void (*task_new) (struct rq *rq, struct task_struct *p); };

 函数描述

enqueue_task:当某个任务进入可运行状态时,该函数将得到调用。它将调度实体(进程)放入红 黑树中,并对 nr_running 变量加 1。

dequeue_task:当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应的调度实体, 并从 nr_running 变量中减 1。

yield_task:在 compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况 下,它将调度实体放在红黑树的最右端。

check_preempt_curr:该函数将检查当前运行的任务是否被抢占。在实际抢占正在运行的任务之 前,CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占。

pick_next_task:该函数选择接下来要运行的最合适的进程。

load_balance:每个调度程序模块实现两个函数,load_balance_start() 和 load_balance_next(), 使用这两个函数实现一个迭代器,在模块的 load_balance 例程中调用。内核调度程序使用这种方 法实现由调度模块管理的进程的负载平衡。

set_curr_task:当任务修改其调度类或修改其任务组时,将调用这个函数。

task_tick:该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running) 抢占。

task_new:内核调度程序为调度模块提供了管理新任务启动的机会。CFS 调度模块使用它进行组调 度,而用于实时任务的调度模块则不会使用这个函数。

5.我的一点学习心得

1.linux的线程调度是基于线程的,线程切换不必调用系统核心;因此调度过程是基于用户程序的,就可以针对用户程序业务逻辑选择更好的调度算法;

2.不同的算法对系统性能的影响也是不一样的,Linux的发展伴随着算法的逐渐优化;

3.进程调度是Linux操作系统的核心功能,了解进程的代码可更好去学习linux操作系统;同时进程的管理是一种复杂的并发程序设计,需要考虑到很多因素,并且它还是一个开源的操作系统,这些给我们学习带来了非常大的价值性和便利性。

6.参考资料

注:题目中已经给出的引用地址此处不再重复列出。

[1]殷联甫,沈士根,郭步.Linux进程结构及组织方式研究[J].计算机应用与软件,2005(11):61-63+143

[2]https://blog.csdn.net/bysun2013/article/details/14053937

[3]https://blog.csdn.net/lizuobin2/article/details/51785812

[4]杨兴强,刘翔鹏,刘毅.Linux进程状态演化过程的图形学表示[J].系统仿真学报,2013,25(10):2444-2448.

[5](荷)安德鲁 S。塔嫩鲍姆(Andrew S. Tanenbaum),(荷)赫伯特.博斯(Herbert Bos)著:陈向群等译,现代操作系统(原书第四版),机械工业出版社,2017

[6]张永选,姚远耀.Linux2.6内核O(1)调度算法剖析[J].韶关学院学报,2009,30(06):5-9.

[7]https://www.cnblogs.com/tianguiyu/articles/6091378.html

[8]http://www.360doc.com/content/15/0922/01/12144668_500602693.shtml

猜你喜欢

转载自www.cnblogs.com/yinbocheng/p/BUPTer.html