您的位置:首页 > 运维架构 > Linux

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

2010-04-05 20:22 826 查看
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位,表示该优先级进程队列不为空。

当调度时,首先获取优先级位图中第一个不为0的bit为的序号,该序号指明了当前最高优先级进程所在的可执行队列,相应的可执行队列的第一个进程也就是“合适”的进程。

大致如下:

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个排他性进程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: