Linux内核使用位图为进程分配pid

Linux允许用户使用一个叫做进程标识符process ID(pid)的数来标识进程,PID存放在进程描述符的pid字段。pid的值有一个上限,举个例子:默认情况下,最大的PID号为32767。那么内核是如何管理这些pid的呢?能不能用数组?可以,不过效率不高,存在循环,比较耗CPU,而且需要空间较大;内核中使用位图来管理这些pid,由于使用内嵌的汇编语言扫描位图(跟循环有质的区别),所以效率较高,数据结构为pidmap-array,内核使用页框来存放位图,一个页框包含32768个位,所以32位体系结构中pidmap-array位图存放在一个单独的页中。然而,在64位体系结构中,可能需要为PID位图增加更多的页,pidmap及pidmap_array定义如下:

    /*
     * nr_free为页框中空闲位的个数
     * page存放页框的地址
     */
    typedef struct pidmap {
        atomic_t nr_free;
        void *page;
    } pidmap_t;

    /*
     * pidmap_array中存放的是所有pidmap位图
     */
    static pidmap_t pidmap_array[PIDMAP_ENTRIES] =
         { [ 0 ... PIDMAP_ENTRIES-1 ] = { ATOMIC_INIT(BITS_PER_PAGE), NULL } };
    /*
     * BITS_PER_PAGE正如字面意思,表示一个页框中位的个数
     */
    #define BITS_PER_PAGE       (PAGE_SIZE*8)
    #define BITS_PER_PAGE_MASK  (BITS_PER_PAGE-1)

内核使用alloc_pidmap()来为进程分配一个pid,代码如下:

    int alloc_pidmap(void)
    {
        /*
         * last_pid为全局变量,表示上次分配的pid
         */
        int i, offset, max_scan, pid, last = last_pid;
        pidmap_t *map;


        pid = last + 1;
        /*
         * RESERVED_PIDS为300,前300个PID是固定的,不可以分配
         */
        if (pid >= pid_max)
            pid = RESERVED_PIDS;

        /*
         * offset为pid在页框中的偏移量,因为pid可能大于一个页框所能容纳的最大位数,即BITS_PER_PAGE
         * 所以pid要对BITS_PER_PAGE取模,下面即是实现取模操作
         */
        offset = pid & BITS_PER_PAGE_MASK;

        /*
         * pid/BITS_PER_PAGE求得pid所在页面在pidmap_array中的下标
         */
        map = &pidmap_array[pid/BITS_PER_PAGE];

        /*
         * max_scan为扫描次数,前面好理解一些,后面的- !offset是什么意思呢?
         * 扫描从pid所在页框开始,如果offset为0,则扫描该页框一次
         * 如果offset非0,则需要扫描两次,第一次为offset之后的位,第二次为0 ~ offset的位
         */
        max_scan = (pid_max + BITS_PER_PAGE - 1)/BITS_PER_PAGE - !offset;

        /* for循环开始扫描 */
        for (i = 0; i <= max_scan; ++i) {
            /* 如果页面不存在则分配页面 */
            if (unlikely(!map->page)) {
                unsigned long page = get_zeroed_page(GFP_KERNEL);
                /*
                 * Free the page if someone raced with us
                 * installing it:
                 */
                spin_lock(&pidmap_lock);
                if (map->page)
                    free_page(page);
                else
                    map->page = (void *)page;
                spin_unlock(&pidmap_lock);
                if (unlikely(!map->page))
                    break;
            }
            /* 如果该页框空闲位数大于0 */
            if (likely(atomic_read(&map->nr_free))) {
                do {
                    /* 如果该offset位空闲,则返回该值 */
                    if (!test_and_set_bit(offset, map->page)) {
                        atomic_dec(&map->nr_free);
                        last_pid = pid;
                        return pid;
                    }

                    /*
                     *否则继续扫描该页框其余位,find_next_offset见下面分析
                     */
                    offset = find_next_offset(map, offset);

                    /* 
                     * 根据页框下标和offset重新算出pid,比较容易理解 
                     * #define mk_pid(map, off) (((map) - pidmap_array)*BITS_PER_PAGE + (off))
                     */
                    pid = mk_pid(map, offset);

                } while (offset < BITS_PER_PAGE && pid < pid_max &&
                        (i != max_scan || pid < last ||
                            !((last+1) & BITS_PER_PAGE_MASK)));
            }

            /* 
             * 如果上述页框分配失败,则继续扫描剩余的页框
             */
            if (map < &pidmap_array[(pid_max-1)/BITS_PER_PAGE]) {
                ++map;
                offset = 0;
            } else {
                map = &pidmap_array[0];
                offset = RESERVED_PIDS;
                if (unlikely(last == offset))
                    break;
            }
            pid = mk_pid(map, offset);
        }
        return -1;
    }

