Linux内核学习笔记之进程管理2—进程的组织形式

来源:互联网 发布:svs打开的软件 编辑:程序博客网 时间:2024/06/05 07:29

Linux内核学习笔记之进程管理2—进程的组织形式

1.       进程标识符构成的哈希表

内核使用一个进程描述符来表示一个进程,因此通过进程描述符的地址来访问一个进程是最方便的. 这里主要是用哈希表来完成进程号到进程描述符地址之间的映射.

内核中设置了PIDTYPE_MAX个不同的哈希表(4),这四个哈希表保存在数组pid_hash,

static struct hlist_head *pid_hash[PIDTYPE_MAX];

pid_type是一个枚举内型。

enum pid_type

{

       PIDTYPE_PID,//进程号哈希表

       PIDTYPE_TGID,//线程组号哈希表

       PIDTYPE_PGID,//进程组号哈希表

       PIDTYPE_SID,//会话号表

       PIDTYPE_MAX

};

在进程描述符中有一个成员变量 pids,定义如下:

struct pid pids[PIDTYPE_MAX];

struct pid{

                        int nr;

                        struct hlist_node  pid_chain;

                        struct list_head  pid_list;

};

pid_chain构成一个链表,表中的nr值不同,哈希函数将这些nr值映射到哈希表中的同一位置,这个链表就是主链。(nr值本身就是进程号)

pid_list将具有相同nr值的进程联系在一起构成从链。例如可以讲同一个线程组的所有线程联系起来。

pid_hashfn(x) 负责进程号映射到哈希表中的位置。理论上哈希函数何以直接查找哈希表,但是哈希函数存在冲突问题,linux内核提供了哈希查找函数find_pid().kernel/pid.c中)

struct pid * fastcall find_pid(enum pid_type type, int nr)
{
      struct hlist_node *elem;
      struct pid *pid;
        hlist_for_each_entry(pid, elem,
                      &pid_hash[type][pid_hashfn(nr)], pid_chain) {//type个哈希表中的第//pid_hashfn(nr)个元素。
               if (pid->nr == nr)
                       return pid;
     }
     return NULL;
}
在创建进程是又设计到向哈希表中添加进程,有函数attach_pid()提供(kernel/pid.c中)
nr值不存在是直接插入到nr对应链表的表头
nr存在时插入对应从表的表尾。
int fastcall attach_pid(task_t *task, enum pid_type type, int nr)
{
       struct pid *pid, *task_pid;
     task_pid = &task->pids[type];
      pid = find_pid(type, nr);
      if (pid == NULL) {
               hlist_add_head(&task_pid->pid_chain,
                               &pid_hash[type][pid_hashfn(nr)]);
                INIT_LIST_HEAD(&task_pid->pid_list);
      } else {
                INIT_HLIST_NODE(&task_pid->pid_chain);
                list_add_tail(&task_pid->pid_list, &pid->pid_list);
       }
       task_pid->nr = nr;
       return 0;
}
进程销毁时从哈希表中移除进程,由函数detach_pid()完成kernel/pid.c中)
void fastcall detach_pid(task_t *task, enum pid_type type)
{
      int tmp, nr;
      nr = __detach_pid(task, type);//解除进程task与第type个哈希表之间的关联
        if (!nr)
              return;
     for (tmp = PIDTYPE_MAX; --tmp >= 0; )
              if (tmp != type && find_pid(tmp, nr))
                      return;
       free_pidmap(nr);//释放该进程号nr
}

 

 

2.       所有进程构成的双向链表

进程描述符中有成员变量 struct list_head tasks。可以构成双向链表。

在进程调度是利用该链表来查找进程显然是不合理的,于是linux内核有构建了新的数据结构提高调度效率。

 

 

3.       执行态进程组成的运行队列

Linux2.6内核采用了新的调度机制(2.4相比),实现了O(1)复杂度。采用了新的运行队列实现了新的调度算法。内核为每个处理器都设置了一个运行队列。

