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;
}