find_next_offset()函数如下:

    #define find_next_offset(map, off)                  \
        find_next_zero_bit((map)->page, BITS_PER_PAGE, off)

    int find_next_zero_bit(const unsigned long *addr, int size, int offset)
    {
        /* 
         * 找出offset在页框中的地址,页框中用字来记录位,所以offset需要右移5位,即32
         */
        unsigned long * p = ((unsigned long *) addr) + (offset >> 5);
        int set = 0, bit = offset & 31, res;

        if (bit) {
            /*
             * 内嵌汇编语言,在p指向的字中找出第一个为0的位
             */
            __asm__(
                /* 
                 * %i为输出输入的变量,从输出开始算,这里的%0为输出set,%1为输入~(*p >> bit)
                 * bsfl将第一个操作数中第一个为1的位数存放到第二个操作数中
                 */
                "bsfl %1,%0\n\t"

                /*
                 * 找得到则返回,找不到则把令set = 32
                 */
                "jne 1f\n\t"
                "movl $32, %0\n"
                "1:"

                /* 输出部分 */

                : "=r" (set)                    

                /* 输入部分,本来是要查找第一个为0的位,取反后利用bsfl找到为1的位 */

                : "r" (~(*p >> bit)));

            /*
             * 如果在该字中找到空闲位,则返回,否则继续下一个字的扫描,由find_first_zero_bit进行、
             */
            if (set < (32 - bit))
                return set + offset;
            set = 32 - bit;
            p++;
        }
        /*
         * find_first_zero_bit继续扫描剩下的位,也是用汇编实现,见下面分析
         */
        res = find_first_zero_bit (p, size - 32 * (p - (unsigned long *) addr));
        return (offset + set + res);
    }

    /*
     * 该函数参考博文:http://blog.csdn.net/weifenghai/article/details/52794872
     */

    static inline int find_first_zero_bit(const unsigned long *addr, unsigned size)
    {
        int d0, d1, d2;
        int res;

        if (!size)
            return 0;

        __asm__ __volatile__(
            /* eax所有位置位 */
            "movl $-1,%%eax\n\t"

            /* edx置为0 */
            "xorl %%edx,%%edx\n\t"
            /* 在edi(addr)中搜索与eax不匹配的(即不全为1的) */
            "repe; scasl\n\t"
            /* 没有找到则跳到1处 */
            "je 1f\n\t"
            /* 将eax中edi为0的位置1,其余置0 */
            "xorl -4(%%edi),%%eax\n\t"
            /* edi存放当前找到的位置 */
            "subl $4,%%edi\n\t"

            /* bsfl指令见上文 */
            "bsfl %%eax,%%edx\n"
            /* edi存放字节偏移位置 */
            "1:\tsubl %%ebx,%%edi\n\t"
            /* edi左移3位,存放整体移位数 */
            "shll $3,%%edi\n\t"
            /* 计算偏移位数 */
            "addl %%edi,%%edx"

            /* 因为是输出,最后将edx值存赋给res,后面同一道理,c代表ecx,d为edi,a为eax */
            :"=d" (res), "=&c" (d0), "=&D" (d1), "=&a" (d2)

            /* 输入部,"1"表示和%1相同,将((size + 31) >> 5)存放到ecx中,后同,将addr存放到edi和ebx */
            :"1" ((size + 31) >> 5), "2" (addr), "b" (addr) : "memory");
        return res;
    }

你可能感兴趣的:(linux,kernel,位图,pid,Linux,kernel,pid,位图)