O(1)复杂度的调度算法的核心数据结构是—-运行队列 struct runqueue;(定义在/kernel/sched.c

其中有一个实现O(1)复杂度的关键成员变量优先级数组:
                    prio_array_t  *active, *expired, arrays[2];

active expired 分别指向活动和超时的优先级数组,arrays[2]用于存储着两个优先级数组。每个进程在时间片用完后,根据优先级重新分配时间片,优先级会在每次进程切换时重新计算,然后将其移到expried优先级数组中,当active数组中没有活动进程是,交换两个指针指向的数组。这是linux2.6实现实现O(1)复杂度调度算法的基础。

 

优先级数组定义如下:(定义在/kernel/sched.c

struct prio_array {
     unsigned int nr_active;//该优先级数组中可运行的进程数
     unsigned long bitmap[BITMAP_SIZE];//优先级位图
     struct list_head queue[MAX_PRIO];//可执行进程队列,为每一个优先级设置一个可执行进程队列
};
大概过程如下:
当进程被创建并投入运行时,首先将进程插入到对应的可执行队列的对尾,然后根据进程的优先级设置对应的优先级位图,例如优先级为N,就设置优先级位图的第N位,表示该优先级进程队列不为空。
当调度时,首先获取优先级位图中第一个不为0bit为的序号,该序号指明了当前最高优先级进程所在的可执行队列,相应的可执行队列的第一个进程也就是合适的进程。
大致如下:
array = rq->active;

 idx = sched_find_first_bit( array->bitmap );

 queue = array->queue + idx;

 next = list_entry( queue->next, task_t, run_list);//next指向合适的进程。

 

4.       阻塞态进程构成的等待队列

 

Linux内核为每一个临界资源设置了一个等待队列,在进程无法获取临界资源时就睡眠在该等待队列上。

等待队列

wait_queue_head_t 表示一个等待队列/include/linux/wait.h

struct __wait_queue_head {
       spinlock_t lock;
      struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

等待队列节点

struct __wait_queue {
       unsigned int flags;//唤醒方式
#define WQ_FLAG_EXCLUSIVE       0x01
      void *private;//保存睡眠进程描述符地址
      wait_queue_func_t func;//唤醒方法
        struct list_head task_list;//
};

唤醒方式有 WQ_FLAG_EXCLUSIVE 或者 0

WQ_FLAG_EXCLUSIVE:表示节点对应进程对临界资源使用具有排他性。在唤醒是会唤醒所有非排他性进程和一定数量的排他性进程。

 

睡眠在等待队列中

进程访问临界资源而阻塞时,先设置自己的状态为睡眠态(中断或者不可中断),然后在该临界资源的等待队列中睡眠,最后才释放处理器,等待该资源可用。

首先初始化等待队列节点(include/linux/wait.h

static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
       q->flags = 0;
       q->private = p;
        q->func = default_wake_function;
}

然后插入到等待队列

void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
       unsigned long flags;
       wait->flags &= ~WQ_FLAG_EXCLUSIVE;
        spin_lock_irqsave(&q->lock, flags);
       __add_wait_queue(q, wait);
       spin_unlock_irqrestore(&q->lock, flags);
}

 

void fastcall add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
{
       unsigned long flags;
       wait->flags |= WQ_FLAG_EXCLUSIVE;
       spin_lock_irqsave(&q->lock, flags);
      __add_wait_queue_tail(q, wait);
        spin_unlock_irqrestore(&q->lock, flags);
}

add_wait_queue 首先清唤醒方式,在后插入到对首

add_wait_queue_exclusive先设置唤醒方式,然后插入到对尾。这样就将等待队列中的链表一分为二,前半部分为没有设置WQ_FLAG_EXCLUSIVE,后半部分为设置了WQ_FLAG_EXCLUSIVE;
的节点。这给后面的唤醒带来了好处。

 

唤醒

Linux2.6内核提供了8个宏来唤醒等待队列中的进程。他们都是基于函数__wake_up_common

位于ernel/sched.c

static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
                            int nr_exclusive, int sync, void *key)
{
      struct list_head *tmp, *next;
      list_for_each_safe(tmp, next, &q->task_list) {
              wait_queue_t *curr;
              unsigned flags;
               curr = list_entry(tmp, wait_queue_t, task_list);
             flags = curr->flags;
             if (curr->func(curr, mode, sync, key) &&
                 (flags & WQ_FLAG_EXCLUSIVE) &&
                  !--nr_exclusive)
                      break;
   }
}

ANSI C标准定义逻辑与的运算规则,前面的为真才会继续运算后面的,所以上面的if可以实现唤醒全部得非排他性进程和nr_exclusive个排他性进程。