CHAPTER3:
一、简介:
人啊,不该偷懒的时候还真不能偷懒。最先开始移植协议栈的时候,我为了方便,把comer中所有向缓冲池申请内存的地方改成了用malloc分配,认为这样简单。但越到后来越觉得这种不规范的操作带来了很多不便——内存的分配是散乱的,没有一个统一的管理机制。另外就是信号灯控制。Comer中很多地方用了signal、wait函数做信号灯控制,我总是在需要的时候创建一个linux信号灯,结果程序里到处都是semget函数,很不方便。为了让程序看上去更规范一些,我重写了xinu下的缓冲池管理函数和信号灯控制函数,把它们都封装到一个c文件里,这样,所有的操作都用同一组函数实现,简单明了,而且对comer协议栈的改动更小了,让同样再跟comer学TCP/IP的朋友能看的更明白一些。以后,凡是用到了xinu系统函数的地方,我都会尽量把它重新在linux下实现一遍(当然,写不出来就只有找办法代替了),并单独写成一篇文章。这样,对comer中调用系统函数不感兴趣的朋友可以跳过这些章节,专心协议栈的实现。
二、源码:
1、 缓冲池管理:
首先,要搞清楚缓冲池和缓冲区的区别。缓冲池是一个管理机制(或数据结构),用于管理缓冲区。缓冲区是一个被缓冲池管理的内存区域链表。comer中缓冲池的数据结构、常量、以及函数声明都放在了bufpool.h文件中。其中struct bpool是缓冲池的数据结构,最为重要。结构如下:
struct bpool { /* Description of a single pool */
int bpsize; /* size of buffers in this pool */
int bpmaxused; /* max ever in use */
int bptotal; /* # buffers this pool */
char *bpnext; /* pointer to next free buffer */
int bpsem; /* semaphore that counts buffers*/
};
字段的注释都很清楚,要注意的是bpmaxused字段是表示曾经有多少个缓冲区被使用,而不是正在被使用的缓冲区个数。Bpnext是指向空闲缓冲区的指针。一个缓冲池被划分成了bptotal个缓冲区。每个缓冲区的头4个字节存放的是下一个缓冲区首地址的指针值。整个缓冲池操作其实就是个链表操作,为了直观的表明缓冲池的结构以及缓冲池是如何管理缓冲区的,下面用图进行描述:
从图中可以看出,缓冲池构成了一个单向链表,其中指向链表下一表项(下一个缓冲区)的指针值存在了缓冲区的头4个字节中。Bpool结构对缓冲池的管理可以用下图表示:
从上图可以看到,bufpool结构对缓冲池的管理实际上是通过移动bpnext指针来进行的。举例来说,当缓冲区1被程序用getbuf函数申请时,函数将缓冲区1头4个字节中存放的值取出来,赋给bpnext指针,然后把缓冲区1的首地址返回给调用者,供其使用。这时,bpnext便指向了缓冲区2的首地址。缓冲区2被申请时,bpnext又经过同样的操作指向了缓冲区3的首地址,以此类推,直到bpnext指向0,也就是空指针,则缓冲池被申请完了,后来的申请操作将被信号灯阻塞。
Getbuf在将缓冲区头4个字节中的地址值赋给bpnext指针的同时,也将当前缓冲池的标号(这个标号是当前缓冲池在缓冲池数组里的序号,bptable是缓冲池数组,共有5个元素)存入了这4个字节。这是为了freebuf函数在释放缓冲区时,能根据该标号将缓冲区返还给相应的缓冲池。释放的过程比较简单,仅仅是根据被释放缓冲区头4个字节中存放的缓冲池标号,找到对应的缓冲池(bufpool),接着将bpnext指针的值存入这4个字节,最后将bpnext指针指向被释放缓冲区的首地址。从整个过程可以看出,各缓冲区在缓冲池的中的位置不是固定的,但这无关紧要,毕竟我们并不要求对缓冲池进行检索操作。
好了,我们已经知道怎么向缓冲池申请缓冲区以及如何释放缓冲区,现在来看看缓冲池是如何被创建的。首先,mkpool函数根据传入的参数确定要分配多少个缓冲区(numbufs参数)以及每个缓冲区的大小(bufsiz参数)。然后根据一系列判断条件,确定要申请的缓冲池是否合理,然后使用了malloc函数分配出整块内存区域(malloc((bufsiz + sizeof(int)) * numbufs)),接着将该内存区域划分成numbufs块,在每块的头4个字节中存入下一块的首地址(最后一块存的是0),最后将bufpool的bpnext指针指向第一块缓冲区的首地址,缓冲池就分配完成了。在使用getbuf函数时,bpnext指针会不断移动,我们就能使用缓冲池中的所有缓冲区。
缓冲池的管理大概就是这样,也许前面的叙述比较拗口,你已经看晕了。不要怕,结合后面的源码和注释,再回过头来理解一下,很快就能弄懂。
/* zbufpool.c - 缓冲池管理函数(2006.4.19)*/
//#include <conf.h>
#include <kernel.h>
#include <bufpool.h>
#include <pthread.h>
#include <zsem.h>
pthread_mutex_t bufpool_mutex;
Bool is_bufpool_inited = FALSE;
struct bpool bptab[NBPOOLS];
int nbpools;
//初始化缓冲池
poolinit()
{
nbpools = 0;
is_bufpool_inited = TRUE;
return OK;
}
//申请一个缓冲池
//numbufs 参数决定要分配的缓冲区块数
//bufsiz表示每块缓冲区的大小
int mkpool(int bufsiz,int numbufs)
{
int poolid;
char *where;
if (!is_bufpool_inited)
poolinit();
pthread_mutex_lock(&bufpool_mutex);
if (bufsiz < BPMINB || bufsiz > BPMAXB || numbufs < 1
|| numbufs > BPMAXN || nbpools >= NBPOOLS
//这里的malloc操作为每个缓冲区多分配了sizeof(int)字节的空间,目的就是用来
//存入下一个缓冲区的首地址值
|| (where = (char *) malloc((bufsiz + sizeof(int)) * numbufs)) == NULL)
{
pthread_mutex_unlock(&bufpool_mutex);
return SYSERR;
}
poolid = nbpools++;
bptab[poolid].bptotal = numbufs;
bptab[poolid].bpmaxused = 0;
bptab[poolid].bpnext = where;
bptab[poolid].bpsize = bufsiz;
bptab[poolid].bpsem = create_sem(numbufs);
bufsiz += sizeof(int);
//这里每个被分配的内存区域头4 个字节是用于存放
//下一块空闲缓冲区的头指针的。
//通过一个循环将缓冲池的所有缓冲区串成一个链表
for (numbufs--;numbufs > 0;numbufs--,where += bufsiz)
*((int *)where) = (int)(where + bufsiz);
pthread_mutex_unlock(&bufpool_mutex);
return poolid;
}
//以阻塞方式在缓冲池中申请一块缓冲区
//参数是缓冲池的标号,表示在poolid号缓冲池中申请一块缓冲区
void *getbuf(unsigned poolid)
{
int *buf,inuse;
if (!is_bufpool_inited || poolid >=nbpools)
return ((void *)SYSERR);
wait_sem(bptab[poolid].bpsem);
//曾经被使用过的最大缓冲区数量
inuse = bptab[poolid].bptotal - scount_sem(bptab[poolid].bpsem);
if (inuse > bptab[poolid].bpmaxused)
bptab[poolid].bpmaxused = inuse;
pthread_mutex_lock(&bufpool_mutex);
buf = (int *)bptab[poolid].bpnext;
//这里把缓冲池表项指向下一块空闲缓冲区的指针指向了
//当前缓冲区头4 个字节里存放的指针值
//注意,每块缓冲区的头4 个字节存放的是指向下一块
//缓冲区的指针值
bptab[poolid].bpnext = (char *)(*buf);
pthread_mutex_unlock(&bufpool_mutex);
//把当前缓冲区头4 个字节里存放的指向下一块缓冲区的
//指针值替换成了当前缓冲区的标号(为了freebuf 释放的时候找到对应的缓冲池),然后将指针向后移动
// 4 个字节(buf 是int 型,自加1 相当于移动4 个字节)
//于是,buf 指针便指向了数据区
*(buf++)=poolid;
return ((void *)buf);
}
//释放缓冲区,参数是缓冲区的首地址
//注意,释放操作并没有删除缓冲区中的值
int freebuf(void *buf0)
{
int *buf = (int *)buf0;
int poolid;
if (!is_bufpool_inited)
return SYSERR;
//取出缓冲区头部存放的缓冲区标号
poolid = *(--buf);
if (poolid < 0|| poolid >=nbpools)
return SYSERR;
pthread_mutex_lock(&bufpool_mutex);
//重新把当前缓冲区的头4 个字节存为下一块
//空闲缓冲区的头指针
*buf = (int)bptab[poolid].bpnext;
//把缓冲区重新接入缓冲池表项中
bptab[poolid].bpnext = (char *)buf;
pthread_mutex_lock(&bufpool_mutex);
signal_sem(bptab[poolid].bpsem);
return OK;
}
整个缓冲池结构的亮点在于bpsem字段(信号灯)。并不是所有的缓冲池结构都设置了信号灯(例如ucOS操作系统中的内存管理)。设置信号灯的好处在于当缓冲区被申请完后,新的申请操作会自动阻塞,直到有缓冲区被释放(这时,系统会自动为阻塞的申请操作返回缓冲区)。如果没有使用信号灯,那么在程序员不得不关注getbuf函数的返回值以及erron变量,以确定申请缓冲区失败的原因,并在一个延时后再次申请。这是非常繁琐的。当然,或许你会认为阻塞操作降低了程序的时实性,但可以想像的是,一旦getbuf操作发生阻塞,说明我们的内存已经耗尽了,在内存被释放之前,不应该进行其它操作。毕竟,没有人希望自己的程序不停的吞噬内存,直到整个系统挂起。
阅读bufpool.h文件,可以了解关于缓冲池的一些常量,例如总共的缓冲池个数(5),缓冲池中每块缓冲区的最大长度(2048)、最小长度(2)等等。这些对应了comer中的缓冲池分配方案——大缓冲区方案和小缓区池方案。说到这里,想必大家都想起comer中在申请缓冲池经常使用的操作getbuf(Net.netpool)和getbuf(Net.lrgpool)。前者是申请小缓冲区,后者是申请大缓冲区。Net变量在net.h文件中声明,其结构是struct netinfo,用于管理系统中所有的网络接口。它的初始化在《网络接口》一文中已经给出,这里详细讲一下:
Net.netpool = mkpool(MAXNETBUF, NETBUFS);
Net.lrgpool = mkpool(MAXLRGBUF, LRGBUFS);
Net.sema = create_sem(1);
Net.nif = 2;
从上面的代码可以看到,netpool字段为一个拥有64个缓冲区,每块缓冲区大小为1524(以太网数据长度 + 扩展以太网帧头长度)字节的缓冲池的标号,这是小缓冲区方案。Lrgpool字段为拥有16个缓冲区,每个缓冲区大小为2048字节的缓冲区标号,这是大缓冲区方案。当要发送的数据包长度大于1524时,就应该申请大缓冲区。当然,这里所谓的大也是个相对概念,如果要将缓冲区分配的足够容纳任何长度的数据包,那么根据协议规定一次得分配64K内存!很明显,这是不合理的。Sema字段是个信号灯,用于一些阻塞操作。Nif字段代表系统中网络接口的个数,这里我们把它设置成了2(一个本地伪接口,一个网卡接口)。好了,你已经明白comer中的缓冲池管理,在以后的代码中再也不会为getbuf、freebuf这些系统调用困惑了。下面我们来看看信号灯控制。
(PS:关于缓冲池管理有一个让我担忧的地方,就是里面的disable(关中断)操作。我在这里再次使用了互斥锁。很明显,这里的disable并不是为了控制对临界资源访问,而是保证我们下面代码的原子性(我对原子性的理解是:操作时cpu的时间片不会被让出去,也就是操作在完成前不会被打断)。但互斥锁却不能完成这个功能,它也会被中断。到现在我仍不明白如何在linux下保持操作的原子性,这也许会成为协议栈中的bug)
2、信号灯控制
相对于缓冲池管理,信号灯控制就比较简单了。只是把linux的信号灯函数封装了一遍。在这里,我并不想讲解linux下的信号灯函数是如何使用的,毕竟这方面的文章太多了,有兴趣的朋友可以查阅相关资料。这里我把源码给出,源码的注释已经非常清楚了,大家稍微花点时间就能看懂。然后我会给出它们与comer中相对应的函数名,以后大家在comer中看到相关的函数就可以用这里封装的信号灯函数代替。
/* sem.c -信号灯控制函数(2006.4.18)*/
#include <linux/sem.h>
/* linux 信号灯设置部分*/
struct sembuf semwait,semsignal;
#define PERMS IPC_CREAT|IPC_NOWAIT
//信号灯操作方式初始化函数
//信号灯操作方式也就是semop 函数的第二个参数
void init_sem_struct(struct sembuf *sem,int semnum,int semop,int semflg)
{
sem->sem_num = semnum;
sem->sem_op = semop;
sem->sem_flg = semflg;
}
//阻塞申请一个资源
void wait_sem(int mutex)
{
semop(mutex,&semwait,1);
}
//释放一个资源
void signal_sem(int mutex)
{
semop(mutex,&semsignal,1);
}
int create_sem(int num)
{
int semid;
union semun arg;
/* semsignal是释放资源的操作(+1) ,SEM_UNDO在我们程序退出后由内核释放信号灯*/
init_sem_struct(&semwait,0,-1,SEM_UNDO);
/* semwait是要求资源的操作(-1) */
init_sem_struct(&semsignal,0,1,SEM_UNDO);
semid = semget(IPC_PRIVATE,1,PERMS);
//创建后先分配num 个可用资源
arg.val = num;
semctl(semid,0,SETVAL,arg);
return semid;
}
//删除信号灯
int del_sem(int semid)
{
return semctl(semid,0,IPC_RMID);
}
//返回信号灯当前值
int scount_sem(int mutex)
{
union semun arg;
return semctl(mutex,0,GETVAL,arg);
}
create_sem(int num)对应comer中的screate函数,用于创建一个信号灯。Num参数指定信号灯的初始值(表示有多少个可用资源)。
Wait_Sem(int mutex)对应wait函数,阻塞申请一个资源。Mutex参数表示信号灯的id。
Signal_sem(int mutex)对应signal函数,释放一个资源。
Scount_sem(int mutex)对应scount函数,返回mutex指定的信号灯的值。
Del_sem(int mutex)对应sdelete函数,删除mutex指定的信号灯。
以上就是信号灯控制的全部,代码非常简单,就不多做解释了。
——未完待续