I 位向量的實現與應用

這篇文章是《讀厚<編程珠璣>》系列博客的第一篇,我們在《編程珠璣》的第一章 - 開篇中就瞭解了位向量是什麼,《編程珠璣》的作者使用位向量來解決了一個海量數據排序問題,這篇文章我們來深入的瞭解一下位向量的實現與應用。

0x00 位向量是什麼?

位向量,也叫位圖,是一個我們經常可以用到的數據結構,在使用小空間來處理大量數據方面有着得天獨厚的優勢。位向量,顧名思義就是「位構成的向量」,我們通常使用0來表示 false,1來表示 true,例:[010111] 我們就可以說它是一個位向量,它表示 [false true false true true true]。在位向量這個數據結構中,我們常常把它的索引和它的值對應起來使用

0x01 位向量的實現

通常我們實現位向量的思路是:使用基本數據類型來表示多個位,使用多個基本數據類型來構成數組。例如:我們使用「int」類型來實現位向量,一個「int」類型有32位,我們使用 int 數組來存放整個位向量。

下面根據這個思路我們來寫一下代碼:

#ifndef bitvector_h
#define bitvector_h

#include <stdio.h>

#define N 1000000       //表示位向量元素個數
#define BITPERINT 32    //int 有32位
#define NUM (N-1)/BITPERINT+1
#define SHIFT 5     //2^5=32,表示移位
#define MASK 0x1F   //二進制11111
#define SET(i){vector[i >> SHIFT] |= (1 << (i & MASK));}    //第i位 置爲1
#define CLR(i){vector[i >> SHIFT] &= ~(1 << (i & MASK));}   //第i位 置爲0

#endif /* bitvector_h */

下面我們來分析一下這段代碼,關鍵的部分就是 SET 和 CLR 這兩個宏函數,我們來分解看一下代碼:

i >> SHIFT表示算數右移5位,即i / 32,該操作的作用是將數組的索引定位到需要操作的那個 int 的位置上,因爲我們的位向量結構是由許多個 int 組成的,例如:如果i = 50,i >> SHIFT等於1,首先將第50位定位到了第2個 int 中。

i & MASK表示取 i 的最後5位,例:50 & MASK 等於10010,然後把1左移這麼多位,即第18位爲1

SET操作是取或運算,即把定位到的 int 中的某位設置爲1,例:i = 50即把第二個 int 的第18位置1。CLR操作也是同樣的道理。

使用位向量:

int vector[NUM];
int i;
for(i = 0; i < N; i++)
{
    CLR(i);
}
SET(1);
SET(20);

0x02 位向量的應用

《編程珠璣》中的問題

問題重述:一個最多包含n個正整數的文件,每個數都小於n,其中n=107,並且沒有重複。最多有1MB內存可用。要求用最快方式將它們排序並按升序輸出。

解決方案就是:把文件一次讀入,出現的數字在位向量對應索引處中標註爲1,讀取完文件之後,將位向量從低位向高位依次將爲1的索引輸出即可。

Linux進程 pid 的分配算法

我們知道:在 Linux 中,進程當中的 pid 號的分配是在 0~32767 之間的,其中 0~299 的進程號是分配給守護進程的,剩下的 pid 號是分配給普通進程的。在進程號(pid)的分配中就使用到了位向量。

(以下Linux 內核代碼版本爲:3.8)

Linux 中用來存放 pid 的位向量結構體叫做 pidmap,具體代碼如下(/include/linux/pid_namespace.h):

struct pidmap {
    atomic_t nr_free;   //表示未分配的 pid 的個數
    void *page;         //用數組來表示位向量,每一位表示該同該索引號的 pid 是否被分配(1表示被分配,0表示未分配)
};

下面我們來分析一下有關 pidmap 的操作:

  • 置位爲1

(/include/asm-generic/bitops/atomic.h)

static inline int test_and_set_bit(int nr, volatile unsigned long *addr)
{
    unsigned long mask = BIT_MASK(nr);
    unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr);
    unsigned long old;
    unsigned long flags;

    _atomic_spin_lock_irqsave(p, flags);
    old = *p;
    *p = old | mask;
    _atomic_spin_unlock_irqrestore(p, flags);

    return (old & mask) != 0;
}

這個函數的作用是:當爲一個進程申請到 pid 之後,將對應的pidmap中的 nr 位置位爲1的函數,並返回置位之前該位的值。\*addr表示的是 pidmap 的地址

下面我們來分析一下具體代碼:

/include/linux/bitops.h 中我們可以找到 BIT_MASK 的宏定義:

#define BIT_MASK(nr)            (1UL << ((nr) % BITS_PER_LONG))

我們同樣可以在 include/asm-generic/bitsperlong.h 找到 BITS_PER_LONG 的定義:

#ifdef CONFIG_64BIT
#define BITS_PER_LONG 64
#else
#define BITS_PER_LONG 32
#endif /* CONFIG_64BIT */

我們可以知道 BITS_PER_LONG 是和機器相關的,爲32或64.

