Linux进程PID哈希实现

linux系统中每个进程由一个进程id标识,在内核中对应一个task_struct结构的进程描述符,系统中所有进程的task_struct通过链表链接在一起,在内核中,经常需要通过进程id来获取进程描述符,最简单的方法可以通过遍历task_struct链表并对比id的值来获取,但这样效率太低,尤其当系统中运行很多个进程的时候。

 

linux内核通过PID散列表来解决这一问题,能快速的通过进程ID获取到进程描述符。

 

PID散列表包含4个表,因为进程描述符包含了表示不同类型PID的字段,每种类型的PID需要自己的散列表。

 

enum pid_type

{

        PIDTYPE_PID,  // 进程的PID

        PIDTYPE_TGID, // 线程组领头进程的PID

        PIDTYPE_PGID, // 进程组领头进程的PID

PIDTYPE_SID,  // 会话领头进程的PID

        PIDTYPE_MAX   // 类型个数

};

 

内核定义了4个全局的hash表,分别对应4种类型

static struct hlist_head *pid_hash[PIDTYPE_MAX];


内核通过一个struct pid的结构来链接各个进程,其定义如下:

struct pid

{

        int nr;

        struct hlist_node pid_chain;

        struct list_head pid_list;

};

 

其中nr代表id号,pid_chain用于链接冲突元素。对于PGID,TGID来说,对于同一进程组的多个进程来说,所有进程的PGID是相同的,内核中很多操作是针对进程组进行的操作,通过pid_list字段,可以将所有相同id的进程链接在一起。

 

task_struct结构中拥有一个pids字段,它是包含PIDTYPE_MAX个元素的pid结构数组,hash表的链接关系由pid结构完成,通过pid结构可获取包含该结构的task_struct描述符。

 

struct task_struct {   

    struct pid pids[PIDTYPE_MAX];

};

 

pid散列表的链接关系如下图所示,本图中描述了以TGID为关键字建立的hash表。

 


通过id获取进程描述符由find_task_by_pid_type函数完成,代码如下,其首先根据指定id的类型,获取对应散列表的表头,然后计算id所对应的hash slot并遍历获取到对应的pid结构,然后调用pid_task获取到包含该pid结构的task_struct描述符。

 

注:本文的代码来自2.6.11版本内核,后因命名空间扩展等原因,PID散列表的实现已经发生了改变,但原理上应该是相同的。

 

task_t *find_task_by_pid_type(int type, int nr)

{

        struct pid *pid;

 

        pid = find_pid(type, nr);

        if (!pid)

                return NULL;

 

        return pid_task(&pid->pid_list, type);

}

 

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) {

                if (pid->nr == nr)

                         return pid;

        }

        return NULL;

}

 

#define pid_task(elem, type) \

          list_entry(elem, struct task_struct, pids[type].pid_list)

Linux内核中通过PID查找进程描述符(task_struct)时,用到了hash表。下面介绍一下这一部分内核中hash函数的实现。


内核用pid_hashfn宏把PID转换为表索引(kernel/pid.c):
#define pid_hashfn(nr, ns)  \
    hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift)

其中hash_long在<linux/hash.h>中定义如下:
/* 2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1 */
#define GOLDEN_RATIO_PRIME_32 0x9e370001UL
/*  2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1 */
#define GOLDEN_RATIO_PRIME_64 0x9e37fffffffc0001UL

#if BITS_PER_LONG == 32
#define GOLDEN_RATIO_PRIME GOLDEN_RATIO_PRIME_32
#define hash_long(val, bits) hash_32(val, bits)
#elif BITS_PER_LONG == 64
#define hash_long(val, bits) hash_64(val, bits)
#define GOLDEN_RATIO_PRIME GOLDEN_RATIO_PRIME_64
#else
#error Wordsize not 32 or 64
#endif

static inline u64 hash_64(u64 val, unsigned int bits)
{
    u64 hash = val;

    /*  Sigh, gcc can't optimise this alone like it does for 32 bits. */
    u64 n = hash;
    n <<= 18;
    hash -= n;
    n <<= 33;
    hash -= n;
    n <<= 3;
    hash += n;
    n <<= 3;
    hash -= n;
    n <<= 4;
    hash += n;
    n <<= 2;
    hash += n;

    /* High bits are more random, so use them. */
    return hash >> (64 - bits);
}

static inline u32 hash_32(u32 val, unsigned int bits)
{
    /* On some cpus multiply is faster, on others gcc will do shifts */
    u32 hash = val * GOLDEN_RATIO_PRIME_32;

    /* High bits are more random, so use them. */
    return hash >> (32 - bits);
}

static inline unsigned long hash_ptr(const void *ptr, unsigned int bits)
{
    return hash_long((unsigned long)ptr, bits);
}
#endif /* _LINUX_HASH_H */

上面的函数很有趣,我们来仔细看一下。

首先,hash的方式是,让key乘以一个大数,于是结果溢出,就把留在32/64位变量中的值作为hash值,又由于散列表的索引长度有限,我们就取这hash值的高几为作为索引值,之所以取高几位,是因为高位的数更具有随机性,能够减少所谓“冲突”。什么是冲突呢?从上面的算法来看,key和hash值并不是一一对应的。有可能两个key算出来得到同一个hash值,这就称为“冲突”。

那么,乘以的这个大数应该是多少呢?从上面的代码来看,32位系统中这个数是0x9e370001UL,64位系统中这个数是0x9e37fffffffc0001UL。这个数是怎么得到的呢?

“Knuth建议,要得到满意的结果,对于32位机器,2^32做黄金分割,这个大树是最接近黄金分割点的素数,0x9e370001UL就是接近 2^32*(sqrt(5)-1)/2 的一个素数,且这个数可以很方便地通过加运算和位移运算得到,因为它等于2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1。对于64位系统,这个数是0x9e37fffffffc0001UL,同样有2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1。”

从程序中可以看到,对于32位系统计算hash值是直接用的乘法,因为gcc在编译时会自动优化算法。而对于64位系统,gcc似乎没有类似的优化,所以用的是位移运算和加运算来计算。首先n=hash, 然后n左移18位,hash-=n,这样hash = hash * (1 - 2^18),下一项是-2^51,而n之前已经左移过18位了,所以只需要再左移33位,于是有n <<= 33,依次类推,最终算出了hash值。
关于进程描述符:http://blog.csdn.net/unclerunning/article/details/51246749


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值