1UL << (nr) % BITS_PER_LONG)表示取 nr 的第0~BITS_PER_LONG位,然後把1左移這麼多位

我們同樣可以在include/asm-generic/bitsperlong.h 找到 BIT_WORD 的定義:

#define BIT_WORD(nr)            ((nr) / BITS_PER_LONG)

它用於定位想要操作的 nr 位在數組中的第幾個單位中,因此 unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr); 這條語句就是將 *p 指向想要操作的位

*p = old | mask; 將第 nr 位置 1

return (old & mask) != 0; 返回置位之前 nr 位的值

  • 置位爲0

(/include/asm-generic/bitops/atomic.h)

static inline int test_and_clear_bit(int nr, volatile unsigned long *addr)
{
    unsigned long mask = BIT_MASK(nr);
    unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr);
    unsigned long old;
    unsigned long flags;

    _atomic_spin_lock_irqsave(p, flags);
    old = *p;
    *p = old & ~mask;
    _atomic_spin_unlock_irqrestore(p, flags);

    return (old & mask) != 0;
}

與上面不同的操作就是 *p = old & ~mask; 將第 nr 位置 0

  • 尋找一下個爲0的位置
unsigned long find_next_zero_bit(const unsigned long *addr, unsigned long size, unsigned long offset)
{//addr 表示 pidmap 的地址,size = PAGE_SIZE*8,offset 一開始爲0
    unsigned long *p = addr + BITOP_WORD(offset);
    unsigned long result = offset & ~(BITS_PER_LONG-1);//如果BITS_PER_LONG爲32,則取 offset 後5位
    unsigned long tmp;

    if (offset >= size)
        return size;
    size -= result;
    offset %= BITS_PER_LONG;//offset位於32位的第幾位
    if (offset) {//如果 offset 不在數組單位中的第一位
        tmp = *(p++);
        tmp |= ~0UL >> (BITS_PER_LONG - offset);//將0~offset 位置1
        if (size < BITS_PER_LONG)//不足32位
                goto found_first;
        if (~tmp)//存在0
            goto found_middle;
        //說明 tmp 爲全1:
        size -= BITS_PER_LONG;//到下一個單位空間
        result += BITS_PER_LONG;//數據減少一個單位大小
    }
    while (size & ~(BITS_PER_LONG-1)) {
        if (~(tmp = *(p++)))
            goto found_middle;
        result += BITS_PER_LONG;//到下一個單位空間
        size -= BITS_PER_LONG;//數據減少一個單位大小
    }
    if (!size)//size = 0 說明已經全部查完,result = size 返回
        return result;
    tmp = *p;//size 不是32的整數倍,說明有額外幾個 bit,繼續查

found_first:
    tmp |= ~0UL << size;
    if (tmp == ~0UL)
        return result + size;
found_middle:
    return result + ffz(tmp);
}
  • 分配 pid
static int alloc_pidmap(struct pid_namespace *pid_ns)
{
    int i, offset, max_scan, pid, last = pid_ns->last_pid;
    struct pidmap *map;

    pid = last + 1;
    if (pid >= pid_max)
        pid = RESERVED_PIDS;//RESERVED_PIDS=300,前300爲守護進程
    offset = pid & BITS_PER_PAGE_MASK;//pid 在一個單位中的偏移量
    map = &pid_ns->pidmap[pid/BITS_PER_PAGE];

    max_scan = DIV_ROUND_UP(pid_max, BITS_PER_PAGE) - !offset;
    for (i = 0; i <= max_scan; ++i) {
        if (unlikely(!map->page)) {
        //分配空間
            void *page = kzalloc(PAGE_SIZE, GFP_KERNEL);

            spin_lock_irq(&pidmap_lock);
            if (!map->page) {
                map->page = page;
                page = NULL;
            }
            spin_unlock_irq(&pidmap_lock);
            kfree(page);
            if (unlikely(!map->page))
                break;
        }
        if (likely(atomic_read(&map->nr_free))) {
            do {
                if (!test_and_set_bit(offset, map->page)) {//offset 位置的 pid 是否被分配
                    atomic_dec(&map->nr_free);//原子操作,將空閒數量減一
                    set_last_pid(pid_ns, last, pid);
                    return pid;//分配該 pid
                }
                offset = find_next_offset(map, offset);
                pid = mk_pid(pid_ns, map, offset);
            } while (offset < BITS_PER_PAGE && pid < pid_max);
        }
        if (map < &pid_ns->pidmap[(pid_max-1)/BITS_PER_PAGE]) {
            ++map;
            offset = 0;
        } else {
            map = &pid_ns->pidmap[0];
            offset = RESERVED_PIDS;
            if (unlikely(last == offset))
            break;
        }
        pid = mk_pid(pid_ns, map, offset);
    }
    return -1;
}

本文的版權歸作者 羅遠航 所有,採用 Attribution-NonCommercial 3.0 License。任何人可以進行轉載、分享,但不可在未經允許的情況下用於商業用途;轉載請註明出處。感謝配合!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章